node-pty 1.2.0-beta.14 → 1.2.0-beta.16

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.
@@ -261,6 +261,10 @@ var UnixTerminal = /** @class */ (function (_super) {
261
261
  };
262
262
  UnixTerminal.prototype.clear = function () {
263
263
  };
264
+ UnixTerminal.prototype.setParentWindow = function (handle) {
265
+ // Windows only
266
+ return false;
267
+ };
264
268
  UnixTerminal.prototype._sanitizeEnv = function (env) {
265
269
  // Make sure we didn't start our server from inside tmux.
266
270
  delete env['TMUX'];
@@ -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;
@@ -21,13 +21,19 @@ var conptyNative;
21
21
  * has started.
22
22
  */
23
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
+ };
24
29
  /**
25
30
  * This agent sits between the WindowsTerminal class and provides an interface for conpty.
26
31
  */
27
32
  var WindowsPtyAgent = /** @class */ (function () {
28
- function WindowsPtyAgent(file, args, env, cwd, cols, rows, debug, _useConptyDll, conptyInheritCursor) {
33
+ function WindowsPtyAgent(file, args, env, cwd, cols, rows, debug, _useConptyDll, conptyInheritCursor, options) {
29
34
  if (_useConptyDll === void 0) { _useConptyDll = false; }
30
35
  if (conptyInheritCursor === void 0) { conptyInheritCursor = false; }
36
+ if (options === void 0) { options = defaultWindowsPtyAgentOptions; }
31
37
  var _this = this;
32
38
  this._useConptyDll = _useConptyDll;
33
39
  this._innerPid = 0;
@@ -54,24 +60,26 @@ var WindowsPtyAgent = /** @class */ (function () {
54
60
  // We must wait for the worker to connect before calling conptyNative.connect()
55
61
  // to avoid blocking the Node.js event loop in ConnectNamedPipe.
56
62
  // See https://github.com/microsoft/node-pty/issues/763
57
- this._conoutSocketWorker = new windowsConoutConnection_1.ConoutConnection(term.conout, this._useConptyDll);
63
+ this._conoutSocketWorker = options.conoutConnectionFactory(term.conout, this._useConptyDll);
58
64
  // Store pending connection info - we'll complete the connection when worker is ready
59
65
  this._pendingPtyInfo = { pty: this._pty, commandLine: commandLine, cwd: cwd, env: env };
60
- // Timeout to ensure connection completes even if worker fails to signal ready
61
- var connectionTimeout = setTimeout(function () {
62
- if (_this._pendingPtyInfo) {
63
- // Worker never signaled ready - complete connection anyway to avoid zombie state
64
- _this._completePtyConnection();
65
- }
66
- }, 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);
67
71
  this._conoutSocketWorker.onReady(function () {
68
- clearTimeout(connectionTimeout);
72
+ if (!_this._pendingPtyInfo) {
73
+ return;
74
+ }
75
+ _this._clearConnectionTimeout();
69
76
  _this._conoutSocketWorker.connectSocket(_this._outSocket);
70
77
  // Now that the worker has connected to the output pipe, we can safely call
71
78
  // conptyNative.connect() which calls ConnectNamedPipe - it won't block because
72
79
  // the client (worker) is already connected
73
80
  _this._completePtyConnection();
74
81
  });
82
+ this._conoutSocketWorker.onError(function (error) { return _this._failPtyConnection(error); });
75
83
  this._outSocket.on('connect', function () {
76
84
  _this._outSocket.emit('ready_datapipe');
77
85
  });
@@ -119,6 +127,7 @@ var WindowsPtyAgent = /** @class */ (function () {
119
127
  if (!this._pendingPtyInfo) {
120
128
  return;
121
129
  }
130
+ this._clearConnectionTimeout();
122
131
  var _b = this._pendingPtyInfo, pty = _b.pty, commandLine = _b.commandLine, cwd = _b.cwd, env = _b.env;
123
132
  this._pendingPtyInfo = undefined;
124
133
  try {
@@ -126,8 +135,8 @@ var WindowsPtyAgent = /** @class */ (function () {
126
135
  this._innerPid = connect.pid;
127
136
  }
128
137
  catch (err) {
129
- // connect() runs from the conout worker's onReady callback (or its
130
- // timeout fallback), so a throw here would otherwise surface as an
138
+ // connect() runs from the conout worker's onReady callback, so a throw
139
+ // here would otherwise surface as an
131
140
  // uncaughtException with no way for the consumer to observe it.
132
141
  var code = (_a = /error code: (\d+)/.exec(err.message)) === null || _a === void 0 ? void 0 : _a[1];
133
142
  this._exitCode = code ? parseInt(code, 10) : -1;
@@ -141,6 +150,28 @@ var WindowsPtyAgent = /** @class */ (function () {
141
150
  this._onError.fire(err);
142
151
  }
143
152
  };
153
+ WindowsPtyAgent.prototype._failPtyConnection = function (error) {
154
+ if (!this._pendingPtyInfo) {
155
+ return;
156
+ }
157
+ this._clearConnectionTimeout();
158
+ this._pendingPtyInfo = undefined;
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
+ }
174
+ };
144
175
  WindowsPtyAgent.prototype.resize = function (cols, rows) {
145
176
  if (this._exitCode !== undefined) {
146
177
  throw new Error('Cannot resize a pty that has already exited');
@@ -150,9 +181,19 @@ var WindowsPtyAgent = /** @class */ (function () {
150
181
  WindowsPtyAgent.prototype.clear = function () {
151
182
  this._ptyNative.clear(this._pty, this._useConptyDll);
152
183
  };
184
+ /**
185
+ * Tells ConPTY which top-level window hosts the terminal (ConptyReparentPseudoConsole). The
186
+ * window becomes the owner of the console window returned by GetConsoleWindow(), so dialogs
187
+ * that console apps parent to it (e.g. WAM sign-in) stack above the host window. Returns
188
+ * whether the request was sent to ConPTY, which applies it asynchronously.
189
+ */
190
+ WindowsPtyAgent.prototype.setParentWindow = function (handle) {
191
+ return this._ptyNative.reparent(this._pty, handle, this._useConptyDll);
192
+ };
153
193
  WindowsPtyAgent.prototype.kill = function () {
154
194
  var _this = this;
155
195
  // Prevent deferred connection from completing after kill
196
+ this._clearConnectionTimeout();
156
197
  this._pendingPtyInfo = undefined;
157
198
  // Tell the agent to kill the pty, this releases handles to the process
158
199
  if (!this._useConptyDll) {
@@ -148,6 +148,11 @@ var WindowsTerminal = /** @class */ (function (_super) {
148
148
  _this._agent.clear();
149
149
  });
150
150
  };
151
+ WindowsTerminal.prototype.setParentWindow = function (handle) {
152
+ // Not deferred: the pseudoconsole exists as soon as the agent is created, and the owner
153
+ // should be requested before the shell starts asking for it.
154
+ return this._agent.setParentWindow(handle);
155
+ };
151
156
  WindowsTerminal.prototype.destroy = function () {
152
157
  var _this = this;
153
158
  this._deferNoArgs(function () {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "Microsoft Corporation"
6
6
  },
7
- "version": "1.2.0-beta.14",
7
+ "version": "1.2.0-beta.16",
8
8
  "license": "MIT",
9
9
  "main": "./lib/index.js",
10
10
  "types": "./typings/node-pty.d.ts",
@@ -27,6 +27,18 @@
27
27
  "bugs": {
28
28
  "url": "https://github.com/microsoft/node-pty/issues"
29
29
  },
30
+ "devEngines": {
31
+ "runtime": {
32
+ "name": "node",
33
+ "version": ">=24",
34
+ "onFail": "error"
35
+ },
36
+ "packageManager": {
37
+ "name": "npm",
38
+ "version": ">=11.6.3",
39
+ "onFail": "error"
40
+ }
41
+ },
30
42
  "keywords": [
31
43
  "pty",
32
44
  "tty",
@@ -51,15 +63,14 @@
51
63
  "node-addon-api": "^7.1.0"
52
64
  },
53
65
  "devDependencies": {
54
- "@types/mocha": "^7.0.2",
66
+ "@types/mocha": "^10.0.10",
55
67
  "@types/node": "12",
56
68
  "@typescript-eslint/eslint-plugin": "^8.0.0",
57
69
  "@typescript-eslint/parser": "^8.0.0",
58
70
  "cross-env": "^5.1.4",
59
71
  "eslint": "^10.4.0",
60
72
  "globals": "^14.0.0",
61
- "mocha": "10",
62
- "node-gyp": "^11.4.2",
73
+ "mocha": "12.0.2",
63
74
  "ps-list": "^6.0.0",
64
75
  "typescript": "^5.0.0"
65
76
  }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/win/conpty.cc CHANGED
@@ -15,6 +15,7 @@
15
15
  #include <assert.h>
16
16
  #include <Shlwapi.h> // PathCombine, PathIsRelative
17
17
  #include <atomic>
18
+ #include <cstring>
18
19
  #include <mutex>
19
20
  #include <sstream>
20
21
  #include <iostream>
@@ -40,6 +41,9 @@ typedef void (__stdcall *PFNRELEASEPSEUDOCONSOLE)(HPCON hpc);
40
41
 
41
42
  #endif
42
43
 
44
+ // Only exported by the bundled conpty.dll; kernel32 has no equivalent.
45
+ typedef HRESULT (__stdcall *PFNREPARENTPSEUDOCONSOLE)(HPCON hpc, HWND newParent);
46
+
43
47
  struct pty_baton {
44
48
  int id;
45
49
  HANDLE hIn;
@@ -495,7 +499,7 @@ static Napi::Value PtyResize(const Napi::CallbackInfo& info) {
495
499
  std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
496
500
  const pty_baton* handle = get_pty_baton(lock, id);
497
501
 
498
- if (handle != nullptr) {
502
+ if (handle != nullptr && handle->hpc != nullptr) {
499
503
  HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
500
504
  bool fLoadedDll = hLibrary != nullptr;
501
505
  if (fLoadedDll)
@@ -537,7 +541,7 @@ static Napi::Value PtyClear(const Napi::CallbackInfo& info) {
537
541
  std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
538
542
  const pty_baton* handle = get_pty_baton(lock, id);
539
543
 
540
- if (handle != nullptr) {
544
+ if (handle != nullptr && handle->hpc != nullptr) {
541
545
  HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
542
546
  bool fLoadedDll = hLibrary != nullptr;
543
547
  if (fLoadedDll)
@@ -553,6 +557,65 @@ static Napi::Value PtyClear(const Napi::CallbackInfo& info) {
553
557
  return env.Undefined();
554
558
  }
555
559
 
560
+ // The handle is the bytes of an HWND in native byte order, which is how Electron's
561
+ // BrowserWindow.getNativeWindowHandle() returns it.
562
+ static bool TryGetWindowHandle(const Napi::Value& value, HWND* hwnd) {
563
+ if (!value.IsBuffer()) {
564
+ return false;
565
+ }
566
+ const auto buffer = value.As<Napi::Buffer<uint8_t>>();
567
+ if (buffer.Length() != sizeof(HWND)) {
568
+ return false;
569
+ }
570
+ std::memcpy(hwnd, buffer.Data(), sizeof(*hwnd));
571
+ return true;
572
+ }
573
+
574
+ static Napi::Value PtyReparent(const Napi::CallbackInfo& info) {
575
+ Napi::Env env(info.Env());
576
+ Napi::HandleScope scope(env);
577
+
578
+ if (info.Length() != 3 ||
579
+ !info[0].IsNumber() ||
580
+ !info[2].IsBoolean()) {
581
+ throw Napi::Error::New(env, "Usage: pty.reparent(id, hwnd, useConptyDll)");
582
+ }
583
+
584
+ int id = info[0].As<Napi::Number>().Int32Value();
585
+ HWND newParent;
586
+ if (!TryGetWindowHandle(info[1], &newParent)) {
587
+ throw Napi::Error::New(env, "pty.reparent: hwnd must be a Buffer the size of an HWND");
588
+ }
589
+ const bool useConptyDll = info[2].As<Napi::Boolean>().Value();
590
+
591
+ // ConptyReparentPseudoConsole only exists in the bundled conpty.dll.
592
+ if (!useConptyDll) {
593
+ return Napi::Boolean::New(env, false);
594
+ }
595
+
596
+ std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
597
+ const pty_baton* handle = get_pty_baton(lock, id);
598
+
599
+ bool sent = false;
600
+ if (handle != nullptr && handle->hpc != nullptr) {
601
+ HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
602
+ bool fLoadedDll = hLibrary != nullptr;
603
+ if (fLoadedDll)
604
+ {
605
+ PFNREPARENTPSEUDOCONSOLE const pfnReparentPseudoConsole = (PFNREPARENTPSEUDOCONSOLE)GetProcAddress(
606
+ (HMODULE)hLibrary, "ConptyReparentPseudoConsole");
607
+ if (pfnReparentPseudoConsole)
608
+ {
609
+ // Asks ConPTY to make newParent the owner of the console window returned by
610
+ // GetConsoleWindow(); ConPTY applies it asynchronously.
611
+ sent = SUCCEEDED(pfnReparentPseudoConsole(handle->hpc, newParent));
612
+ }
613
+ }
614
+ }
615
+
616
+ return Napi::Boolean::New(env, sent);
617
+ }
618
+
556
619
  static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
557
620
  Napi::Env env(info.Env());
558
621
  Napi::HandleScope scope(env);
@@ -577,9 +640,11 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
577
640
  PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress(
578
641
  (HMODULE)hLibrary,
579
642
  useConptyDll ? "ConptyClosePseudoConsole" : "ClosePseudoConsole");
580
- if (pfnClosePseudoConsole)
643
+ if (pfnClosePseudoConsole && handle->hpc != nullptr)
581
644
  {
582
645
  pfnClosePseudoConsole(handle->hpc);
646
+ // Closing frees the pseudoconsole, but the baton lives until the exit thread erases it.
647
+ handle->hpc = nullptr;
583
648
  }
584
649
  }
585
650
  // Defensive: if PtyConnect was never called (or failed before closing the
@@ -609,6 +674,7 @@ Napi::Object init(Napi::Env env, Napi::Object exports) {
609
674
  exports.Set("connect", Napi::Function::New(env, PtyConnect));
610
675
  exports.Set("resize", Napi::Function::New(env, PtyResize));
611
676
  exports.Set("clear", Napi::Function::New(env, PtyClear));
677
+ exports.Set("reparent", Napi::Function::New(env, PtyReparent));
612
678
  exports.Set("kill", Napi::Function::New(env, PtyKill));
613
679
  return exports;
614
680
  };
@@ -173,6 +173,23 @@ declare module 'node-pty' {
173
173
  */
174
174
  clear(): void;
175
175
 
176
+ /**
177
+ * Sets the handle (HWND) of the top-level window that hosts this terminal. ConPTY makes it the
178
+ * owner of the console window returned by `GetConsoleWindow()`, so windows that console
179
+ * applications parent to it (for example `GetAncestor(GetConsoleWindow(), GA_ROOTOWNER)`, which
180
+ * MSAL/WAM sign-in uses) open on top of the host window instead of behind it. Calling it right
181
+ * after spawning sends the request before the process is launched. Call it again whenever the
182
+ * terminal moves to a different window. Optional so existing implementations of `IPty` keep
183
+ * compiling.
184
+ * @param handle The window handle (HWND) as its bytes in native byte order, the way Electron's
185
+ * `BrowserWindow.getNativeWindowHandle()` returns it. Throws unless it's a Buffer the size of
186
+ * an HWND (8 bytes in a 64-bit process).
187
+ * @returns Whether the request was sent to ConPTY, which applies it asynchronously. This is
188
+ * always false unless on Windows with `useConptyDll`, and false once the pseudoconsole is
189
+ * closed.
190
+ */
191
+ setParentWindow?(handle: Buffer): boolean;
192
+
176
193
  /**
177
194
  * Writes data to the pty.
178
195
  * @param data The data to write.