Skip to content

unixTerminal: input is silently queued forever when libuv threadpool completions stop arriving (CustomWriteStream has no watchdog) #939

Description

@moseoh

Environment

Issue

CustomWriteStream.write() queues data and relies on the async fs.write completion callback to advance the queue:

  • write() only starts processing when the queue transitions 0 → 1
  • _processWriteQueue() issues one fs.write and continues from its callback

fs.write completes on the libuv threadpool. If the pool stops delivering completions — which we observed live in production: all 4 workers idle in pthread_cond_wait, submitted work never picked up, event loop perfectly healthy — the callback never fires, the queue head never advances, and every subsequent write() appends to the queue forever. There is no error, no timeout, and no log: from the embedder's perspective the terminal simply stops accepting input while output (event-loop reads) keeps flowing. Terminal.write() returns void, so the embedder cannot observe or mitigate this from outside.

We confirmed this mechanism while investigating Orca's long-running "terminal intermittently stops accepting input" issue (stablyai/orca#8104). In the live wedged process, _writeQueue held every lost keystroke with the first task still at offset: 0, and hot-patching write to fs.writeSync restored input instantly — the fd was fine, only the threadpool path was dead.

We're investigating the Electron/libuv threadpool wedge separately. This issue is about how node-pty behaves when it happens: input stays queued indefinitely with no error and no recovery path.

Deterministic repro

The wedge can be simulated exactly by pinning every threadpool worker with a FIFO reader-open (blocks in open() until a writer appears):

const fs = require('fs');
const pty = require('node-pty');
const { execFileSync } = require('child_process');

execFileSync('mkfifo', ['/tmp/stall.fifo']);
const p = pty.spawn('/bin/sh', ['-c', 'read line; echo "GOT:$line"'], {});
p.onData((d) => process.stdout.write(d));

setTimeout(() => {
  for (let i = 0; i < 6; i++) fs.open('/tmp/stall.fifo', 'r', () => {});
  setTimeout(() => {
    p.write('hello\n');           // never reaches the shell
    fs.stat('/tmp', () => console.log('pool alive')); // never prints
  }, 300);
}, 300);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions