@swimmingliu/autovpn 1.6.1 → 1.6.2
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/backend/node-backend.js +1 -1
- package/dist/backend/select-backend.js +1 -2
- package/dist/cli/main.js +23 -14
- package/dist/cli/native-commands.js +12 -12
- package/dist/jobs/commands.js +8 -19
- package/dist/pipeline/availability.js +25 -12
- package/dist/pipeline/dedupe.js +3 -5
- package/dist/pipeline/deploy.js +3 -5
- package/dist/pipeline/extract.js +35 -7
- package/dist/pipeline/obfuscate.js +3 -5
- package/dist/pipeline/orchestrator.js +240 -39
- package/dist/pipeline/postprocess.js +3 -5
- package/dist/pipeline/proxy-runtime.js +42 -6
- package/dist/pipeline/render.js +3 -5
- package/dist/pipeline/speedtest.js +72 -36
- package/dist/runtime/managed-tools.js +9 -2
- package/dist/server/http.js +11 -8
- package/dist/server/runtime.js +40 -3
- package/dist/web/renderer/app.js +62 -4
- package/dist/web/renderer/i18n.js +1 -1
- package/dist/web/renderer/styles.css +321 -1
- package/dist/web/renderer/views.js +8 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { mergeProjectEnv } from '../runtime/env.js';
|
|
|
6
6
|
import { resolveArtifactsRoot, resolveProfilePath } from '../runtime/paths.js';
|
|
7
7
|
import { redactText } from '../runtime/redaction.js';
|
|
8
8
|
import { fetchSourceLinksWithBackend } from './extract.js';
|
|
9
|
-
import { dedupeVmessLinksWithBackend } from './dedupe.js';
|
|
9
|
+
import { canonicalVmessKey, dedupeVmessLinksWithBackend, parseVmessLink } from './dedupe.js';
|
|
10
10
|
import { normalizeSpeedTestConfig, probeSpeedtestLinksInNode, selectSpeedtestCandidates, speedtestLinksWithBackend, testSpeedtestLinkInNode } from './speedtest.js';
|
|
11
11
|
import { checkLinkAvailabilityBatchWithBackend } from './availability.js';
|
|
12
12
|
import { decorateLinkWithCountry, postprocessLinksWithBackend } from './postprocess.js';
|
|
@@ -69,6 +69,27 @@ async function readLines(filePath) {
|
|
|
69
69
|
}
|
|
70
70
|
return (await readFile(filePath, 'utf8')).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
71
71
|
}
|
|
72
|
+
function speedtestFailureMessage(results, minDownloadMbS) {
|
|
73
|
+
const reachable = results.filter((result) => result.reachable);
|
|
74
|
+
if (reachable.length === 0) {
|
|
75
|
+
return 'No links passed speed test';
|
|
76
|
+
}
|
|
77
|
+
const bestSpeed = Math.max(...reachable.map((result) => Number(result.average_download_mb_s) || 0));
|
|
78
|
+
return `No links met minimum speed threshold ${minDownloadMbS}MB/s; best speed was ${bestSpeed}MB/s`;
|
|
79
|
+
}
|
|
80
|
+
function deployMinimumFinalLinks(profile) {
|
|
81
|
+
const raw = Number(profile.deploy?.min_final_links ?? 10);
|
|
82
|
+
if (!Number.isFinite(raw) || raw < 0) {
|
|
83
|
+
return 10;
|
|
84
|
+
}
|
|
85
|
+
return Math.trunc(raw);
|
|
86
|
+
}
|
|
87
|
+
function assertDeployMinimumFinalLinks(profile, finalLinks) {
|
|
88
|
+
const minimum = deployMinimumFinalLinks(profile);
|
|
89
|
+
if (minimum > 0 && finalLinks.length < minimum) {
|
|
90
|
+
throw new Error(`final node count ${finalLinks.length} is below deploy minimum ${minimum}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
72
93
|
async function copyIfExists(source, destination) {
|
|
73
94
|
if (!fs.existsSync(source)) {
|
|
74
95
|
return;
|
|
@@ -140,6 +161,14 @@ function orderPreservingUnique(values) {
|
|
|
140
161
|
}
|
|
141
162
|
return result;
|
|
142
163
|
}
|
|
164
|
+
function streamingDedupeKey(link) {
|
|
165
|
+
try {
|
|
166
|
+
return canonicalVmessKey(parseVmessLink(link));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return link;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
143
172
|
function defaultCountryFor(_link, _speedResult, _availabilityResult) {
|
|
144
173
|
return 'US';
|
|
145
174
|
}
|
|
@@ -264,6 +293,24 @@ async function forEachWithConcurrency(items, concurrency, mapper) {
|
|
|
264
293
|
}
|
|
265
294
|
}));
|
|
266
295
|
}
|
|
296
|
+
function createLimiter(concurrency) {
|
|
297
|
+
const limit = Math.max(1, Math.trunc(concurrency));
|
|
298
|
+
let active = 0;
|
|
299
|
+
const queue = [];
|
|
300
|
+
return async (task) => {
|
|
301
|
+
if (active >= limit) {
|
|
302
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
303
|
+
}
|
|
304
|
+
active += 1;
|
|
305
|
+
try {
|
|
306
|
+
return await task();
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
active -= 1;
|
|
310
|
+
queue.shift()?.();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
267
314
|
async function speedtestResumeStateFromEventLog(eventLog) {
|
|
268
315
|
const probes = new Map();
|
|
269
316
|
const fullResults = new Map();
|
|
@@ -341,9 +388,11 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
341
388
|
};
|
|
342
389
|
const writeReport = () => writeJson(artifactDir, 'pipeline_report.json', summary);
|
|
343
390
|
let activeStage;
|
|
344
|
-
const setStage = async (stage, status) => {
|
|
391
|
+
const setStage = async (stage, status, trackActive = true) => {
|
|
345
392
|
summary.stage_status[stage] = status;
|
|
346
|
-
|
|
393
|
+
if (trackActive) {
|
|
394
|
+
activeStage = status === 'running' ? stage : activeStage === stage ? undefined : activeStage;
|
|
395
|
+
}
|
|
347
396
|
emit('stage', { stage, status });
|
|
348
397
|
await writeReport();
|
|
349
398
|
};
|
|
@@ -357,16 +406,126 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
357
406
|
await setStage('doctor', 'running');
|
|
358
407
|
await setStage('doctor', 'success');
|
|
359
408
|
const profile = await readProfile(projectRoot, env);
|
|
360
|
-
await setStage('extract', 'running');
|
|
361
409
|
const sourcesToRun = enabledSources(profile);
|
|
410
|
+
const runtimePath = path.join(artifactDir, 'runtime');
|
|
411
|
+
const speedConfig = normalizeSpeedTestConfig(profile.speed_test);
|
|
412
|
+
const useStreamingStages = !context.stages?.speedtest || Boolean(context.stages.speedtestLink);
|
|
413
|
+
let rawLinks = [];
|
|
414
|
+
let dedupedLinks = [];
|
|
415
|
+
let speedResults = [];
|
|
416
|
+
let passedSpeedLinks = [];
|
|
417
|
+
let availabilityResults = [];
|
|
418
|
+
let availableLinks = [];
|
|
419
|
+
const streamedDeduped = new Set();
|
|
420
|
+
const speedTasks = [];
|
|
421
|
+
const availabilityTasks = [];
|
|
422
|
+
const limitSpeedtest = createLimiter(Number(speedConfig.concurrency ?? 1));
|
|
423
|
+
const limitAvailability = createLimiter(Number(speedConfig.concurrency ?? 1));
|
|
424
|
+
let speedCompleted = 0;
|
|
425
|
+
let availabilityCompleted = 0;
|
|
426
|
+
const runStreamingAvailability = (speedResult) => {
|
|
427
|
+
const task = limitAvailability(async () => {
|
|
428
|
+
const results = context.stages?.availability
|
|
429
|
+
? await context.stages.availability([speedResult], profile.speed_test ?? {}, runtimePath, profile.availability_targets)
|
|
430
|
+
: await checkLinkAvailabilityBatchWithBackend({
|
|
431
|
+
results: [speedResult],
|
|
432
|
+
config: profile.speed_test ?? {},
|
|
433
|
+
runtime_path: runtimePath,
|
|
434
|
+
targets: profile.availability_targets
|
|
435
|
+
}, { cwd: projectRoot, env: runtimeStageEnv });
|
|
436
|
+
for (const result of results) {
|
|
437
|
+
availabilityResults.push(result);
|
|
438
|
+
availabilityCompleted += 1;
|
|
439
|
+
if (context.stages?.availability) {
|
|
440
|
+
emit('availability_link_result', {
|
|
441
|
+
completed: availabilityCompleted,
|
|
442
|
+
total: passedSpeedLinks.length,
|
|
443
|
+
link: result.link,
|
|
444
|
+
all_passed: result.all_passed,
|
|
445
|
+
provider_results: result.provider_results
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
availabilityTasks.push(task);
|
|
451
|
+
task.catch(() => undefined);
|
|
452
|
+
};
|
|
453
|
+
const runStreamingSpeedtest = (link) => {
|
|
454
|
+
const task = limitSpeedtest(async () => {
|
|
455
|
+
const result = context.stages?.speedtestLink
|
|
456
|
+
? await context.stages.speedtestLink(link, profile.speed_test ?? {}, runtimePath)
|
|
457
|
+
: await testSpeedtestLinkInNode({ link, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
458
|
+
cwd: projectRoot,
|
|
459
|
+
env: runtimeStageEnv
|
|
460
|
+
});
|
|
461
|
+
speedResults.push(result);
|
|
462
|
+
speedCompleted += 1;
|
|
463
|
+
const passed = result.reachable && result.average_download_mb_s >= speedConfig.min_download_mb_s;
|
|
464
|
+
emit('speedtest_result', {
|
|
465
|
+
completed: speedCompleted,
|
|
466
|
+
total: dedupedLinks.length,
|
|
467
|
+
link: result.link,
|
|
468
|
+
reachable: result.reachable,
|
|
469
|
+
average_download_mb_s: result.average_download_mb_s,
|
|
470
|
+
latency_ms: result.latency_ms,
|
|
471
|
+
passed_threshold: passed,
|
|
472
|
+
error: result.error ?? ''
|
|
473
|
+
});
|
|
474
|
+
emit('log', {
|
|
475
|
+
message: `[speedtest] ${speedCompleted}/${dedupedLinks.length} reachable=${result.reachable} speed=${result.average_download_mb_s}MB/s`
|
|
476
|
+
});
|
|
477
|
+
if (passed) {
|
|
478
|
+
passedSpeedLinks.push(result.link);
|
|
479
|
+
runStreamingAvailability(result);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
speedTasks.push(task);
|
|
483
|
+
task.catch(() => undefined);
|
|
484
|
+
};
|
|
485
|
+
const onExtractedLinks = async (links) => {
|
|
486
|
+
for (const link of links) {
|
|
487
|
+
rawLinks.push(link);
|
|
488
|
+
const key = streamingDedupeKey(link);
|
|
489
|
+
if (streamedDeduped.has(key)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
streamedDeduped.add(key);
|
|
493
|
+
dedupedLinks.push(link);
|
|
494
|
+
runStreamingSpeedtest(link);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
await setStage('extract', 'running');
|
|
498
|
+
if (useStreamingStages) {
|
|
499
|
+
await setStage('dedupe', 'running', false);
|
|
500
|
+
await setStage('speedtest', 'running', false);
|
|
501
|
+
await setStage('availability', 'running', false);
|
|
502
|
+
}
|
|
362
503
|
const extractResults = await Promise.all(sourcesToRun.map(async ([sourceName, source]) => {
|
|
504
|
+
const streamedLinks = new Set();
|
|
505
|
+
const stream = useStreamingStages
|
|
506
|
+
? {
|
|
507
|
+
onLinks: async (links) => {
|
|
508
|
+
for (const link of links) {
|
|
509
|
+
streamedLinks.add(link);
|
|
510
|
+
}
|
|
511
|
+
await onExtractedLinks(links);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
: undefined;
|
|
363
515
|
const result = context.stages?.extract
|
|
364
|
-
? await context.stages.extract({ source_name: sourceName, source })
|
|
516
|
+
? await context.stages.extract({ source_name: sourceName, source }, stream)
|
|
365
517
|
: await fetchSourceLinksWithBackend({ source_name: sourceName, source }, {
|
|
366
518
|
cwd: projectRoot,
|
|
367
519
|
env: runtimeStageEnv,
|
|
368
|
-
eventCallback: (type, payload) => emit(type, payload)
|
|
520
|
+
eventCallback: (type, payload) => emit(type, payload),
|
|
521
|
+
linksCallback: stream?.onLinks
|
|
369
522
|
});
|
|
523
|
+
if (useStreamingStages && stream) {
|
|
524
|
+
const missingLinks = result.links.filter((link) => !streamedLinks.has(link));
|
|
525
|
+
if (missingLinks.length > 0) {
|
|
526
|
+
await stream.onLinks(missingLinks);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
370
529
|
summary.source_counts[result.source_name] = {
|
|
371
530
|
raw_links: result.links.length,
|
|
372
531
|
successful_iterations: result.successful_iterations,
|
|
@@ -374,48 +533,82 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
374
533
|
};
|
|
375
534
|
return result;
|
|
376
535
|
}));
|
|
377
|
-
|
|
536
|
+
if (!useStreamingStages) {
|
|
537
|
+
rawLinks = extractResults.flatMap((result) => result.links);
|
|
538
|
+
}
|
|
378
539
|
if (sourcesToRun.length > 0 && rawLinks.length === 0 && extractResults.some((result) => result.requested_iterations > 0 || result.failed_iterations > 0)) {
|
|
379
540
|
throw new Error('No links extracted from configured sources');
|
|
380
541
|
}
|
|
381
542
|
summary.counts.raw_links = rawLinks.length;
|
|
382
543
|
await writeLines(artifactDir, 'vpn_node_raw.txt', rawLinks);
|
|
383
544
|
await setStage('extract', 'success');
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
545
|
+
if (summary.stage_status.dedupe !== 'running') {
|
|
546
|
+
await setStage('dedupe', 'running');
|
|
547
|
+
}
|
|
548
|
+
dedupedLinks = useStreamingStages
|
|
549
|
+
? dedupedLinks
|
|
550
|
+
: context.stages?.extract
|
|
551
|
+
? orderPreservingUnique(rawLinks)
|
|
552
|
+
: await dedupeVmessLinksWithBackend(rawLinks, { cwd: projectRoot, env });
|
|
388
553
|
summary.counts.deduped_links = dedupedLinks.length;
|
|
389
554
|
await writeLines(artifactDir, 'vpn_node_deduped.txt', dedupedLinks);
|
|
390
|
-
await setStage('dedupe', 'success');
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
555
|
+
await setStage('dedupe', 'success', !useStreamingStages);
|
|
556
|
+
if (summary.stage_status.speedtest !== 'running') {
|
|
557
|
+
await setStage('speedtest', 'running');
|
|
558
|
+
}
|
|
559
|
+
if (useStreamingStages) {
|
|
560
|
+
const requestedRuntime = String(runtimeStageEnv.AUTOVPN_SPEEDTEST_RUNTIME ?? '').trim().toLowerCase();
|
|
561
|
+
emit('speedtest_runtime', {
|
|
562
|
+
runtime_core: requestedRuntime === 'direct' ? 'direct' : 'mihomo',
|
|
563
|
+
probe_url: speedConfig.probe_url,
|
|
564
|
+
urls: [...speedConfig.urls]
|
|
565
|
+
});
|
|
566
|
+
emit('log', {
|
|
567
|
+
message: `[speedtest] runtime_core=${requestedRuntime === 'direct' ? 'direct' : 'mihomo'} probe_url=${speedConfig.probe_url}`
|
|
568
|
+
});
|
|
569
|
+
await Promise.all(speedTasks);
|
|
570
|
+
await Promise.all(availabilityTasks);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
speedResults = context.stages?.speedtest
|
|
574
|
+
? await context.stages.speedtest(dedupedLinks, profile.speed_test ?? {}, runtimePath)
|
|
575
|
+
: await speedtestLinksWithBackend({ links: dedupedLinks, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
576
|
+
cwd: projectRoot,
|
|
577
|
+
env: runtimeStageEnv,
|
|
578
|
+
progressCallback: (message) => emit('log', { message }),
|
|
579
|
+
eventCallback: (type, payload) => emit(type, payload)
|
|
580
|
+
});
|
|
581
|
+
passedSpeedLinks = speedResults
|
|
582
|
+
.filter((result) => result.reachable && result.average_download_mb_s >= Number(profile.speed_test?.min_download_mb_s ?? 0))
|
|
583
|
+
.map((result) => result.link);
|
|
584
|
+
}
|
|
399
585
|
summary.counts.speedtest_links = passedSpeedLinks.length;
|
|
400
586
|
await writeLines(artifactDir, 'vpn_node_speedtest.txt', passedSpeedLinks);
|
|
401
587
|
await writeJson(artifactDir, 'vpn_node_speedtest_report.json', speedResults);
|
|
402
|
-
|
|
403
|
-
|
|
588
|
+
if (dedupedLinks.length > 0 && passedSpeedLinks.length === 0) {
|
|
589
|
+
throw new Error(speedtestFailureMessage(speedResults, Number(profile.speed_test?.min_download_mb_s ?? 0)));
|
|
590
|
+
}
|
|
591
|
+
await setStage('speedtest', 'success', !useStreamingStages);
|
|
592
|
+
if (summary.stage_status.availability !== 'running') {
|
|
593
|
+
await setStage('availability', 'running');
|
|
594
|
+
}
|
|
404
595
|
const speedResultByLink = new Map(speedResults.map((result) => [result.link, result]));
|
|
405
596
|
const candidateSpeedResults = passedSpeedLinks.map((link) => speedResultByLink.get(link)).filter((result) => Boolean(result));
|
|
406
|
-
|
|
407
|
-
?
|
|
408
|
-
:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
597
|
+
availabilityResults = useStreamingStages
|
|
598
|
+
? availabilityResults
|
|
599
|
+
: context.stages?.availability
|
|
600
|
+
? await context.stages.availability(candidateSpeedResults, profile.speed_test ?? {}, runtimePath, profile.availability_targets)
|
|
601
|
+
: await checkLinkAvailabilityBatchWithBackend({
|
|
602
|
+
results: candidateSpeedResults,
|
|
603
|
+
config: profile.speed_test ?? {},
|
|
604
|
+
runtime_path: runtimePath,
|
|
605
|
+
targets: profile.availability_targets
|
|
606
|
+
}, { cwd: projectRoot, env: runtimeStageEnv });
|
|
607
|
+
availableLinks = availabilityResults.filter((result) => result.all_passed).map((result) => result.link);
|
|
415
608
|
summary.counts.availability_links = availableLinks.length;
|
|
416
609
|
await writeLines(artifactDir, 'vpn_node_availability.txt', availableLinks);
|
|
417
610
|
await writeJson(artifactDir, 'vpn_node_availability_report.json', availabilityResults);
|
|
418
|
-
await setStage('availability', 'success');
|
|
611
|
+
await setStage('availability', 'success', !useStreamingStages);
|
|
419
612
|
await setStage('postprocess', 'running');
|
|
420
613
|
const countryLookup = context.stages?.countryLookup ?? defaultCountryFor;
|
|
421
614
|
const availabilityByLink = new Map(availabilityResults.map((result) => [result.link, result]));
|
|
@@ -448,6 +641,7 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
448
641
|
await setStage('deploy', 'skipped');
|
|
449
642
|
}
|
|
450
643
|
else {
|
|
644
|
+
assertDeployMinimumFinalLinks(profile, postprocessed.links);
|
|
451
645
|
await setStage('deploy', 'running');
|
|
452
646
|
const deployment = context.stages?.deploy
|
|
453
647
|
? await context.stages.deploy({ projectRoot, bundleDir, profile })
|
|
@@ -547,7 +741,12 @@ export async function retryNodePipelineStage(options, context = {}) {
|
|
|
547
741
|
await setStage('speedtest', 'running');
|
|
548
742
|
speedResults = context.stages?.speedtest
|
|
549
743
|
? await context.stages.speedtest(dedupedLinks, profile.speed_test ?? {}, runtimePath)
|
|
550
|
-
: await speedtestLinksWithBackend({ links: dedupedLinks, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
744
|
+
: await speedtestLinksWithBackend({ links: dedupedLinks, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
745
|
+
cwd: projectRoot,
|
|
746
|
+
env: runtimeStageEnv,
|
|
747
|
+
progressCallback: (message) => emit('log', { message }),
|
|
748
|
+
eventCallback: (type, payload) => emit(type, payload)
|
|
749
|
+
});
|
|
551
750
|
const passedSpeedLinks = speedResults
|
|
552
751
|
.filter((result) => result.reachable && result.average_download_mb_s >= Number(profile.speed_test?.min_download_mb_s ?? 0))
|
|
553
752
|
.map((result) => result.link);
|
|
@@ -557,9 +756,9 @@ export async function retryNodePipelineStage(options, context = {}) {
|
|
|
557
756
|
if (passedSpeedLinks.length === 0) {
|
|
558
757
|
await setStage('speedtest', 'failed');
|
|
559
758
|
summary.run_status = 'failed';
|
|
560
|
-
summary.error =
|
|
759
|
+
summary.error = `Error: ${speedtestFailureMessage(speedResults, Number(profile.speed_test?.min_download_mb_s ?? 0))}`;
|
|
561
760
|
await writeReport();
|
|
562
|
-
throw new Error(
|
|
761
|
+
throw new Error(speedtestFailureMessage(speedResults, Number(profile.speed_test?.min_download_mb_s ?? 0)));
|
|
563
762
|
}
|
|
564
763
|
speedResults = speedResults.filter((result) => passedSpeedLinks.includes(result.link));
|
|
565
764
|
await setStage('speedtest', 'success');
|
|
@@ -662,6 +861,7 @@ export async function retryNodePipelineStage(options, context = {}) {
|
|
|
662
861
|
if (!fs.existsSync(bundleDir)) {
|
|
663
862
|
throw new Error('No Pages bundle available to retry deploy');
|
|
664
863
|
}
|
|
864
|
+
assertDeployMinimumFinalLinks(profile, finalLinks);
|
|
665
865
|
await setStage('deploy', 'running');
|
|
666
866
|
const deployment = context.stages?.deploy
|
|
667
867
|
? await context.stages.deploy({ projectRoot, bundleDir, profile })
|
|
@@ -760,8 +960,8 @@ async function resumeNodeSpeedtest(options, context = {}) {
|
|
|
760
960
|
const speedConfig = normalizeSpeedTestConfig(profile.speed_test);
|
|
761
961
|
const requestedRuntime = String(env.AUTOVPN_SPEEDTEST_RUNTIME ?? '').trim().toLowerCase();
|
|
762
962
|
const usingInjectedSpeedtestStages = Boolean(context.stages?.speedtestProbe && context.stages?.speedtestLink);
|
|
763
|
-
if (requestedRuntime
|
|
764
|
-
throw new Error('Node resume speedtest
|
|
963
|
+
if (requestedRuntime === 'direct' && !usingInjectedSpeedtestStages) {
|
|
964
|
+
throw new Error('Node resume speedtest cannot use AUTOVPN_SPEEDTEST_RUNTIME=direct');
|
|
765
965
|
}
|
|
766
966
|
const remainingProbeLinks = dedupedLinks.filter((link) => !probes.has(link));
|
|
767
967
|
if (remainingProbeLinks.length > 0) {
|
|
@@ -826,9 +1026,9 @@ async function resumeNodeSpeedtest(options, context = {}) {
|
|
|
826
1026
|
if (fastResults.length === 0) {
|
|
827
1027
|
await setStage('speedtest', 'failed');
|
|
828
1028
|
summary.run_status = 'failed';
|
|
829
|
-
summary.error =
|
|
1029
|
+
summary.error = `Error: ${speedtestFailureMessage(orderedFullResults, speedConfig.min_download_mb_s)}`;
|
|
830
1030
|
await writeReport();
|
|
831
|
-
throw new Error(
|
|
1031
|
+
throw new Error(speedtestFailureMessage(orderedFullResults, speedConfig.min_download_mb_s));
|
|
832
1032
|
}
|
|
833
1033
|
await setStage('speedtest', 'success');
|
|
834
1034
|
summary.run_status = 'success';
|
|
@@ -978,6 +1178,7 @@ export async function resumeNodePipeline(options, context = {}) {
|
|
|
978
1178
|
await setStage('verify', 'skipped');
|
|
979
1179
|
}
|
|
980
1180
|
else {
|
|
1181
|
+
assertDeployMinimumFinalLinks(profile, postprocessed.links);
|
|
981
1182
|
await setStage('deploy', 'running');
|
|
982
1183
|
const deployment = context.stages?.deploy
|
|
983
1184
|
? await context.stages.deploy({ projectRoot, bundleDir, profile })
|
|
@@ -150,11 +150,9 @@ export function runPostprocess(input) {
|
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
export function selectPipelineStageBackend(stage, env = process.env) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const selected = stageOverride || pipelineOverride || 'node';
|
|
157
|
-
return selected === 'python' ? 'python' : 'node';
|
|
153
|
+
void stage;
|
|
154
|
+
void env;
|
|
155
|
+
return 'node';
|
|
158
156
|
}
|
|
159
157
|
async function defaultResolvePythonCli(env) {
|
|
160
158
|
// @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn as defaultSpawn } from 'node:child_process';
|
|
2
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdtemp, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
@@ -13,6 +13,7 @@ export const PROXY_ENV_KEYS = [
|
|
|
13
13
|
'NO_PROXY',
|
|
14
14
|
'no_proxy'
|
|
15
15
|
];
|
|
16
|
+
const MIHOMO_DELAY_TIMEOUT_MAX_MS = 30_000;
|
|
16
17
|
function padBase64(encoded) {
|
|
17
18
|
return encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
|
|
18
19
|
}
|
|
@@ -99,7 +100,8 @@ export async function selectMihomoProxy(controllerUrl, proxyName, _timeoutSecond
|
|
|
99
100
|
export async function probeMihomoProxyDelay(controllerUrl, proxyName, probeUrl, timeoutSeconds, options = {}) {
|
|
100
101
|
const fetchImpl = requireFetch(options.fetch);
|
|
101
102
|
const url = new URL(`${controllerUrl}/proxies/${encodeURIComponent(proxyName)}/delay`);
|
|
102
|
-
|
|
103
|
+
const timeoutMs = Math.min(MIHOMO_DELAY_TIMEOUT_MAX_MS, Math.max(1, Math.trunc(timeoutSeconds * 1000)));
|
|
104
|
+
url.searchParams.set('timeout', String(timeoutMs));
|
|
103
105
|
url.searchParams.set('url', probeUrl);
|
|
104
106
|
const response = await fetchImpl(url.toString(), { method: 'GET' });
|
|
105
107
|
requireOkResponse(response, 'mihomo proxy delay probe');
|
|
@@ -176,6 +178,27 @@ async function closeChildProcess(process) {
|
|
|
176
178
|
process.kill('SIGTERM');
|
|
177
179
|
});
|
|
178
180
|
}
|
|
181
|
+
async function resolveMihomoCommand(runtimePath) {
|
|
182
|
+
const candidate = String(runtimePath ?? '').trim();
|
|
183
|
+
if (!candidate) {
|
|
184
|
+
return 'mihomo';
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const info = await stat(candidate);
|
|
188
|
+
if (info.isFile()) {
|
|
189
|
+
return candidate;
|
|
190
|
+
}
|
|
191
|
+
if (info.isDirectory() && path.basename(candidate) === 'runtime') {
|
|
192
|
+
return 'mihomo';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
if (path.basename(candidate) === 'runtime') {
|
|
197
|
+
return 'mihomo';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return candidate;
|
|
201
|
+
}
|
|
179
202
|
export async function openMihomoRuntime(link, options = {}) {
|
|
180
203
|
const startupWaitSeconds = Number(options.startupWaitSeconds ?? 1);
|
|
181
204
|
const mixedPort = Number.isInteger(options.mixedPort) && Number(options.mixedPort) > 0
|
|
@@ -191,10 +214,19 @@ export async function openMihomoRuntime(link, options = {}) {
|
|
|
191
214
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'autovpn-mihomo-'));
|
|
192
215
|
const configPath = path.join(tempDir, 'config.json');
|
|
193
216
|
await writeFile(configPath, JSON.stringify(config), 'utf8');
|
|
194
|
-
const
|
|
217
|
+
const command = await resolveMihomoCommand(options.runtimePath);
|
|
218
|
+
const child = (options.spawn ?? defaultSpawn)(command, ['-f', configPath], {
|
|
195
219
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
196
220
|
env: stripProxyEnv(options.env ?? process.env)
|
|
197
221
|
});
|
|
222
|
+
let rejectStartupError;
|
|
223
|
+
const startupError = new Promise((_resolve, reject) => {
|
|
224
|
+
rejectStartupError = reject;
|
|
225
|
+
});
|
|
226
|
+
const onStartupError = (error) => {
|
|
227
|
+
rejectStartupError?.(error);
|
|
228
|
+
};
|
|
229
|
+
child.once('error', onStartupError);
|
|
198
230
|
let closed = false;
|
|
199
231
|
const close = async () => {
|
|
200
232
|
if (closed) {
|
|
@@ -206,11 +238,15 @@ export async function openMihomoRuntime(link, options = {}) {
|
|
|
206
238
|
};
|
|
207
239
|
try {
|
|
208
240
|
const waitForPort = options.waitForPort ?? defaultWaitForPort;
|
|
209
|
-
await waitForPort(mixedPort, startupWaitSeconds + 4);
|
|
210
|
-
await waitForPort(controllerPort, startupWaitSeconds + 4);
|
|
211
|
-
await (options.selectProxy ?? ((url, name, timeoutSeconds) => selectMihomoProxy(url, name, timeoutSeconds)))(controllerUrl, proxyName, startupWaitSeconds + 4);
|
|
241
|
+
await Promise.race([waitForPort(mixedPort, startupWaitSeconds + 4), startupError]);
|
|
242
|
+
await Promise.race([waitForPort(controllerPort, startupWaitSeconds + 4), startupError]);
|
|
243
|
+
await Promise.race([(options.selectProxy ?? ((url, name, timeoutSeconds) => selectMihomoProxy(url, name, timeoutSeconds)))(controllerUrl, proxyName, startupWaitSeconds + 4), startupError]);
|
|
244
|
+
child.off('error', onStartupError);
|
|
245
|
+
child.on('error', () => { });
|
|
212
246
|
}
|
|
213
247
|
catch (error) {
|
|
248
|
+
child.off('error', onStartupError);
|
|
249
|
+
child.on('error', () => { });
|
|
214
250
|
await close();
|
|
215
251
|
throw error;
|
|
216
252
|
}
|
package/dist/pipeline/render.js
CHANGED
|
@@ -28,11 +28,9 @@ export function runRender(input) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
export function selectPipelineStageBackend(stage, env = process.env) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const selected = stageOverride || pipelineOverride || 'node';
|
|
35
|
-
return selected === 'python' ? 'python' : 'node';
|
|
31
|
+
void stage;
|
|
32
|
+
void env;
|
|
33
|
+
return 'node';
|
|
36
34
|
}
|
|
37
35
|
async function defaultResolvePythonCli(env) {
|
|
38
36
|
// @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
|