diff --git a/config/patches/node-pty@1.1.0.patch b/config/patches/node-pty@1.1.0.patch index 31674490e56..f25c303b61f 100644 --- a/config/patches/node-pty@1.1.0.patch +++ b/config/patches/node-pty@1.1.0.patch @@ -367,7 +367,15 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc #endif /* NSIG - macro for highest signal + 1, should be defined */ -@@ -228,6 +267,9 @@ Napi::Value PtyGetProc(const Napi::CallbackInfo& info); +@@ -169,6 +208,7 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) { + } + } + } ++ close(kq); + #else + while (true) { + errno = 0; +@@ -228,6 +268,9 @@ Napi::Value PtyGetProc(const Napi::CallbackInfo& info); static int pty_nonblock(int); @@ -377,7 +385,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc #if defined(__APPLE__) static char * pty_getproc(int); -@@ -237,13 +279,23 @@ pty_getproc(int, char *); +@@ -237,13 +280,23 @@ pty_getproc(int, char *); #endif #if defined(__APPLE__) || defined(__OpenBSD__) @@ -402,7 +410,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc #endif struct DelBuf { -@@ -345,8 +397,8 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) { +@@ -345,8 +398,8 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) { term->c_cc[VSTATUS] = 20; #endif @@ -413,7 +421,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc // helperPath std::string helper_path = info[9].As(); -@@ -367,14 +419,18 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) { +@@ -367,14 +420,18 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) { argv[i + 3] = strdup(arg.c_str()); } @@ -436,7 +444,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc #else int argc = argv_.Length(); int argl = argc + 2; -@@ -445,6 +501,9 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) { +@@ -445,6 +502,9 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) { if (pty_nonblock(master) == -1) { throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking."); } @@ -446,7 +454,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc } #endif -@@ -586,6 +645,23 @@ pty_nonblock(int fd) { +@@ -586,6 +646,23 @@ pty_nonblock(int fd) { return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } @@ -470,7 +478,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc /** * pty_getproc * Taken from tmux. -@@ -684,15 +760,73 @@ pty_getproc(int fd, char *tty) { +@@ -684,15 +761,73 @@ pty_getproc(int fd, char *tty) { #endif #if defined(__APPLE__) @@ -546,7 +554,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc for (; count < 3; count++) { low_fds[count] = posix_openpt(O_RDWR); -@@ -706,84 +840,499 @@ pty_posix_spawn(char** argv, char** env, +@@ -706,84 +841,499 @@ pty_posix_spawn(char** argv, char** env, POSIX_SPAWN_SETSID; *master = posix_openpt(O_RDWR); if (*master == -1) { @@ -1070,7 +1078,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..381d59e11bf158d269335c8231b407fc /** * Init */ -@@ -793,6 +1342,11 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { +@@ -793,6 +1343,11 @@ Napi::Object init(Napi::Env env, Napi::Object exports) { exports.Set("open", Napi::Function::New(env, PtyOpen)); exports.Set("resize", Napi::Function::New(env, PtyResize)); exports.Set("process", Napi::Function::New(env, PtyGetProc)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90fa4a7fc1e..efa16c67e12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,7 +185,7 @@ patchedDependencies: i18next-cli@1.74.2: 2ee32eef742615cd133e1a6940f027deb3723a8430bb7953bba3a6769b0aac47 lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673 monaco-editor@0.55.1: 53b71f7d8a7c00b76ffdb73ce83d18e31692f193f5fafa6a390e48f5b3fdd636 - node-pty@1.1.0: f81acb44ac52265a6053af59e60128471f4655de39e64f8c47c916e0dc8c9416 + node-pty@1.1.0: 98dadd30788d9235a1a28c4596078918f5936bd92804ca529ab1958f1a0fc278 oxlint-plugin-anti-slop@0.1.2: c10c871702365a0c2c59e9f1c5fb92df97cae5755d534c0a3d83c6368f6feafe pdfjs-dist@6.3.289: 424fde648e16c965ce45c55bdda49ecb3a4aa992c856900714c841c3f13f8415 sprintf-js@1.1.3: 05316a8b01f155fe19a1e0cb4042c81d43d798a76755c407684e1199a18b1e08 @@ -234,7 +234,7 @@ importers: version: 3.3.1 node-pty: specifier: ^1.1.0 - version: 1.1.0(patch_hash=f81acb44ac52265a6053af59e60128471f4655de39e64f8c47c916e0dc8c9416) + version: 1.1.0(patch_hash=98dadd30788d9235a1a28c4596078918f5936bd92804ca529ab1958f1a0fc278) posthog-node: specifier: ^5.52.5 version: 5.52.5 @@ -13756,7 +13756,7 @@ snapshots: node-int64@0.4.0: {} - node-pty@1.1.0(patch_hash=f81acb44ac52265a6053af59e60128471f4655de39e64f8c47c916e0dc8c9416): + node-pty@1.1.0(patch_hash=98dadd30788d9235a1a28c4596078918f5936bd92804ca529ab1958f1a0fc278): dependencies: node-addon-api: 7.1.1 diff --git a/src/main/daemon/node-pty-fd-leak.test.ts b/src/main/daemon/node-pty-fd-leak.test.ts index 4bd0cc3eaf2..a1276219606 100644 --- a/src/main/daemon/node-pty-fd-leak.test.ts +++ b/src/main/daemon/node-pty-fd-leak.test.ts @@ -18,6 +18,12 @@ function currentOpenFdCount(): number { .filter((line) => line.trim().length > 0).length } +function currentKqueueFdCount(): number { + return execFileSync('lsof', ['-p', String(process.pid)], { encoding: 'utf8' }) + .split('\n') + .filter((line) => line.includes(' KQUEUE ')).length +} + function getExistingSpawnHelper(): string { const helperPath = getNodePtySpawnHelperCandidates().find((candidate) => existsSync(candidate)) expect(helperPath).toBeTruthy() @@ -42,6 +48,19 @@ async function spawnExitingPty(index: number): Promise { const describeOnDarwin = process.platform === 'darwin' ? describe : describe.skip describeOnDarwin('node-pty macOS spawn fd handling', () => { + // Forty serial real shell launches need a 15-second budget. + it('releases the child-exit kqueue fd across repeated pty churn', async () => { + const before = currentKqueueFdCount() + + for (let i = 0; i < 40; i++) { + await spawnExitingPty(i) + } + + const after = currentKqueueFdCount() + + expect(after - before).toBe(0) + }, 15000) + it('does not leak revoked slave tty fds across exited pty spawns', async () => { const before = currentRevokedFdCount()