@gtkx/utils 1.6.0 → 2.0.0-beta.2

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.
Files changed (50) hide show
  1. package/README.md +5 -5
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/path.d.ts +5 -0
  7. package/dist/path.d.ts.map +1 -0
  8. package/dist/path.js +10 -0
  9. package/dist/path.js.map +1 -0
  10. package/dist/process/index.d.ts +1 -0
  11. package/dist/process/index.d.ts.map +1 -1
  12. package/dist/process/index.js +1 -0
  13. package/dist/process/index.js.map +1 -1
  14. package/dist/process/kill-marked-processes.d.ts +3 -2
  15. package/dist/process/kill-marked-processes.d.ts.map +1 -1
  16. package/dist/process/kill-marked-processes.js +44 -13
  17. package/dist/process/kill-marked-processes.js.map +1 -1
  18. package/dist/process/kill-process-group.d.ts +16 -0
  19. package/dist/process/kill-process-group.d.ts.map +1 -0
  20. package/dist/process/kill-process-group.js +72 -0
  21. package/dist/process/kill-process-group.js.map +1 -0
  22. package/dist/process/process-guard.js +77 -2
  23. package/dist/process/process-guard.js.map +1 -1
  24. package/dist/process/resolve-executable.d.ts +2 -2
  25. package/dist/process/resolve-executable.d.ts.map +1 -1
  26. package/dist/process/resolve-executable.js +3 -37
  27. package/dist/process/resolve-executable.js.map +1 -1
  28. package/dist/process/spawn-with-parent-death-signal.d.ts +1 -0
  29. package/dist/process/spawn-with-parent-death-signal.d.ts.map +1 -1
  30. package/dist/process/spawn-with-parent-death-signal.js +66 -3
  31. package/dist/process/spawn-with-parent-death-signal.js.map +1 -1
  32. package/package.json +9 -3
  33. package/src/index.ts +1 -1
  34. package/src/path.ts +17 -0
  35. package/src/process/index.ts +5 -0
  36. package/src/process/kill-marked-processes.ts +62 -13
  37. package/src/process/kill-process-group.ts +104 -0
  38. package/src/process/process-guard.ts +106 -2
  39. package/src/process/resolve-executable.ts +4 -46
  40. package/src/process/spawn-with-parent-death-signal.ts +106 -3
  41. package/dist/map/get-or-insert.d.ts +0 -8
  42. package/dist/map/get-or-insert.d.ts.map +0 -1
  43. package/dist/map/get-or-insert.js +0 -10
  44. package/dist/map/get-or-insert.js.map +0 -1
  45. package/dist/map/index.d.ts +0 -2
  46. package/dist/map/index.d.ts.map +0 -1
  47. package/dist/map/index.js +0 -2
  48. package/dist/map/index.js.map +0 -1
  49. package/src/map/get-or-insert.ts +0 -18
  50. package/src/map/index.ts +0 -1
@@ -4,12 +4,16 @@ import { Socket } from "node:net";
4
4
  import { dirname, extname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { killMarkedProcesses, PROCESS_MARKER } from "./kill-marked-processes.js";
7
+ import { cleanupDirectoryIdentity, killProcessGroup, processGroupIdentity, removeCleanupDirectory, } from "./kill-process-group.js";
7
8
  import { resolveExecutable } from "./resolve-executable.js";
8
9
  const MODULE_PATH = fileURLToPath(import.meta.url);
9
10
  const GUARD_PATH = join(dirname(MODULE_PATH), `process-guard${extname(MODULE_PATH)}`);
10
11
  const RUN_ID = randomBytes(8).toString("hex");
11
12
  const RUN_PREFIX = `${PROCESS_MARKER}=${RUN_ID}/`;
12
- const guard = {};
13
+ const guard = { jobs: new Map() };
14
+ const writeGuardCommand = (child, operation, job) => {
15
+ child.stdin?.write(`${operation}${JSON.stringify(job)}\n`);
16
+ };
13
17
  const releaseGuard = (child) => {
14
18
  child.unref();
15
19
  const { stdin } = child;
@@ -26,23 +30,82 @@ const startGuard = () => {
26
30
  stdio: ["pipe", "ignore", "ignore"],
27
31
  });
28
32
  child.on("error", () => {
29
- guard.child = undefined;
33
+ if (guard.child === child) {
34
+ guard.child = undefined;
35
+ }
36
+ });
37
+ child.on("exit", () => {
38
+ if (guard.child === child) {
39
+ guard.child = undefined;
40
+ }
41
+ });
42
+ child.stdin.on("error", () => {
43
+ if (guard.child === child) {
44
+ guard.child = undefined;
45
+ }
30
46
  });
31
47
  guard.child = child;
48
+ for (const job of guard.jobs.values()) {
49
+ writeGuardCommand(child, "+", job);
50
+ }
32
51
  releaseGuard(child);
33
52
  };
53
+ const registerJob = (job) => {
54
+ guard.jobs.set(job.marker, job);
55
+ if (guard.child) {
56
+ writeGuardCommand(guard.child, "+", job);
57
+ }
58
+ };
59
+ const unregisterJob = (job) => {
60
+ guard.jobs.delete(job.marker);
61
+ if (guard.child) {
62
+ writeGuardCommand(guard.child, "-", job);
63
+ }
64
+ };
65
+ const rollbackSpawn = ({ child, marker, processGroup, cleanupDirectories = [] }) => {
66
+ if (processGroup === undefined) {
67
+ child.kill("SIGKILL");
68
+ }
69
+ else {
70
+ killProcessGroup(processGroup);
71
+ }
72
+ killMarkedProcesses(marker);
73
+ for (const identity of cleanupDirectories) {
74
+ removeCleanupDirectory(identity);
75
+ }
76
+ };
34
77
  function spawnWithParentDeathSignal(command, args, options = {}) {
35
78
  const executable = resolveExecutable(command);
36
79
  const setpriv = resolveExecutable("setpriv");
37
80
  const jobValue = `${RUN_ID}/${randomBytes(4).toString("hex")}`;
81
+ const marker = `${PROCESS_MARKER}=${jobValue}`;
38
82
  startGuard();
39
83
  const child = spawn(setpriv, ["--pdeathsig", "SIGKILL", executable, ...args], {
40
84
  detached: true,
41
85
  stdio: options.stdio ?? "ignore",
42
86
  env: { ...(options.env ?? process.env), [PROCESS_MARKER]: jobValue },
43
87
  });
88
+ const processGroupId = child.pid;
89
+ const group = processGroupId === undefined ? undefined : processGroupIdentity(processGroupId);
90
+ if (group === undefined) {
91
+ rollbackSpawn({ child, marker });
92
+ throw new Error(`Failed to identify process group for ${command}`);
93
+ }
94
+ const cleanupDirectories = (options.cleanupDirectories ?? []).map((path) => cleanupDirectoryIdentity(path));
95
+ if (cleanupDirectories.includes(undefined)) {
96
+ const captured = cleanupDirectories.filter((identity) => identity !== undefined);
97
+ rollbackSpawn({ child, marker, processGroup: group, cleanupDirectories: captured });
98
+ throw new Error(`Failed to identify a cleanup directory for ${command}`);
99
+ }
100
+ const job = {
101
+ marker,
102
+ processGroup: group,
103
+ cleanupDirectories: cleanupDirectories.filter((identity) => identity !== undefined),
104
+ };
105
+ registerJob(job);
44
106
  child.on("exit", () => {
45
- killMarkedProcesses(`${PROCESS_MARKER}=${jobValue}`);
107
+ killMarkedProcesses(job.marker);
108
+ unregisterJob(job);
46
109
  });
47
110
  return child;
48
111
  }
@@ -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 { 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 GuardJob = {\n marker: string;\n processGroup: ProcessGroupIdentity;\n cleanupDirectories: CleanupDirectoryIdentity[];\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 | undefined;\n cleanupDirectories?: CleanupDirectoryIdentity[] | 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 = { jobs: new Map() };\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\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 const marker = `${PROCESS_MARKER}=${jobValue}`;\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 const processGroupId = child.pid;\n const group = processGroupId === undefined ? undefined : processGroupIdentity(processGroupId);\n\n if (group === undefined) {\n rollbackSpawn({ child, marker });\n throw new Error(`Failed to identify process group for ${command}`);\n }\n\n const cleanupDirectories = (options.cleanupDirectories ?? []).map((path) => cleanupDirectoryIdentity(path));\n\n if (cleanupDirectories.includes(undefined)) {\n const captured = cleanupDirectories.filter(\n (identity): identity is CleanupDirectoryIdentity => identity !== undefined,\n );\n rollbackSpawn({ child, marker, processGroup: group, cleanupDirectories: captured });\n throw new Error(`Failed to identify a cleanup directory for ${command}`);\n }\n\n const job: GuardJob = {\n marker,\n processGroup: group,\n cleanupDirectories: cleanupDirectories.filter((identity) => identity !== undefined),\n };\n registerJob(job);\n\n child.on(\"exit\", () => {\n killMarkedProcesses(job.marker);\n unregisterJob(job);\n });\n\n return child;\n}\n\nexport { spawnWithParentDeathSignal };\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/utils",
3
- "version": "1.6.0",
3
+ "version": "2.0.0-beta.2",
4
4
  "description": "Shared utilities for the GTK toolchain: logging, errors, strings.",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -26,6 +26,8 @@
26
26
  "./package.json": "./package.json",
27
27
  ".": {
28
28
  "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "require": null,
29
31
  "default": "./dist/index.js"
30
32
  }
31
33
  },
@@ -45,10 +47,14 @@
45
47
  }
46
48
  },
47
49
  "dependencies": {
48
- "picocolors": "^1.1.1"
50
+ "picocolors": "^1.1.1",
51
+ "which": "^7.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/which": "^3.0.4"
49
55
  },
50
56
  "engines": {
51
- "node": ">=24"
57
+ "node": ">=26.7.0"
52
58
  },
53
59
  "scripts": {
54
60
  "release": "tsx ../../scripts/release-package.ts"
package/src/index.ts CHANGED
@@ -2,8 +2,8 @@ export * from "./array/index.ts";
2
2
  export * from "./class/index.ts";
3
3
  export * from "./error/index.ts";
4
4
  export * from "./log/index.ts";
5
- export * from "./map/index.ts";
6
5
  export * from "./object/index.ts";
6
+ export * from "./path.ts";
7
7
  export * from "./predicate/index.ts";
8
8
  export * from "./process/index.ts";
9
9
  export * from "./set/index.ts";
package/src/path.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { isAbsolute, relative, sep } from "node:path";
2
+
3
+ const isOutsidePath = (path: string): boolean =>
4
+ path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path);
5
+
6
+ const isPathInside = (parent: string, candidate: string): boolean => {
7
+ const path = relative(parent, candidate);
8
+
9
+ return path !== "" && !isOutsidePath(path);
10
+ };
11
+
12
+ const isPathWithin = (parent: string, candidate: string): boolean =>
13
+ !isOutsidePath(relative(parent, candidate));
14
+
15
+ const toPosixPath = (path: string): string => path.replaceAll("\\", "/");
16
+
17
+ export { isPathInside, isPathWithin, toPosixPath };
@@ -1,4 +1,9 @@
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
9
  export { spawnWithParentDeathSignal } 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): void => {
45
+ if (!isCurrentProcessGroup(identity)) {
46
+ return;
47
+ }
48
+
49
+ try {
50
+ process.kill(-identity.processGroupId, "SIGKILL");
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,121 @@
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
+ };
16
+
17
+ const state: { bufferedCommands: string; isSweeping: boolean; jobs: Map<string, GuardJob> } = {
18
+ bufferedCommands: "",
19
+ isSweeping: false,
20
+ jobs: new Map(),
21
+ };
22
+
23
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
24
+ typeof value === "object" && value !== null && !Array.isArray(value);
25
+
26
+ const isProcessGroupIdentity = (value: unknown): value is ProcessGroupIdentity =>
27
+ isRecord(value) &&
28
+ typeof value.processGroupId === "number" &&
29
+ Number.isSafeInteger(value.processGroupId) &&
30
+ value.processGroupId > 1 &&
31
+ typeof value.leaderStartTime === "string" &&
32
+ /^\d+$/.test(value.leaderStartTime);
33
+
34
+ const isCleanupDirectoryIdentity = (value: unknown): value is CleanupDirectoryIdentity =>
35
+ isRecord(value) &&
36
+ typeof value.path === "string" &&
37
+ typeof value.device === "string" &&
38
+ typeof value.inode === "string" &&
39
+ typeof value.userId === "string";
40
+
41
+ const isGuardJob = (value: unknown): value is GuardJob =>
42
+ isRecord(value) &&
43
+ typeof value.marker === "string" &&
44
+ isProcessGroupIdentity(value.processGroup) &&
45
+ Array.isArray(value.cleanupDirectories) &&
46
+ value.cleanupDirectories.every(isCleanupDirectoryIdentity);
47
+
48
+ const applyCommand = (command: string): void => {
49
+ const operation = command[0];
50
+ let value: unknown;
51
+
52
+ try {
53
+ value = JSON.parse(command.slice(1));
54
+ } catch {
55
+ return;
56
+ }
57
+
58
+ if (!isGuardJob(value)) {
59
+ return;
60
+ }
61
+
62
+ if (operation === "+") {
63
+ state.jobs.set(value.marker, value);
64
+ } else if (operation === "-") {
65
+ state.jobs.delete(value.marker);
66
+ }
67
+ };
68
+
69
+ const receiveCommands = (chunk: Buffer | string): void => {
70
+ state.bufferedCommands += chunk.toString();
71
+ const commands = state.bufferedCommands.split("\n");
72
+ state.bufferedCommands = commands.pop() ?? "";
73
+
74
+ for (const command of commands) {
75
+ applyCommand(command);
76
+ }
77
+ };
78
+
79
+ const killJobs = (): void => {
80
+ for (const job of state.jobs.values()) {
81
+ killProcessGroup(job.processGroup);
82
+ killMarkedProcesses(job.marker);
83
+ }
84
+ };
85
+
86
+ const removeCleanupDirectories = (): void => {
87
+ const cleanupDirectories: Map<string, CleanupDirectoryIdentity> = new Map();
88
+
89
+ for (const job of state.jobs.values()) {
90
+ for (const identity of job.cleanupDirectories) {
91
+ cleanupDirectories.set(`${identity.device}:${identity.inode}`, identity);
92
+ }
93
+ }
94
+
95
+ for (const identity of cleanupDirectories.values()) {
96
+ removeCleanupDirectory(identity);
97
+ }
98
+ };
5
99
 
6
100
  const sweep = (): void => {
101
+ if (state.isSweeping) {
102
+ return;
103
+ }
104
+
105
+ state.isSweeping = true;
106
+ applyCommand(state.bufferedCommands);
107
+ killJobs();
108
+
7
109
  if (GUARD_PREFIX.length > 0) {
8
- killMarkedProcesses(GUARD_PREFIX);
110
+ killMarkedProcessRun(GUARD_PREFIX);
9
111
  }
10
112
 
113
+ removeCleanupDirectories();
11
114
  process.exit(0);
12
115
  };
13
116
 
14
117
  process.stdin.resume();
118
+ process.stdin.on("data", receiveCommands);
15
119
  process.stdin.on("end", sweep);
16
120
  process.stdin.on("error", sweep);
17
121
 
@@ -1,50 +1,8 @@
1
- import { accessSync, constants, statSync } from "node:fs";
2
- import { delimiter, isAbsolute, join, resolve } from "node:path";
1
+ import which from "which";
3
2
 
4
- const isExecutable = (path: string): boolean => {
5
- try {
6
- accessSync(path, constants.X_OK);
3
+ const tryResolveExecutable = (command: string): string | undefined =>
4
+ which.sync(command, { nothrow: true }) ?? undefined;
7
5
 
8
- return statSync(path).isFile();
9
- } catch {
10
- return false;
11
- }
12
- };
13
-
14
- const findOnPath = (command: string): string | undefined => {
15
- const searchPaths = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry.length > 0);
16
-
17
- for (const directory of searchPaths) {
18
- const candidate = join(directory, command);
19
-
20
- if (isExecutable(candidate)) {
21
- return candidate;
22
- }
23
- }
24
-
25
- return undefined;
26
- };
27
-
28
- function tryResolveExecutable(command: string): string | undefined {
29
- if (isAbsolute(command)) {
30
- return command;
31
- }
32
-
33
- if (command.includes("/")) {
34
- return resolve(command);
35
- }
36
-
37
- return findOnPath(command);
38
- }
39
-
40
- function resolveExecutable(command: string): string {
41
- const found = tryResolveExecutable(command);
42
-
43
- if (found === undefined) {
44
- throw new Error(`Cannot find the "${command}" executable on PATH`);
45
- }
46
-
47
- return found;
48
- }
6
+ const resolveExecutable = (command: string): string => which.sync(command);
49
7
 
50
8
  export { resolveExecutable, tryResolveExecutable };