@gtkx/utils 2.0.0-beta.1 → 2.0.0-beta.3

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.
@@ -1,5 +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
4
  export { resolveExecutable, tryResolveExecutable } from "./resolve-executable.ts";
4
- export { spawnWithParentDeathSignal } from "./spawn-with-parent-death-signal.ts";
5
+ export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, } from "./spawn-with-parent-death-signal.ts";
5
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,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC"}
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,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EACH,0BAA0B,EAC1B,8BAA8B,GACjC,MAAM,qCAAqC,CAAC"}
@@ -1,5 +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
4
  export { resolveExecutable, tryResolveExecutable } from "./resolve-executable.js";
4
- export { spawnWithParentDeathSignal } from "./spawn-with-parent-death-signal.js";
5
+ export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor, } from "./spawn-with-parent-death-signal.js";
5
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,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC","sourcesContent":["export { exitCodeForSignal } from \"./exit-code-for-signal.ts\";\nexport { installGracefulShutdown } from \"./install-graceful-shutdown.ts\";\nexport { resolveExecutable, tryResolveExecutable } from \"./resolve-executable.ts\";\nexport { spawnWithParentDeathSignal } from \"./spawn-with-parent-death-signal.ts\";\n"]}
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,EACH,gBAAgB,EAEhB,oBAAoB,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EACH,0BAA0B,EAC1B,8BAA8B,GACjC,MAAM,qCAAqC,CAAC","sourcesContent":["export { exitCodeForSignal } from \"./exit-code-for-signal.ts\";\nexport { installGracefulShutdown } from \"./install-graceful-shutdown.ts\";\nexport {\n killProcessGroup,\n type ProcessGroupIdentity,\n processGroupIdentity,\n} from \"./kill-process-group.ts\";\nexport { resolveExecutable, tryResolveExecutable } from \"./resolve-executable.ts\";\nexport {\n spawnWithParentDeathSignal,\n spawnWithParentDeathSupervisor,\n} from \"./spawn-with-parent-death-signal.ts\";\n"]}
@@ -1,4 +1,5 @@
1
1
  declare const PROCESS_MARKER = "GTKX_PROCESS_GUARD";
2
- declare function killMarkedProcesses(prefix: string): void;
3
- export { PROCESS_MARKER, killMarkedProcesses };
2
+ declare const killMarkedProcesses: (marker: string) => void;
3
+ declare const killMarkedProcessRun: (runPrefix: string) => void;
4
+ export { PROCESS_MARKER, killMarkedProcessRun, killMarkedProcesses };
4
5
  //# sourceMappingURL=kill-marked-processes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"kill-marked-processes.d.ts","sourceRoot":"","sources":["../../src/process/kill-marked-processes.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,cAAc,uBAAuB,CAAC;AA2B5C,iBAAS,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAYjD;AAED,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,CAAC"}
1
+ {"version":3,"file":"kill-marked-processes.d.ts","sourceRoot":"","sources":["../../src/process/kill-marked-processes.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,cAAc,uBAAuB,CAAC;AAwE5C,QAAA,MAAM,mBAAmB,GAAI,QAAQ,MAAM,KAAG,IAE7C,CAAC;AAEF,QAAA,MAAM,oBAAoB,GAAI,WAAW,MAAM,KAAG,IAIjD,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,CAAC"}
@@ -1,38 +1,69 @@
1
1
  import { readdirSync, readFileSync } from "node:fs";
2
2
  const PROCESS_MARKER = "GTKX_PROCESS_GUARD";
3
3
  const MAX_KILL_PASSES = 8;
4
- const isMarked = (pid, prefix) => {
4
+ const JOB_ID_PATTERN = /^[0-9a-f]{8}$/;
5
+ const processIdentity = (pid) => {
6
+ try {
7
+ const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
8
+ const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ");
9
+ const sessionId = Number(fields[3]);
10
+ const startTime = fields[19];
11
+ return startTime !== undefined && Number.isSafeInteger(sessionId)
12
+ ? { pid, sessionId, startTime }
13
+ : undefined;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ };
19
+ const isMarked = (pid, isMatch) => {
5
20
  try {
6
21
  return readFileSync(`/proc/${String(pid)}/environ`, "utf8")
7
22
  .split("\0")
8
- .some((assignment) => assignment.startsWith(prefix));
23
+ .some((assignment) => isMatch(assignment));
9
24
  }
10
25
  catch {
11
26
  return false;
12
27
  }
13
28
  };
14
- const markedPids = (prefix) => readdirSync("/proc")
29
+ const markedProcesses = (isMatch) => readdirSync("/proc")
15
30
  .map(Number)
16
31
  .filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid)
17
- .filter((pid) => isMarked(pid, prefix));
18
- const killPid = (pid) => {
32
+ .filter((pid) => isMarked(pid, isMatch))
33
+ .map((pid) => processIdentity(pid))
34
+ .filter((identity) => identity !== undefined);
35
+ const killProcess = (identity, isMatch) => {
36
+ if (!isMarked(identity.pid, isMatch)) {
37
+ return;
38
+ }
39
+ const current = processIdentity(identity.pid);
40
+ if (current?.sessionId !== identity.sessionId ||
41
+ current.startTime !== identity.startTime) {
42
+ return;
43
+ }
19
44
  try {
20
- process.kill(pid, "SIGKILL");
45
+ process.kill(identity.pid, "SIGKILL");
21
46
  }
22
47
  catch {
23
48
  return;
24
49
  }
25
50
  };
26
- function killMarkedProcesses(prefix) {
51
+ const killMatchingProcesses = (isMatch) => {
27
52
  for (let pass = 0; pass < MAX_KILL_PASSES; pass += 1) {
28
- const pids = markedPids(prefix);
29
- if (pids.length === 0) {
53
+ const processes = markedProcesses(isMatch);
54
+ if (processes.length === 0) {
30
55
  return;
31
56
  }
32
- for (const pid of pids) {
33
- killPid(pid);
57
+ for (const identity of processes) {
58
+ killProcess(identity, isMatch);
34
59
  }
35
60
  }
36
- }
37
- export { PROCESS_MARKER, killMarkedProcesses };
61
+ };
62
+ const killMarkedProcesses = (marker) => {
63
+ killMatchingProcesses((assignment) => assignment === marker);
64
+ };
65
+ const killMarkedProcessRun = (runPrefix) => {
66
+ killMatchingProcesses((assignment) => assignment.startsWith(runPrefix) && JOB_ID_PATTERN.test(assignment.slice(runPrefix.length)));
67
+ };
68
+ export { PROCESS_MARKER, killMarkedProcessRun, killMarkedProcesses };
38
69
  //# sourceMappingURL=kill-marked-processes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"kill-marked-processes.js","sourceRoot":"","sources":["../../src/process/kill-marked-processes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,cAAc,GAAG,oBAAoB,CAAC;AAC5C,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,MAAM,QAAQ,GAAG,CAAC,GAAW,EAAE,MAAc,EAAW,EAAE;IACtD,IAAI,CAAC;QACD,OAAO,YAAY,CAAC,SAAS,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC;aACtD,KAAK,CAAC,IAAI,CAAC;aACX,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,UAAU,GAAG,CAAC,MAAc,EAAY,EAAE,CAC5C,WAAW,CAAC,OAAO,CAAC;KACf,GAAG,CAAC,MAAM,CAAC;KACX,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;KAC5E,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;AAEhD,MAAM,OAAO,GAAG,CAAC,GAAW,EAAQ,EAAE;IAClC,IAAI,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACL,OAAO;IACX,CAAC;AACL,CAAC,CAAC;AAEF,SAAS,mBAAmB,CAAC,MAAc;IACvC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,eAAe,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;QAEhC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpB,OAAO;QACX,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,OAAO,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;IACL,CAAC;AACL,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,CAAC","sourcesContent":["import { readdirSync, readFileSync } from \"node:fs\";\n\nconst PROCESS_MARKER = \"GTKX_PROCESS_GUARD\";\nconst MAX_KILL_PASSES = 8;\n\nconst isMarked = (pid: number, prefix: string): boolean => {\n try {\n return readFileSync(`/proc/${String(pid)}/environ`, \"utf8\")\n .split(\"\\0\")\n .some((assignment) => assignment.startsWith(prefix));\n } catch {\n return false;\n }\n};\n\nconst markedPids = (prefix: string): number[] =>\n readdirSync(\"/proc\")\n .map(Number)\n .filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid)\n .filter((pid) => isMarked(pid, prefix));\n\nconst killPid = (pid: number): void => {\n try {\n process.kill(pid, \"SIGKILL\");\n } catch {\n return;\n }\n};\n\nfunction killMarkedProcesses(prefix: string): void {\n for (let pass = 0; pass < MAX_KILL_PASSES; pass += 1) {\n const pids = markedPids(prefix);\n\n if (pids.length === 0) {\n return;\n }\n\n for (const pid of pids) {\n killPid(pid);\n }\n }\n}\n\nexport { PROCESS_MARKER, killMarkedProcesses };\n"]}
1
+ {"version":3,"file":"kill-marked-processes.js","sourceRoot":"","sources":["../../src/process/kill-marked-processes.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync, readFileSync } from \"node:fs\";\n\ntype ProcessIdentity = {\n pid: number;\n sessionId: number;\n startTime: string;\n};\n\ntype MarkerMatcher = (assignment: string) => boolean;\n\nconst PROCESS_MARKER = \"GTKX_PROCESS_GUARD\";\nconst MAX_KILL_PASSES = 8;\nconst JOB_ID_PATTERN = /^[0-9a-f]{8}$/;\n\nconst processIdentity = (pid: number): ProcessIdentity | undefined => {\n try {\n const stat = readFileSync(`/proc/${String(pid)}/stat`, \"utf8\");\n const fields = stat.slice(stat.lastIndexOf(\") \") + 2).split(\" \");\n const sessionId = Number(fields[3]);\n const startTime = fields[19];\n\n return startTime !== undefined && Number.isSafeInteger(sessionId)\n ? { pid, sessionId, startTime }\n : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst isMarked = (pid: number, isMatch: MarkerMatcher): boolean => {\n try {\n return readFileSync(`/proc/${String(pid)}/environ`, \"utf8\")\n .split(\"\\0\")\n .some((assignment) => isMatch(assignment));\n } catch {\n return false;\n }\n};\n\nconst markedProcesses = (isMatch: MarkerMatcher): ProcessIdentity[] =>\n readdirSync(\"/proc\")\n .map(Number)\n .filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid)\n .filter((pid) => isMarked(pid, isMatch))\n .map((pid) => processIdentity(pid))\n .filter((identity): identity is ProcessIdentity => identity !== undefined);\n\nconst killProcess = (identity: ProcessIdentity, isMatch: MarkerMatcher): void => {\n if (!isMarked(identity.pid, isMatch)) {\n return;\n }\n\n const current = processIdentity(identity.pid);\n\n if (\n current?.sessionId !== identity.sessionId ||\n current.startTime !== identity.startTime\n ) {\n return;\n }\n\n try {\n process.kill(identity.pid, \"SIGKILL\");\n } catch {\n return;\n }\n};\n\nconst killMatchingProcesses = (isMatch: MarkerMatcher): void => {\n for (let pass = 0; pass < MAX_KILL_PASSES; pass += 1) {\n const processes = markedProcesses(isMatch);\n\n if (processes.length === 0) {\n return;\n }\n\n for (const identity of processes) {\n killProcess(identity, isMatch);\n }\n }\n};\n\nconst killMarkedProcesses = (marker: string): void => {\n killMatchingProcesses((assignment) => assignment === marker);\n};\n\nconst killMarkedProcessRun = (runPrefix: string): void => {\n killMatchingProcesses((assignment) =>\n assignment.startsWith(runPrefix) && JOB_ID_PATTERN.test(assignment.slice(runPrefix.length)),\n );\n};\n\nexport { PROCESS_MARKER, killMarkedProcessRun, killMarkedProcesses };\n"]}
@@ -0,0 +1,16 @@
1
+ type ProcessGroupIdentity = {
2
+ processGroupId: number;
3
+ leaderStartTime: string;
4
+ };
5
+ type CleanupDirectoryIdentity = {
6
+ path: string;
7
+ device: string;
8
+ inode: string;
9
+ userId: string;
10
+ };
11
+ declare const processGroupIdentity: (processGroupId: number) => ProcessGroupIdentity | undefined;
12
+ declare const killProcessGroup: (identity: ProcessGroupIdentity, signal?: NodeJS.Signals) => void;
13
+ declare const cleanupDirectoryIdentity: (path: string) => CleanupDirectoryIdentity | undefined;
14
+ declare const removeCleanupDirectory: (identity: CleanupDirectoryIdentity) => void;
15
+ export { type CleanupDirectoryIdentity, cleanupDirectoryIdentity, killProcessGroup, type ProcessGroupIdentity, processGroupIdentity, removeCleanupDirectory, };
16
+ //# sourceMappingURL=kill-process-group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kill-process-group.d.ts","sourceRoot":"","sources":["../../src/process/kill-process-group.ts"],"names":[],"mappings":"AAGA,KAAK,oBAAoB,GAAG;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAClB,CAAC;AAIF,QAAA,MAAM,oBAAoB,GAAI,gBAAgB,MAAM,KAAG,oBAAoB,GAAG,SAkB7E,CAAC;AAQF,QAAA,MAAM,gBAAgB,GAAI,UAAU,oBAAoB,EAAE,SAAQ,MAAM,CAAC,OAAmB,KAAG,IAU9F,CAAC;AAEF,QAAA,MAAM,wBAAwB,GAAI,MAAM,MAAM,KAAG,wBAAwB,GAAG,SAqB3E,CAAC;AAEF,QAAA,MAAM,sBAAsB,GAAI,UAAU,wBAAwB,KAAG,IAgBpE,CAAC;AAEF,OAAO,EACH,KAAK,wBAAwB,EAC7B,wBAAwB,EACxB,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,oBAAoB,EACpB,sBAAsB,GACzB,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { lstatSync, readFileSync, rmSync } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ const isProcessGroupId = (value) => Number.isSafeInteger(value) && value > 1;
4
+ const processGroupIdentity = (processGroupId) => {
5
+ if (!isProcessGroupId(processGroupId)) {
6
+ return undefined;
7
+ }
8
+ try {
9
+ const stat = readFileSync(`/proc/${String(processGroupId)}/stat`, "utf8");
10
+ const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ");
11
+ const actualProcessGroupId = Number(fields[2]);
12
+ const sessionId = Number(fields[3]);
13
+ const leaderStartTime = fields[19];
14
+ return actualProcessGroupId === processGroupId && sessionId === processGroupId && leaderStartTime !== undefined
15
+ ? { processGroupId, leaderStartTime }
16
+ : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ };
22
+ const isCurrentProcessGroup = (identity) => {
23
+ const current = processGroupIdentity(identity.processGroupId);
24
+ return current?.leaderStartTime === identity.leaderStartTime;
25
+ };
26
+ const killProcessGroup = (identity, signal = "SIGKILL") => {
27
+ if (!isCurrentProcessGroup(identity)) {
28
+ return;
29
+ }
30
+ try {
31
+ process.kill(-identity.processGroupId, signal);
32
+ }
33
+ catch {
34
+ return;
35
+ }
36
+ };
37
+ const cleanupDirectoryIdentity = (path) => {
38
+ const absolutePath = resolve(path);
39
+ if (absolutePath === "/" || !isAbsolute(path)) {
40
+ return undefined;
41
+ }
42
+ try {
43
+ const entry = lstatSync(absolutePath, { bigint: true });
44
+ return entry.isDirectory()
45
+ ? {
46
+ path: absolutePath,
47
+ device: entry.dev.toString(),
48
+ inode: entry.ino.toString(),
49
+ userId: entry.uid.toString(),
50
+ }
51
+ : undefined;
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ };
57
+ const removeCleanupDirectory = (identity) => {
58
+ const current = cleanupDirectoryIdentity(identity.path);
59
+ if (current?.device !== identity.device ||
60
+ current.inode !== identity.inode ||
61
+ current.userId !== identity.userId) {
62
+ return;
63
+ }
64
+ try {
65
+ rmSync(identity.path, { recursive: true, force: true });
66
+ }
67
+ catch {
68
+ return;
69
+ }
70
+ };
71
+ export { cleanupDirectoryIdentity, killProcessGroup, processGroupIdentity, removeCleanupDirectory, };
72
+ //# sourceMappingURL=kill-process-group.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kill-process-group.js","sourceRoot":"","sources":["../../src/process/kill-process-group.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstatSync, readFileSync, rmSync } from \"node:fs\";\nimport { isAbsolute, resolve } from \"node:path\";\n\ntype ProcessGroupIdentity = {\n processGroupId: number;\n leaderStartTime: string;\n};\n\ntype CleanupDirectoryIdentity = {\n path: string;\n device: string;\n inode: string;\n userId: string;\n};\n\nconst isProcessGroupId = (value: number): boolean => Number.isSafeInteger(value) && value > 1;\n\nconst processGroupIdentity = (processGroupId: number): ProcessGroupIdentity | undefined => {\n if (!isProcessGroupId(processGroupId)) {\n return undefined;\n }\n\n try {\n const stat = readFileSync(`/proc/${String(processGroupId)}/stat`, \"utf8\");\n const fields = stat.slice(stat.lastIndexOf(\") \") + 2).split(\" \");\n const actualProcessGroupId = Number(fields[2]);\n const sessionId = Number(fields[3]);\n const leaderStartTime = fields[19];\n\n return actualProcessGroupId === processGroupId && sessionId === processGroupId && leaderStartTime !== undefined\n ? { processGroupId, leaderStartTime }\n : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst isCurrentProcessGroup = (identity: ProcessGroupIdentity): boolean => {\n const current = processGroupIdentity(identity.processGroupId);\n\n return current?.leaderStartTime === identity.leaderStartTime;\n};\n\nconst killProcessGroup = (identity: ProcessGroupIdentity, signal: NodeJS.Signals = \"SIGKILL\"): void => {\n if (!isCurrentProcessGroup(identity)) {\n return;\n }\n\n try {\n process.kill(-identity.processGroupId, signal);\n } catch {\n return;\n }\n};\n\nconst cleanupDirectoryIdentity = (path: string): CleanupDirectoryIdentity | undefined => {\n const absolutePath = resolve(path);\n\n if (absolutePath === \"/\" || !isAbsolute(path)) {\n return undefined;\n }\n\n try {\n const entry = lstatSync(absolutePath, { bigint: true });\n\n return entry.isDirectory()\n ? {\n path: absolutePath,\n device: entry.dev.toString(),\n inode: entry.ino.toString(),\n userId: entry.uid.toString(),\n }\n : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst removeCleanupDirectory = (identity: CleanupDirectoryIdentity): void => {\n const current = cleanupDirectoryIdentity(identity.path);\n\n if (\n current?.device !== identity.device ||\n current.inode !== identity.inode ||\n current.userId !== identity.userId\n ) {\n return;\n }\n\n try {\n rmSync(identity.path, { recursive: true, force: true });\n } catch {\n return;\n }\n};\n\nexport {\n type CleanupDirectoryIdentity,\n cleanupDirectoryIdentity,\n killProcessGroup,\n type ProcessGroupIdentity,\n processGroupIdentity,\n removeCleanupDirectory,\n};\n"]}
@@ -1,13 +1,91 @@
1
- import { killMarkedProcesses } from "./kill-marked-processes.js";
1
+ import { killMarkedProcesses, killMarkedProcessRun } from "./kill-marked-processes.js";
2
+ import { killProcessGroup, removeCleanupDirectory, } from "./kill-process-group.js";
2
3
  const GUARD_PREFIX = process.argv[2] ?? "";
3
4
  const WATCHED_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
5
+ const state = {
6
+ bufferedCommands: "",
7
+ isSweeping: false,
8
+ jobs: new Map(),
9
+ };
10
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11
+ const isProcessGroupIdentity = (value) => isRecord(value) &&
12
+ typeof value.processGroupId === "number" &&
13
+ Number.isSafeInteger(value.processGroupId) &&
14
+ value.processGroupId > 1 &&
15
+ typeof value.leaderStartTime === "string" &&
16
+ /^\d+$/.test(value.leaderStartTime);
17
+ const isCleanupDirectoryIdentity = (value) => isRecord(value) &&
18
+ typeof value.path === "string" &&
19
+ typeof value.device === "string" &&
20
+ typeof value.inode === "string" &&
21
+ typeof value.userId === "string";
22
+ const isGuardJob = (value) => isRecord(value) &&
23
+ typeof value.marker === "string" &&
24
+ isProcessGroupIdentity(value.processGroup) &&
25
+ Array.isArray(value.cleanupDirectories) &&
26
+ value.cleanupDirectories.every(isCleanupDirectoryIdentity) &&
27
+ (value.signal === "SIGKILL" || value.signal === "SIGCONT");
28
+ const applyCommand = (command) => {
29
+ const operation = command[0];
30
+ let value;
31
+ try {
32
+ value = JSON.parse(command.slice(1));
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ if (!isGuardJob(value)) {
38
+ return;
39
+ }
40
+ if (operation === "+") {
41
+ state.jobs.set(value.marker, value);
42
+ }
43
+ else if (operation === "-") {
44
+ state.jobs.delete(value.marker);
45
+ }
46
+ };
47
+ const receiveCommands = (chunk) => {
48
+ state.bufferedCommands += chunk.toString();
49
+ const commands = state.bufferedCommands.split("\n");
50
+ state.bufferedCommands = commands.pop() ?? "";
51
+ for (const command of commands) {
52
+ applyCommand(command);
53
+ }
54
+ };
55
+ const killJobs = () => {
56
+ for (const job of state.jobs.values()) {
57
+ killProcessGroup(job.processGroup, job.signal);
58
+ if (job.signal === "SIGKILL") {
59
+ killMarkedProcesses(job.marker);
60
+ }
61
+ }
62
+ };
63
+ const removeCleanupDirectories = () => {
64
+ const cleanupDirectories = new Map();
65
+ for (const job of state.jobs.values()) {
66
+ for (const identity of job.cleanupDirectories) {
67
+ cleanupDirectories.set(`${identity.device}:${identity.inode}`, identity);
68
+ }
69
+ }
70
+ for (const identity of cleanupDirectories.values()) {
71
+ removeCleanupDirectory(identity);
72
+ }
73
+ };
4
74
  const sweep = () => {
75
+ if (state.isSweeping) {
76
+ return;
77
+ }
78
+ state.isSweeping = true;
79
+ applyCommand(state.bufferedCommands);
80
+ killJobs();
81
+ removeCleanupDirectories();
5
82
  if (GUARD_PREFIX.length > 0) {
6
- killMarkedProcesses(GUARD_PREFIX);
83
+ killMarkedProcessRun(GUARD_PREFIX);
7
84
  }
8
85
  process.exit(0);
9
86
  };
10
87
  process.stdin.resume();
88
+ process.stdin.on("data", receiveCommands);
11
89
  process.stdin.on("end", sweep);
12
90
  process.stdin.on("error", sweep);
13
91
  for (const signal of WATCHED_SIGNALS) {
@@ -1 +1 @@
1
- {"version":3,"file":"process-guard.js","sourceRoot":"","sources":["../../src/process/process-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3C,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAqC,CAAC;AAE5F,MAAM,KAAK,GAAG,GAAS,EAAE;IACrB,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,mBAAmB,CAAC,YAAY,CAAC,CAAC;IACtC,CAAC;IAED,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,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 } from \"./kill-marked-processes.ts\";\n\nconst GUARD_PREFIX = process.argv[2] ?? \"\";\nconst WATCHED_SIGNALS = [\"SIGTERM\", \"SIGINT\", \"SIGHUP\"] as const satisfies NodeJS.Signals[];\n\nconst sweep = (): void => {\n if (GUARD_PREFIX.length > 0) {\n killMarkedProcesses(GUARD_PREFIX);\n }\n\n process.exit(0);\n};\n\nprocess.stdin.resume();\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 { 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"]}
@@ -2,7 +2,12 @@ import { type ChildProcess, type StdioOptions } from "node:child_process";
2
2
  type ParentDeathSpawnOptions = {
3
3
  stdio?: StdioOptions;
4
4
  env?: NodeJS.ProcessEnv;
5
+ cleanupDirectories?: string[];
6
+ };
7
+ type ParentDeathSupervisorOptions = Omit<ParentDeathSpawnOptions, "cleanupDirectories"> & {
8
+ cleanupDirectory: string;
5
9
  };
6
10
  declare function spawnWithParentDeathSignal(command: string, args: string[], options?: ParentDeathSpawnOptions): ChildProcess;
7
- export { spawnWithParentDeathSignal };
11
+ declare const spawnWithParentDeathSupervisor: (command: string, args: string[], options: ParentDeathSupervisorOptions) => ChildProcess;
12
+ export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor };
8
13
  //# 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;AAQjF,KAAK,uBAAuB,GAAG;IAC3B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAC3B,CAAC;AAuCF,iBAAS,0BAA0B,CAC/B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,uBAA4B,GACtC,YAAY,CAiBd;AAED,OAAO,EAAE,0BAA0B,EAAE,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;AA0MF,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,YAkCF,CAAC;AAEF,OAAO,EAAE,0BAA0B,EAAE,8BAA8B,EAAE,CAAC"}
@@ -1,15 +1,60 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomBytes } from "node:crypto";
3
+ import { 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";
6
7
  import { killMarkedProcesses, PROCESS_MARKER } from "./kill-marked-processes.js";
8
+ import { cleanupDirectoryIdentity, killProcessGroup, processGroupIdentity, removeCleanupDirectory, } from "./kill-process-group.js";
7
9
  import { resolveExecutable } from "./resolve-executable.js";
8
10
  const MODULE_PATH = fileURLToPath(import.meta.url);
9
11
  const GUARD_PATH = join(dirname(MODULE_PATH), `process-guard${extname(MODULE_PATH)}`);
10
12
  const RUN_ID = randomBytes(8).toString("hex");
11
13
  const RUN_PREFIX = `${PROCESS_MARKER}=${RUN_ID}/`;
12
- const guard = {};
14
+ const guard = { jobs: new Map() };
15
+ const SUPERVISOR_NAME = "gtkx-process-supervisor";
16
+ const SUPERVISOR_SCRIPT = [
17
+ "runtime=$1",
18
+ "identity=$2",
19
+ "expected_parent=$3",
20
+ "expected_parent_start=$4",
21
+ "stat_command=$5",
22
+ "rm_command=$6",
23
+ "shift 6",
24
+ "matches_directory() {",
25
+ ' [ "$("$stat_command" -c "%d:%i:%u" -- "$runtime" 2>/dev/null)" = "$identity" ]',
26
+ "}",
27
+ "matches_parent() {",
28
+ " parent_stat=",
29
+ ' IFS= read -r parent_stat < "/proc/$expected_parent/stat" || return 1',
30
+ " parent_tail=${parent_stat##*) }",
31
+ " set -- $parent_tail",
32
+ ' [ "${20}" = "$expected_parent_start" ]',
33
+ "}",
34
+ "terminate() {",
35
+ ' trap "" CONT TERM INT HUP',
36
+ ' if matches_directory; then "$rm_command" -rf -- "$runtime"; fi',
37
+ ' kill -KILL "-$$" 2>/dev/null',
38
+ " exit 1",
39
+ "}",
40
+ "trap terminate CONT TERM INT HUP",
41
+ "if ! matches_parent; then terminate; fi",
42
+ '"$@" &',
43
+ "child=$!",
44
+ 'wait "$child"',
45
+ "exit $?",
46
+ ].join("\n");
47
+ const processStartTime = (pid) => {
48
+ const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
49
+ const start = stat.slice(stat.lastIndexOf(") ") + 2).split(" ", 20)[19];
50
+ if (start === undefined) {
51
+ throw new Error(`Failed to identify process ${String(pid)}`);
52
+ }
53
+ return start;
54
+ };
55
+ const writeGuardCommand = (child, operation, job) => {
56
+ child.stdin?.write(`${operation}${JSON.stringify(job)}\n`);
57
+ };
13
58
  const releaseGuard = (child) => {
14
59
  child.unref();
15
60
  const { stdin } = child;
@@ -26,25 +71,112 @@ const startGuard = () => {
26
71
  stdio: ["pipe", "ignore", "ignore"],
27
72
  });
28
73
  child.on("error", () => {
29
- guard.child = undefined;
74
+ if (guard.child === child) {
75
+ guard.child = undefined;
76
+ }
77
+ });
78
+ child.on("exit", () => {
79
+ if (guard.child === child) {
80
+ guard.child = undefined;
81
+ }
82
+ });
83
+ child.stdin.on("error", () => {
84
+ if (guard.child === child) {
85
+ guard.child = undefined;
86
+ }
30
87
  });
31
88
  guard.child = child;
89
+ for (const job of guard.jobs.values()) {
90
+ writeGuardCommand(child, "+", job);
91
+ }
32
92
  releaseGuard(child);
33
93
  };
34
- function spawnWithParentDeathSignal(command, args, options = {}) {
94
+ const registerJob = (job) => {
95
+ guard.jobs.set(job.marker, job);
96
+ if (guard.child) {
97
+ writeGuardCommand(guard.child, "+", job);
98
+ }
99
+ };
100
+ const unregisterJob = (job) => {
101
+ guard.jobs.delete(job.marker);
102
+ if (guard.child) {
103
+ writeGuardCommand(guard.child, "-", job);
104
+ }
105
+ };
106
+ const rollbackSpawn = ({ child, marker, processGroup, cleanupDirectories = [] }) => {
107
+ if (processGroup === undefined) {
108
+ child.kill("SIGKILL");
109
+ }
110
+ else {
111
+ killProcessGroup(processGroup);
112
+ }
113
+ killMarkedProcesses(marker);
114
+ for (const identity of cleanupDirectories) {
115
+ removeCleanupDirectory(identity);
116
+ }
117
+ };
118
+ const captureCleanupDirectories = (command, paths) => {
119
+ const identities = paths.map((path) => cleanupDirectoryIdentity(path));
120
+ if (identities.includes(undefined)) {
121
+ throw new Error(`Failed to identify a cleanup directory for ${command}`);
122
+ }
123
+ return identities.filter((identity) => identity !== undefined);
124
+ };
125
+ const spawnGuarded = (command, options, signal, launchArguments) => {
35
126
  const executable = resolveExecutable(command);
36
127
  const setpriv = resolveExecutable("setpriv");
37
128
  const jobValue = `${RUN_ID}/${randomBytes(4).toString("hex")}`;
129
+ const marker = `${PROCESS_MARKER}=${jobValue}`;
130
+ const cleanupDirectories = captureCleanupDirectories(command, options.cleanupDirectories ?? []);
38
131
  startGuard();
39
- const child = spawn(setpriv, ["--pdeathsig", "SIGKILL", executable, ...args], {
132
+ const child = spawn(setpriv, launchArguments(executable, cleanupDirectories), {
40
133
  detached: true,
41
134
  stdio: options.stdio ?? "ignore",
42
135
  env: { ...(options.env ?? process.env), [PROCESS_MARKER]: jobValue },
43
136
  });
137
+ const processGroupId = child.pid;
138
+ const group = processGroupId === undefined ? undefined : processGroupIdentity(processGroupId);
139
+ if (group === undefined) {
140
+ rollbackSpawn({ child, marker, cleanupDirectories });
141
+ throw new Error(`Failed to identify process group for ${command}`);
142
+ }
143
+ const job = { marker, processGroup: group, cleanupDirectories, signal };
144
+ registerJob(job);
44
145
  child.on("exit", () => {
45
- killMarkedProcesses(`${PROCESS_MARKER}=${jobValue}`);
146
+ killMarkedProcesses(job.marker);
147
+ unregisterJob(job);
46
148
  });
47
149
  return child;
150
+ };
151
+ function spawnWithParentDeathSignal(command, args, options = {}) {
152
+ return spawnGuarded(command, options, "SIGKILL", (executable) => ["--pdeathsig", "SIGKILL", executable, ...args]);
48
153
  }
49
- export { spawnWithParentDeathSignal };
154
+ const spawnWithParentDeathSupervisor = (command, args, options) => {
155
+ const shell = resolveExecutable("sh");
156
+ const stat = resolveExecutable("stat");
157
+ const rm = resolveExecutable("rm");
158
+ return spawnGuarded(command, { ...options, cleanupDirectories: [options.cleanupDirectory] }, "SIGCONT", (executable, cleanupDirectories) => {
159
+ const identity = cleanupDirectories[0];
160
+ if (identity === undefined) {
161
+ throw new Error(`Failed to identify a cleanup directory for ${command}`);
162
+ }
163
+ return [
164
+ "--pdeathsig",
165
+ "SIGCONT",
166
+ shell,
167
+ "-c",
168
+ SUPERVISOR_SCRIPT,
169
+ SUPERVISOR_NAME,
170
+ identity.path,
171
+ `${identity.device}:${identity.inode}:${identity.userId}`,
172
+ String(process.pid),
173
+ processStartTime(process.pid),
174
+ stat,
175
+ rm,
176
+ executable,
177
+ ...args,
178
+ ];
179
+ });
180
+ };
181
+ export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor };
50
182
  //# 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 { 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 { resolveExecutable } from \"./resolve-executable.ts\";\n\ntype ParentDeathSpawnOptions = {\n stdio?: StdioOptions;\n env?: NodeJS.ProcessEnv;\n};\n\ntype GuardState = {\n child?: ChildProcess | undefined;\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 = {};\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 guard.child = undefined;\n });\n\n guard.child = child;\n releaseGuard(child);\n};\n\nfunction spawnWithParentDeathSignal(\n command: string,\n args: string[],\n options: ParentDeathSpawnOptions = {},\n): ChildProcess {\n const executable = resolveExecutable(command);\n const setpriv = resolveExecutable(\"setpriv\");\n const jobValue = `${RUN_ID}/${randomBytes(4).toString(\"hex\")}`;\n startGuard();\n\n const child = spawn(setpriv, [\"--pdeathsig\", \"SIGKILL\", executable, ...args], {\n detached: true,\n stdio: options.stdio ?? \"ignore\",\n env: { ...(options.env ?? process.env), [PROCESS_MARKER]: jobValue },\n });\n\n child.on(\"exit\", () => {\n killMarkedProcesses(`${PROCESS_MARKER}=${jobValue}`);\n });\n\n return child;\n}\n\nexport { spawnWithParentDeathSignal };\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 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/utils",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.3",
4
4
  "description": "Shared utilities for the GTK toolchain: logging, errors, strings.",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -1,4 +1,12 @@
1
1
  export { exitCodeForSignal } from "./exit-code-for-signal.ts";
2
2
  export { installGracefulShutdown } from "./install-graceful-shutdown.ts";
3
+ export {
4
+ killProcessGroup,
5
+ type ProcessGroupIdentity,
6
+ processGroupIdentity,
7
+ } from "./kill-process-group.ts";
3
8
  export { resolveExecutable, tryResolveExecutable } from "./resolve-executable.ts";
4
- export { spawnWithParentDeathSignal } from "./spawn-with-parent-death-signal.ts";
9
+ export {
10
+ spawnWithParentDeathSignal,
11
+ spawnWithParentDeathSupervisor,
12
+ } from "./spawn-with-parent-death-signal.ts";
@@ -1,44 +1,93 @@
1
1
  import { readdirSync, readFileSync } from "node:fs";
2
2
 
3
+ type ProcessIdentity = {
4
+ pid: number;
5
+ sessionId: number;
6
+ startTime: string;
7
+ };
8
+
9
+ type MarkerMatcher = (assignment: string) => boolean;
10
+
3
11
  const PROCESS_MARKER = "GTKX_PROCESS_GUARD";
4
12
  const MAX_KILL_PASSES = 8;
13
+ const JOB_ID_PATTERN = /^[0-9a-f]{8}$/;
5
14
 
6
- const isMarked = (pid: number, prefix: string): boolean => {
15
+ const processIdentity = (pid: number): ProcessIdentity | undefined => {
16
+ try {
17
+ const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
18
+ const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ");
19
+ const sessionId = Number(fields[3]);
20
+ const startTime = fields[19];
21
+
22
+ return startTime !== undefined && Number.isSafeInteger(sessionId)
23
+ ? { pid, sessionId, startTime }
24
+ : undefined;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ };
29
+
30
+ const isMarked = (pid: number, isMatch: MarkerMatcher): boolean => {
7
31
  try {
8
32
  return readFileSync(`/proc/${String(pid)}/environ`, "utf8")
9
33
  .split("\0")
10
- .some((assignment) => assignment.startsWith(prefix));
34
+ .some((assignment) => isMatch(assignment));
11
35
  } catch {
12
36
  return false;
13
37
  }
14
38
  };
15
39
 
16
- const markedPids = (prefix: string): number[] =>
40
+ const markedProcesses = (isMatch: MarkerMatcher): ProcessIdentity[] =>
17
41
  readdirSync("/proc")
18
42
  .map(Number)
19
43
  .filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid)
20
- .filter((pid) => isMarked(pid, prefix));
44
+ .filter((pid) => isMarked(pid, isMatch))
45
+ .map((pid) => processIdentity(pid))
46
+ .filter((identity): identity is ProcessIdentity => identity !== undefined);
47
+
48
+ const killProcess = (identity: ProcessIdentity, isMatch: MarkerMatcher): void => {
49
+ if (!isMarked(identity.pid, isMatch)) {
50
+ return;
51
+ }
52
+
53
+ const current = processIdentity(identity.pid);
54
+
55
+ if (
56
+ current?.sessionId !== identity.sessionId ||
57
+ current.startTime !== identity.startTime
58
+ ) {
59
+ return;
60
+ }
21
61
 
22
- const killPid = (pid: number): void => {
23
62
  try {
24
- process.kill(pid, "SIGKILL");
63
+ process.kill(identity.pid, "SIGKILL");
25
64
  } catch {
26
65
  return;
27
66
  }
28
67
  };
29
68
 
30
- function killMarkedProcesses(prefix: string): void {
69
+ const killMatchingProcesses = (isMatch: MarkerMatcher): void => {
31
70
  for (let pass = 0; pass < MAX_KILL_PASSES; pass += 1) {
32
- const pids = markedPids(prefix);
71
+ const processes = markedProcesses(isMatch);
33
72
 
34
- if (pids.length === 0) {
73
+ if (processes.length === 0) {
35
74
  return;
36
75
  }
37
76
 
38
- for (const pid of pids) {
39
- killPid(pid);
77
+ for (const identity of processes) {
78
+ killProcess(identity, isMatch);
40
79
  }
41
80
  }
42
- }
81
+ };
82
+
83
+ const killMarkedProcesses = (marker: string): void => {
84
+ killMatchingProcesses((assignment) => assignment === marker);
85
+ };
86
+
87
+ const killMarkedProcessRun = (runPrefix: string): void => {
88
+ killMatchingProcesses((assignment) =>
89
+ assignment.startsWith(runPrefix) && JOB_ID_PATTERN.test(assignment.slice(runPrefix.length)),
90
+ );
91
+ };
43
92
 
44
- export { PROCESS_MARKER, killMarkedProcesses };
93
+ export { PROCESS_MARKER, killMarkedProcessRun, killMarkedProcesses };
@@ -0,0 +1,104 @@
1
+ import { lstatSync, readFileSync, rmSync } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+
4
+ type ProcessGroupIdentity = {
5
+ processGroupId: number;
6
+ leaderStartTime: string;
7
+ };
8
+
9
+ type CleanupDirectoryIdentity = {
10
+ path: string;
11
+ device: string;
12
+ inode: string;
13
+ userId: string;
14
+ };
15
+
16
+ const isProcessGroupId = (value: number): boolean => Number.isSafeInteger(value) && value > 1;
17
+
18
+ const processGroupIdentity = (processGroupId: number): ProcessGroupIdentity | undefined => {
19
+ if (!isProcessGroupId(processGroupId)) {
20
+ return undefined;
21
+ }
22
+
23
+ try {
24
+ const stat = readFileSync(`/proc/${String(processGroupId)}/stat`, "utf8");
25
+ const fields = stat.slice(stat.lastIndexOf(") ") + 2).split(" ");
26
+ const actualProcessGroupId = Number(fields[2]);
27
+ const sessionId = Number(fields[3]);
28
+ const leaderStartTime = fields[19];
29
+
30
+ return actualProcessGroupId === processGroupId && sessionId === processGroupId && leaderStartTime !== undefined
31
+ ? { processGroupId, leaderStartTime }
32
+ : undefined;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ };
37
+
38
+ const isCurrentProcessGroup = (identity: ProcessGroupIdentity): boolean => {
39
+ const current = processGroupIdentity(identity.processGroupId);
40
+
41
+ return current?.leaderStartTime === identity.leaderStartTime;
42
+ };
43
+
44
+ const killProcessGroup = (identity: ProcessGroupIdentity, signal: NodeJS.Signals = "SIGKILL"): void => {
45
+ if (!isCurrentProcessGroup(identity)) {
46
+ return;
47
+ }
48
+
49
+ try {
50
+ process.kill(-identity.processGroupId, signal);
51
+ } catch {
52
+ return;
53
+ }
54
+ };
55
+
56
+ const cleanupDirectoryIdentity = (path: string): CleanupDirectoryIdentity | undefined => {
57
+ const absolutePath = resolve(path);
58
+
59
+ if (absolutePath === "/" || !isAbsolute(path)) {
60
+ return undefined;
61
+ }
62
+
63
+ try {
64
+ const entry = lstatSync(absolutePath, { bigint: true });
65
+
66
+ return entry.isDirectory()
67
+ ? {
68
+ path: absolutePath,
69
+ device: entry.dev.toString(),
70
+ inode: entry.ino.toString(),
71
+ userId: entry.uid.toString(),
72
+ }
73
+ : undefined;
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ };
78
+
79
+ const removeCleanupDirectory = (identity: CleanupDirectoryIdentity): void => {
80
+ const current = cleanupDirectoryIdentity(identity.path);
81
+
82
+ if (
83
+ current?.device !== identity.device ||
84
+ current.inode !== identity.inode ||
85
+ current.userId !== identity.userId
86
+ ) {
87
+ return;
88
+ }
89
+
90
+ try {
91
+ rmSync(identity.path, { recursive: true, force: true });
92
+ } catch {
93
+ return;
94
+ }
95
+ };
96
+
97
+ export {
98
+ type CleanupDirectoryIdentity,
99
+ cleanupDirectoryIdentity,
100
+ killProcessGroup,
101
+ type ProcessGroupIdentity,
102
+ processGroupIdentity,
103
+ removeCleanupDirectory,
104
+ };
@@ -1,17 +1,125 @@
1
- import { killMarkedProcesses } from "./kill-marked-processes.ts";
1
+ import { killMarkedProcesses, killMarkedProcessRun } from "./kill-marked-processes.ts";
2
+ import {
3
+ type CleanupDirectoryIdentity,
4
+ killProcessGroup,
5
+ type ProcessGroupIdentity,
6
+ removeCleanupDirectory,
7
+ } from "./kill-process-group.ts";
2
8
 
3
9
  const GUARD_PREFIX = process.argv[2] ?? "";
4
10
  const WATCHED_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"] as const satisfies NodeJS.Signals[];
11
+ type GuardJob = {
12
+ marker: string;
13
+ processGroup: ProcessGroupIdentity;
14
+ cleanupDirectories: CleanupDirectoryIdentity[];
15
+ signal: NodeJS.Signals;
16
+ };
17
+
18
+ const state: { bufferedCommands: string; isSweeping: boolean; jobs: Map<string, GuardJob> } = {
19
+ bufferedCommands: "",
20
+ isSweeping: false,
21
+ jobs: new Map(),
22
+ };
23
+
24
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
25
+ typeof value === "object" && value !== null && !Array.isArray(value);
26
+
27
+ const isProcessGroupIdentity = (value: unknown): value is ProcessGroupIdentity =>
28
+ isRecord(value) &&
29
+ typeof value.processGroupId === "number" &&
30
+ Number.isSafeInteger(value.processGroupId) &&
31
+ value.processGroupId > 1 &&
32
+ typeof value.leaderStartTime === "string" &&
33
+ /^\d+$/.test(value.leaderStartTime);
34
+
35
+ const isCleanupDirectoryIdentity = (value: unknown): value is CleanupDirectoryIdentity =>
36
+ isRecord(value) &&
37
+ typeof value.path === "string" &&
38
+ typeof value.device === "string" &&
39
+ typeof value.inode === "string" &&
40
+ typeof value.userId === "string";
41
+
42
+ const isGuardJob = (value: unknown): value is GuardJob =>
43
+ isRecord(value) &&
44
+ typeof value.marker === "string" &&
45
+ isProcessGroupIdentity(value.processGroup) &&
46
+ Array.isArray(value.cleanupDirectories) &&
47
+ value.cleanupDirectories.every(isCleanupDirectoryIdentity) &&
48
+ (value.signal === "SIGKILL" || value.signal === "SIGCONT");
49
+
50
+ const applyCommand = (command: string): void => {
51
+ const operation = command[0];
52
+ let value: unknown;
53
+
54
+ try {
55
+ value = JSON.parse(command.slice(1));
56
+ } catch {
57
+ return;
58
+ }
59
+
60
+ if (!isGuardJob(value)) {
61
+ return;
62
+ }
63
+
64
+ if (operation === "+") {
65
+ state.jobs.set(value.marker, value);
66
+ } else if (operation === "-") {
67
+ state.jobs.delete(value.marker);
68
+ }
69
+ };
70
+
71
+ const receiveCommands = (chunk: Buffer | string): void => {
72
+ state.bufferedCommands += chunk.toString();
73
+ const commands = state.bufferedCommands.split("\n");
74
+ state.bufferedCommands = commands.pop() ?? "";
75
+
76
+ for (const command of commands) {
77
+ applyCommand(command);
78
+ }
79
+ };
80
+
81
+ const killJobs = (): void => {
82
+ for (const job of state.jobs.values()) {
83
+ killProcessGroup(job.processGroup, job.signal);
84
+
85
+ if (job.signal === "SIGKILL") {
86
+ killMarkedProcesses(job.marker);
87
+ }
88
+ }
89
+ };
90
+
91
+ const removeCleanupDirectories = (): void => {
92
+ const cleanupDirectories: Map<string, CleanupDirectoryIdentity> = new Map();
93
+
94
+ for (const job of state.jobs.values()) {
95
+ for (const identity of job.cleanupDirectories) {
96
+ cleanupDirectories.set(`${identity.device}:${identity.inode}`, identity);
97
+ }
98
+ }
99
+
100
+ for (const identity of cleanupDirectories.values()) {
101
+ removeCleanupDirectory(identity);
102
+ }
103
+ };
5
104
 
6
105
  const sweep = (): void => {
7
- if (GUARD_PREFIX.length > 0) {
8
- killMarkedProcesses(GUARD_PREFIX);
106
+ if (state.isSweeping) {
107
+ return;
9
108
  }
10
109
 
110
+ state.isSweeping = true;
111
+ applyCommand(state.bufferedCommands);
112
+ killJobs();
113
+ removeCleanupDirectories();
114
+
115
+ if (GUARD_PREFIX.length > 0) {
116
+ killMarkedProcessRun(GUARD_PREFIX);
117
+ }
11
118
  process.exit(0);
12
119
  };
13
120
 
14
121
  process.stdin.resume();
122
+ process.stdin.on("data", receiveCommands);
15
123
  process.stdin.on("end", sweep);
16
124
  process.stdin.on("error", sweep);
17
125
 
@@ -1,25 +1,105 @@
1
1
  import { type ChildProcess, spawn, type StdioOptions } from "node:child_process";
2
2
  import { randomBytes } from "node:crypto";
3
+ import { 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";
6
7
  import { killMarkedProcesses, PROCESS_MARKER } from "./kill-marked-processes.ts";
8
+ import {
9
+ type CleanupDirectoryIdentity,
10
+ cleanupDirectoryIdentity,
11
+ killProcessGroup,
12
+ type ProcessGroupIdentity,
13
+ processGroupIdentity,
14
+ removeCleanupDirectory,
15
+ } from "./kill-process-group.ts";
7
16
  import { resolveExecutable } from "./resolve-executable.ts";
8
17
 
9
18
  type ParentDeathSpawnOptions = {
10
19
  stdio?: StdioOptions;
11
20
  env?: NodeJS.ProcessEnv;
21
+ cleanupDirectories?: string[];
22
+ };
23
+
24
+ type ParentDeathSupervisorOptions = Omit<ParentDeathSpawnOptions, "cleanupDirectories"> & {
25
+ cleanupDirectory: string;
26
+ };
27
+
28
+ type GuardSignal = "SIGKILL" | "SIGCONT";
29
+
30
+ type LaunchArguments = (executable: string, cleanupDirectories: CleanupDirectoryIdentity[]) => string[];
31
+
32
+ type GuardJob = {
33
+ marker: string;
34
+ processGroup: ProcessGroupIdentity;
35
+ cleanupDirectories: CleanupDirectoryIdentity[];
36
+ signal: GuardSignal;
12
37
  };
13
38
 
14
39
  type GuardState = {
15
40
  child?: ChildProcess | undefined;
41
+ jobs: Map<string, GuardJob>;
42
+ };
43
+
44
+ type SpawnRollback = {
45
+ child: ChildProcess;
46
+ marker: string;
47
+ processGroup?: ProcessGroupIdentity;
48
+ cleanupDirectories?: CleanupDirectoryIdentity[];
16
49
  };
17
50
 
18
51
  const MODULE_PATH = fileURLToPath(import.meta.url);
19
52
  const GUARD_PATH = join(dirname(MODULE_PATH), `process-guard${extname(MODULE_PATH)}`);
20
53
  const RUN_ID = randomBytes(8).toString("hex");
21
54
  const RUN_PREFIX = `${PROCESS_MARKER}=${RUN_ID}/`;
22
- const guard: GuardState = {};
55
+ const guard: GuardState = { jobs: new Map() };
56
+ const SUPERVISOR_NAME = "gtkx-process-supervisor";
57
+ const SUPERVISOR_SCRIPT = [
58
+ "runtime=$1",
59
+ "identity=$2",
60
+ "expected_parent=$3",
61
+ "expected_parent_start=$4",
62
+ "stat_command=$5",
63
+ "rm_command=$6",
64
+ "shift 6",
65
+ "matches_directory() {",
66
+ ' [ "$("$stat_command" -c "%d:%i:%u" -- "$runtime" 2>/dev/null)" = "$identity" ]',
67
+ "}",
68
+ "matches_parent() {",
69
+ " parent_stat=",
70
+ ' IFS= read -r parent_stat < "/proc/$expected_parent/stat" || return 1',
71
+ " parent_tail=${parent_stat##*) }",
72
+ " set -- $parent_tail",
73
+ ' [ "${20}" = "$expected_parent_start" ]',
74
+ "}",
75
+ "terminate() {",
76
+ ' trap "" CONT TERM INT HUP',
77
+ ' if matches_directory; then "$rm_command" -rf -- "$runtime"; fi',
78
+ ' kill -KILL "-$$" 2>/dev/null',
79
+ " exit 1",
80
+ "}",
81
+ "trap terminate CONT TERM INT HUP",
82
+ "if ! matches_parent; then terminate; fi",
83
+ '"$@" &',
84
+ "child=$!",
85
+ 'wait "$child"',
86
+ "exit $?",
87
+ ].join("\n");
88
+
89
+ const processStartTime = (pid: number): string => {
90
+ const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
91
+ const start = stat.slice(stat.lastIndexOf(") ") + 2).split(" ", 20)[19];
92
+
93
+ if (start === undefined) {
94
+ throw new Error(`Failed to identify process ${String(pid)}`);
95
+ }
96
+
97
+ return start;
98
+ };
99
+
100
+ const writeGuardCommand = (child: ChildProcess, operation: "+" | "-", job: GuardJob): void => {
101
+ child.stdin?.write(`${operation}${JSON.stringify(job)}\n`);
102
+ };
23
103
 
24
104
  const releaseGuard = (child: ChildProcess): void => {
25
105
  child.unref();
@@ -41,34 +121,161 @@ const startGuard = (): void => {
41
121
  });
42
122
 
43
123
  child.on("error", () => {
44
- guard.child = undefined;
124
+ if (guard.child === child) {
125
+ guard.child = undefined;
126
+ }
127
+ });
128
+
129
+ child.on("exit", () => {
130
+ if (guard.child === child) {
131
+ guard.child = undefined;
132
+ }
133
+ });
134
+
135
+ child.stdin.on("error", () => {
136
+ if (guard.child === child) {
137
+ guard.child = undefined;
138
+ }
45
139
  });
46
140
 
47
141
  guard.child = child;
142
+
143
+ for (const job of guard.jobs.values()) {
144
+ writeGuardCommand(child, "+", job);
145
+ }
146
+
48
147
  releaseGuard(child);
49
148
  };
50
149
 
51
- function spawnWithParentDeathSignal(
150
+ const registerJob = (job: GuardJob): void => {
151
+ guard.jobs.set(job.marker, job);
152
+
153
+ if (guard.child) {
154
+ writeGuardCommand(guard.child, "+", job);
155
+ }
156
+ };
157
+
158
+ const unregisterJob = (job: GuardJob): void => {
159
+ guard.jobs.delete(job.marker);
160
+
161
+ if (guard.child) {
162
+ writeGuardCommand(guard.child, "-", job);
163
+ }
164
+ };
165
+
166
+ const rollbackSpawn = ({ child, marker, processGroup, cleanupDirectories = [] }: SpawnRollback): void => {
167
+ if (processGroup === undefined) {
168
+ child.kill("SIGKILL");
169
+ } else {
170
+ killProcessGroup(processGroup);
171
+ }
172
+
173
+ killMarkedProcesses(marker);
174
+
175
+ for (const identity of cleanupDirectories) {
176
+ removeCleanupDirectory(identity);
177
+ }
178
+ };
179
+
180
+ const captureCleanupDirectories = (command: string, paths: string[]): CleanupDirectoryIdentity[] => {
181
+ const identities = paths.map((path) => cleanupDirectoryIdentity(path));
182
+
183
+ if (identities.includes(undefined)) {
184
+ throw new Error(`Failed to identify a cleanup directory for ${command}`);
185
+ }
186
+
187
+ return identities.filter((identity): identity is CleanupDirectoryIdentity => identity !== undefined);
188
+ };
189
+
190
+ const spawnGuarded = (
52
191
  command: string,
53
- args: string[],
54
- options: ParentDeathSpawnOptions = {},
55
- ): ChildProcess {
192
+ options: ParentDeathSpawnOptions,
193
+ signal: GuardSignal,
194
+ launchArguments: LaunchArguments,
195
+ ): ChildProcess => {
56
196
  const executable = resolveExecutable(command);
57
197
  const setpriv = resolveExecutable("setpriv");
58
198
  const jobValue = `${RUN_ID}/${randomBytes(4).toString("hex")}`;
199
+ const marker = `${PROCESS_MARKER}=${jobValue}`;
200
+ const cleanupDirectories = captureCleanupDirectories(command, options.cleanupDirectories ?? []);
59
201
  startGuard();
60
202
 
61
- const child = spawn(setpriv, ["--pdeathsig", "SIGKILL", executable, ...args], {
203
+ const child = spawn(setpriv, launchArguments(executable, cleanupDirectories), {
62
204
  detached: true,
63
205
  stdio: options.stdio ?? "ignore",
64
206
  env: { ...(options.env ?? process.env), [PROCESS_MARKER]: jobValue },
65
207
  });
66
208
 
209
+ const processGroupId = child.pid;
210
+ const group = processGroupId === undefined ? undefined : processGroupIdentity(processGroupId);
211
+
212
+ if (group === undefined) {
213
+ rollbackSpawn({ child, marker, cleanupDirectories });
214
+ throw new Error(`Failed to identify process group for ${command}`);
215
+ }
216
+
217
+ const job: GuardJob = { marker, processGroup: group, cleanupDirectories, signal };
218
+ registerJob(job);
219
+
67
220
  child.on("exit", () => {
68
- killMarkedProcesses(`${PROCESS_MARKER}=${jobValue}`);
221
+ killMarkedProcesses(job.marker);
222
+ unregisterJob(job);
69
223
  });
70
224
 
71
225
  return child;
226
+ };
227
+
228
+ function spawnWithParentDeathSignal(
229
+ command: string,
230
+ args: string[],
231
+ options: ParentDeathSpawnOptions = {},
232
+ ): ChildProcess {
233
+ return spawnGuarded(
234
+ command,
235
+ options,
236
+ "SIGKILL",
237
+ (executable) => ["--pdeathsig", "SIGKILL", executable, ...args],
238
+ );
72
239
  }
73
240
 
74
- export { spawnWithParentDeathSignal };
241
+ const spawnWithParentDeathSupervisor = (
242
+ command: string,
243
+ args: string[],
244
+ options: ParentDeathSupervisorOptions,
245
+ ): ChildProcess => {
246
+ const shell = resolveExecutable("sh");
247
+ const stat = resolveExecutable("stat");
248
+ const rm = resolveExecutable("rm");
249
+
250
+ return spawnGuarded(
251
+ command,
252
+ { ...options, cleanupDirectories: [options.cleanupDirectory] },
253
+ "SIGCONT",
254
+ (executable, cleanupDirectories) => {
255
+ const identity = cleanupDirectories[0];
256
+
257
+ if (identity === undefined) {
258
+ throw new Error(`Failed to identify a cleanup directory for ${command}`);
259
+ }
260
+
261
+ return [
262
+ "--pdeathsig",
263
+ "SIGCONT",
264
+ shell,
265
+ "-c",
266
+ SUPERVISOR_SCRIPT,
267
+ SUPERVISOR_NAME,
268
+ identity.path,
269
+ `${identity.device}:${identity.inode}:${identity.userId}`,
270
+ String(process.pid),
271
+ processStartTime(process.pid),
272
+ stat,
273
+ rm,
274
+ executable,
275
+ ...args,
276
+ ];
277
+ },
278
+ );
279
+ };
280
+
281
+ export { spawnWithParentDeathSignal, spawnWithParentDeathSupervisor };