@gtkx/utils 2.0.0-beta.3 → 2.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/process/index.d.ts +2 -2
- package/dist/process/index.d.ts.map +1 -1
- package/dist/process/index.js +2 -2
- package/dist/process/index.js.map +1 -1
- package/dist/process/process-guard.js +117 -9
- package/dist/process/process-guard.js.map +1 -1
- package/dist/process/spawn-with-parent-death-signal.d.ts +2 -1
- package/dist/process/spawn-with-parent-death-signal.d.ts.map +1 -1
- package/dist/process/spawn-with-parent-death-signal.js +165 -17
- package/dist/process/spawn-with-parent-death-signal.js.map +1 -1
- package/package.json +1 -1
- package/src/process/index.ts +4 -0
- package/src/process/process-guard.ts +155 -8
- package/src/process/spawn-with-parent-death-signal.ts +188 -17
package/dist/process/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { exitCodeForSignal } from "./exit-code-for-signal.ts";
|
|
2
2
|
export { installGracefulShutdown } from "./install-graceful-shutdown.ts";
|
|
3
|
-
export { killProcessGroup, type ProcessGroupIdentity, processGroupIdentity, } from "./kill-process-group.ts";
|
|
3
|
+
export { type CleanupDirectoryIdentity, cleanupDirectoryIdentity, killProcessGroup, type ProcessGroupIdentity, processGroupIdentity, removeCleanupDirectory, } from "./kill-process-group.ts";
|
|
4
4
|
export { resolveExecutable, tryResolveExecutable } from "./resolve-executable.ts";
|
|
5
|
-
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, } from "./spawn-with-parent-death-signal.ts";
|
|
5
|
+
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, watchParentProcess, } from "./spawn-with-parent-death-signal.ts";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/process/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EACH,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,oBAAoB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/process/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EACH,KAAK,wBAAwB,EAC7B,wBAAwB,EACxB,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,oBAAoB,EACpB,sBAAsB,GACzB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EACH,0BAA0B,EAC1B,8BAA8B,EAC9B,kBAAkB,GACrB,MAAM,qCAAqC,CAAC"}
|
package/dist/process/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { exitCodeForSignal } from "./exit-code-for-signal.js";
|
|
2
2
|
export { installGracefulShutdown } from "./install-graceful-shutdown.js";
|
|
3
|
-
export { killProcessGroup, processGroupIdentity, } from "./kill-process-group.js";
|
|
3
|
+
export { cleanupDirectoryIdentity, killProcessGroup, processGroupIdentity, removeCleanupDirectory, } from "./kill-process-group.js";
|
|
4
4
|
export { resolveExecutable, tryResolveExecutable } from "./resolve-executable.js";
|
|
5
|
-
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, } from "./spawn-with-parent-death-signal.js";
|
|
5
|
+
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, watchParentProcess, } from "./spawn-with-parent-death-signal.js";
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/process/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/process/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAEH,wBAAwB,EACxB,gBAAgB,EAEhB,oBAAoB,EACpB,sBAAsB,GACzB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EACH,0BAA0B,EAC1B,8BAA8B,EAC9B,kBAAkB,GACrB,MAAM,qCAAqC,CAAC","sourcesContent":["export { exitCodeForSignal } from \"./exit-code-for-signal.ts\";\nexport { installGracefulShutdown } from \"./install-graceful-shutdown.ts\";\nexport {\n type CleanupDirectoryIdentity,\n cleanupDirectoryIdentity,\n killProcessGroup,\n type ProcessGroupIdentity,\n processGroupIdentity,\n removeCleanupDirectory,\n} from \"./kill-process-group.ts\";\nexport { resolveExecutable, tryResolveExecutable } from \"./resolve-executable.ts\";\nexport {\n spawnWithParentDeathSignal,\n spawnWithParentDeathSupervisor,\n watchParentProcess,\n} from \"./spawn-with-parent-death-signal.ts\";\n"]}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { killMarkedProcesses, killMarkedProcessRun } from "./kill-marked-processes.js";
|
|
2
|
-
import { killProcessGroup, removeCleanupDirectory, } from "./kill-process-group.js";
|
|
3
|
+
import { killProcessGroup, processGroupIdentity, removeCleanupDirectory, } from "./kill-process-group.js";
|
|
3
4
|
const GUARD_PREFIX = process.argv[2] ?? "";
|
|
5
|
+
const PROCESS_WATCH_ARGUMENT = process.argv[3];
|
|
4
6
|
const WATCHED_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
7
|
+
const OWNER_POLL_INTERVAL_MS = 50;
|
|
8
|
+
const SUPERVISOR_EXIT_TIMEOUT_MS = 2000;
|
|
5
9
|
const state = {
|
|
6
10
|
bufferedCommands: "",
|
|
7
11
|
isSweeping: false,
|
|
8
12
|
jobs: new Map(),
|
|
9
13
|
};
|
|
10
14
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
const isProcessIdentity = (value) => isRecord(value) &&
|
|
16
|
+
typeof value.pid === "number" &&
|
|
17
|
+
Number.isSafeInteger(value.pid) &&
|
|
18
|
+
value.pid > 1 &&
|
|
19
|
+
typeof value.startTime === "string" &&
|
|
20
|
+
/^\d+$/.test(value.startTime);
|
|
21
|
+
const isProcessWatch = (value) => isRecord(value) && isProcessIdentity(value.owner) && isProcessIdentity(value.target);
|
|
11
22
|
const isProcessGroupIdentity = (value) => isRecord(value) &&
|
|
12
23
|
typeof value.processGroupId === "number" &&
|
|
13
24
|
Number.isSafeInteger(value.processGroupId) &&
|
|
@@ -25,6 +36,44 @@ const isGuardJob = (value) => isRecord(value) &&
|
|
|
25
36
|
Array.isArray(value.cleanupDirectories) &&
|
|
26
37
|
value.cleanupDirectories.every(isCleanupDirectoryIdentity) &&
|
|
27
38
|
(value.signal === "SIGKILL" || value.signal === "SIGCONT");
|
|
39
|
+
const parseProcessWatch = () => {
|
|
40
|
+
if (PROCESS_WATCH_ARGUMENT === undefined) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const value = JSON.parse(PROCESS_WATCH_ARGUMENT);
|
|
45
|
+
return isProcessWatch(value) ? value : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const currentProcessIdentity = (pid) => {
|
|
52
|
+
try {
|
|
53
|
+
const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
|
|
54
|
+
const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ", 20);
|
|
55
|
+
const state = fields[0];
|
|
56
|
+
const startTime = fields[19];
|
|
57
|
+
return startTime !== undefined && state !== undefined && !["Z", "X", "x"].includes(state)
|
|
58
|
+
? { pid, startTime }
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const isCurrentProcess = (identity) => currentProcessIdentity(identity.pid)?.startTime === identity.startTime;
|
|
66
|
+
const killProcess = (identity) => {
|
|
67
|
+
if (!isCurrentProcess(identity)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
process.kill(identity.pid, "SIGKILL");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
28
77
|
const applyCommand = (command) => {
|
|
29
78
|
const operation = command[0];
|
|
30
79
|
let value;
|
|
@@ -71,24 +120,83 @@ const removeCleanupDirectories = () => {
|
|
|
71
120
|
removeCleanupDirectory(identity);
|
|
72
121
|
}
|
|
73
122
|
};
|
|
74
|
-
const
|
|
123
|
+
const hasRunningSupervisor = () => {
|
|
124
|
+
for (const job of state.jobs.values()) {
|
|
125
|
+
if (job.signal !== "SIGCONT") {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const current = processGroupIdentity(job.processGroup.processGroupId);
|
|
129
|
+
if (current?.leaderStartTime === job.processGroup.leaderStartTime) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
};
|
|
135
|
+
const forceSupervisors = () => {
|
|
136
|
+
for (const job of state.jobs.values()) {
|
|
137
|
+
if (job.signal === "SIGCONT") {
|
|
138
|
+
killProcessGroup(job.processGroup);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const finishSweep = () => {
|
|
143
|
+
if (GUARD_PREFIX.length > 0) {
|
|
144
|
+
killMarkedProcessRun(GUARD_PREFIX);
|
|
145
|
+
}
|
|
146
|
+
removeCleanupDirectories();
|
|
147
|
+
process.exit(0);
|
|
148
|
+
};
|
|
149
|
+
const awaitSupervisors = (deadline) => {
|
|
150
|
+
if (hasRunningSupervisor() && Date.now() < deadline) {
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
awaitSupervisors(deadline);
|
|
153
|
+
}, OWNER_POLL_INTERVAL_MS);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
forceSupervisors();
|
|
157
|
+
finishSweep();
|
|
158
|
+
};
|
|
159
|
+
const sweep = (target) => {
|
|
75
160
|
if (state.isSweeping) {
|
|
76
161
|
return;
|
|
77
162
|
}
|
|
78
163
|
state.isSweeping = true;
|
|
79
164
|
applyCommand(state.bufferedCommands);
|
|
165
|
+
if (target !== undefined) {
|
|
166
|
+
killProcess(target);
|
|
167
|
+
}
|
|
80
168
|
killJobs();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
169
|
+
awaitSupervisors(Date.now() + SUPERVISOR_EXIT_TIMEOUT_MS);
|
|
170
|
+
};
|
|
171
|
+
const watch = parseProcessWatch();
|
|
172
|
+
const startOwnerPoll = () => {
|
|
173
|
+
if (watch === undefined) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const ownerPoll = setInterval(() => {
|
|
177
|
+
if (isCurrentProcess(watch.owner)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
clearInterval(ownerPoll);
|
|
181
|
+
sweep(watch.target);
|
|
182
|
+
}, OWNER_POLL_INTERVAL_MS);
|
|
183
|
+
if (!isCurrentProcess(watch.owner)) {
|
|
184
|
+
clearInterval(ownerPoll);
|
|
185
|
+
sweep(watch.target);
|
|
84
186
|
}
|
|
85
|
-
process.exit(0);
|
|
86
187
|
};
|
|
188
|
+
startOwnerPoll();
|
|
87
189
|
process.stdin.resume();
|
|
88
190
|
process.stdin.on("data", receiveCommands);
|
|
89
|
-
process.stdin.on("end",
|
|
90
|
-
|
|
191
|
+
process.stdin.on("end", () => {
|
|
192
|
+
sweep();
|
|
193
|
+
});
|
|
194
|
+
process.stdin.on("error", () => {
|
|
195
|
+
sweep();
|
|
196
|
+
});
|
|
91
197
|
for (const signal of WATCHED_SIGNALS) {
|
|
92
|
-
process.on(signal,
|
|
198
|
+
process.on(signal, () => {
|
|
199
|
+
sweep();
|
|
200
|
+
});
|
|
93
201
|
}
|
|
94
202
|
//# sourceMappingURL=process-guard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-guard.js","sourceRoot":"","sources":["../../src/process/process-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvF,OAAO,EAEH,gBAAgB,EAEhB,sBAAsB,GACzB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3C,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAqC,CAAC;AAQ5F,MAAM,KAAK,GAAmF;IAC1F,gBAAgB,EAAE,EAAE;IACpB,UAAU,EAAE,KAAK;IACjB,IAAI,EAAE,IAAI,GAAG,EAAE;CAClB,CAAC;AAEF,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CAClE,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAEzE,MAAM,sBAAsB,GAAG,CAAC,KAAc,EAAiC,EAAE,CAC7E,QAAQ,CAAC,KAAK,CAAC;IACf,OAAO,KAAK,CAAC,cAAc,KAAK,QAAQ;IACxC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,cAAc,CAAC;IAC1C,KAAK,CAAC,cAAc,GAAG,CAAC;IACxB,OAAO,KAAK,CAAC,eAAe,KAAK,QAAQ;IACzC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;AAExC,MAAM,0BAA0B,GAAG,CAAC,KAAc,EAAqC,EAAE,CACrF,QAAQ,CAAC,KAAK,CAAC;IACf,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;IAC9B,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;IAChC,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;IAC/B,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC;AAErC,MAAM,UAAU,GAAG,CAAC,KAAc,EAAqB,EAAE,CACrD,QAAQ,CAAC,KAAK,CAAC;IACf,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ;IAChC,sBAAsB,CAAC,KAAK,CAAC,YAAY,CAAC;IAC1C,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC;IACvC,KAAK,CAAC,kBAAkB,CAAC,KAAK,CAAC,0BAA0B,CAAC;IAC1D,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC;AAE/D,MAAM,YAAY,GAAG,CAAC,OAAe,EAAQ,EAAE;IAC3C,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,KAAc,CAAC;IAEnB,IAAI,CAAC;QACD,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO;IACX,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO;IACX,CAAC;IAED,IAAI,SAAS,KAAK,GAAG,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC;SAAM,IAAI,SAAS,KAAK,GAAG,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,KAAsB,EAAQ,EAAE;IACrD,KAAK,CAAC,gBAAgB,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC3C,MAAM,QAAQ,GAAG,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpD,KAAK,CAAC,gBAAgB,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAE9C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,YAAY,CAAC,OAAO,CAAC,CAAC;IAC1B,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,GAAS,EAAE;IACxB,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QACpC,gBAAgB,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAE/C,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC3B,mBAAmB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC;IACL,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,wBAAwB,GAAG,GAAS,EAAE;IACxC,MAAM,kBAAkB,GAA0C,IAAI,GAAG,EAAE,CAAC;IAE5E,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QACpC,KAAK,MAAM,QAAQ,IAAI,GAAG,CAAC,kBAAkB,EAAE,CAAC;YAC5C,kBAAkB,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,KAAK,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC7E,CAAC;IACL,CAAC;IAED,KAAK,MAAM,QAAQ,IAAI,kBAAkB,CAAC,MAAM,EAAE,EAAE,CAAC;QACjD,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,KAAK,GAAG,GAAS,EAAE;IACrB,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,OAAO;IACX,CAAC;IAED,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC;IACxB,YAAY,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACrC,QAAQ,EAAE,CAAC;IACX,wBAAwB,EAAE,CAAC;IAE3B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,oBAAoB,CAAC,YAAY,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC;AAEF,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;AACvB,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAC1C,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAC/B,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AAEjC,KAAK,MAAM,MAAM,IAAI,eAAe,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAC9B,CAAC","sourcesContent":["import { killMarkedProcesses, killMarkedProcessRun } from \"./kill-marked-processes.ts\";\nimport {\n type CleanupDirectoryIdentity,\n killProcessGroup,\n type ProcessGroupIdentity,\n removeCleanupDirectory,\n} from \"./kill-process-group.ts\";\n\nconst GUARD_PREFIX = process.argv[2] ?? \"\";\nconst WATCHED_SIGNALS = [\"SIGTERM\", \"SIGINT\", \"SIGHUP\"] as const satisfies NodeJS.Signals[];\ntype GuardJob = {\n marker: string;\n processGroup: ProcessGroupIdentity;\n cleanupDirectories: CleanupDirectoryIdentity[];\n signal: NodeJS.Signals;\n};\n\nconst state: { bufferedCommands: string; isSweeping: boolean; jobs: Map<string, GuardJob> } = {\n bufferedCommands: \"\",\n isSweeping: false,\n jobs: new Map(),\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isProcessGroupIdentity = (value: unknown): value is ProcessGroupIdentity =>\n isRecord(value) &&\n typeof value.processGroupId === \"number\" &&\n Number.isSafeInteger(value.processGroupId) &&\n value.processGroupId > 1 &&\n typeof value.leaderStartTime === \"string\" &&\n /^\\d+$/.test(value.leaderStartTime);\n\nconst isCleanupDirectoryIdentity = (value: unknown): value is CleanupDirectoryIdentity =>\n isRecord(value) &&\n typeof value.path === \"string\" &&\n typeof value.device === \"string\" &&\n typeof value.inode === \"string\" &&\n typeof value.userId === \"string\";\n\nconst isGuardJob = (value: unknown): value is GuardJob =>\n isRecord(value) &&\n typeof value.marker === \"string\" &&\n isProcessGroupIdentity(value.processGroup) &&\n Array.isArray(value.cleanupDirectories) &&\n value.cleanupDirectories.every(isCleanupDirectoryIdentity) &&\n (value.signal === \"SIGKILL\" || value.signal === \"SIGCONT\");\n\nconst applyCommand = (command: string): void => {\n const operation = command[0];\n let value: unknown;\n\n try {\n value = JSON.parse(command.slice(1));\n } catch {\n return;\n }\n\n if (!isGuardJob(value)) {\n return;\n }\n\n if (operation === \"+\") {\n state.jobs.set(value.marker, value);\n } else if (operation === \"-\") {\n state.jobs.delete(value.marker);\n }\n};\n\nconst receiveCommands = (chunk: Buffer | string): void => {\n state.bufferedCommands += chunk.toString();\n const commands = state.bufferedCommands.split(\"\\n\");\n state.bufferedCommands = commands.pop() ?? \"\";\n\n for (const command of commands) {\n applyCommand(command);\n }\n};\n\nconst killJobs = (): void => {\n for (const job of state.jobs.values()) {\n killProcessGroup(job.processGroup, job.signal);\n\n if (job.signal === \"SIGKILL\") {\n killMarkedProcesses(job.marker);\n }\n }\n};\n\nconst removeCleanupDirectories = (): void => {\n const cleanupDirectories: Map<string, CleanupDirectoryIdentity> = new Map();\n\n for (const job of state.jobs.values()) {\n for (const identity of job.cleanupDirectories) {\n cleanupDirectories.set(`${identity.device}:${identity.inode}`, identity);\n }\n }\n\n for (const identity of cleanupDirectories.values()) {\n removeCleanupDirectory(identity);\n }\n};\n\nconst sweep = (): void => {\n if (state.isSweeping) {\n return;\n }\n\n state.isSweeping = true;\n applyCommand(state.bufferedCommands);\n killJobs();\n removeCleanupDirectories();\n\n if (GUARD_PREFIX.length > 0) {\n killMarkedProcessRun(GUARD_PREFIX);\n }\n process.exit(0);\n};\n\nprocess.stdin.resume();\nprocess.stdin.on(\"data\", receiveCommands);\nprocess.stdin.on(\"end\", sweep);\nprocess.stdin.on(\"error\", sweep);\n\nfor (const signal of WATCHED_SIGNALS) {\n process.on(signal, sweep);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"process-guard.js","sourceRoot":"","sources":["../../src/process/process-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { killMarkedProcesses, killMarkedProcessRun } from \"./kill-marked-processes.ts\";\nimport {\n type CleanupDirectoryIdentity,\n killProcessGroup,\n type ProcessGroupIdentity,\n processGroupIdentity,\n removeCleanupDirectory,\n} from \"./kill-process-group.ts\";\n\nconst GUARD_PREFIX = process.argv[2] ?? \"\";\nconst PROCESS_WATCH_ARGUMENT = process.argv[3];\nconst WATCHED_SIGNALS = [\"SIGTERM\", \"SIGINT\", \"SIGHUP\"] as const satisfies NodeJS.Signals[];\nconst OWNER_POLL_INTERVAL_MS = 50;\nconst SUPERVISOR_EXIT_TIMEOUT_MS = 2000;\n\ntype ProcessIdentity = {\n pid: number;\n startTime: string;\n};\n\ntype ProcessWatch = {\n owner: ProcessIdentity;\n target: ProcessIdentity;\n};\n\ntype GuardJob = {\n marker: string;\n processGroup: ProcessGroupIdentity;\n cleanupDirectories: CleanupDirectoryIdentity[];\n signal: NodeJS.Signals;\n};\n\nconst state: { bufferedCommands: string; isSweeping: boolean; jobs: Map<string, GuardJob> } = {\n bufferedCommands: \"\",\n isSweeping: false,\n jobs: new Map(),\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isProcessIdentity = (value: unknown): value is ProcessIdentity =>\n isRecord(value) &&\n typeof value.pid === \"number\" &&\n Number.isSafeInteger(value.pid) &&\n value.pid > 1 &&\n typeof value.startTime === \"string\" &&\n /^\\d+$/.test(value.startTime);\n\nconst isProcessWatch = (value: unknown): value is ProcessWatch =>\n isRecord(value) && isProcessIdentity(value.owner) && isProcessIdentity(value.target);\n\nconst isProcessGroupIdentity = (value: unknown): value is ProcessGroupIdentity =>\n isRecord(value) &&\n typeof value.processGroupId === \"number\" &&\n Number.isSafeInteger(value.processGroupId) &&\n value.processGroupId > 1 &&\n typeof value.leaderStartTime === \"string\" &&\n /^\\d+$/.test(value.leaderStartTime);\n\nconst isCleanupDirectoryIdentity = (value: unknown): value is CleanupDirectoryIdentity =>\n isRecord(value) &&\n typeof value.path === \"string\" &&\n typeof value.device === \"string\" &&\n typeof value.inode === \"string\" &&\n typeof value.userId === \"string\";\n\nconst isGuardJob = (value: unknown): value is GuardJob =>\n isRecord(value) &&\n typeof value.marker === \"string\" &&\n isProcessGroupIdentity(value.processGroup) &&\n Array.isArray(value.cleanupDirectories) &&\n value.cleanupDirectories.every(isCleanupDirectoryIdentity) &&\n (value.signal === \"SIGKILL\" || value.signal === \"SIGCONT\");\n\nconst parseProcessWatch = (): ProcessWatch | undefined => {\n if (PROCESS_WATCH_ARGUMENT === undefined) {\n return undefined;\n }\n\n try {\n const value: unknown = JSON.parse(PROCESS_WATCH_ARGUMENT);\n\n return isProcessWatch(value) ? value : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst currentProcessIdentity = (pid: number): ProcessIdentity | undefined => {\n try {\n const stat = readFileSync(`/proc/${String(pid)}/stat`, \"utf8\");\n const fields = stat.slice(stat.lastIndexOf(\") \") + 2).split(\" \", 20);\n const state = fields[0];\n const startTime = fields[19];\n\n return startTime !== undefined && state !== undefined && ![\"Z\", \"X\", \"x\"].includes(state)\n ? { pid, startTime }\n : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst isCurrentProcess = (identity: ProcessIdentity): boolean =>\n currentProcessIdentity(identity.pid)?.startTime === identity.startTime;\n\nconst killProcess = (identity: ProcessIdentity): void => {\n if (!isCurrentProcess(identity)) {\n return;\n }\n\n try {\n process.kill(identity.pid, \"SIGKILL\");\n } catch {\n return;\n }\n};\n\nconst applyCommand = (command: string): void => {\n const operation = command[0];\n let value: unknown;\n\n try {\n value = JSON.parse(command.slice(1));\n } catch {\n return;\n }\n\n if (!isGuardJob(value)) {\n return;\n }\n\n if (operation === \"+\") {\n state.jobs.set(value.marker, value);\n } else if (operation === \"-\") {\n state.jobs.delete(value.marker);\n }\n};\n\nconst receiveCommands = (chunk: Buffer | string): void => {\n state.bufferedCommands += chunk.toString();\n const commands = state.bufferedCommands.split(\"\\n\");\n state.bufferedCommands = commands.pop() ?? \"\";\n\n for (const command of commands) {\n applyCommand(command);\n }\n};\n\nconst killJobs = (): void => {\n for (const job of state.jobs.values()) {\n killProcessGroup(job.processGroup, job.signal);\n\n if (job.signal === \"SIGKILL\") {\n killMarkedProcesses(job.marker);\n }\n }\n};\n\nconst removeCleanupDirectories = (): void => {\n const cleanupDirectories: Map<string, CleanupDirectoryIdentity> = new Map();\n\n for (const job of state.jobs.values()) {\n for (const identity of job.cleanupDirectories) {\n cleanupDirectories.set(`${identity.device}:${identity.inode}`, identity);\n }\n }\n\n for (const identity of cleanupDirectories.values()) {\n removeCleanupDirectory(identity);\n }\n};\n\nconst hasRunningSupervisor = (): boolean => {\n for (const job of state.jobs.values()) {\n if (job.signal !== \"SIGCONT\") {\n continue;\n }\n\n const current = processGroupIdentity(job.processGroup.processGroupId);\n\n if (current?.leaderStartTime === job.processGroup.leaderStartTime) {\n return true;\n }\n }\n\n return false;\n};\n\nconst forceSupervisors = (): void => {\n for (const job of state.jobs.values()) {\n if (job.signal === \"SIGCONT\") {\n killProcessGroup(job.processGroup);\n }\n }\n};\n\nconst finishSweep = (): void => {\n if (GUARD_PREFIX.length > 0) {\n killMarkedProcessRun(GUARD_PREFIX);\n }\n\n removeCleanupDirectories();\n process.exit(0);\n};\n\nconst awaitSupervisors = (deadline: number): void => {\n if (hasRunningSupervisor() && Date.now() < deadline) {\n setTimeout(() => {\n awaitSupervisors(deadline);\n }, OWNER_POLL_INTERVAL_MS);\n\n return;\n }\n\n forceSupervisors();\n finishSweep();\n};\n\nconst sweep = (target?: ProcessIdentity): void => {\n if (state.isSweeping) {\n return;\n }\n\n state.isSweeping = true;\n applyCommand(state.bufferedCommands);\n\n if (target !== undefined) {\n killProcess(target);\n }\n\n killJobs();\n awaitSupervisors(Date.now() + SUPERVISOR_EXIT_TIMEOUT_MS);\n};\n\nconst watch = parseProcessWatch();\n\nconst startOwnerPoll = (): void => {\n if (watch === undefined) {\n return;\n }\n\n const ownerPoll = setInterval(() => {\n if (isCurrentProcess(watch.owner)) {\n return;\n }\n\n clearInterval(ownerPoll);\n sweep(watch.target);\n }, OWNER_POLL_INTERVAL_MS);\n\n if (!isCurrentProcess(watch.owner)) {\n clearInterval(ownerPoll);\n sweep(watch.target);\n }\n};\n\nstartOwnerPoll();\n\nprocess.stdin.resume();\nprocess.stdin.on(\"data\", receiveCommands);\nprocess.stdin.on(\"end\", () => {\n sweep();\n});\nprocess.stdin.on(\"error\", () => {\n sweep();\n});\n\nfor (const signal of WATCHED_SIGNALS) {\n process.on(signal, () => {\n sweep();\n });\n}\n"]}
|
|
@@ -7,7 +7,8 @@ type ParentDeathSpawnOptions = {
|
|
|
7
7
|
type ParentDeathSupervisorOptions = Omit<ParentDeathSpawnOptions, "cleanupDirectories"> & {
|
|
8
8
|
cleanupDirectory: string;
|
|
9
9
|
};
|
|
10
|
+
declare const watchParentProcess: () => void;
|
|
10
11
|
declare function spawnWithParentDeathSignal(command: string, args: string[], options?: ParentDeathSpawnOptions): ChildProcess;
|
|
11
12
|
declare const spawnWithParentDeathSupervisor: (command: string, args: string[], options: ParentDeathSupervisorOptions) => ChildProcess;
|
|
12
|
-
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor };
|
|
13
|
+
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, watchParentProcess };
|
|
13
14
|
//# sourceMappingURL=spawn-with-parent-death-signal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-with-parent-death-signal.d.ts","sourceRoot":"","sources":["../../src/process/spawn-with-parent-death-signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAiBjF,KAAK,uBAAuB,GAAG;IAC3B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC,CAAC;AAEF,KAAK,4BAA4B,GAAG,IAAI,CAAC,uBAAuB,EAAE,oBAAoB,CAAC,GAAG;IACtF,gBAAgB,EAAE,MAAM,CAAC;CAC5B,CAAC;
|
|
1
|
+
{"version":3,"file":"spawn-with-parent-death-signal.d.ts","sourceRoot":"","sources":["../../src/process/spawn-with-parent-death-signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAiBjF,KAAK,uBAAuB,GAAG;IAC3B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC,CAAC;AAEF,KAAK,4BAA4B,GAAG,IAAI,CAAC,uBAAuB,EAAE,oBAAoB,CAAC,GAAG;IACtF,gBAAgB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAgRF,QAAA,MAAM,kBAAkB,QAAO,IAY9B,CAAC;AAgFF,iBAAS,0BAA0B,CAC/B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,uBAA4B,GACtC,YAAY,CAOd;AAED,QAAA,MAAM,8BAA8B,GAChC,SAAS,MAAM,EACf,MAAM,MAAM,EAAE,EACd,SAAS,4BAA4B,KACtC,YA2CF,CAAC;AAEF,OAAO,EAAE,0BAA0B,EAAE,8BAA8B,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -13,6 +13,76 @@ const RUN_ID = randomBytes(8).toString("hex");
|
|
|
13
13
|
const RUN_PREFIX = `${PROCESS_MARKER}=${RUN_ID}/`;
|
|
14
14
|
const guard = { jobs: new Map() };
|
|
15
15
|
const SUPERVISOR_NAME = "gtkx-process-supervisor";
|
|
16
|
+
const SUPERVISOR_CLEANUP_NAME = "gtkx-process-cleanup";
|
|
17
|
+
const SUPERVISOR_WATCH_NAME = "gtkx-process-watch";
|
|
18
|
+
const SUPERVISOR_KILL_GROUP_BODY = [
|
|
19
|
+
"remaining=40",
|
|
20
|
+
'while [ "$remaining" -gt 0 ] && kill -KILL "-$group" 2>/dev/null; do',
|
|
21
|
+
' "$sleep_command" 0.025',
|
|
22
|
+
" remaining=$((remaining - 1))",
|
|
23
|
+
"done",
|
|
24
|
+
];
|
|
25
|
+
const SUPERVISOR_REMOVE_RUNTIME_BODY = [
|
|
26
|
+
"remaining=40",
|
|
27
|
+
'while [ "$remaining" -gt 0 ] && ' +
|
|
28
|
+
'[ "$("$stat_command" -c "%d:%i:%u" -- "$runtime" 2>/dev/null)" = "$identity" ]; do',
|
|
29
|
+
' "$rm_command" -rf -- "$runtime"',
|
|
30
|
+
' "$sleep_command" 0.025',
|
|
31
|
+
" remaining=$((remaining - 1))",
|
|
32
|
+
"done",
|
|
33
|
+
];
|
|
34
|
+
const SUPERVISOR_CLEANUP_BODY = [...SUPERVISOR_KILL_GROUP_BODY, ...SUPERVISOR_REMOVE_RUNTIME_BODY];
|
|
35
|
+
const SUPERVISOR_CLEANUP_SCRIPT = [
|
|
36
|
+
"runtime=$1",
|
|
37
|
+
"identity=$2",
|
|
38
|
+
"group=$3",
|
|
39
|
+
"stat_command=$4",
|
|
40
|
+
"rm_command=$5",
|
|
41
|
+
"sleep_command=$6",
|
|
42
|
+
...SUPERVISOR_CLEANUP_BODY,
|
|
43
|
+
].join("\n");
|
|
44
|
+
const SUPERVISOR_WATCH_SCRIPT = [
|
|
45
|
+
"runtime=$1",
|
|
46
|
+
"identity=$2",
|
|
47
|
+
"group=$3",
|
|
48
|
+
"group_start=$4",
|
|
49
|
+
"expected_parent=$5",
|
|
50
|
+
"expected_parent_start=$6",
|
|
51
|
+
"stat_command=$7",
|
|
52
|
+
"rm_command=$8",
|
|
53
|
+
"sleep_command=$9",
|
|
54
|
+
"matches_process() {",
|
|
55
|
+
" expected_process=$1",
|
|
56
|
+
" expected_start=$2",
|
|
57
|
+
" process_stat=",
|
|
58
|
+
' IFS= read -r process_stat < "/proc/$expected_process/stat" || return 1',
|
|
59
|
+
" process_tail=${process_stat##*) }",
|
|
60
|
+
" set -- $process_tail",
|
|
61
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
62
|
+
' [ "${20}" = "$expected_start" ]',
|
|
63
|
+
"}",
|
|
64
|
+
"matches_parent() {",
|
|
65
|
+
' matches_process "$expected_parent" "$expected_parent_start"',
|
|
66
|
+
"}",
|
|
67
|
+
"kill_group() {",
|
|
68
|
+
...SUPERVISOR_KILL_GROUP_BODY.map((line) => ` ${line}`),
|
|
69
|
+
"}",
|
|
70
|
+
"cleanup() {",
|
|
71
|
+
' trap "" TERM INT HUP',
|
|
72
|
+
" kill_group",
|
|
73
|
+
...SUPERVISOR_REMOVE_RUNTIME_BODY.map((line) => ` ${line}`),
|
|
74
|
+
" exit 0",
|
|
75
|
+
"}",
|
|
76
|
+
"trap cleanup TERM INT HUP",
|
|
77
|
+
'while matches_process "$group" "$group_start" && matches_parent; do',
|
|
78
|
+
' "$sleep_command" 0.1',
|
|
79
|
+
"done",
|
|
80
|
+
"kill_group",
|
|
81
|
+
"while matches_parent; do",
|
|
82
|
+
' "$sleep_command" 0.1',
|
|
83
|
+
"done",
|
|
84
|
+
"cleanup",
|
|
85
|
+
].join("\n");
|
|
16
86
|
const SUPERVISOR_SCRIPT = [
|
|
17
87
|
"runtime=$1",
|
|
18
88
|
"identity=$2",
|
|
@@ -20,37 +90,75 @@ const SUPERVISOR_SCRIPT = [
|
|
|
20
90
|
"expected_parent_start=$4",
|
|
21
91
|
"stat_command=$5",
|
|
22
92
|
"rm_command=$6",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
93
|
+
"shell_command=$7",
|
|
94
|
+
"setsid_command=$8",
|
|
95
|
+
"sleep_command=$9",
|
|
96
|
+
"shift 9",
|
|
97
|
+
"watch_name=$1",
|
|
98
|
+
"watch_script=$2",
|
|
99
|
+
"cleanup_name=$3",
|
|
100
|
+
"cleanup_script=$4",
|
|
101
|
+
"shift 4",
|
|
102
|
+
"process_start() {",
|
|
103
|
+
" process_stat=",
|
|
104
|
+
' IFS= read -r process_stat < "/proc/$1/stat" || return 1',
|
|
105
|
+
" process_tail=${process_stat##*) }",
|
|
106
|
+
" set -- $process_tail",
|
|
107
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
108
|
+
' printf "%s" "${20}"',
|
|
26
109
|
"}",
|
|
27
110
|
"matches_parent() {",
|
|
28
111
|
" parent_stat=",
|
|
29
112
|
' IFS= read -r parent_stat < "/proc/$expected_parent/stat" || return 1',
|
|
30
113
|
" parent_tail=${parent_stat##*) }",
|
|
31
114
|
" set -- $parent_tail",
|
|
115
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
32
116
|
' [ "${20}" = "$expected_parent_start" ]',
|
|
33
117
|
"}",
|
|
34
118
|
"terminate() {",
|
|
35
119
|
' trap "" CONT TERM INT HUP',
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
|
|
120
|
+
" while :; do",
|
|
121
|
+
' "$setsid_command" "$shell_command" -c "$cleanup_script" "$cleanup_name" "$runtime" "$identity" "$$" ' +
|
|
122
|
+
'"$stat_command" "$rm_command" "$sleep_command" </dev/null >/dev/null 2>&1 &',
|
|
123
|
+
" cleanup=$!",
|
|
124
|
+
' wait "$cleanup" 2>/dev/null',
|
|
125
|
+
' "$sleep_command" 0.025',
|
|
126
|
+
" done",
|
|
127
|
+
"}",
|
|
128
|
+
"continue_or_terminate() {",
|
|
129
|
+
" remaining=40",
|
|
130
|
+
' while matches_parent && [ "$remaining" -gt 0 ]; do',
|
|
131
|
+
' "$sleep_command" 0.025',
|
|
132
|
+
" remaining=$((remaining - 1))",
|
|
133
|
+
" done",
|
|
134
|
+
" if ! matches_parent; then terminate; fi",
|
|
39
135
|
"}",
|
|
40
|
-
"trap
|
|
136
|
+
"trap continue_or_terminate CONT",
|
|
137
|
+
"trap terminate TERM INT HUP",
|
|
138
|
+
"supervisor_start=$(process_start \"$$\") || terminate",
|
|
139
|
+
'( unset GTKX_PROCESS_GUARD; exec "$setsid_command" "$shell_command" -c "$watch_script" "$watch_name" "$runtime" ' +
|
|
140
|
+
'"$identity" "$$" "$supervisor_start" "$expected_parent" "$expected_parent_start" "$stat_command" ' +
|
|
141
|
+
'"$rm_command" "$sleep_command" ) </dev/null >/dev/null 2>&1 &',
|
|
142
|
+
"child=",
|
|
41
143
|
"if ! matches_parent; then terminate; fi",
|
|
42
144
|
'"$@" &',
|
|
43
145
|
"child=$!",
|
|
44
|
-
|
|
45
|
-
|
|
146
|
+
"while :; do",
|
|
147
|
+
' wait "$child"',
|
|
148
|
+
" status=$?",
|
|
149
|
+
' if kill -0 "$child" 2>/dev/null; then continue; fi',
|
|
150
|
+
' exit "$status"',
|
|
151
|
+
"done",
|
|
46
152
|
].join("\n");
|
|
47
|
-
const
|
|
153
|
+
const processIdentity = (pid) => {
|
|
48
154
|
const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
|
|
49
|
-
const
|
|
50
|
-
|
|
155
|
+
const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ", 20);
|
|
156
|
+
const state = fields[0];
|
|
157
|
+
const startTime = fields[19];
|
|
158
|
+
if (startTime === undefined || state === undefined || ["Z", "X", "x"].includes(state)) {
|
|
51
159
|
throw new Error(`Failed to identify process ${String(pid)}`);
|
|
52
160
|
}
|
|
53
|
-
return
|
|
161
|
+
return { pid, startTime };
|
|
54
162
|
};
|
|
55
163
|
const writeGuardCommand = (child, operation, job) => {
|
|
56
164
|
child.stdin?.write(`${operation}${JSON.stringify(job)}\n`);
|
|
@@ -62,11 +170,31 @@ const releaseGuard = (child) => {
|
|
|
62
170
|
stdin.unref();
|
|
63
171
|
}
|
|
64
172
|
};
|
|
65
|
-
const
|
|
173
|
+
const isSameProcessWatch = (left, right) => left.owner.pid === right.owner.pid &&
|
|
174
|
+
left.owner.startTime === right.owner.startTime &&
|
|
175
|
+
left.target.pid === right.target.pid &&
|
|
176
|
+
left.target.startTime === right.target.startTime;
|
|
177
|
+
const configureProcessWatch = (watch) => {
|
|
178
|
+
if (watch === undefined) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (guard.watch !== undefined && !isSameProcessWatch(guard.watch, watch)) {
|
|
182
|
+
throw new Error("The process guard is already watching another owner");
|
|
183
|
+
}
|
|
184
|
+
if (guard.child !== undefined && guard.watch === undefined) {
|
|
185
|
+
throw new Error("The process guard was started before its owner was registered");
|
|
186
|
+
}
|
|
187
|
+
guard.watch = watch;
|
|
188
|
+
};
|
|
189
|
+
const guardArguments = () => guard.watch === undefined
|
|
190
|
+
? [GUARD_PATH, RUN_PREFIX]
|
|
191
|
+
: [GUARD_PATH, RUN_PREFIX, JSON.stringify(guard.watch)];
|
|
192
|
+
const startGuard = (watch) => {
|
|
193
|
+
configureProcessWatch(watch);
|
|
66
194
|
if (guard.child) {
|
|
67
195
|
return;
|
|
68
196
|
}
|
|
69
|
-
const child = spawn(process.execPath,
|
|
197
|
+
const child = spawn(process.execPath, guardArguments(), {
|
|
70
198
|
detached: true,
|
|
71
199
|
stdio: ["pipe", "ignore", "ignore"],
|
|
72
200
|
});
|
|
@@ -91,6 +219,17 @@ const startGuard = () => {
|
|
|
91
219
|
}
|
|
92
220
|
releaseGuard(child);
|
|
93
221
|
};
|
|
222
|
+
const watchParentProcess = () => {
|
|
223
|
+
const parentId = process.ppid;
|
|
224
|
+
const watch = {
|
|
225
|
+
owner: processIdentity(parentId),
|
|
226
|
+
target: processIdentity(process.pid),
|
|
227
|
+
};
|
|
228
|
+
if (process.ppid !== parentId) {
|
|
229
|
+
throw new Error("The parent process exited while its guard was starting");
|
|
230
|
+
}
|
|
231
|
+
startGuard(watch);
|
|
232
|
+
};
|
|
94
233
|
const registerJob = (job) => {
|
|
95
234
|
guard.jobs.set(job.marker, job);
|
|
96
235
|
if (guard.child) {
|
|
@@ -155,6 +294,8 @@ const spawnWithParentDeathSupervisor = (command, args, options) => {
|
|
|
155
294
|
const shell = resolveExecutable("sh");
|
|
156
295
|
const stat = resolveExecutable("stat");
|
|
157
296
|
const rm = resolveExecutable("rm");
|
|
297
|
+
const setsid = resolveExecutable("setsid");
|
|
298
|
+
const sleep = resolveExecutable("sleep");
|
|
158
299
|
return spawnGuarded(command, { ...options, cleanupDirectories: [options.cleanupDirectory] }, "SIGCONT", (executable, cleanupDirectories) => {
|
|
159
300
|
const identity = cleanupDirectories[0];
|
|
160
301
|
if (identity === undefined) {
|
|
@@ -170,13 +311,20 @@ const spawnWithParentDeathSupervisor = (command, args, options) => {
|
|
|
170
311
|
identity.path,
|
|
171
312
|
`${identity.device}:${identity.inode}:${identity.userId}`,
|
|
172
313
|
String(process.pid),
|
|
173
|
-
|
|
314
|
+
processIdentity(process.pid).startTime,
|
|
174
315
|
stat,
|
|
175
316
|
rm,
|
|
317
|
+
shell,
|
|
318
|
+
setsid,
|
|
319
|
+
sleep,
|
|
320
|
+
SUPERVISOR_WATCH_NAME,
|
|
321
|
+
SUPERVISOR_WATCH_SCRIPT,
|
|
322
|
+
SUPERVISOR_CLEANUP_NAME,
|
|
323
|
+
SUPERVISOR_CLEANUP_SCRIPT,
|
|
176
324
|
executable,
|
|
177
325
|
...args,
|
|
178
326
|
];
|
|
179
327
|
});
|
|
180
328
|
};
|
|
181
|
-
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor };
|
|
329
|
+
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, watchParentProcess };
|
|
182
330
|
//# sourceMappingURL=spawn-with-parent-death-signal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spawn-with-parent-death-signal.js","sourceRoot":"","sources":["../../src/process/spawn-with-parent-death-signal.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn, type StdioOptions } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { Socket } from \"node:net\";\nimport { dirname, extname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { killMarkedProcesses, PROCESS_MARKER } from \"./kill-marked-processes.ts\";\nimport {\n type CleanupDirectoryIdentity,\n cleanupDirectoryIdentity,\n killProcessGroup,\n type ProcessGroupIdentity,\n processGroupIdentity,\n removeCleanupDirectory,\n} from \"./kill-process-group.ts\";\nimport { resolveExecutable } from \"./resolve-executable.ts\";\n\ntype ParentDeathSpawnOptions = {\n stdio?: StdioOptions;\n env?: NodeJS.ProcessEnv;\n cleanupDirectories?: string[];\n};\n\ntype ParentDeathSupervisorOptions = Omit<ParentDeathSpawnOptions, \"cleanupDirectories\"> & {\n cleanupDirectory: string;\n};\n\ntype GuardSignal = \"SIGKILL\" | \"SIGCONT\";\n\ntype LaunchArguments = (executable: string, cleanupDirectories: CleanupDirectoryIdentity[]) => string[];\n\ntype GuardJob = {\n marker: string;\n processGroup: ProcessGroupIdentity;\n cleanupDirectories: CleanupDirectoryIdentity[];\n signal: GuardSignal;\n};\n\ntype GuardState = {\n child?: ChildProcess | undefined;\n jobs: Map<string, GuardJob>;\n};\n\ntype SpawnRollback = {\n child: ChildProcess;\n marker: string;\n processGroup?: ProcessGroupIdentity;\n cleanupDirectories?: CleanupDirectoryIdentity[];\n};\n\nconst MODULE_PATH = fileURLToPath(import.meta.url);\nconst GUARD_PATH = join(dirname(MODULE_PATH), `process-guard${extname(MODULE_PATH)}`);\nconst RUN_ID = randomBytes(8).toString(\"hex\");\nconst RUN_PREFIX = `${PROCESS_MARKER}=${RUN_ID}/`;\nconst guard: GuardState = { jobs: new Map() };\nconst SUPERVISOR_NAME = \"gtkx-process-supervisor\";\nconst SUPERVISOR_SCRIPT = [\n \"runtime=$1\",\n \"identity=$2\",\n \"expected_parent=$3\",\n \"expected_parent_start=$4\",\n \"stat_command=$5\",\n \"rm_command=$6\",\n \"shift 6\",\n \"matches_directory() {\",\n ' [ \"$(\"$stat_command\" -c \"%d:%i:%u\" -- \"$runtime\" 2>/dev/null)\" = \"$identity\" ]',\n \"}\",\n \"matches_parent() {\",\n \" parent_stat=\",\n ' IFS= read -r parent_stat < \"/proc/$expected_parent/stat\" || return 1',\n \" parent_tail=${parent_stat##*) }\",\n \" set -- $parent_tail\",\n ' [ \"${20}\" = \"$expected_parent_start\" ]',\n \"}\",\n \"terminate() {\",\n ' trap \"\" CONT TERM INT HUP',\n ' if matches_directory; then \"$rm_command\" -rf -- \"$runtime\"; fi',\n ' kill -KILL \"-$$\" 2>/dev/null',\n \" exit 1\",\n \"}\",\n \"trap terminate CONT TERM INT HUP\",\n \"if ! matches_parent; then terminate; fi\",\n '\"$@\" &',\n \"child=$!\",\n 'wait \"$child\"',\n \"exit $?\",\n].join(\"\\n\");\n\nconst processStartTime = (pid: number): string => {\n const stat = readFileSync(`/proc/${String(pid)}/stat`, \"utf8\");\n const start = stat.slice(stat.lastIndexOf(\") \") + 2).split(\" \", 20)[19];\n\n if (start === undefined) {\n throw new Error(`Failed to identify process ${String(pid)}`);\n }\n\n return start;\n};\n\nconst writeGuardCommand = (child: ChildProcess, operation: \"+\" | \"-\", job: GuardJob): void => {\n child.stdin?.write(`${operation}${JSON.stringify(job)}\\n`);\n};\n\nconst releaseGuard = (child: ChildProcess): void => {\n child.unref();\n const { stdin } = child;\n\n if (stdin instanceof Socket) {\n stdin.unref();\n }\n};\n\nconst startGuard = (): void => {\n if (guard.child) {\n return;\n }\n\n const child = spawn(process.execPath, [GUARD_PATH, RUN_PREFIX], {\n detached: true,\n stdio: [\"pipe\", \"ignore\", \"ignore\"],\n });\n\n child.on(\"error\", () => {\n if (guard.child === child) {\n guard.child = undefined;\n }\n });\n\n child.on(\"exit\", () => {\n if (guard.child === child) {\n guard.child = undefined;\n }\n });\n\n child.stdin.on(\"error\", () => {\n if (guard.child === child) {\n guard.child = undefined;\n }\n });\n\n guard.child = child;\n\n for (const job of guard.jobs.values()) {\n writeGuardCommand(child, \"+\", job);\n }\n\n releaseGuard(child);\n};\n\nconst registerJob = (job: GuardJob): void => {\n guard.jobs.set(job.marker, job);\n\n if (guard.child) {\n writeGuardCommand(guard.child, \"+\", job);\n }\n};\n\nconst unregisterJob = (job: GuardJob): void => {\n guard.jobs.delete(job.marker);\n\n if (guard.child) {\n writeGuardCommand(guard.child, \"-\", job);\n }\n};\n\nconst rollbackSpawn = ({ child, marker, processGroup, cleanupDirectories = [] }: SpawnRollback): void => {\n if (processGroup === undefined) {\n child.kill(\"SIGKILL\");\n } else {\n killProcessGroup(processGroup);\n }\n\n killMarkedProcesses(marker);\n\n for (const identity of cleanupDirectories) {\n removeCleanupDirectory(identity);\n }\n};\n\nconst captureCleanupDirectories = (command: string, paths: string[]): CleanupDirectoryIdentity[] => {\n const identities = paths.map((path) => cleanupDirectoryIdentity(path));\n\n if (identities.includes(undefined)) {\n throw new Error(`Failed to identify a cleanup directory for ${command}`);\n }\n\n return identities.filter((identity): identity is CleanupDirectoryIdentity => identity !== undefined);\n};\n\nconst spawnGuarded = (\n command: string,\n options: ParentDeathSpawnOptions,\n signal: GuardSignal,\n launchArguments: LaunchArguments,\n): ChildProcess => {\n const executable = resolveExecutable(command);\n const setpriv = resolveExecutable(\"setpriv\");\n const jobValue = `${RUN_ID}/${randomBytes(4).toString(\"hex\")}`;\n const marker = `${PROCESS_MARKER}=${jobValue}`;\n const cleanupDirectories = captureCleanupDirectories(command, options.cleanupDirectories ?? []);\n startGuard();\n\n const child = spawn(setpriv, launchArguments(executable, cleanupDirectories), {\n detached: true,\n stdio: options.stdio ?? \"ignore\",\n env: { ...(options.env ?? process.env), [PROCESS_MARKER]: jobValue },\n });\n\n const processGroupId = child.pid;\n const group = processGroupId === undefined ? undefined : processGroupIdentity(processGroupId);\n\n if (group === undefined) {\n rollbackSpawn({ child, marker, cleanupDirectories });\n throw new Error(`Failed to identify process group for ${command}`);\n }\n\n const job: GuardJob = { marker, processGroup: group, cleanupDirectories, signal };\n registerJob(job);\n\n child.on(\"exit\", () => {\n killMarkedProcesses(job.marker);\n unregisterJob(job);\n });\n\n return child;\n};\n\nfunction spawnWithParentDeathSignal(\n command: string,\n args: string[],\n options: ParentDeathSpawnOptions = {},\n): ChildProcess {\n return spawnGuarded(\n command,\n options,\n \"SIGKILL\",\n (executable) => [\"--pdeathsig\", \"SIGKILL\", executable, ...args],\n );\n}\n\nconst spawnWithParentDeathSupervisor = (\n command: string,\n args: string[],\n options: ParentDeathSupervisorOptions,\n): ChildProcess => {\n const shell = resolveExecutable(\"sh\");\n const stat = resolveExecutable(\"stat\");\n const rm = resolveExecutable(\"rm\");\n\n return spawnGuarded(\n command,\n { ...options, cleanupDirectories: [options.cleanupDirectory] },\n \"SIGCONT\",\n (executable, cleanupDirectories) => {\n const identity = cleanupDirectories[0];\n\n if (identity === undefined) {\n throw new Error(`Failed to identify a cleanup directory for ${command}`);\n }\n\n return [\n \"--pdeathsig\",\n \"SIGCONT\",\n shell,\n \"-c\",\n SUPERVISOR_SCRIPT,\n SUPERVISOR_NAME,\n identity.path,\n `${identity.device}:${identity.inode}:${identity.userId}`,\n String(process.pid),\n processStartTime(process.pid),\n stat,\n rm,\n executable,\n ...args,\n ];\n },\n );\n};\n\nexport { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor };\n"]}
|
|
1
|
+
{"version":3,"file":"spawn-with-parent-death-signal.js","sourceRoot":"","sources":["../../src/process/spawn-with-parent-death-signal.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn, type StdioOptions } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { Socket } from \"node:net\";\nimport { dirname, extname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { killMarkedProcesses, PROCESS_MARKER } from \"./kill-marked-processes.ts\";\nimport {\n type CleanupDirectoryIdentity,\n cleanupDirectoryIdentity,\n killProcessGroup,\n type ProcessGroupIdentity,\n processGroupIdentity,\n removeCleanupDirectory,\n} from \"./kill-process-group.ts\";\nimport { resolveExecutable } from \"./resolve-executable.ts\";\n\ntype ParentDeathSpawnOptions = {\n stdio?: StdioOptions;\n env?: NodeJS.ProcessEnv;\n cleanupDirectories?: string[];\n};\n\ntype ParentDeathSupervisorOptions = Omit<ParentDeathSpawnOptions, \"cleanupDirectories\"> & {\n cleanupDirectory: string;\n};\n\ntype GuardSignal = \"SIGKILL\" | \"SIGCONT\";\n\ntype ProcessIdentity = {\n pid: number;\n startTime: string;\n};\n\ntype ProcessWatch = {\n owner: ProcessIdentity;\n target: ProcessIdentity;\n};\n\ntype LaunchArguments = (executable: string, cleanupDirectories: CleanupDirectoryIdentity[]) => string[];\n\ntype GuardJob = {\n marker: string;\n processGroup: ProcessGroupIdentity;\n cleanupDirectories: CleanupDirectoryIdentity[];\n signal: GuardSignal;\n};\n\ntype GuardState = {\n child?: ChildProcess | undefined;\n jobs: Map<string, GuardJob>;\n watch?: ProcessWatch;\n};\n\ntype SpawnRollback = {\n child: ChildProcess;\n marker: string;\n processGroup?: ProcessGroupIdentity;\n cleanupDirectories?: CleanupDirectoryIdentity[];\n};\n\nconst MODULE_PATH = fileURLToPath(import.meta.url);\nconst GUARD_PATH = join(dirname(MODULE_PATH), `process-guard${extname(MODULE_PATH)}`);\nconst RUN_ID = randomBytes(8).toString(\"hex\");\nconst RUN_PREFIX = `${PROCESS_MARKER}=${RUN_ID}/`;\nconst guard: GuardState = { jobs: new Map() };\nconst SUPERVISOR_NAME = \"gtkx-process-supervisor\";\nconst SUPERVISOR_CLEANUP_NAME = \"gtkx-process-cleanup\";\nconst SUPERVISOR_WATCH_NAME = \"gtkx-process-watch\";\nconst SUPERVISOR_KILL_GROUP_BODY = [\n \"remaining=40\",\n 'while [ \"$remaining\" -gt 0 ] && kill -KILL \"-$group\" 2>/dev/null; do',\n ' \"$sleep_command\" 0.025',\n \" remaining=$((remaining - 1))\",\n \"done\",\n];\nconst SUPERVISOR_REMOVE_RUNTIME_BODY = [\n \"remaining=40\",\n 'while [ \"$remaining\" -gt 0 ] && ' +\n '[ \"$(\"$stat_command\" -c \"%d:%i:%u\" -- \"$runtime\" 2>/dev/null)\" = \"$identity\" ]; do',\n ' \"$rm_command\" -rf -- \"$runtime\"',\n ' \"$sleep_command\" 0.025',\n \" remaining=$((remaining - 1))\",\n \"done\",\n];\nconst SUPERVISOR_CLEANUP_BODY = [...SUPERVISOR_KILL_GROUP_BODY, ...SUPERVISOR_REMOVE_RUNTIME_BODY];\nconst SUPERVISOR_CLEANUP_SCRIPT = [\n \"runtime=$1\",\n \"identity=$2\",\n \"group=$3\",\n \"stat_command=$4\",\n \"rm_command=$5\",\n \"sleep_command=$6\",\n ...SUPERVISOR_CLEANUP_BODY,\n].join(\"\\n\");\nconst SUPERVISOR_WATCH_SCRIPT = [\n \"runtime=$1\",\n \"identity=$2\",\n \"group=$3\",\n \"group_start=$4\",\n \"expected_parent=$5\",\n \"expected_parent_start=$6\",\n \"stat_command=$7\",\n \"rm_command=$8\",\n \"sleep_command=$9\",\n \"matches_process() {\",\n \" expected_process=$1\",\n \" expected_start=$2\",\n \" process_stat=\",\n ' IFS= read -r process_stat < \"/proc/$expected_process/stat\" || return 1',\n \" process_tail=${process_stat##*) }\",\n \" set -- $process_tail\",\n ' case \"$1\" in Z|X|x) return 1 ;; esac',\n ' [ \"${20}\" = \"$expected_start\" ]',\n \"}\",\n \"matches_parent() {\",\n ' matches_process \"$expected_parent\" \"$expected_parent_start\"',\n \"}\",\n \"kill_group() {\",\n ...SUPERVISOR_KILL_GROUP_BODY.map((line) => ` ${line}`),\n \"}\",\n \"cleanup() {\",\n ' trap \"\" TERM INT HUP',\n \" kill_group\",\n ...SUPERVISOR_REMOVE_RUNTIME_BODY.map((line) => ` ${line}`),\n \" exit 0\",\n \"}\",\n \"trap cleanup TERM INT HUP\",\n 'while matches_process \"$group\" \"$group_start\" && matches_parent; do',\n ' \"$sleep_command\" 0.1',\n \"done\",\n \"kill_group\",\n \"while matches_parent; do\",\n ' \"$sleep_command\" 0.1',\n \"done\",\n \"cleanup\",\n].join(\"\\n\");\nconst SUPERVISOR_SCRIPT = [\n \"runtime=$1\",\n \"identity=$2\",\n \"expected_parent=$3\",\n \"expected_parent_start=$4\",\n \"stat_command=$5\",\n \"rm_command=$6\",\n \"shell_command=$7\",\n \"setsid_command=$8\",\n \"sleep_command=$9\",\n \"shift 9\",\n \"watch_name=$1\",\n \"watch_script=$2\",\n \"cleanup_name=$3\",\n \"cleanup_script=$4\",\n \"shift 4\",\n \"process_start() {\",\n \" process_stat=\",\n ' IFS= read -r process_stat < \"/proc/$1/stat\" || return 1',\n \" process_tail=${process_stat##*) }\",\n \" set -- $process_tail\",\n ' case \"$1\" in Z|X|x) return 1 ;; esac',\n ' printf \"%s\" \"${20}\"',\n \"}\",\n \"matches_parent() {\",\n \" parent_stat=\",\n ' IFS= read -r parent_stat < \"/proc/$expected_parent/stat\" || return 1',\n \" parent_tail=${parent_stat##*) }\",\n \" set -- $parent_tail\",\n ' case \"$1\" in Z|X|x) return 1 ;; esac',\n ' [ \"${20}\" = \"$expected_parent_start\" ]',\n \"}\",\n \"terminate() {\",\n ' trap \"\" CONT TERM INT HUP',\n \" while :; do\",\n ' \"$setsid_command\" \"$shell_command\" -c \"$cleanup_script\" \"$cleanup_name\" \"$runtime\" \"$identity\" \"$$\" ' +\n '\"$stat_command\" \"$rm_command\" \"$sleep_command\" </dev/null >/dev/null 2>&1 &',\n \" cleanup=$!\",\n ' wait \"$cleanup\" 2>/dev/null',\n ' \"$sleep_command\" 0.025',\n \" done\",\n \"}\",\n \"continue_or_terminate() {\",\n \" remaining=40\",\n ' while matches_parent && [ \"$remaining\" -gt 0 ]; do',\n ' \"$sleep_command\" 0.025',\n \" remaining=$((remaining - 1))\",\n \" done\",\n \" if ! matches_parent; then terminate; fi\",\n \"}\",\n \"trap continue_or_terminate CONT\",\n \"trap terminate TERM INT HUP\",\n \"supervisor_start=$(process_start \\\"$$\\\") || terminate\",\n '( unset GTKX_PROCESS_GUARD; exec \"$setsid_command\" \"$shell_command\" -c \"$watch_script\" \"$watch_name\" \"$runtime\" ' +\n '\"$identity\" \"$$\" \"$supervisor_start\" \"$expected_parent\" \"$expected_parent_start\" \"$stat_command\" ' +\n '\"$rm_command\" \"$sleep_command\" ) </dev/null >/dev/null 2>&1 &',\n \"child=\",\n \"if ! matches_parent; then terminate; fi\",\n '\"$@\" &',\n \"child=$!\",\n \"while :; do\",\n ' wait \"$child\"',\n \" status=$?\",\n ' if kill -0 \"$child\" 2>/dev/null; then continue; fi',\n ' exit \"$status\"',\n \"done\",\n].join(\"\\n\");\n\nconst processIdentity = (pid: number): ProcessIdentity => {\n const stat = readFileSync(`/proc/${String(pid)}/stat`, \"utf8\");\n const fields = stat.slice(stat.lastIndexOf(\") \") + 2).split(\" \", 20);\n const state = fields[0];\n const startTime = fields[19];\n\n if (startTime === undefined || state === undefined || [\"Z\", \"X\", \"x\"].includes(state)) {\n throw new Error(`Failed to identify process ${String(pid)}`);\n }\n\n return { pid, startTime };\n};\n\nconst writeGuardCommand = (child: ChildProcess, operation: \"+\" | \"-\", job: GuardJob): void => {\n child.stdin?.write(`${operation}${JSON.stringify(job)}\\n`);\n};\n\nconst releaseGuard = (child: ChildProcess): void => {\n child.unref();\n const { stdin } = child;\n\n if (stdin instanceof Socket) {\n stdin.unref();\n }\n};\n\nconst isSameProcessWatch = (left: ProcessWatch, right: ProcessWatch): boolean =>\n left.owner.pid === right.owner.pid &&\n left.owner.startTime === right.owner.startTime &&\n left.target.pid === right.target.pid &&\n left.target.startTime === right.target.startTime;\n\nconst configureProcessWatch = (watch?: ProcessWatch): void => {\n if (watch === undefined) {\n return;\n }\n\n if (guard.watch !== undefined && !isSameProcessWatch(guard.watch, watch)) {\n throw new Error(\"The process guard is already watching another owner\");\n }\n\n if (guard.child !== undefined && guard.watch === undefined) {\n throw new Error(\"The process guard was started before its owner was registered\");\n }\n\n guard.watch = watch;\n};\n\nconst guardArguments = (): string[] =>\n guard.watch === undefined\n ? [GUARD_PATH, RUN_PREFIX]\n : [GUARD_PATH, RUN_PREFIX, JSON.stringify(guard.watch)];\n\nconst startGuard = (watch?: ProcessWatch): void => {\n configureProcessWatch(watch);\n\n if (guard.child) {\n return;\n }\n\n const child = spawn(process.execPath, guardArguments(), {\n detached: true,\n stdio: [\"pipe\", \"ignore\", \"ignore\"],\n });\n\n child.on(\"error\", () => {\n if (guard.child === child) {\n guard.child = undefined;\n }\n });\n\n child.on(\"exit\", () => {\n if (guard.child === child) {\n guard.child = undefined;\n }\n });\n\n child.stdin.on(\"error\", () => {\n if (guard.child === child) {\n guard.child = undefined;\n }\n });\n\n guard.child = child;\n\n for (const job of guard.jobs.values()) {\n writeGuardCommand(child, \"+\", job);\n }\n\n releaseGuard(child);\n};\n\nconst watchParentProcess = (): void => {\n const parentId = process.ppid;\n const watch = {\n owner: processIdentity(parentId),\n target: processIdentity(process.pid),\n };\n\n if (process.ppid !== parentId) {\n throw new Error(\"The parent process exited while its guard was starting\");\n }\n\n startGuard(watch);\n};\n\nconst registerJob = (job: GuardJob): void => {\n guard.jobs.set(job.marker, job);\n\n if (guard.child) {\n writeGuardCommand(guard.child, \"+\", job);\n }\n};\n\nconst unregisterJob = (job: GuardJob): void => {\n guard.jobs.delete(job.marker);\n\n if (guard.child) {\n writeGuardCommand(guard.child, \"-\", job);\n }\n};\n\nconst rollbackSpawn = ({ child, marker, processGroup, cleanupDirectories = [] }: SpawnRollback): void => {\n if (processGroup === undefined) {\n child.kill(\"SIGKILL\");\n } else {\n killProcessGroup(processGroup);\n }\n\n killMarkedProcesses(marker);\n\n for (const identity of cleanupDirectories) {\n removeCleanupDirectory(identity);\n }\n};\n\nconst captureCleanupDirectories = (command: string, paths: string[]): CleanupDirectoryIdentity[] => {\n const identities = paths.map((path) => cleanupDirectoryIdentity(path));\n\n if (identities.includes(undefined)) {\n throw new Error(`Failed to identify a cleanup directory for ${command}`);\n }\n\n return identities.filter((identity): identity is CleanupDirectoryIdentity => identity !== undefined);\n};\n\nconst spawnGuarded = (\n command: string,\n options: ParentDeathSpawnOptions,\n signal: GuardSignal,\n launchArguments: LaunchArguments,\n): ChildProcess => {\n const executable = resolveExecutable(command);\n const setpriv = resolveExecutable(\"setpriv\");\n const jobValue = `${RUN_ID}/${randomBytes(4).toString(\"hex\")}`;\n const marker = `${PROCESS_MARKER}=${jobValue}`;\n const cleanupDirectories = captureCleanupDirectories(command, options.cleanupDirectories ?? []);\n startGuard();\n\n const child = spawn(setpriv, launchArguments(executable, cleanupDirectories), {\n detached: true,\n stdio: options.stdio ?? \"ignore\",\n env: { ...(options.env ?? process.env), [PROCESS_MARKER]: jobValue },\n });\n\n const processGroupId = child.pid;\n const group = processGroupId === undefined ? undefined : processGroupIdentity(processGroupId);\n\n if (group === undefined) {\n rollbackSpawn({ child, marker, cleanupDirectories });\n throw new Error(`Failed to identify process group for ${command}`);\n }\n\n const job: GuardJob = { marker, processGroup: group, cleanupDirectories, signal };\n registerJob(job);\n\n child.on(\"exit\", () => {\n killMarkedProcesses(job.marker);\n unregisterJob(job);\n });\n\n return child;\n};\n\nfunction spawnWithParentDeathSignal(\n command: string,\n args: string[],\n options: ParentDeathSpawnOptions = {},\n): ChildProcess {\n return spawnGuarded(\n command,\n options,\n \"SIGKILL\",\n (executable) => [\"--pdeathsig\", \"SIGKILL\", executable, ...args],\n );\n}\n\nconst spawnWithParentDeathSupervisor = (\n command: string,\n args: string[],\n options: ParentDeathSupervisorOptions,\n): ChildProcess => {\n const shell = resolveExecutable(\"sh\");\n const stat = resolveExecutable(\"stat\");\n const rm = resolveExecutable(\"rm\");\n const setsid = resolveExecutable(\"setsid\");\n const sleep = resolveExecutable(\"sleep\");\n\n return spawnGuarded(\n command,\n { ...options, cleanupDirectories: [options.cleanupDirectory] },\n \"SIGCONT\",\n (executable, cleanupDirectories) => {\n const identity = cleanupDirectories[0];\n\n if (identity === undefined) {\n throw new Error(`Failed to identify a cleanup directory for ${command}`);\n }\n\n return [\n \"--pdeathsig\",\n \"SIGCONT\",\n shell,\n \"-c\",\n SUPERVISOR_SCRIPT,\n SUPERVISOR_NAME,\n identity.path,\n `${identity.device}:${identity.inode}:${identity.userId}`,\n String(process.pid),\n processIdentity(process.pid).startTime,\n stat,\n rm,\n shell,\n setsid,\n sleep,\n SUPERVISOR_WATCH_NAME,\n SUPERVISOR_WATCH_SCRIPT,\n SUPERVISOR_CLEANUP_NAME,\n SUPERVISOR_CLEANUP_SCRIPT,\n executable,\n ...args,\n ];\n },\n );\n};\n\nexport { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, watchParentProcess };\n"]}
|
package/package.json
CHANGED
package/src/process/index.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
export { exitCodeForSignal } from "./exit-code-for-signal.ts";
|
|
2
2
|
export { installGracefulShutdown } from "./install-graceful-shutdown.ts";
|
|
3
3
|
export {
|
|
4
|
+
type CleanupDirectoryIdentity,
|
|
5
|
+
cleanupDirectoryIdentity,
|
|
4
6
|
killProcessGroup,
|
|
5
7
|
type ProcessGroupIdentity,
|
|
6
8
|
processGroupIdentity,
|
|
9
|
+
removeCleanupDirectory,
|
|
7
10
|
} from "./kill-process-group.ts";
|
|
8
11
|
export { resolveExecutable, tryResolveExecutable } from "./resolve-executable.ts";
|
|
9
12
|
export {
|
|
10
13
|
spawnWithParentDeathSignal,
|
|
11
14
|
spawnWithParentDeathSupervisor,
|
|
15
|
+
watchParentProcess,
|
|
12
16
|
} from "./spawn-with-parent-death-signal.ts";
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { killMarkedProcesses, killMarkedProcessRun } from "./kill-marked-processes.ts";
|
|
2
3
|
import {
|
|
3
4
|
type CleanupDirectoryIdentity,
|
|
4
5
|
killProcessGroup,
|
|
5
6
|
type ProcessGroupIdentity,
|
|
7
|
+
processGroupIdentity,
|
|
6
8
|
removeCleanupDirectory,
|
|
7
9
|
} from "./kill-process-group.ts";
|
|
8
10
|
|
|
9
11
|
const GUARD_PREFIX = process.argv[2] ?? "";
|
|
12
|
+
const PROCESS_WATCH_ARGUMENT = process.argv[3];
|
|
10
13
|
const WATCHED_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"] as const satisfies NodeJS.Signals[];
|
|
14
|
+
const OWNER_POLL_INTERVAL_MS = 50;
|
|
15
|
+
const SUPERVISOR_EXIT_TIMEOUT_MS = 2000;
|
|
16
|
+
|
|
17
|
+
type ProcessIdentity = {
|
|
18
|
+
pid: number;
|
|
19
|
+
startTime: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ProcessWatch = {
|
|
23
|
+
owner: ProcessIdentity;
|
|
24
|
+
target: ProcessIdentity;
|
|
25
|
+
};
|
|
26
|
+
|
|
11
27
|
type GuardJob = {
|
|
12
28
|
marker: string;
|
|
13
29
|
processGroup: ProcessGroupIdentity;
|
|
@@ -24,6 +40,17 @@ const state: { bufferedCommands: string; isSweeping: boolean; jobs: Map<string,
|
|
|
24
40
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
25
41
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
42
|
|
|
43
|
+
const isProcessIdentity = (value: unknown): value is ProcessIdentity =>
|
|
44
|
+
isRecord(value) &&
|
|
45
|
+
typeof value.pid === "number" &&
|
|
46
|
+
Number.isSafeInteger(value.pid) &&
|
|
47
|
+
value.pid > 1 &&
|
|
48
|
+
typeof value.startTime === "string" &&
|
|
49
|
+
/^\d+$/.test(value.startTime);
|
|
50
|
+
|
|
51
|
+
const isProcessWatch = (value: unknown): value is ProcessWatch =>
|
|
52
|
+
isRecord(value) && isProcessIdentity(value.owner) && isProcessIdentity(value.target);
|
|
53
|
+
|
|
27
54
|
const isProcessGroupIdentity = (value: unknown): value is ProcessGroupIdentity =>
|
|
28
55
|
isRecord(value) &&
|
|
29
56
|
typeof value.processGroupId === "number" &&
|
|
@@ -47,6 +74,50 @@ const isGuardJob = (value: unknown): value is GuardJob =>
|
|
|
47
74
|
value.cleanupDirectories.every(isCleanupDirectoryIdentity) &&
|
|
48
75
|
(value.signal === "SIGKILL" || value.signal === "SIGCONT");
|
|
49
76
|
|
|
77
|
+
const parseProcessWatch = (): ProcessWatch | undefined => {
|
|
78
|
+
if (PROCESS_WATCH_ARGUMENT === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const value: unknown = JSON.parse(PROCESS_WATCH_ARGUMENT);
|
|
84
|
+
|
|
85
|
+
return isProcessWatch(value) ? value : undefined;
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const currentProcessIdentity = (pid: number): ProcessIdentity | undefined => {
|
|
92
|
+
try {
|
|
93
|
+
const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
|
|
94
|
+
const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ", 20);
|
|
95
|
+
const state = fields[0];
|
|
96
|
+
const startTime = fields[19];
|
|
97
|
+
|
|
98
|
+
return startTime !== undefined && state !== undefined && !["Z", "X", "x"].includes(state)
|
|
99
|
+
? { pid, startTime }
|
|
100
|
+
: undefined;
|
|
101
|
+
} catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const isCurrentProcess = (identity: ProcessIdentity): boolean =>
|
|
107
|
+
currentProcessIdentity(identity.pid)?.startTime === identity.startTime;
|
|
108
|
+
|
|
109
|
+
const killProcess = (identity: ProcessIdentity): void => {
|
|
110
|
+
if (!isCurrentProcess(identity)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
process.kill(identity.pid, "SIGKILL");
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
50
121
|
const applyCommand = (command: string): void => {
|
|
51
122
|
const operation = command[0];
|
|
52
123
|
let value: unknown;
|
|
@@ -102,27 +173,103 @@ const removeCleanupDirectories = (): void => {
|
|
|
102
173
|
}
|
|
103
174
|
};
|
|
104
175
|
|
|
105
|
-
const
|
|
176
|
+
const hasRunningSupervisor = (): boolean => {
|
|
177
|
+
for (const job of state.jobs.values()) {
|
|
178
|
+
if (job.signal !== "SIGCONT") {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const current = processGroupIdentity(job.processGroup.processGroupId);
|
|
183
|
+
|
|
184
|
+
if (current?.leaderStartTime === job.processGroup.leaderStartTime) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const forceSupervisors = (): void => {
|
|
193
|
+
for (const job of state.jobs.values()) {
|
|
194
|
+
if (job.signal === "SIGCONT") {
|
|
195
|
+
killProcessGroup(job.processGroup);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const finishSweep = (): void => {
|
|
201
|
+
if (GUARD_PREFIX.length > 0) {
|
|
202
|
+
killMarkedProcessRun(GUARD_PREFIX);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
removeCleanupDirectories();
|
|
206
|
+
process.exit(0);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const awaitSupervisors = (deadline: number): void => {
|
|
210
|
+
if (hasRunningSupervisor() && Date.now() < deadline) {
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
awaitSupervisors(deadline);
|
|
213
|
+
}, OWNER_POLL_INTERVAL_MS);
|
|
214
|
+
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
forceSupervisors();
|
|
219
|
+
finishSweep();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const sweep = (target?: ProcessIdentity): void => {
|
|
106
223
|
if (state.isSweeping) {
|
|
107
224
|
return;
|
|
108
225
|
}
|
|
109
226
|
|
|
110
227
|
state.isSweeping = true;
|
|
111
228
|
applyCommand(state.bufferedCommands);
|
|
229
|
+
|
|
230
|
+
if (target !== undefined) {
|
|
231
|
+
killProcess(target);
|
|
232
|
+
}
|
|
233
|
+
|
|
112
234
|
killJobs();
|
|
113
|
-
|
|
235
|
+
awaitSupervisors(Date.now() + SUPERVISOR_EXIT_TIMEOUT_MS);
|
|
236
|
+
};
|
|
114
237
|
|
|
115
|
-
|
|
116
|
-
|
|
238
|
+
const watch = parseProcessWatch();
|
|
239
|
+
|
|
240
|
+
const startOwnerPoll = (): void => {
|
|
241
|
+
if (watch === undefined) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const ownerPoll = setInterval(() => {
|
|
246
|
+
if (isCurrentProcess(watch.owner)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
clearInterval(ownerPoll);
|
|
251
|
+
sweep(watch.target);
|
|
252
|
+
}, OWNER_POLL_INTERVAL_MS);
|
|
253
|
+
|
|
254
|
+
if (!isCurrentProcess(watch.owner)) {
|
|
255
|
+
clearInterval(ownerPoll);
|
|
256
|
+
sweep(watch.target);
|
|
117
257
|
}
|
|
118
|
-
process.exit(0);
|
|
119
258
|
};
|
|
120
259
|
|
|
260
|
+
startOwnerPoll();
|
|
261
|
+
|
|
121
262
|
process.stdin.resume();
|
|
122
263
|
process.stdin.on("data", receiveCommands);
|
|
123
|
-
process.stdin.on("end",
|
|
124
|
-
|
|
264
|
+
process.stdin.on("end", () => {
|
|
265
|
+
sweep();
|
|
266
|
+
});
|
|
267
|
+
process.stdin.on("error", () => {
|
|
268
|
+
sweep();
|
|
269
|
+
});
|
|
125
270
|
|
|
126
271
|
for (const signal of WATCHED_SIGNALS) {
|
|
127
|
-
process.on(signal,
|
|
272
|
+
process.on(signal, () => {
|
|
273
|
+
sweep();
|
|
274
|
+
});
|
|
128
275
|
}
|
|
@@ -27,6 +27,16 @@ type ParentDeathSupervisorOptions = Omit<ParentDeathSpawnOptions, "cleanupDirect
|
|
|
27
27
|
|
|
28
28
|
type GuardSignal = "SIGKILL" | "SIGCONT";
|
|
29
29
|
|
|
30
|
+
type ProcessIdentity = {
|
|
31
|
+
pid: number;
|
|
32
|
+
startTime: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ProcessWatch = {
|
|
36
|
+
owner: ProcessIdentity;
|
|
37
|
+
target: ProcessIdentity;
|
|
38
|
+
};
|
|
39
|
+
|
|
30
40
|
type LaunchArguments = (executable: string, cleanupDirectories: CleanupDirectoryIdentity[]) => string[];
|
|
31
41
|
|
|
32
42
|
type GuardJob = {
|
|
@@ -39,6 +49,7 @@ type GuardJob = {
|
|
|
39
49
|
type GuardState = {
|
|
40
50
|
child?: ChildProcess | undefined;
|
|
41
51
|
jobs: Map<string, GuardJob>;
|
|
52
|
+
watch?: ProcessWatch;
|
|
42
53
|
};
|
|
43
54
|
|
|
44
55
|
type SpawnRollback = {
|
|
@@ -54,6 +65,76 @@ const RUN_ID = randomBytes(8).toString("hex");
|
|
|
54
65
|
const RUN_PREFIX = `${PROCESS_MARKER}=${RUN_ID}/`;
|
|
55
66
|
const guard: GuardState = { jobs: new Map() };
|
|
56
67
|
const SUPERVISOR_NAME = "gtkx-process-supervisor";
|
|
68
|
+
const SUPERVISOR_CLEANUP_NAME = "gtkx-process-cleanup";
|
|
69
|
+
const SUPERVISOR_WATCH_NAME = "gtkx-process-watch";
|
|
70
|
+
const SUPERVISOR_KILL_GROUP_BODY = [
|
|
71
|
+
"remaining=40",
|
|
72
|
+
'while [ "$remaining" -gt 0 ] && kill -KILL "-$group" 2>/dev/null; do',
|
|
73
|
+
' "$sleep_command" 0.025',
|
|
74
|
+
" remaining=$((remaining - 1))",
|
|
75
|
+
"done",
|
|
76
|
+
];
|
|
77
|
+
const SUPERVISOR_REMOVE_RUNTIME_BODY = [
|
|
78
|
+
"remaining=40",
|
|
79
|
+
'while [ "$remaining" -gt 0 ] && ' +
|
|
80
|
+
'[ "$("$stat_command" -c "%d:%i:%u" -- "$runtime" 2>/dev/null)" = "$identity" ]; do',
|
|
81
|
+
' "$rm_command" -rf -- "$runtime"',
|
|
82
|
+
' "$sleep_command" 0.025',
|
|
83
|
+
" remaining=$((remaining - 1))",
|
|
84
|
+
"done",
|
|
85
|
+
];
|
|
86
|
+
const SUPERVISOR_CLEANUP_BODY = [...SUPERVISOR_KILL_GROUP_BODY, ...SUPERVISOR_REMOVE_RUNTIME_BODY];
|
|
87
|
+
const SUPERVISOR_CLEANUP_SCRIPT = [
|
|
88
|
+
"runtime=$1",
|
|
89
|
+
"identity=$2",
|
|
90
|
+
"group=$3",
|
|
91
|
+
"stat_command=$4",
|
|
92
|
+
"rm_command=$5",
|
|
93
|
+
"sleep_command=$6",
|
|
94
|
+
...SUPERVISOR_CLEANUP_BODY,
|
|
95
|
+
].join("\n");
|
|
96
|
+
const SUPERVISOR_WATCH_SCRIPT = [
|
|
97
|
+
"runtime=$1",
|
|
98
|
+
"identity=$2",
|
|
99
|
+
"group=$3",
|
|
100
|
+
"group_start=$4",
|
|
101
|
+
"expected_parent=$5",
|
|
102
|
+
"expected_parent_start=$6",
|
|
103
|
+
"stat_command=$7",
|
|
104
|
+
"rm_command=$8",
|
|
105
|
+
"sleep_command=$9",
|
|
106
|
+
"matches_process() {",
|
|
107
|
+
" expected_process=$1",
|
|
108
|
+
" expected_start=$2",
|
|
109
|
+
" process_stat=",
|
|
110
|
+
' IFS= read -r process_stat < "/proc/$expected_process/stat" || return 1',
|
|
111
|
+
" process_tail=${process_stat##*) }",
|
|
112
|
+
" set -- $process_tail",
|
|
113
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
114
|
+
' [ "${20}" = "$expected_start" ]',
|
|
115
|
+
"}",
|
|
116
|
+
"matches_parent() {",
|
|
117
|
+
' matches_process "$expected_parent" "$expected_parent_start"',
|
|
118
|
+
"}",
|
|
119
|
+
"kill_group() {",
|
|
120
|
+
...SUPERVISOR_KILL_GROUP_BODY.map((line) => ` ${line}`),
|
|
121
|
+
"}",
|
|
122
|
+
"cleanup() {",
|
|
123
|
+
' trap "" TERM INT HUP',
|
|
124
|
+
" kill_group",
|
|
125
|
+
...SUPERVISOR_REMOVE_RUNTIME_BODY.map((line) => ` ${line}`),
|
|
126
|
+
" exit 0",
|
|
127
|
+
"}",
|
|
128
|
+
"trap cleanup TERM INT HUP",
|
|
129
|
+
'while matches_process "$group" "$group_start" && matches_parent; do',
|
|
130
|
+
' "$sleep_command" 0.1',
|
|
131
|
+
"done",
|
|
132
|
+
"kill_group",
|
|
133
|
+
"while matches_parent; do",
|
|
134
|
+
' "$sleep_command" 0.1',
|
|
135
|
+
"done",
|
|
136
|
+
"cleanup",
|
|
137
|
+
].join("\n");
|
|
57
138
|
const SUPERVISOR_SCRIPT = [
|
|
58
139
|
"runtime=$1",
|
|
59
140
|
"identity=$2",
|
|
@@ -61,40 +142,78 @@ const SUPERVISOR_SCRIPT = [
|
|
|
61
142
|
"expected_parent_start=$4",
|
|
62
143
|
"stat_command=$5",
|
|
63
144
|
"rm_command=$6",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
145
|
+
"shell_command=$7",
|
|
146
|
+
"setsid_command=$8",
|
|
147
|
+
"sleep_command=$9",
|
|
148
|
+
"shift 9",
|
|
149
|
+
"watch_name=$1",
|
|
150
|
+
"watch_script=$2",
|
|
151
|
+
"cleanup_name=$3",
|
|
152
|
+
"cleanup_script=$4",
|
|
153
|
+
"shift 4",
|
|
154
|
+
"process_start() {",
|
|
155
|
+
" process_stat=",
|
|
156
|
+
' IFS= read -r process_stat < "/proc/$1/stat" || return 1',
|
|
157
|
+
" process_tail=${process_stat##*) }",
|
|
158
|
+
" set -- $process_tail",
|
|
159
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
160
|
+
' printf "%s" "${20}"',
|
|
67
161
|
"}",
|
|
68
162
|
"matches_parent() {",
|
|
69
163
|
" parent_stat=",
|
|
70
164
|
' IFS= read -r parent_stat < "/proc/$expected_parent/stat" || return 1',
|
|
71
165
|
" parent_tail=${parent_stat##*) }",
|
|
72
166
|
" set -- $parent_tail",
|
|
167
|
+
' case "$1" in Z|X|x) return 1 ;; esac',
|
|
73
168
|
' [ "${20}" = "$expected_parent_start" ]',
|
|
74
169
|
"}",
|
|
75
170
|
"terminate() {",
|
|
76
171
|
' trap "" CONT TERM INT HUP',
|
|
77
|
-
|
|
78
|
-
'
|
|
79
|
-
"
|
|
172
|
+
" while :; do",
|
|
173
|
+
' "$setsid_command" "$shell_command" -c "$cleanup_script" "$cleanup_name" "$runtime" "$identity" "$$" ' +
|
|
174
|
+
'"$stat_command" "$rm_command" "$sleep_command" </dev/null >/dev/null 2>&1 &',
|
|
175
|
+
" cleanup=$!",
|
|
176
|
+
' wait "$cleanup" 2>/dev/null',
|
|
177
|
+
' "$sleep_command" 0.025',
|
|
178
|
+
" done",
|
|
80
179
|
"}",
|
|
81
|
-
"
|
|
180
|
+
"continue_or_terminate() {",
|
|
181
|
+
" remaining=40",
|
|
182
|
+
' while matches_parent && [ "$remaining" -gt 0 ]; do',
|
|
183
|
+
' "$sleep_command" 0.025',
|
|
184
|
+
" remaining=$((remaining - 1))",
|
|
185
|
+
" done",
|
|
186
|
+
" if ! matches_parent; then terminate; fi",
|
|
187
|
+
"}",
|
|
188
|
+
"trap continue_or_terminate CONT",
|
|
189
|
+
"trap terminate TERM INT HUP",
|
|
190
|
+
"supervisor_start=$(process_start \"$$\") || terminate",
|
|
191
|
+
'( unset GTKX_PROCESS_GUARD; exec "$setsid_command" "$shell_command" -c "$watch_script" "$watch_name" "$runtime" ' +
|
|
192
|
+
'"$identity" "$$" "$supervisor_start" "$expected_parent" "$expected_parent_start" "$stat_command" ' +
|
|
193
|
+
'"$rm_command" "$sleep_command" ) </dev/null >/dev/null 2>&1 &',
|
|
194
|
+
"child=",
|
|
82
195
|
"if ! matches_parent; then terminate; fi",
|
|
83
196
|
'"$@" &',
|
|
84
197
|
"child=$!",
|
|
85
|
-
|
|
86
|
-
|
|
198
|
+
"while :; do",
|
|
199
|
+
' wait "$child"',
|
|
200
|
+
" status=$?",
|
|
201
|
+
' if kill -0 "$child" 2>/dev/null; then continue; fi',
|
|
202
|
+
' exit "$status"',
|
|
203
|
+
"done",
|
|
87
204
|
].join("\n");
|
|
88
205
|
|
|
89
|
-
const
|
|
206
|
+
const processIdentity = (pid: number): ProcessIdentity => {
|
|
90
207
|
const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
|
|
91
|
-
const
|
|
208
|
+
const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ", 20);
|
|
209
|
+
const state = fields[0];
|
|
210
|
+
const startTime = fields[19];
|
|
92
211
|
|
|
93
|
-
if (
|
|
212
|
+
if (startTime === undefined || state === undefined || ["Z", "X", "x"].includes(state)) {
|
|
94
213
|
throw new Error(`Failed to identify process ${String(pid)}`);
|
|
95
214
|
}
|
|
96
215
|
|
|
97
|
-
return
|
|
216
|
+
return { pid, startTime };
|
|
98
217
|
};
|
|
99
218
|
|
|
100
219
|
const writeGuardCommand = (child: ChildProcess, operation: "+" | "-", job: GuardJob): void => {
|
|
@@ -110,12 +229,41 @@ const releaseGuard = (child: ChildProcess): void => {
|
|
|
110
229
|
}
|
|
111
230
|
};
|
|
112
231
|
|
|
113
|
-
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
|
+
|
|
114
262
|
if (guard.child) {
|
|
115
263
|
return;
|
|
116
264
|
}
|
|
117
265
|
|
|
118
|
-
const child = spawn(process.execPath,
|
|
266
|
+
const child = spawn(process.execPath, guardArguments(), {
|
|
119
267
|
detached: true,
|
|
120
268
|
stdio: ["pipe", "ignore", "ignore"],
|
|
121
269
|
});
|
|
@@ -147,6 +295,20 @@ const startGuard = (): void => {
|
|
|
147
295
|
releaseGuard(child);
|
|
148
296
|
};
|
|
149
297
|
|
|
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
|
+
|
|
150
312
|
const registerJob = (job: GuardJob): void => {
|
|
151
313
|
guard.jobs.set(job.marker, job);
|
|
152
314
|
|
|
@@ -246,6 +408,8 @@ const spawnWithParentDeathSupervisor = (
|
|
|
246
408
|
const shell = resolveExecutable("sh");
|
|
247
409
|
const stat = resolveExecutable("stat");
|
|
248
410
|
const rm = resolveExecutable("rm");
|
|
411
|
+
const setsid = resolveExecutable("setsid");
|
|
412
|
+
const sleep = resolveExecutable("sleep");
|
|
249
413
|
|
|
250
414
|
return spawnGuarded(
|
|
251
415
|
command,
|
|
@@ -268,9 +432,16 @@ const spawnWithParentDeathSupervisor = (
|
|
|
268
432
|
identity.path,
|
|
269
433
|
`${identity.device}:${identity.inode}:${identity.userId}`,
|
|
270
434
|
String(process.pid),
|
|
271
|
-
|
|
435
|
+
processIdentity(process.pid).startTime,
|
|
272
436
|
stat,
|
|
273
437
|
rm,
|
|
438
|
+
shell,
|
|
439
|
+
setsid,
|
|
440
|
+
sleep,
|
|
441
|
+
SUPERVISOR_WATCH_NAME,
|
|
442
|
+
SUPERVISOR_WATCH_SCRIPT,
|
|
443
|
+
SUPERVISOR_CLEANUP_NAME,
|
|
444
|
+
SUPERVISOR_CLEANUP_SCRIPT,
|
|
274
445
|
executable,
|
|
275
446
|
...args,
|
|
276
447
|
];
|
|
@@ -278,4 +449,4 @@ const spawnWithParentDeathSupervisor = (
|
|
|
278
449
|
);
|
|
279
450
|
};
|
|
280
451
|
|
|
281
|
-
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor };
|
|
452
|
+
export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, watchParentProcess };
|