From 02797ceea6098944f6c4d29f7df84172a0812aa6 Mon Sep 17 00:00:00 2001 From: LonestoneBot Date: Sat, 11 Jul 2026 15:53:01 +0200 Subject: [PATCH] Fix "posix_spawnp failed" by restoring spawn-helper execute bit When the plugin is distributed as an unpacked bundle (rather than a fresh `npm install`), node-pty's native `spawn-helper` binary can lose its execute permission. On macOS and Linux, node-pty exec's this helper to launch the shell, so without the +x bit every pty.spawn() fails with: Error: Failed to spawn shell: posix_spawnp failed Add ensureSpawnHelperExecutable(), run once at startup after node-pty loads. It locates the helper in both the prebuilds// and build/Release/ layouts and restores the execute bit when missing. Best-effort and no-op on Windows, so it can't regress a working install. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/server.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/server.ts b/src/server.ts index 1fe8706..c6c7af0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -74,9 +74,60 @@ function findModule(name: string): any { throw new Error(`[web-terminal] Cannot find module '${name}' - run npm install in ${__dirname}`); } +// ── Native helper permissions ─────────────────────────────────────────────────── +// Some plugin bundles are unpacked in a way that drops the execute bit on +// node-pty's `spawn-helper` binary. Without it, pty.spawn() fails on macOS and +// Linux with "posix_spawnp failed". Restore the bit defensively at startup so the +// terminal works regardless of how the plugin was distributed. + +function ensureSpawnHelperExecutable(): void { + if (process.platform === 'win32') return; + + let entry: string; + try { + entry = require.resolve('node-pty'); + } catch { + return; + } + + // Walk up to the node-pty package root (the directory holding prebuilds/ or build/). + let root = path.dirname(entry); + for (let i = 0; i < 12; i++) { + if (fs.existsSync(path.join(root, 'prebuilds')) || fs.existsSync(path.join(root, 'build'))) break; + const parent = path.dirname(root); + if (parent === root) return; + root = parent; + } + + const helpers: string[] = []; + const buildHelper = path.join(root, 'build', 'Release', 'spawn-helper'); + if (fs.existsSync(buildHelper)) helpers.push(buildHelper); + const prebuildsDir = path.join(root, 'prebuilds'); + if (fs.existsSync(prebuildsDir)) { + for (const sub of fs.readdirSync(prebuildsDir)) { + const helper = path.join(prebuildsDir, sub, 'spawn-helper'); + if (fs.existsSync(helper)) helpers.push(helper); + } + } + + for (const helper of helpers) { + try { + fs.accessSync(helper, fs.constants.X_OK); + } catch { + try { + fs.chmodSync(helper, fs.statSync(helper).mode | 0o111); + console.error(`[web-terminal] restored missing execute bit on ${helper}`); + } catch (err) { + console.error(`[web-terminal] failed to chmod ${helper}: ${(err as Error).message}`); + } + } + } +} + // ── Dependencies ────────────────────────────────────────────────────────────── const pty = findModule('node-pty') as PtyModule; +ensureSpawnHelperExecutable(); const { WebSocketServer, WebSocket } = findModule('ws') as WsModule; // ── State ─────────────────────────────────────────────────────────────────────