Skip to content

Commit df68f2d

Browse files
committed
fix: destroy piped streams on child exit to prevent grandchild deadlock
When a child process spawns a grandchild that inherits the piped stdout/stderr file descriptors, the grandchild holds them open after the child exits. Node's close event waits for all fds to be released, so it never fires. readStream and combineStreams hang indefinitely because the streams never end. Listen for the exit event (fires when the process exits, regardless of pipe state) and destroy the piped streams. This forces the PassThrough and readline consumers to complete. Refs: lint-staged/lint-staged#1800
1 parent bf59661 commit df68f2d

2 files changed

Lines changed: 13 additions & 3 deletions

File tree

src/main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ export class ExecProcess implements Result {
334334

335335
this._process = handle;
336336
handle.once('error', this._onError);
337+
handle.once('exit', this._onExit);
337338
handle.once('close', this._onClose);
338339

339340
if (handle.stdin) {
@@ -366,6 +367,15 @@ export class ExecProcess implements Result {
366367
this._thrownError = err;
367368
};
368369

370+
protected _onExit = (): void => {
371+
// Destroy piped streams when the child process exits, even if grandchild
372+
// processes still hold the underlying file descriptors open. Without this,
373+
// the 'close' event never fires (it waits for all fds to be released),
374+
// causing readStream and combineStreams to hang indefinitely.
375+
this._streamOut?.destroy();
376+
this._streamErr?.destroy();
377+
};
378+
369379
protected _onClose = (): void => {
370380
if (this._resolveClose) {
371381
this._resolveClose();

src/test/grandchild_test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {x} from '../main.js';
21
import {describe, test, expect} from 'vitest';
32
import os from 'node:os';
43
import fs from 'node:fs';
@@ -15,10 +14,10 @@ describe.skipIf(isWindows)('exec (grandchild pipe inheritance)', () => {
1514
fs.writeFileSync(
1615
childScript,
1716
`import { spawn } from 'node:child_process'
17+
console.log('output')
1818
spawn('node', ['-e', 'setTimeout(() => void 0, 30000)'], {
1919
stdio: ['ignore', 1, 'ignore'],
2020
})
21-
process.stdout.write('output\\n')
2221
process.exit(0)
2322
`
2423
);
@@ -72,10 +71,11 @@ process.exit(result === 'completed' ? 0 : 1)
7271
fs.writeFileSync(
7372
childScript,
7473
`import { spawn } from 'node:child_process'
74+
console.log('line1')
75+
console.log('line2')
7576
spawn('node', ['-e', 'setTimeout(() => void 0, 30000)'], {
7677
stdio: ['ignore', 1, 'ignore'],
7778
})
78-
process.stdout.write('line1\\nline2\\n')
7979
process.exit(0)
8080
`
8181
);

0 commit comments

Comments
 (0)