Summary
On Windows with ConPTY (useConpty: true, useConptyDll: false), each pty leaks one PipeWrap handle for its lifetime of the host process. The conin socket is never destroyed.
The ConPTY DLL path a few lines away does destroy it, so this looks like an asymmetry rather than a deliberate design.
Where
lib/windowsPtyAgent.js, in kill().
The useConpty && !useConptyDll branch marks both sockets unreadable and disposes the conout worker:
this._inSocket.readable = false;
this._outSocket.readable = false;
this._getConsoleProcessList().then(...);
this._ptyNative.kill(this._pty, this._useConptyDll);
this._conoutSocketWorker.dispose();
The useConptyDll branch does destroy it:
this._inSocket.destroy();
this._ptyNative.kill(this._pty, this._useConptyDll);
_inSocket.destroy() is never called on the first path.
Reproduction
node-pty 1.1.0, Node 26.0.0, Windows 11 Pro 26200 x64.
Spawn N ptys, wait for each to be ready, kill it, wait for exit, then count PipeWrap entries from process.getActiveResourcesInfo() after things settle.
sessions: 3 baseline PipeWrap=1 8s after: PipeWrap=5
sessions: 30 baseline PipeWrap=1 8s after: PipeWrap=32
Linear, roughly one handle per pty. Identical whether the pty is killed or exits on its own.
Destroying the socket from outside, with everything else unchanged:
leave 30 sessions: PipeWrap=32
destroy 30 sessions: PipeWrap=2
The remaining two are the last session still settling.
Impact
For a short-lived process this is invisible. For a long-running host that spawns and disposes ptys repeatedly, which is our case, it accumulates without bound.
Workaround
Reaching into the agent from outside after exit:
const socket = term._agent && term._agent.inSocket;
if (socket && typeof socket.destroy === 'function' && !socket.destroyed) {
socket.destroy();
}
Happy to open a PR adding this._inSocket.destroy() to the ConPTY branch if that is the fix you would want.
Summary
On Windows with ConPTY (
useConpty: true,useConptyDll: false), each pty leaks onePipeWraphandle for its lifetime of the host process. The conin socket is never destroyed.The ConPTY DLL path a few lines away does destroy it, so this looks like an asymmetry rather than a deliberate design.
Where
lib/windowsPtyAgent.js, inkill().The
useConpty && !useConptyDllbranch marks both sockets unreadable and disposes the conout worker:The
useConptyDllbranch does destroy it:_inSocket.destroy()is never called on the first path.Reproduction
node-pty 1.1.0, Node 26.0.0, Windows 11 Pro 26200 x64.
Spawn N ptys, wait for each to be ready, kill it, wait for exit, then count
PipeWrapentries fromprocess.getActiveResourcesInfo()after things settle.Linear, roughly one handle per pty. Identical whether the pty is killed or exits on its own.
Destroying the socket from outside, with everything else unchanged:
The remaining two are the last session still settling.
Impact
For a short-lived process this is invisible. For a long-running host that spawns and disposes ptys repeatedly, which is our case, it accumulates without bound.
Workaround
Reaching into the agent from outside after exit:
Happy to open a PR adding
this._inSocket.destroy()to the ConPTY branch if that is the fix you would want.