Make the lifetime of the processes pnpm starts follow one contract What a command leaves running now outlives pnpm exactly when it would outlive the command run without pnpm, as with cargo, `turbo exec`, npm, `deno task`, just, and moon. The contract, with the tests behind each of its rows, is in `pnpm/crates/executor/PROCESS_LIFETIME.md`, and `pnpm/AGENTS.md` makes changes to process handling go through it. pnpm used to apply a multi-task orchestrator's policy to every command: on Windows its Job Object ended everything a script had started whenever the command failed or the user pressed Ctrl+C, including a browser opened by the `open` package (#17465), after the same class of bug in #14356 and #16628. Single commands: - Every exit pnpm makes on its own goes through `pnpm_executor::exit` or `before_exit`, which release the Job Object first. `clippy.toml` bans `std::process::exit` elsewhere. A closing console, and an interrupt while pnpm runs no command, release it too. An interrupt that a running command outlasted every escalation of still ends the tree. - The Job Object still ends the tree when pnpm is killed or crashes, so a cancelled CI job leaves no dev server holding its port. `CREATE_BREAKAWAY_FROM_JOB` stays exempt (#16628). Multi-task runs: - A task that ends on its own keeps its leftovers, as pnpm 11 and Nx do. - A bail stops the tasks still running. On Unix they get SIGTERM once, and SIGKILL after a two-second grace for whatever is still provably the task: a group that still has a member, a child pnpm has not reaped, and descendants from a fresh scan. On Linux descendants are pinned through pidfds, so a reused pid is never hit. pnpm waits out the grace before it exits. The registration is dropped when its child is reaped, and kept as the child's group alone while pnpm waits for that group after an interrupt. - On Windows each such task runs in a nested Job Object, which the bail terminates. That reaches detached processes whose parent has exited, which `taskkill /T` missed, without walking parent links. Trade-off: a child joins a job it is assigned to by hand right after its creation, so a process it starts in that moment is not covered. Closing the window needs `PROC_THREAD_ATTRIBUTE_JOB_LIST`, which stable Rust's `Command` cannot pass yet. Creating the child suspended instead would overwrite a caller's creation flags, and resuming it would mean a walk of every thread on the system for each child. Tests: a differential matrix covers `exec`, `run`, `dlx`, and lifecycle scripts after success and failure. The tests also cover Ctrl+C, a closing terminal, repeated Ctrl+C, pnpm being killed, breakaway, leftovers of finished and failed tasks in recursive runs, the graceful bail, and the job-based bail. The Ctrl+C and terminal-close tests are differential on both platforms, through a pseudo-terminal on Unix and a private console on Windows. Closes pnpm/pnpm#17465
12 lines
856 B
TOML
12 lines
856 B
TOML
# `print_stderr` and `print_stdout` guard the shipped binary against a closed stderr or stdout.
|
|
# Tests may print freely.
|
|
allow-print-in-tests = true
|
|
|
|
# A temp file placed next to its target is usually persisted over it, and these create it 0600.
|
|
disallowed-methods = [
|
|
{ path = "tempfile::NamedTempFile::new_in", reason = "use pnpm_fs::umask_named_tempfile_in or pnpm_fs::private_named_tempfile_in to choose the file's mode" },
|
|
{ path = "tempfile::Builder::tempfile_in", reason = "use pnpm_fs::umask_named_tempfile_in or pnpm_fs::private_named_tempfile_in to choose the file's mode" },
|
|
# Exiting without releasing the Windows Job Object kills whatever the command left running (pnpm/crates/executor/PROCESS_LIFETIME.md).
|
|
{ path = "std::process::exit", reason = "use pnpm_executor::exit, which releases the process-tree cleanup first" },
|
|
]
|