@singbox-iac/cli 0.1.18 → 0.1.19

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.
Files changed (48) hide show
  1. package/README-en.md +47 -1
  2. package/README.md +47 -1
  3. package/dist/cli/commands/diagnose.js +19 -5
  4. package/dist/cli/commands/diagnose.js.map +1 -1
  5. package/dist/cli/commands/reload.js +13 -1
  6. package/dist/cli/commands/reload.js.map +1 -1
  7. package/dist/cli/commands/restart.js +25 -5
  8. package/dist/cli/commands/restart.js.map +1 -1
  9. package/dist/cli/commands/start.js +70 -12
  10. package/dist/cli/commands/start.js.map +1 -1
  11. package/dist/cli/commands/status.js +5 -4
  12. package/dist/cli/commands/status.js.map +1 -1
  13. package/dist/cli/commands/stop.js +15 -4
  14. package/dist/cli/commands/stop.js.map +1 -1
  15. package/dist/cli/commands/verify.js +19 -1
  16. package/dist/cli/commands/verify.js.map +1 -1
  17. package/dist/config/load-config.js +24 -1
  18. package/dist/config/load-config.js.map +1 -1
  19. package/dist/modules/authoring/index.js +21 -1
  20. package/dist/modules/authoring/index.js.map +1 -1
  21. package/dist/modules/desktop-runtime/index.d.ts +25 -0
  22. package/dist/modules/desktop-runtime/index.js +119 -0
  23. package/dist/modules/desktop-runtime/index.js.map +1 -1
  24. package/dist/modules/diagnostics/index.js +83 -8
  25. package/dist/modules/diagnostics/index.js.map +1 -1
  26. package/dist/modules/doctor/index.js +41 -0
  27. package/dist/modules/doctor/index.js.map +1 -1
  28. package/dist/modules/manager/index.js +15 -5
  29. package/dist/modules/manager/index.js.map +1 -1
  30. package/dist/modules/natural-language/index.d.ts +1 -0
  31. package/dist/modules/natural-language/index.js +83 -35
  32. package/dist/modules/natural-language/index.js.map +1 -1
  33. package/dist/modules/runtime-watchdog/index.d.ts +10 -0
  34. package/dist/modules/runtime-watchdog/index.js +33 -0
  35. package/dist/modules/runtime-watchdog/index.js.map +1 -1
  36. package/dist/modules/status/index.d.ts +4 -0
  37. package/dist/modules/status/index.js +55 -3
  38. package/dist/modules/status/index.js.map +1 -1
  39. package/dist/modules/verification/index.d.ts +22 -0
  40. package/dist/modules/verification/index.js +449 -8
  41. package/dist/modules/verification/index.js.map +1 -1
  42. package/dist/modules/verification-plan/index.js +4 -3
  43. package/dist/modules/verification-plan/index.js.map +1 -1
  44. package/docs/natural-language-authoring.md +6 -0
  45. package/docs/runtime-modes.md +23 -7
  46. package/docs/runtime-on-macos.md +48 -9
  47. package/docs/sing-box-config-primer.md +10 -1
  48. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { constants } from "node:fs";
3
3
  import { access } from "node:fs/promises";
4
- import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
4
+ import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
5
5
  import net from "node:net";
6
6
  import { tmpdir } from "node:os";
7
7
  import path from "node:path";
8
+ import { isProcessRoot } from "../desktop-runtime/index.js";
8
9
  import { checkConfig, resolveSingBoxBinary } from "../manager/index.js";
9
10
  import { resolveChromeDependency } from "../runtime-dependencies/index.js";
10
11
  export function assertVerificationReportPassed(report) {
@@ -16,6 +17,7 @@ export function assertVerificationReportPassed(report) {
16
17
  .map((scenario) => `- ${scenario.name}\n url: ${scenario.url}\n details: ${scenario.details}`)
17
18
  .join("\n")}`);
18
19
  }
20
+ const nativeProcessProbeUrl = "https://example.com/";
19
21
  export async function verifyConfigRoutes(input) {
20
22
  const singBoxBinary = await resolveSingBoxBinary(input.singBoxBinary);
21
23
  const requestBinary = await resolveCurlBinary();
@@ -98,6 +100,120 @@ export function verifyAppPlan(plan, config) {
98
100
  details: describeProcessAwareRouteResult(rules, check),
99
101
  }));
100
102
  }
103
+ export async function verifyNativeProcessE2E(input) {
104
+ const nativeChecks = input.plan.appChecks.filter((check) => check.expectedCaptureBackend === "native-process");
105
+ if (nativeChecks.length === 0) {
106
+ return [];
107
+ }
108
+ if (!isProcessRoot()) {
109
+ return nativeChecks.map((check) => ({
110
+ app: check.app,
111
+ status: "SKIP",
112
+ expectedOutboundGroup: check.expectedOutboundGroup,
113
+ details: "Native-process e2e requires root. Re-run `sudo singbox-iac verify app -c <builder.config.yaml>`.",
114
+ }));
115
+ }
116
+ const singBoxBinary = await resolveSingBoxBinary(input.singBoxBinary);
117
+ const cCompilerBinary = await resolveCCompilerBinary();
118
+ const rawConfig = JSON.parse(await readFile(input.configPath, "utf8"));
119
+ const results = [];
120
+ for (const check of nativeChecks) {
121
+ const prepared = await prepareVerificationConfig(rawConfig);
122
+ const conflictingOutbound = chooseConflictingOutbound(prepared.config, check.expectedOutboundGroup);
123
+ if (!conflictingOutbound) {
124
+ results.push({
125
+ app: check.app,
126
+ status: "SKIP",
127
+ expectedOutboundGroup: check.expectedOutboundGroup,
128
+ details: "No conflicting fallback outbound is available in the compiled config for native-process e2e.",
129
+ });
130
+ continue;
131
+ }
132
+ injectNativeProcessProbeRule(prepared.config, conflictingOutbound);
133
+ const processName = selectNativeProcessProbeName(prepared.config, check.expectedOutboundGroup);
134
+ if (!processName) {
135
+ results.push({
136
+ app: check.app,
137
+ status: "SKIP",
138
+ expectedOutboundGroup: check.expectedOutboundGroup,
139
+ details: "No concrete process_name matcher was available for native-process e2e. Add a plain processName matcher to the bundle or explicit process policy.",
140
+ });
141
+ continue;
142
+ }
143
+ const runDir = await mkdtemp(path.join(tmpdir(), "singbox-iac-native-process-"));
144
+ const verifyConfigPath = path.join(runDir, "native-process.config.json");
145
+ const logPath = path.join(runDir, "native-process.log");
146
+ const probeBinaryPath = path.join(runDir, processName);
147
+ await compileNativeProcessProbe({
148
+ runDir,
149
+ compilerBinary: cCompilerBinary,
150
+ probeBinaryPath,
151
+ });
152
+ injectNativeProcessPathProbeRule(prepared.config, probeBinaryPath, check.expectedOutboundGroup);
153
+ await writeFile(verifyConfigPath, `${JSON.stringify(prepared.config, null, 2)}\n`, "utf8");
154
+ await checkConfig({ configPath: verifyConfigPath, singBoxBinary });
155
+ const logBuffer = { text: "" };
156
+ const appender = async (chunk) => {
157
+ const text = chunk.toString();
158
+ logBuffer.text += text;
159
+ await writeFile(logPath, text, { encoding: "utf8", flag: "a" });
160
+ };
161
+ const child = spawn(singBoxBinary, ["run", "-c", verifyConfigPath], {
162
+ stdio: ["ignore", "pipe", "pipe"],
163
+ });
164
+ child.stdout.on("data", (chunk) => void appender(chunk));
165
+ child.stderr.on("data", (chunk) => void appender(chunk));
166
+ const exitPromise = new Promise((resolve, reject) => {
167
+ child.on("error", reject);
168
+ child.on("close", (code) => resolve(code ?? 0));
169
+ });
170
+ try {
171
+ await waitForLog(logBuffer, /sing-box started/, 15_000, "Timed out waiting for sing-box startup during native-process verification.");
172
+ const scenario = await verifyNativeProcessRuntimeScenario({
173
+ id: `native-process-${slugifyProcessName(processName)}`,
174
+ name: `native-process e2e for ${check.app}`,
175
+ url: nativeProcessProbeUrl,
176
+ inboundTag: "in-tun",
177
+ proxyPort: prepared.mixedPort,
178
+ expectedOutboundGroup: check.expectedOutboundGroup,
179
+ processName,
180
+ probeBinaryPath,
181
+ }, logBuffer);
182
+ results.push({
183
+ app: check.app,
184
+ status: scenario.passed ? "PASS" : "FAIL",
185
+ expectedOutboundGroup: check.expectedOutboundGroup,
186
+ processName,
187
+ details: scenario.passed
188
+ ? `Probe binary=${probeBinaryPath}; compiled process_name=${processName}; conflicting fallback=${conflictingOutbound}. ${scenario.details}`
189
+ : `Probe binary=${probeBinaryPath}; compiled process_name=${processName}; conflicting fallback=${conflictingOutbound}. ${scenario.details}`,
190
+ });
191
+ }
192
+ catch (error) {
193
+ results.push({
194
+ app: check.app,
195
+ status: "FAIL",
196
+ expectedOutboundGroup: check.expectedOutboundGroup,
197
+ processName,
198
+ details: error instanceof Error ? error.message : String(error),
199
+ });
200
+ }
201
+ finally {
202
+ child.kill("SIGINT");
203
+ await Promise.race([exitPromise, new Promise((resolve) => setTimeout(resolve, 2_000))]);
204
+ }
205
+ }
206
+ return results;
207
+ }
208
+ export function assertNativeProcessE2EPassed(results) {
209
+ const failures = results.filter((result) => result.status === "FAIL");
210
+ if (failures.length === 0) {
211
+ return;
212
+ }
213
+ throw new Error(`Native-process e2e failed:\n${failures
214
+ .map((result) => `- ${result.app}${result.processName ? ` (${result.processName})` : ""}: ${result.details}`)
215
+ .join("\n")}`);
216
+ }
101
217
  function hasExpectedProcessAwareRoute(rules, check) {
102
218
  switch (check.expectedCaptureBackend) {
103
219
  case "fallback":
@@ -318,6 +434,52 @@ async function verifyRuntimeScenario(scenario, logBuffer, requestBinary) {
318
434
  expectedOutboundTag: scenario.expectedOutboundTag,
319
435
  };
320
436
  }
437
+ async function verifyNativeProcessRuntimeScenario(scenario, logBuffer) {
438
+ const hostname = new URL(scenario.url).hostname;
439
+ const inboundLog = new RegExp(`inbound/mixed\\[${escapeRegExp(scenario.inboundTag)}\\]: inbound connection to ${escapeRegExp(hostname)}:443`);
440
+ const routeLogAlternatives = [
441
+ new RegExp(`router: match\\[\\d+\\].*process_path[^\\n]*${escapeRegExp(scenario.probeBinaryPath)}[^\\n]*=> route\\(${escapeRegExp(scenario.expectedOutboundGroup)}\\)`),
442
+ new RegExp(`router: match\\[\\d+\\].*process_path[^\\n]*${escapeRegExp(path.basename(scenario.probeBinaryPath))}[^\\n]*=> route\\(${escapeRegExp(scenario.expectedOutboundGroup)}\\)`),
443
+ new RegExp(`router: match\\[\\d+\\].*process_name[^\\n]*${escapeRegExp(scenario.processName)}[^\\n]*=> route\\(${escapeRegExp(scenario.expectedOutboundGroup)}\\)`),
444
+ ];
445
+ let lastFailure = `Expected native-process logs were not observed for ${hostname} within the timeout.`;
446
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
447
+ const offset = logBuffer.text.length;
448
+ const requestResult = await runNativeProcessProbeScenario({
449
+ probeBinaryPath: scenario.probeBinaryPath,
450
+ proxyPort: scenario.proxyPort,
451
+ targetHost: hostname,
452
+ targetPort: 443,
453
+ });
454
+ const excerpt = await waitForScenarioLogsWithAlternatives(logBuffer, offset, [inboundLog], routeLogAlternatives, 20_000);
455
+ const requestFailure = detectRequestFailure(requestResult);
456
+ if (excerpt !== undefined) {
457
+ return {
458
+ name: scenario.name,
459
+ passed: true,
460
+ details: requestFailure === undefined
461
+ ? excerpt.trim()
462
+ : `${excerpt.trim()}\nRequest note: ${requestFailure}`,
463
+ url: scenario.url,
464
+ inboundTag: scenario.inboundTag,
465
+ expectedOutboundTag: scenario.expectedOutboundGroup,
466
+ };
467
+ }
468
+ lastFailure =
469
+ requestFailure ?? `Expected native-process logs were not observed for ${hostname} within the timeout.`;
470
+ if (attempt < 2) {
471
+ await sleep(500);
472
+ }
473
+ }
474
+ return {
475
+ name: scenario.name,
476
+ passed: false,
477
+ details: lastFailure,
478
+ url: scenario.url,
479
+ inboundTag: scenario.inboundTag,
480
+ expectedOutboundTag: scenario.expectedOutboundGroup,
481
+ };
482
+ }
321
483
  export function isRouteLevelProxySuccess(scenario, requestResult) {
322
484
  if (scenario.inboundTag !== "in-proxifier") {
323
485
  return false;
@@ -385,6 +547,22 @@ export async function resolveCurlBinary(explicitPath) {
385
547
  }
386
548
  throw new Error("Unable to find a usable curl binary for route verification.");
387
549
  }
550
+ export async function resolveCCompilerBinary(explicitPath) {
551
+ const candidates = [
552
+ explicitPath,
553
+ process.env.CC,
554
+ "/usr/bin/cc",
555
+ "/usr/bin/clang",
556
+ ...resolvePathCandidates("cc"),
557
+ ...resolvePathCandidates("clang"),
558
+ ].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
559
+ for (const candidate of candidates) {
560
+ if (await isExecutable(candidate)) {
561
+ return candidate;
562
+ }
563
+ }
564
+ throw new Error("Unable to find a usable C compiler for native-process verification.");
565
+ }
388
566
  export async function prepareVerificationConfig(config) {
389
567
  const cloned = structuredClone(config);
390
568
  const mixedPort = await findAvailablePort();
@@ -431,9 +609,16 @@ export async function prepareVerificationConfig(config) {
431
609
  tag: "dns-local-verify",
432
610
  },
433
611
  ];
612
+ dns.rules = [];
434
613
  dns.final = "dns-local-verify";
614
+ dns.reverse_mapping = false;
435
615
  const route = asObject(cloned.route, "Config is missing route.");
436
616
  route.default_domain_resolver = "dns-local-verify";
617
+ if (typeof cloned.experimental === "object" && cloned.experimental !== null) {
618
+ const experimental = asObject(cloned.experimental, "Config has an invalid experimental block.");
619
+ const { cache_file: _cacheFile, ...remaining } = experimental;
620
+ cloned.experimental = Object.keys(remaining).length === 0 ? undefined : remaining;
621
+ }
437
622
  const outbounds = ensureArray(cloned.outbounds, "Config is missing outbounds.");
438
623
  const globalIndex = outbounds.findIndex((outbound) => outbound.tag === "Global");
439
624
  if (globalIndex >= 0) {
@@ -455,6 +640,59 @@ export async function prepareVerificationConfig(config) {
455
640
  proxifierPort,
456
641
  };
457
642
  }
643
+ export function chooseConflictingOutbound(config, expectedOutboundGroup) {
644
+ const outbounds = ensureArray(config.outbounds, "Config is missing outbounds.");
645
+ const preferred = ["direct", "Global", "AI-Out", "Dev-Common-Out", "Stitch-Out", "Process-Proxy"];
646
+ for (const tag of preferred) {
647
+ if (tag === expectedOutboundGroup) {
648
+ continue;
649
+ }
650
+ if (outbounds.some((outbound) => outbound.tag === tag)) {
651
+ return tag;
652
+ }
653
+ }
654
+ return outbounds
655
+ .map((outbound) => (typeof outbound.tag === "string" ? outbound.tag : undefined))
656
+ .find((tag) => typeof tag === "string" && tag !== expectedOutboundGroup);
657
+ }
658
+ export function injectNativeProcessProbeRule(config, conflictingOutbound) {
659
+ const route = asObject(config.route, "Config is missing route.");
660
+ const rules = ensureArray(route.rules, "Route is missing rules.");
661
+ const probeHost = new URL(nativeProcessProbeUrl).hostname;
662
+ const insertionIndex = findLastProcessAwareRuleIndex(rules) + 1;
663
+ rules.splice(insertionIndex, 0, {
664
+ domain: [probeHost],
665
+ action: "route",
666
+ outbound: conflictingOutbound,
667
+ });
668
+ }
669
+ export function injectNativeProcessPathProbeRule(config, probeBinaryPath, expectedOutboundGroup) {
670
+ const route = asObject(config.route, "Config is missing route.");
671
+ const rules = ensureArray(route.rules, "Route is missing rules.");
672
+ const insertionIndex = findLastProcessAwareRuleIndex(rules) + 1;
673
+ rules.splice(insertionIndex, 0, {
674
+ process_path: [probeBinaryPath],
675
+ action: "route",
676
+ outbound: expectedOutboundGroup,
677
+ });
678
+ }
679
+ export function selectNativeProcessProbeName(config, expectedOutboundGroup) {
680
+ const route = asObject(config.route, "Config is missing route.");
681
+ const rules = ensureArray(route.rules, "Route is missing rules.");
682
+ for (const rule of rules) {
683
+ if (rule.outbound !== expectedOutboundGroup || !Array.isArray(rule.process_name)) {
684
+ continue;
685
+ }
686
+ const candidate = rule.process_name.find((value) => typeof value === "string" &&
687
+ value.length > 0 &&
688
+ !value.includes("/") &&
689
+ !value.includes("*"));
690
+ if (candidate) {
691
+ return candidate;
692
+ }
693
+ }
694
+ return undefined;
695
+ }
458
696
  export function validateConfigInvariants(config) {
459
697
  const route = asObject(config.route, "Config is missing route.");
460
698
  const rules = ensureArray(route.rules, "Route is missing rules.");
@@ -526,6 +764,16 @@ export function validateConfigInvariants(config) {
526
764
  .join(", ")}`));
527
765
  return checks;
528
766
  }
767
+ function findLastProcessAwareRuleIndex(rules) {
768
+ return rules.reduce((lastIndex, rule, index) => Array.isArray(rule.process_name) ||
769
+ Array.isArray(rule.process_path) ||
770
+ Array.isArray(rule.process_path_regex)
771
+ ? index
772
+ : lastIndex, -1);
773
+ }
774
+ function slugifyProcessName(processName) {
775
+ return processName.replaceAll(/[^A-Za-z0-9._-]+/g, "-");
776
+ }
529
777
  export function resolveDefaultLeafOutboundTag(config, tag) {
530
778
  const outbounds = ensureArray(config.outbounds, "Config is missing outbounds.");
531
779
  const byTag = new Map();
@@ -562,20 +810,44 @@ export function resolveDefaultLeafOutboundTag(config, tag) {
562
810
  }
563
811
  }
564
812
  function buildRuntimeScenarios(config, mixedPort, proxifierPort, configuredScenarios) {
565
- const sourceScenarios = configuredScenarios && configuredScenarios.length > 0
813
+ const sourceScenarios = normalizeConfiguredVerificationScenarios(config, configuredScenarios && configuredScenarios.length > 0
566
814
  ? configuredScenarios
567
- : defaultConfiguredScenarios;
568
- return sourceScenarios.map((scenario) => ({
815
+ : defaultConfiguredScenarios);
816
+ const fallbackScenarios = sourceScenarios.length > 0
817
+ ? sourceScenarios
818
+ : normalizeConfiguredVerificationScenarios(config, defaultConfiguredScenarios);
819
+ return fallbackScenarios.map((scenario) => ({
569
820
  id: scenario.id,
570
821
  name: scenario.name,
571
822
  url: scenario.url,
572
823
  inboundTag: scenario.inbound,
573
824
  proxyPort: scenario.inbound === "in-proxifier" ? proxifierPort : mixedPort,
574
- expectedOutboundTag: scenario.expectedOutbound === "direct" || scenario.expectedOutbound === "block"
575
- ? scenario.expectedOutbound
576
- : resolveDefaultLeafOutboundTag(config, scenario.expectedOutbound),
825
+ expectedOutboundTag: resolveExpectedVerificationOutboundTag(config, scenario.expectedOutbound),
577
826
  }));
578
827
  }
828
+ export function resolveExpectedVerificationOutboundTag(config, expectedOutboundTag) {
829
+ return expectedOutboundTag === "direct" || expectedOutboundTag === "block"
830
+ ? expectedOutboundTag
831
+ : resolveDefaultLeafOutboundTag(config, expectedOutboundTag);
832
+ }
833
+ export function normalizeConfiguredVerificationScenarios(config, scenarios) {
834
+ const availableInbounds = new Set(ensureArray(config.inbounds, "Config is missing inbounds.")
835
+ .map((inbound) => inbound.tag)
836
+ .filter((tag) => typeof tag === "string" && tag.length > 0));
837
+ const hasTun = availableInbounds.has("in-tun");
838
+ const hasMixed = availableInbounds.has("in-mixed");
839
+ const hasProxifier = availableInbounds.has("in-proxifier");
840
+ const prefersTunFallback = hasTun && !hasMixed && !hasProxifier;
841
+ return scenarios.flatMap((scenario) => {
842
+ if (availableInbounds.has(scenario.inbound)) {
843
+ return [scenario];
844
+ }
845
+ if (scenario.inbound === "in-mixed" && (hasTun || prefersTunFallback)) {
846
+ return [{ ...scenario, inbound: "in-tun" }];
847
+ }
848
+ return [];
849
+ });
850
+ }
579
851
  const defaultConfiguredScenarios = [
580
852
  {
581
853
  id: "stitch",
@@ -632,11 +904,49 @@ async function runProxyRequestScenario(input) {
632
904
  "5",
633
905
  "--proxy",
634
906
  `http://127.0.0.1:${input.proxyPort}`,
907
+ "--noproxy",
908
+ "",
635
909
  input.url,
636
910
  ];
637
911
  return new Promise((resolve, reject) => {
638
912
  const child = spawn(input.requestBinary, args, {
639
913
  stdio: ["ignore", "pipe", "pipe"],
914
+ env: buildVerificationRequestEnv(),
915
+ });
916
+ let stdout = "";
917
+ let stderr = "";
918
+ let timedOut = false;
919
+ const timeout = setTimeout(() => {
920
+ timedOut = true;
921
+ child.kill("SIGKILL");
922
+ }, 12_000);
923
+ child.stdout.on("data", (chunk) => {
924
+ stdout += chunk.toString();
925
+ });
926
+ child.stderr.on("data", (chunk) => {
927
+ stderr += chunk.toString();
928
+ });
929
+ child.on("error", (error) => {
930
+ clearTimeout(timeout);
931
+ reject(error);
932
+ });
933
+ child.on("close", (code) => {
934
+ clearTimeout(timeout);
935
+ resolve({
936
+ exitCode: code ?? 0,
937
+ stdout,
938
+ stderr,
939
+ timedOut,
940
+ });
941
+ });
942
+ });
943
+ }
944
+ async function runNativeProcessProbeScenario(input) {
945
+ const args = ["127.0.0.1", String(input.proxyPort), input.targetHost, String(input.targetPort)];
946
+ return new Promise((resolve, reject) => {
947
+ const child = spawn(input.probeBinaryPath, args, {
948
+ stdio: ["ignore", "pipe", "pipe"],
949
+ env: buildVerificationRequestEnv(),
640
950
  });
641
951
  let stdout = "";
642
952
  let stderr = "";
@@ -681,11 +991,12 @@ async function runJsonRequestScenario(input) {
681
991
  input.url,
682
992
  ];
683
993
  if (typeof input.proxyPort === "number") {
684
- args.splice(args.length - 1, 0, "--proxy", `http://127.0.0.1:${input.proxyPort}`);
994
+ args.splice(args.length - 1, 0, "--proxy", `http://127.0.0.1:${input.proxyPort}`, "--noproxy", "");
685
995
  }
686
996
  return new Promise((resolve, reject) => {
687
997
  const child = spawn(input.requestBinary, args, {
688
998
  stdio: ["ignore", "pipe", "pipe"],
999
+ env: buildVerificationRequestEnv(),
689
1000
  });
690
1001
  let stdout = "";
691
1002
  let stderr = "";
@@ -805,6 +1116,18 @@ async function waitForScenarioLogs(buffer, offset, patterns, timeoutMs) {
805
1116
  }
806
1117
  return undefined;
807
1118
  }
1119
+ async function waitForScenarioLogsWithAlternatives(buffer, offset, requiredPatterns, alternativePatterns, timeoutMs) {
1120
+ const startedAt = Date.now();
1121
+ while (Date.now() - startedAt < timeoutMs) {
1122
+ const excerpt = buffer.text.slice(offset);
1123
+ if (requiredPatterns.every((pattern) => pattern.test(excerpt)) &&
1124
+ alternativePatterns.some((pattern) => pattern.test(excerpt))) {
1125
+ return excerpt;
1126
+ }
1127
+ await sleep(250);
1128
+ }
1129
+ return undefined;
1130
+ }
808
1131
  async function waitForLog(buffer, pattern, timeoutMs, errorMessage) {
809
1132
  const startedAt = Date.now();
810
1133
  while (Date.now() - startedAt < timeoutMs) {
@@ -855,6 +1178,124 @@ function makeCheck(name, passed, details) {
855
1178
  function escapeRegExp(input) {
856
1179
  return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
857
1180
  }
1181
+ export function buildVerificationRequestEnv(baseEnv = process.env) {
1182
+ const stripped = {};
1183
+ for (const [key, value] of Object.entries(baseEnv)) {
1184
+ if (/^(http_proxy|https_proxy|all_proxy|no_proxy)$/i.test(key)) {
1185
+ continue;
1186
+ }
1187
+ stripped[key] = value;
1188
+ }
1189
+ return stripped;
1190
+ }
1191
+ async function compileNativeProcessProbe(input) {
1192
+ const sourcePath = path.join(input.runDir, "native-process-probe.c");
1193
+ await writeFile(sourcePath, buildNativeProcessProbeSource(), "utf8");
1194
+ await new Promise((resolve, reject) => {
1195
+ const child = spawn(input.compilerBinary, ["-O2", "-Wall", "-Wextra", "-o", input.probeBinaryPath, sourcePath], {
1196
+ stdio: ["ignore", "pipe", "pipe"],
1197
+ });
1198
+ let stderr = "";
1199
+ child.stdout.on("data", () => {
1200
+ // ignore compiler stdout
1201
+ });
1202
+ child.stderr.on("data", (chunk) => {
1203
+ stderr += chunk.toString();
1204
+ });
1205
+ child.on("error", reject);
1206
+ child.on("close", (code) => {
1207
+ if ((code ?? 0) === 0) {
1208
+ resolve();
1209
+ return;
1210
+ }
1211
+ reject(new Error(`Failed to compile native-process probe with ${input.compilerBinary}.\n${stderr.trim()}`));
1212
+ });
1213
+ });
1214
+ await chmod(input.probeBinaryPath, 0o755);
1215
+ }
1216
+ function buildNativeProcessProbeSource() {
1217
+ return `#include <arpa/inet.h>
1218
+ #include <errno.h>
1219
+ #include <netinet/in.h>
1220
+ #include <stdio.h>
1221
+ #include <stdlib.h>
1222
+ #include <stdint.h>
1223
+ #include <string.h>
1224
+ #include <sys/socket.h>
1225
+ #include <sys/time.h>
1226
+ #include <unistd.h>
1227
+
1228
+ int main(int argc, char **argv) {
1229
+ if (argc != 5) {
1230
+ fprintf(stderr, "usage: %s <proxy-host> <proxy-port> <target-host> <target-port>\\n", argv[0]);
1231
+ return 2;
1232
+ }
1233
+
1234
+ const char *proxyHost = argv[1];
1235
+ const char *proxyPort = argv[2];
1236
+ const char *targetHost = argv[3];
1237
+ const char *targetPort = argv[4];
1238
+
1239
+ int socketFd = socket(AF_INET, SOCK_STREAM, 0);
1240
+ if (socketFd < 0) {
1241
+ perror("socket");
1242
+ return 3;
1243
+ }
1244
+
1245
+ struct timeval timeout;
1246
+ timeout.tv_sec = 3;
1247
+ timeout.tv_usec = 0;
1248
+ setsockopt(socketFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
1249
+ setsockopt(socketFd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
1250
+
1251
+ struct sockaddr_in proxyAddress;
1252
+ memset(&proxyAddress, 0, sizeof(proxyAddress));
1253
+ proxyAddress.sin_family = AF_INET;
1254
+ proxyAddress.sin_port = htons((uint16_t) atoi(proxyPort));
1255
+
1256
+ if (inet_pton(AF_INET, proxyHost, &proxyAddress.sin_addr) != 1) {
1257
+ fprintf(stderr, "invalid proxy host: %s\\n", proxyHost);
1258
+ close(socketFd);
1259
+ return 4;
1260
+ }
1261
+
1262
+ if (connect(socketFd, (struct sockaddr *) &proxyAddress, sizeof(proxyAddress)) != 0) {
1263
+ perror("connect");
1264
+ close(socketFd);
1265
+ return 5;
1266
+ }
1267
+
1268
+ char request[1024];
1269
+ int written = snprintf(
1270
+ request,
1271
+ sizeof(request),
1272
+ "CONNECT %s:%s HTTP/1.1\\r\\nHost: %s:%s\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n",
1273
+ targetHost,
1274
+ targetPort,
1275
+ targetHost,
1276
+ targetPort
1277
+ );
1278
+ if (written < 0 || written >= (int) sizeof(request)) {
1279
+ fprintf(stderr, "failed to encode request\\n");
1280
+ close(socketFd);
1281
+ return 6;
1282
+ }
1283
+
1284
+ ssize_t sent = send(socketFd, request, (size_t) written, 0);
1285
+ if (sent < 0) {
1286
+ perror("send");
1287
+ close(socketFd);
1288
+ return 7;
1289
+ }
1290
+
1291
+ char response[512];
1292
+ recv(socketFd, response, sizeof(response) - 1, 0);
1293
+ usleep(250000);
1294
+ close(socketFd);
1295
+ return 0;
1296
+ }
1297
+ `;
1298
+ }
858
1299
  async function isExecutable(filePath) {
859
1300
  try {
860
1301
  await access(filePath, constants.X_OK);