@swimmingliu/autovpn 1.6.6 → 1.6.7

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.
@@ -285,32 +285,28 @@ async function restoreResumeSpeedResults(artifactDir, eventLog, passedLinks) {
285
285
  }
286
286
  async function forEachWithConcurrency(items, concurrency, mapper) {
287
287
  let nextIndex = 0;
288
+ let failed = false;
289
+ let firstError;
288
290
  const workerCount = Math.max(1, Math.min(Math.max(1, Math.floor(concurrency)), items.length));
289
- await Promise.all(Array.from({ length: workerCount }, async () => {
290
- while (nextIndex < items.length) {
291
+ const workers = Array.from({ length: workerCount }, async () => {
292
+ while (nextIndex < items.length && !failed) {
291
293
  const index = nextIndex;
292
294
  nextIndex += 1;
293
- await mapper(items[index]);
294
- }
295
- }));
296
- }
297
- function createLimiter(concurrency) {
298
- const limit = Math.max(1, Math.trunc(concurrency));
299
- let active = 0;
300
- const queue = [];
301
- return async (task) => {
302
- if (active >= limit) {
303
- await new Promise((resolve) => queue.push(resolve));
304
- }
305
- active += 1;
306
- try {
307
- return await task();
308
- }
309
- finally {
310
- active -= 1;
311
- queue.shift()?.();
295
+ try {
296
+ await mapper(items[index]);
297
+ }
298
+ catch (error) {
299
+ if (!failed) {
300
+ failed = true;
301
+ firstError = error;
302
+ }
303
+ }
312
304
  }
313
- };
305
+ });
306
+ await Promise.allSettled(workers);
307
+ if (failed) {
308
+ throw firstError;
309
+ }
314
310
  }
315
311
  async function speedtestResumeStateFromEventLog(eventLog) {
316
312
  const probes = new Map();
@@ -418,98 +414,6 @@ export async function runNodePipeline(options, context = {}) {
418
414
  let availabilityResults = [];
419
415
  let availableLinks = [];
420
416
  const streamedDeduped = new Set();
421
- const speedTasks = [];
422
- const availabilityTasks = [];
423
- const limitSpeedtest = createLimiter(Number(speedConfig.concurrency ?? 1));
424
- const limitAvailability = createLimiter(Number(speedConfig.concurrency ?? 1));
425
- let speedCompleted = 0;
426
- let availabilityCompleted = 0;
427
- let speedArtifactFlush = Promise.resolve();
428
- let availabilityArtifactFlush = Promise.resolve();
429
- const flushStreamingSpeedArtifacts = async () => {
430
- const resultSnapshot = [...speedResults];
431
- const linkSnapshot = [...passedSpeedLinks];
432
- speedArtifactFlush = speedArtifactFlush.catch(() => undefined).then(async () => {
433
- summary.counts.speedtest_links = linkSnapshot.length;
434
- await writeLines(artifactDir, 'vpn_node_speedtest.txt', linkSnapshot);
435
- await writeJson(artifactDir, 'vpn_node_speedtest_report.json', resultSnapshot);
436
- await writeReport();
437
- });
438
- await speedArtifactFlush;
439
- };
440
- const flushStreamingAvailabilityArtifacts = async () => {
441
- const resultSnapshot = [...availabilityResults];
442
- const linkSnapshot = resultSnapshot.filter((result) => result.all_passed).map((result) => result.link);
443
- availabilityArtifactFlush = availabilityArtifactFlush.catch(() => undefined).then(async () => {
444
- summary.counts.availability_links = linkSnapshot.length;
445
- await writeLines(artifactDir, 'vpn_node_availability.txt', linkSnapshot);
446
- await writeJson(artifactDir, 'vpn_node_availability_report.json', resultSnapshot);
447
- await writeReport();
448
- });
449
- await availabilityArtifactFlush;
450
- };
451
- const runStreamingAvailability = (speedResult) => {
452
- const task = limitAvailability(async () => {
453
- const results = context.stages?.availability
454
- ? await context.stages.availability([speedResult], profile.speed_test ?? {}, runtimePath, profile.availability_targets)
455
- : await checkLinkAvailabilityBatchWithBackend({
456
- results: [speedResult],
457
- config: profile.speed_test ?? {},
458
- runtime_path: runtimePath,
459
- targets: profile.availability_targets
460
- }, { cwd: projectRoot, env: runtimeStageEnv });
461
- for (const result of results) {
462
- availabilityResults.push(result);
463
- availabilityCompleted += 1;
464
- await flushStreamingAvailabilityArtifacts();
465
- emit('availability_link_result', {
466
- completed: availabilityCompleted,
467
- total: passedSpeedLinks.length,
468
- link: result.link,
469
- all_passed: result.all_passed,
470
- provider_results: result.provider_results
471
- });
472
- }
473
- });
474
- availabilityTasks.push(task);
475
- task.catch(() => undefined);
476
- };
477
- const runStreamingSpeedtest = (link) => {
478
- const task = limitSpeedtest(async () => {
479
- const result = context.stages?.speedtestLink
480
- ? await context.stages.speedtestLink(link, profile.speed_test ?? {}, runtimePath)
481
- : await testSpeedtestLinkInNode({ link, config: profile.speed_test, runtime_path: runtimePath }, {
482
- cwd: projectRoot,
483
- env: runtimeStageEnv
484
- });
485
- speedResults.push(result);
486
- speedCompleted += 1;
487
- const passed = result.reachable && result.average_download_mb_s >= speedConfig.min_download_mb_s;
488
- emit('speedtest_result', {
489
- completed: speedCompleted,
490
- total: dedupedLinks.length,
491
- link: result.link,
492
- reachable: result.reachable,
493
- average_download_mb_s: result.average_download_mb_s,
494
- latency_ms: result.latency_ms,
495
- passed_threshold: passed,
496
- error: result.error ?? ''
497
- });
498
- emit('log', {
499
- message: `[speedtest] ${speedCompleted}/${dedupedLinks.length} reachable=${result.reachable} speed=${result.average_download_mb_s}MB/s`
500
- });
501
- if (passed) {
502
- passedSpeedLinks.push(result.link);
503
- await flushStreamingSpeedArtifacts();
504
- runStreamingAvailability(result);
505
- }
506
- else if (useStreamingStages) {
507
- await flushStreamingSpeedArtifacts();
508
- }
509
- });
510
- speedTasks.push(task);
511
- task.catch(() => undefined);
512
- };
513
417
  const onExtractedLinks = async (links) => {
514
418
  for (const link of links) {
515
419
  rawLinks.push(link);
@@ -519,7 +423,6 @@ export async function runNodePipeline(options, context = {}) {
519
423
  }
520
424
  streamedDeduped.add(key);
521
425
  dedupedLinks.push(link);
522
- runStreamingSpeedtest(link);
523
426
  }
524
427
  };
525
428
  await setStage('extract', 'running');
@@ -594,8 +497,81 @@ export async function runNodePipeline(options, context = {}) {
594
497
  emit('log', {
595
498
  message: `[speedtest] runtime_core=${requestedRuntime === 'direct' ? 'direct' : 'mihomo'} probe_url=${speedConfig.probe_url}`
596
499
  });
597
- await Promise.all(speedTasks);
598
- await Promise.all(availabilityTasks);
500
+ const probes = context.stages?.speedtestProbe
501
+ ? await context.stages.speedtestProbe(dedupedLinks, profile.speed_test ?? {}, runtimePath)
502
+ : await probeSpeedtestLinksInNode({ links: dedupedLinks, config: profile.speed_test, runtime_path: runtimePath }, {
503
+ cwd: projectRoot,
504
+ env: runtimeStageEnv
505
+ });
506
+ for (let index = 0; index < probes.length; index += 1) {
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 ?? ''
516
+ });
517
+ }
518
+ const candidateLinks = selectSpeedtestCandidates(probes, speedConfig.max_download_candidates);
519
+ const reachableCount = probes.filter((probe) => probe.reachable).length;
520
+ emit('log', { message: `[speedtest] selected ${candidateLinks.length}/${reachableCount} reachable links for full download test` });
521
+ emit('speedtest_selected', {
522
+ total_links: dedupedLinks.length,
523
+ reachable_count: reachableCount,
524
+ candidate_count: candidateLinks.length
525
+ });
526
+ const probeByLink = new Map(probes.map((probe) => [probe.link, probe]));
527
+ const resultByLink = new Map();
528
+ for (const probe of probes) {
529
+ if (!probe.reachable) {
530
+ resultByLink.set(probe.link, {
531
+ link: probe.link,
532
+ reachable: false,
533
+ average_download_mb_s: 0,
534
+ latency_ms: probe.latency_ms,
535
+ error: probe.error ?? ''
536
+ });
537
+ }
538
+ }
539
+ let speedCompleted = 0;
540
+ await forEachWithConcurrency(candidateLinks, speedConfig.concurrency, async (link) => {
541
+ const result = context.stages?.speedtestLink
542
+ ? await context.stages.speedtestLink(link, profile.speed_test ?? {}, runtimePath)
543
+ : await testSpeedtestLinkInNode({ link, config: profile.speed_test, runtime_path: runtimePath }, {
544
+ cwd: projectRoot,
545
+ env: runtimeStageEnv
546
+ });
547
+ if (result.reachable && result.latency_ms <= 0) {
548
+ result.latency_ms = probeByLink.get(result.link)?.latency_ms ?? 0;
549
+ }
550
+ resultByLink.set(result.link, result);
551
+ speedCompleted += 1;
552
+ const passed = result.reachable && result.average_download_mb_s >= speedConfig.min_download_mb_s;
553
+ emit('speedtest_result', {
554
+ completed: speedCompleted,
555
+ total: candidateLinks.length,
556
+ link: result.link,
557
+ reachable: result.reachable,
558
+ average_download_mb_s: result.average_download_mb_s,
559
+ latency_ms: result.latency_ms,
560
+ passed_threshold: passed,
561
+ error: result.error ?? ''
562
+ });
563
+ emit('log', {
564
+ message: `[speedtest] ${speedCompleted}/${candidateLinks.length} reachable=${result.reachable} speed=${result.average_download_mb_s}MB/s`
565
+ });
566
+ });
567
+ speedResults = [
568
+ ...probes.filter((probe) => !probe.reachable).map((probe) => resultByLink.get(probe.link)),
569
+ ...candidateLinks.map((link) => resultByLink.get(link)).filter((result) => Boolean(result))
570
+ ];
571
+ passedSpeedLinks = candidateLinks.filter((link) => {
572
+ const result = resultByLink.get(link);
573
+ return Boolean(result?.reachable && result.average_download_mb_s >= speedConfig.min_download_mb_s);
574
+ });
599
575
  }
600
576
  else {
601
577
  speedResults = context.stages?.speedtest
@@ -622,20 +598,22 @@ export async function runNodePipeline(options, context = {}) {
622
598
  }
623
599
  const speedResultByLink = new Map(speedResults.map((result) => [result.link, result]));
624
600
  const candidateSpeedResults = passedSpeedLinks.map((link) => speedResultByLink.get(link)).filter((result) => Boolean(result));
625
- availabilityResults = useStreamingStages
626
- ? availabilityResults
627
- : context.stages?.availability
628
- ? await context.stages.availability(candidateSpeedResults, profile.speed_test ?? {}, runtimePath, profile.availability_targets)
629
- : await checkLinkAvailabilityBatchWithBackend({
630
- results: candidateSpeedResults,
631
- config: profile.speed_test ?? {},
632
- runtime_path: runtimePath,
633
- targets: profile.availability_targets
634
- }, { cwd: projectRoot, env: runtimeStageEnv });
601
+ availabilityResults = context.stages?.availability
602
+ ? await context.stages.availability(candidateSpeedResults, profile.speed_test ?? {}, runtimePath, profile.availability_targets)
603
+ : await checkLinkAvailabilityBatchWithBackend({
604
+ results: candidateSpeedResults,
605
+ config: profile.speed_test ?? {},
606
+ runtime_path: runtimePath,
607
+ targets: profile.availability_targets
608
+ }, { cwd: projectRoot, env: runtimeStageEnv });
635
609
  availableLinks = availabilityResults.filter((result) => result.all_passed).map((result) => result.link);
636
610
  summary.counts.availability_links = availableLinks.length;
637
611
  await writeLines(artifactDir, 'vpn_node_availability.txt', availableLinks);
638
612
  await writeJson(artifactDir, 'vpn_node_availability_report.json', availabilityResults);
613
+ if (passedSpeedLinks.length > 0 && availableLinks.length === 0) {
614
+ await setStage('availability', 'failed');
615
+ throw new Error('No links passed availability');
616
+ }
639
617
  await setStage('availability', 'success', !useStreamingStages);
640
618
  await setStage('postprocess', 'running');
641
619
  const countryLookup = context.stages?.countryLookup ?? defaultCountryFor;
@@ -1,6 +1,3 @@
1
- import path from 'node:path';
2
- import { spawn as defaultSpawn } from 'node:child_process';
3
- import { mergeProjectEnv } from '../runtime/env.js';
4
1
  const EMOJI_MAP = {
5
2
  AE: '🇦🇪',
6
3
  AR: '🇦🇷',
@@ -46,30 +43,6 @@ const DEFAULT_FILTERS = {
46
43
  per_country_limit: {}
47
44
  };
48
45
  const LEADING_EMOJI_AND_COUNTRY_PATTERN = /^(?:\S+\s+)?([A-Za-z]{2})\s+(.*)$/;
49
- const PYTHON_POSTPROCESS_HELPER = `
50
- import json
51
- import sys
52
- from vpn_automation.config.models import FilterConfig
53
- from vpn_automation.pipeline.postprocess import decorate_link_with_country, select_links_by_country_limit
54
-
55
- payload = json.load(sys.stdin)
56
- filters = payload.get("filters")
57
- ranked_links = payload.get("ranked_links") or []
58
- filter_config = FilterConfig() if filters is None else FilterConfig(
59
- excluded_country_codes=list(filters.get("excluded_country_codes") or []),
60
- per_country_limit=dict(filters.get("per_country_limit") or {}),
61
- )
62
- selected = select_links_by_country_limit(
63
- [(item["link"], {}, item.get("country_code") or item.get("countryCode") or "") for item in ranked_links],
64
- filter_config,
65
- )
66
- country_by_link = {
67
- item["link"]: item.get("country_code") or item.get("countryCode") or ""
68
- for item in ranked_links
69
- }
70
- json.dump({"links": [decorate_link_with_country(link, country_by_link[link]) for link in selected]}, sys.stdout, ensure_ascii=False)
71
- sys.stdout.write("\\n")
72
- `;
73
46
  export function parseVmessLink(link) {
74
47
  const encoded = link.replace(/^vmess:\/\//, '');
75
48
  const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
@@ -149,64 +122,7 @@ export function runPostprocess(input) {
149
122
  links: selected.map((link) => decorateLinkWithCountry(link, countries.get(link) ?? ''))
150
123
  };
151
124
  }
152
- export function selectPipelineStageBackend(stage, env = process.env) {
153
- void stage;
154
- void env;
155
- return 'node';
156
- }
157
- async function defaultResolvePythonCli(env) {
158
- // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
159
- const runner = await import('../../lib/runner.mjs');
160
- return runner.resolveOrInstallPythonCli({ env });
161
- }
162
- function pythonCommandFor(resolved) {
163
- const command = resolved.command;
164
- const name = path.basename(command).toLowerCase();
165
- if (['autovpn', 'autovpn.exe'].includes(name)) {
166
- const executable = process.platform === 'win32' ? 'python.exe' : 'python';
167
- return path.join(path.dirname(command), executable);
168
- }
169
- return process.platform === 'win32' ? 'python.exe' : 'python3';
170
- }
171
- async function postprocessWithPython(input, options) {
172
- const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
173
- const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
174
- const helperInput = { ...input, filters: resolveFilters(input.filters) };
175
- const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_POSTPROCESS_HELPER], {
176
- cwd: options.cwd ?? process.cwd(),
177
- env,
178
- stdio: ['pipe', 'pipe', 'pipe']
179
- });
180
- let stdout = '';
181
- let stderr = '';
182
- child.stdout?.on('data', (chunk) => {
183
- stdout += String(chunk);
184
- });
185
- child.stderr?.on('data', (chunk) => {
186
- stderr += String(chunk);
187
- });
188
- const completion = new Promise((resolve, reject) => {
189
- child.on('error', reject);
190
- child.on('close', (code) => {
191
- if (code !== 0) {
192
- reject(new Error(`Python postprocess backend failed with exit code ${code}: ${stderr.trim()}`));
193
- return;
194
- }
195
- try {
196
- resolve(JSON.parse(stdout));
197
- }
198
- catch (error) {
199
- reject(new Error(`Python postprocess backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
200
- }
201
- });
202
- });
203
- child.stdin?.write(JSON.stringify(helperInput));
204
- child.stdin?.end();
205
- return completion;
206
- }
207
125
  export async function postprocessLinksWithBackend(input, options = {}) {
208
- if (selectPipelineStageBackend('postprocess', options.env ?? process.env) === 'python') {
209
- return options.pythonPostprocess ? options.pythonPostprocess(input) : postprocessWithPython(input, options);
210
- }
126
+ void options;
211
127
  return runPostprocess(input);
212
128
  }
@@ -274,6 +274,11 @@ export async function openMihomoRuntime(link, options = {}) {
274
274
  child.off('error', onStartupError);
275
275
  child.on('error', () => { });
276
276
  await close();
277
+ if (error instanceof Error
278
+ && error.name !== 'AbortError'
279
+ && /^proxy port \d+ did not open in time$/.test(error.message)) {
280
+ error.code = 'AUTOVPN_INTERNAL_TIMEOUT';
281
+ }
277
282
  throw error;
278
283
  }
279
284
  return {
@@ -1,20 +1,4 @@
1
- import path from 'node:path';
2
- import { spawn as defaultSpawn } from 'node:child_process';
3
- import { mergeProjectEnv } from '../runtime/env.js';
4
1
  export const MAIN_DATA_PLACEHOLDER = '__MAIN_DATA__';
5
- const PYTHON_RENDER_HELPER = `
6
- import json
7
- import sys
8
- from vpn_automation.pipeline.render import replace_main_data
9
-
10
- payload = json.load(sys.stdin)
11
- json.dump(
12
- {"rendered_source": replace_main_data(payload["template"], list(payload.get("links") or []))},
13
- sys.stdout,
14
- ensure_ascii=False,
15
- )
16
- sys.stdout.write("\\n")
17
- `;
18
2
  export function replaceMainData(template, links) {
19
3
  const occurrences = template.split(MAIN_DATA_PLACEHOLDER).length - 1;
20
4
  if (occurrences !== 1) {
@@ -27,63 +11,7 @@ export function runRender(input) {
27
11
  rendered_source: replaceMainData(input.template, input.links ?? [])
28
12
  };
29
13
  }
30
- export function selectPipelineStageBackend(stage, env = process.env) {
31
- void stage;
32
- void env;
33
- return 'node';
34
- }
35
- async function defaultResolvePythonCli(env) {
36
- // @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
37
- const runner = await import('../../lib/runner.mjs');
38
- return runner.resolveOrInstallPythonCli({ env });
39
- }
40
- function pythonCommandFor(resolved) {
41
- const command = resolved.command;
42
- const name = path.basename(command).toLowerCase();
43
- if (['autovpn', 'autovpn.exe'].includes(name)) {
44
- const executable = process.platform === 'win32' ? 'python.exe' : 'python';
45
- return path.join(path.dirname(command), executable);
46
- }
47
- return process.platform === 'win32' ? 'python.exe' : 'python3';
48
- }
49
- async function renderWithPython(input, options) {
50
- const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
51
- const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
52
- const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_RENDER_HELPER], {
53
- cwd: options.cwd ?? process.cwd(),
54
- env,
55
- stdio: ['pipe', 'pipe', 'pipe']
56
- });
57
- let stdout = '';
58
- let stderr = '';
59
- child.stdout?.on('data', (chunk) => {
60
- stdout += String(chunk);
61
- });
62
- child.stderr?.on('data', (chunk) => {
63
- stderr += String(chunk);
64
- });
65
- const completion = new Promise((resolve, reject) => {
66
- child.on('error', reject);
67
- child.on('close', (code) => {
68
- if (code !== 0) {
69
- reject(new Error(`Python render backend failed with exit code ${code}: ${stderr.trim()}`));
70
- return;
71
- }
72
- try {
73
- resolve(JSON.parse(stdout));
74
- }
75
- catch (error) {
76
- reject(new Error(`Python render backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
77
- }
78
- });
79
- });
80
- child.stdin?.write(JSON.stringify(input));
81
- child.stdin?.end();
82
- return completion;
83
- }
84
14
  export async function renderMainDataWithBackend(input, options = {}) {
85
- if (selectPipelineStageBackend('render', options.env ?? process.env) === 'python') {
86
- return options.pythonRender ? options.pythonRender(input) : renderWithPython(input, options);
87
- }
15
+ void options;
88
16
  return runRender(input);
89
17
  }