Skip to content

exec hangs when grandchild process holds piped stdout open #138

@Mearman

Description

@Mearman

When a child process spawned by x() creates a grandchild that inherits the piped stdout fd, the grandchild keeps the pipe open after the child exits. Both await x() and the async iterator hang indefinitely.

Reproduction:

import { x } from 'tinyexec'

// child.mjs:
//   import { spawn } from 'node:child_process'
//   spawn('node', ['-e', 'setTimeout(() => void 0, 30000)'], {
//     stdio: ['ignore', 1, 'ignore'],
//   })
//   process.stdout.write('output\\n')
//   process.exit(0)

const result = await x('node', ['child.mjs'])
// never reaches here

The grandchild inherits fd 1 (the piped stdout). When the child exits, the grandchild keeps the pipe open. readStream and combineStreams hang because the streams never end. Node's close event doesn't fire either, since it waits for all fds to be released.

This is what causes lint-staged to deadlock when eslint uses typescript-eslint's projectService (tsserver inherits the piped fds): lint-staged/lint-staged#1800.

Node v24.14.1, macOS (Apple Silicon). Only reproduces when stdout is piped (not a TTY).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions