From 6a994d9849f8d18f8ff8c5e918c0219736fe8310 Mon Sep 17 00:00:00 2001 From: fei <204683769+feiiiiii5@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:04:10 +0800 Subject: [PATCH] fix(unix): do not rewrite already-unpacked asar helper paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract resolveSpawnHelperPath and skip the app.asar / node_modules.asar rewrite when the unpacked variant is already present. When node-pty is installed under app.asar.unpacked (e.g. an Electron app that unpacks native modules), String.prototype.replace matches the 'app.asar' prefix inside 'app.asar.unpacked' and produces 'app.asar.unpacked.unpacked' — a path that does not exist, so PTY spawn fails with 'posix_spawnp failed.' on macOS. Adds regression tests for both the packed and already-unpacked forms. Co-authored-by: DeepSeek Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com> --- src/unixTerminal.test.ts | 32 +++++++++++++++++++++++++++++++- src/unixTerminal.ts | 30 ++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/unixTerminal.test.ts b/src/unixTerminal.test.ts index a666e91aa..c63780088 100644 --- a/src/unixTerminal.test.ts +++ b/src/unixTerminal.test.ts @@ -18,7 +18,37 @@ const FIXTURES_PATH = path.normalize(path.join(__dirname, '..', 'fixtures', 'utf if (process.platform !== 'win32') { // Dynamic require to avoid loading pty.node on Windows // eslint-disable-next-line @typescript-eslint/naming-convention - const { UnixTerminal } = require('./unixTerminal') as { UnixTerminal: typeof UnixTerminalType }; + const { UnixTerminal, resolveSpawnHelperPath } = require('./unixTerminal') as { UnixTerminal: typeof UnixTerminalType; resolveSpawnHelperPath: (nativeDir: string, moduleDir: string) => string }; + + describe('resolveSpawnHelperPath', () => { + it('rewrites a packed asar path to its unpacked form', () => { + const helperPath = resolveSpawnHelperPath( + '/Example.app/Contents/Resources/app.asar/node_modules/node-pty/build/Release', + '/Example.app/Contents/Resources/app.asar/node_modules/node-pty/lib'); + assert.strictEqual(helperPath, '/Example.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/build/Release/spawn-helper'); + }); + + it('does not double-rewrite an already-unpacked asar path', () => { + const helperPath = resolveSpawnHelperPath( + '/Example.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/build/Release', + '/Example.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/lib'); + assert.strictEqual(helperPath, '/Example.app/Contents/Resources/app.asar.unpacked/node_modules/node-pty/build/Release/spawn-helper'); + }); + + it('rewrites a packed node_modules.asar path to its unpacked form', () => { + const helperPath = resolveSpawnHelperPath( + '/app/node_modules.asar/node-pty/build/Release', + '/app/node_modules.asar/node-pty/lib'); + assert.strictEqual(helperPath, '/app/node_modules.asar.unpacked/node-pty/build/Release/spawn-helper'); + }); + + it('does not double-rewrite an already-unpacked node_modules.asar path', () => { + const helperPath = resolveSpawnHelperPath( + '/app/node_modules.asar.unpacked/node-pty/build/Release', + '/app/node_modules.asar.unpacked/node-pty/lib'); + assert.strictEqual(helperPath, '/app/node_modules.asar.unpacked/node-pty/build/Release/spawn-helper'); + }); + }); describe('UnixTerminal', () => { describe('Constructor', () => { diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts index 2776d501e..44d202b83 100644 --- a/src/unixTerminal.ts +++ b/src/unixTerminal.ts @@ -14,10 +14,32 @@ import { assign, loadNativeModule } from './utils'; const native = loadNativeModule('pty'); const pty: IUnixNative = native.module; -let helperPath = native.dir + '/spawn-helper'; -helperPath = path.resolve(__dirname, helperPath); -helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); -helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); + +/** + * Resolve the absolute path of the platform spawn helper binary. + * + * @param nativeDir - the directory that holds the compiled native module. + * @param moduleDir - the directory of this module (resolves relative dirs). + * @returns the absolute spawn-helper path. + */ +export function resolveSpawnHelperPath(nativeDir: string, moduleDir: string): string { + let helperPath = path.resolve(moduleDir, nativeDir + '/spawn-helper'); + // String.prototype.replace matches the first occurrence, so when helperPath + // already contains 'app.asar.unpacked' (e.g. node-pty is installed under an + // unpacked asar directory) the substring 'app.asar' matches the prefix of + // 'app.asar.unpacked' and produces 'app.asar.unpacked.unpacked' — a path + // that does not exist on disk. Skip each rewrite when the unpacked variant + // is already present. + if (helperPath.indexOf('app.asar.unpacked') === -1) { + helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); + } + if (helperPath.indexOf('node_modules.asar.unpacked') === -1) { + helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); + } + return helperPath; +} + +const helperPath = resolveSpawnHelperPath(native.dir, __dirname); const DEFAULT_FILE = 'sh'; const DEFAULT_NAME = 'xterm';