@pnpm/exec.lifecycle 1100.1.1 → 1100.1.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.
package/lib/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { runLifecycleHook, type RunLifecycleHookOptions } from './runLifecycleHook.js';
2
2
  import { runLifecycleHooksConcurrently, type RunLifecycleHooksConcurrentlyOptions } from './runLifecycleHooksConcurrently.js';
3
+ import { killTrackedProcessTrees, type TrackableChildProcess, trackChildProcess } from './trackChildProcess.js';
3
4
  export declare function makeNodeRequireOption(modulePath: string, env?: Record<string, string | undefined>): {
4
5
  NODE_OPTIONS: string;
5
6
  };
6
7
  export declare function makeNodePackageMapOption(packageMapPath: string, env?: Record<string, string | undefined>): {
7
8
  NODE_OPTIONS: string;
8
9
  };
9
- export { runLifecycleHook, type RunLifecycleHookOptions, runLifecycleHooksConcurrently, type RunLifecycleHooksConcurrentlyOptions, };
10
+ export { killTrackedProcessTrees, runLifecycleHook, type RunLifecycleHookOptions, runLifecycleHooksConcurrently, type RunLifecycleHooksConcurrentlyOptions, type TrackableChildProcess, trackChildProcess, };
10
11
  export declare function runPostinstallHooks(opts: RunLifecycleHookOptions): Promise<boolean>;
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader';
2
2
  import { runLifecycleHook } from './runLifecycleHook.js';
3
3
  import { runLifecycleHooksConcurrently } from './runLifecycleHooksConcurrently.js';
4
+ import { killTrackedProcessTrees, trackChildProcess } from './trackChildProcess.js';
4
5
  export function makeNodeRequireOption(modulePath, env) {
5
6
  let { NODE_OPTIONS } = env ?? process.env;
6
7
  NODE_OPTIONS = `${NODE_OPTIONS ?? process.env.NODE_OPTIONS ?? ''} --require=${quotePathIfNeeded(modulePath)}`.trim();
@@ -30,7 +31,7 @@ function removeNodePackageMapOption(nodeOptions) {
30
31
  .replace(/(?:^|\s)--experimental-package-map\s+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\S+)/g, '')
31
32
  .trim();
32
33
  }
33
- export { runLifecycleHook, runLifecycleHooksConcurrently, };
34
+ export { killTrackedProcessTrees, runLifecycleHook, runLifecycleHooksConcurrently, trackChildProcess, };
34
35
  export async function runPostinstallHooks(opts) {
35
36
  const pkg = await safeReadPackageJsonFromDir(opts.pkgRoot);
36
37
  if (pkg == null)
@@ -0,0 +1,23 @@
1
+ export interface TrackableChildProcess {
2
+ pid?: number;
3
+ once: (event: 'close' | 'error', listener: () => void) => unknown;
4
+ }
5
+ /**
6
+ * Registers a child process spawned for a user command so that
7
+ * killTrackedProcessTrees() can terminate its process tree if pnpm exits
8
+ * while the command is still running.
9
+ */
10
+ export declare function trackChildProcess(child: TrackableChildProcess): void;
11
+ /**
12
+ * Kills the process trees of the still-running child processes registered
13
+ * with trackChildProcess(). Best-effort: children that exited concurrently
14
+ * are skipped silently.
15
+ *
16
+ * On Windows the tree is killed with `taskkill /T`, which terminates every
17
+ * descendant of a known PID without enumerating the system process list (an
18
+ * enumeration needs `wmic` or PowerShell there and can take tens of seconds).
19
+ * On POSIX only the tracked child itself is signalled; descendants are
20
+ * expected to be handled by the caller (the error handler enumerates them
21
+ * cheaply with one `ps` call).
22
+ */
23
+ export declare function killTrackedProcessTrees(): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ const trackedChildPids = new Set();
4
+ /**
5
+ * Registers a child process spawned for a user command so that
6
+ * killTrackedProcessTrees() can terminate its process tree if pnpm exits
7
+ * while the command is still running.
8
+ */
9
+ export function trackChildProcess(child) {
10
+ const pid = child.pid;
11
+ if (pid == null)
12
+ return;
13
+ trackedChildPids.add(pid);
14
+ const untrack = () => {
15
+ trackedChildPids.delete(pid);
16
+ };
17
+ child.once('close', untrack);
18
+ child.once('error', untrack);
19
+ }
20
+ /**
21
+ * Kills the process trees of the still-running child processes registered
22
+ * with trackChildProcess(). Best-effort: children that exited concurrently
23
+ * are skipped silently.
24
+ *
25
+ * On Windows the tree is killed with `taskkill /T`, which terminates every
26
+ * descendant of a known PID without enumerating the system process list (an
27
+ * enumeration needs `wmic` or PowerShell there and can take tens of seconds).
28
+ * On POSIX only the tracked child itself is signalled; descendants are
29
+ * expected to be handled by the caller (the error handler enumerates them
30
+ * cheaply with one `ps` call).
31
+ */
32
+ export async function killTrackedProcessTrees() {
33
+ await Promise.all(Array.from(trackedChildPids, killProcessTree));
34
+ }
35
+ async function killProcessTree(pid) {
36
+ if (process.platform === 'win32') {
37
+ // Resolve taskkill to its absolute System32 location so a taskkill.exe
38
+ // planted in the current directory or on PATH can't be run in its place
39
+ // during error cleanup. `process.env` is case-insensitive on Windows, so
40
+ // `SystemRoot` also matches the SYSTEMROOT/systemroot spellings.
41
+ const taskkillPath = path.join(process.env.SystemRoot ?? process.env.windir ?? 'C:\\Windows', 'System32', 'taskkill.exe');
42
+ await new Promise((resolve) => {
43
+ const taskkill = spawn(taskkillPath, ['/pid', pid.toString(), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
44
+ // pnpm is on its error-exit path and must not hang, so the wait is
45
+ // bounded by a timer that resolves even if taskkill never emits 'exit'
46
+ // or 'error' (and reaps a stuck taskkill). The kill is best-effort
47
+ // either way. The timer is unref'd so it can't keep the process alive.
48
+ const timer = setTimeout(() => {
49
+ taskkill.kill();
50
+ resolve();
51
+ }, 10_000);
52
+ timer.unref();
53
+ const done = () => {
54
+ clearTimeout(timer);
55
+ resolve();
56
+ };
57
+ // A non-zero exit code (128 when the process is already gone, 1 when
58
+ // access is denied) is deliberately ignored.
59
+ taskkill.once('error', done);
60
+ taskkill.once('exit', done);
61
+ });
62
+ }
63
+ else {
64
+ try {
65
+ process.kill(pid);
66
+ }
67
+ catch {
68
+ // the process exited before it could be signalled
69
+ }
70
+ }
71
+ }
72
+ //# sourceMappingURL=trackChildProcess.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/exec.lifecycle",
3
- "version": "1100.1.1",
3
+ "version": "1100.1.3",
4
4
  "description": "Package lifecycle hook runner",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,34 +29,34 @@
29
29
  "!*.map"
30
30
  ],
31
31
  "dependencies": {
32
+ "@pnpm/bins.linker": "1100.0.17",
33
+ "@pnpm/core-loggers": "1100.2.2",
34
+ "@pnpm/error": "1100.0.1",
35
+ "@pnpm/fetching.directory-fetcher": "1100.0.20",
32
36
  "@pnpm/npm-lifecycle": "^1100.0.0",
37
+ "@pnpm/pkg-manifest.reader": "1100.0.10",
38
+ "@pnpm/store.cafs-types": "1100.0.1",
39
+ "@pnpm/store.controller-types": "1100.1.8",
40
+ "@pnpm/types": "1101.4.0",
33
41
  "chalk": "^5.6.2",
34
42
  "is-windows": "^1.0.2",
35
43
  "path-exists": "^5.0.0",
36
44
  "run-groups": "^5.0.0",
37
- "shlex": "^3.0.0",
38
- "@pnpm/bins.linker": "1100.0.16",
39
- "@pnpm/error": "1100.0.1",
40
- "@pnpm/core-loggers": "1100.2.1",
41
- "@pnpm/store.cafs-types": "1100.0.1",
42
- "@pnpm/pkg-manifest.reader": "1100.0.9",
43
- "@pnpm/store.controller-types": "1100.1.6",
44
- "@pnpm/fetching.directory-fetcher": "1100.0.18",
45
- "@pnpm/types": "1101.3.2"
45
+ "shlex": "^3.0.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@pnpm/logger": "^1100.0.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@jest/globals": "30.4.1",
52
- "@types/is-windows": "^1.0.2",
53
- "@zkochan/rimraf": "^4.0.0",
54
- "load-json-file": "^7.0.1",
52
+ "@pnpm/exec.lifecycle": "1100.1.3",
55
53
  "@pnpm/logger": "1100.0.0",
56
- "@pnpm/exec.lifecycle": "1100.1.1",
57
- "@pnpm/test-ipc-server": "1100.0.0",
54
+ "@pnpm/prepare": "1100.0.19",
58
55
  "@pnpm/test-fixtures": "1100.0.0",
59
- "@pnpm/prepare": "1100.0.17"
56
+ "@pnpm/test-ipc-server": "1100.0.0",
57
+ "@types/is-windows": "^1.0.2",
58
+ "@zkochan/rimraf": "^4.0.0",
59
+ "load-json-file": "^7.0.1"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=22.13"