fix: deterministic lockfile and process reaping for lifecycle scripts#9790
Open
AmariahAK wants to merge 1 commit into
Open
fix: deterministic lockfile and process reaping for lifecycle scripts#9790AmariahAK wants to merge 1 commit into
AmariahAK wants to merge 1 commit into
Conversation
Lockfile integrity (arborist): Replaced this.options.save with this.#requestedTreeMutation in three locations that gated peerOptional edge re-resolution. Previously, npm install (save !== false) would unconditionally treat invalid peerOptional edges as problems and re-resolve them from the registry, mutating package-lock.json even when no changes were made to package.json. Now only explicit tree mutations (--add, --rm, --update, npm audit fix) trigger re-resolution. Process reaping (@npmcli/run-script): Changed signal-manager.js to use process.kill(-pid, signal) to kill the entire process group rooted at the child, rather than proc.kill(signal) which only killed the direct child and left grandchildren (e.g. webpack-dev-server workers) as ghost processes holding ports. Added detached: true to spawn options when stdio === 'inherit' so the child becomes a process-group leader. Removed the stdio === 'inherit' gate on signalManager.add() so that install-time lifecycle scripts (stdio: 'pipe') are also tracked and terminated when signals interrupt the build phase. Fixes: npm#9776 Co-authored-by: atlarix-agent <agent@atlarix.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
What
Two distinct bugs in the npm lifecycle that cause non-deterministic installs and ghost processes:
package-lock.jsonhash changes between identicalnpm installruns with no changes topackage.json, because peerOptional dependency edges are unconditionally re-resolved from the registry.npm run startand Ctrl+C, lingeringnode/webpackprocesses hold ports (EADDRINUSE). The signal manager only kills direct children — grandchildren survive. Install-time lifecycle scripts running withstdio: 'pipe'are never tracked by the signal manager, so interruptingnpm installduring the build phase also orphans subprocesses.Where
Lockfile integrity —
workspaces/arborist/lib/arborist/build-ideal-tree.jsin three locations:#initTree()— virtual tree edge validationthis.options.save === false#problemEdges()— edge problem detectionthis.options.save !== falsethis.options.save || (...)Process reaping —
node_modules/@npmcli/run-script/lib/in three files:signal-manager.jsproc.kill(signal)only kills direct childrun-script-pkg.jssignalManager.add()gated onstdio === 'inherit'make-spawn-args.jsdetachedoption in spawnHow
Lockfile integrity: Replaced
this.options.savewiththis.#requestedTreeMutationin all three locations.#requestedTreeMutationistrueonly when the user explicitly asks for changes (--add,--rm,--update,npm audit fix). For a barenpm installwith no arguments, it'sfalse, and the code trusts the lockfile rather than re-resolving peerOptional edges.Process reaping:
signal-manager.js: ChangedhandleSignalto callprocess.kill(-proc.pid, signal)which targets the entire process group, with try/catch fallback toproc.kill(signal)for platforms or edge cases where process-group signalling is unsupported.make-spawn-args.js: Addeddetached: trueto spawn options whenstdio === 'inherit', so the child becomes its own process-group leader (required for-pidto work).run-script-pkg.js: Removed thestdio === 'inherit'gate onsignalManager.add()so that all subprocesses — including install-time lifecycle scripts running withstdio: 'pipe'— are tracked and terminated when signals arrive.Why this method over others
Lockfile:
#requestedTreeMutationalready existed in the codebase and was wired to--add,--rm, and--update. It precisely captures "did the user ask for changes?" — which is what should gate re-resolution — versussavewhich captures "should the lockfile be written?". The fix is a one-term removal from three conditions, reusing existing infrastructure with no new state.Process reaping: The
detached+-pidpattern is the standard Unix mechanism for tree-kill.detached: truecreates a new process group with the child as leader, andprocess.kill(-pid, signal)atomically terminates every process in that group — the shell,webpack-dev-server, and all its workers. The try/catch fallback preserves existing behavior when no process group exists (e.g. on Windows). Alternatives considered: tree-kill libraries (adds a dependency),taskkill /T(Windows-only), iterating/proc(non-portable, racy).How to test
Lockfile stability:
Process reaping:
Idempotent install:
References
Fixes: #9776