Add local-port and public-tunnel reachability checks

This commit is contained in:
2026-10-30 22:42:40 +08:00
parent baff9449bd
commit 5f98c2834f
6 changed files with 411 additions and 8 deletions
+183 -4
View File
@@ -64,8 +64,20 @@ type App struct {
configLabel *widget.Label
startButton *widget.Button
routeBox *fyne.Container
routeRows map[string]*routeRow
checkSummary *widget.Label
logText *widget.TextGrid
banner fyne.CanvasObject
checkMu sync.Mutex
checks map[string]client.RouteCheck
checking bool
lastCheck time.Time
}
type routeRow struct {
local *widget.Label
public *widget.Label
}
// Run creates and runs the desktop app.
@@ -167,9 +179,16 @@ func (a *App) buildUI() {
routeHeader := container.NewHBox(
widget.NewLabelWithStyle("Routes", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
layout.NewSpacer(),
widget.NewButtonWithIcon("Check now", theme.ViewRefreshIcon(), a.checkRoutes),
widget.NewButtonWithIcon("Add route", theme.ContentAddIcon(), a.addRouteDialog),
)
a.routeBox = container.NewVBox()
a.routeRows = map[string]*routeRow{}
a.checks = map[string]client.RouteCheck{}
a.checkSummary = widget.NewLabel("Not checked yet")
a.checkSummary.Importance = widget.LowImportance
checkDetails := widget.NewButton("Details", a.showCheckDetails)
checkBar := container.NewHBox(a.checkSummary, checkDetails)
a.logText = widget.NewTextGrid()
logScroll := container.NewScroll(a.logText)
logScroll.SetMinSize(fyne.NewSize(0, 220))
@@ -190,6 +209,7 @@ func (a *App) buildUI() {
a.banner,
routeHeader,
a.routeBox,
checkBar,
widget.NewLabelWithStyle("Log", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
logScroll,
)
@@ -230,10 +250,20 @@ func (a *App) refresh() {
}
a.refreshRoutes()
a.updateStatus()
if a.svc != nil && a.svc.CurrentStatus().Connected {
a.maybeCheck()
}
}
func (a *App) refreshRoutes() {
a.routeBox.Objects = nil
a.routeRows = map[string]*routeRow{}
a.checkMu.Lock()
checks := make(map[string]client.RouteCheck, len(a.checks))
for name, check := range a.checks {
checks[name] = check
}
a.checkMu.Unlock()
for _, name := range a.routeNames() {
route := a.cfg.Routes[name]
linkURL := a.cfg.RouteURL(name)
@@ -241,25 +271,169 @@ func (a *App) refreshRoutes() {
if parsed, err := url.Parse(linkURL); err == nil {
link.SetURL(parsed)
}
local := widget.NewLabel("local —")
public := widget.NewLabel("public —")
local.Importance = widget.LowImportance
public.Importance = widget.LowImportance
row := &routeRow{local: local, public: public}
if check, ok := checks[name]; ok {
setCheckLabel(local, "local", check.LocalUp, check.LocalError)
setCheckLabel(public, "public", check.PublicUp, check.PublicError)
}
a.routeRows[name] = row
copyButton := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() {
a.app.Clipboard().SetContent(linkURL)
})
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
a.removeRoute(name)
})
row := container.NewHBox(
container.NewGridWrap(fyne.NewSize(130, 32), widget.NewLabel(name)),
container.NewGridWrap(fyne.NewSize(170, 32), widget.NewLabel(route.Target)),
line := container.NewHBox(
container.NewGridWrap(fyne.NewSize(120, 32), widget.NewLabel(name)),
container.NewGridWrap(fyne.NewSize(150, 32), widget.NewLabel(route.Target)),
link,
layout.NewSpacer(),
local,
public,
copyButton,
deleteButton,
)
a.routeBox.Add(row)
a.routeBox.Add(line)
}
a.routeBox.Refresh()
}
func setCheckLabel(label *widget.Label, kind string, up bool, detail string) {
if up {
label.SetText(kind + " ✓")
label.Importance = widget.SuccessImportance
} else {
label.SetText(kind + " ✗")
label.Importance = widget.DangerImportance
}
label.Refresh()
_ = detail
}
func (a *App) checkRoutes() {
if a.cfg == nil {
return
}
routes := make(map[string]client.Route, len(a.cfg.Routes))
for name, route := range a.cfg.Routes {
routes[name] = route
}
if len(routes) == 0 {
a.checkSummary.SetText("No routes")
a.checkSummary.Importance = widget.LowImportance
a.checkSummary.Refresh()
return
}
snapshot := &client.Config{API: a.cfg.API, Hostname: a.cfg.Hostname, Routes: routes}
a.checkMu.Lock()
if a.checking {
a.checkMu.Unlock()
return
}
a.checking = true
a.checkMu.Unlock()
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
results := client.CheckAll(ctx, snapshot)
checks := make(map[string]client.RouteCheck, len(results))
for _, result := range results {
checks[result.Route] = result
}
a.checkMu.Lock()
a.checks = checks
a.lastCheck = time.Now()
a.checking = false
a.checkMu.Unlock()
fyne.Do(a.applyChecks)
}()
}
func (a *App) maybeCheck() {
a.checkMu.Lock()
due := !a.checking && time.Since(a.lastCheck) > 30*time.Second
a.checkMu.Unlock()
if due {
a.checkRoutes()
}
}
func (a *App) applyChecks() {
a.checkMu.Lock()
checks := make(map[string]client.RouteCheck, len(a.checks))
for name, check := range a.checks {
checks[name] = check
}
a.checkMu.Unlock()
results := make([]client.RouteCheck, 0, len(a.routeRows))
for name, row := range a.routeRows {
check, ok := checks[name]
if !ok {
continue
}
setCheckLabel(row.local, "local", check.LocalUp, check.LocalError)
setCheckLabel(row.public, "public", check.PublicUp, check.PublicError)
results = append(results, check)
}
if len(results) == 0 {
a.checkSummary.SetText("Not checked yet")
a.checkSummary.Importance = widget.LowImportance
} else {
a.checkSummary.SetText(client.CheckSummary(results))
a.checkSummary.Importance = widget.SuccessImportance
for _, result := range results {
if !result.LocalUp || !result.PublicUp {
a.checkSummary.Importance = widget.DangerImportance
break
}
}
}
a.checkSummary.Refresh()
}
func (a *App) showCheckDetails() {
a.checkMu.Lock()
checks := make(map[string]client.RouteCheck, len(a.checks))
for name, check := range a.checks {
checks[name] = check
}
a.checkMu.Unlock()
lines := []string{}
for _, name := range a.routeNames() {
check, ok := checks[name]
if !ok {
continue
}
if check.LocalError != "" {
lines = append(lines, fmt.Sprintf("%s local: %s", name, check.LocalError))
}
if check.PublicError != "" {
lines = append(lines, fmt.Sprintf("%s public: %s", name, check.PublicError))
}
}
if len(lines) == 0 {
lines = []string{"No errors recorded."}
}
a.showInfo(strings.Join(lines, "\n"))
}
func (a *App) resetChecks() {
a.checkMu.Lock()
a.checks = map[string]client.RouteCheck{}
a.checking = false
a.lastCheck = time.Time{}
a.checkMu.Unlock()
a.checkSummary.SetText("Not checked yet")
a.checkSummary.Importance = widget.LowImportance
a.checkSummary.Refresh()
}
func (a *App) refreshLog() {
a.logText.SetText(a.logWriter.text(300))
}
@@ -307,6 +481,9 @@ func (a *App) updateStatus() {
a.dot.Refresh()
a.startButton.Enable()
a.refreshRoutes()
if status.Connected {
a.maybeCheck()
}
}
func (a *App) isRunning() bool {
@@ -471,6 +648,7 @@ func (a *App) addRouteDialog() {
a.showError(err)
return
}
a.resetChecks()
a.refresh()
if !a.isRunning() && a.cfg.API != "" {
a.start()
@@ -487,6 +665,7 @@ func (a *App) removeRoute(name string) {
a.showError(err)
return
}
a.resetChecks()
a.refresh()
}