Skip to content

Commit 5835665

Browse files
committed
Add TUI preflight e2e check with context cancellation and parallel resolvers
1 parent 6e3408f commit 5835665

2 files changed

Lines changed: 73 additions & 7 deletions

File tree

internal/scanner/e2e.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,11 @@ type PreflightE2EResult struct {
398398
// returns an error. This handles blocked resolvers (e.g. Google in Iran) by
399399
// racing them — whichever resolver is reachable responds first.
400400
func PreflightE2E(bin, domain, pubkey, testURL, proxyAuth string, timeout time.Duration) PreflightE2EResult {
401+
return PreflightE2EContext(context.Background(), bin, domain, pubkey, testURL, proxyAuth, timeout)
402+
}
403+
404+
// PreflightE2EContext is like PreflightE2E but accepts a parent context for cancellation.
405+
func PreflightE2EContext(parent context.Context, bin, domain, pubkey, testURL, proxyAuth string, timeout time.Duration) PreflightE2EResult {
401406
if testURL == "" {
402407
testURL = defaultTestURL
403408
}
@@ -406,12 +411,12 @@ func PreflightE2E(bin, domain, pubkey, testURL, proxyAuth string, timeout time.D
406411
basePort := 29900
407412
results := make(chan PreflightE2EResult, len(preflightResolvers))
408413

409-
ctx, cancel := context.WithTimeout(context.Background(), timeout)
414+
ctx, cancel := context.WithTimeout(parent, timeout)
410415
defer cancel()
411416

412417
for i, resolver := range preflightResolvers {
413418
go func(res string, port int) {
414-
r := preflightSingle(bin, res, domain, pubkey, testURL, proxyAuth, port, timeout)
419+
r := preflightSingle(ctx, bin, res, domain, pubkey, testURL, proxyAuth, port, timeout)
415420
results <- r
416421
}(resolver, basePort+i)
417422
}
@@ -441,8 +446,8 @@ func PreflightE2E(bin, domain, pubkey, testURL, proxyAuth string, timeout time.D
441446
}
442447
}
443448

444-
func preflightSingle(bin, resolver, domain, pubkey, testURL, proxyAuth string, port int, timeout time.Duration) PreflightE2EResult {
445-
ctx, cancel := context.WithTimeout(context.Background(), timeout)
449+
func preflightSingle(parent context.Context, bin, resolver, domain, pubkey, testURL, proxyAuth string, port int, timeout time.Duration) PreflightE2EResult {
450+
ctx, cancel := context.WithTimeout(parent, timeout)
446451
defer cancel()
447452

448453
var stderrBuf bytes.Buffer

internal/tui/screen_config.go

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tui
22

33
import (
4+
"context"
45
"fmt"
56
"strconv"
67
"strings"
@@ -364,7 +365,11 @@ func viewConfig(m Model) string {
364365

365366
// Show binary status after E2E toggle when enabled
366367
if fd.id == fE2E && m.config.E2E {
367-
b.WriteString(binaryStatus(strings.TrimSpace(m.configInputs[txtDomain].Value())))
368+
domain := strings.TrimSpace(m.configInputs[txtDomain].Value())
369+
pubkey := strings.TrimSpace(m.configInputs[txtPubkey].Value())
370+
testURL := strings.TrimSpace(m.configInputs[txtTestURL].Value())
371+
proxyAuth := strings.TrimSpace(m.configInputs[txtProxyAuth].Value())
372+
b.WriteString(binaryStatus(domain, pubkey, testURL, proxyAuth))
368373
}
369374
}
370375

@@ -407,7 +412,17 @@ var nsCache struct {
407412
loading bool
408413
}
409414

410-
func binaryStatus(domain string) string {
415+
// e2eCache stores the cached preflight e2e check result.
416+
var e2eCache struct {
417+
mu sync.Mutex
418+
key string // "domain|pubkey" — invalidate if either changes
419+
result scanner.PreflightE2EResult
420+
done bool
421+
loading bool
422+
cancel context.CancelFunc // cancels in-flight preflight when key changes
423+
}
424+
425+
func binaryStatus(domain, pubkey, testURL, proxyAuth string) string {
411426
var b strings.Builder
412427
bins := []struct {
413428
name string
@@ -417,12 +432,16 @@ func binaryStatus(domain string) string {
417432
{"slipstream-client", "slipstream-client"},
418433
{"curl", "curl"},
419434
}
435+
var dnsttBin string
420436
for _, bin := range bins {
421437
path, err := binutil.Find(bin.bin)
422438
if err != nil {
423439
b.WriteString(fmt.Sprintf(" %s %s\n", redStyle.Render("✘"), dimStyle.Render(bin.name+" not found")))
424440
} else {
425441
b.WriteString(fmt.Sprintf(" %s %s\n", greenStyle.Render("✔"), dimStyle.Render(bin.name+" → "+path)))
442+
if bin.bin == "dnstt-client" {
443+
dnsttBin = path
444+
}
426445
}
427446
}
428447
// Verify NS delegation if domain is set (non-blocking)
@@ -447,7 +466,6 @@ func binaryStatus(domain string) string {
447466
go func(d string) {
448467
hosts, ok := scanner.QueryNSMulti(d, 5*time.Second)
449468
nsCache.mu.Lock()
450-
// Only store if domain hasn't changed while we were querying
451469
if nsCache.domain == d {
452470
nsCache.hosts = hosts
453471
nsCache.ok = ok
@@ -462,6 +480,49 @@ func binaryStatus(domain string) string {
462480
b.WriteString(fmt.Sprintf(" %s %s\n", dimStyle.Render("…"), dimStyle.Render("Checking NS delegation...")))
463481
}
464482
}
483+
// Preflight e2e tunnel check (non-blocking, parallel)
484+
if dnsttBin != "" && domain != "" && pubkey != "" {
485+
cacheKey := domain + "|" + pubkey
486+
e2eCache.mu.Lock()
487+
if e2eCache.key != cacheKey {
488+
// Cancel any in-flight preflight for the old key
489+
if e2eCache.cancel != nil {
490+
e2eCache.cancel()
491+
e2eCache.cancel = nil
492+
}
493+
e2eCache.done = false
494+
e2eCache.loading = false
495+
}
496+
if e2eCache.done {
497+
r := e2eCache.result
498+
e2eCache.mu.Unlock()
499+
if r.OK {
500+
b.WriteString(fmt.Sprintf(" %s %s\n", greenStyle.Render("✔"), dimStyle.Render("Tunnel preflight → connected via "+r.Resolver)))
501+
} else {
502+
b.WriteString(fmt.Sprintf(" %s %s\n", redStyle.Render("✘"), redStyle.Render("Tunnel preflight FAILED")))
503+
}
504+
} else if !e2eCache.loading {
505+
e2eCache.loading = true
506+
e2eCache.key = cacheKey
507+
ctx, cancel := context.WithCancel(context.Background())
508+
e2eCache.cancel = cancel
509+
e2eCache.mu.Unlock()
510+
go func(ctx context.Context, bin, d, pk, tu, pa, key string) {
511+
r := scanner.PreflightE2EContext(ctx, bin, d, pk, tu, pa, 20*time.Second)
512+
e2eCache.mu.Lock()
513+
if e2eCache.key == key {
514+
e2eCache.result = r
515+
e2eCache.done = true
516+
e2eCache.loading = false
517+
}
518+
e2eCache.mu.Unlock()
519+
}(ctx, dnsttBin, domain, pubkey, testURL, proxyAuth, cacheKey)
520+
b.WriteString(fmt.Sprintf(" %s %s\n", dimStyle.Render("…"), dimStyle.Render("Testing tunnel connectivity...")))
521+
} else {
522+
e2eCache.mu.Unlock()
523+
b.WriteString(fmt.Sprintf(" %s %s\n", dimStyle.Render("…"), dimStyle.Render("Testing tunnel connectivity...")))
524+
}
525+
}
465526
return b.String()
466527
}
467528

0 commit comments

Comments
 (0)