Skip to content

Commit 6ee84cf

Browse files
Update main.go
1 parent 9bef100 commit 6ee84cf

1 file changed

Lines changed: 140 additions & 107 deletions

File tree

main.go

Lines changed: 140 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5658
var cfg Settings
57-
var portPool chan int
58-
5959
var 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+
80102
type 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-
318331
type 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

Comments
 (0)