Skip to content

Commit f955a49

Browse files
hiii
1 parent 2481b00 commit f955a49

1 file changed

Lines changed: 163 additions & 27 deletions

File tree

main.go

Lines changed: 163 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,12 @@ func isProtocolSupported(proto string) bool {
510510
return false
511511
}
512512

513+
// failDetail holds per-protocol failure reason counts.
514+
type failDetail struct {
515+
mu sync.Mutex
516+
reasons map[string]int
517+
}
518+
513519
func validateAll(lines []string) []configResult {
514520
seen := make(map[string]bool)
515521
byProto := make(map[string][]string)
@@ -555,6 +561,66 @@ func validateAll(lines []string) []configResult {
555561
gLog.writeLine("")
556562
}
557563

564+
565+
// ── Per-protocol failure detail tracking ──────────────────────────────────
566+
protoFails := make(map[string]*failDetail)
567+
for _, p := range cfg.ProtocolOrder {
568+
protoFails[p] = &failDetail{reasons: make(map[string]int)}
569+
}
570+
571+
// normalizeReason converts a raw failReason into a short, groupable key.
572+
normalizeReason := func(reason string) string {
573+
switch {
574+
case strings.HasPrefix(reason, "PARSE: base64:"):
575+
return "PARSE: base64 decode error"
576+
case strings.HasPrefix(reason, "PARSE: json:"):
577+
return "PARSE: json decode error"
578+
case strings.HasPrefix(reason, "PARSE: url parse:"):
579+
return "PARSE: url parse error"
580+
case strings.HasPrefix(reason, "PARSE: unsupported cipher:"):
581+
return "PARSE: unsupported cipher"
582+
case strings.HasPrefix(reason, "PARSE:"):
583+
msg := strings.TrimPrefix(reason, "PARSE: ")
584+
if len(msg) > 50 {
585+
msg = msg[:50]
586+
}
587+
return "PARSE: " + msg
588+
case strings.HasPrefix(reason, "SINGBOX_START:"), strings.HasPrefix(reason, "START:"):
589+
r := reason
590+
if i := strings.Index(r, ": "); i != -1 {
591+
r = r[i+2:]
592+
}
593+
// Strip ANSI escape codes
594+
r = strings.Map(func(c rune) rune {
595+
if c == 0x1b {
596+
return -1
597+
}
598+
return c
599+
}, r)
600+
if strings.Contains(r, "port not open") {
601+
return "START: port not open (timeout)"
602+
}
603+
if strings.Contains(r, "decode config") || strings.Contains(r, "outbound") {
604+
return "START: invalid config (sing-box rejected)"
605+
}
606+
if len(r) > 60 {
607+
r = r[:60]
608+
}
609+
return "START: " + r
610+
case strings.HasPrefix(reason, "CONN:"):
611+
r := strings.TrimPrefix(reason, "CONN: ")
612+
if i := strings.Index(r, " | "); i != -1 {
613+
r = r[:i]
614+
}
615+
if len(r) > 60 {
616+
r = r[:60]
617+
}
618+
return "CONN: " + r
619+
default:
620+
return reason
621+
}
622+
}
623+
558624
resultsCh := make(chan configResult, cfg.Validation.NumWorkers*4)
559625
sem := make(chan struct{}, cfg.Validation.NumWorkers)
560626

@@ -564,23 +630,26 @@ func validateAll(lines []string) []configResult {
564630
var failedStart int64
565631
var failedConn int64
566632

567-
var outerWg sync.WaitGroup
568-
569-
for _, proto := range cfg.ProtocolOrder {
570-
protoLines := byProto[proto]
571-
if len(protoLines) == 0 {
572-
continue
573-
}
574-
if gLog != nil {
575-
gLog.logProtoStart(proto, len(protoLines))
576-
}
577-
fmt.Printf("\n🔵 %s (%d configs)\n", proto, len(protoLines))
633+
// ── Sequential per-protocol processing ────────────────────────────────────
634+
// Each protocol is fully completed before the next one starts.
635+
// Within each protocol, workers run in parallel up to NumWorkers.
636+
go func() {
637+
for _, proto := range cfg.ProtocolOrder {
638+
protoLines := byProto[proto]
639+
if len(protoLines) == 0 {
640+
continue
641+
}
642+
if gLog != nil {
643+
gLog.logProtoStart(proto, len(protoLines))
644+
}
645+
protoStart := time.Now()
646+
fmt.Printf("\n🔵 [%s] Starting — %d configs\n", strings.ToUpper(proto), len(protoLines))
578647

579-
outerWg.Add(1)
580-
go func(p string, pLines []string) {
581-
defer outerWg.Done()
582648
var wg sync.WaitGroup
583-
for _, line := range pLines {
649+
var protoPassed int64
650+
var protoFailed int64
651+
652+
for _, line := range protoLines {
584653
l := line
585654
wg.Add(1)
586655
sem <- struct{}{}
@@ -589,38 +658,43 @@ func validateAll(lines []string) []configResult {
589658
defer func() { <-sem }()
590659

591660
idx := atomic.AddInt64(&testedCount, 1)
592-
res := validate(l, p)
661+
res := validate(l, proto)
593662

594663
if gLog != nil {
595-
gLog.logResult(idx, p, l, res)
664+
gLog.logResult(idx, proto, l, res)
596665
}
597666

598667
if res.totalScore >= cfg.Validation.MinPassScore {
599668
atomic.AddInt64(&passedCount, 1)
600-
fmt.Printf("✅ [%d] PASS | %s | %dms\n", idx, p, res.latency.Milliseconds())
601-
resultsCh <- configResult{line: l, proto: p, latency: res.latency}
669+
atomic.AddInt64(&protoPassed, 1)
670+
resultsCh <- configResult{line: l, proto: proto, latency: res.latency}
602671
} else {
672+
atomic.AddInt64(&protoFailed, 1)
603673
reason := res.failReason
674+
norm := normalizeReason(reason)
675+
fd := protoFails[proto]
676+
fd.mu.Lock()
677+
fd.reasons[norm]++
678+
fd.mu.Unlock()
679+
604680
if strings.HasPrefix(reason, "PARSE:") {
605681
atomic.AddInt64(&failedParse, 1)
606682
} else if strings.HasPrefix(reason, "SINGBOX_START:") || strings.HasPrefix(reason, "START:") {
607683
atomic.AddInt64(&failedStart, 1)
608684
} else {
609685
atomic.AddInt64(&failedConn, 1)
610686
}
611-
if idx <= 30 || strings.HasPrefix(reason, "PARSE:") || strings.HasPrefix(reason, "SINGBOX_START:") {
612-
fmt.Printf("❌ [%d] FAIL | %s | %s\n", idx, p, shortenErr(reason))
613-
}
614687
}
615688
}()
616689
}
617690
wg.Wait()
618-
fmt.Printf("✔️ %s done.\n", p)
619-
}(proto, protoLines)
620-
}
621691

622-
go func() {
623-
outerWg.Wait()
692+
elapsed := time.Since(protoStart).Seconds()
693+
total := int64(len(protoLines))
694+
passRate := float64(protoPassed) / float64(total) * 100
695+
fmt.Printf("✅ [%s] Done — passed=%d/%d (%.1f%%) in %.1fs\n",
696+
strings.ToUpper(proto), protoPassed, total, passRate, elapsed)
697+
}
624698
close(resultsCh)
625699
}()
626700

@@ -629,16 +703,78 @@ func validateAll(lines []string) []configResult {
629703
out = append(out, r)
630704
}
631705

706+
// ── Global summary line ────────────────────────────────────────────────────
632707
fmt.Printf("\n📊 Tested=%d | Passed=%d | ParseFail=%d | StartFail=%d | ConnFail=%d\n",
633708
atomic.LoadInt64(&testedCount),
634709
atomic.LoadInt64(&passedCount),
635710
atomic.LoadInt64(&failedParse),
636711
atomic.LoadInt64(&failedStart),
637712
atomic.LoadInt64(&failedConn))
638713

714+
// ── Detailed per-protocol failure report ─────────────────────────────────
715+
printFailureReport(protoFails, byProto)
716+
639717
return out
640718
}
641719

720+
// printFailureReport prints a detailed statistical breakdown of failures per protocol.
721+
func printFailureReport(protoFails map[string]*failDetail, byProto map[string][]string) {
722+
fmt.Println()
723+
fmt.Println("╔══════════════════════════════════════════════════════════════════════╗")
724+
fmt.Println("║ FAILURE ANALYSIS REPORT (per protocol) ║")
725+
fmt.Println("╚══════════════════════════════════════════════════════════════════════╝")
726+
727+
for _, proto := range cfg.ProtocolOrder {
728+
fd := protoFails[proto]
729+
if fd == nil {
730+
continue
731+
}
732+
total := len(byProto[proto])
733+
if total == 0 {
734+
continue
735+
}
736+
totalFails := 0
737+
for _, c := range fd.reasons {
738+
totalFails += c
739+
}
740+
passed := total - totalFails
741+
passRate := float64(passed) / float64(total) * 100
742+
743+
fmt.Printf("\n┌─ %s ─── total=%d passed=%d failed=%d pass_rate=%.1f%%\n",
744+
strings.ToUpper(proto), total, passed, totalFails, passRate)
745+
746+
if totalFails == 0 {
747+
fmt.Println("│ (no failures)")
748+
fmt.Println("└" + strings.Repeat("─", 70))
749+
continue
750+
}
751+
752+
// Sort reasons by count descending
753+
type kv struct {
754+
key string
755+
val int
756+
}
757+
var sorted []kv
758+
for k, v := range fd.reasons {
759+
sorted = append(sorted, kv{k, v})
760+
}
761+
sort.Slice(sorted, func(i, j int) bool { return sorted[i].val > sorted[j].val })
762+
763+
for i, item := range sorted {
764+
prefix := "│ ├"
765+
if i == len(sorted)-1 {
766+
prefix = "│ └"
767+
}
768+
pct := float64(item.val) / float64(totalFails) * 100
769+
bar := strings.Repeat("█", int(pct/5))
770+
fmt.Printf("%s %-42s %5d (%5.1f%%) %s\n",
771+
prefix, item.key, item.val, pct, bar)
772+
}
773+
fmt.Println("└" + strings.Repeat("─", 70))
774+
}
775+
fmt.Println()
776+
}
777+
642778
func validate(configURL, protocol string) validationResult {
643779
result := validationResult{failReason: "unknown"}
644780

0 commit comments

Comments
 (0)