internal/gui: wire real checker.Run + CancelCheck binding + RawHex display
- RunCheck now drives internal/checker.Run instead of the fake 7-step sleep loop. Streams checker.Result events as "check:result" with Duration converted to milliseconds; emits "check:done" summary on channel close. Re-running while a check is in flight cancels the previous run and waits for its goroutine to drain so two emitter goroutines never race on event order. - New CancelCheck method (no-op when nothing is running) is bound through wailsjs/go/gui/App.js and surfaced in useDrover as cancelCheck() with a "check cancelled by user" log line. - Classic.jsx: while phase==='checking', the Check button collapses to a disabled "Checking…" pill side-by-side with a Cancel button (uses Stop's secondary visual weight, t.danger on hover). The expanded failure row now renders r.rawHex (truncated to 64 chars) on its own mono line and the copy button includes raw=<hex> when present. - CheckResult struct gains RawHex (json:"rawHex") and Attempt fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+99
-38
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.okcu.io/root/drover-go/internal/checker"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,15 @@ type App struct {
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
startedAt time.Time
|
||||
|
||||
// muCheck guards cancelCheck and checkDone.
|
||||
// cancelCheck is the cancel func of the in-flight checker.Run context (nil
|
||||
// when no check is running). checkDone is closed by the runner goroutine
|
||||
// once it has drained the result channel — RunCheck waits on it before
|
||||
// starting a new run, so we never have two emitter goroutines alive.
|
||||
muCheck sync.Mutex
|
||||
cancelCheck context.CancelFunc
|
||||
checkDone chan struct{}
|
||||
}
|
||||
|
||||
// NewApp returns a fresh App stamped with the binary's build version
|
||||
@@ -55,60 +65,111 @@ type Config struct {
|
||||
}
|
||||
|
||||
// CheckResult is one row in the diagnostic table; the frontend listens
|
||||
// for them on the "check:result" event.
|
||||
// for them on the "check:result" event. Mirrors checker.Result but with
|
||||
// Duration converted to milliseconds (int) for the JS side.
|
||||
type CheckResult struct {
|
||||
ID string `json:"id"` // tcp / greet / auth / connect / udp / stun / api
|
||||
Status string `json:"status"` // running | passed | failed | skipped
|
||||
Metric string `json:"metric"`
|
||||
ID string `json:"id"` // tcp / greet / auth / connect / udp / stun / api
|
||||
Status string `json:"status"` // running | passed | failed | skipped
|
||||
Metric string `json:"metric,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Hint string `json:"hint,omitempty"`
|
||||
RawHex string `json:"rawHex,omitempty"`
|
||||
Duration int64 `json:"duration_ms,omitempty"`
|
||||
Attempt int `json:"attempt,omitempty"`
|
||||
}
|
||||
|
||||
// RunCheck plays out a fake 7-step diagnostic. Each result is pushed to
|
||||
// the JS side as a "check:result" event; when the run finishes we emit
|
||||
// "check:done" with the overall summary.
|
||||
// RunCheck runs a real 7-step SOCKS5 diagnostic via internal/checker. Each
|
||||
// Result from the checker channel is forwarded to the frontend as a
|
||||
// "check:result" event; when the channel closes (run finished, or context
|
||||
// cancelled) we emit "check:done" with the {total, passed, failed} summary.
|
||||
//
|
||||
// Phase 1+: replace this with internal/checker.Run() and stream its
|
||||
// real results.
|
||||
// If a previous check is still in flight, its context is cancelled and we
|
||||
// wait for the previous goroutine to finish before launching the new one
|
||||
// — this guarantees event ordering (no two emitters alive simultaneously).
|
||||
func (a *App) RunCheck(cfg Config) {
|
||||
// Cancel any in-flight check and wait for its goroutine to drain.
|
||||
a.muCheck.Lock()
|
||||
prevCancel := a.cancelCheck
|
||||
prevDone := a.checkDone
|
||||
a.muCheck.Unlock()
|
||||
if prevCancel != nil {
|
||||
prevCancel()
|
||||
}
|
||||
if prevDone != nil {
|
||||
<-prevDone
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(a.ctx)
|
||||
done := make(chan struct{})
|
||||
|
||||
a.muCheck.Lock()
|
||||
a.cancelCheck = cancel
|
||||
a.checkDone = done
|
||||
a.muCheck.Unlock()
|
||||
|
||||
ckCfg := checker.Config{
|
||||
ProxyHost: cfg.Host,
|
||||
ProxyPort: cfg.Port,
|
||||
UseAuth: cfg.Auth,
|
||||
ProxyLogin: cfg.Login,
|
||||
ProxyPassword: cfg.Password,
|
||||
// Leave PerTestTimeout / MaxRetries / RetryBackoff /
|
||||
// DiscordGateway / DiscordAPI / StunServer at zero so the
|
||||
// checker package applies its own defaults.
|
||||
}
|
||||
|
||||
go func() {
|
||||
ids := []string{"tcp", "greet"}
|
||||
if cfg.Auth {
|
||||
ids = append(ids, "auth")
|
||||
}
|
||||
ids = append(ids, "connect", "udp", "stun", "api")
|
||||
|
||||
fakeMetric := map[string]string{
|
||||
"tcp": "14 ms", "greet": "SOCKS5/0x05", "auth": "user/pass · ok",
|
||||
"connect": "gateway.discord.gg", "udp": "relay 1.2.3.4:54321",
|
||||
"stun": "38 ms RTT", "api": "204 OK · 89 ms",
|
||||
}
|
||||
|
||||
passedCount := 0
|
||||
for _, id := range ids {
|
||||
runtime.EventsEmit(a.ctx, "check:result", CheckResult{ID: id, Status: "running"})
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
|
||||
// Stub: everything passes. Real checker will fail UDP if proxy
|
||||
// rejects ASSOCIATE, etc.
|
||||
defer close(done)
|
||||
var passed, failed int
|
||||
for r := range checker.Run(ctx, ckCfg) {
|
||||
// Always emit on a.ctx, never on the per-check ctx — the
|
||||
// per-check ctx may already be cancelled when the final
|
||||
// "cancelled" result arrives, which would silently drop it.
|
||||
runtime.EventsEmit(a.ctx, "check:result", CheckResult{
|
||||
ID: id,
|
||||
Status: "passed",
|
||||
Metric: fakeMetric[id],
|
||||
Duration: 350,
|
||||
ID: r.ID,
|
||||
Status: string(r.Status),
|
||||
Metric: r.Metric,
|
||||
Error: r.Error,
|
||||
Hint: r.Hint,
|
||||
RawHex: r.RawHex,
|
||||
Duration: r.Duration.Milliseconds(),
|
||||
Attempt: r.Attempt,
|
||||
})
|
||||
passedCount++
|
||||
switch r.Status {
|
||||
case checker.StatusPassed:
|
||||
passed++
|
||||
case checker.StatusFailed:
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
runtime.EventsEmit(a.ctx, "check:done", map[string]int{
|
||||
"total": len(ids),
|
||||
"passed": passedCount,
|
||||
"failed": len(ids) - passedCount,
|
||||
"total": passed + failed,
|
||||
"passed": passed,
|
||||
"failed": failed,
|
||||
})
|
||||
|
||||
// Clear cancel/done if we're still the current run (RunCheck may
|
||||
// have already replaced them with a newer run by the time we get
|
||||
// here, in which case leave those alone).
|
||||
a.muCheck.Lock()
|
||||
if a.checkDone == done {
|
||||
a.cancelCheck = nil
|
||||
a.checkDone = nil
|
||||
}
|
||||
a.muCheck.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// CancelCheck cancels the currently-running diagnostic, if any. Safe to
|
||||
// call when no check is running (no-op).
|
||||
func (a *App) CancelCheck() {
|
||||
a.muCheck.Lock()
|
||||
defer a.muCheck.Unlock()
|
||||
if a.cancelCheck != nil {
|
||||
a.cancelCheck()
|
||||
}
|
||||
}
|
||||
|
||||
// StartEngine flips the proxy on. In the stub we just toggle the flag and
|
||||
// note the start time so GetStats can produce a believable uptime.
|
||||
func (a *App) StartEngine() error {
|
||||
@@ -156,7 +217,7 @@ func (a *App) statsLoop() {
|
||||
a.mu.Unlock()
|
||||
|
||||
runtime.EventsEmit(a.ctx, "stats:update", map[string]any{
|
||||
"up": r.Intn(50_000) + 5_000, // bytes/sec out
|
||||
"up": r.Intn(50_000) + 5_000, // bytes/sec out
|
||||
"down": r.Intn(500_000) + 50_000, // bytes/sec in
|
||||
"tcp": r.Intn(8) + 1,
|
||||
"udp": r.Intn(5) + 1,
|
||||
|
||||
Reference in New Issue
Block a user