From 3ecca9c60db2d73946760fb594b154351d319ee4 Mon Sep 17 00:00:00 2001 From: Mathias Rav Date: Wed, 1 Apr 2026 10:24:53 +0200 Subject: [PATCH 1/2] fix: use 'exec' in binstub When cmd-shim is used in pnpm to create a wrapper for "node", you cannot kill the node process by killing the process spawned by invoking node_modules/.bin/node, because the binstub doesn't use 'exec'. Commit 1033be3 ("fix: improve sh binstubs", 2020-01-16) added 'exec' to the "shLongProg" code path, but for some reason didn't add it to the non-"shLongProg" path. Add it in the "shLongProg" case to fix the issue of killing node. --- src/index.ts | 2 +- test/test.js.snapshot | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0c26a52..202a530 100644 --- a/src/index.ts +++ b/src/index.ts @@ -525,7 +525,7 @@ fi ` } else { sh += `\ -${shProg} ${args} ${shTarget} ${progArgs}"$@" +exec ${shProg} ${args} ${shTarget} ${progArgs}"$@" exit $? ` } diff --git a/test/test.js.snapshot b/test/test.js.snapshot index 12d1f82..456763a 100644 --- a/test/test.js.snapshot +++ b/test/test.js.snapshot @@ -77,7 +77,7 @@ case \`uname\` in ;; esac -"/.pnpm/nodejs/16.0.0/node" "$basedir/src.env" "$@" +exec "/.pnpm/nodejs/16.0.0/node" "$basedir/src.env" "$@" exit $? `; @@ -818,7 +818,7 @@ case \`uname\` in ;; esac -"$basedir/src.exe" "$@" +exec "$basedir/src.exe" "$@" exit $? `; @@ -857,7 +857,7 @@ case \`uname\` in ;; esac -"$basedir/src.exe" "$@" +exec "$basedir/src.exe" "$@" exit $? `; From 73d60b6e686ec497cc617d5ad809ce21e70bd281 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Tue, 12 May 2026 01:10:22 +0200 Subject: [PATCH 2/2] test(e2e): cover binstub exec + update stale .exe snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a POSIX regression test that wraps `process.execPath` (a no-shebang binary, the case that hits the non-shLongProg branch of the sh shim), invokes the shim, and asserts the wrapped binary's `process.pid` equals the shim's spawn pid. That can only hold when the shell `exec`s into the target — without exec the shell wraps the binary in its own process, breaking signal delivery and producing a different PID. Manually verified: 7 tests fail when the `exec` is reverted, 59/59 pass with it. Also updates the `.exe` shim e2e snapshot so the existing Windows test matches the new `exec`-prefixed output. --- Written by an agent (Claude Code, claude-opus-4-7). --- test/e2e.test.js | 34 +++++++++++++++++++++++++++++++++- test/e2e.test.js.snapshot | 2 +- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/test/e2e.test.js b/test/e2e.test.js index e05ba33..e9ce5aa 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -1,4 +1,4 @@ -import { spawnSync } from 'node:child_process' +import { spawn, spawnSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import { describe, test, snapshot } from 'node:test' @@ -11,6 +11,7 @@ import tempy from 'tempy' import { cmdShim } from '../index.js' const describeOnWindows = process.platform === 'win32' ? describe : describe.skip +const describeOnPosix = process.platform === 'win32' ? describe.skip : describe describeOnWindows('create a command shim for a .exe file', () => { test('shim files', async (t) => { @@ -57,3 +58,34 @@ describeOnWindows('sh shim wrapping a .cmd target invoked from Git Bash', () => ) }) }) + +describeOnPosix('sh shim binstub uses exec', () => { + // Regression for the binstub bug: without `exec`, the shell process + // wraps the wrapped binary, so signals sent to the shim do not reach + // the wrapped process and the binary's PID differs from the shim's + // spawn PID. With `exec`, the shell process is replaced in place and + // the wrapped binary inherits the shim's PID. + test('wrapped binary inherits the shim\'s PID (proves exec replaced the shell)', async () => { + const tempDir = tempy.directory() + // process.execPath is a no-shebang native binary, so cmdShim hits the + // non-shLongProg branch of generateShShim — the one that needs `exec`. + const shim = path.join(tempDir, 'shim') + await cmdShim(process.execPath, shim) + + const proc = spawn('/bin/sh', [shim, '-p', 'process.pid'], { + stdio: ['ignore', 'pipe', 'pipe'], + }) + const shimPid = proc.pid + let stdout = '' + proc.stdout.on('data', (chunk) => { stdout += chunk }) + const exitCode = await new Promise((resolve) => proc.on('exit', resolve)) + + assert.equal(exitCode, 0, `shim exited ${exitCode}; stdout=${stdout}`) + const reportedPid = Number(stdout.trim()) + assert.equal( + reportedPid, + shimPid, + `expected child to inherit shim PID via exec — got reported=${reportedPid}, shim=${shimPid}` + ) + }) +}) diff --git a/test/e2e.test.js.snapshot b/test/e2e.test.js.snapshot index 0382a04..7f119de 100644 --- a/test/e2e.test.js.snapshot +++ b/test/e2e.test.js.snapshot @@ -11,7 +11,7 @@ case \`uname\` in ;; esac -"$basedir/foo" "$@" +exec "$basedir/foo" "$@" exit $? `;