peachsim 0.1.1469 → 0.1.1481
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/cli/commands/control.ts +68 -42
- package/cli/commands/detox.ts +15 -10
- package/cli/commands/flow.ts +1 -2
- package/cli/commands/inspect/settling.ts +5 -1
- package/cli/commands/setup.ts +20 -13
- package/cli/commands/test.ts +2 -1
- package/cli/dev-build-freshness.ts +14 -14
- package/cli/drivers/playwright-sim-host.ts +7 -2
- package/cli/drivers/playwright.ts +6 -6
- package/cli/outbound-endpoints.ts +80 -6
- package/detox/index.ts +27 -14
- package/detox/matchers.ts +17 -6
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +54 -15
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +19 -3
- package/dist-lib/config.mjs +19 -3
- package/dist-lib/detox/index.cjs +125 -52
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +30 -9
- package/dist-lib/expo-swift-metadata.cjs +18 -6
- package/dist-lib/expo-swift-metadata.mjs +18 -6
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +621 -390
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +46 -19
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +24 -6
- package/dist-lib/metro-production-bundle.mjs +22 -6
- package/dist-lib/metro.cjs +41 -18
- package/dist-lib/native-app-identity-project.cjs +1 -1
- package/dist-lib/native-app-identity-project.mjs +1 -1
- package/dist-lib/native-app-identity.cjs +19 -5
- package/dist-lib/native-app-identity.mjs +19 -5
- package/dist-lib/native-firebase-metadata.cjs +22 -7
- package/dist-lib/native-firebase-metadata.mjs +20 -7
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +3 -1
- package/dist-lib/react-native-host-modules.mjs +3 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +404 -279
- package/dist-lib/sdk.cjs +6 -2
- package/dist-lib/sdk.mjs +6 -2
- package/dist-lib/skills.cjs +757 -531
- package/dist-lib/swift-compile.cjs +7 -2
- package/dist-lib/swift-compile.mjs +7 -2
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +7 -2
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +27 -7
- package/dist-lib/worklet-executable.mjs +27 -7
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +493 -188
- package/src/admitted-mac-build.ts +12 -0
- package/src/bridge-constants.ts +5 -3
- package/src/bundleGlobalFooter.ts +26 -3
- package/src/config.ts +3 -2
- package/src/configured-bundle-producer.ts +2 -1
- package/src/expo-swift-host.ts +33 -7
- package/src/host/bridge-host.ts +34 -6
- package/src/metro-production-bundle.ts +11 -4
- package/src/native-app-identity.ts +3 -2
- package/src/native-firebase-metadata.ts +5 -3
- package/src/react-native-host-modules.ts +2 -0
- package/src/swift-compile.ts +5 -4
- package/src/worker-system-chunks.ts +61 -58
- package/src/worklet-executable.ts +38 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1481 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -280,6 +280,7 @@ __export(dev_server_scanner_exports, {
|
|
|
280
280
|
__shouldReuseScannerCacheEntry: () => __shouldReuseScannerCacheEntry,
|
|
281
281
|
discoverListeningPorts: () => discoverListeningPorts,
|
|
282
282
|
discoverListeningProcesses: () => discoverListeningProcesses,
|
|
283
|
+
pinSharedListenerScan: () => pinSharedListenerScan,
|
|
283
284
|
probePort: () => probePort,
|
|
284
285
|
resolveListeningPid: () => resolveListeningPid,
|
|
285
286
|
resolveProcessCwd: () => resolveProcessCwd,
|
|
@@ -292,161 +293,9 @@ var import_promises2 = require("fs/promises");
|
|
|
292
293
|
var import_http = __toESM(require("http"), 1);
|
|
293
294
|
var import_net = __toESM(require("net"), 1);
|
|
294
295
|
var import_node_fs2 = require("node:fs");
|
|
296
|
+
var import_node_os = require("node:os");
|
|
295
297
|
var import_node_path3 = require("node:path");
|
|
296
298
|
|
|
297
|
-
// ../contrast/src/runtime/process-group.ts
|
|
298
|
-
var import_node_child_process = require("node:child_process");
|
|
299
|
-
|
|
300
|
-
// ../contrast/src/sleep.ts
|
|
301
|
-
function sleep(ms) {
|
|
302
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// ../contrast/src/runtime/process-group.ts
|
|
306
|
-
var GRACEFUL_TIMEOUT_MS = 3e3;
|
|
307
|
-
var FORCED_EXIT_TIMEOUT_MS = 1e3;
|
|
308
|
-
function signalProcessGroup(child, signal) {
|
|
309
|
-
if (!child.pid) return;
|
|
310
|
-
try {
|
|
311
|
-
process.kill(-child.pid, signal);
|
|
312
|
-
} catch {
|
|
313
|
-
try {
|
|
314
|
-
process.kill(child.pid, signal);
|
|
315
|
-
} catch {
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
function runningPids(pids) {
|
|
320
|
-
return pids.filter((pid) => {
|
|
321
|
-
try {
|
|
322
|
-
process.kill(pid, 0);
|
|
323
|
-
return true;
|
|
324
|
-
} catch (error) {
|
|
325
|
-
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
function groupIsEmpty(pgid) {
|
|
330
|
-
try {
|
|
331
|
-
process.kill(-pgid, 0);
|
|
332
|
-
} catch (error) {
|
|
333
|
-
if (!(error instanceof Error && "code" in error && error.code === "EPERM"))
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
const result = (0, import_node_child_process.spawnSync)("ps", ["-A", "-o", "pgid=,stat="], {
|
|
337
|
-
encoding: "utf8",
|
|
338
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
339
|
-
});
|
|
340
|
-
if (result.error || result.status !== 0) return false;
|
|
341
|
-
return !result.stdout.split("\n").some((line) => {
|
|
342
|
-
const [group, state] = line.trim().split(/\s+/);
|
|
343
|
-
return Number(group) === pgid && state !== void 0 && !state.startsWith("Z");
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
async function waitForExit(pids, pgid, timeoutMs) {
|
|
347
|
-
const deadline = Date.now() + timeoutMs;
|
|
348
|
-
const gone = () => runningPids(pids).length === 0 && groupIsEmpty(pgid);
|
|
349
|
-
while (Date.now() < deadline) {
|
|
350
|
-
if (gone()) return true;
|
|
351
|
-
await sleep(50);
|
|
352
|
-
}
|
|
353
|
-
return gone();
|
|
354
|
-
}
|
|
355
|
-
async function terminateProcessGroup(child, signal = "SIGTERM", gracefulTimeoutMs = GRACEFUL_TIMEOUT_MS) {
|
|
356
|
-
if (!child.pid) {
|
|
357
|
-
try {
|
|
358
|
-
child.kill(signal);
|
|
359
|
-
} catch {
|
|
360
|
-
}
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
signalProcessGroup(child, "SIGSTOP");
|
|
364
|
-
const descendants = /* @__PURE__ */ new Set();
|
|
365
|
-
let parents = [child.pid];
|
|
366
|
-
let inspectionError = null;
|
|
367
|
-
while (parents.length > 0) {
|
|
368
|
-
const result = (0, import_node_child_process.spawnSync)("pgrep", ["-P", parents.join(",")], {
|
|
369
|
-
encoding: "utf8",
|
|
370
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
371
|
-
});
|
|
372
|
-
if (result.error || result.status !== 0 && result.status !== 1) {
|
|
373
|
-
inspectionError = new Error(
|
|
374
|
-
`could not inspect descendants of processes ${parents.join(",")}: ${result.error?.message || result.stderr.trim() || `pgrep exited ${result.status}`}`
|
|
375
|
-
);
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
const next = [];
|
|
379
|
-
for (const line of result.stdout.split("\n")) {
|
|
380
|
-
const descendantPid = Number(line.trim());
|
|
381
|
-
if (Number.isInteger(descendantPid) && descendantPid > 1 && !descendants.has(descendantPid)) {
|
|
382
|
-
descendants.add(descendantPid);
|
|
383
|
-
next.push(descendantPid);
|
|
384
|
-
try {
|
|
385
|
-
process.kill(descendantPid, "SIGSTOP");
|
|
386
|
-
} catch {
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
parents = next;
|
|
391
|
-
}
|
|
392
|
-
const pids = [child.pid, ...descendants];
|
|
393
|
-
const gracefulDeadline = Date.now() + gracefulTimeoutMs;
|
|
394
|
-
try {
|
|
395
|
-
process.kill(child.pid, signal);
|
|
396
|
-
} catch {
|
|
397
|
-
}
|
|
398
|
-
signalProcessGroup(child, "SIGCONT");
|
|
399
|
-
for (const pid of descendants) {
|
|
400
|
-
try {
|
|
401
|
-
process.kill(pid, "SIGCONT");
|
|
402
|
-
} catch {
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
let rootExited = child.exitCode !== null || child.signalCode !== null;
|
|
406
|
-
if (!rootExited) {
|
|
407
|
-
rootExited = await new Promise((resolveExit) => {
|
|
408
|
-
const onExit = () => {
|
|
409
|
-
clearTimeout(deadlineTimer);
|
|
410
|
-
resolveExit(true);
|
|
411
|
-
};
|
|
412
|
-
const deadlineTimer = setTimeout(
|
|
413
|
-
() => {
|
|
414
|
-
child.removeListener("exit", onExit);
|
|
415
|
-
resolveExit(false);
|
|
416
|
-
},
|
|
417
|
-
Math.max(0, gracefulDeadline - Date.now())
|
|
418
|
-
);
|
|
419
|
-
child.once("exit", onExit);
|
|
420
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
421
|
-
child.removeListener("exit", onExit);
|
|
422
|
-
onExit();
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
if (rootExited) {
|
|
427
|
-
signalProcessGroup(child, signal);
|
|
428
|
-
for (const pid of descendants) {
|
|
429
|
-
try {
|
|
430
|
-
process.kill(pid, signal);
|
|
431
|
-
} catch {
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
if (await waitForExit(pids, child.pid, Math.max(0, gracefulDeadline - Date.now()))) {
|
|
436
|
-
if (inspectionError) throw inspectionError;
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
for (const pid of pids) {
|
|
440
|
-
try {
|
|
441
|
-
process.kill(pid, "SIGKILL");
|
|
442
|
-
} catch {
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
signalProcessGroup(child, "SIGKILL");
|
|
446
|
-
await waitForExit(pids, child.pid, FORCED_EXIT_TIMEOUT_MS);
|
|
447
|
-
if (inspectionError) throw inspectionError;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
299
|
// src/backend-origin.ts
|
|
451
300
|
var LOOPBACK_PROBE_HOSTNAMES = ["127.0.0.1", "::1"];
|
|
452
301
|
function normalizeHostname(hostname) {
|
|
@@ -990,52 +839,211 @@ function optionalDemoAppsSync() {
|
|
|
990
839
|
}
|
|
991
840
|
|
|
992
841
|
// scripts/dev-server-scanner.ts
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
};
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
842
|
+
var LISTENER_LOCK_PATH = (0, import_node_path3.join)((0, import_node_os.tmpdir)(), "peach-listener-scan.lock");
|
|
843
|
+
var LISTENER_RESULT_PATH = (0, import_node_path3.join)((0, import_node_os.tmpdir)(), "peach-listener-scan.result");
|
|
844
|
+
var LISTENER_LOCK_STALE_MS = 25e3;
|
|
845
|
+
var LISTENER_LOCK_WAIT_MS = 8e3;
|
|
846
|
+
function sleep(ms) {
|
|
847
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
848
|
+
}
|
|
849
|
+
function pidIsAlive(pid) {
|
|
850
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
851
|
+
try {
|
|
852
|
+
process.kill(pid, 0);
|
|
853
|
+
return true;
|
|
854
|
+
} catch (error) {
|
|
855
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
856
|
+
return code !== "ESRCH";
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function readLockOwner() {
|
|
860
|
+
try {
|
|
861
|
+
const raw = (0, import_node_fs2.readFileSync)(LISTENER_LOCK_PATH, "utf8");
|
|
862
|
+
const mtimeMs = (0, import_node_fs2.statSync)(LISTENER_LOCK_PATH).mtimeMs;
|
|
863
|
+
const pid = Number(raw.split("-")[0]);
|
|
864
|
+
return { raw, pid: Number.isInteger(pid) ? pid : -1, mtimeMs };
|
|
865
|
+
} catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function lockIsAbandoned(owner) {
|
|
870
|
+
if (owner.pid <= 0 || !pidIsAlive(owner.pid)) return true;
|
|
871
|
+
return Date.now() - owner.mtimeMs > LISTENER_LOCK_STALE_MS;
|
|
872
|
+
}
|
|
873
|
+
function clearLock(raw) {
|
|
874
|
+
try {
|
|
875
|
+
if ((0, import_node_fs2.readFileSync)(LISTENER_LOCK_PATH, "utf8") !== raw) return;
|
|
876
|
+
(0, import_node_fs2.unlinkSync)(LISTENER_LOCK_PATH);
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function readResult() {
|
|
881
|
+
try {
|
|
882
|
+
const parsed = JSON.parse(
|
|
883
|
+
(0, import_node_fs2.readFileSync)(LISTENER_RESULT_PATH, "utf8")
|
|
884
|
+
);
|
|
885
|
+
if (!parsed || typeof parsed.token !== "string" || typeof parsed.key !== "string")
|
|
886
|
+
return null;
|
|
887
|
+
return parsed;
|
|
888
|
+
} catch {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function publishResult(payload) {
|
|
893
|
+
const tmp = `${LISTENER_RESULT_PATH}.${process.pid}.tmp`;
|
|
894
|
+
(0, import_node_fs2.writeFileSync)(tmp, JSON.stringify(payload));
|
|
895
|
+
(0, import_node_fs2.renameSync)(tmp, LISTENER_RESULT_PATH);
|
|
896
|
+
}
|
|
897
|
+
function throwPublished(payload) {
|
|
898
|
+
throw Object.assign(new Error(payload.message || "listener scan failed"), {
|
|
899
|
+
killed: payload.killed === true
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
async function waitForSharedResult(owner, key) {
|
|
903
|
+
const cap = Date.now() + LISTING_TIMEOUT_MS;
|
|
904
|
+
let current = owner;
|
|
905
|
+
while (Date.now() < cap) {
|
|
906
|
+
const published = readResult();
|
|
907
|
+
if (published && published.token === current.raw) {
|
|
908
|
+
if (published.key !== key) return { kind: "retry" };
|
|
909
|
+
if (!published.ok) throwPublished(published);
|
|
910
|
+
return { kind: "value", value: published.value };
|
|
911
|
+
}
|
|
912
|
+
const latest = readLockOwner();
|
|
913
|
+
if (!latest) {
|
|
914
|
+
const again = readResult();
|
|
915
|
+
if (again && again.token === current.raw && again.key === key) {
|
|
916
|
+
if (!again.ok) throwPublished(again);
|
|
917
|
+
return { kind: "value", value: again.value };
|
|
1028
918
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
919
|
+
return { kind: "retry" };
|
|
920
|
+
}
|
|
921
|
+
if (latest.raw !== current.raw) current = latest;
|
|
922
|
+
if (lockIsAbandoned(latest)) {
|
|
923
|
+
clearLock(latest.raw);
|
|
924
|
+
return { kind: "retry" };
|
|
925
|
+
}
|
|
926
|
+
await sleep(25);
|
|
927
|
+
}
|
|
928
|
+
throw Object.assign(new Error("listener scan lock was held past the wait"), {
|
|
929
|
+
killed: true
|
|
1037
930
|
});
|
|
1038
931
|
}
|
|
932
|
+
async function withListenerLock(key, body) {
|
|
933
|
+
const token = `${process.pid}-${Date.now()}-${Math.random()}`;
|
|
934
|
+
const deadline = Date.now() + LISTENER_LOCK_WAIT_MS;
|
|
935
|
+
for (; ; ) {
|
|
936
|
+
let fd = null;
|
|
937
|
+
try {
|
|
938
|
+
fd = (0, import_node_fs2.openSync)(LISTENER_LOCK_PATH, "wx");
|
|
939
|
+
} catch (error) {
|
|
940
|
+
const code = error instanceof Error && "code" in error ? error.code : "";
|
|
941
|
+
if (code !== "EEXIST") throw error;
|
|
942
|
+
const owner = readLockOwner();
|
|
943
|
+
if (!owner || lockIsAbandoned(owner)) {
|
|
944
|
+
if (owner) clearLock(owner.raw);
|
|
945
|
+
else {
|
|
946
|
+
try {
|
|
947
|
+
(0, import_node_fs2.unlinkSync)(LISTENER_LOCK_PATH);
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (Date.now() >= deadline) {
|
|
954
|
+
const shared = await waitForSharedResult(owner, key);
|
|
955
|
+
if (shared.kind === "value") return shared.value;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
await sleep(25);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (fd === null) continue;
|
|
962
|
+
try {
|
|
963
|
+
(0, import_node_fs2.writeSync)(fd, token);
|
|
964
|
+
try {
|
|
965
|
+
const value = await body();
|
|
966
|
+
publishResult({ token, key, ok: true, value });
|
|
967
|
+
return value;
|
|
968
|
+
} catch (error) {
|
|
969
|
+
publishResult({
|
|
970
|
+
token,
|
|
971
|
+
key,
|
|
972
|
+
ok: false,
|
|
973
|
+
message: error instanceof Error ? error.message : String(error),
|
|
974
|
+
killed: !!(error && typeof error === "object" && error.killed)
|
|
975
|
+
});
|
|
976
|
+
throw error;
|
|
977
|
+
}
|
|
978
|
+
} finally {
|
|
979
|
+
try {
|
|
980
|
+
(0, import_node_fs2.closeSync)(fd);
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
clearLock(token);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function spawnReaped(file, args, timeoutMs, maxBuffer = 1024 * 1024) {
|
|
988
|
+
return withListenerLock(
|
|
989
|
+
`${file}\0${args.join("\0")}`,
|
|
990
|
+
() => new Promise((resolvePromise, reject) => {
|
|
991
|
+
const child = (0, import_child_process.spawn)(file, args, {
|
|
992
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
993
|
+
});
|
|
994
|
+
let stdout = "";
|
|
995
|
+
let settled = false;
|
|
996
|
+
let timedOut = false;
|
|
997
|
+
let overflow = false;
|
|
998
|
+
let spawnError = null;
|
|
999
|
+
const timeoutError = () => Object.assign(new Error(`\`${file}\` did not exit within ${timeoutMs}ms`), {
|
|
1000
|
+
killed: true
|
|
1001
|
+
});
|
|
1002
|
+
const finish = (err, code) => {
|
|
1003
|
+
if (settled) return;
|
|
1004
|
+
settled = true;
|
|
1005
|
+
clearTimeout(deadline);
|
|
1006
|
+
clearTimeout(backup);
|
|
1007
|
+
if (err) reject(err);
|
|
1008
|
+
else resolvePromise({ stdout, code });
|
|
1009
|
+
};
|
|
1010
|
+
const killChild = () => {
|
|
1011
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
1012
|
+
try {
|
|
1013
|
+
child.kill("SIGKILL");
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
let backup;
|
|
1018
|
+
const deadline = setTimeout(() => {
|
|
1019
|
+
timedOut = true;
|
|
1020
|
+
killChild();
|
|
1021
|
+
backup = setTimeout(() => finish(timeoutError(), null), 1e3);
|
|
1022
|
+
}, timeoutMs);
|
|
1023
|
+
child.stdout?.setEncoding("utf8");
|
|
1024
|
+
child.stderr?.on("data", () => {
|
|
1025
|
+
});
|
|
1026
|
+
child.stdout?.on("data", (chunk) => {
|
|
1027
|
+
stdout += chunk;
|
|
1028
|
+
if (stdout.length > maxBuffer) {
|
|
1029
|
+
overflow = true;
|
|
1030
|
+
killChild();
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
child.on("error", (err) => {
|
|
1034
|
+
spawnError = err;
|
|
1035
|
+
killChild();
|
|
1036
|
+
});
|
|
1037
|
+
child.on("close", (code) => {
|
|
1038
|
+
if (timedOut) finish(timeoutError(), code);
|
|
1039
|
+
else if (overflow)
|
|
1040
|
+
finish(new Error(`\`${file}\` exceeded maxBuffer ${maxBuffer}`), code);
|
|
1041
|
+
else if (spawnError && !stdout) finish(spawnError, code);
|
|
1042
|
+
else finish(null, code);
|
|
1043
|
+
});
|
|
1044
|
+
})
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1039
1047
|
var TIMEOUT_MS = 250;
|
|
1040
1048
|
var MANIFEST_TIMEOUT_MS = 1500;
|
|
1041
1049
|
var TCP_GATE_MS = 120;
|
|
@@ -1155,57 +1163,145 @@ async function scanListeningProcesses(excludePorts = []) {
|
|
|
1155
1163
|
processes: listing.processes.filter((p) => acceptPort(p.port, excluded))
|
|
1156
1164
|
};
|
|
1157
1165
|
}
|
|
1166
|
+
var LISTENER_CACHE_MS = 2e3;
|
|
1167
|
+
var listenerCache = null;
|
|
1158
1168
|
var inflightListing = null;
|
|
1169
|
+
var listenerHolds = 0;
|
|
1170
|
+
function pinSharedListenerScan() {
|
|
1171
|
+
listenerHolds += 1;
|
|
1172
|
+
let released = false;
|
|
1173
|
+
return () => {
|
|
1174
|
+
if (released) return;
|
|
1175
|
+
released = true;
|
|
1176
|
+
listenerHolds -= 1;
|
|
1177
|
+
if (listenerHolds === 0) listenerCache = null;
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function cachedPidsAlive(scan) {
|
|
1181
|
+
if (scan.state !== "looked") return true;
|
|
1182
|
+
return scan.processes.every((entry) => entry.pid <= 0 || pidIsAlive(entry.pid));
|
|
1183
|
+
}
|
|
1184
|
+
async function cachedPortsStillHeld(scan) {
|
|
1185
|
+
if (scan.state !== "looked") return true;
|
|
1186
|
+
const owned = scan.processes.filter((entry) => entry.pid > 0);
|
|
1187
|
+
if (owned.length === 0) return true;
|
|
1188
|
+
const pids = [...new Set(owned.map((entry) => entry.pid))];
|
|
1189
|
+
try {
|
|
1190
|
+
const { stdout, code } = await spawnReaped(
|
|
1191
|
+
"lsof",
|
|
1192
|
+
["-nP", "-b", "-w", "-a", "-p", pids.join(","), "-iTCP", "-sTCP:LISTEN", "-Fpcn"],
|
|
1193
|
+
8e3,
|
|
1194
|
+
16 * 1024 * 1024
|
|
1195
|
+
);
|
|
1196
|
+
if (code !== 0 && code !== 1) return false;
|
|
1197
|
+
const live = new Set(
|
|
1198
|
+
parseLsofListeners(stdout).map((entry) => `${entry.port}:${entry.pid}`)
|
|
1199
|
+
);
|
|
1200
|
+
return owned.every((entry) => live.has(`${entry.port}:${entry.pid}`));
|
|
1201
|
+
} catch {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1159
1205
|
function listListenersShared() {
|
|
1160
|
-
inflightListing ??=
|
|
1206
|
+
inflightListing ??= loadListeners().finally(() => {
|
|
1161
1207
|
inflightListing = null;
|
|
1162
1208
|
});
|
|
1163
1209
|
return inflightListing;
|
|
1164
1210
|
}
|
|
1211
|
+
async function loadListeners() {
|
|
1212
|
+
const cached2 = listenerCache;
|
|
1213
|
+
if (cached2 && (listenerHolds > 0 || Date.now() - cached2.at < LISTENER_CACHE_MS) && cachedPidsAlive(cached2.scan) && await cachedPortsStillHeld(cached2.scan)) {
|
|
1214
|
+
return cached2.scan;
|
|
1215
|
+
}
|
|
1216
|
+
listenerCache = null;
|
|
1217
|
+
const scan = await listListeners();
|
|
1218
|
+
listenerCache = { at: Date.now(), scan };
|
|
1219
|
+
return scan;
|
|
1220
|
+
}
|
|
1221
|
+
function isSpawnMissing(err) {
|
|
1222
|
+
return !!(err && typeof err === "object" && err.code === "ENOENT");
|
|
1223
|
+
}
|
|
1224
|
+
function parseLsofListeners(stdout) {
|
|
1225
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1226
|
+
let pid = 0;
|
|
1227
|
+
let command = "";
|
|
1228
|
+
for (const line of stdout.split("\n")) {
|
|
1229
|
+
if (line.startsWith("p")) {
|
|
1230
|
+
pid = Number(line.slice(1));
|
|
1231
|
+
command = "";
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
if (line.startsWith("c")) {
|
|
1235
|
+
command = line.slice(1);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (!line.startsWith("n") || pid <= 0) continue;
|
|
1239
|
+
if (!/^(node|bun|peach|vite)/.test(command)) continue;
|
|
1240
|
+
const match = line.match(/:(\d+)$/);
|
|
1241
|
+
if (!match) continue;
|
|
1242
|
+
const port = Number(match[1]);
|
|
1243
|
+
if (!seen.has(port)) seen.set(port, pid);
|
|
1244
|
+
}
|
|
1245
|
+
return [...seen.entries()].map(([port, owner]) => ({ port, pid: owner }));
|
|
1246
|
+
}
|
|
1247
|
+
function parseSsListeners(stdout) {
|
|
1248
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1249
|
+
for (const line of stdout.split("\n")) {
|
|
1250
|
+
if (!/"(node|bun|peach|vite)/.test(line)) continue;
|
|
1251
|
+
const portMatch = line.match(/:(\d+)\s/);
|
|
1252
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
1253
|
+
if (!portMatch) continue;
|
|
1254
|
+
const port = Number(portMatch[1]);
|
|
1255
|
+
const pid = pidMatch ? Number(pidMatch[1]) : 0;
|
|
1256
|
+
if (!seen.has(port)) seen.set(port, pid);
|
|
1257
|
+
}
|
|
1258
|
+
return [...seen.entries()].map(([port, pid]) => ({ port, pid }));
|
|
1259
|
+
}
|
|
1165
1260
|
async function listListeners() {
|
|
1166
1261
|
let timedOut = false;
|
|
1167
1262
|
try {
|
|
1168
|
-
const { stdout } = await
|
|
1169
|
-
|
|
1170
|
-
|
|
1263
|
+
const { stdout, code } = await spawnReaped(
|
|
1264
|
+
"lsof",
|
|
1265
|
+
[
|
|
1266
|
+
"-nP",
|
|
1267
|
+
"-a",
|
|
1268
|
+
"-c",
|
|
1269
|
+
"node",
|
|
1270
|
+
"-c",
|
|
1271
|
+
"bun",
|
|
1272
|
+
"-c",
|
|
1273
|
+
"peach",
|
|
1274
|
+
"-c",
|
|
1275
|
+
"vite",
|
|
1276
|
+
"-iTCP",
|
|
1277
|
+
"-sTCP:LISTEN",
|
|
1278
|
+
"-Fpcn"
|
|
1279
|
+
],
|
|
1280
|
+
LISTING_TIMEOUT_MS,
|
|
1281
|
+
16 * 1024 * 1024
|
|
1171
1282
|
);
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const parts = line.trim().split(/\s+/);
|
|
1175
|
-
if (parts.length < 9) continue;
|
|
1176
|
-
const pid = Number(parts[1]);
|
|
1177
|
-
const m = parts[8].match(/:(\d+)$/);
|
|
1178
|
-
if (!m) continue;
|
|
1179
|
-
const port = Number(m[1]);
|
|
1180
|
-
if (!seen.has(port)) seen.set(port, pid);
|
|
1283
|
+
if (code === 0 || code === 1) {
|
|
1284
|
+
return { state: "looked", processes: parseLsofListeners(stdout) };
|
|
1181
1285
|
}
|
|
1182
|
-
return {
|
|
1183
|
-
state: "looked",
|
|
1184
|
-
processes: [...seen.entries()].map(([port, pid]) => ({ port, pid }))
|
|
1185
|
-
};
|
|
1186
1286
|
} catch (err) {
|
|
1187
1287
|
if (wasKilledByTimeout(err)) timedOut = true;
|
|
1288
|
+
else if (!isSpawnMissing(err)) return { state: "could-not-look" };
|
|
1188
1289
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
if (
|
|
1290
|
+
if (!timedOut) {
|
|
1291
|
+
try {
|
|
1292
|
+
const { stdout, code } = await spawnReaped(
|
|
1293
|
+
"ss",
|
|
1294
|
+
["-H", "-ltnp"],
|
|
1295
|
+
LISTING_TIMEOUT_MS,
|
|
1296
|
+
16 * 1024 * 1024
|
|
1297
|
+
);
|
|
1298
|
+
if (code === 0 || code === 1) {
|
|
1299
|
+
return { state: "looked", processes: parseSsListeners(stdout) };
|
|
1300
|
+
}
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
if (wasKilledByTimeout(err)) timedOut = true;
|
|
1303
|
+
else if (!isSpawnMissing(err)) return { state: "could-not-look" };
|
|
1202
1304
|
}
|
|
1203
|
-
return {
|
|
1204
|
-
state: "looked",
|
|
1205
|
-
processes: [...seen.entries()].map(([port, pid]) => ({ port, pid }))
|
|
1206
|
-
};
|
|
1207
|
-
} catch (err) {
|
|
1208
|
-
if (wasKilledByTimeout(err)) timedOut = true;
|
|
1209
1305
|
}
|
|
1210
1306
|
if (timedOut) return { state: "could-not-look" };
|
|
1211
1307
|
return { state: "looked", processes: FALLBACK_PORTS };
|
|
@@ -1216,61 +1312,86 @@ async function discoverListeningPorts(excludePorts = []) {
|
|
|
1216
1312
|
}
|
|
1217
1313
|
async function resolveListeningPid(port) {
|
|
1218
1314
|
if (!Number.isInteger(port) || port <= 0) return null;
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
} catch {
|
|
1228
|
-
return null;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
try {
|
|
1232
|
-
const { stdout } = await execGuarded(`ss -tlnp 2>/dev/null`, 8e3, {
|
|
1233
|
-
maxBuffer: 16 * 1024 * 1024
|
|
1234
|
-
});
|
|
1235
|
-
for (const line of stdout.split("\n")) {
|
|
1236
|
-
if (!new RegExp(`:${port}\\s`).test(line)) continue;
|
|
1237
|
-
const pid = Number(line.match(/pid=(\d+)/)?.[1]);
|
|
1238
|
-
if (pid > 0) return pid;
|
|
1239
|
-
}
|
|
1240
|
-
} catch {
|
|
1241
|
-
}
|
|
1242
|
-
return null;
|
|
1315
|
+
const scan = await listListenersShared();
|
|
1316
|
+
if (scan.state !== "looked") return null;
|
|
1317
|
+
const pids = [
|
|
1318
|
+
...new Set(
|
|
1319
|
+
scan.processes.filter((entry) => entry.port === port && entry.pid > 0).map((entry) => entry.pid)
|
|
1320
|
+
)
|
|
1321
|
+
];
|
|
1322
|
+
return pids.length === 1 && pids[0] !== void 0 ? pids[0] : null;
|
|
1243
1323
|
}
|
|
1244
1324
|
var cwdByPid = /* @__PURE__ */ new Map();
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1325
|
+
var cwdQueue = [];
|
|
1326
|
+
var cwdFlushing = false;
|
|
1327
|
+
function kickCwdFlush() {
|
|
1328
|
+
if (cwdFlushing || cwdQueue.length === 0) return;
|
|
1329
|
+
cwdFlushing = true;
|
|
1330
|
+
void flushCwdQueue().finally(() => {
|
|
1331
|
+
cwdFlushing = false;
|
|
1332
|
+
kickCwdFlush();
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
async function flushCwdQueue() {
|
|
1336
|
+
await Promise.resolve();
|
|
1337
|
+
while (cwdQueue.length > 0) {
|
|
1338
|
+
const batch = cwdQueue.splice(0, cwdQueue.length);
|
|
1339
|
+
const pending = [...new Set(batch.map((waiter) => waiter.pid))].filter(
|
|
1340
|
+
(pid) => !cwdByPid.has(pid)
|
|
1261
1341
|
);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1342
|
+
const needLsof = [];
|
|
1343
|
+
for (const pid of pending) {
|
|
1344
|
+
try {
|
|
1345
|
+
const cwd = await (0, import_promises2.readlink)(`/proc/${pid}/cwd`);
|
|
1265
1346
|
if (cwd) {
|
|
1266
1347
|
cwdByPid.set(pid, cwd);
|
|
1267
|
-
|
|
1348
|
+
continue;
|
|
1268
1349
|
}
|
|
1350
|
+
} catch {
|
|
1269
1351
|
}
|
|
1352
|
+
needLsof.push(pid);
|
|
1270
1353
|
}
|
|
1271
|
-
|
|
1354
|
+
if (needLsof.length > 0) {
|
|
1355
|
+
let looked = false;
|
|
1356
|
+
try {
|
|
1357
|
+
const { stdout, code } = await spawnReaped(
|
|
1358
|
+
"lsof",
|
|
1359
|
+
["-b", "-w", "-nP", "-a", "-d", "cwd", "-p", needLsof.join(","), "-Fn"],
|
|
1360
|
+
8e3,
|
|
1361
|
+
4 * 1024 * 1024
|
|
1362
|
+
);
|
|
1363
|
+
if (code === 0 || code === 1) {
|
|
1364
|
+
looked = true;
|
|
1365
|
+
let current = 0;
|
|
1366
|
+
for (const line of stdout.split("\n")) {
|
|
1367
|
+
if (line.startsWith("p")) {
|
|
1368
|
+
current = Number(line.slice(1));
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
if (!line.startsWith("n") || line.length <= 1 || !needLsof.includes(current))
|
|
1372
|
+
continue;
|
|
1373
|
+
const cwd = line.slice(1).trim();
|
|
1374
|
+
if (cwd) cwdByPid.set(current, cwd);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
if (looked) {
|
|
1380
|
+
for (const pid of needLsof) {
|
|
1381
|
+
if (!cwdByPid.has(pid)) cwdByPid.set(pid, null);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
for (const waiter of batch) waiter.resolve(cwdByPid.get(waiter.pid) ?? null);
|
|
1272
1386
|
}
|
|
1273
|
-
|
|
1387
|
+
}
|
|
1388
|
+
function resolveProcessCwd(pid) {
|
|
1389
|
+
if (pid <= 0) return Promise.resolve(null);
|
|
1390
|
+
if (cwdByPid.has(pid)) return Promise.resolve(cwdByPid.get(pid) ?? null);
|
|
1391
|
+
return new Promise((resolveCwd) => {
|
|
1392
|
+
cwdQueue.push({ pid, resolve: resolveCwd });
|
|
1393
|
+
kickCwdFlush();
|
|
1394
|
+
});
|
|
1274
1395
|
}
|
|
1275
1396
|
function makeResult(hostname, port, framework, owner) {
|
|
1276
1397
|
return {
|
|
@@ -1678,6 +1799,8 @@ function __resetScannerCache() {
|
|
|
1678
1799
|
enrichmentCache.clear();
|
|
1679
1800
|
enrichmentInFlight.clear();
|
|
1680
1801
|
portCacheInvalidatedAt.clear();
|
|
1802
|
+
listenerCache = null;
|
|
1803
|
+
cwdByPid.clear();
|
|
1681
1804
|
__resetAppSettingsBundleCacheForTests();
|
|
1682
1805
|
}
|
|
1683
1806
|
async function attachAppSettingsBundle(result, projectRoot) {
|
|
@@ -1696,6 +1819,7 @@ async function scanDevServers(opts = {}) {
|
|
|
1696
1819
|
const scan = await scanListeningProcesses(opts.excludePorts);
|
|
1697
1820
|
if (scan.state === "could-not-look") throw new DevServerScanUnavailableError();
|
|
1698
1821
|
const processes = scan.processes;
|
|
1822
|
+
await Promise.all(processes.map((entry) => resolveProcessCwd(entry.pid)));
|
|
1699
1823
|
const currentPorts = new Set(processes.map((p) => p.port));
|
|
1700
1824
|
for (const p of [...portCache.keys()]) {
|
|
1701
1825
|
if (!currentPorts.has(p)) portCache.delete(p);
|
|
@@ -1785,6 +1909,7 @@ async function scanDevServers(opts = {}) {
|
|
|
1785
1909
|
__shouldReuseScannerCacheEntry,
|
|
1786
1910
|
discoverListeningPorts,
|
|
1787
1911
|
discoverListeningProcesses,
|
|
1912
|
+
pinSharedListenerScan,
|
|
1788
1913
|
probePort,
|
|
1789
1914
|
resolveListeningPid,
|
|
1790
1915
|
resolveProcessCwd,
|