From 0447da0de04b7c98e3e37eff82b1d8e8926de5da Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Sat, 30 May 2026 14:50:15 +0100 Subject: [PATCH 1/2] test: fix up grandchild tests They passed on main before, now they actually fail before the destroy fix. --- src/test/main_test.ts | 10 ++++------ test/fixtures/child.mjs | 11 +++++++++++ test/fixtures/child_multiline.mjs | 9 +++++++++ test/fixtures/grandchild.mjs | 15 ++------------- test/fixtures/grandchild_multiline.mjs | 11 ----------- 5 files changed, 26 insertions(+), 30 deletions(-) create mode 100644 test/fixtures/child.mjs create mode 100644 test/fixtures/child_multiline.mjs delete mode 100644 test/fixtures/grandchild_multiline.mjs diff --git a/src/test/main_test.ts b/src/test/main_test.ts index f2c70c6..bc44e28 100644 --- a/src/test/main_test.ts +++ b/src/test/main_test.ts @@ -373,9 +373,7 @@ if (!isWindows) { ); const runnerScript = path.join(dir, 'runner.mjs'); const distPath = JSON.stringify(path.join(distDir, 'main.mjs')); - const fixturePath = JSON.stringify( - path.join(fixturesDir, 'grandchild.mjs') - ); + const fixturePath = JSON.stringify(path.join(fixturesDir, 'child.mjs')); fs.writeFileSync( runnerScript, @@ -399,7 +397,7 @@ if (!isWindows) { expect(parsed.exitCode).toBe(0); expect(parsed.stdout).toBe('output\n'); } finally { - spawnSync('pkill', ['-f', 'tinyexec-test-grandchild']); + spawnSync('pkill', ['-f', 'grandchild.mjs']); fs.rmSync(dir, {recursive: true, force: true}); } }); @@ -411,7 +409,7 @@ if (!isWindows) { const runnerScript = path.join(dir, 'runner.mjs'); const distPath = JSON.stringify(path.join(distDir, 'main.mjs')); const fixturePath = JSON.stringify( - path.join(fixturesDir, 'grandchild_multiline.mjs') + path.join(fixturesDir, 'child_multiline.mjs') ); fs.writeFileSync( @@ -438,7 +436,7 @@ if (!isWindows) { const parsed = JSON.parse(proc.stdout.trim()); expect(parsed).toEqual(['line1', 'line2']); } finally { - spawnSync('pkill', ['-f', 'tinyexec-test-grandchild']); + spawnSync('pkill', ['-f', 'grandchild.mjs']); fs.rmSync(dir, {recursive: true, force: true}); } }); diff --git a/test/fixtures/child.mjs b/test/fixtures/child.mjs new file mode 100644 index 0000000..a04c9dc --- /dev/null +++ b/test/fixtures/child.mjs @@ -0,0 +1,11 @@ +import {spawn} from 'node:child_process'; +import path from 'node:path'; + +// Spawn a grandchild that inherits our piped stdout fd (fd 1), simulating +// tsserver inheriting eslint's piped streams. The grandchild outlives us and +// holds the pipe open. +const grandchild = path.join(import.meta.dirname, 'grandchild.mjs'); +spawn(process.argv[0], [grandchild], {stdio: ['ignore', 1, 'ignore']}); + +console.log('output'); +process.exit(0); diff --git a/test/fixtures/child_multiline.mjs b/test/fixtures/child_multiline.mjs new file mode 100644 index 0000000..048bab1 --- /dev/null +++ b/test/fixtures/child_multiline.mjs @@ -0,0 +1,9 @@ +import {spawn} from 'node:child_process'; +import path from 'node:path'; + +const grandchild = path.join(import.meta.dirname, 'grandchild.mjs'); +spawn(process.argv[0], [grandchild], {stdio: ['ignore', 1, 'ignore']}); + +console.log('line1'); +console.log('line2'); +process.exit(0); diff --git a/test/fixtures/grandchild.mjs b/test/fixtures/grandchild.mjs index 52ef8ab..b5fcae4 100644 --- a/test/fixtures/grandchild.mjs +++ b/test/fixtures/grandchild.mjs @@ -1,13 +1,2 @@ -import {spawn} from 'node:child_process'; - -// Spawn a grandchild that inherits the piped stdout fd, simulating -// tsserver inheriting eslint's piped streams. Short timeout to avoid -// blocking test teardown. -spawn( - process.argv[0], - ['-e', 'setTimeout(() => void 0, 3000)'], - {stdio: ['ignore', 1, 'ignore']} -); - -console.log('output'); -process.exit(0); +// Run for longer than the test timeout +setTimeout(() => {}, 30000); diff --git a/test/fixtures/grandchild_multiline.mjs b/test/fixtures/grandchild_multiline.mjs deleted file mode 100644 index 253e1e2..0000000 --- a/test/fixtures/grandchild_multiline.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import {spawn} from 'node:child_process'; - -spawn( - process.argv[0], - ['-e', 'setTimeout(() => void 0, 3000)'], - {stdio: ['ignore', 1, 'ignore']} -); - -console.log('line1'); -console.log('line2'); -process.exit(0); From 07de56ca20bd0f94fc1abca18a40c807c1212c04 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Sun, 31 May 2026 10:10:54 +0100 Subject: [PATCH 2/2] chore: revert fix for now --- src/main.ts | 31 +------------------------------ src/test/main_test.ts | 4 ++-- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/src/main.ts b/src/main.ts index 5ac033f..cffa0a7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,8 +17,6 @@ export {NonZeroExitError, normalizeSpawnCommand}; const LINE_SEPARATOR_REGEX = /\r?\n/; -const pipedStreams = new WeakSet(); - export interface Output { stderr: string; stdout: string; @@ -336,7 +334,6 @@ export class ExecProcess implements Result { this._process = handle; handle.once('error', this._onError); - handle.once('exit', this._onExit); handle.once('close', this._onClose); if (handle.stdin) { @@ -345,11 +342,7 @@ export class ExecProcess implements Result { if (typeof stdin === 'string') { handle.stdin.end(stdin); } else { - const src = stdin?.process?.stdout; - if (src) { - src.pipe(handle.stdin); - pipedStreams.add(src); - } + stdin?.process?.stdout?.pipe(handle.stdin); } } } @@ -373,28 +366,6 @@ export class ExecProcess implements Result { this._thrownError = err; }; - protected _onExit = (): void => { - // Node emits 'exit' before stdio streams have drained. Use setImmediate - // to let buffered data flow through before destroying the streams. - // If grandchild processes hold the pipe fds open, they would never fire - // 'close', so we destroy here to unblock readStream and combineStreams. - const out = - this._streamOut && !pipedStreams.has(this._streamOut) - ? this._streamOut - : undefined; - const err = - this._streamErr && !pipedStreams.has(this._streamErr) - ? this._streamErr - : undefined; - if (!out && !err) { - return; - } - setImmediate(() => { - out?.destroy(); - err?.destroy(); - }); - }; - protected _onClose = (): void => { if (this._resolveClose) { this._resolveClose(); diff --git a/src/test/main_test.ts b/src/test/main_test.ts index bc44e28..2dfb581 100644 --- a/src/test/main_test.ts +++ b/src/test/main_test.ts @@ -367,7 +367,7 @@ if (!isWindows) { } }); - test('resolves when grandchild holds piped stdout open', async () => { + test.skip('resolves when grandchild holds piped stdout open', async () => { const dir = fs.mkdtempSync( path.join(os.tmpdir(), 'tinyexec-grandchild-') ); @@ -402,7 +402,7 @@ if (!isWindows) { } }); - test('iterator completes when grandchild holds piped stdout open', async () => { + test.skip('iterator completes when grandchild holds piped stdout open', async () => { const dir = fs.mkdtempSync( path.join(os.tmpdir(), 'tinyexec-grandchild-') );