node-pty 1.2.0-beta.13 → 1.2.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,25 +50,15 @@ var eventEmitter2_1 = require("./eventEmitter2");
50
50
  * the timer has started.
51
51
  */
52
52
  var FLUSH_DATA_INTERVAL = 1000;
53
- /**
54
- * Connects to and manages the lifecycle of the conout socket. This socket must be drained on
55
- * another thread in order to avoid deadlocks where Conpty waits for the out socket to drain
56
- * when `ClosePseudoConsole` is called. This happens when data is being written to the terminal when
57
- * the pty is closed.
58
- *
59
- * See also:
60
- * - https://github.com/microsoft/node-pty/issues/375
61
- * - https://github.com/microsoft/vscode/issues/76548
62
- * - https://github.com/microsoft/terminal/issues/1810
63
- * - https://docs.microsoft.com/en-us/windows/console/closepseudoconsole
64
- */
65
53
  var ConoutConnection = /** @class */ (function () {
66
54
  function ConoutConnection(_conoutPipeName, _useConptyDll) {
67
55
  var _this = this;
68
56
  this._conoutPipeName = _conoutPipeName;
69
57
  this._useConptyDll = _useConptyDll;
70
58
  this._isDisposed = false;
59
+ this._isReady = false;
71
60
  this._onReady = new eventEmitter2_1.EventEmitter2();
61
+ this._onError = new eventEmitter2_1.EventEmitter2();
72
62
  var workerData = {
73
63
  conoutPipeName: _conoutPipeName
74
64
  };
@@ -77,18 +67,34 @@ var ConoutConnection = /** @class */ (function () {
77
67
  this._worker.on('message', function (message) {
78
68
  switch (message) {
79
69
  case 1 /* ConoutWorkerMessage.READY */:
70
+ _this._isReady = true;
80
71
  _this._onReady.fire();
81
72
  return;
82
73
  default:
83
74
  console.warn('Unexpected ConoutWorkerMessage', message);
84
75
  }
85
76
  });
77
+ this._worker.on('error', function (error) {
78
+ if (!_this._isDisposed && !_this._isReady) {
79
+ _this._onError.fire(error);
80
+ }
81
+ });
82
+ this._worker.on('exit', function (code) {
83
+ if (!_this._isDisposed && !_this._isReady) {
84
+ _this._onError.fire(new Error("Conout worker exited before connecting (code ".concat(code, ")")));
85
+ }
86
+ });
86
87
  }
87
88
  Object.defineProperty(ConoutConnection.prototype, "onReady", {
88
89
  get: function () { return this._onReady.event; },
89
90
  enumerable: false,
90
91
  configurable: true
91
92
  });
93
+ Object.defineProperty(ConoutConnection.prototype, "onError", {
94
+ get: function () { return this._onError.event; },
95
+ enumerable: false,
96
+ configurable: true
97
+ });
92
98
  ConoutConnection.prototype.dispose = function () {
93
99
  if (!this._useConptyDll && this._isDisposed) {
94
100
  return;
@@ -12,6 +12,7 @@ var path = require("path");
12
12
  var child_process_1 = require("child_process");
13
13
  var net_1 = require("net");
14
14
  var windowsConoutConnection_1 = require("./windowsConoutConnection");
15
+ var eventEmitter2_1 = require("./eventEmitter2");
15
16
  var utils_1 = require("./utils");
16
17
  var conptyNative;
17
18
  /**
@@ -20,16 +21,23 @@ var conptyNative;
20
21
  * has started.
21
22
  */
22
23
  var FLUSH_DATA_INTERVAL = 1000;
24
+ var CONNECTION_TIMEOUT = 5000;
25
+ var defaultWindowsPtyAgentOptions = {
26
+ connectionTimeout: CONNECTION_TIMEOUT,
27
+ conoutConnectionFactory: function (conoutPipeName, useConptyDll) { return new windowsConoutConnection_1.ConoutConnection(conoutPipeName, useConptyDll); }
28
+ };
23
29
  /**
24
30
  * This agent sits between the WindowsTerminal class and provides an interface for conpty.
25
31
  */
26
32
  var WindowsPtyAgent = /** @class */ (function () {
27
- function WindowsPtyAgent(file, args, env, cwd, cols, rows, debug, _useConptyDll, conptyInheritCursor) {
33
+ function WindowsPtyAgent(file, args, env, cwd, cols, rows, debug, _useConptyDll, conptyInheritCursor, options) {
28
34
  if (_useConptyDll === void 0) { _useConptyDll = false; }
29
35
  if (conptyInheritCursor === void 0) { conptyInheritCursor = false; }
36
+ if (options === void 0) { options = defaultWindowsPtyAgentOptions; }
30
37
  var _this = this;
31
38
  this._useConptyDll = _useConptyDll;
32
39
  this._innerPid = 0;
40
+ this._onError = new eventEmitter2_1.EventEmitter2();
33
41
  if (!conptyNative) {
34
42
  conptyNative = (0, utils_1.loadNativeModule)('conpty').module;
35
43
  }
@@ -52,24 +60,26 @@ var WindowsPtyAgent = /** @class */ (function () {
52
60
  // We must wait for the worker to connect before calling conptyNative.connect()
53
61
  // to avoid blocking the Node.js event loop in ConnectNamedPipe.
54
62
  // See https://github.com/microsoft/node-pty/issues/763
55
- this._conoutSocketWorker = new windowsConoutConnection_1.ConoutConnection(term.conout, this._useConptyDll);
63
+ this._conoutSocketWorker = options.conoutConnectionFactory(term.conout, this._useConptyDll);
56
64
  // Store pending connection info - we'll complete the connection when worker is ready
57
65
  this._pendingPtyInfo = { pty: this._pty, commandLine: commandLine, cwd: cwd, env: env };
58
- // Timeout to ensure connection completes even if worker fails to signal ready
59
- var connectionTimeout = setTimeout(function () {
60
- if (_this._pendingPtyInfo) {
61
- // Worker never signaled ready - complete connection anyway to avoid zombie state
62
- _this._completePtyConnection();
63
- }
64
- }, 5000);
66
+ // Never call connect() before the worker is ready, as ConnectNamedPipe
67
+ // would block the Node.js event loop while waiting for the output client.
68
+ this._connectionTimeout = setTimeout(function () {
69
+ _this._failPtyConnection(new Error('Timed out waiting for ConPTY output worker'));
70
+ }, options.connectionTimeout);
65
71
  this._conoutSocketWorker.onReady(function () {
66
- clearTimeout(connectionTimeout);
72
+ if (!_this._pendingPtyInfo) {
73
+ return;
74
+ }
75
+ _this._clearConnectionTimeout();
67
76
  _this._conoutSocketWorker.connectSocket(_this._outSocket);
68
77
  // Now that the worker has connected to the output pipe, we can safely call
69
78
  // conptyNative.connect() which calls ConnectNamedPipe - it won't block because
70
79
  // the client (worker) is already connected
71
80
  _this._completePtyConnection();
72
81
  });
82
+ this._conoutSocketWorker.onError(function (error) { return _this._failPtyConnection(error); });
73
83
  this._outSocket.on('connect', function () {
74
84
  _this._outSocket.emit('ready_datapipe');
75
85
  });
@@ -81,6 +91,11 @@ var WindowsPtyAgent = /** @class */ (function () {
81
91
  });
82
92
  this._inSocket.setEncoding('utf8');
83
93
  }
94
+ Object.defineProperty(WindowsPtyAgent.prototype, "onError", {
95
+ get: function () { return this._onError.event; },
96
+ enumerable: false,
97
+ configurable: true
98
+ });
84
99
  Object.defineProperty(WindowsPtyAgent.prototype, "inSocket", {
85
100
  get: function () { return this._inSocket; },
86
101
  enumerable: false,
@@ -108,13 +123,54 @@ var WindowsPtyAgent = /** @class */ (function () {
108
123
  });
109
124
  WindowsPtyAgent.prototype._completePtyConnection = function () {
110
125
  var _this = this;
126
+ var _a;
127
+ if (!this._pendingPtyInfo) {
128
+ return;
129
+ }
130
+ this._clearConnectionTimeout();
131
+ var _b = this._pendingPtyInfo, pty = _b.pty, commandLine = _b.commandLine, cwd = _b.cwd, env = _b.env;
132
+ this._pendingPtyInfo = undefined;
133
+ try {
134
+ var connect = conptyNative.connect(pty, commandLine, cwd, env, this._useConptyDll, function (c) { return _this._$onProcessExit(c); });
135
+ this._innerPid = connect.pid;
136
+ }
137
+ catch (err) {
138
+ // connect() runs from the conout worker's onReady callback, so a throw
139
+ // here would otherwise surface as an
140
+ // uncaughtException with no way for the consumer to observe it.
141
+ var code = (_a = /error code: (\d+)/.exec(err.message)) === null || _a === void 0 ? void 0 : _a[1];
142
+ this._exitCode = code ? parseInt(code, 10) : -1;
143
+ try {
144
+ this._ptyNative.kill(this._pty, this._useConptyDll);
145
+ }
146
+ catch ( /* already gone */_c) { /* already gone */ }
147
+ this._conoutSocketWorker.dispose();
148
+ this._inSocket.destroy();
149
+ this._outSocket.destroy();
150
+ this._onError.fire(err);
151
+ }
152
+ };
153
+ WindowsPtyAgent.prototype._failPtyConnection = function (error) {
111
154
  if (!this._pendingPtyInfo) {
112
155
  return;
113
156
  }
114
- var _a = this._pendingPtyInfo, pty = _a.pty, commandLine = _a.commandLine, cwd = _a.cwd, env = _a.env;
157
+ this._clearConnectionTimeout();
115
158
  this._pendingPtyInfo = undefined;
116
- var connect = conptyNative.connect(pty, commandLine, cwd, env, this._useConptyDll, function (c) { return _this._$onProcessExit(c); });
117
- this._innerPid = connect.pid;
159
+ this._exitCode = -1;
160
+ try {
161
+ this._ptyNative.kill(this._pty, this._useConptyDll);
162
+ }
163
+ catch ( /* already gone */_a) { /* already gone */ }
164
+ this._conoutSocketWorker.dispose();
165
+ this._inSocket.destroy();
166
+ this._outSocket.destroy();
167
+ this._onError.fire(error);
168
+ };
169
+ WindowsPtyAgent.prototype._clearConnectionTimeout = function () {
170
+ if (this._connectionTimeout) {
171
+ clearTimeout(this._connectionTimeout);
172
+ this._connectionTimeout = undefined;
173
+ }
118
174
  };
119
175
  WindowsPtyAgent.prototype.resize = function (cols, rows) {
120
176
  if (this._exitCode !== undefined) {
@@ -128,6 +184,7 @@ var WindowsPtyAgent = /** @class */ (function () {
128
184
  WindowsPtyAgent.prototype.kill = function () {
129
185
  var _this = this;
130
186
  // Prevent deferred connection from completing after kill
187
+ this._clearConnectionTimeout();
131
188
  this._pendingPtyInfo = undefined;
132
189
  // Tell the agent to kill the pty, this releases handles to the process
133
190
  if (!this._useConptyDll) {
@@ -52,6 +52,12 @@ var WindowsTerminal = /** @class */ (function (_super) {
52
52
  // Create new termal.
53
53
  _this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConptyDll, opt.conptyInheritCursor);
54
54
  _this._socket = _this._agent.outSocket;
55
+ // The socket 'close' handler that drives 'exit' is only attached after
56
+ // ready_datapipe, which never fires when connect() fails.
57
+ _this._agent.onError(function () {
58
+ _this._close();
59
+ _this.emit('exit', _this._agent.exitCode);
60
+ });
55
61
  // Not available until `ready` event emitted.
56
62
  _this._pid = _this._agent.innerPid;
57
63
  _this._fd = _this._agent.fd;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Microsoft Corporation"
6
6
  },
7
- "version": "1.2.0-beta.13",
7
+ "version": "1.2.0-beta.15",
8
8
  "license": "MIT",
9
9
  "main": "./lib/index.js",
10
10
  "types": "./typings/node-pty.d.ts",
@@ -56,7 +56,8 @@
56
56
  "@typescript-eslint/eslint-plugin": "^8.0.0",
57
57
  "@typescript-eslint/parser": "^8.0.0",
58
58
  "cross-env": "^5.1.4",
59
- "eslint": "^9.0.0",
59
+ "eslint": "^10.4.0",
60
+ "globals": "^14.0.0",
60
61
  "mocha": "10",
61
62
  "node-gyp": "^11.4.2",
62
63
  "ps-list": "^6.0.0",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/unix/pty.cc CHANGED
@@ -203,6 +203,7 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) {
203
203
  }
204
204
  }
205
205
  }
206
+ close(kq);
206
207
  #else
207
208
  while (true) {
208
209
  errno = 0;
package/src/win/conpty.cc CHANGED
@@ -46,7 +46,7 @@ struct pty_baton {
46
46
  HANDLE hOut;
47
47
  HPCON hpc;
48
48
 
49
- HANDLE hShell;
49
+ HANDLE hShell = nullptr;
50
50
 
51
51
  pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {};
52
52
  };
@@ -384,6 +384,14 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
384
384
  ConnectNamedPipe(handle->hIn, nullptr);
385
385
  ConnectNamedPipe(handle->hOut, nullptr);
386
386
 
387
+ // Close hIn/hOut on every exit so a throw doesn't leak the named-pipe server handles.
388
+ auto closePipeHandles = [handle]() {
389
+ CloseHandle(handle->hIn);
390
+ CloseHandle(handle->hOut);
391
+ handle->hIn = nullptr;
392
+ handle->hOut = nullptr;
393
+ };
394
+
387
395
  // Attach the pseudoconsole to the client application we're creating
388
396
  STARTUPINFOEXW siEx{0};
389
397
  siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW);
@@ -394,12 +402,14 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
394
402
 
395
403
  SIZE_T size = 0;
396
404
  InitializeProcThreadAttributeList(NULL, 1, 0, &size);
397
- BYTE *attrList = new BYTE[size];
398
- siEx.lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList);
405
+ std::unique_ptr<BYTE[]> attrList = std::make_unique<BYTE[]>(size);
406
+ siEx.lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList.get());
399
407
 
400
408
  fSuccess = InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size);
401
409
  if (!fSuccess) {
402
- throw errorWithCode(info, "InitializeProcThreadAttributeList failed");
410
+ Napi::Error error = errorWithCode(info, "InitializeProcThreadAttributeList failed");
411
+ closePipeHandles();
412
+ throw error;
403
413
  }
404
414
  fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList,
405
415
  0,
@@ -409,7 +419,10 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
409
419
  NULL,
410
420
  NULL);
411
421
  if (!fSuccess) {
412
- throw errorWithCode(info, "UpdateProcThreadAttribute failed");
422
+ Napi::Error error = errorWithCode(info, "UpdateProcThreadAttribute failed");
423
+ DeleteProcThreadAttributeList(siEx.lpAttributeList);
424
+ closePipeHandles();
425
+ throw error;
413
426
  }
414
427
 
415
428
  PROCESS_INFORMATION piClient{};
@@ -426,9 +439,14 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
426
439
  &piClient // lpProcessInformation
427
440
  );
428
441
  if (!fSuccess) {
429
- throw errorWithCode(info, "Cannot create process");
442
+ Napi::Error error = errorWithCode(info, "Cannot create process");
443
+ DeleteProcThreadAttributeList(siEx.lpAttributeList);
444
+ closePipeHandles();
445
+ throw error;
430
446
  }
431
447
 
448
+ DeleteProcThreadAttributeList(siEx.lpAttributeList);
449
+
432
450
  HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
433
451
  bool fLoadedDll = hLibrary != nullptr;
434
452
  if (useConptyDll && fLoadedDll)
@@ -447,8 +465,7 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
447
465
  // Close the thread handle to avoid resource leak
448
466
  CloseHandle(piClient.hThread);
449
467
  // Close the input read and output write handle of the pseudoconsole
450
- CloseHandle(handle->hIn);
451
- CloseHandle(handle->hOut);
468
+ closePipeHandles();
452
469
 
453
470
  SetupExitCallback(env, exitCallback, handle);
454
471
 
@@ -550,7 +567,7 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
550
567
  const bool useConptyDll = info[1].As<Napi::Boolean>().Value();
551
568
 
552
569
  std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
553
- const pty_baton* handle = get_pty_baton(lock, id);
570
+ pty_baton* handle = get_pty_baton(lock, id);
554
571
 
555
572
  if (handle != nullptr) {
556
573
  HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
@@ -565,7 +582,17 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
565
582
  pfnClosePseudoConsole(handle->hpc);
566
583
  }
567
584
  }
568
- if (useConptyDll) {
585
+ // Defensive: if PtyConnect was never called (or failed before closing the
586
+ // pipe handles), release the named pipe server handles here.
587
+ if (handle->hIn != nullptr) {
588
+ CloseHandle(handle->hIn);
589
+ handle->hIn = nullptr;
590
+ }
591
+ if (handle->hOut != nullptr) {
592
+ CloseHandle(handle->hOut);
593
+ handle->hOut = nullptr;
594
+ }
595
+ if (useConptyDll && handle->hShell != nullptr) {
569
596
  TerminateProcess(handle->hShell, 1);
570
597
  }
571
598
  }