create-opentray 0.27.2 → 0.27.4
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/{bin-2M6ud0DS.mjs → bin-DgadYsE1.mjs} +455 -260
- package/dist/bin-DgadYsE1.mjs.map +1 -0
- package/dist/bin.mjs +1 -1
- package/dist/index.d.mts +9 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +6 -6
- package/dist/bin-2M6ud0DS.mjs.map +0 -1
|
@@ -11,8 +11,8 @@ import { execFile, spawn, spawnSync } from "node:child_process";
|
|
|
11
11
|
import net from "node:net";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { getSvgPath } from "figma-squircle";
|
|
14
|
-
import { resolveDefaultDarwinAppBundlePath, sanitizeAppBundleName } from "@opentray/packaging";
|
|
15
14
|
import { promisify } from "node:util";
|
|
15
|
+
import { resolveDefaultDarwinAppBundlePath, sanitizeAppBundleName } from "@opentray/packaging";
|
|
16
16
|
import { createServer } from "node:http";
|
|
17
17
|
//#region packages/core/src/app-id.ts
|
|
18
18
|
/** A token that looks like a command option, e.g. `--xx`, `-p`, or `--port=8080`. */
|
|
@@ -7517,7 +7517,7 @@ const killProcessTree$1 = async (child) => {
|
|
|
7517
7517
|
child.kill("SIGTERM");
|
|
7518
7518
|
}
|
|
7519
7519
|
await sweepDescendants(pid, "SIGTERM");
|
|
7520
|
-
await waitForExit(child, 3e3).then((exited) => {
|
|
7520
|
+
await waitForExit$1(child, 3e3).then((exited) => {
|
|
7521
7521
|
if (!exited) {
|
|
7522
7522
|
try {
|
|
7523
7523
|
process.kill(-pid, "SIGKILL");
|
|
@@ -7555,7 +7555,7 @@ const sweepDescendants = async (pid, signal) => {
|
|
|
7555
7555
|
}
|
|
7556
7556
|
}
|
|
7557
7557
|
};
|
|
7558
|
-
const waitForExit = (child, timeoutMs) => new Promise((resolve) => {
|
|
7558
|
+
const waitForExit$1 = (child, timeoutMs) => new Promise((resolve) => {
|
|
7559
7559
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7560
7560
|
resolve(true);
|
|
7561
7561
|
return;
|
|
@@ -7581,6 +7581,8 @@ const runExecFileCapture = (command, args) => new Promise((resolve, reject) => {
|
|
|
7581
7581
|
});
|
|
7582
7582
|
//#endregion
|
|
7583
7583
|
//#region packages/core/src/toolbar-carrier.ts
|
|
7584
|
+
/** Debounce window that collapses a reload's close burst into one rebuild. */
|
|
7585
|
+
const TOOLBAR_CHANNEL_REBUILD_DEBOUNCE_MS = 300;
|
|
7584
7586
|
/**
|
|
7585
7587
|
* The generated-entry helper source. The embedding template supplies
|
|
7586
7588
|
* `column`/`fixed`/`grow` (imported from "@opentray/ext-webview" beside
|
|
@@ -7592,6 +7594,8 @@ const toolbarCarrierSource = () => `// Toolbar navigation carrier (add-webview-o
|
|
|
7592
7594
|
// it through the create-private message channel. Channel failure is an
|
|
7593
7595
|
// exposed bug (D12: no fallback path) — errors land in app.log through
|
|
7594
7596
|
// options.log instead of being silently swallowed.
|
|
7597
|
+
const TOOLBAR_CHANNEL_REBUILD_DEBOUNCE_MS = ${TOOLBAR_CHANNEL_REBUILD_DEBOUNCE_MS};
|
|
7598
|
+
|
|
7595
7599
|
const attachToolbarCarrier = async (shell, options) => {
|
|
7596
7600
|
// Per-child bridge policy (D2): the toolbar page is trusted shell UI and
|
|
7597
7601
|
// gets exactly the channel surface; the content webview gets NO policy, so
|
|
@@ -7605,11 +7609,21 @@ const attachToolbarCarrier = async (shell, options) => {
|
|
|
7605
7609
|
// Declarative layered layout (D8 sugar): toolbar pinned at a fixed 44px
|
|
7606
7610
|
// height, content filling the remainder; resize is recomputed natively.
|
|
7607
7611
|
await shell.setLayout(column([fixed("toolbar", 44), grow("content")]));
|
|
7608
|
-
const channel = await shell.createMessageChannel({ target: "toolbar" });
|
|
7609
7612
|
const note = (error) => {
|
|
7610
7613
|
const message = error instanceof Error ? error.message : String(error);
|
|
7611
7614
|
void options.log("toolbar carrier: " + message);
|
|
7612
7615
|
};
|
|
7616
|
+
|
|
7617
|
+
// Channel self-heal state (P1-3): the toolbar page's document owns its
|
|
7618
|
+
// endpoint half, so a document death (manual ⌘R reload) closes the channel
|
|
7619
|
+
// (D11 document_navigated semantics). The host half heals itself: debounced
|
|
7620
|
+
// recreate → reinstall command surface → re-seed the address bar. Posts
|
|
7621
|
+
// always flow through the CURRENT channel, so a dead document's last
|
|
7622
|
+
// in-flight pushes fail into app.log instead of a zombie endpoint.
|
|
7623
|
+
let channel = null;
|
|
7624
|
+
let stopped = false;
|
|
7625
|
+
let rebuildTimer = null;
|
|
7626
|
+
|
|
7613
7627
|
// Address-bar truth (D13): the content webview's urlChange events ARE the
|
|
7614
7628
|
// address bar's source of truth — pushed over the channel, never scraped
|
|
7615
7629
|
// from a wrapper document.
|
|
@@ -7653,37 +7667,86 @@ const attachToolbarCarrier = async (shell, options) => {
|
|
|
7653
7667
|
projectTitle(event.title);
|
|
7654
7668
|
});
|
|
7655
7669
|
}
|
|
7670
|
+
|
|
7656
7671
|
// Command surface (D12): instructions and state queries arrive as channel
|
|
7657
7672
|
// payloads; back/forward drive the content webview's NATIVE history and
|
|
7658
7673
|
// reload re-navigates to the current URL through the native navigate
|
|
7659
|
-
// command (never evaluate(location.reload()) on a wrapper).
|
|
7660
|
-
channel
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7674
|
+
// command (never evaluate(location.reload()) on a wrapper). Installed as
|
|
7675
|
+
// one function so every rebuilt channel gets the identical surface.
|
|
7676
|
+
const installChannelCommandSurface = () => {
|
|
7677
|
+
channel.onMessage((payload) => {
|
|
7678
|
+
const message = typeof payload === "object" && payload !== null ? payload : {};
|
|
7679
|
+
const kind = message.kind;
|
|
7680
|
+
if (kind === "navigate" && typeof message.url === "string" && message.url.length > 0) {
|
|
7681
|
+
void content.navigate(message.url).catch(note);
|
|
7682
|
+
return;
|
|
7683
|
+
}
|
|
7684
|
+
if (kind === "back") {
|
|
7685
|
+
void content.back().catch(note);
|
|
7686
|
+
return;
|
|
7687
|
+
}
|
|
7688
|
+
if (kind === "forward") {
|
|
7689
|
+
void content.forward().catch(note);
|
|
7690
|
+
return;
|
|
7691
|
+
}
|
|
7692
|
+
if (kind === "reload") {
|
|
7693
|
+
void content.getUrl().then((state) => content.navigate(state.url)).catch(note);
|
|
7694
|
+
return;
|
|
7695
|
+
}
|
|
7696
|
+
if (kind === "get-url") {
|
|
7697
|
+
void content.getUrl().then((state) => pushUrl(state.url)).catch(note);
|
|
7698
|
+
}
|
|
7699
|
+
});
|
|
7700
|
+
};
|
|
7701
|
+
// Seed the address bar with the current truth (pre-subscription buffering
|
|
7702
|
+
// on both endpoints covers the race before the toolbar page registers).
|
|
7703
|
+
const seedAddressBar = () => {
|
|
7704
|
+
void content.getUrl().then((state) => pushUrl(state.url)).catch(note);
|
|
7705
|
+
};
|
|
7706
|
+
|
|
7707
|
+
// One channel lifecycle: create, install the surface, observe the single
|
|
7708
|
+
// onClose (D11), and drop the replaced channel's tombstone (destroy is
|
|
7709
|
+
// best-effort cleanup of already-closed state).
|
|
7710
|
+
const openChannel = async () => {
|
|
7711
|
+
const next = await shell.createMessageChannel({ target: "toolbar" });
|
|
7712
|
+
if (stopped) {
|
|
7713
|
+
try { await next.destroy(); } catch { /* raced teardown */ }
|
|
7677
7714
|
return;
|
|
7678
7715
|
}
|
|
7679
|
-
|
|
7680
|
-
|
|
7716
|
+
const replaced = channel;
|
|
7717
|
+
channel = next;
|
|
7718
|
+
installChannelCommandSurface();
|
|
7719
|
+
channel.onClose(() => scheduleChannelRebuild());
|
|
7720
|
+
if (replaced !== null) {
|
|
7721
|
+
try { await replaced.destroy(); } catch { /* already closed */ }
|
|
7681
7722
|
}
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
//
|
|
7685
|
-
|
|
7686
|
-
|
|
7723
|
+
seedAddressBar();
|
|
7724
|
+
};
|
|
7725
|
+
// Debounced self-heal (P1-3): a reload can emit several closes (both
|
|
7726
|
+
// endpoints observe one each, plus repeated reloads); exactly one rebuild
|
|
7727
|
+
// serves the burst. After stop() the carrier never rebuilds — a teardown
|
|
7728
|
+
// close (window destroyed / session closed) must not resurrect a channel
|
|
7729
|
+
// against a dying session.
|
|
7730
|
+
const scheduleChannelRebuild = () => {
|
|
7731
|
+
if (stopped || rebuildTimer !== null) return;
|
|
7732
|
+
rebuildTimer = setTimeout(() => {
|
|
7733
|
+
rebuildTimer = null;
|
|
7734
|
+
if (stopped) return;
|
|
7735
|
+
void openChannel().catch(note);
|
|
7736
|
+
}, TOOLBAR_CHANNEL_REBUILD_DEBOUNCE_MS);
|
|
7737
|
+
};
|
|
7738
|
+
|
|
7739
|
+
await openChannel();
|
|
7740
|
+
return {
|
|
7741
|
+
content,
|
|
7742
|
+
stop: () => {
|
|
7743
|
+
stopped = true;
|
|
7744
|
+
if (rebuildTimer !== null) {
|
|
7745
|
+
clearTimeout(rebuildTimer);
|
|
7746
|
+
rebuildTimer = null;
|
|
7747
|
+
}
|
|
7748
|
+
},
|
|
7749
|
+
};
|
|
7687
7750
|
};
|
|
7688
7751
|
`;
|
|
7689
7752
|
//#endregion
|
|
@@ -7879,29 +7942,48 @@ const main = async () => {
|
|
|
7879
7942
|
? { "darwin-icon-only": { type: "file", path: resolve(PROJECT_DIR, trayIcon.path), isTemplate: true } }
|
|
7880
7943
|
: { "icon-only": { type: "file", path: resolve(PROJECT_DIR, trayIcon.path) } };
|
|
7881
7944
|
|
|
7882
|
-
const tray = await
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7945
|
+
const tray = await (async () => {
|
|
7946
|
+
try {
|
|
7947
|
+
return await createTray({
|
|
7948
|
+
id: config.appId,
|
|
7949
|
+
tooltip: { title: config.appName, description: \`\${config.appName} (OpenTray)\` },
|
|
7950
|
+
icon: Object.keys(trayIconCandidates).length > 0
|
|
7951
|
+
? trayIconCandidates
|
|
7952
|
+
: { "text-only": "${config.appName.replace(/['"\\]/gu, "").slice(0, 2) || "A"}" },
|
|
7953
|
+
menu: { items: [
|
|
7954
|
+
{ type: "item", id: 1, title: \`Show \${config.appName}\`, primaryEvent: true },
|
|
7955
|
+
{ type: "separator" },
|
|
7956
|
+
{ type: "item", id: 2, title: "Quit" },
|
|
7957
|
+
] },
|
|
7958
|
+
}, {
|
|
7959
|
+
appId: config.appId,
|
|
7960
|
+
appName: config.appName,
|
|
7961
|
+
...(appIcon === undefined ? {} : { appIcon }),
|
|
7962
|
+
appLaunch: {
|
|
7963
|
+
// The generated entry embeds a native PTY (@lydell/node-pty), which
|
|
7964
|
+
// requires a Node host — cold launches must not inherit a Bun execPath.
|
|
7965
|
+
command: nodeRuntime(),
|
|
7966
|
+
args: [resolve(PROJECT_DIR, "main.mjs")],
|
|
7967
|
+
cwd: PROJECT_DIR,
|
|
7968
|
+
},
|
|
7969
|
+
});
|
|
7970
|
+
} catch (error) {
|
|
7971
|
+
// Carrier-resurrection yield: a Dock-pinned carrier can cold-start this
|
|
7972
|
+
// entry while another instance already owns the broker session. The
|
|
7973
|
+
// owner keeps the app open; this launcher exits cleanly (no retry, no
|
|
7974
|
+
// empty tray shell, no orphaned command supervision) and the evidence
|
|
7975
|
+
// lands in app.log. The supervised command started BEFORE createTray,
|
|
7976
|
+
// so the yield takes it down (direct kill; the closing PTY master adds
|
|
7977
|
+
// SIGHUP) — the shell server dies with this process.
|
|
7978
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7979
|
+
if (message.includes("OPENTRAY_BROKER_SINGLE_SESSION")) {
|
|
7980
|
+
await logSink("[create-opentray] broker session already owned by another instance of this app; this cold-started launcher exits (yield)\\n", "utf8");
|
|
7981
|
+
try { command.killDirect(); } catch { /* already dead */ }
|
|
7982
|
+
process.exit(0);
|
|
7983
|
+
}
|
|
7984
|
+
throw error;
|
|
7985
|
+
}
|
|
7986
|
+
})();
|
|
7905
7987
|
|
|
7906
7988
|
const baseTitle = config.appName;
|
|
7907
7989
|
// v1 developerMode maps ONLY to per-window DevTools admission. When false it
|
|
@@ -7962,12 +8044,15 @@ const main = async () => {
|
|
|
7962
8044
|
...devtools,
|
|
7963
8045
|
});
|
|
7964
8046
|
await win.show().catch(() => {});
|
|
7965
|
-
await attachToolbarCarrier(win, {
|
|
8047
|
+
const carrier = await attachToolbarCarrier(win, {
|
|
7966
8048
|
toolbarUrl: \`http://127.0.0.1:\${shellPort}/toolbar.html\`,
|
|
7967
8049
|
contentUrl: direct,
|
|
7968
8050
|
titleFollows,
|
|
7969
8051
|
log: logNote,
|
|
7970
8052
|
});
|
|
8053
|
+
// P1-3 exit race: teardown closes the channel; the carrier must never
|
|
8054
|
+
// rebuild against a dying session. Stop before the window goes down.
|
|
8055
|
+
process.once("exit", carrier.stop);
|
|
7971
8056
|
serviceWindows.set(port, { win, detached: false });
|
|
7972
8057
|
return;
|
|
7973
8058
|
}
|
|
@@ -8203,28 +8288,43 @@ ${toolbarCarrierSource()}const main = async () => {
|
|
|
8203
8288
|
? { "darwin-icon-only": { type: "file", path: resolve(PROJECT_DIR, trayIcon.path), isTemplate: true } }
|
|
8204
8289
|
: { "icon-only": { type: "file", path: resolve(PROJECT_DIR, trayIcon.path) } };
|
|
8205
8290
|
|
|
8206
|
-
const tray = await
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8291
|
+
const tray = await (async () => {
|
|
8292
|
+
try {
|
|
8293
|
+
return await createTray({
|
|
8294
|
+
id: config.appId,
|
|
8295
|
+
tooltip: { title: config.appName, description: \`\${config.appName} (OpenTray)\` },
|
|
8296
|
+
icon: Object.keys(trayIconCandidates).length > 0
|
|
8297
|
+
? trayIconCandidates
|
|
8298
|
+
: { "text-only": "${config.appName.replace(/['"\\]/gu, "").slice(0, 2) || "A"}" },
|
|
8299
|
+
menu: { items: [
|
|
8300
|
+
{ type: "item", id: 1, title: \`Show \${config.appName}\`, primaryEvent: true },
|
|
8301
|
+
{ type: "item", id: 3, title: "Reload" },
|
|
8302
|
+
{ type: "separator" },
|
|
8303
|
+
{ type: "item", id: 2, title: "Quit" },
|
|
8304
|
+
] },
|
|
8305
|
+
}, {
|
|
8306
|
+
appId: config.appId,
|
|
8307
|
+
appName: config.appName,
|
|
8308
|
+
...(appIcon === undefined ? {} : { appIcon }),
|
|
8309
|
+
appLaunch: {
|
|
8310
|
+
command: nodeRuntime(),
|
|
8311
|
+
args: [resolve(PROJECT_DIR, "main.mjs")],
|
|
8312
|
+
cwd: PROJECT_DIR,
|
|
8313
|
+
},
|
|
8314
|
+
});
|
|
8315
|
+
} catch (error) {
|
|
8316
|
+
// Carrier-resurrection yield: a Dock-pinned carrier can cold-start this
|
|
8317
|
+
// entry while another instance already owns the broker session. The
|
|
8318
|
+
// owner keeps the app open; this launcher exits cleanly (no retry, no
|
|
8319
|
+
// empty tray shell) and the evidence lands in app.log.
|
|
8320
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8321
|
+
if (message.includes("OPENTRAY_BROKER_SINGLE_SESSION")) {
|
|
8322
|
+
await logSink("[create-opentray] broker session already owned by another instance of this app; this cold-started launcher exits (yield)\\n", "utf8");
|
|
8323
|
+
process.exit(0);
|
|
8324
|
+
}
|
|
8325
|
+
throw error;
|
|
8326
|
+
}
|
|
8327
|
+
})();
|
|
8228
8328
|
|
|
8229
8329
|
// v1 developerMode maps ONLY to per-window DevTools admission. When false it
|
|
8230
8330
|
// must not request devtools at all — default windows are not inspectable.
|
|
@@ -8265,12 +8365,16 @@ ${toolbarCarrierSource()}const main = async () => {
|
|
|
8265
8365
|
|
|
8266
8366
|
let content = null;
|
|
8267
8367
|
if (toolbarReady) {
|
|
8268
|
-
|
|
8368
|
+
const carrier = await attachToolbarCarrier(window, {
|
|
8269
8369
|
toolbarUrl: \`http://127.0.0.1:\${shellPort}/toolbar.html\`,
|
|
8270
8370
|
contentUrl: config.url,
|
|
8271
8371
|
titleFollows,
|
|
8272
8372
|
log: logNote,
|
|
8273
|
-
})
|
|
8373
|
+
});
|
|
8374
|
+
({ content } = carrier);
|
|
8375
|
+
// P1-3 exit race: teardown closes the channel; the carrier must never
|
|
8376
|
+
// rebuild against a dying session. Stop before the window goes down.
|
|
8377
|
+
process.once("exit", carrier.stop);
|
|
8274
8378
|
}
|
|
8275
8379
|
|
|
8276
8380
|
// Tray reload (D11): toolbar mode reloads the CONTENT webview natively
|
|
@@ -8831,6 +8935,182 @@ const runPackageManagerInstall = async (options) => {
|
|
|
8831
8935
|
});
|
|
8832
8936
|
};
|
|
8833
8937
|
//#endregion
|
|
8938
|
+
//#region packages/core/src/errors.ts
|
|
8939
|
+
const ok = (value) => ({
|
|
8940
|
+
ok: true,
|
|
8941
|
+
value
|
|
8942
|
+
});
|
|
8943
|
+
const err = (code, message, details) => ({
|
|
8944
|
+
ok: false,
|
|
8945
|
+
error: {
|
|
8946
|
+
code,
|
|
8947
|
+
message,
|
|
8948
|
+
...details === void 0 ? {} : { details }
|
|
8949
|
+
}
|
|
8950
|
+
});
|
|
8951
|
+
//#endregion
|
|
8952
|
+
//#region packages/core/src/runtime-record.ts
|
|
8953
|
+
const execFileAsync$1 = promisify(execFile);
|
|
8954
|
+
const RUNTIME_FILENAME = "runtime.json";
|
|
8955
|
+
const readRuntimeRecord = async (registrationDir) => {
|
|
8956
|
+
try {
|
|
8957
|
+
const raw = JSON.parse(await readFile(join(registrationDir, RUNTIME_FILENAME), "utf8"));
|
|
8958
|
+
if (typeof raw === "object" && raw !== null && typeof raw.pid === "number" && typeof raw.token === "string") {
|
|
8959
|
+
const record = raw;
|
|
8960
|
+
return {
|
|
8961
|
+
pid: record.pid,
|
|
8962
|
+
token: record.token,
|
|
8963
|
+
startedAt: typeof record.startedAt === "number" ? record.startedAt : null,
|
|
8964
|
+
launchedAt: typeof record.launchedAt === "number" ? record.launchedAt : Date.now()
|
|
8965
|
+
};
|
|
8966
|
+
}
|
|
8967
|
+
return;
|
|
8968
|
+
} catch {
|
|
8969
|
+
return;
|
|
8970
|
+
}
|
|
8971
|
+
};
|
|
8972
|
+
const clearRuntimeRecord = async (registrationDir) => {
|
|
8973
|
+
await rm(join(registrationDir, RUNTIME_FILENAME), { force: true });
|
|
8974
|
+
};
|
|
8975
|
+
const isPidAlive$1 = (pid) => {
|
|
8976
|
+
try {
|
|
8977
|
+
process.kill(pid, 0);
|
|
8978
|
+
return true;
|
|
8979
|
+
} catch (error) {
|
|
8980
|
+
return error.code === "EPERM";
|
|
8981
|
+
}
|
|
8982
|
+
};
|
|
8983
|
+
/** Linux: /proc/<pid>/stat field 22 (starttime in clock ticks since boot). */
|
|
8984
|
+
const linuxStartTicks = async (pid) => {
|
|
8985
|
+
try {
|
|
8986
|
+
const stat = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
8987
|
+
const close = stat.lastIndexOf(")");
|
|
8988
|
+
if (close < 0) return void 0;
|
|
8989
|
+
const fields = stat.slice(close + 2).split(" ");
|
|
8990
|
+
const ticks = Number.parseInt(fields[19] ?? "", 10);
|
|
8991
|
+
return Number.isInteger(ticks) ? ticks : void 0;
|
|
8992
|
+
} catch {
|
|
8993
|
+
return;
|
|
8994
|
+
}
|
|
8995
|
+
};
|
|
8996
|
+
const bootEpochMs = async () => {
|
|
8997
|
+
try {
|
|
8998
|
+
const stat = await readFile("/proc/stat", "utf8");
|
|
8999
|
+
const btime = /btime (\d+)/u.exec(stat);
|
|
9000
|
+
return btime === null ? void 0 : Number.parseInt(btime[1], 10) * 1e3;
|
|
9001
|
+
} catch {
|
|
9002
|
+
return;
|
|
9003
|
+
}
|
|
9004
|
+
};
|
|
9005
|
+
/** macOS: `ps -o lstart= -p <pid>` prints the absolute start timestamp. */
|
|
9006
|
+
const macosStartEpochMs = async (pid) => {
|
|
9007
|
+
try {
|
|
9008
|
+
const { stdout } = await execFileAsync$1("ps", [
|
|
9009
|
+
"-o",
|
|
9010
|
+
"lstart=",
|
|
9011
|
+
"-p",
|
|
9012
|
+
String(pid)
|
|
9013
|
+
], {
|
|
9014
|
+
timeout: 5e3,
|
|
9015
|
+
env: {
|
|
9016
|
+
...process.env,
|
|
9017
|
+
LC_ALL: "C",
|
|
9018
|
+
LANG: "C"
|
|
9019
|
+
}
|
|
9020
|
+
});
|
|
9021
|
+
const parsed = Date.parse(stdout.trim());
|
|
9022
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
9023
|
+
} catch {
|
|
9024
|
+
return;
|
|
9025
|
+
}
|
|
9026
|
+
};
|
|
9027
|
+
/** Windows: PowerShell Get-Process StartTime for the pid. */
|
|
9028
|
+
const windowsStartEpochMs = async (pid) => {
|
|
9029
|
+
try {
|
|
9030
|
+
const { stdout } = await execFileAsync$1("powershell.exe", [
|
|
9031
|
+
"-NoProfile",
|
|
9032
|
+
"-NonInteractive",
|
|
9033
|
+
"-Command",
|
|
9034
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate`
|
|
9035
|
+
], {
|
|
9036
|
+
timeout: 1e4,
|
|
9037
|
+
windowsHide: true
|
|
9038
|
+
});
|
|
9039
|
+
const trimmed = stdout.trim();
|
|
9040
|
+
const dmtf = /^(\d{14})(?:\.(\d+))?(?:([+-]\d+))?$/u.exec(trimmed);
|
|
9041
|
+
if (dmtf !== null) {
|
|
9042
|
+
const stamp = dmtf[1] ?? "";
|
|
9043
|
+
const fraction = dmtf[2];
|
|
9044
|
+
const offset = dmtf[3];
|
|
9045
|
+
const iso = `${stamp.slice(0, 4)}-${stamp.slice(4, 6)}-${stamp.slice(6, 8)}T${stamp.slice(8, 10)}:${stamp.slice(10, 12)}:${stamp.slice(12, 14)}${fraction === void 0 ? "" : `.${fraction.slice(0, 3)}`}${offset ?? "Z"}`;
|
|
9046
|
+
const parsed = Date.parse(iso);
|
|
9047
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
9048
|
+
}
|
|
9049
|
+
const parsed = Date.parse(trimmed);
|
|
9050
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
9051
|
+
} catch {
|
|
9052
|
+
return;
|
|
9053
|
+
}
|
|
9054
|
+
};
|
|
9055
|
+
/**
|
|
9056
|
+
* Read a process's start fingerprint (epoch ms) on the current platform;
|
|
9057
|
+
* undefined when the platform cannot report it.
|
|
9058
|
+
*/
|
|
9059
|
+
const readProcessStartEpochMs = async (pid, platform = process.platform) => {
|
|
9060
|
+
if (platform === "linux") {
|
|
9061
|
+
const [ticks, boot] = await Promise.all([linuxStartTicks(pid), bootEpochMs()]);
|
|
9062
|
+
if (ticks !== void 0 && boot !== void 0) return boot + Math.floor(ticks / 100 * 1e3);
|
|
9063
|
+
return;
|
|
9064
|
+
}
|
|
9065
|
+
if (platform === "darwin") return macosStartEpochMs(pid);
|
|
9066
|
+
if (platform === "win32") return windowsStartEpochMs(pid);
|
|
9067
|
+
};
|
|
9068
|
+
/**
|
|
9069
|
+
* Inspect a recorded PID against its recorded start fingerprint.
|
|
9070
|
+
*
|
|
9071
|
+
* - `dead`: the process does not exist (stale record).
|
|
9072
|
+
* - `reused`: a different process now owns the PID (start mismatch).
|
|
9073
|
+
* - `live`: alive and (when the platform reports start times) the same launch.
|
|
9074
|
+
* - `unverified`: alive but identity cannot be verified on this platform.
|
|
9075
|
+
*/
|
|
9076
|
+
const inspectProcess = async (pid, expectedStartedAt, platform = process.platform) => {
|
|
9077
|
+
if (!isPidAlive$1(pid)) return "dead";
|
|
9078
|
+
if (expectedStartedAt === null) return "unverified";
|
|
9079
|
+
const toleranceMs = 2e3;
|
|
9080
|
+
const observed = await readProcessStartEpochMs(pid, platform);
|
|
9081
|
+
if (observed === void 0) return "unverified";
|
|
9082
|
+
return Math.abs(observed - expectedStartedAt) <= toleranceMs ? "live" : "reused";
|
|
9083
|
+
};
|
|
9084
|
+
/** Terminate a verified process tree (never called for unverified PIDs). */
|
|
9085
|
+
const killProcessTree = async (pid, platform = process.platform) => {
|
|
9086
|
+
try {
|
|
9087
|
+
if (platform === "win32") await execFileAsync$1("taskkill", [
|
|
9088
|
+
"/PID",
|
|
9089
|
+
String(pid),
|
|
9090
|
+
"/T",
|
|
9091
|
+
"/F"
|
|
9092
|
+
], {
|
|
9093
|
+
timeout: 15e3,
|
|
9094
|
+
windowsHide: true
|
|
9095
|
+
});
|
|
9096
|
+
else {
|
|
9097
|
+
const { stdout } = await execFileAsync$1("pgrep", ["-P", String(pid)], { timeout: 5e3 }).catch(() => ({ stdout: "" }));
|
|
9098
|
+
const children = stdout.split("\n").map((line) => Number.parseInt(line.trim(), 10)).filter((value) => Number.isInteger(value) && value > 0);
|
|
9099
|
+
process.kill(pid, "SIGTERM");
|
|
9100
|
+
for (const child of children) try {
|
|
9101
|
+
process.kill(child, "SIGTERM");
|
|
9102
|
+
} catch {}
|
|
9103
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
9104
|
+
for (const target of [pid, ...children]) if (isPidAlive$1(target)) try {
|
|
9105
|
+
process.kill(target, "SIGKILL");
|
|
9106
|
+
} catch {}
|
|
9107
|
+
}
|
|
9108
|
+
return ok({ pid });
|
|
9109
|
+
} catch (error) {
|
|
9110
|
+
return err("process_unverified", `failed to terminate process tree for pid ${pid}: ${error instanceof Error ? error.message : String(error)}`, { pid });
|
|
9111
|
+
}
|
|
9112
|
+
};
|
|
9113
|
+
//#endregion
|
|
8834
9114
|
//#region packages/core/src/user-messages.ts
|
|
8835
9115
|
const UI_LOCALES = [
|
|
8836
9116
|
"zh-CN",
|
|
@@ -8957,6 +9237,7 @@ const userMessages = (locale) => CATALOGS[locale];
|
|
|
8957
9237
|
const ptyUnavailableMessage = (code, locale) => code === "pty_bun_terminal_missing" ? CATALOGS[locale].ptyBunTerminalMissing : CATALOGS[locale].ptyNodePtyMissing;
|
|
8958
9238
|
//#endregion
|
|
8959
9239
|
//#region packages/core/src/open-app.ts
|
|
9240
|
+
const execFileAsync = promisify(execFile);
|
|
8960
9241
|
/** The generated entry embeds a native PTY: it needs Node, never a Bun host. */
|
|
8961
9242
|
const nodeExecutable = () => {
|
|
8962
9243
|
if (process.versions.bun === void 0 && process.execPath.includes("node")) return process.execPath;
|
|
@@ -8997,14 +9278,100 @@ const spawnEntryCold = (projectDir) => {
|
|
|
8997
9278
|
detail: `launched app entry (pid ${child.pid})`
|
|
8998
9279
|
};
|
|
8999
9280
|
};
|
|
9281
|
+
const isPidAlive = (pid) => {
|
|
9282
|
+
try {
|
|
9283
|
+
process.kill(pid, 0);
|
|
9284
|
+
return true;
|
|
9285
|
+
} catch (error) {
|
|
9286
|
+
return error.code === "EPERM";
|
|
9287
|
+
}
|
|
9288
|
+
};
|
|
9289
|
+
/**
|
|
9290
|
+
* Live entry pids for one payload by argv identity: the entry is always
|
|
9291
|
+
* `node <projectDir>/main.mjs` (the carrier's opentray-launch.json vector and
|
|
9292
|
+
* the cold-start spawn both use the absolute script path), so matching the
|
|
9293
|
+
* script path is exact identity — never a name-based guess. POSIX uses
|
|
9294
|
+
* `pgrep -f`; Windows a bounded CIM CommandLine query (best effort — a probe
|
|
9295
|
+
* failure reports no live instances instead of guessing).
|
|
9296
|
+
*/
|
|
9297
|
+
const defaultFindEntryPids = async (projectDir, platform) => {
|
|
9298
|
+
const scriptPath = join(projectDir, "main.mjs");
|
|
9299
|
+
const own = process.pid;
|
|
9300
|
+
if (platform === "win32") try {
|
|
9301
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
9302
|
+
"-NoProfile",
|
|
9303
|
+
"-NonInteractive",
|
|
9304
|
+
"-Command",
|
|
9305
|
+
`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${scriptPath.replaceAll("'", "''")}*' } | Select-Object -ExpandProperty ProcessId`
|
|
9306
|
+
], {
|
|
9307
|
+
timeout: 1e4,
|
|
9308
|
+
windowsHide: true
|
|
9309
|
+
});
|
|
9310
|
+
return stdout.split(/\r?\n/u).map((line) => Number.parseInt(line.trim(), 10)).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== own);
|
|
9311
|
+
} catch {
|
|
9312
|
+
return [];
|
|
9313
|
+
}
|
|
9314
|
+
try {
|
|
9315
|
+
const { stdout } = await execFileAsync("pgrep", ["-f", scriptPath], { timeout: 5e3 });
|
|
9316
|
+
return stdout.split("\n").map((line) => Number.parseInt(line.trim(), 10)).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== own);
|
|
9317
|
+
} catch {
|
|
9318
|
+
return [];
|
|
9319
|
+
}
|
|
9320
|
+
};
|
|
9321
|
+
/** Bounded wait for every stopped pid to actually exit (100ms cadence). */
|
|
9322
|
+
const waitForExit = async (pids, budgetMs) => {
|
|
9323
|
+
const deadline = Date.now() + budgetMs;
|
|
9324
|
+
for (;;) {
|
|
9325
|
+
if (!pids.some(isPidAlive)) return true;
|
|
9326
|
+
if (Date.now() >= deadline) return false;
|
|
9327
|
+
await new Promise((resolve) => {
|
|
9328
|
+
setTimeout(resolve, 100);
|
|
9329
|
+
});
|
|
9330
|
+
}
|
|
9331
|
+
};
|
|
9332
|
+
/**
|
|
9333
|
+
* Replace-semantics stop for one payload's live entry instances (Dev Launch
|
|
9334
|
+
* Law): probe by argv identity, terminate each tree, and wait bounded for the
|
|
9335
|
+
* pids (and behind them the broker session) to release before a new start may
|
|
9336
|
+
* claim the endpoint. A probe that finds nothing is a no-op.
|
|
9337
|
+
*/
|
|
9338
|
+
const stopLiveAppInstances = async (projectDir, options = {}) => {
|
|
9339
|
+
const platform = options.platform ?? process.platform;
|
|
9340
|
+
const findPids = options.probe?.findPids ?? defaultFindEntryPids;
|
|
9341
|
+
const killTree = options.probe?.killTree ?? ((pid) => killProcessTree(pid, platform));
|
|
9342
|
+
const pids = [...new Set(await findPids(projectDir, platform))].filter(isPidAlive);
|
|
9343
|
+
if (pids.length === 0) return {
|
|
9344
|
+
stoppedPids: [],
|
|
9345
|
+
hung: false
|
|
9346
|
+
};
|
|
9347
|
+
for (const pid of pids) await killTree(pid);
|
|
9348
|
+
if (!await waitForExit(pids, options.waitBudgetMs ?? 5e3)) return {
|
|
9349
|
+
stoppedPids: pids,
|
|
9350
|
+
hung: true
|
|
9351
|
+
};
|
|
9352
|
+
await new Promise((resolve) => {
|
|
9353
|
+
setTimeout(resolve, 300);
|
|
9354
|
+
});
|
|
9355
|
+
return {
|
|
9356
|
+
stoppedPids: pids,
|
|
9357
|
+
hung: false
|
|
9358
|
+
};
|
|
9359
|
+
};
|
|
9000
9360
|
const openMaterializedApp = async (input) => {
|
|
9001
|
-
|
|
9361
|
+
const platform = input.platform ?? process.platform;
|
|
9362
|
+
const replaced = await stopLiveAppInstances(input.projectDir, {
|
|
9363
|
+
platform,
|
|
9364
|
+
probe: input.probe,
|
|
9365
|
+
waitBudgetMs: input.stopWaitBudgetMs
|
|
9366
|
+
});
|
|
9367
|
+
const replacedNote = replaced.stoppedPids.length === 0 ? "" : `replaced live instance (pid ${replaced.stoppedPids.join(", ")})${replaced.hung ? " — WARNING: pid still alive after stop" : ""}; `;
|
|
9368
|
+
if (platform === "darwin") {
|
|
9002
9369
|
const bundlePath = input.bundlePath ?? await expectedBundleForProject(input.projectDir);
|
|
9003
|
-
if (bundlePath === void 0) return spawnEntryCold(input.projectDir);
|
|
9370
|
+
if (bundlePath === void 0) return withNote(spawnEntryCold(input.projectDir), replacedNote);
|
|
9004
9371
|
try {
|
|
9005
9372
|
await stat(bundlePath);
|
|
9006
9373
|
} catch {
|
|
9007
|
-
return spawnEntryCold(input.projectDir);
|
|
9374
|
+
return withNote(spawnEntryCold(input.projectDir), replacedNote);
|
|
9008
9375
|
}
|
|
9009
9376
|
const child = spawn("open", [bundlePath], {
|
|
9010
9377
|
stdio: "ignore",
|
|
@@ -9016,14 +9383,18 @@ const openMaterializedApp = async (input) => {
|
|
|
9016
9383
|
});
|
|
9017
9384
|
if (status === 0) return {
|
|
9018
9385
|
ok: true,
|
|
9019
|
-
detail:
|
|
9386
|
+
detail: `${replacedNote}opened ${bundlePath}`
|
|
9020
9387
|
};
|
|
9021
9388
|
return {
|
|
9022
9389
|
ok: false,
|
|
9023
|
-
detail:
|
|
9390
|
+
detail: `${replacedNote}open ${bundlePath} failed with ${status ?? "spawn error"}`
|
|
9024
9391
|
};
|
|
9025
9392
|
}
|
|
9026
|
-
return spawnEntryCold(input.projectDir);
|
|
9393
|
+
return withNote(spawnEntryCold(input.projectDir), replacedNote);
|
|
9394
|
+
};
|
|
9395
|
+
const withNote = (result, note) => note === "" ? result : {
|
|
9396
|
+
...result,
|
|
9397
|
+
detail: `${note}${result.detail}`
|
|
9027
9398
|
};
|
|
9028
9399
|
/**
|
|
9029
9400
|
* Platform-truthful pinning hint; makes no persistence claim before first
|
|
@@ -9037,20 +9408,6 @@ const pinningHint = (platform = process.platform, locale = "zh-CN") => {
|
|
|
9037
9408
|
return messages.pinHintOther;
|
|
9038
9409
|
};
|
|
9039
9410
|
//#endregion
|
|
9040
|
-
//#region packages/core/src/errors.ts
|
|
9041
|
-
const ok = (value) => ({
|
|
9042
|
-
ok: true,
|
|
9043
|
-
value
|
|
9044
|
-
});
|
|
9045
|
-
const err = (code, message, details) => ({
|
|
9046
|
-
ok: false,
|
|
9047
|
-
error: {
|
|
9048
|
-
code,
|
|
9049
|
-
message,
|
|
9050
|
-
...details === void 0 ? {} : { details }
|
|
9051
|
-
}
|
|
9052
|
-
});
|
|
9053
|
-
//#endregion
|
|
9054
9411
|
//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
|
|
9055
9412
|
var _a$1;
|
|
9056
9413
|
function $constructor(name, initializer, params) {
|
|
@@ -13602,168 +13959,6 @@ const listRegistrations = async (homeDir) => {
|
|
|
13602
13959
|
return records;
|
|
13603
13960
|
};
|
|
13604
13961
|
//#endregion
|
|
13605
|
-
//#region packages/core/src/runtime-record.ts
|
|
13606
|
-
const execFileAsync = promisify(execFile);
|
|
13607
|
-
const RUNTIME_FILENAME = "runtime.json";
|
|
13608
|
-
const readRuntimeRecord = async (registrationDir) => {
|
|
13609
|
-
try {
|
|
13610
|
-
const raw = JSON.parse(await readFile(join(registrationDir, RUNTIME_FILENAME), "utf8"));
|
|
13611
|
-
if (typeof raw === "object" && raw !== null && typeof raw.pid === "number" && typeof raw.token === "string") {
|
|
13612
|
-
const record = raw;
|
|
13613
|
-
return {
|
|
13614
|
-
pid: record.pid,
|
|
13615
|
-
token: record.token,
|
|
13616
|
-
startedAt: typeof record.startedAt === "number" ? record.startedAt : null,
|
|
13617
|
-
launchedAt: typeof record.launchedAt === "number" ? record.launchedAt : Date.now()
|
|
13618
|
-
};
|
|
13619
|
-
}
|
|
13620
|
-
return;
|
|
13621
|
-
} catch {
|
|
13622
|
-
return;
|
|
13623
|
-
}
|
|
13624
|
-
};
|
|
13625
|
-
const clearRuntimeRecord = async (registrationDir) => {
|
|
13626
|
-
await rm(join(registrationDir, RUNTIME_FILENAME), { force: true });
|
|
13627
|
-
};
|
|
13628
|
-
const isPidAlive = (pid) => {
|
|
13629
|
-
try {
|
|
13630
|
-
process.kill(pid, 0);
|
|
13631
|
-
return true;
|
|
13632
|
-
} catch (error) {
|
|
13633
|
-
return error.code === "EPERM";
|
|
13634
|
-
}
|
|
13635
|
-
};
|
|
13636
|
-
/** Linux: /proc/<pid>/stat field 22 (starttime in clock ticks since boot). */
|
|
13637
|
-
const linuxStartTicks = async (pid) => {
|
|
13638
|
-
try {
|
|
13639
|
-
const stat = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
13640
|
-
const close = stat.lastIndexOf(")");
|
|
13641
|
-
if (close < 0) return void 0;
|
|
13642
|
-
const fields = stat.slice(close + 2).split(" ");
|
|
13643
|
-
const ticks = Number.parseInt(fields[19] ?? "", 10);
|
|
13644
|
-
return Number.isInteger(ticks) ? ticks : void 0;
|
|
13645
|
-
} catch {
|
|
13646
|
-
return;
|
|
13647
|
-
}
|
|
13648
|
-
};
|
|
13649
|
-
const bootEpochMs = async () => {
|
|
13650
|
-
try {
|
|
13651
|
-
const stat = await readFile("/proc/stat", "utf8");
|
|
13652
|
-
const btime = /btime (\d+)/u.exec(stat);
|
|
13653
|
-
return btime === null ? void 0 : Number.parseInt(btime[1], 10) * 1e3;
|
|
13654
|
-
} catch {
|
|
13655
|
-
return;
|
|
13656
|
-
}
|
|
13657
|
-
};
|
|
13658
|
-
/** macOS: `ps -o lstart= -p <pid>` prints the absolute start timestamp. */
|
|
13659
|
-
const macosStartEpochMs = async (pid) => {
|
|
13660
|
-
try {
|
|
13661
|
-
const { stdout } = await execFileAsync("ps", [
|
|
13662
|
-
"-o",
|
|
13663
|
-
"lstart=",
|
|
13664
|
-
"-p",
|
|
13665
|
-
String(pid)
|
|
13666
|
-
], {
|
|
13667
|
-
timeout: 5e3,
|
|
13668
|
-
env: {
|
|
13669
|
-
...process.env,
|
|
13670
|
-
LC_ALL: "C",
|
|
13671
|
-
LANG: "C"
|
|
13672
|
-
}
|
|
13673
|
-
});
|
|
13674
|
-
const parsed = Date.parse(stdout.trim());
|
|
13675
|
-
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
13676
|
-
} catch {
|
|
13677
|
-
return;
|
|
13678
|
-
}
|
|
13679
|
-
};
|
|
13680
|
-
/** Windows: PowerShell Get-Process StartTime for the pid. */
|
|
13681
|
-
const windowsStartEpochMs = async (pid) => {
|
|
13682
|
-
try {
|
|
13683
|
-
const { stdout } = await execFileAsync("powershell.exe", [
|
|
13684
|
-
"-NoProfile",
|
|
13685
|
-
"-NonInteractive",
|
|
13686
|
-
"-Command",
|
|
13687
|
-
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate`
|
|
13688
|
-
], {
|
|
13689
|
-
timeout: 1e4,
|
|
13690
|
-
windowsHide: true
|
|
13691
|
-
});
|
|
13692
|
-
const trimmed = stdout.trim();
|
|
13693
|
-
const dmtf = /^(\d{14})(?:\.(\d+))?(?:([+-]\d+))?$/u.exec(trimmed);
|
|
13694
|
-
if (dmtf !== null) {
|
|
13695
|
-
const stamp = dmtf[1] ?? "";
|
|
13696
|
-
const fraction = dmtf[2];
|
|
13697
|
-
const offset = dmtf[3];
|
|
13698
|
-
const iso = `${stamp.slice(0, 4)}-${stamp.slice(4, 6)}-${stamp.slice(6, 8)}T${stamp.slice(8, 10)}:${stamp.slice(10, 12)}:${stamp.slice(12, 14)}${fraction === void 0 ? "" : `.${fraction.slice(0, 3)}`}${offset ?? "Z"}`;
|
|
13699
|
-
const parsed = Date.parse(iso);
|
|
13700
|
-
if (!Number.isNaN(parsed)) return parsed;
|
|
13701
|
-
}
|
|
13702
|
-
const parsed = Date.parse(trimmed);
|
|
13703
|
-
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
13704
|
-
} catch {
|
|
13705
|
-
return;
|
|
13706
|
-
}
|
|
13707
|
-
};
|
|
13708
|
-
/**
|
|
13709
|
-
* Read a process's start fingerprint (epoch ms) on the current platform;
|
|
13710
|
-
* undefined when the platform cannot report it.
|
|
13711
|
-
*/
|
|
13712
|
-
const readProcessStartEpochMs = async (pid, platform = process.platform) => {
|
|
13713
|
-
if (platform === "linux") {
|
|
13714
|
-
const [ticks, boot] = await Promise.all([linuxStartTicks(pid), bootEpochMs()]);
|
|
13715
|
-
if (ticks !== void 0 && boot !== void 0) return boot + Math.floor(ticks / 100 * 1e3);
|
|
13716
|
-
return;
|
|
13717
|
-
}
|
|
13718
|
-
if (platform === "darwin") return macosStartEpochMs(pid);
|
|
13719
|
-
if (platform === "win32") return windowsStartEpochMs(pid);
|
|
13720
|
-
};
|
|
13721
|
-
/**
|
|
13722
|
-
* Inspect a recorded PID against its recorded start fingerprint.
|
|
13723
|
-
*
|
|
13724
|
-
* - `dead`: the process does not exist (stale record).
|
|
13725
|
-
* - `reused`: a different process now owns the PID (start mismatch).
|
|
13726
|
-
* - `live`: alive and (when the platform reports start times) the same launch.
|
|
13727
|
-
* - `unverified`: alive but identity cannot be verified on this platform.
|
|
13728
|
-
*/
|
|
13729
|
-
const inspectProcess = async (pid, expectedStartedAt, platform = process.platform) => {
|
|
13730
|
-
if (!isPidAlive(pid)) return "dead";
|
|
13731
|
-
if (expectedStartedAt === null) return "unverified";
|
|
13732
|
-
const toleranceMs = 2e3;
|
|
13733
|
-
const observed = await readProcessStartEpochMs(pid, platform);
|
|
13734
|
-
if (observed === void 0) return "unverified";
|
|
13735
|
-
return Math.abs(observed - expectedStartedAt) <= toleranceMs ? "live" : "reused";
|
|
13736
|
-
};
|
|
13737
|
-
/** Terminate a verified process tree (never called for unverified PIDs). */
|
|
13738
|
-
const killProcessTree = async (pid, platform = process.platform) => {
|
|
13739
|
-
try {
|
|
13740
|
-
if (platform === "win32") await execFileAsync("taskkill", [
|
|
13741
|
-
"/PID",
|
|
13742
|
-
String(pid),
|
|
13743
|
-
"/T",
|
|
13744
|
-
"/F"
|
|
13745
|
-
], {
|
|
13746
|
-
timeout: 15e3,
|
|
13747
|
-
windowsHide: true
|
|
13748
|
-
});
|
|
13749
|
-
else {
|
|
13750
|
-
const { stdout } = await execFileAsync("pgrep", ["-P", String(pid)], { timeout: 5e3 }).catch(() => ({ stdout: "" }));
|
|
13751
|
-
const children = stdout.split("\n").map((line) => Number.parseInt(line.trim(), 10)).filter((value) => Number.isInteger(value) && value > 0);
|
|
13752
|
-
process.kill(pid, "SIGTERM");
|
|
13753
|
-
for (const child of children) try {
|
|
13754
|
-
process.kill(child, "SIGTERM");
|
|
13755
|
-
} catch {}
|
|
13756
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
13757
|
-
for (const target of [pid, ...children]) if (isPidAlive(target)) try {
|
|
13758
|
-
process.kill(target, "SIGKILL");
|
|
13759
|
-
} catch {}
|
|
13760
|
-
}
|
|
13761
|
-
return ok({ pid });
|
|
13762
|
-
} catch (error) {
|
|
13763
|
-
return err("process_unverified", `failed to terminate process tree for pid ${pid}: ${error instanceof Error ? error.message : String(error)}`, { pid });
|
|
13764
|
-
}
|
|
13765
|
-
};
|
|
13766
|
-
//#endregion
|
|
13767
13962
|
//#region packages/core/src/scan.ts
|
|
13768
13963
|
const exists$1 = async (path) => {
|
|
13769
13964
|
try {
|
|
@@ -18421,4 +18616,4 @@ if (isMainModule()) main(process.argv.slice(2)).then((code) => {
|
|
|
18421
18616
|
//#endregion
|
|
18422
18617
|
export { resolveLaunchVector as A, parseLsofPorts as C, verifyHttpService as D, serviceUrl as E, isValidAppId as F, toProjectDirectoryName as I, tokenizeCommandLine as M, deriveDefaultAppId as N, waitForTcpPort as O, deriveDefaultAppName as P, createPortDiscovery as S, parsePowerShellPorts as T, extractTitle as _, createWizardServer as a, resolveFaviconUrl as b, createWizardSession as c, detectPackageManager as d, expectedDarwinBundlePath as f, extractFaviconCandidates as g, writeScaffold as h, parseWizardCli as i, resolveOnPath as j, parseShebangInterpreter as k, openMaterializedApp as l, materialize as m, normalizeDraftForm as n, isAuthorized as o, isDirectoryOccupied$1 as p, openBrowser as r, isLoopbackHost as s, main as t, pinningHint as u, faviconCandidateSize as v, parseNetstatPorts as w, scrapeService as x, rankFaviconCandidates as y };
|
|
18423
18618
|
|
|
18424
|
-
//# sourceMappingURL=bin-
|
|
18619
|
+
//# sourceMappingURL=bin-DgadYsE1.mjs.map
|