@gtkx/utils 2.0.0-beta.1 → 2.0.0-beta.10
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/README.md +43 -40
- package/dist/process/index.d.ts +3 -1
- package/dist/process/index.d.ts.map +1 -1
- package/dist/process/index.js +3 -1
- package/dist/process/index.js.map +1 -1
- package/dist/process/kill-marked-processes.d.ts +3 -2
- package/dist/process/kill-marked-processes.d.ts.map +1 -1
- package/dist/process/kill-marked-processes.js +39 -13
- package/dist/process/kill-marked-processes.js.map +1 -1
- package/dist/process/kill-process-group.d.ts +16 -0
- package/dist/process/kill-process-group.d.ts.map +1 -0
- package/dist/process/kill-process-group.js +70 -0
- package/dist/process/kill-process-group.js.map +1 -0
- package/dist/process/process-guard.js +192 -6
- package/dist/process/process-guard.js.map +1 -1
- package/dist/process/process-status.d.ts +5 -0
- package/dist/process/process-status.d.ts.map +1 -0
- package/dist/process/process-status.js +19 -0
- package/dist/process/process-status.js.map +1 -0
- package/dist/process/spawn-with-parent-death-signal.d.ts +7 -1
- package/dist/process/spawn-with-parent-death-signal.d.ts.map +1 -1
- package/dist/process/spawn-with-parent-death-signal.js +315 -8
- package/dist/process/spawn-with-parent-death-signal.js.map +1 -1
- package/package.json +4 -2
- package/src/process/index.ts +14 -1
- package/src/process/kill-marked-processes.ts +58 -13
- package/src/process/kill-process-group.ts +105 -0
- package/src/process/process-guard.ts +261 -6
- package/src/process/process-status.ts +25 -0
- package/src/process/spawn-with-parent-death-signal.ts +422 -11
|
@@ -1,25 +1,224 @@
|
|
|
1
1
|
import { type ChildProcess, spawn, type StdioOptions } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
4
|
import { Socket } from "node:net";
|
|
4
5
|
import { dirname, extname, join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { warn } from "../log/default-logger.ts";
|
|
6
8
|
import { killMarkedProcesses, PROCESS_MARKER } from "./kill-marked-processes.ts";
|
|
9
|
+
import {
|
|
10
|
+
type CleanupDirectoryIdentity,
|
|
11
|
+
cleanupDirectoryIdentity,
|
|
12
|
+
killProcessGroup,
|
|
13
|
+
type ProcessGroupIdentity,
|
|
14
|
+
processGroupIdentity,
|
|
15
|
+
removeCleanupDirectory,
|
|
16
|
+
} from "./kill-process-group.ts";
|
|
17
|
+
import { isReapedState, readProcessStatFields } from "./process-status.ts";
|
|
7
18
|
import { resolveExecutable } from "./resolve-executable.ts";
|
|
8
19
|
|
|
9
20
|
type ParentDeathSpawnOptions = {
|
|
10
21
|
stdio?: StdioOptions;
|
|
11
22
|
env?: NodeJS.ProcessEnv;
|
|
23
|
+
cleanupDirectories?: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ParentDeathSupervisorOptions = Omit<ParentDeathSpawnOptions, "cleanupDirectories"> & {
|
|
27
|
+
cleanupDirectory: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type GuardSignal = "SIGKILL" | "SIGCONT";
|
|
31
|
+
|
|
32
|
+
type ProcessIdentity = {
|
|
33
|
+
pid: number;
|
|
34
|
+
startTime: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ProcessWatch = {
|
|
38
|
+
owner: ProcessIdentity;
|
|
39
|
+
target: ProcessIdentity;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type LaunchArguments = (executable: string, cleanupDirectories: CleanupDirectoryIdentity[]) => string[];
|
|
43
|
+
|
|
44
|
+
type GuardJob = {
|
|
45
|
+
marker: string;
|
|
46
|
+
processGroup: ProcessGroupIdentity;
|
|
47
|
+
cleanupDirectories: CleanupDirectoryIdentity[];
|
|
48
|
+
signal: GuardSignal;
|
|
12
49
|
};
|
|
13
50
|
|
|
14
51
|
type GuardState = {
|
|
15
52
|
child?: ChildProcess | undefined;
|
|
53
|
+
jobs: Map<string, GuardJob>;
|
|
54
|
+
watch?: ProcessWatch;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type SpawnRollback = {
|
|
58
|
+
child: ChildProcess;
|
|
59
|
+
marker: string;
|
|
60
|
+
processGroup?: ProcessGroupIdentity;
|
|
61
|
+
cleanupDirectories?: CleanupDirectoryIdentity[];
|
|
16
62
|
};
|
|
17
63
|
|
|
18
64
|
const MODULE_PATH = fileURLToPath(import.meta.url);
|
|
19
65
|
const GUARD_PATH = join(dirname(MODULE_PATH), `process-guard${extname(MODULE_PATH)}`);
|
|
20
66
|
const RUN_ID = randomBytes(8).toString("hex");
|
|
21
67
|
const RUN_PREFIX = `${PROCESS_MARKER}=${RUN_ID}/`;
|
|
22
|
-
const guard: GuardState = {};
|
|
68
|
+
const guard: GuardState = { jobs: new Map() };
|
|
69
|
+
const SUPERVISOR_NAME = "gtkx-process-supervisor";
|
|
70
|
+
const SUPERVISOR_CLEANUP_NAME = "gtkx-process-cleanup";
|
|
71
|
+
const SUPERVISOR_WATCH_NAME = "gtkx-process-watch";
|
|
72
|
+
const SUPERVISOR_KILL_GROUP_BODY = [
|
|
73
|
+
"remaining=40",
|
|
74
|
+
'while [ "$remaining" -gt 0 ] && kill -KILL "-$group" 2>/dev/null; do',
|
|
75
|
+
' "$sleep_command" 0.025',
|
|
76
|
+
" remaining=$((remaining - 1))",
|
|
77
|
+
"done",
|
|
78
|
+
];
|
|
79
|
+
const SUPERVISOR_REMOVE_RUNTIME_BODY = [
|
|
80
|
+
"remaining=40",
|
|
81
|
+
'while [ "$remaining" -gt 0 ] && ' +
|
|
82
|
+
'[ "$("$stat_command" -c "%d:%i:%u" -- "$runtime" 2>/dev/null)" = "$identity" ]; do',
|
|
83
|
+
' "$rm_command" -rf -- "$runtime"',
|
|
84
|
+
' "$sleep_command" 0.025',
|
|
85
|
+
" remaining=$((remaining - 1))",
|
|
86
|
+
"done",
|
|
87
|
+
];
|
|
88
|
+
const SUPERVISOR_CLEANUP_BODY = [...SUPERVISOR_KILL_GROUP_BODY, ...SUPERVISOR_REMOVE_RUNTIME_BODY];
|
|
89
|
+
const SUPERVISOR_CLEANUP_SCRIPT = [
|
|
90
|
+
"runtime=$1",
|
|
91
|
+
"identity=$2",
|
|
92
|
+
"group=$3",
|
|
93
|
+
"stat_command=$4",
|
|
94
|
+
"rm_command=$5",
|
|
95
|
+
"sleep_command=$6",
|
|
96
|
+
...SUPERVISOR_CLEANUP_BODY,
|
|
97
|
+
].join("\n");
|
|
98
|
+
const SUPERVISOR_WATCH_SCRIPT = [
|
|
99
|
+
"runtime=$1",
|
|
100
|
+
"identity=$2",
|
|
101
|
+
"group=$3",
|
|
102
|
+
"group_start=$4",
|
|
103
|
+
"expected_parent=$5",
|
|
104
|
+
"expected_parent_start=$6",
|
|
105
|
+
"stat_command=$7",
|
|
106
|
+
"rm_command=$8",
|
|
107
|
+
"sleep_command=$9",
|
|
108
|
+
"matches_process() {",
|
|
109
|
+
" expected_process=$1",
|
|
110
|
+
" expected_start=$2",
|
|
111
|
+
" process_stat=",
|
|
112
|
+
' IFS= read -r process_stat < "/proc/$expected_process/stat" || return 1',
|
|
113
|
+
" process_tail=${process_stat##*) }",
|
|
114
|
+
" set -- $process_tail",
|
|
115
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
116
|
+
' [ "${20}" = "$expected_start" ]',
|
|
117
|
+
"}",
|
|
118
|
+
"matches_parent() {",
|
|
119
|
+
' matches_process "$expected_parent" "$expected_parent_start"',
|
|
120
|
+
"}",
|
|
121
|
+
"kill_group() {",
|
|
122
|
+
...SUPERVISOR_KILL_GROUP_BODY.map((line) => ` ${line}`),
|
|
123
|
+
"}",
|
|
124
|
+
"cleanup() {",
|
|
125
|
+
' trap "" TERM INT HUP',
|
|
126
|
+
" kill_group",
|
|
127
|
+
...SUPERVISOR_REMOVE_RUNTIME_BODY.map((line) => ` ${line}`),
|
|
128
|
+
" exit 0",
|
|
129
|
+
"}",
|
|
130
|
+
"trap cleanup TERM INT HUP",
|
|
131
|
+
'while matches_process "$group" "$group_start" && matches_parent; do',
|
|
132
|
+
' "$sleep_command" 0.1',
|
|
133
|
+
"done",
|
|
134
|
+
"kill_group",
|
|
135
|
+
"while matches_parent; do",
|
|
136
|
+
' "$sleep_command" 0.1',
|
|
137
|
+
"done",
|
|
138
|
+
"cleanup",
|
|
139
|
+
].join("\n");
|
|
140
|
+
const SUPERVISOR_SCRIPT = [
|
|
141
|
+
"runtime=$1",
|
|
142
|
+
"identity=$2",
|
|
143
|
+
"expected_parent=$3",
|
|
144
|
+
"expected_parent_start=$4",
|
|
145
|
+
"stat_command=$5",
|
|
146
|
+
"rm_command=$6",
|
|
147
|
+
"shell_command=$7",
|
|
148
|
+
"setsid_command=$8",
|
|
149
|
+
"sleep_command=$9",
|
|
150
|
+
"shift 9",
|
|
151
|
+
"watch_name=$1",
|
|
152
|
+
"watch_script=$2",
|
|
153
|
+
"cleanup_name=$3",
|
|
154
|
+
"cleanup_script=$4",
|
|
155
|
+
"shift 4",
|
|
156
|
+
"process_start() {",
|
|
157
|
+
" process_stat=",
|
|
158
|
+
' IFS= read -r process_stat < "/proc/$1/stat" || return 1',
|
|
159
|
+
" process_tail=${process_stat##*) }",
|
|
160
|
+
" set -- $process_tail",
|
|
161
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
162
|
+
' printf "%s" "${20}"',
|
|
163
|
+
"}",
|
|
164
|
+
"matches_parent() {",
|
|
165
|
+
" parent_stat=",
|
|
166
|
+
' IFS= read -r parent_stat < "/proc/$expected_parent/stat" || return 1',
|
|
167
|
+
" parent_tail=${parent_stat##*) }",
|
|
168
|
+
" set -- $parent_tail",
|
|
169
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
170
|
+
' [ "${20}" = "$expected_parent_start" ]',
|
|
171
|
+
"}",
|
|
172
|
+
"terminate() {",
|
|
173
|
+
' trap "" CONT TERM INT HUP',
|
|
174
|
+
" while :; do",
|
|
175
|
+
' "$setsid_command" "$shell_command" -c "$cleanup_script" "$cleanup_name" "$runtime" "$identity" "$$" ' +
|
|
176
|
+
'"$stat_command" "$rm_command" "$sleep_command" </dev/null >/dev/null 2>&1 &',
|
|
177
|
+
" cleanup=$!",
|
|
178
|
+
' wait "$cleanup" 2>/dev/null',
|
|
179
|
+
' "$sleep_command" 0.025',
|
|
180
|
+
" done",
|
|
181
|
+
"}",
|
|
182
|
+
"continue_or_terminate() {",
|
|
183
|
+
" remaining=40",
|
|
184
|
+
' while matches_parent && [ "$remaining" -gt 0 ]; do',
|
|
185
|
+
' "$sleep_command" 0.025',
|
|
186
|
+
" remaining=$((remaining - 1))",
|
|
187
|
+
" done",
|
|
188
|
+
" if ! matches_parent; then terminate; fi",
|
|
189
|
+
"}",
|
|
190
|
+
"trap continue_or_terminate CONT",
|
|
191
|
+
"trap terminate TERM INT HUP",
|
|
192
|
+
"supervisor_start=$(process_start \"$$\") || terminate",
|
|
193
|
+
'( unset GTKX_PROCESS_GUARD; exec "$setsid_command" "$shell_command" -c "$watch_script" "$watch_name" "$runtime" ' +
|
|
194
|
+
'"$identity" "$$" "$supervisor_start" "$expected_parent" "$expected_parent_start" "$stat_command" ' +
|
|
195
|
+
'"$rm_command" "$sleep_command" ) </dev/null >/dev/null 2>&1 &',
|
|
196
|
+
"child=",
|
|
197
|
+
"if ! matches_parent; then terminate; fi",
|
|
198
|
+
'"$@" &',
|
|
199
|
+
"child=$!",
|
|
200
|
+
"while :; do",
|
|
201
|
+
' wait "$child"',
|
|
202
|
+
" status=$?",
|
|
203
|
+
' if kill -0 "$child" 2>/dev/null; then continue; fi',
|
|
204
|
+
' exit "$status"',
|
|
205
|
+
"done",
|
|
206
|
+
].join("\n");
|
|
207
|
+
|
|
208
|
+
const processIdentity = (pid: number): ProcessIdentity => {
|
|
209
|
+
const fields = readProcessStatFields(pid);
|
|
210
|
+
const startTime = fields?.[19];
|
|
211
|
+
|
|
212
|
+
if (startTime === undefined || isReapedState(fields?.[0])) {
|
|
213
|
+
throw new Error(`Failed to identify process ${String(pid)}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { pid, startTime };
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const writeGuardCommand = (child: ChildProcess, operation: "+" | "-", job: GuardJob): void => {
|
|
220
|
+
child.stdin?.write(`${operation}${JSON.stringify(job)}\n`);
|
|
221
|
+
};
|
|
23
222
|
|
|
24
223
|
const releaseGuard = (child: ChildProcess): void => {
|
|
25
224
|
child.unref();
|
|
@@ -30,45 +229,257 @@ const releaseGuard = (child: ChildProcess): void => {
|
|
|
30
229
|
}
|
|
31
230
|
};
|
|
32
231
|
|
|
33
|
-
const
|
|
232
|
+
const isSameProcessWatch = (left: ProcessWatch, right: ProcessWatch): boolean =>
|
|
233
|
+
left.owner.pid === right.owner.pid &&
|
|
234
|
+
left.owner.startTime === right.owner.startTime &&
|
|
235
|
+
left.target.pid === right.target.pid &&
|
|
236
|
+
left.target.startTime === right.target.startTime;
|
|
237
|
+
|
|
238
|
+
const configureProcessWatch = (watch?: ProcessWatch): void => {
|
|
239
|
+
if (watch === undefined) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (guard.watch !== undefined && !isSameProcessWatch(guard.watch, watch)) {
|
|
244
|
+
throw new Error("The process guard is already watching another owner");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (guard.child !== undefined && guard.watch === undefined) {
|
|
248
|
+
throw new Error("The process guard was started before its owner was registered");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
guard.watch = watch;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const guardArguments = (): string[] =>
|
|
255
|
+
guard.watch === undefined
|
|
256
|
+
? [GUARD_PATH, RUN_PREFIX]
|
|
257
|
+
: [GUARD_PATH, RUN_PREFIX, JSON.stringify(guard.watch)];
|
|
258
|
+
|
|
259
|
+
const startGuard = (watch?: ProcessWatch): void => {
|
|
260
|
+
configureProcessWatch(watch);
|
|
261
|
+
|
|
34
262
|
if (guard.child) {
|
|
35
263
|
return;
|
|
36
264
|
}
|
|
37
265
|
|
|
38
|
-
const child = spawn(process.execPath,
|
|
266
|
+
const child = spawn(process.execPath, guardArguments(), {
|
|
39
267
|
detached: true,
|
|
40
268
|
stdio: ["pipe", "ignore", "ignore"],
|
|
41
269
|
});
|
|
42
270
|
|
|
43
271
|
child.on("error", () => {
|
|
44
|
-
guard.child
|
|
272
|
+
if (guard.child === child) {
|
|
273
|
+
guard.child = undefined;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
child.on("exit", () => {
|
|
278
|
+
if (guard.child === child) {
|
|
279
|
+
guard.child = undefined;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
child.stdin.on("error", () => {
|
|
284
|
+
if (guard.child === child) {
|
|
285
|
+
guard.child = undefined;
|
|
286
|
+
}
|
|
45
287
|
});
|
|
46
288
|
|
|
47
289
|
guard.child = child;
|
|
290
|
+
|
|
291
|
+
for (const job of guard.jobs.values()) {
|
|
292
|
+
writeGuardCommand(child, "+", job);
|
|
293
|
+
}
|
|
294
|
+
|
|
48
295
|
releaseGuard(child);
|
|
49
296
|
};
|
|
50
297
|
|
|
51
|
-
|
|
298
|
+
const watchParentProcess = (): void => {
|
|
299
|
+
const parentId = process.ppid;
|
|
300
|
+
const watch = {
|
|
301
|
+
owner: processIdentity(parentId),
|
|
302
|
+
target: processIdentity(process.pid),
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (process.ppid !== parentId) {
|
|
306
|
+
throw new Error("The parent process exited while its guard was starting");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
startGuard(watch);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const registerJob = (job: GuardJob): void => {
|
|
313
|
+
guard.jobs.set(job.marker, job);
|
|
314
|
+
|
|
315
|
+
if (guard.child) {
|
|
316
|
+
writeGuardCommand(guard.child, "+", job);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const unregisterJob = (job: GuardJob): void => {
|
|
321
|
+
guard.jobs.delete(job.marker);
|
|
322
|
+
|
|
323
|
+
if (guard.child) {
|
|
324
|
+
writeGuardCommand(guard.child, "-", job);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const rollbackSpawn = ({ child, marker, processGroup, cleanupDirectories = [] }: SpawnRollback): void => {
|
|
329
|
+
if (processGroup === undefined) {
|
|
330
|
+
child.kill("SIGKILL");
|
|
331
|
+
} else {
|
|
332
|
+
killProcessGroup(processGroup);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
killMarkedProcesses(marker);
|
|
336
|
+
|
|
337
|
+
for (const identity of cleanupDirectories) {
|
|
338
|
+
removeCleanupDirectory(identity);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const captureCleanupDirectories = (command: string, paths: string[]): CleanupDirectoryIdentity[] => {
|
|
343
|
+
const identities = paths.map((path) => cleanupDirectoryIdentity(path));
|
|
344
|
+
|
|
345
|
+
if (identities.includes(undefined)) {
|
|
346
|
+
throw new Error(`Failed to identify a cleanup directory for ${command}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return identities.filter((identity): identity is CleanupDirectoryIdentity => identity !== undefined);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const readResource = (path: string): string => {
|
|
353
|
+
try {
|
|
354
|
+
return readFileSync(path, "utf8").trim();
|
|
355
|
+
} catch {
|
|
356
|
+
return "unknown";
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const openDescriptors = (): string => {
|
|
361
|
+
try {
|
|
362
|
+
return String(readdirSync("/proc/self/fd").length);
|
|
363
|
+
} catch {
|
|
364
|
+
return "unknown";
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const resourceSummary = (): string =>
|
|
369
|
+
`open descriptors ${openDescriptors()}, ` +
|
|
370
|
+
`cgroup pids ${readResource("/sys/fs/cgroup/pids.current")}/${readResource("/sys/fs/cgroup/pids.max")}`;
|
|
371
|
+
|
|
372
|
+
const spawnedProcessGroup = (child: ChildProcess, command: string): ProcessGroupIdentity | undefined => {
|
|
373
|
+
const processGroupId = child.pid;
|
|
374
|
+
|
|
375
|
+
if (processGroupId !== undefined) {
|
|
376
|
+
return processGroupIdentity(processGroupId);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
child.on("error", (cause: NodeJS.ErrnoException) => {
|
|
380
|
+
warn(`Cannot spawn ${command}: ${cause.code ?? cause.message}`);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return undefined;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const spawnGuarded = (
|
|
52
387
|
command: string,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
388
|
+
options: ParentDeathSpawnOptions,
|
|
389
|
+
signal: GuardSignal,
|
|
390
|
+
launchArguments: LaunchArguments,
|
|
391
|
+
): ChildProcess => {
|
|
56
392
|
const executable = resolveExecutable(command);
|
|
57
393
|
const setpriv = resolveExecutable("setpriv");
|
|
58
394
|
const jobValue = `${RUN_ID}/${randomBytes(4).toString("hex")}`;
|
|
395
|
+
const marker = `${PROCESS_MARKER}=${jobValue}`;
|
|
396
|
+
const cleanupDirectories = captureCleanupDirectories(command, options.cleanupDirectories ?? []);
|
|
59
397
|
startGuard();
|
|
60
398
|
|
|
61
|
-
const child = spawn(setpriv,
|
|
399
|
+
const child = spawn(setpriv, launchArguments(executable, cleanupDirectories), {
|
|
62
400
|
detached: true,
|
|
63
401
|
stdio: options.stdio ?? "ignore",
|
|
64
402
|
env: { ...(options.env ?? process.env), [PROCESS_MARKER]: jobValue },
|
|
65
403
|
});
|
|
66
404
|
|
|
405
|
+
const group = spawnedProcessGroup(child, command);
|
|
406
|
+
|
|
407
|
+
if (group === undefined) {
|
|
408
|
+
rollbackSpawn({ child, marker, cleanupDirectories });
|
|
409
|
+
throw new Error(`Failed to spawn ${command} or identify its process group (${resourceSummary()})`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const job: GuardJob = { marker, processGroup: group, cleanupDirectories, signal };
|
|
413
|
+
registerJob(job);
|
|
414
|
+
|
|
67
415
|
child.on("exit", () => {
|
|
68
|
-
killMarkedProcesses(
|
|
416
|
+
killMarkedProcesses(job.marker);
|
|
417
|
+
unregisterJob(job);
|
|
69
418
|
});
|
|
70
419
|
|
|
71
420
|
return child;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
function spawnWithParentDeathSignal(
|
|
424
|
+
command: string,
|
|
425
|
+
args: string[],
|
|
426
|
+
options: ParentDeathSpawnOptions = {},
|
|
427
|
+
): ChildProcess {
|
|
428
|
+
return spawnGuarded(
|
|
429
|
+
command,
|
|
430
|
+
options,
|
|
431
|
+
"SIGKILL",
|
|
432
|
+
(executable) => ["--pdeathsig", "SIGKILL", executable, ...args],
|
|
433
|
+
);
|
|
72
434
|
}
|
|
73
435
|
|
|
74
|
-
|
|
436
|
+
const spawnWithParentDeathSupervisor = (
|
|
437
|
+
command: string,
|
|
438
|
+
args: string[],
|
|
439
|
+
options: ParentDeathSupervisorOptions,
|
|
440
|
+
): ChildProcess => {
|
|
441
|
+
const shell = resolveExecutable("sh");
|
|
442
|
+
const stat = resolveExecutable("stat");
|
|
443
|
+
const rm = resolveExecutable("rm");
|
|
444
|
+
const setsid = resolveExecutable("setsid");
|
|
445
|
+
const sleep = resolveExecutable("sleep");
|
|
446
|
+
|
|
447
|
+
return spawnGuarded(
|
|
448
|
+
command,
|
|
449
|
+
{ ...options, cleanupDirectories: [options.cleanupDirectory] },
|
|
450
|
+
"SIGCONT",
|
|
451
|
+
(executable, cleanupDirectories) => {
|
|
452
|
+
const identity = cleanupDirectories[0];
|
|
453
|
+
|
|
454
|
+
if (identity === undefined) {
|
|
455
|
+
throw new Error(`Failed to identify a cleanup directory for ${command}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return [
|
|
459
|
+
"--pdeathsig",
|
|
460
|
+
"SIGCONT",
|
|
461
|
+
shell,
|
|
462
|
+
"-c",
|
|
463
|
+
SUPERVISOR_SCRIPT,
|
|
464
|
+
SUPERVISOR_NAME,
|
|
465
|
+
identity.path,
|
|
466
|
+
`${identity.device}:${identity.inode}:${identity.userId}`,
|
|
467
|
+
String(process.pid),
|
|
468
|
+
processIdentity(process.pid).startTime,
|
|
469
|
+
stat,
|
|
470
|
+
rm,
|
|
471
|
+
shell,
|
|
472
|
+
setsid,
|
|
473
|
+
sleep,
|
|
474
|
+
SUPERVISOR_WATCH_NAME,
|
|
475
|
+
SUPERVISOR_WATCH_SCRIPT,
|
|
476
|
+
SUPERVISOR_CLEANUP_NAME,
|
|
477
|
+
SUPERVISOR_CLEANUP_SCRIPT,
|
|
478
|
+
executable,
|
|
479
|
+
...args,
|
|
480
|
+
];
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, watchParentProcess };
|