@@ -44,6 +44,8 @@ type ValidationSettings struct {
4444 PortCheckTimeoutMs int `json:"port_check_timeout_ms"`
4545 MaxRetries int `json:"max_retries"`
4646 BasePort int `json:"base_port"`
47+ BatchRestMs int `json:"batch_rest_ms"`
48+ ProcessKillWaitMs int `json:"process_kill_wait_ms"`
4749 TestURLs []string `json:"test_urls"`
4850}
4951
@@ -54,8 +56,6 @@ type OutputSettings struct {
5456}
5557
5658var cfg Settings
57- var portPool chan int
58-
5959var fetchHTTPClient = & http.Client {
6060 // Timeout is set per-request via context; this is a fallback only.
6161 Transport : & http.Transport {
@@ -77,6 +77,28 @@ type protoStat struct {
7777 totalLatMs int64
7878}
7979
80+ type batchTracker struct {
81+ mu sync.Mutex
82+ cmds []* exec.Cmd
83+ }
84+
85+ func (bt * batchTracker ) register (cmd * exec.Cmd ) {
86+ bt .mu .Lock ()
87+ bt .cmds = append (bt .cmds , cmd )
88+ bt .mu .Unlock ()
89+ }
90+
91+ func (bt * batchTracker ) killAll () {
92+ bt .mu .Lock ()
93+ cmds := make ([]* exec.Cmd , len (bt .cmds ))
94+ copy (cmds , bt .cmds )
95+ bt .cmds = bt .cmds [:0 ]
96+ bt .mu .Unlock ()
97+ for _ , cmd := range cmds {
98+ killGroup (cmd )
99+ }
100+ }
101+
80102type Logger struct {
81103 mu sync.Mutex
82104 file * os.File
@@ -306,15 +328,6 @@ func loadSettings(path string) error {
306328 return json .Unmarshal (data , & cfg )
307329}
308330
309- func initPortPool () {
310- v := cfg .Validation
311- size := v .NumWorkers + 10
312- portPool = make (chan int , size )
313- for i := 0 ; i < size ; i ++ {
314- portPool <- v .BasePort + i
315- }
316- }
317-
318331type fetchResult struct {
319332 url string
320333 content string
@@ -519,8 +532,6 @@ func main() {
519532 defer gLog .close ()
520533 }
521534
522- initPortPool ()
523-
524535 start := time .Now ()
525536 v := cfg .Validation
526537 fmt .Println ("🚀 Starting V2Ray config aggregator..." )
@@ -740,125 +751,145 @@ func validateAll(lines []string) []configResult {
740751 gLog .writeLine ("" )
741752 }
742753
743- // ── Per-protocol failure detail tracking ──────────────────────────────────
744754 protoFails := make (map [string ]* failDetail )
745755 for _ , p := range cfg .ProtocolOrder {
746756 protoFails [p ] = & failDetail {reasons : make (map [string ]int )}
747757 }
748758
749- resultsCh := make (chan configResult , cfg .Validation .NumWorkers * 4 )
750- sem := make (chan struct {}, cfg .Validation .NumWorkers )
751-
752759 var testedCount int64
753760 var passedCount int64
754761 var failedParse int64
755762 var failedStart int64
756763 var failedConn int64
764+ var out []configResult
757765
758- // ── Sequential per-protocol processing ────────────────────────────────────
759- // Each protocol is fully completed before the next one starts.
760- // Within each protocol, workers run in parallel up to NumWorkers.
761- go func () {
762- for _ , proto := range cfg .ProtocolOrder {
763- protoLines := byProto [proto ]
764- if len (protoLines ) == 0 {
765- continue
766+ v := cfg .Validation
767+ batchSize := v .NumWorkers
768+ if batchSize <= 0 {
769+ batchSize = 50
770+ }
771+
772+ for _ , proto := range cfg .ProtocolOrder {
773+ protoLines := byProto [proto ]
774+ if len (protoLines ) == 0 {
775+ continue
776+ }
777+ if gLog != nil {
778+ gLog .logProtoStart (proto , len (protoLines ))
779+ }
780+
781+ protoTotal := len (protoLines )
782+ totalBatches := (protoTotal + batchSize - 1 ) / batchSize
783+ protoStart := time .Now ()
784+
785+ fmt .Printf ("\n 🔵 [%s] Starting — %d configs in %d batches of %d\n " ,
786+ strings .ToUpper (proto ), protoTotal , totalBatches , batchSize )
787+
788+ var protoPassed int64
789+
790+ for batchIdx := 0 ; batchIdx < totalBatches ; batchIdx ++ {
791+ start := batchIdx * batchSize
792+ end := start + batchSize
793+ if end > protoTotal {
794+ end = protoTotal
766795 }
767- if gLog != nil {
768- gLog .logProtoStart (proto , len (protoLines ))
796+ batch := protoLines [start :end ]
797+ actualBatchSize := len (batch )
798+
799+ localPorts := make (chan int , actualBatchSize )
800+ for i := 0 ; i < actualBatchSize ; i ++ {
801+ localPorts <- v .BasePort + i
802+ }
803+
804+ bt := & batchTracker {}
805+
806+ type workerResult struct {
807+ line string
808+ res validationResult
769809 }
770- protoStart := time .Now ()
771- protoTotal := int64 (len (protoLines ))
772- fmt .Printf ("\n 🔵 [%s] Starting — %d configs\n " , strings .ToUpper (proto ), protoTotal )
810+ workerResults := make ([]workerResult , actualBatchSize )
773811
774812 var wg sync.WaitGroup
775- var protoPassed int64
776- var protoTested int64
777-
778- stopBar := make (chan struct {})
779- go func () {
780- const barWidth = 25
781- tick := time .NewTicker (300 * time .Millisecond )
782- defer tick .Stop ()
783- for {
784- select {
785- case <- stopBar :
786- return
787- case <- tick .C :
788- tested := atomic .LoadInt64 (& protoTested )
789- passed := atomic .LoadInt64 (& protoPassed )
790- failed := tested - passed
791- pct := 0.0
792- if protoTotal > 0 {
793- pct = float64 (tested ) / float64 (protoTotal ) * 100
794- }
795- filled := int (pct / 100 * float64 (barWidth ))
796- if filled > barWidth {
797- filled = barWidth
798- }
799- bar := strings .Repeat ("█" , filled ) + strings .Repeat ("░" , barWidth - filled )
800- line := fmt .Sprintf ("\r [%s] %5.1f%% tested=%-7d ✓%-6d ✗%-6d " , bar , pct , tested , passed , failed )
801- os .Stdout .WriteString (line )
802- }
803- }
804- }()
813+ batchStart := time .Now ()
805814
806- for _ , line := range protoLines {
807- l := line
815+ for i , line := range batch {
808816 wg .Add (1 )
809- sem <- struct {}{}
810- go func () {
817+ go func (idx int , l string ) {
811818 defer wg .Done ()
812- defer func () { <- sem }()
813-
814- idx := atomic .AddInt64 (& testedCount , 1 )
815- res := validate (l , proto )
816- atomic .AddInt64 (& protoTested , 1 )
817-
819+ globalIdx := atomic .AddInt64 (& testedCount , 1 )
820+ res := validateWithTracker (l , proto , localPorts , bt )
818821 if gLog != nil {
819- gLog .logResult (idx , proto , l , res )
822+ gLog .logResult (globalIdx , proto , l , res )
820823 }
824+ workerResults [idx ] = workerResult {line : l , res : res }
825+ }(i , line )
826+ }
827+
828+ wg .Wait ()
821829
822- if res .passed {
823- atomic .AddInt64 (& passedCount , 1 )
824- atomic .AddInt64 (& protoPassed , 1 )
825- resultsCh <- configResult {line : l , proto : proto }
830+ bt .killAll ()
831+ if v .ProcessKillWaitMs > 0 {
832+ time .Sleep (time .Duration (v .ProcessKillWaitMs ) * time .Millisecond )
833+ }
834+
835+ var bPassed , bFailed , bParse , bStart , bConn int
836+
837+ for _ , wr := range workerResults {
838+ res := wr .res
839+ if res .passed {
840+ bPassed ++
841+ atomic .AddInt64 (& passedCount , 1 )
842+ atomic .AddInt64 (& protoPassed , 1 )
843+ out = append (out , configResult {line : wr .line , proto : proto })
844+ } else {
845+ bFailed ++
846+ reason := res .failReason
847+ norm := classifyFailReason (reason )
848+ fd := protoFails [proto ]
849+ fd .mu .Lock ()
850+ fd .reasons [norm ]++
851+ fd .mu .Unlock ()
852+
853+ if strings .HasPrefix (reason , "PARSE:" ) {
854+ bParse ++
855+ atomic .AddInt64 (& failedParse , 1 )
856+ } else if strings .HasPrefix (reason , "SINGBOX_START:" ) || strings .HasPrefix (reason , "START:" ) {
857+ bStart ++
858+ atomic .AddInt64 (& failedStart , 1 )
826859 } else {
827- reason := res .failReason
828- norm := classifyFailReason (reason )
829- fd := protoFails [proto ]
830- fd .mu .Lock ()
831- fd .reasons [norm ]++
832- fd .mu .Unlock ()
833-
834- if strings .HasPrefix (reason , "PARSE:" ) {
835- atomic .AddInt64 (& failedParse , 1 )
836- } else if strings .HasPrefix (reason , "SINGBOX_START:" ) || strings .HasPrefix (reason , "START:" ) {
837- atomic .AddInt64 (& failedStart , 1 )
838- } else {
839- atomic .AddInt64 (& failedConn , 1 )
840- }
860+ bConn ++
861+ atomic .AddInt64 (& failedConn , 1 )
841862 }
842- }()
863+ }
843864 }
844- wg .Wait ()
845- close (stopBar )
846- fmt .Println ()
847865
848- elapsed := time .Since (protoStart ).Seconds ()
849- passRate := float64 (protoPassed ) / float64 (protoTotal ) * 100
850- fmt .Printf ("✅ [%s] Done — passed=%d/%d (%.1f%%) in %.1fs\n " ,
851- strings .ToUpper (proto ), protoPassed , protoTotal , passRate , elapsed )
866+ batchElapsed := time .Since (batchStart ).Seconds ()
867+ batchPassRate := 0.0
868+ if actualBatchSize > 0 {
869+ batchPassRate = float64 (bPassed ) / float64 (actualBatchSize ) * 100
870+ }
871+ totalDone := (batchIdx + 1 ) * batchSize
872+ if totalDone > protoTotal {
873+ totalDone = protoTotal
874+ }
875+
876+ fmt .Printf (" 📦 Batch %d/%d [%d configs] ✅%d ❌%d Rate:%.1f%% Time:%.1fs\n " ,
877+ batchIdx + 1 , totalBatches , actualBatchSize , bPassed , bFailed , batchPassRate , batchElapsed )
878+ fmt .Printf (" Parse✗:%-5d Start✗:%-5d Conn✗:%-5d Total:%d/%d\n " ,
879+ bParse , bStart , bConn , totalDone , protoTotal )
880+
881+ if batchIdx < totalBatches - 1 && v .BatchRestMs > 0 {
882+ fmt .Printf (" 💤 %dms rest...\n " , v .BatchRestMs )
883+ time .Sleep (time .Duration (v .BatchRestMs ) * time .Millisecond )
884+ }
852885 }
853- close (resultsCh )
854- }()
855886
856- var out []configResult
857- for r := range resultsCh {
858- out = append (out , r )
887+ protoElapsed := time .Since (protoStart ).Seconds ()
888+ protoPassRate := float64 (protoPassed ) / float64 (protoTotal ) * 100
889+ fmt .Printf ("✅ [%s] Done — passed=%d/%d (%.1f%%) in %.1fs\n " ,
890+ strings .ToUpper (proto ), protoPassed , protoTotal , protoPassRate , protoElapsed )
859891 }
860892
861- // ── Global summary line ────────────────────────────────────────────────────
862893 fmt .Printf ("\n 📊 Tested=%d | Passed=%d | ParseFail=%d | StartFail=%d | ConnFail=%d\n " ,
863894 atomic .LoadInt64 (& testedCount ),
864895 atomic .LoadInt64 (& passedCount ),
@@ -1147,7 +1178,7 @@ func printFailureReport(protoFails map[string]*failDetail, byProto map[string][]
11471178 fmt .Println ()
11481179}
11491180
1150- func validate (configURL , protocol string ) validationResult {
1181+ func validateWithTracker (configURL , protocol string , localPorts chan int , bt * batchTracker ) validationResult {
11511182 var result validationResult
11521183
11531184 outboundJSON , parseErr := toSingBoxOutbound (configURL , protocol )
@@ -1156,8 +1187,8 @@ func validate(configURL, protocol string) validationResult {
11561187 return result
11571188 }
11581189
1159- port := <- portPool
1160- defer func () { portPool <- port }()
1190+ port := <- localPorts
1191+ defer func () { localPorts <- port }()
11611192
11621193 v := cfg .Validation
11631194 fullConfig := buildSingBoxConfig (outboundJSON , port )
@@ -1191,6 +1222,8 @@ func validate(configURL, protocol string) validationResult {
11911222 return result
11921223 }
11931224
1225+ bt .register (cmd )
1226+
11941227 addr := fmt .Sprintf ("127.0.0.1:%d" , port )
11951228 started := waitForPort (addr ,
11961229 time .Duration (v .SingboxStartTimeoutMs )* time .Millisecond ,
@@ -1202,7 +1235,7 @@ func validate(configURL, protocol string) validationResult {
12021235 killGroup (cmd )
12031236 sbErr := extractErr (stderr .String ())
12041237 if sbErr == "" {
1205- sbErr = fmt .Sprintf ("port not open after %dms (ctx=%v) " , v .SingboxStartTimeoutMs , ctx . Err () )
1238+ sbErr = fmt .Sprintf ("port not open after %dms" , v .SingboxStartTimeoutMs )
12061239 }
12071240 result .failReason = "SINGBOX_START: " + sbErr
12081241 return result
0 commit comments