@swimmingliu/autovpn 1.6.8 → 1.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pipeline/orchestrator.js +174 -53
- package/dist/pipeline/speedtest.js +28 -9
- package/dist/web/renderer/i18n.js +1 -1
- package/package.json +1 -1
|
@@ -308,6 +308,24 @@ async function forEachWithConcurrency(items, concurrency, mapper) {
|
|
|
308
308
|
throw firstError;
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
|
+
function createLimiter(concurrency) {
|
|
312
|
+
const limit = Math.max(1, Math.trunc(Number(concurrency) || 1));
|
|
313
|
+
let active = 0;
|
|
314
|
+
const queue = [];
|
|
315
|
+
return async (task) => {
|
|
316
|
+
if (active >= limit) {
|
|
317
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
318
|
+
}
|
|
319
|
+
active += 1;
|
|
320
|
+
try {
|
|
321
|
+
return await task();
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
active -= 1;
|
|
325
|
+
queue.shift()?.();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
311
329
|
async function speedtestResumeStateFromEventLog(eventLog) {
|
|
312
330
|
const probes = new Map();
|
|
313
331
|
const fullResults = new Map();
|
|
@@ -414,6 +432,65 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
414
432
|
let availabilityResults = [];
|
|
415
433
|
let availableLinks = [];
|
|
416
434
|
const streamedDeduped = new Set();
|
|
435
|
+
const streamingAvailabilityTasks = [];
|
|
436
|
+
const limitAvailability = createLimiter(speedConfig.concurrency);
|
|
437
|
+
let availabilityCompleted = 0;
|
|
438
|
+
let availabilityStageStart;
|
|
439
|
+
let speedArtifactFlush = Promise.resolve();
|
|
440
|
+
let availabilityArtifactFlush = Promise.resolve();
|
|
441
|
+
const flushStreamingSpeedArtifacts = async (results, links) => {
|
|
442
|
+
const resultSnapshot = [...results];
|
|
443
|
+
const linkSnapshot = [...links];
|
|
444
|
+
speedArtifactFlush = speedArtifactFlush.catch(() => undefined).then(async () => {
|
|
445
|
+
await writeLines(artifactDir, 'vpn_node_speedtest.txt', linkSnapshot);
|
|
446
|
+
await writeJson(artifactDir, 'vpn_node_speedtest_report.json', resultSnapshot);
|
|
447
|
+
});
|
|
448
|
+
await speedArtifactFlush;
|
|
449
|
+
};
|
|
450
|
+
const flushStreamingAvailabilityArtifacts = async () => {
|
|
451
|
+
const resultSnapshot = [...availabilityResults];
|
|
452
|
+
const linkSnapshot = resultSnapshot.filter((result) => result.all_passed).map((result) => result.link);
|
|
453
|
+
availabilityArtifactFlush = availabilityArtifactFlush.catch(() => undefined).then(async () => {
|
|
454
|
+
await writeLines(artifactDir, 'vpn_node_availability.txt', linkSnapshot);
|
|
455
|
+
await writeJson(artifactDir, 'vpn_node_availability_report.json', resultSnapshot);
|
|
456
|
+
});
|
|
457
|
+
await availabilityArtifactFlush;
|
|
458
|
+
};
|
|
459
|
+
const ensureAvailabilityStageStarted = () => {
|
|
460
|
+
availabilityStageStart ??= setStage('availability', 'running', false);
|
|
461
|
+
return availabilityStageStart;
|
|
462
|
+
};
|
|
463
|
+
const runStreamingAvailability = (speedResult) => {
|
|
464
|
+
const task = limitAvailability(async () => {
|
|
465
|
+
await ensureAvailabilityStageStarted();
|
|
466
|
+
const results = context.stages?.availability
|
|
467
|
+
? await context.stages.availability([speedResult], profile.speed_test ?? {}, runtimePath, profile.availability_targets)
|
|
468
|
+
: await checkLinkAvailabilityBatchWithBackend({
|
|
469
|
+
results: [speedResult],
|
|
470
|
+
config: profile.speed_test ?? {},
|
|
471
|
+
runtime_path: runtimePath,
|
|
472
|
+
targets: profile.availability_targets
|
|
473
|
+
}, { cwd: projectRoot, env: runtimeStageEnv });
|
|
474
|
+
for (const result of results) {
|
|
475
|
+
availabilityResults.push(result);
|
|
476
|
+
availabilityCompleted += 1;
|
|
477
|
+
await flushStreamingAvailabilityArtifacts();
|
|
478
|
+
const statuses = Object.entries(result.provider_results)
|
|
479
|
+
.map(([name, provider]) => `${name}=${provider.passed ? 'ok' : provider.reason}`)
|
|
480
|
+
.join(' ');
|
|
481
|
+
emit('log', { message: `[availability] ${availabilityCompleted}/${passedSpeedLinks.length} ${statuses}` });
|
|
482
|
+
emit('availability_link_result', {
|
|
483
|
+
completed: availabilityCompleted,
|
|
484
|
+
total: passedSpeedLinks.length,
|
|
485
|
+
link: result.link,
|
|
486
|
+
all_passed: result.all_passed,
|
|
487
|
+
provider_results: result.provider_results
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
streamingAvailabilityTasks.push(task);
|
|
492
|
+
task.catch(() => undefined);
|
|
493
|
+
};
|
|
417
494
|
const onExtractedLinks = async (links) => {
|
|
418
495
|
for (const link of links) {
|
|
419
496
|
rawLinks.push(link);
|
|
@@ -428,8 +505,6 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
428
505
|
await setStage('extract', 'running');
|
|
429
506
|
if (useStreamingStages) {
|
|
430
507
|
await setStage('dedupe', 'running', false);
|
|
431
|
-
await setStage('speedtest', 'running', false);
|
|
432
|
-
await setStage('availability', 'running', false);
|
|
433
508
|
}
|
|
434
509
|
const extractResults = await Promise.all(sourcesToRun.map(async ([sourceName, source]) => {
|
|
435
510
|
const streamedLinks = new Set();
|
|
@@ -501,19 +576,23 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
501
576
|
? await context.stages.speedtestProbe(dedupedLinks, profile.speed_test ?? {}, runtimePath)
|
|
502
577
|
: await probeSpeedtestLinksInNode({ links: dedupedLinks, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
503
578
|
cwd: projectRoot,
|
|
504
|
-
env: runtimeStageEnv
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const result = probes[index];
|
|
508
|
-
emit('log', { message: `[speedtest:probe] ${index + 1}/${dedupedLinks.length} reachable=${result.reachable} latency=${result.latency_ms}ms` });
|
|
509
|
-
emit('speedtest_probe_result', {
|
|
510
|
-
completed: index + 1,
|
|
511
|
-
total: dedupedLinks.length,
|
|
512
|
-
link: result.link,
|
|
513
|
-
reachable: result.reachable,
|
|
514
|
-
latency_ms: result.latency_ms,
|
|
515
|
-
error: result.error ?? ''
|
|
579
|
+
env: runtimeStageEnv,
|
|
580
|
+
progressCallback: (message) => emit('log', { message }),
|
|
581
|
+
eventCallback: (type, payload) => emit(type, payload)
|
|
516
582
|
});
|
|
583
|
+
if (context.stages?.speedtestProbe) {
|
|
584
|
+
for (let index = 0; index < probes.length; index += 1) {
|
|
585
|
+
const result = probes[index];
|
|
586
|
+
emit('log', { message: `[speedtest:probe] ${index + 1}/${dedupedLinks.length} reachable=${result.reachable} latency=${result.latency_ms}ms` });
|
|
587
|
+
emit('speedtest_probe_result', {
|
|
588
|
+
completed: index + 1,
|
|
589
|
+
total: dedupedLinks.length,
|
|
590
|
+
link: result.link,
|
|
591
|
+
reachable: result.reachable,
|
|
592
|
+
latency_ms: result.latency_ms,
|
|
593
|
+
error: result.error ?? ''
|
|
594
|
+
});
|
|
595
|
+
}
|
|
517
596
|
}
|
|
518
597
|
const candidateLinks = selectSpeedtestCandidates(probes, speedConfig.max_download_candidates);
|
|
519
598
|
const reachableCount = probes.filter((probe) => probe.reachable).length;
|
|
@@ -537,33 +616,56 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
537
616
|
}
|
|
538
617
|
}
|
|
539
618
|
let speedCompleted = 0;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
619
|
+
try {
|
|
620
|
+
await forEachWithConcurrency(candidateLinks, speedConfig.concurrency, async (link) => {
|
|
621
|
+
const result = context.stages?.speedtestLink
|
|
622
|
+
? await context.stages.speedtestLink(link, profile.speed_test ?? {}, runtimePath)
|
|
623
|
+
: await testSpeedtestLinkInNode({ link, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
624
|
+
cwd: projectRoot,
|
|
625
|
+
env: runtimeStageEnv
|
|
626
|
+
});
|
|
627
|
+
if (result.reachable && result.latency_ms <= 0) {
|
|
628
|
+
result.latency_ms = probeByLink.get(result.link)?.latency_ms ?? 0;
|
|
629
|
+
}
|
|
630
|
+
resultByLink.set(result.link, result);
|
|
631
|
+
speedCompleted += 1;
|
|
632
|
+
const passed = result.reachable && result.average_download_mb_s >= speedConfig.min_download_mb_s;
|
|
633
|
+
emit('speedtest_result', {
|
|
634
|
+
completed: speedCompleted,
|
|
635
|
+
total: candidateLinks.length,
|
|
636
|
+
link: result.link,
|
|
637
|
+
reachable: result.reachable,
|
|
638
|
+
average_download_mb_s: result.average_download_mb_s,
|
|
639
|
+
latency_ms: result.latency_ms,
|
|
640
|
+
passed_threshold: passed,
|
|
641
|
+
error: result.error ?? ''
|
|
546
642
|
});
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
message: `[speedtest] ${speedCompleted}/${candidateLinks.length} reachable=${result.reachable} speed=${result.average_download_mb_s}MB/s`
|
|
643
|
+
emit('log', {
|
|
644
|
+
message: `[speedtest] ${speedCompleted}/${candidateLinks.length} reachable=${result.reachable} speed=${result.average_download_mb_s}MB/s`
|
|
645
|
+
});
|
|
646
|
+
const currentSpeedResults = [
|
|
647
|
+
...probes.filter((probe) => !probe.reachable).map((probe) => resultByLink.get(probe.link)),
|
|
648
|
+
...candidateLinks.map((candidate) => resultByLink.get(candidate)).filter((entry) => Boolean(entry))
|
|
649
|
+
];
|
|
650
|
+
const currentPassedLinks = candidateLinks.filter((candidate) => {
|
|
651
|
+
const entry = resultByLink.get(candidate);
|
|
652
|
+
return Boolean(entry?.reachable && entry.average_download_mb_s >= speedConfig.min_download_mb_s);
|
|
653
|
+
});
|
|
654
|
+
summary.counts.speedtest_links = currentPassedLinks.length;
|
|
655
|
+
await flushStreamingSpeedArtifacts(currentSpeedResults, currentPassedLinks);
|
|
656
|
+
if (passed) {
|
|
657
|
+
passedSpeedLinks.push(result.link);
|
|
658
|
+
runStreamingAvailability(result);
|
|
659
|
+
}
|
|
565
660
|
});
|
|
566
|
-
}
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
await Promise.allSettled(streamingAvailabilityTasks);
|
|
664
|
+
if (summary.stage_status.availability === 'running') {
|
|
665
|
+
await setStage('availability', 'failed', false);
|
|
666
|
+
}
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
567
669
|
speedResults = [
|
|
568
670
|
...probes.filter((probe) => !probe.reachable).map((probe) => resultByLink.get(probe.link)),
|
|
569
671
|
...candidateLinks.map((link) => resultByLink.get(link)).filter((result) => Boolean(result))
|
|
@@ -592,21 +694,40 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
592
694
|
if (dedupedLinks.length > 0 && passedSpeedLinks.length === 0) {
|
|
593
695
|
throw new Error(speedtestFailureMessage(speedResults, Number(profile.speed_test?.min_download_mb_s ?? 0)));
|
|
594
696
|
}
|
|
595
|
-
await setStage('speedtest', 'success'
|
|
596
|
-
if (
|
|
697
|
+
await setStage('speedtest', 'success');
|
|
698
|
+
if (useStreamingStages) {
|
|
699
|
+
try {
|
|
700
|
+
await Promise.all(streamingAvailabilityTasks);
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
await setStage('availability', 'failed', false);
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
const availabilityByLink = new Map(availabilityResults.map((result) => [result.link, result]));
|
|
707
|
+
availabilityResults = passedSpeedLinks.map((link) => availabilityByLink.get(link)).filter((result) => Boolean(result));
|
|
708
|
+
}
|
|
709
|
+
else if (summary.stage_status.availability !== 'running') {
|
|
597
710
|
await setStage('availability', 'running');
|
|
598
711
|
}
|
|
599
712
|
const speedResultByLink = new Map(speedResults.map((result) => [result.link, result]));
|
|
600
713
|
const candidateSpeedResults = passedSpeedLinks.map((link) => speedResultByLink.get(link)).filter((result) => Boolean(result));
|
|
601
|
-
availabilityResults =
|
|
602
|
-
?
|
|
603
|
-
:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
714
|
+
availabilityResults = useStreamingStages
|
|
715
|
+
? availabilityResults
|
|
716
|
+
: context.stages?.availability
|
|
717
|
+
? await context.stages.availability(candidateSpeedResults, profile.speed_test ?? {}, runtimePath, profile.availability_targets)
|
|
718
|
+
: await checkLinkAvailabilityBatchWithBackend({
|
|
719
|
+
results: candidateSpeedResults,
|
|
720
|
+
config: profile.speed_test ?? {},
|
|
721
|
+
runtime_path: runtimePath,
|
|
722
|
+
targets: profile.availability_targets
|
|
723
|
+
}, {
|
|
724
|
+
cwd: projectRoot,
|
|
725
|
+
env: runtimeStageEnv,
|
|
726
|
+
progressCallback: (message) => emit('log', { message }),
|
|
727
|
+
eventCallback: (type, payload) => emit(type, payload)
|
|
728
|
+
});
|
|
729
|
+
const availabilityByLinkForOrder = new Map(availabilityResults.map((result) => [result.link, result]));
|
|
730
|
+
availableLinks = passedSpeedLinks.filter((link) => availabilityByLinkForOrder.get(link)?.all_passed);
|
|
610
731
|
summary.counts.availability_links = availableLinks.length;
|
|
611
732
|
await writeLines(artifactDir, 'vpn_node_availability.txt', availableLinks);
|
|
612
733
|
await writeJson(artifactDir, 'vpn_node_availability_report.json', availabilityResults);
|
|
@@ -647,8 +768,8 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
647
768
|
await setStage('deploy', 'skipped');
|
|
648
769
|
}
|
|
649
770
|
else {
|
|
650
|
-
assertDeployMinimumFinalLinks(profile, postprocessed.links);
|
|
651
771
|
await setStage('deploy', 'running');
|
|
772
|
+
assertDeployMinimumFinalLinks(profile, postprocessed.links);
|
|
652
773
|
const deployment = context.stages?.deploy
|
|
653
774
|
? await context.stages.deploy({ projectRoot, bundleDir, profile })
|
|
654
775
|
: await deployPagesWithBackend({ projectRoot, bundleDir, deploy: profile.deploy ?? {} }, { cwd: projectRoot, env });
|
|
@@ -869,8 +990,8 @@ export async function retryNodePipelineStage(options, context = {}) {
|
|
|
869
990
|
if (!fs.existsSync(bundleDir)) {
|
|
870
991
|
throw new Error('No Pages bundle available to retry deploy');
|
|
871
992
|
}
|
|
872
|
-
assertDeployMinimumFinalLinks(profile, finalLinks);
|
|
873
993
|
await setStage('deploy', 'running');
|
|
994
|
+
assertDeployMinimumFinalLinks(profile, finalLinks);
|
|
874
995
|
const deployment = context.stages?.deploy
|
|
875
996
|
? await context.stages.deploy({ projectRoot, bundleDir, profile })
|
|
876
997
|
: await deployPagesWithBackend({ projectRoot, bundleDir, deploy: profile.deploy ?? {} }, { cwd: projectRoot, env });
|
|
@@ -1186,8 +1307,8 @@ export async function resumeNodePipeline(options, context = {}) {
|
|
|
1186
1307
|
await setStage('verify', 'skipped');
|
|
1187
1308
|
}
|
|
1188
1309
|
else {
|
|
1189
|
-
assertDeployMinimumFinalLinks(profile, postprocessed.links);
|
|
1190
1310
|
await setStage('deploy', 'running');
|
|
1311
|
+
assertDeployMinimumFinalLinks(profile, postprocessed.links);
|
|
1191
1312
|
const deployment = context.stages?.deploy
|
|
1192
1313
|
? await context.stages.deploy({ projectRoot, bundleDir, profile })
|
|
1193
1314
|
: await deployPagesWithBackend({ projectRoot, bundleDir, deploy: profile.deploy ?? {} }, { cwd: projectRoot, env });
|
|
@@ -66,7 +66,11 @@ async function mapWithConcurrency(items, concurrency, worker, onComplete) {
|
|
|
66
66
|
onComplete?.(result, index, completed);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
await Promise.
|
|
69
|
+
const workers = await Promise.allSettled(Array.from({ length: Math.min(limit, items.length) }, () => runWorker()));
|
|
70
|
+
const failed = workers.find((result) => result.status === 'rejected');
|
|
71
|
+
if (failed) {
|
|
72
|
+
throw failed.reason;
|
|
73
|
+
}
|
|
70
74
|
return results;
|
|
71
75
|
}
|
|
72
76
|
function defaultNow() {
|
|
@@ -367,7 +371,7 @@ export async function downloadUrlViaHttpProxy(url, proxyUrl, maxBytes, timeoutSe
|
|
|
367
371
|
].join('\r\n'));
|
|
368
372
|
return await readHttpBodyBytes(secureSocket, Math.max(1, maxBytes), timeoutMs);
|
|
369
373
|
}
|
|
370
|
-
async function probeLinksDirect(links, config, options) {
|
|
374
|
+
async function probeLinksDirect(links, config, options, onComplete) {
|
|
371
375
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
372
376
|
if (!fetchImpl) {
|
|
373
377
|
throw new Error('Node speedtest backend requires fetch support');
|
|
@@ -387,9 +391,9 @@ async function probeLinksDirect(links, config, options) {
|
|
|
387
391
|
catch (error) {
|
|
388
392
|
return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
|
|
389
393
|
}
|
|
390
|
-
});
|
|
394
|
+
}, (result, _index, completed) => onComplete?.(result, completed));
|
|
391
395
|
}
|
|
392
|
-
async function probeLinksMihomo(links, config, runtimePath, options) {
|
|
396
|
+
async function probeLinksMihomo(links, config, runtimePath, options, onComplete) {
|
|
393
397
|
const openRuntime = options.openMihomoRuntime ?? defaultOpenMihomoRuntime;
|
|
394
398
|
const probeDelay = options.probeMihomoProxyDelay ?? defaultProbeMihomoProxyDelay;
|
|
395
399
|
return mapWithConcurrency(links, config.concurrency, async (link) => {
|
|
@@ -413,7 +417,7 @@ async function probeLinksMihomo(links, config, runtimePath, options) {
|
|
|
413
417
|
catch (error) {
|
|
414
418
|
return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
|
|
415
419
|
}
|
|
416
|
-
});
|
|
420
|
+
}, (result, _index, completed) => onComplete?.(result, completed));
|
|
417
421
|
}
|
|
418
422
|
async function testLinkDirect(link, config, options) {
|
|
419
423
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
@@ -587,10 +591,25 @@ export async function probeSpeedtestLinksInNode(input, options = {}) {
|
|
|
587
591
|
const runtimePath = input.runtime_path ?? '';
|
|
588
592
|
const requestedRuntime = String((options.env ?? process.env).AUTOVPN_SPEEDTEST_RUNTIME ?? '').trim().toLowerCase();
|
|
589
593
|
const useMihomoRuntime = requestedRuntime !== 'direct';
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
+
const emitProbe = (result, completed) => {
|
|
595
|
+
options.progressCallback?.(`[speedtest:probe] ${completed}/${input.links.length} reachable=${result.reachable} latency=${result.latency_ms}ms`);
|
|
596
|
+
emitEvent(options.eventCallback, 'speedtest_probe_result', {
|
|
597
|
+
completed,
|
|
598
|
+
total: input.links.length,
|
|
599
|
+
link: result.link,
|
|
600
|
+
reachable: result.reachable,
|
|
601
|
+
latency_ms: result.latency_ms,
|
|
602
|
+
error: result.error ?? ''
|
|
603
|
+
});
|
|
604
|
+
};
|
|
605
|
+
if (options.probeLinks) {
|
|
606
|
+
const results = await options.probeLinks(input.links, config, { runtime_path: runtimePath });
|
|
607
|
+
results.forEach((result, index) => emitProbe(result, index + 1));
|
|
608
|
+
return results;
|
|
609
|
+
}
|
|
610
|
+
return useMihomoRuntime
|
|
611
|
+
? probeLinksMihomo(input.links, config, runtimePath, options, emitProbe)
|
|
612
|
+
: probeLinksDirect(input.links, config, options, emitProbe);
|
|
594
613
|
}
|
|
595
614
|
export async function testSpeedtestLinkInNode(input, options = {}) {
|
|
596
615
|
const config = normalizeSpeedTestConfig(input.config);
|