nodejs / nodejs/node

Feature request: PTY support in `child_process.spawn()`

未关闭
#64,019 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

feature request
主要语言
JavaScript
星标
122k
派生
37.4k
平均合并
4 天 2 小时
30 天内合并 PR
283

描述

What is the problem this feature will solve?

Please add first-class pseudo-terminal (PTY) support to child_process.spawn()
(and ideally spawnSync()), so child processes see a real TTY on stdout/stderr/stdin
instead of pipes.

Today, the only practical option is third-party native addons such as
node-pty. That works, but it adds
native compilation, Electron/ABI friction, and an extra dependency for a capability
that shells and other runtimes expose natively.

Built-in PTY support would make Node.js a better cross-platform choice for
developer tools, CI runners, test harnesses, and build orchestrators compared
with wrapping PowerShell or Python just to get a TTY.

Problem

When spawn() uses the default stdio: ['pipe', 'pipe', 'pipe'], the child's
stdout/stderr are not TTYs. Many programs behave differently:

  • Block buffering instead of line buffering (bash, make, gcc, pacman, etc.)
  • isatty() / process.stdout.isTTY is false (no ANSI colors, different log format)
  • Interactive prompts break (password prompts, read, pagers)
  • Progress output is delayed until buffers fill or the process exits

Workarounds are incomplete:

  • stdbuf -oL -eL only affects some libc-buffered programs; grandchild processes
    (e.g. make jobs) may still block-buffer.
  • script -q -f allocates a PTY but adds Script started/done noise to logs.
  • Redirecting to a file (>log.txt) inside the shell has the same non-TTY behavior.

Concrete use case

I maintain a cross-platform bootstrap/build runner (Windows + MSYS/Cygwin) written
in TypeScript/Node.js because it is faster and simpler to ship than PowerShell or
Python for this use case.

The runner uses spawn(bash, ['--login', '-c', script]) and tees output to both
the terminal and a log file. Without a PTY:

  1. The terminal shows almost nothing for minutes ("stuck" UX).
  2. The log file updates in large bursts, not line-by-line.
  3. Some toolchain steps change behavior under non-TTY conditions.

We currently wrap commands in stdbuf -oL -eL and manually tee data events.
That helps a little but is fragile and platform-dependent. A PTY would fix the
root cause.

Minimal desired behavior:

import { spawn } from 'node:child_process';
import { createWriteStream } from 'node:fs';

const log = createWriteStream('build.log');
const child = spawn('bash', ['--login', '-c', 'make -j8'], {
  cwd: repoRoot,
  env: process.env,
  stdio: ['pipe', 'pty', 'pty'], // or a dedicated option; see below
});

child.on('data', (chunk) => {
  log.write(chunk);
  process.stdout.write(chunk);
});

The child should believe it has a terminal (isatty(1) === true), use
line-oriented output, and flush promptly.

Why not node-pty?

node-pty is widely used (VS Code, etc.)
and I am grateful it exists, but for application-level build/CI tooling it has
drawbacks:

  • Native addon: requires node-gyp/prebuilds; breaks or needs rebuilds across
    Node/Electron ABI changes.
  • Extra dependency for something that feels like core process/spawn behavior.
  • API divergence from child_process.spawn() (different return type, resize
    events, etc.).

For terminal emulators, node-pty is fine. For "run this shell script and stream
output live to console + log", PTY belongs in core next to spawn.

Proposed API (sketch)

Option A - extend stdio:

spawn(cmd, args, {
  stdio: ['pipe', 'pty', 'pty'],
  pty: {
    cols: process.stdout.columns ?? 80,
    rows: process.stdout.rows ?? 24,
    name: 'xterm-256color',
  },
});

Option B - dedicated flag:

spawn(cmd, args, {
  pty: true,
  stdio: ['pipe', 'pipe', 'pipe'],
});

Requirements:

  • Works on Linux, macOS, Windows 10+ (ConPTY).
  • Document graceful failure on older Windows (throw or fallback to pipes).
  • Optional resize(cols, rows) on the child handle.
  • PTY stream emits data events; stdin remains writable for automation.
  • Document interaction with shell: true, detached, windowsHide.

Platform notes

  • POSIX: forkpty(3) / openpty + session setup (Node already has test helpers in
    test/pseudo-tty/pty_helper.py).
  • Windows: CreatePseudoConsole() (ConPTY). Older Windows may need documented
    unsupported behavior rather than winpty-level emulation in core.

Prior art / related issues

This request is blocked on libuv PTY landing first; once libuv exposes
spawn-with-PTY, please expose it through child_process.

Why this matters for Node.js

Node is already the default for cross-platform CLI tooling (yarn, vite, eslint,
etc.). Live, faithful subprocess output is a basic expectation for:

  • build systems and monorepo orchestrators
  • test runners
  • dev environment bootstrap scripts
  • CI log streaming

Without PTY, authors either accept broken UX, add native deps, or shell out to
PowerShell/Python/bash script hacks. Native PTY would close that gap and reduce
reliance on node-pty for non-terminal-emulator use cases.

What is the feature you are proposing to solve the problem?

Add optional pseudo-terminal (PTY) support to child_process.spawn() (and
spawnSync()) so a spawned child can use a real TTY instead of pipes.

Proposed API (either shape is fine):

spawn(cmd, args, {
stdio: ['pipe', 'pty', 'pty'],
pty: { cols, rows, name: 'xterm-256color' },
});

or:

spawn(cmd, args, { pty: true, stdio: ['pipe', 'pipe', 'pipe'] });

Behavior:

  • Child sees isatty(stdout/stderr) === true (line-buffered output, colors,
    interactive prompts work as in a real terminal).
  • Parent gets a readable/writable PTY stream (emit 'data', write stdin).
  • Optional child.resize(cols, rows) for terminal size changes.
  • Linux/macOS via POSIX PTY; Windows 10+ via ConPTY; document unsupported
    fallback on older Windows.

This should be implemented on top of libuv PTY support (libuv#2640,
libuv PR#4802) and exposed through child_process, similar to how pipe
and inherit stdio modes work today.

What alternatives have you considered?

No response

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 child_process.spawn() 入口开始,检查 test/pseudo-tty/pty_helper.py,然后查看所引用的 libuv issue 和 PR,因为该请求受阻于 libuv 的 PTY 支持。要完成这项工作,需要一个就绪的 API,具备跨平台的 POSIX 和 ConPTY 行为,包括 stream、resize、fallback 和 spawnSync 处理。

由索引模型根据 Issue 内容生成。

评估

技术栈
javascript, node.js
领域
api, backend
Issue 类型
功能
难度
5/5
预计耗时
一周以上
活跃度
冷清
描述清晰度
基本清楚
新手友好度
32/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。