@khorsheed/dsh-ankh-guard 0.2.0 → 0.3.0
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 +9 -0
- package/README.en.md +4 -4
- package/README.i18n.yaml +2 -2
- package/README.md +4 -4
- package/lib/cli.js +1 -1
- package/lib/client.js +104 -17
- package/lib/index.js +29 -6
- package/lib/{restart-context-D6nISh28.js → restart-context-DlaDZWKq.js} +12 -7
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.js +108 -24
- package/lib/types/index.js +39 -6
- package/lib/types/restart-context.js +11 -6
- package/package.json +9 -9
- package/lib/processes-BjZgJjQr.js +0 -344
- package/lib/state-4f7yny39.js +0 -286
- package/lib/test-seam-DnvLWTeO.js +0 -119
package/lib/types/index.js
CHANGED
|
@@ -42,6 +42,24 @@ export function deriveSessionPreset(host, session) {
|
|
|
42
42
|
}
|
|
43
43
|
return host.resolveSessionPreset?.(session);
|
|
44
44
|
}
|
|
45
|
+
/** Probe the session-persistence service's cold-read face. */
|
|
46
|
+
function probeColdReader(ctx) {
|
|
47
|
+
const service = ctx.get('sessionPersistence');
|
|
48
|
+
if (service === undefined || service === null)
|
|
49
|
+
return undefined;
|
|
50
|
+
return typeof service.open === 'function' ? service : undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Read one persisted session's header and full log through a read handle. */
|
|
53
|
+
async function readColdLog(persistence, id) {
|
|
54
|
+
const handle = await persistence.open(id, 'read');
|
|
55
|
+
try {
|
|
56
|
+
const cold = await handle.read(0);
|
|
57
|
+
return { meta: handle.header, events: cold.events };
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await handle.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
45
63
|
export const Config = z.object({
|
|
46
64
|
maxAgeMinutes: z.natural().min(1).default(10),
|
|
47
65
|
stateDir: z.string().default(''),
|
|
@@ -180,10 +198,10 @@ export function apply(ctx, config) {
|
|
|
180
198
|
if (probe === undefined) {
|
|
181
199
|
probe = (async () => {
|
|
182
200
|
try {
|
|
183
|
-
const persistence = ctx
|
|
201
|
+
const persistence = probeColdReader(ctx);
|
|
184
202
|
if (persistence === undefined)
|
|
185
203
|
return false;
|
|
186
|
-
const { events } = await persistence
|
|
204
|
+
const { events } = await readColdLog(persistence, id);
|
|
187
205
|
return isParkedOnUserInput(events);
|
|
188
206
|
}
|
|
189
207
|
catch {
|
|
@@ -220,7 +238,19 @@ export function apply(ctx, config) {
|
|
|
220
238
|
return;
|
|
221
239
|
const id = agent.id;
|
|
222
240
|
const exitAt = pendingContinue.get(id);
|
|
223
|
-
|
|
241
|
+
let record = followupReport ? pendingRestartRecord(stateDir) : null;
|
|
242
|
+
// A bare PLANNED outcome with no initiating session (the restart was
|
|
243
|
+
// driven from outside the host — the operator's terminal already has
|
|
244
|
+
// the announcement) has no in-host owner: settle the record instead of
|
|
245
|
+
// waking whichever root session happens to mount first. Records
|
|
246
|
+
// carrying diagnostics someone must hear about — an unplanned
|
|
247
|
+
// recovery, a composition rollback, a failed restart — keep the
|
|
248
|
+
// first-created claim.
|
|
249
|
+
if (record !== null && record.initiator === undefined
|
|
250
|
+
&& record.unexpected !== true && record.compositionRecovered !== true && record.error === undefined) {
|
|
251
|
+
acknowledgeRestartRecord(stateDir, record, Date.now());
|
|
252
|
+
record = null;
|
|
253
|
+
}
|
|
224
254
|
const owesReport = record !== null && (record.initiator === undefined || id === record.initiator);
|
|
225
255
|
if (exitAt !== undefined) {
|
|
226
256
|
void (async () => {
|
|
@@ -326,10 +356,13 @@ export function apply(ctx, config) {
|
|
|
326
356
|
agentOptions.model = selection.model;
|
|
327
357
|
let setup;
|
|
328
358
|
const presets = ctx.get('agentPresets');
|
|
329
|
-
const persistence = ctx
|
|
359
|
+
const persistence = probeColdReader(ctx);
|
|
330
360
|
if (presets !== undefined && persistence !== undefined) {
|
|
331
|
-
const inspected = await persistence
|
|
332
|
-
const presetId = deriveSessionPreset(agentPresetsHost, {
|
|
361
|
+
const inspected = await readColdLog(persistence, id);
|
|
362
|
+
const presetId = deriveSessionPreset(agentPresetsHost, {
|
|
363
|
+
header: inspected.meta,
|
|
364
|
+
events: inspected.events,
|
|
365
|
+
});
|
|
333
366
|
setup = async (agentCtx) => { await presets.mount(agentCtx, (await presets.resolve(presetId)).id); };
|
|
334
367
|
}
|
|
335
368
|
return { resumeSessionId: id, agentOptions, ...(setup === undefined ? {} : { setup }) };
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
* Restart-record context injection and interrupted-session continuity. After
|
|
3
3
|
* a scheduled restart, the FULL report waits for the initiating session's
|
|
4
4
|
* root agent, whenever it resumes (session restore is lazy, so no other
|
|
5
|
-
* session is ever woken for reporting)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* session is ever woken for reporting). A record without an initiator splits
|
|
6
|
+
* by kind: one carrying diagnostics someone must hear about (an unplanned
|
|
7
|
+
* recovery, a composition rollback, a failed restart) is claimed by the
|
|
8
|
+
* first root agent created, while a bare planned outcome — the restart was
|
|
9
|
+
* driven from outside the host and the operator's terminal already has the
|
|
10
|
+
* announcement — is settled without waking anyone. Separately, a snapshot
|
|
11
|
+
* written at SIGTERM time (`interrupted-sessions.json`) records which root
|
|
12
|
+
* sessions had a live turn when the process stopped, so the next restart
|
|
13
|
+
* boot can resume those sessions and queue a "continue" turn. Pure logic
|
|
14
|
+
* reads the durable files; the plugin wires them into `agent/created` and
|
|
15
|
+
* `agent.followup`.
|
|
11
16
|
*/
|
|
12
17
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
18
|
import { stateFile } from "./state-files.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khorsheed/dsh-ankh-guard",
|
|
3
3
|
"description": "Hard gate for self-modification restarts: a green-build credential bound to the git HEAD, checked before any restart of the running instance",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
64
64
|
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
|
|
65
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.
|
|
67
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
68
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
69
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
70
|
-
"@deepseek-ai/dsh-skill": "^0.1.
|
|
65
|
+
"@deepseek-ai/dsh-agent": "^0.1.5-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-skill": "^0.1.5-rc.1",
|
|
71
71
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
72
72
|
"@types/node": "^22.0.0",
|
|
73
73
|
"tsdown": "^0.22.2",
|
|
@@ -95,9 +95,9 @@
|
|
|
95
95
|
"immediately": true
|
|
96
96
|
},
|
|
97
97
|
"compat": {
|
|
98
|
-
"minHost": "0.1.
|
|
98
|
+
"minHost": "0.1.5-rc.1",
|
|
99
99
|
"notes": "composition-preflight runs via the standalone preflight-runner and degrades to a notice when no live harness checkout resolves; original-tab browser handoff feature-probes optional WebServer/connection auth seams; reversible state quarantine was exercised in a live 0.1.1-rc.2 to 0.1.2-alpha.4 cutover",
|
|
100
|
-
"verifiedHost": "0.1.
|
|
100
|
+
"verifiedHost": "0.1.5-rc.1"
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
}
|
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
//#region lib/types/processes.js
|
|
5
|
-
/**
|
|
6
|
-
* Process primitives shared by the guard CLI and the exit agent: locating the
|
|
7
|
-
* listener on a port and killing a process tree. One owner — the watchdog's
|
|
8
|
-
* bash side keeps its own copy (different runtime), but every TypeScript
|
|
9
|
-
* caller goes through here.
|
|
10
|
-
*/
|
|
11
|
-
function executable(name, candidates) {
|
|
12
|
-
return candidates.find((candidate) => existsSync(candidate)) ?? name;
|
|
13
|
-
}
|
|
14
|
-
const LSOF = executable("lsof", ["/usr/sbin/lsof", "/usr/bin/lsof"]);
|
|
15
|
-
const PS = executable("ps", ["/bin/ps", "/usr/bin/ps"]);
|
|
16
|
-
const PGREP = executable("pgrep", ["/usr/bin/pgrep", "/bin/pgrep"]);
|
|
17
|
-
const SYSCTL = executable("sysctl", ["/usr/sbin/sysctl", "/sbin/sysctl"]);
|
|
18
|
-
const PYTHON = executable("python3", ["/usr/bin/python3", "/opt/homebrew/bin/python3"]);
|
|
19
|
-
const DARWIN_START_TOKEN = [
|
|
20
|
-
"import ctypes,struct,sys",
|
|
21
|
-
"p=int(sys.argv[1])",
|
|
22
|
-
"b=ctypes.create_string_buffer(136)",
|
|
23
|
-
"n=ctypes.CDLL(\"/usr/lib/libproc.dylib\").proc_pidinfo(p,3,0,b,136)",
|
|
24
|
-
"n == 136 or sys.exit(1)",
|
|
25
|
-
"s,u=struct.unpack_from(\"QQ\",b.raw,120)",
|
|
26
|
-
"print(f\"darwin:{s}:{u}\",end=\"\")"
|
|
27
|
-
].join(";");
|
|
28
|
-
/** Every process listening on a TCP port. An empty list also covers unavailable lsof. */
|
|
29
|
-
function findPidsOnPort(port) {
|
|
30
|
-
try {
|
|
31
|
-
const out = execFileSync(LSOF, [
|
|
32
|
-
`-tiTCP:${port}`,
|
|
33
|
-
"-sTCP:LISTEN",
|
|
34
|
-
"-P"
|
|
35
|
-
], {
|
|
36
|
-
encoding: "utf8",
|
|
37
|
-
stdio: "pipe"
|
|
38
|
-
}).trim();
|
|
39
|
-
return [...new Set(out.split("\n").map(Number).filter((pid) => Number.isInteger(pid) && pid > 0))];
|
|
40
|
-
} catch {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/** TCP listen ports owned directly by one PID, using the same absolute lsof resolution. */
|
|
45
|
-
function listeningPortsForPid(pid) {
|
|
46
|
-
try {
|
|
47
|
-
const out = execFileSync(LSOF, [
|
|
48
|
-
"-nP",
|
|
49
|
-
"-a",
|
|
50
|
-
"-p",
|
|
51
|
-
String(pid),
|
|
52
|
-
"-iTCP",
|
|
53
|
-
"-sTCP:LISTEN"
|
|
54
|
-
], {
|
|
55
|
-
encoding: "utf8",
|
|
56
|
-
stdio: "pipe"
|
|
57
|
-
});
|
|
58
|
-
return [...new Set([...out.matchAll(/:(\d+) \(LISTEN\)/g)].map((match) => Number(match[1])).filter(Number.isInteger))];
|
|
59
|
-
} catch {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/** The first process listening on a TCP port, or null when none is (via lsof). */
|
|
64
|
-
function findPidOnPort(port) {
|
|
65
|
-
return findPidsOnPort(port)[0]?.toString() ?? null;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Current identity for a live process, or null when it cannot be proved.
|
|
69
|
-
* Linux exposes a boot-scoped kernel start tick. Other POSIX hosts do not,
|
|
70
|
-
* so bind the start instant to the boot, uid, session, and settled command.
|
|
71
|
-
* This materially strengthens macOS ps(1)'s second-granularity lstart; all
|
|
72
|
-
* guard captures occur after the watchdog/child has reached its steady argv.
|
|
73
|
-
*/
|
|
74
|
-
function processIdentity(pid) {
|
|
75
|
-
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
76
|
-
try {
|
|
77
|
-
const status = execFileSync(PS, [
|
|
78
|
-
"-o",
|
|
79
|
-
"stat=",
|
|
80
|
-
"-p",
|
|
81
|
-
String(pid)
|
|
82
|
-
], {
|
|
83
|
-
encoding: "utf8",
|
|
84
|
-
stdio: "pipe"
|
|
85
|
-
}).trim();
|
|
86
|
-
if (status === "" || status.startsWith("Z")) return null;
|
|
87
|
-
if (process.platform === "darwin") try {
|
|
88
|
-
const nativeStart = execFileSync(PYTHON, [
|
|
89
|
-
"-c",
|
|
90
|
-
DARWIN_START_TOKEN,
|
|
91
|
-
String(pid)
|
|
92
|
-
], {
|
|
93
|
-
encoding: "utf8",
|
|
94
|
-
stdio: "pipe"
|
|
95
|
-
}).trim();
|
|
96
|
-
if (/^darwin:\d+:\d+$/.test(nativeStart)) return {
|
|
97
|
-
pid,
|
|
98
|
-
startToken: nativeStart
|
|
99
|
-
};
|
|
100
|
-
} catch {}
|
|
101
|
-
const procStat = `/proc/${pid}/stat`;
|
|
102
|
-
if (existsSync(procStat)) {
|
|
103
|
-
const stat = readFileSync(procStat, "utf8");
|
|
104
|
-
const commandEnd = stat.lastIndexOf(")");
|
|
105
|
-
const startTicks = (commandEnd < 0 ? [] : stat.slice(commandEnd + 1).trim().split(/\s+/))[19];
|
|
106
|
-
if (startTicks === void 0 || startTicks === "") return null;
|
|
107
|
-
let bootId = "unknown-boot";
|
|
108
|
-
try {
|
|
109
|
-
bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim() || bootId;
|
|
110
|
-
} catch {}
|
|
111
|
-
return {
|
|
112
|
-
pid,
|
|
113
|
-
startToken: `linux:${bootId}:${startTicks}`
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
const processEvidence = execFileSync(PS, [
|
|
117
|
-
"-o",
|
|
118
|
-
"sess=",
|
|
119
|
-
"-o",
|
|
120
|
-
"uid=",
|
|
121
|
-
"-o",
|
|
122
|
-
"lstart=",
|
|
123
|
-
"-o",
|
|
124
|
-
"command=",
|
|
125
|
-
"-p",
|
|
126
|
-
String(pid)
|
|
127
|
-
], {
|
|
128
|
-
encoding: "utf8",
|
|
129
|
-
stdio: "pipe"
|
|
130
|
-
}).trim();
|
|
131
|
-
if (processEvidence === "") return null;
|
|
132
|
-
let bootEvidence = "unknown-boot";
|
|
133
|
-
try {
|
|
134
|
-
bootEvidence = execFileSync(SYSCTL, ["-n", "kern.boottime"], {
|
|
135
|
-
encoding: "utf8",
|
|
136
|
-
stdio: "pipe"
|
|
137
|
-
}).trim() || bootEvidence;
|
|
138
|
-
} catch {}
|
|
139
|
-
return {
|
|
140
|
-
pid,
|
|
141
|
-
startToken: `posix:${createHash("sha256").update(bootEvidence).update("\0").update(processEvidence).digest("hex")}`
|
|
142
|
-
};
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/** Whether the same, non-recycled process is still alive. */
|
|
148
|
-
function processIdentityMatches(identity) {
|
|
149
|
-
return processIdentity(identity.pid)?.startToken === identity.startToken;
|
|
150
|
-
}
|
|
151
|
-
function parentPid(pid) {
|
|
152
|
-
try {
|
|
153
|
-
const value = Number(execFileSync(PS, [
|
|
154
|
-
"-o",
|
|
155
|
-
"ppid=",
|
|
156
|
-
"-p",
|
|
157
|
-
String(pid)
|
|
158
|
-
], {
|
|
159
|
-
encoding: "utf8",
|
|
160
|
-
stdio: "pipe"
|
|
161
|
-
}).trim());
|
|
162
|
-
return Number.isInteger(value) && value > 0 ? value : null;
|
|
163
|
-
} catch {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
/** The live POSIX process-group id for one PID, or null when unavailable. */
|
|
168
|
-
function processGroupId(pid) {
|
|
169
|
-
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
170
|
-
try {
|
|
171
|
-
const value = Number(execFileSync(PS, [
|
|
172
|
-
"-o",
|
|
173
|
-
"pgid=",
|
|
174
|
-
"-p",
|
|
175
|
-
String(pid)
|
|
176
|
-
], {
|
|
177
|
-
encoding: "utf8",
|
|
178
|
-
stdio: "pipe"
|
|
179
|
-
}).trim());
|
|
180
|
-
return Number.isInteger(value) && value > 0 ? value : null;
|
|
181
|
-
} catch {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Prove that exactly one port listener belongs to a supervisor and return the
|
|
187
|
-
* direct child root through which that supervisor owns it. This is captured
|
|
188
|
-
* before cutover; the successor must stop this identity, never an arbitrary
|
|
189
|
-
* process discovered later from the shared port.
|
|
190
|
-
*/
|
|
191
|
-
function findOwnedListener(port, supervisorPid) {
|
|
192
|
-
const listeners = findPidsOnPort(port);
|
|
193
|
-
if (listeners.length !== 1) return null;
|
|
194
|
-
const listenerPid = listeners[0];
|
|
195
|
-
if (listenerPid === void 0) return null;
|
|
196
|
-
const listener = processIdentity(listenerPid);
|
|
197
|
-
if (listener === null) return null;
|
|
198
|
-
let cursor = listenerPid;
|
|
199
|
-
let childRoot = null;
|
|
200
|
-
const seen = /* @__PURE__ */ new Set();
|
|
201
|
-
while (cursor > 0 && !seen.has(cursor)) {
|
|
202
|
-
seen.add(cursor);
|
|
203
|
-
const parent = parentPid(cursor);
|
|
204
|
-
if (parent === supervisorPid) {
|
|
205
|
-
childRoot = cursor;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
if (parent === null) return null;
|
|
209
|
-
cursor = parent;
|
|
210
|
-
}
|
|
211
|
-
if (childRoot === null) return null;
|
|
212
|
-
const child = processIdentity(childRoot);
|
|
213
|
-
return child === null ? null : {
|
|
214
|
-
child,
|
|
215
|
-
listener
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
/** POSIX single-quote one word for a shell command line. */
|
|
219
|
-
function shellQuote(word) {
|
|
220
|
-
return `'${word.replace(/'/g, "'\\''")}'`;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Discover how the process on a port was launched — its exact argv from
|
|
224
|
-
* `ps -o command=`, its cwd from lsof, and its DSH_* environment from
|
|
225
|
-
* `ps eww` — rendered as a shell command. This exists because the agent's
|
|
226
|
-
* sandbox blocks ps entirely, so every fresh-machine agent fell into a
|
|
227
|
-
* process-tree archaeology loop before its first restart; the CLI (running
|
|
228
|
-
* unsandboxed) answers the same question mechanically and reliably. Returns
|
|
229
|
-
* null when the process is gone or ps/lsof are unavailable.
|
|
230
|
-
* @param pid - the listener's pid.
|
|
231
|
-
*/
|
|
232
|
-
function discoverLaunchCommand(pid) {
|
|
233
|
-
let argv;
|
|
234
|
-
let cwd;
|
|
235
|
-
try {
|
|
236
|
-
argv = execFileSync(PS, [
|
|
237
|
-
"-o",
|
|
238
|
-
"command=",
|
|
239
|
-
"-p",
|
|
240
|
-
pid
|
|
241
|
-
], {
|
|
242
|
-
encoding: "utf8",
|
|
243
|
-
stdio: "pipe"
|
|
244
|
-
}).trim();
|
|
245
|
-
if (argv === "") return null;
|
|
246
|
-
} catch (error) {
|
|
247
|
-
throw new Error(`ps unavailable: ${String(error)}`);
|
|
248
|
-
}
|
|
249
|
-
try {
|
|
250
|
-
const out = execFileSync(LSOF, [
|
|
251
|
-
"-a",
|
|
252
|
-
"-p",
|
|
253
|
-
pid,
|
|
254
|
-
"-d",
|
|
255
|
-
"cwd",
|
|
256
|
-
"-Fn"
|
|
257
|
-
], {
|
|
258
|
-
encoding: "utf8",
|
|
259
|
-
stdio: "pipe"
|
|
260
|
-
});
|
|
261
|
-
const match = /^n(.+)$/m.exec(out);
|
|
262
|
-
if (match === null) return null;
|
|
263
|
-
cwd = match[1] ?? "";
|
|
264
|
-
} catch (error) {
|
|
265
|
-
throw new Error(`lsof cwd unavailable: ${String(error)}`);
|
|
266
|
-
}
|
|
267
|
-
const TRANSIENT = /* @__PURE__ */ new Set([
|
|
268
|
-
"DSH_ANKH_RESTART_DRIVER",
|
|
269
|
-
"DSH_SESSION_ID",
|
|
270
|
-
"DSH_SESSION_JSONL",
|
|
271
|
-
"DSH_WEB_URL",
|
|
272
|
-
"DSH_SHELL"
|
|
273
|
-
]);
|
|
274
|
-
const env = {};
|
|
275
|
-
try {
|
|
276
|
-
const out = execFileSync(PS, [
|
|
277
|
-
"eww",
|
|
278
|
-
"-o",
|
|
279
|
-
"command",
|
|
280
|
-
"-p",
|
|
281
|
-
pid
|
|
282
|
-
], {
|
|
283
|
-
encoding: "utf8",
|
|
284
|
-
stdio: "pipe"
|
|
285
|
-
});
|
|
286
|
-
for (const token of out.split(/\s+/)) {
|
|
287
|
-
const eq = token.indexOf("=");
|
|
288
|
-
if (eq > 0 && token.slice(0, eq).startsWith("DSH_") && !TRANSIENT.has(token.slice(0, eq))) env[token.slice(0, eq)] = token.slice(eq + 1);
|
|
289
|
-
}
|
|
290
|
-
} catch {}
|
|
291
|
-
const envPart = Object.entries(env).map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ");
|
|
292
|
-
return `cd ${shellQuote(cwd)} && ${envPart !== "" ? `${envPart} ` : ""}${argv}`;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Kill a pid AND its descendants, deepest first (best effort). The supervised
|
|
296
|
-
* instance may have forked children; a plain signal on the pid alone would
|
|
297
|
-
* orphan them (the EADDRINUSE race the watchdog's EADDRINUSE branch exists
|
|
298
|
-
* for). The process-group model is NOT assumed — the instance is not
|
|
299
|
-
* setsid'd — so the sweep walks `pgrep -P` instead. `pgrep` missing or
|
|
300
|
-
* returning nothing is fine: the pid itself still gets the signal.
|
|
301
|
-
*/
|
|
302
|
-
function signalFrozenTree(pid, signal) {
|
|
303
|
-
let safe = true;
|
|
304
|
-
let children = [];
|
|
305
|
-
try {
|
|
306
|
-
const out = execFileSync(PGREP, ["-P", String(pid)], {
|
|
307
|
-
encoding: "utf8",
|
|
308
|
-
stdio: "pipe"
|
|
309
|
-
}).trim();
|
|
310
|
-
children = out === "" ? [] : out.split("\n");
|
|
311
|
-
} catch {}
|
|
312
|
-
for (const raw of children) {
|
|
313
|
-
const child = Number(raw);
|
|
314
|
-
if (!Number.isInteger(child) || child <= 0) continue;
|
|
315
|
-
try {
|
|
316
|
-
process.kill(child, "SIGSTOP");
|
|
317
|
-
} catch {
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (parentPid(child) !== pid) {
|
|
321
|
-
safe = false;
|
|
322
|
-
try {
|
|
323
|
-
process.kill(child, "SIGCONT");
|
|
324
|
-
} catch {}
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
safe = signalFrozenTree(child, signal) && safe;
|
|
328
|
-
}
|
|
329
|
-
try {
|
|
330
|
-
process.kill(pid, signal);
|
|
331
|
-
if (signal !== "SIGKILL") process.kill(pid, "SIGCONT");
|
|
332
|
-
} catch {}
|
|
333
|
-
return safe;
|
|
334
|
-
}
|
|
335
|
-
function killPidTree(pid, signal) {
|
|
336
|
-
try {
|
|
337
|
-
process.kill(pid, "SIGSTOP");
|
|
338
|
-
} catch {
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
signalFrozenTree(pid, signal);
|
|
342
|
-
}
|
|
343
|
-
//#endregion
|
|
344
|
-
export { listeningPortsForPid as a, processIdentityMatches as c, killPidTree as i, findOwnedListener as n, processGroupId as o, findPidOnPort as r, processIdentity as s, discoverLaunchCommand as t };
|