@code-yeongyu/senpi 2026.8.31 → 2026.9.2-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/CHANGELOG.md +119 -2
- package/dist/beta/omo-local-update-fingerprint.d.ts.map +1 -1
- package/dist/beta/omo-local-update-fingerprint.js +0 -1
- package/dist/beta/omo-local-update-fingerprint.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -4
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/options.d.ts.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/options.js +6 -2
- package/dist/core/extensions/builtin/claude-sdk-oauth/options.js.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/stream.d.ts.map +1 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/stream.js +2 -1
- package/dist/core/extensions/builtin/claude-sdk-oauth/stream.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts +6 -0
- package/dist/core/extensions/builtin/compaction/context-pipeline.d.ts.map +1 -1
- package/dist/core/extensions/builtin/compaction/context-pipeline.js +11 -0
- package/dist/core/extensions/builtin/compaction/context-pipeline.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/extension-wiring.js +1 -1
- package/dist/core/extensions/builtin/compaction/extension-wiring.js.map +1 -1
- package/dist/core/extensions/builtin/compaction/index.js +4 -4
- package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.d.ts +3 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.d.ts.map +1 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +110 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js.map +1 -0
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +2 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js +7 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js +7 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js +7 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js +7 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +2 -1
- package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/execution-tooling.d.ts +50 -0
- package/dist/core/extensions/builtin/prompt-preset/execution-tooling.d.ts.map +1 -0
- package/dist/core/extensions/builtin/prompt-preset/execution-tooling.js +66 -0
- package/dist/core/extensions/builtin/prompt-preset/execution-tooling.js.map +1 -0
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.d.ts +5 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +6 -1
- package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js +11 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.js +11 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +2 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/presets.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/presets.js +18 -1
- package/dist/core/extensions/builtin/prompt-preset/presets.js.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts.map +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
- package/dist/core/extensions/builtin/prompt-preset/settings.js.map +1 -1
- package/dist/core/extensions/builtin/recommended-models/index.d.ts +1 -1
- package/dist/core/extensions/builtin/recommended-models/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/recommended-models/index.js +1 -1
- package/dist/core/extensions/builtin/recommended-models/index.js.map +1 -1
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +17 -8
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/retry-fallback/settings.d.ts.map +1 -1
- package/dist/core/retry-fallback/settings.js +1 -0
- package/dist/core/retry-fallback/settings.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +5 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +4 -4
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +10 -6
- package/dist/main.js.map +1 -1
- package/dist/modes/app-server/daemon/process.d.ts +13 -2
- package/dist/modes/app-server/daemon/process.d.ts.map +1 -1
- package/dist/modes/app-server/daemon/process.js +58 -12
- package/dist/modes/app-server/daemon/process.js.map +1 -1
- package/dist/modes/app-server/daemon.js +62 -4
- package/dist/modes/app-server/daemon.js.map +1 -1
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +11 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +35 -6
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/tips/catalog/ethos-tips.js +1 -1
- package/dist/modes/interactive/tips/catalog/ethos-tips.js.map +1 -1
- package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
- package/dist/modes/rpc/connection-handler.js +8 -3
- package/dist/modes/rpc/connection-handler.js.map +1 -1
- package/dist/modes/rpc/custom-capability.d.ts +1 -0
- package/dist/modes/rpc/custom-capability.d.ts.map +1 -1
- package/dist/modes/rpc/custom-capability.js +1 -0
- package/dist/modes/rpc/custom-capability.js.map +1 -1
- package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
- package/dist/modes/rpc/host-ensure.js +114 -22
- package/dist/modes/rpc/host-ensure.js.map +1 -1
- package/dist/modes/rpc/host-lifecycle.d.ts +15 -0
- package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
- package/dist/modes/rpc/host-lifecycle.js +180 -55
- package/dist/modes/rpc/host-lifecycle.js.map +1 -1
- package/dist/modes/rpc/host-watchdog.d.ts +2 -2
- package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
- package/dist/modes/rpc/host-watchdog.js +89 -21
- package/dist/modes/rpc/host-watchdog.js.map +1 -1
- package/dist/modes/rpc/multi-session-host.d.ts +4 -4
- package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +98 -60
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +8 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +70 -8
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +7 -2
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-event-fanout.d.ts +38 -0
- package/dist/modes/rpc/session-event-fanout.d.ts.map +1 -0
- package/dist/modes/rpc/session-event-fanout.js +133 -0
- package/dist/modes/rpc/session-event-fanout.js.map +1 -0
- package/dist/modes/rpc/session-event-writer.d.ts +4 -13
- package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
- package/dist/modes/rpc/session-event-writer.js +34 -96
- package/dist/modes/rpc/session-event-writer.js.map +1 -1
- package/dist/modes/rpc/socket-transport.d.ts +13 -0
- package/dist/modes/rpc/socket-transport.d.ts.map +1 -0
- package/dist/modes/rpc/socket-transport.js +92 -0
- package/dist/modes/rpc/socket-transport.js.map +1 -0
- package/docs/extensions.md +1 -1
- package/docs/rpc.md +11 -5
- package/docs/settings.md +8 -8
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +57 -0
- package/node_modules/@code-yeongyu/senpi-codemode/README.md +4 -2
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/extension/skill-contribution.ts +43 -9
- package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +4 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +10 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/skill/bun-1-4/SKILL.md +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/skill/bun-1-4/references/runtime-apis.md +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool-options.ts +3 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +2 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts +3 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +2 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/anthropic.d.ts +5 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/anthropic.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/anthropic.js +26 -4
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/anthropic.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/anthropic.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/google.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/groq.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry-profile/profiles.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry-profile/profiles.js +3 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry-profile/profiles.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* Both variables are unset for every other host launch, so nothing changes for
|
|
15
15
|
* plain `senpi --mode rpc` runs, hosts started by hand, or embedders.
|
|
16
16
|
*/
|
|
17
|
-
import { createReadStream } from "node:fs";
|
|
17
|
+
import { createReadStream, rmSync } from "node:fs";
|
|
18
18
|
import { rm } from "node:fs/promises";
|
|
19
19
|
import { envValue } from "../../core/brand.js";
|
|
20
|
+
import { readProcessIdentity } from "../app-server/daemon/process.js";
|
|
20
21
|
/** Inherited fd whose EOF means "the supervisor died"; set by the supervisor only. */
|
|
21
22
|
export const HOST_WATCH_FD_ENV = "SENPI_RPC_HOST_WATCH_FD";
|
|
22
23
|
/** Supervisor-owned private directory the host removes on watchdog shutdown. */
|
|
@@ -24,7 +25,9 @@ export const HOST_SCRATCH_DIR_ENV = "SENPI_RPC_HOST_SCRATCH_DIR";
|
|
|
24
25
|
/** Fallback binding when no inherited fd is available: poll this pid. */
|
|
25
26
|
export const HOST_WATCH_PPID_ENV = "SENPI_RPC_HOST_WATCH_PPID";
|
|
26
27
|
/** Poll cadence for the ppid fallback. */
|
|
27
|
-
export const HOST_WATCH_PPID_INTERVAL_MS =
|
|
28
|
+
export const HOST_WATCH_PPID_INTERVAL_MS = 250;
|
|
29
|
+
/** Bound the Windows PowerShell identity probe so one stuck query cannot stop future polls. */
|
|
30
|
+
const HOST_WATCH_PPID_PROBE_TIMEOUT_MS = 1_000;
|
|
28
31
|
export const HOST_CLEANUP_PATHS_ENV = "SENPI_RPC_HOST_CLEANUP_PATHS";
|
|
29
32
|
function parsePositiveInteger(value) {
|
|
30
33
|
if (value === undefined || !/^\d+$/.test(value.trim()))
|
|
@@ -53,11 +56,15 @@ export function readHostWatchdogConfig(env = process.env) {
|
|
|
53
56
|
}
|
|
54
57
|
/** Same configuration, resolved through the brand-aware env prefixes. */
|
|
55
58
|
export function readHostWatchdogConfigFromBrandEnv() {
|
|
59
|
+
// The lifecycle supervisor intentionally uses the canonical SENPI_RPC_HOST_*
|
|
60
|
+
// names when spawning a child. envValue() resolves branded aliases for normal
|
|
61
|
+
// launches, but must not hide the canonical variables from the supervisor
|
|
62
|
+
// handoff or the lifetime binding is silently disabled.
|
|
56
63
|
return readHostWatchdogConfig({
|
|
57
|
-
[HOST_WATCH_FD_ENV]: envValue("RPC_HOST_WATCH_FD"),
|
|
58
|
-
[HOST_WATCH_PPID_ENV]: envValue("RPC_HOST_WATCH_PPID"),
|
|
59
|
-
[HOST_SCRATCH_DIR_ENV]: envValue("RPC_HOST_SCRATCH_DIR"),
|
|
60
|
-
[HOST_CLEANUP_PATHS_ENV]: envValue("RPC_HOST_CLEANUP_PATHS"),
|
|
64
|
+
[HOST_WATCH_FD_ENV]: process.env[HOST_WATCH_FD_ENV] ?? envValue("RPC_HOST_WATCH_FD"),
|
|
65
|
+
[HOST_WATCH_PPID_ENV]: process.env[HOST_WATCH_PPID_ENV] ?? envValue("RPC_HOST_WATCH_PPID"),
|
|
66
|
+
[HOST_SCRATCH_DIR_ENV]: process.env[HOST_SCRATCH_DIR_ENV] ?? envValue("RPC_HOST_SCRATCH_DIR"),
|
|
67
|
+
[HOST_CLEANUP_PATHS_ENV]: process.env[HOST_CLEANUP_PATHS_ENV] ?? envValue("RPC_HOST_CLEANUP_PATHS"),
|
|
61
68
|
});
|
|
62
69
|
}
|
|
63
70
|
/**
|
|
@@ -75,7 +82,16 @@ export function armHostWatchdog(config, onSupervisorGone) {
|
|
|
75
82
|
return () => { };
|
|
76
83
|
const fire = (reason) => {
|
|
77
84
|
disarm();
|
|
78
|
-
|
|
85
|
+
if (process.platform === "win32") {
|
|
86
|
+
// Arm the host shutdown fallback before attempting metadata cleanup. The
|
|
87
|
+
// synchronous Win32 removal below handles the state files deterministically,
|
|
88
|
+
// while the fallback still covers a named-pipe close that never completes.
|
|
89
|
+
const cleanup = Promise.resolve().then(() => cleanupWatchdogPaths(config));
|
|
90
|
+
onSupervisorGone(reason, cleanup);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
void cleanupWatchdogPaths(config).finally(() => onSupervisorGone(reason));
|
|
94
|
+
}
|
|
79
95
|
};
|
|
80
96
|
const disarmers = [];
|
|
81
97
|
const disarm = () => {
|
|
@@ -96,23 +112,41 @@ export function armHostWatchdog(config, onSupervisorGone) {
|
|
|
96
112
|
*/
|
|
97
113
|
function watchFdForEof(fd, fire) {
|
|
98
114
|
let stream;
|
|
115
|
+
let streamFailed = false;
|
|
116
|
+
let fired = false;
|
|
117
|
+
const fireOnce = (reason) => {
|
|
118
|
+
if (fired)
|
|
119
|
+
return;
|
|
120
|
+
fired = true;
|
|
121
|
+
fire(reason);
|
|
122
|
+
};
|
|
99
123
|
try {
|
|
100
|
-
|
|
124
|
+
// The watchdog owns this inherited read end. autoClose is required on
|
|
125
|
+
// Win32 so the stream releases fd 3 and observes the pipe's terminal close.
|
|
126
|
+
stream = createReadStream("", { fd, autoClose: true });
|
|
101
127
|
}
|
|
102
128
|
catch {
|
|
103
129
|
return () => { };
|
|
104
130
|
}
|
|
105
131
|
stream.resume();
|
|
106
|
-
const onEnd = () =>
|
|
132
|
+
const onEnd = () => fireOnce(`supervisor pipe fd ${fd} closed`);
|
|
133
|
+
const onClose = () => {
|
|
134
|
+
if (!streamFailed)
|
|
135
|
+
onEnd();
|
|
136
|
+
};
|
|
137
|
+
const onError = () => {
|
|
138
|
+
streamFailed = true;
|
|
139
|
+
};
|
|
107
140
|
stream.once("end", onEnd);
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
stream.once("error",
|
|
141
|
+
// Win32 pipe teardown may report close without end; POSIX invalid-fd close is
|
|
142
|
+
// not supervisor death and remains inert.
|
|
143
|
+
if (process.platform === "win32")
|
|
144
|
+
stream.once("close", onClose);
|
|
145
|
+
stream.once("error", onError);
|
|
113
146
|
return () => {
|
|
114
147
|
stream.off("end", onEnd);
|
|
115
|
-
stream.off("
|
|
148
|
+
stream.off("close", onClose);
|
|
149
|
+
stream.off("error", onError);
|
|
116
150
|
stream.destroy();
|
|
117
151
|
};
|
|
118
152
|
}
|
|
@@ -122,10 +156,37 @@ function watchFdForEof(fd, fire) {
|
|
|
122
156
|
* fd could be inherited.
|
|
123
157
|
*/
|
|
124
158
|
function watchPpid(supervisorPid, fire) {
|
|
159
|
+
let checking = false;
|
|
160
|
+
let missingIdentityChecks = 0;
|
|
161
|
+
// Tests and embedders may bind the watchdog to this process itself; that
|
|
162
|
+
// is a valid live binding rather than evidence of supervisor loss.
|
|
163
|
+
if (supervisorPid === process.pid)
|
|
164
|
+
return () => { };
|
|
125
165
|
const timer = setInterval(() => {
|
|
126
|
-
if (
|
|
166
|
+
if (!processAlive(supervisorPid)) {
|
|
167
|
+
fire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (checking)
|
|
127
171
|
return;
|
|
128
|
-
|
|
172
|
+
checking = true;
|
|
173
|
+
void readProcessIdentity(supervisorPid, process.platform, HOST_WATCH_PPID_PROBE_TIMEOUT_MS)
|
|
174
|
+
.then((result) => {
|
|
175
|
+
if (result.kind === "error")
|
|
176
|
+
return;
|
|
177
|
+
if (result.kind === "absent")
|
|
178
|
+
missingIdentityChecks++;
|
|
179
|
+
else
|
|
180
|
+
missingIdentityChecks = 0;
|
|
181
|
+
if (process.ppid === supervisorPid && result.kind === "present")
|
|
182
|
+
return;
|
|
183
|
+
if (process.ppid === supervisorPid && missingIdentityChecks < 3)
|
|
184
|
+
return;
|
|
185
|
+
fire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);
|
|
186
|
+
})
|
|
187
|
+
.finally(() => {
|
|
188
|
+
checking = false;
|
|
189
|
+
});
|
|
129
190
|
}, HOST_WATCH_PPID_INTERVAL_MS);
|
|
130
191
|
timer.unref?.();
|
|
131
192
|
return () => clearInterval(timer);
|
|
@@ -136,18 +197,25 @@ function processAlive(pid) {
|
|
|
136
197
|
return true;
|
|
137
198
|
}
|
|
138
199
|
catch (cause) {
|
|
139
|
-
return cause instanceof Error && "code" in cause && cause.code
|
|
200
|
+
return cause instanceof Error && "code" in cause && cause.code !== "ESRCH";
|
|
140
201
|
}
|
|
141
202
|
}
|
|
142
203
|
async function cleanupWatchdogPaths(config) {
|
|
143
204
|
const paths = [...(config.cleanupPaths ?? []), ...(config.scratchDir ? [config.scratchDir] : [])];
|
|
205
|
+
if (process.platform === "win32") {
|
|
206
|
+
for (const path of paths) {
|
|
207
|
+
try {
|
|
208
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
209
|
+
}
|
|
210
|
+
catch { }
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
144
214
|
await Promise.all(paths.map(async (path) => {
|
|
145
215
|
try {
|
|
146
216
|
await rm(path, { recursive: true, force: true });
|
|
147
217
|
}
|
|
148
|
-
catch {
|
|
149
|
-
/* best effort: the host is exiting either way. */
|
|
150
|
-
}
|
|
218
|
+
catch { }
|
|
151
219
|
}));
|
|
152
220
|
}
|
|
153
221
|
//# sourceMappingURL=host-watchdog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-watchdog.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-watchdog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAG,yBAAyB,CAAC;AAC3D,gFAAgF;AAChF,MAAM,CAAC,MAAM,oBAAoB,GAAG,4BAA4B,CAAC;AACjE,yEAAyE;AACzE,MAAM,CAAC,MAAM,mBAAmB,GAAG,2BAA2B,CAAC;AAC/D,0CAA0C;AAC1C,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,MAAM,CAAC,MAAM,sBAAsB,GAAG,8BAA8B,CAAC;AASrE,SAAS,oBAAoB,CAAC,KAAyB;IACtD,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IACzE,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACpC,OAAO,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACxE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACrC,MAAoD,OAAO,CAAC,GAAG;IAE/D,MAAM,EAAE,GAAG,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,oBAAoB,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC;IAC5D,IAAI,EAAE,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC7D,MAAM,UAAU,GAAG,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAC7C,MAAM,YAAY,GAAG,GAAG,CAAC,sBAAsB,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9E,OAAO;QACN,EAAE;QACF,IAAI;QACJ,UAAU,EAAE,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU;QAClF,YAAY;KACZ,CAAC;AACH,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,kCAAkC;IACjD,OAAO,sBAAsB,CAAC;QAC7B,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC;QAClD,CAAC,mBAAmB,CAAC,EAAE,QAAQ,CAAC,qBAAqB,CAAC;QACtD,CAAC,oBAAoB,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC;QACxD,CAAC,sBAAsB,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC;KAC5D,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAC9B,MAAsC,EACtC,gBAA0C;IAE1C,IAAI,CAAC,MAAM;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAC7B,MAAM,IAAI,GAAG,CAAC,MAAc,EAAQ,EAAE;QACrC,MAAM,EAAE,CAAC;QACT,KAAK,oBAAoB,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3E,CAAC,CAAC;IACF,MAAM,SAAS,GAAsB,EAAE,CAAC;IACxC,MAAM,MAAM,GAAG,GAAS,EAAE;QACzB,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,IAAI,EAAE,CAAC;IAChD,CAAC,CAAC;IACF,IAAI,MAAM,CAAC,EAAE,KAAK,SAAS;QAAE,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;IAC5E,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAC5E,OAAO,MAAM,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,EAAU,EAAE,IAA8B;IAChE,IAAI,MAA2C,CAAC;IAChD,IAAI,CAAC;QACJ,MAAM,GAAG,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IACjB,CAAC;IACD,MAAM,CAAC,MAAM,EAAE,CAAC;IAChB,MAAM,KAAK,GAAG,GAAS,EAAE,CAAC,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC;IAClE,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC1B,6EAA6E;IAC7E,6EAA6E;IAC7E,0EAA0E;IAC1E,6EAA6E;IAC7E,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC/B,OAAO,GAAG,EAAE;QACX,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC3B,MAAM,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,SAAS,CAAC,aAAqB,EAAE,IAA8B;IACvE,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC9B,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,IAAI,YAAY,CAAC,aAAa,CAAC;YAAE,OAAO;QAC1E,IAAI,CAAC,kBAAkB,aAAa,kBAAkB,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;IACxE,CAAC,EAAE,2BAA2B,CAAC,CAAC;IAChC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IAChB,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAChC,IAAI,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC;IAC5E,CAAC;AACF,CAAC;AAED,KAAK,UAAU,oBAAoB,CAAC,MAA0B;IAC7D,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClG,MAAM,OAAO,CAAC,GAAG,CAChB,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACxB,IAAI,CAAC;YACJ,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACR,kDAAkD;QACnD,CAAC;IACF,CAAC,CAAC,CACF,CAAC;AACH,CAAC","sourcesContent":["/**\n * Opt-in supervisor-lifetime watchdog for the RPC socket host.\n *\n * A host started under the `host-lifecycle.ts` supervisor must not outlive it.\n * Catchable signals cannot carry that guarantee: `kill -9`, an OOM kill, or a\n * crashed supervisor run no JS handler, and the host is then reparented to init\n * as a permanent orphan holding its private socket forever.\n *\n * The binding used here is the OS itself. The supervisor spawns the host with an\n * extra inherited pipe and keeps the write end open without ever writing to it.\n * The kernel closes that end when the supervisor dies for ANY reason, so the\n * host's read end reaches EOF and the host shuts down cleanly.\n *\n * Both variables are unset for every other host launch, so nothing changes for\n * plain `senpi --mode rpc` runs, hosts started by hand, or embedders.\n */\nimport { createReadStream } from \"node:fs\";\nimport { rm } from \"node:fs/promises\";\nimport { envValue } from \"../../core/brand.ts\";\n\n/** Inherited fd whose EOF means \"the supervisor died\"; set by the supervisor only. */\nexport const HOST_WATCH_FD_ENV = \"SENPI_RPC_HOST_WATCH_FD\";\n/** Supervisor-owned private directory the host removes on watchdog shutdown. */\nexport const HOST_SCRATCH_DIR_ENV = \"SENPI_RPC_HOST_SCRATCH_DIR\";\n/** Fallback binding when no inherited fd is available: poll this pid. */\nexport const HOST_WATCH_PPID_ENV = \"SENPI_RPC_HOST_WATCH_PPID\";\n/** Poll cadence for the ppid fallback. */\nexport const HOST_WATCH_PPID_INTERVAL_MS = 2_000;\nexport const HOST_CLEANUP_PATHS_ENV = \"SENPI_RPC_HOST_CLEANUP_PATHS\";\n\nexport interface HostWatchdogConfig {\n\treadonly fd?: number;\n\treadonly ppid?: number;\n\treadonly scratchDir?: string;\n\treadonly cleanupPaths?: readonly string[];\n}\n\nfunction parsePositiveInteger(value: string | undefined): number | undefined {\n\tif (value === undefined || !/^\\d+$/.test(value.trim())) return undefined;\n\tconst parsed = Number(value.trim());\n\treturn Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/**\n * Reads the watchdog configuration from the environment. Returns `undefined`\n * when neither binding is requested, which is the case for every host launch\n * that does not come from the lifecycle supervisor.\n */\nexport function readHostWatchdogConfig(\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n): HostWatchdogConfig | undefined {\n\tconst fd = parsePositiveInteger(env[HOST_WATCH_FD_ENV]);\n\tconst ppid = parsePositiveInteger(env[HOST_WATCH_PPID_ENV]);\n\tif (fd === undefined && ppid === undefined) return undefined;\n\tconst scratchDir = env[HOST_SCRATCH_DIR_ENV];\n\tconst cleanupPaths = env[HOST_CLEANUP_PATHS_ENV]?.split(\"\\n\").filter(Boolean);\n\treturn {\n\t\tfd,\n\t\tppid,\n\t\tscratchDir: scratchDir === undefined || scratchDir === \"\" ? undefined : scratchDir,\n\t\tcleanupPaths,\n\t};\n}\n\n/** Same configuration, resolved through the brand-aware env prefixes. */\nexport function readHostWatchdogConfigFromBrandEnv(): HostWatchdogConfig | undefined {\n\treturn readHostWatchdogConfig({\n\t\t[HOST_WATCH_FD_ENV]: envValue(\"RPC_HOST_WATCH_FD\"),\n\t\t[HOST_WATCH_PPID_ENV]: envValue(\"RPC_HOST_WATCH_PPID\"),\n\t\t[HOST_SCRATCH_DIR_ENV]: envValue(\"RPC_HOST_SCRATCH_DIR\"),\n\t\t[HOST_CLEANUP_PATHS_ENV]: envValue(\"RPC_HOST_CLEANUP_PATHS\"),\n\t});\n}\n\n/**\n * Arms the configured watchdog. `onSupervisorGone` receives the reason and is\n * expected to perform the host's normal clean shutdown; the scratch directory\n * (the supervisor's private socket directory, which no longer has an owner) is\n * removed first so nothing is left behind even if shutdown then hangs.\n *\n * Takes ownership of `config.fd`: disarming (or firing) closes it, so callers\n * must not close it themselves. Returns a disarm function; a no-op when no\n * binding is configured.\n */\nexport function armHostWatchdog(\n\tconfig: HostWatchdogConfig | undefined,\n\tonSupervisorGone: (reason: string) => void,\n): () => void {\n\tif (!config) return () => {};\n\tconst fire = (reason: string): void => {\n\t\tdisarm();\n\t\tvoid cleanupWatchdogPaths(config).finally(() => onSupervisorGone(reason));\n\t};\n\tconst disarmers: Array<() => void> = [];\n\tconst disarm = (): void => {\n\t\tfor (const stop of disarmers.splice(0)) stop();\n\t};\n\tif (config.fd !== undefined) disarmers.push(watchFdForEof(config.fd, fire));\n\tif (config.ppid !== undefined) disarmers.push(watchPpid(config.ppid, fire));\n\treturn disarm;\n}\n\n/**\n * EOF on the inherited pipe is the primary signal. The supervisor never writes\n * to it, so any readable data is ignored; only close matters. An fd that cannot\n * be opened (never inherited) leaves the binding inert rather than killing a\n * healthy host.\n */\nfunction watchFdForEof(fd: number, fire: (reason: string) => void): () => void {\n\tlet stream: ReturnType<typeof createReadStream>;\n\ttry {\n\t\tstream = createReadStream(\"\", { fd, autoClose: false });\n\t} catch {\n\t\treturn () => {};\n\t}\n\tstream.resume();\n\tconst onEnd = (): void => fire(`supervisor pipe fd ${fd} closed`);\n\tstream.once(\"end\", onEnd);\n\t// A read error means the pipe is unusable, which is indistinguishable from a\n\t// dead supervisor from this side; treating it as EOF keeps the binding safe.\n\t// An unavailable inherited fd is a configuration/setup failure, not proof\n\t// that the supervisor died. The PPID binding, when supplied, remains active.\n\tstream.once(\"error\", () => {});\n\treturn () => {\n\t\tstream.off(\"end\", onEnd);\n\t\tstream.off(\"error\", onEnd);\n\t\tstream.destroy();\n\t};\n}\n\n/**\n * Fallback binding: the supervisor pid is gone, or this process was reparented\n * to init because the supervisor died. Polling covers the case where no extra\n * fd could be inherited.\n */\nfunction watchPpid(supervisorPid: number, fire: (reason: string) => void): () => void {\n\tconst timer = setInterval(() => {\n\t\tif (process.ppid === supervisorPid && processAlive(supervisorPid)) return;\n\t\tfire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);\n\t}, HOST_WATCH_PPID_INTERVAL_MS);\n\ttimer.unref?.();\n\treturn () => clearInterval(timer);\n}\n\nfunction processAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (cause) {\n\t\treturn cause instanceof Error && \"code\" in cause && cause.code === \"EPERM\";\n\t}\n}\n\nasync function cleanupWatchdogPaths(config: HostWatchdogConfig): Promise<void> {\n\tconst paths = [...(config.cleanupPaths ?? []), ...(config.scratchDir ? [config.scratchDir] : [])];\n\tawait Promise.all(\n\t\tpaths.map(async (path) => {\n\t\t\ttry {\n\t\t\t\tawait rm(path, { recursive: true, force: true });\n\t\t\t} catch {\n\t\t\t\t/* best effort: the host is exiting either way. */\n\t\t\t}\n\t\t}),\n\t);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"host-watchdog.js","sourceRoot":"","sources":["../../../src/modes/rpc/host-watchdog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opt-in supervisor-lifetime watchdog for the RPC socket host.\n *\n * A host started under the `host-lifecycle.ts` supervisor must not outlive it.\n * Catchable signals cannot carry that guarantee: `kill -9`, an OOM kill, or a\n * crashed supervisor run no JS handler, and the host is then reparented to init\n * as a permanent orphan holding its private socket forever.\n *\n * The binding used here is the OS itself. The supervisor spawns the host with an\n * extra inherited pipe and keeps the write end open without ever writing to it.\n * The kernel closes that end when the supervisor dies for ANY reason, so the\n * host's read end reaches EOF and the host shuts down cleanly.\n *\n * Both variables are unset for every other host launch, so nothing changes for\n * plain `senpi --mode rpc` runs, hosts started by hand, or embedders.\n */\nimport { createReadStream, rmSync } from \"node:fs\";\nimport { rm } from \"node:fs/promises\";\nimport { envValue } from \"../../core/brand.ts\";\nimport { readProcessIdentity } from \"../app-server/daemon/process.ts\";\n\n/** Inherited fd whose EOF means \"the supervisor died\"; set by the supervisor only. */\nexport const HOST_WATCH_FD_ENV = \"SENPI_RPC_HOST_WATCH_FD\";\n/** Supervisor-owned private directory the host removes on watchdog shutdown. */\nexport const HOST_SCRATCH_DIR_ENV = \"SENPI_RPC_HOST_SCRATCH_DIR\";\n/** Fallback binding when no inherited fd is available: poll this pid. */\nexport const HOST_WATCH_PPID_ENV = \"SENPI_RPC_HOST_WATCH_PPID\";\n/** Poll cadence for the ppid fallback. */\nexport const HOST_WATCH_PPID_INTERVAL_MS = 250;\n/** Bound the Windows PowerShell identity probe so one stuck query cannot stop future polls. */\nconst HOST_WATCH_PPID_PROBE_TIMEOUT_MS = 1_000;\nexport const HOST_CLEANUP_PATHS_ENV = \"SENPI_RPC_HOST_CLEANUP_PATHS\";\n\nexport interface HostWatchdogConfig {\n\treadonly fd?: number;\n\treadonly ppid?: number;\n\treadonly scratchDir?: string;\n\treadonly cleanupPaths?: readonly string[];\n}\n\nfunction parsePositiveInteger(value: string | undefined): number | undefined {\n\tif (value === undefined || !/^\\d+$/.test(value.trim())) return undefined;\n\tconst parsed = Number(value.trim());\n\treturn Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/**\n * Reads the watchdog configuration from the environment. Returns `undefined`\n * when neither binding is requested, which is the case for every host launch\n * that does not come from the lifecycle supervisor.\n */\nexport function readHostWatchdogConfig(\n\tenv: Readonly<Record<string, string | undefined>> = process.env,\n): HostWatchdogConfig | undefined {\n\tconst fd = parsePositiveInteger(env[HOST_WATCH_FD_ENV]);\n\tconst ppid = parsePositiveInteger(env[HOST_WATCH_PPID_ENV]);\n\tif (fd === undefined && ppid === undefined) return undefined;\n\tconst scratchDir = env[HOST_SCRATCH_DIR_ENV];\n\tconst cleanupPaths = env[HOST_CLEANUP_PATHS_ENV]?.split(\"\\n\").filter(Boolean);\n\treturn {\n\t\tfd,\n\t\tppid,\n\t\tscratchDir: scratchDir === undefined || scratchDir === \"\" ? undefined : scratchDir,\n\t\tcleanupPaths,\n\t};\n}\n\n/** Same configuration, resolved through the brand-aware env prefixes. */\nexport function readHostWatchdogConfigFromBrandEnv(): HostWatchdogConfig | undefined {\n\t// The lifecycle supervisor intentionally uses the canonical SENPI_RPC_HOST_*\n\t// names when spawning a child. envValue() resolves branded aliases for normal\n\t// launches, but must not hide the canonical variables from the supervisor\n\t// handoff or the lifetime binding is silently disabled.\n\treturn readHostWatchdogConfig({\n\t\t[HOST_WATCH_FD_ENV]: process.env[HOST_WATCH_FD_ENV] ?? envValue(\"RPC_HOST_WATCH_FD\"),\n\t\t[HOST_WATCH_PPID_ENV]: process.env[HOST_WATCH_PPID_ENV] ?? envValue(\"RPC_HOST_WATCH_PPID\"),\n\t\t[HOST_SCRATCH_DIR_ENV]: process.env[HOST_SCRATCH_DIR_ENV] ?? envValue(\"RPC_HOST_SCRATCH_DIR\"),\n\t\t[HOST_CLEANUP_PATHS_ENV]: process.env[HOST_CLEANUP_PATHS_ENV] ?? envValue(\"RPC_HOST_CLEANUP_PATHS\"),\n\t});\n}\n\n/**\n * Arms the configured watchdog. `onSupervisorGone` receives the reason and is\n * expected to perform the host's normal clean shutdown; the scratch directory\n * (the supervisor's private socket directory, which no longer has an owner) is\n * removed first so nothing is left behind even if shutdown then hangs.\n *\n * Takes ownership of `config.fd`: disarming (or firing) closes it, so callers\n * must not close it themselves. Returns a disarm function; a no-op when no\n * binding is configured.\n */\nexport function armHostWatchdog(\n\tconfig: HostWatchdogConfig | undefined,\n\tonSupervisorGone: (reason: string, cleanup?: Promise<void>) => void,\n): () => void {\n\tif (!config) return () => {};\n\tconst fire = (reason: string): void => {\n\t\tdisarm();\n\t\tif (process.platform === \"win32\") {\n\t\t\t// Arm the host shutdown fallback before attempting metadata cleanup. The\n\t\t\t// synchronous Win32 removal below handles the state files deterministically,\n\t\t\t// while the fallback still covers a named-pipe close that never completes.\n\t\t\tconst cleanup = Promise.resolve().then(() => cleanupWatchdogPaths(config));\n\t\t\tonSupervisorGone(reason, cleanup);\n\t\t} else {\n\t\t\tvoid cleanupWatchdogPaths(config).finally(() => onSupervisorGone(reason));\n\t\t}\n\t};\n\tconst disarmers: Array<() => void> = [];\n\tconst disarm = (): void => {\n\t\tfor (const stop of disarmers.splice(0)) stop();\n\t};\n\tif (config.fd !== undefined) disarmers.push(watchFdForEof(config.fd, fire));\n\tif (config.ppid !== undefined) disarmers.push(watchPpid(config.ppid, fire));\n\treturn disarm;\n}\n\n/**\n * EOF on the inherited pipe is the primary signal. The supervisor never writes\n * to it, so any readable data is ignored; only close matters. An fd that cannot\n * be opened (never inherited) leaves the binding inert rather than killing a\n * healthy host.\n */\nfunction watchFdForEof(fd: number, fire: (reason: string) => void): () => void {\n\tlet stream: ReturnType<typeof createReadStream>;\n\tlet streamFailed = false;\n\tlet fired = false;\n\tconst fireOnce = (reason: string): void => {\n\t\tif (fired) return;\n\t\tfired = true;\n\t\tfire(reason);\n\t};\n\ttry {\n\t\t// The watchdog owns this inherited read end. autoClose is required on\n\t\t// Win32 so the stream releases fd 3 and observes the pipe's terminal close.\n\t\tstream = createReadStream(\"\", { fd, autoClose: true });\n\t} catch {\n\t\treturn () => {};\n\t}\n\tstream.resume();\n\tconst onEnd = (): void => fireOnce(`supervisor pipe fd ${fd} closed`);\n\tconst onClose = (): void => {\n\t\tif (!streamFailed) onEnd();\n\t};\n\tconst onError = (): void => {\n\t\tstreamFailed = true;\n\t};\n\tstream.once(\"end\", onEnd);\n\t// Win32 pipe teardown may report close without end; POSIX invalid-fd close is\n\t// not supervisor death and remains inert.\n\tif (process.platform === \"win32\") stream.once(\"close\", onClose);\n\tstream.once(\"error\", onError);\n\treturn () => {\n\t\tstream.off(\"end\", onEnd);\n\t\tstream.off(\"close\", onClose);\n\t\tstream.off(\"error\", onError);\n\t\tstream.destroy();\n\t};\n}\n\n/**\n * Fallback binding: the supervisor pid is gone, or this process was reparented\n * to init because the supervisor died. Polling covers the case where no extra\n * fd could be inherited.\n */\nfunction watchPpid(supervisorPid: number, fire: (reason: string) => void): () => void {\n\tlet checking = false;\n\tlet missingIdentityChecks = 0;\n\t// Tests and embedders may bind the watchdog to this process itself; that\n\t// is a valid live binding rather than evidence of supervisor loss.\n\tif (supervisorPid === process.pid) return () => {};\n\tconst timer = setInterval(() => {\n\t\tif (!processAlive(supervisorPid)) {\n\t\t\tfire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);\n\t\t\treturn;\n\t\t}\n\t\tif (checking) return;\n\t\tchecking = true;\n\t\tvoid readProcessIdentity(supervisorPid, process.platform, HOST_WATCH_PPID_PROBE_TIMEOUT_MS)\n\t\t\t.then((result) => {\n\t\t\t\tif (result.kind === \"error\") return;\n\t\t\t\tif (result.kind === \"absent\") missingIdentityChecks++;\n\t\t\t\telse missingIdentityChecks = 0;\n\t\t\t\tif (process.ppid === supervisorPid && result.kind === \"present\") return;\n\t\t\t\tif (process.ppid === supervisorPid && missingIdentityChecks < 3) return;\n\t\t\t\tfire(`supervisor pid ${supervisorPid} is gone (ppid=${process.ppid})`);\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tchecking = false;\n\t\t\t});\n\t}, HOST_WATCH_PPID_INTERVAL_MS);\n\ttimer.unref?.();\n\treturn () => clearInterval(timer);\n}\n\nfunction processAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (cause) {\n\t\treturn cause instanceof Error && \"code\" in cause && cause.code !== \"ESRCH\";\n\t}\n}\n\nasync function cleanupWatchdogPaths(config: HostWatchdogConfig): Promise<void> {\n\tconst paths = [...(config.cleanupPaths ?? []), ...(config.scratchDir ? [config.scratchDir] : [])];\n\tif (process.platform === \"win32\") {\n\t\tfor (const path of paths) {\n\t\t\ttry {\n\t\t\t\trmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });\n\t\t\t} catch {}\n\t\t}\n\t\treturn;\n\t}\n\tawait Promise.all(\n\t\tpaths.map(async (path) => {\n\t\t\ttry {\n\t\t\t\tawait rm(path, { recursive: true, force: true });\n\t\t\t} catch {}\n\t\t}),\n\t);\n}\n"]}
|
|
@@ -46,10 +46,10 @@ export declare function resolveHostIdlePolicy(env: Readonly<Record<string, strin
|
|
|
46
46
|
maxSessions: number;
|
|
47
47
|
};
|
|
48
48
|
/**
|
|
49
|
-
* Socket
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* Socket agent events are delivered only to connections attached to their
|
|
50
|
+
* session, tagged with its routing sessionId. Content-free session lifecycle
|
|
51
|
+
* events remain visible to every connection. Responses and extension UI
|
|
52
|
+
* requests remain requester-only; foreign observation uses attach-on-open.
|
|
53
53
|
*/
|
|
54
54
|
export declare function runMultiSessionHost(options: MultiSessionHostOptions): Promise<never>;
|
|
55
55
|
export declare function createHostCore(options: MultiSessionHostOptions, writer: SessionEventWriter, capabilities?: string[], idle?: HostIdleOverrides): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multi-session-host.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/multi-session-host.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,qCAAqC,CAAC;AAgB5F,OAAO,EAAE,KAAK,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"multi-session-host.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/multi-session-host.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,qCAAqC,CAAC;AAgB5F,OAAO,EAAE,KAAK,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAC3F,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,gCAAgC,CAAC;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,iBAAiB,CAAC;CAClC;AAED,kFAAkF;AAClF,eAAO,MAAM,gCAAgC,uCAAuC,CAAC;AACrF,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAC7D,4EAA4E;AAC5E,eAAO,MAAM,0BAA0B,iCAAiC,CAAC;AACzE,sGAAsG;AACtG,eAAO,MAAM,gCAAgC,QAAc,CAAC;AAC5D,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,0GAA0G;AAC1G,eAAO,MAAM,0BAA0B,QAAc,CAAC;AAItD,kGAAkG;AAClG,MAAM,WAAW,iBAAiB;IACjC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;CACjC;AAQD,0FAA0F;AAC1F,wBAAgB,qBAAqB,CACpC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EACjD,SAAS,GAAE,iBAAsB,GAC/B;IAAE,GAAG,EAAE,MAAM,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAWzF;AASD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,KAAK,CAAC,CAG1F;AAED,wBAAgB,cAAc,CAC7B,OAAO,EAAE,uBAAuB,EAChC,MAAM,EAAE,kBAAkB,EAC1B,YAAY,WAA+D,EAC3E,IAAI,GAAE,iBAAsB;;mBAsBA,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;EAiBlD"}
|
|
@@ -12,6 +12,7 @@ import { rpcCommandShapeError } from "./rpc-input-validation.js";
|
|
|
12
12
|
import { SessionCommandRouter } from "./session-command-router.js";
|
|
13
13
|
import { SessionEventWriter } from "./session-event-writer.js";
|
|
14
14
|
import { RpcSessionRegistry } from "./session-registry.js";
|
|
15
|
+
import { authenticateSocket, ensureSocketSecret, resolveSocketTransportAddress, SOCKET_SECRET_FILE_ENV, socketSecretPath, } from "./socket-transport.js";
|
|
15
16
|
/** Environment override for the idle-session eviction window, in milliseconds. */
|
|
16
17
|
export const RPC_SESSION_IDLE_EVICTION_MS_ENV = "SENPI_RPC_SESSION_IDLE_EVICTION_MS";
|
|
17
18
|
/** Environment override for the concurrent open-session cap. */
|
|
@@ -24,6 +25,8 @@ export const DEFAULT_SESSION_IDLE_EVICTION_MS = 30 * 60_000;
|
|
|
24
25
|
export const DEFAULT_MAX_SESSIONS = 8;
|
|
25
26
|
/** Default empty-host exit: 15 minutes with zero open sessions, matching the supervisor's idle window. */
|
|
26
27
|
export const DEFAULT_HOST_EMPTY_EXIT_MS = 15 * 60_000;
|
|
28
|
+
/** Win32 named-pipe close can leave libuv's server callback pending after handles are destroyed. */
|
|
29
|
+
const WINDOWS_SHUTDOWN_HARD_EXIT_MS = 2_000;
|
|
27
30
|
function parseSessionCap(value) {
|
|
28
31
|
if (value === undefined || !/^\d+$/.test(value.trim()))
|
|
29
32
|
return undefined;
|
|
@@ -42,10 +45,10 @@ export function resolveHostIdlePolicy(env, overrides = {}) {
|
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
47
|
/**
|
|
45
|
-
* Socket
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
48
|
+
* Socket agent events are delivered only to connections attached to their
|
|
49
|
+
* session, tagged with its routing sessionId. Content-free session lifecycle
|
|
50
|
+
* events remain visible to every connection. Responses and extension UI
|
|
51
|
+
* requests remain requester-only; foreign observation uses attach-on-open.
|
|
49
52
|
*/
|
|
50
53
|
export async function runMultiSessionHost(options) {
|
|
51
54
|
if (options.listen === undefined || options.listen === "stdio://")
|
|
@@ -132,78 +135,108 @@ async function runSocketHost(options, socketPath) {
|
|
|
132
135
|
{ onEmptyExit: () => void shutdown(0), canExitWhenEmpty: () => connections.size === 0 });
|
|
133
136
|
let nextConnection = 0;
|
|
134
137
|
let shuttingDown = false;
|
|
138
|
+
const secret = process.platform === "win32"
|
|
139
|
+
? await ensureSocketSecret(process.env[SOCKET_SECRET_FILE_ENV] ?? socketSecretPath(socketPath))
|
|
140
|
+
: undefined;
|
|
135
141
|
const server = createServer((socket) => {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.withConnection(id, () => handle(line))
|
|
144
|
-
.catch((cause) => {
|
|
145
|
-
process.stderr.write(`senpi rpc connection ${id} failed: ${errorMessage(cause)}\n`);
|
|
146
|
-
});
|
|
147
|
-
}, {
|
|
148
|
-
maxLineLength: MAX_RPC_LINE_CHARACTERS,
|
|
149
|
-
onOversizedLine: () => {
|
|
142
|
+
const accept = () => {
|
|
143
|
+
const id = `socket-${++nextConnection}`;
|
|
144
|
+
const sink = socketSink(socket);
|
|
145
|
+
writer.registerConnection(id, sink);
|
|
146
|
+
const detachReader = attachJsonlLineReader(socket, (line) => {
|
|
147
|
+
// Do not serialize awaited commands: extension_ui_response and other
|
|
148
|
+
// re-entrant frames must be able to resolve a command already awaiting them.
|
|
150
149
|
void writer
|
|
151
|
-
.withConnection(id, () =>
|
|
152
|
-
.catch((cause) =>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
connections.delete(id);
|
|
163
|
-
// A socket that dies without close_session still owns its sessions' attachments
|
|
164
|
-
// and path reservations. Release them on the command chain so this runs after any
|
|
165
|
-
// in-flight command for this connection settles, otherwise the path stays pinned
|
|
166
|
-
// by a runtime whose client is gone and later resumes attach to that orphan.
|
|
167
|
-
void router.releaseConnection(id).catch((cause) => {
|
|
168
|
-
process.stderr.write(`senpi rpc connection ${id} release failed: ${errorMessage(cause)}\n`);
|
|
150
|
+
.withConnection(id, () => handle(line))
|
|
151
|
+
.catch((cause) => {
|
|
152
|
+
process.stderr.write(`senpi rpc connection ${id} failed: ${errorMessage(cause)}\n`);
|
|
153
|
+
});
|
|
154
|
+
}, {
|
|
155
|
+
maxLineLength: MAX_RPC_LINE_CHARACTERS,
|
|
156
|
+
onOversizedLine: () => {
|
|
157
|
+
void writer
|
|
158
|
+
.withConnection(id, () => writer.enqueueControl(parseError(oversizedLineError())))
|
|
159
|
+
.catch((cause) => process.stderr.write(`senpi rpc connection ${id} failed: ${errorMessage(cause)}\n`));
|
|
160
|
+
},
|
|
169
161
|
});
|
|
162
|
+
let detached = false;
|
|
163
|
+
const detach = () => {
|
|
164
|
+
if (detached)
|
|
165
|
+
return;
|
|
166
|
+
detached = true;
|
|
167
|
+
detachReader();
|
|
168
|
+
writer.unregisterConnection(id);
|
|
169
|
+
connections.delete(id);
|
|
170
|
+
// A socket that dies without close_session still owns its sessions' attachments
|
|
171
|
+
// and path reservations. Release them on the command chain so this runs after any
|
|
172
|
+
// in-flight command for this connection settles, otherwise the path stays pinned
|
|
173
|
+
// by a runtime whose client is gone and later resumes attach to that orphan.
|
|
174
|
+
void router.releaseConnection(id).catch((cause) => {
|
|
175
|
+
process.stderr.write(`senpi rpc connection ${id} release failed: ${errorMessage(cause)}\n`);
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
connections.set(id, { id, sink, detach, close: () => socket.destroy() });
|
|
179
|
+
socket.once("close", detach);
|
|
180
|
+
socket.once("error", () => detach());
|
|
170
181
|
};
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
182
|
+
if (secret)
|
|
183
|
+
authenticateSocket(socket, secret, accept);
|
|
184
|
+
else
|
|
185
|
+
accept();
|
|
174
186
|
});
|
|
175
187
|
server.on("error", (cause) => {
|
|
176
188
|
if (!shuttingDown)
|
|
177
189
|
process.stderr.write(`senpi rpc socket listener failed: ${errorMessage(cause)}\n`);
|
|
178
190
|
});
|
|
179
|
-
const shutdown = async (exitCode = 0) => {
|
|
191
|
+
const shutdown = async (exitCode = 0, watchdogCleanup) => {
|
|
180
192
|
if (shuttingDown)
|
|
181
193
|
process.exit(exitCode);
|
|
182
194
|
shuttingDown = true;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
195
|
+
// On Windows, destroying named-pipe sockets does not always make libuv's
|
|
196
|
+
// server.close callback fire: connected pipe instances can remain in the
|
|
197
|
+
// kernel after the JavaScript handles are destroyed. Keep the normal drain
|
|
198
|
+
// path, but never let that platform-specific close stall orphan the host.
|
|
199
|
+
try {
|
|
200
|
+
for (const connection of connections.values()) {
|
|
201
|
+
connection.detach();
|
|
202
|
+
connection.close();
|
|
203
|
+
}
|
|
204
|
+
await (process.platform === "win32"
|
|
205
|
+
? Promise.race([closeServer(server), delay(WINDOWS_SHUTDOWN_HARD_EXIT_MS)])
|
|
206
|
+
: closeServer(server));
|
|
207
|
+
await router.dispose();
|
|
208
|
+
await writer.flush();
|
|
209
|
+
await removeSocketPath(socketPath);
|
|
210
|
+
if (watchdogCleanup)
|
|
211
|
+
await watchdogCleanup;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
// Explicitly terminate after every shutdown trigger. Windows named-pipe
|
|
215
|
+
// handles are not fully controllable from JS, and an unresolved cleanup
|
|
216
|
+
// must not leave this daemon or its public endpoint alive.
|
|
217
|
+
process.exit(exitCode);
|
|
186
218
|
}
|
|
187
|
-
await closeServer(server);
|
|
188
|
-
await router.dispose();
|
|
189
|
-
await writer.flush();
|
|
190
|
-
await removeSocketPath(socketPath);
|
|
191
|
-
process.exit(exitCode);
|
|
192
219
|
};
|
|
193
220
|
registerShutdownSignals(shutdown);
|
|
194
221
|
// Arm before listen: a supervisor death during the listen transition must
|
|
195
222
|
// still close the child and clean its private endpoint.
|
|
196
|
-
armHostWatchdog(readHostWatchdogConfigFromBrandEnv(), (reason) => {
|
|
223
|
+
armHostWatchdog(readHostWatchdogConfigFromBrandEnv(), (reason, cleanup) => {
|
|
197
224
|
process.stderr.write(`senpi rpc host: ${reason}; shutting down\n`);
|
|
198
|
-
|
|
199
|
-
|
|
225
|
+
// Enter shutdown before killing session-owned child processes. The Windows
|
|
226
|
+
// tree killer is synchronous, while the shutdown fallback must be armed
|
|
227
|
+
// before any such cleanup can delay the event loop.
|
|
228
|
+
void shutdown(0, cleanup);
|
|
229
|
+
setImmediate(killTrackedDetachedChildren);
|
|
200
230
|
});
|
|
201
|
-
await listen(server, socketPath);
|
|
231
|
+
await listen(server, socketPath, secret);
|
|
202
232
|
process.stderr.write(`senpi rpc listening on ${formatSocketAddress(socketPath)}\n`);
|
|
203
233
|
// Opt-in only: set by the lifecycle supervisor so this host can never outlive
|
|
204
234
|
// it, including when the supervisor is SIGKILLed and runs no handler at all.
|
|
205
235
|
return new Promise(() => { });
|
|
206
236
|
}
|
|
237
|
+
function delay(ms) {
|
|
238
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
239
|
+
}
|
|
207
240
|
function parseError(error) {
|
|
208
241
|
return { type: "response", command: "parse", success: false, error };
|
|
209
242
|
}
|
|
@@ -253,7 +286,7 @@ function socketSink(socket) {
|
|
|
253
286
|
};
|
|
254
287
|
}
|
|
255
288
|
async function prepareSocketPath(socketPath) {
|
|
256
|
-
if (socketPath.startsWith("\0"))
|
|
289
|
+
if (process.platform === "win32" || socketPath.startsWith("\0"))
|
|
257
290
|
return;
|
|
258
291
|
await mkdir(dirname(socketPath), { recursive: true, mode: 0o700 });
|
|
259
292
|
try {
|
|
@@ -270,7 +303,7 @@ async function prepareSocketPath(socketPath) {
|
|
|
270
303
|
}
|
|
271
304
|
function probeSocket(socketPath) {
|
|
272
305
|
return new Promise((resolve) => {
|
|
273
|
-
const socket = createConnection(socketPath);
|
|
306
|
+
const socket = createConnection(resolveSocketTransportAddress(socketPath, process.platform));
|
|
274
307
|
const settle = (live) => {
|
|
275
308
|
socket.destroy();
|
|
276
309
|
resolve(live);
|
|
@@ -280,14 +313,19 @@ function probeSocket(socketPath) {
|
|
|
280
313
|
socket.setTimeout(1_000, () => settle(false));
|
|
281
314
|
});
|
|
282
315
|
}
|
|
283
|
-
function listen(server, socketPath) {
|
|
316
|
+
function listen(server, socketPath, secret) {
|
|
284
317
|
return new Promise((resolve, reject) => {
|
|
285
318
|
server.once("error", reject);
|
|
286
|
-
server.listen(socketPath, async () => {
|
|
319
|
+
server.listen(resolveSocketTransportAddress(socketPath, process.platform, secret), async () => {
|
|
287
320
|
server.off("error", reject);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
321
|
+
try {
|
|
322
|
+
if (process.platform !== "win32" && !socketPath.startsWith("\0"))
|
|
323
|
+
await chmod(socketPath, 0o600);
|
|
324
|
+
resolve();
|
|
325
|
+
}
|
|
326
|
+
catch (cause) {
|
|
327
|
+
reject(cause);
|
|
328
|
+
}
|
|
291
329
|
});
|
|
292
330
|
});
|
|
293
331
|
}
|
|
@@ -297,7 +335,7 @@ function closeServer(server) {
|
|
|
297
335
|
});
|
|
298
336
|
}
|
|
299
337
|
async function removeSocketPath(socketPath) {
|
|
300
|
-
if (socketPath.startsWith("\0"))
|
|
338
|
+
if (process.platform === "win32" || socketPath.startsWith("\0"))
|
|
301
339
|
return;
|
|
302
340
|
try {
|
|
303
341
|
await unlink(socketPath);
|