Skip to content

Windows: ConoutConnection worker thread prevents Node.js from exiting after kill() #887

@privatenumber

Description

@privatenumber

Environment

  • OS: Windows (ConPTY path)
  • node-pty version: 1.2.0-beta.10
  • Node.js: v22.22.0

Description

After calling kill() on a Windows PTY and awaiting its exit event, Node.js cannot exit because active handles remain on the event loop. This forces consumers to call process.exit() as a workaround.

The root cause is that the ConoutConnection worker thread, its internal sockets, and several cleanup timeouts are never unref()'d. There is no .unref() call anywhere in node-pty's source.

Reproduction

const pty = require('node-pty');

const term = pty.spawn('cmd.exe', [], { cols: 80, rows: 24 });

term.onExit(() => {
  console.log('exited');
  // Node.js should exit here, but it hangs indefinitely
});

setTimeout(() => term.kill(), 500);

Expected: process exits after "exited" is logged.
Actual: process hangs indefinitely.

Root cause

There are three categories of handles keeping the event loop alive after kill():

1. Worker thread (primary)

src/windowsConoutConnection.ts:47 — the Worker is created but never unref()'d:

this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData });

The worker runs a net.Socket + net.createServer (src/worker/conoutSocketWorker.ts:11-17) that are also never unref()'d. Both the worker thread and its internal sockets keep the event loop alive.

2. Drain timeout

When dispose() is called, it schedules a 1-second timeout before terminating the worker (src/windowsConoutConnection.ts:76):

this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);

This timeout is not unref()'d, keeping the event loop alive for the duration.

A similar un-unref()'d timeout exists in WindowsPtyAgent._flushDataAndCleanUp() (src/windowsPtyAgent.ts:179):

this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);

3. I/O sockets

_outSocket and _inSocket in WindowsPtyAgent (src/windowsPtyAgent.ts:78-94) are never unref()'d.

Suggested fix

Call unref() on handles that shouldn't prevent process exit:

// windowsConoutConnection.ts — constructor
this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData });
this._worker.unref();

// windowsConoutConnection.ts — _drainDataAndClose
this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);
this._drainTimeout.unref();

// windowsPtyAgent.ts — constructor (sockets)
this._outSocket.unref();
this._inSocket.unref();

// windowsPtyAgent.ts — _flushDataAndCleanUp
this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);
this._closeTimeout.unref();

This ensures cleanup completes if other work keeps the event loop alive, but doesn't prevent the process from exiting when everything else is done.

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