-
Notifications
You must be signed in to change notification settings - Fork 297
Description
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.