Files
orca/config/patches/node-pty@1.1.0.patch
T

919 lines
34 KiB
Diff

diff --git a/deps/winpty/src/winpty.gyp b/deps/winpty/src/winpty.gyp
index 1ac5758bedd8cf54f32280dea4e4aeb5afdee30d..6047590bc9558dabacd3ece5114ed41c76e0221f 100644
--- a/deps/winpty/src/winpty.gyp
+++ b/deps/winpty/src/winpty.gyp
@@ -10,7 +10,7 @@
# make -j4 CXX=i686-w64-mingw32-g++ LDFLAGS="-static -static-libgcc -static-libstdc++"
'variables': {
- 'WINPTY_COMMIT_HASH%': '<!(cmd /c "cd shared && GetCommitHash.bat")',
+ 'WINPTY_COMMIT_HASH%': '<!(cmd /c "cd shared && .\\GetCommitHash.bat")',
},
'target_defaults' : {
'defines' : [
@@ -22,7 +22,7 @@
'include_dirs': [
# Add the 'src/gen' directory to the include path and force gyp to
# run the script (re)generating the version header.
- '<!(cmd /c "cd shared && UpdateGenVersion.bat <(WINPTY_COMMIT_HASH)")',
+ '<!(cmd /c "cd shared && .\\UpdateGenVersion.bat <(WINPTY_COMMIT_HASH)")',
]
},
'targets' : [
diff --git a/lib/conpty_console_list_agent.js b/lib/conpty_console_list_agent.js
index 8c4fca9022a6d6f015bca87f61625cde2278f428..0a01730616488119aa21ef441cf3c441e02a974c 100644
--- a/lib/conpty_console_list_agent.js
+++ b/lib/conpty_console_list_agent.js
@@ -10,7 +10,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
var utils_1 = require("./utils");
var getConsoleProcessList = utils_1.loadNativeModule('conpty_console_list').module.getConsoleProcessList;
var shellPid = parseInt(process.argv[2], 10);
-var consoleProcessList = getConsoleProcessList(shellPid);
+var consoleProcessList;
+try {
+ consoleProcessList = getConsoleProcessList(shellPid);
+}
+catch (_a) {
+ // Why: AttachConsole can fail after the shell exits; parent already has this fallback.
+ consoleProcessList = [shellPid];
+}
process.send({ consoleProcessList: consoleProcessList });
process.exit(0);
//# sourceMappingURL=conpty_console_list_agent.js.map
\ No newline at end of file
diff --git a/lib/unixTerminal.js b/lib/unixTerminal.js
index 1ec12f796a822c78fba9ad7f6448c3987e325c23..cec8b67aef02f8199e5606a0d257088bf1865877 100644
--- a/lib/unixTerminal.js
+++ b/lib/unixTerminal.js
@@ -28,8 +28,12 @@ var native = utils_1.loadNativeModule('pty');
var pty = native.module;
var helperPath = native.dir + '/spawn-helper';
helperPath = path.resolve(__dirname, helperPath);
-helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
-helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
+if (!helperPath.includes('app.asar.unpacked')) {
+ helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
+}
+if (!helperPath.includes('node_modules.asar.unpacked')) {
+ helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
+}
var DEFAULT_FILE = 'sh';
var DEFAULT_NAME = 'xterm';
var DESTROY_SOCKET_TIMEOUT_MS = 200;
diff --git a/lib/windowsPtyAgent.js b/lib/windowsPtyAgent.js
index a358ffb177357e177661033c1b092f9c9d0e5f5a..b34cfef73fcdcf7897b6f4646ec464f348311330 100644
--- a/lib/windowsPtyAgent.js
+++ b/lib/windowsPtyAgent.js
@@ -26,14 +26,17 @@ var FLUSH_DATA_INTERVAL = 1000;
* and winpty.
*/
var WindowsPtyAgent = /** @class */ (function () {
- function WindowsPtyAgent(file, args, env, cwd, cols, rows, debug, _useConpty, _useConptyDll, conptyInheritCursor) {
+ function WindowsPtyAgent(file, args, env, cwd, cols, rows, debug, _useConpty, _useConptyDll, conptyInheritCursor, windowsAgentJob) {
var _this = this;
if (_useConptyDll === void 0) { _useConptyDll = false; }
if (conptyInheritCursor === void 0) { conptyInheritCursor = false; }
+ if (windowsAgentJob === void 0) { windowsAgentJob = false; }
this._useConpty = _useConpty;
this._useConptyDll = _useConptyDll;
this._pid = 0;
this._innerPid = 0;
+ this._processExited = false;
+ this._processExitListeners = [];
if (this._useConpty === undefined || this._useConpty === true) {
this._useConpty = this._getWindowsBuildNumber() >= 18309;
}
@@ -86,7 +89,7 @@ var WindowsPtyAgent = /** @class */ (function () {
});
this._inSocket.setEncoding('utf8');
if (this._useConpty) {
- var connect = this._ptyNative.connect(this._pty, commandLine, cwd, env, this._useConptyDll, function (c) { return _this._$onProcessExit(c); });
+ var connect = this._ptyNative.connect(this._pty, commandLine, cwd, env, this._useConptyDll, windowsAgentJob, function (c) { return _this._$onProcessExit(c); });
this._innerPid = connect.pid;
}
}
@@ -130,6 +133,20 @@ var WindowsPtyAgent = /** @class */ (function () {
this._ptyNative.clear(this._pty, this._useConptyDll);
}
};
+ WindowsPtyAgent.prototype.terminateJobTree = function (timeoutMs) {
+ if (!this._useConpty) {
+ return undefined;
+ }
+ var _a;
+ return (_a = this._ptyNative.terminateJobTree) === null || _a === void 0 ? void 0 : _a.call(this._ptyNative, this._pty, timeoutMs);
+ };
+ WindowsPtyAgent.prototype.onProcessExit = function (listener) {
+ if (this._processExited) {
+ listener();
+ return;
+ }
+ this._processExitListeners.push(listener);
+ };
WindowsPtyAgent.prototype.kill = function () {
var _this = this;
// Tell the agent to kill the pty, this releases handles to the process
@@ -221,6 +238,10 @@ var WindowsPtyAgent = /** @class */ (function () {
WindowsPtyAgent.prototype._$onProcessExit = function (exitCode) {
var _this = this;
this._exitCode = exitCode;
+ this._processExited = true;
+ var listeners = this._processExitListeners;
+ this._processExitListeners = [];
+ listeners.forEach(function (listener) { return listener(); });
if (!this._useConptyDll) {
this._flushDataAndCleanUp();
this._outSocket.on('data', function () { return _this._flushDataAndCleanUp(); });
diff --git a/lib/windowsTerminal.js b/lib/windowsTerminal.js
index 3c38f89d14cece8081782a9600ebc582b7c2a62a..7a488cbd891e0227b367857af894b4818a7a6031 100644
--- a/lib/windowsTerminal.js
+++ b/lib/windowsTerminal.js
@@ -48,8 +48,23 @@ var WindowsTerminal = /** @class */ (function (_super) {
// Functions that need to run after `ready` event is emitted.
_this._deferreds = [];
// Create new termal.
- _this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
+ _this._agent = new windowsPtyAgent_1.WindowsPtyAgent(file, args, parsedEnv, cwd, _this._cols, _this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor, opt.windowsAgentJob);
_this._socket = _this._agent.outSocket;
+ var socketClosed = false;
+ var agentProcessExited = false;
+ var exitEmitted = false;
+ var emitExitWhenSafe = function () {
+ if (exitEmitted || !socketClosed || (opt.windowsAgentJob && !agentProcessExited)) {
+ return;
+ }
+ exitEmitted = true;
+ _this.emit('exit', _this._agent.exitCode);
+ _this._close();
+ };
+ _this._agent.onProcessExit(function () {
+ agentProcessExited = true;
+ emitExitWhenSafe();
+ });
// Not available until `ready` event emitted.
_this._pid = _this._agent.innerPid;
_this._fd = _this._agent.fd;
@@ -95,8 +110,10 @@ var WindowsTerminal = /** @class */ (function (_super) {
});
// Cleanup after the socket is closed.
_this._socket.on('close', function () {
- _this.emit('exit', _this._agent.exitCode);
- _this._close();
+ // Why: a Job-owned root can close ConPTY output before the native exit
+ // callback proves every descendant is gone; public exit needs both facts.
+ socketClosed = true;
+ emitExitWhenSafe();
});
});
_this._file = file;
@@ -144,6 +161,9 @@ var WindowsTerminal = /** @class */ (function (_super) {
_this.kill();
});
};
+ WindowsTerminal.prototype.terminateJobTree = function (timeoutMs) {
+ return this._agent.terminateJobTree(timeoutMs);
+ };
WindowsTerminal.prototype.kill = function (signal) {
var _this = this;
this._deferNoArgs(function () {
diff --git a/src/conpty_console_list_agent.ts b/src/conpty_console_list_agent.ts
index 181ccabbbe9c4948a9725fb1db907a68e9de01fc..67f31facf85562b67adbfbd04ce28ddd8eeb4a79 100644
--- a/src/conpty_console_list_agent.ts
+++ b/src/conpty_console_list_agent.ts
@@ -10,6 +10,12 @@ import { loadNativeModule } from './utils';
const getConsoleProcessList = loadNativeModule('conpty_console_list').module.getConsoleProcessList;
const shellPid = parseInt(process.argv[2], 10);
-const consoleProcessList = getConsoleProcessList(shellPid);
+let consoleProcessList: number[];
+try {
+ consoleProcessList = getConsoleProcessList(shellPid);
+} catch {
+ // Why: AttachConsole can fail after the shell exits; parent already has this fallback.
+ consoleProcessList = [shellPid];
+}
process.send!({ consoleProcessList });
process.exit(0);
diff --git a/src/interfaces.ts b/src/interfaces.ts
index a269e77bd2acf31c762b0fd8e13e6bf435fd7d07..adc2e8d10190de6bbeea1f2b53320ef92e0b2935 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -121,6 +121,8 @@ export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
useConpty?: boolean;
useConptyDll?: boolean;
conptyInheritCursor?: boolean;
+ /** Orca-specific: own this ConPTY and all descendants in a kill-on-close Job. */
+ windowsAgentJob?: boolean;
}
export interface IPtyOpenOptions {
diff --git a/src/native.d.ts b/src/native.d.ts
index c53e086b4968d6fb0bf601ace3a813809433fedb..ce2d197bbf618f17cdcbf1e6127e831715a8beb3 100644
--- a/src/native.d.ts
+++ b/src/native.d.ts
@@ -4,10 +4,11 @@
interface IConptyNative {
startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, conptyInheritCursor: boolean, useConptyDll: boolean): IConptyProcess;
- connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number };
+ connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, windowsAgentJob: boolean, onExitCallback: (exitCode: number) => void): { pid: number };
resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void;
clear(ptyId: number, useConptyDll: boolean): void;
kill(ptyId: number, useConptyDll: boolean): void;
+ terminateJobTree?(ptyId: number, timeoutMs: number): Promise<boolean> | undefined;
}
interface IWinptyNative {
diff --git a/src/unix/pty.cc b/src/unix/pty.cc
index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..61f39f0cbb91faa2c515f35d2ca850564e6368d9 100644
--- a/src/unix/pty.cc
+++ b/src/unix/pty.cc
@@ -23,7 +23,9 @@
#include <errno.h>
#include <string.h>
#include <stdlib.h>
+#include <stdio.h>
#include <unistd.h>
+#include <string>
#include <thread>
#include <sys/types.h>
@@ -237,13 +239,23 @@ pty_getproc(int, char *);
#endif
#if defined(__APPLE__) || defined(__OpenBSD__)
+struct pty_spawn_error {
+ const char* step;
+ int errnum;
+ std::string detail_name;
+ std::string detail_value;
+};
+
+static std::string
+pty_format_spawn_error(const pty_spawn_error&);
+
static void
pty_posix_spawn(char** argv, char** env,
const struct termios *termp,
const struct winsize *winp,
int* master,
pid_t* pid,
- int* err);
+ pty_spawn_error* err);
#endif
struct DelBuf {
@@ -367,10 +379,11 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
argv[i + 3] = strdup(arg.c_str());
}
- int err = -1;
- pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err);
- if (err != 0) {
- throw Napi::Error::New(napiEnv, "posix_spawnp failed.");
+ pty_spawn_error spawn_error = { NULL, 0, "", "" };
+ pty_posix_spawn(argv, env, term, &winp, &master, &pid, &spawn_error);
+ if (spawn_error.errnum != 0) {
+ std::string spawn_message = pty_format_spawn_error(spawn_error);
+ throw Napi::Error::New(napiEnv, spawn_message);
}
if (pty_nonblock(master) == -1) {
throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
@@ -684,15 +697,73 @@ pty_getproc(int fd, char *tty) {
#endif
#if defined(__APPLE__)
+static const char*
+pty_errno_name(int errnum) {
+ switch (errnum) {
+ case E2BIG: return "E2BIG";
+ case EACCES: return "EACCES";
+ case EAGAIN: return "EAGAIN";
+ case EMFILE: return "EMFILE";
+ case ENFILE: return "ENFILE";
+ case ENOENT: return "ENOENT";
+ case ENOMEM: return "ENOMEM";
+ default: return "errno";
+ }
+}
+
+static void
+pty_set_spawn_error(pty_spawn_error* err,
+ const char* step,
+ int errnum,
+ const char* detail_name = NULL,
+ const char* detail_value = NULL) {
+ err->step = step;
+ err->errnum = errnum;
+ err->detail_name = detail_name ? detail_name : "";
+ err->detail_value = detail_value ? detail_value : "";
+}
+
+static std::string
+pty_format_spawn_error(const pty_spawn_error& err) {
+ char errno_buf[64];
+ snprintf(errno_buf, sizeof(errno_buf), "%d", err.errnum);
+
+ std::string message = "node-pty: ";
+ message += err.step ? err.step : "unknown";
+ message += " failed: ";
+ message += pty_errno_name(err.errnum);
+ message += " (errno ";
+ message += errno_buf;
+ message += ", ";
+ message += strerror(err.errnum);
+ message += ")";
+
+ if (!err.detail_name.empty()) {
+ message += " - ";
+ message += err.detail_name;
+ message += "='";
+ message += err.detail_value;
+ message += "'";
+ }
+
+ return message;
+}
+
static void
pty_posix_spawn(char** argv, char** env,
const struct termios *termp,
const struct winsize *winp,
int* master,
pid_t* pid,
- int* err) {
- int low_fds[3];
+ pty_spawn_error* err) {
+ int low_fds[3] = {-1, -1, -1};
size_t count = 0;
+ int res = -1;
+ int slave = -1;
+ posix_spawn_file_actions_t acts;
+ bool acts_initialized = false;
+ posix_spawnattr_t attrs;
+ bool attrs_initialized = false;
for (; count < 3; count++) {
low_fds[count] = posix_openpt(O_RDWR);
@@ -706,80 +777,118 @@ pty_posix_spawn(char** argv, char** env,
POSIX_SPAWN_SETSID;
*master = posix_openpt(O_RDWR);
if (*master == -1) {
- return;
+ pty_set_spawn_error(err, "posix_openpt", errno);
+ goto done;
+ }
+
+ res = grantpt(*master);
+ if (res == -1) {
+ pty_set_spawn_error(err, "grantpt", errno);
+ goto done;
}
- int res = grantpt(*master) || unlockpt(*master);
+ res = unlockpt(*master);
if (res == -1) {
- return;
+ pty_set_spawn_error(err, "unlockpt", errno);
+ goto done;
}
// Use TIOCPTYGNAME instead of ptsname() to avoid threading problems.
- int slave;
char slave_pty_name[128];
res = ioctl(*master, TIOCPTYGNAME, slave_pty_name);
if (res == -1) {
- return;
+ pty_set_spawn_error(err, "ioctl_TIOCPTYGNAME", errno);
+ goto done;
}
slave = open(slave_pty_name, O_RDWR | O_NOCTTY);
if (slave == -1) {
- return;
+ pty_set_spawn_error(err, "open_slave", errno, "slave", slave_pty_name);
+ goto done;
}
if (termp) {
res = tcsetattr(slave, TCSANOW, termp);
if (res == -1) {
- return;
+ pty_set_spawn_error(err, "tcsetattr", errno, "slave", slave_pty_name);
+ goto done;
};
}
if (winp) {
res = ioctl(slave, TIOCSWINSZ, winp);
if (res == -1) {
- return;
+ pty_set_spawn_error(err, "ioctl_TIOCSWINSZ", errno, "slave", slave_pty_name);
+ goto done;
}
}
- posix_spawn_file_actions_t acts;
- posix_spawn_file_actions_init(&acts);
+ res = posix_spawn_file_actions_init(&acts);
+ if (res != 0) {
+ pty_set_spawn_error(err, "posix_spawn_file_actions_init", res);
+ goto done;
+ }
+ acts_initialized = true;
posix_spawn_file_actions_adddup2(&acts, slave, STDIN_FILENO);
posix_spawn_file_actions_adddup2(&acts, slave, STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&acts, slave, STDERR_FILENO);
posix_spawn_file_actions_addclose(&acts, slave);
posix_spawn_file_actions_addclose(&acts, *master);
- posix_spawnattr_t attrs;
- posix_spawnattr_init(&attrs);
- *err = posix_spawnattr_setflags(&attrs, flags);
- if (*err != 0) {
+ res = posix_spawnattr_init(&attrs);
+ if (res != 0) {
+ pty_set_spawn_error(err, "posix_spawnattr_init", res);
+ goto done;
+ }
+ attrs_initialized = true;
+ res = posix_spawnattr_setflags(&attrs, flags);
+ if (res != 0) {
+ pty_set_spawn_error(err, "posix_spawnattr_setflags", res);
goto done;
}
sigset_t signal_set;
/* Reset all signal the child to their default behavior */
sigfillset(&signal_set);
- *err = posix_spawnattr_setsigdefault(&attrs, &signal_set);
- if (*err != 0) {
+ res = posix_spawnattr_setsigdefault(&attrs, &signal_set);
+ if (res != 0) {
+ pty_set_spawn_error(err, "posix_spawnattr_setsigdefault", res);
goto done;
}
/* Reset the signal mask for all signals */
sigemptyset(&signal_set);
- *err = posix_spawnattr_setsigmask(&attrs, &signal_set);
- if (*err != 0) {
+ res = posix_spawnattr_setsigmask(&attrs, &signal_set);
+ if (res != 0) {
+ pty_set_spawn_error(err, "posix_spawnattr_setsigmask", res);
goto done;
}
do
- *err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env);
- while (*err == EINTR);
+ res = posix_spawn(pid, argv[0], &acts, &attrs, argv, env);
+ while (res == EINTR);
+ if (res != 0) {
+ pty_set_spawn_error(err, "posix_spawn", res, "helper", argv[0]);
+ }
done:
- posix_spawn_file_actions_destroy(&acts);
- posix_spawnattr_destroy(&attrs);
+ if (acts_initialized) {
+ posix_spawn_file_actions_destroy(&acts);
+ }
+ if (attrs_initialized) {
+ posix_spawnattr_destroy(&attrs);
+ }
+ if (slave != -1) {
+ close(slave);
+ }
+ if (err->errnum != 0 && *master != -1) {
+ close(*master);
+ *master = -1;
+ }
- for (; count > 0; count--) {
- close(low_fds[count]);
+ for (size_t i = 0; i <= count && i < 3; i++) {
+ if (low_fds[i] != -1) {
+ close(low_fds[i]);
+ }
}
}
#endif
diff --git a/src/win/conpty.cc b/src/win/conpty.cc
index 7b286d3d644c26141df516929703aa6e129df4b2..16f8d99c627159a35cb643a2752ded3094399230 100644
--- a/src/win/conpty.cc
+++ b/src/win/conpty.cc
@@ -45,6 +45,7 @@ struct pty_baton {
HPCON hpc;
HANDLE hShell;
+ HANDLE hJob = nullptr;
pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {};
};
@@ -91,7 +92,18 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) {
delete th;
});
*th = std::thread([tsfn = std::move(tsfn), baton] {
- auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) {
+ auto callback = [baton](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) {
+ if (baton->hShell != nullptr) {
+ GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code));
+ CloseHandle(baton->hShell);
+ baton->hShell = nullptr;
+ }
+ HANDLE hJob = baton->hJob;
+ baton->hJob = nullptr;
+ assert(remove_pty_baton(baton->id));
+ if (hJob != nullptr) {
+ CloseHandle(hJob);
+ }
cb.Call({Napi::Number::New(env, exit_event->exit_code)});
delete exit_event;
};
@@ -99,15 +111,41 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) {
ExitEvent *exit_event = new ExitEvent;
// Wait for process to complete.
WaitForSingleObject(baton->hShell, INFINITE);
- // Get process exit code.
- GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code));
- // Clean up handles
- CloseHandle(baton->hShell);
- assert(remove_pty_baton(baton->id));
+
+ if (baton->hJob != nullptr) {
+ // Why: an agent root can exit before its detached tools. Do not publish
+ // PTY exit until Windows proves every member has released its handles.
+ TerminateJobObject(baton->hJob, 1);
+ while (true) {
+ JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting{};
+ if (QueryInformationJobObject(
+ baton->hJob,
+ JobObjectBasicAccountingInformation,
+ &accounting,
+ sizeof(accounting),
+ nullptr) && accounting.ActiveProcesses == 0) {
+ break;
+ }
+ // A permanent query/termination failure deliberately keeps the PTY
+ // owned and un-reaped so destructive callers cannot race deletion.
+ Sleep(25);
+ }
+ }
auto status = tsfn.BlockingCall(exit_event, callback); // In main thread
switch (status) {
case napi_closing:
+ // The environment no longer accepts JS calls, so no native method can
+ // race these per-baton handles during finalization.
+ if (baton->hShell != nullptr) {
+ CloseHandle(baton->hShell);
+ baton->hShell = nullptr;
+ }
+ if (baton->hJob != nullptr) {
+ CloseHandle(baton->hJob);
+ baton->hJob = nullptr;
+ }
+ delete exit_event;
break;
case napi_queue_full:
@@ -333,14 +371,15 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
std::stringstream errorText;
BOOL fSuccess = FALSE;
- if (info.Length() != 6 ||
+ if (info.Length() != 7 ||
!info[0].IsNumber() ||
!info[1].IsString() ||
!info[2].IsString() ||
!info[3].IsArray() ||
!info[4].IsBoolean() ||
- !info[5].IsFunction()) {
- throw Napi::Error::New(env, "Usage: pty.connect(id, cmdline, cwd, env, useConptyDll, exitCallback)");
+ !info[5].IsBoolean() ||
+ !info[6].IsFunction()) {
+ throw Napi::Error::New(env, "Usage: pty.connect(id, cmdline, cwd, env, useConptyDll, windowsAgentJob, exitCallback)");
}
const int id = info[0].As<Napi::Number>().Int32Value();
@@ -348,7 +387,8 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
const std::wstring cwd(path_util::to_wstring(info[2].As<Napi::String>()));
const Napi::Array envValues = info[3].As<Napi::Array>();
const bool useConptyDll = info[4].As<Napi::Boolean>().Value();
- Napi::Function exitCallback = info[5].As<Napi::Function>();
+ const bool windowsAgentJob = info[5].As<Napi::Boolean>().Value();
+ Napi::Function exitCallback = info[6].As<Napi::Function>();
// Fetch pty handle from ID and start process
pty_baton* handle = get_pty_baton(id);
@@ -416,7 +456,8 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
nullptr, // lpProcessAttributes
nullptr, // lpThreadAttributes
false, // bInheritHandles VERY IMPORTANT that this is false
- EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags
+ EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT |
+ (windowsAgentJob ? CREATE_SUSPENDED : 0), // dwCreationFlags
envArg, // lpEnvironment
mutableCwd.get(), // lpCurrentDirectory
&siEx.StartupInfo, // lpStartupInfo
@@ -426,6 +467,39 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
throw errorWithCode(info, "Cannot create process");
}
+ if (windowsAgentJob) {
+ // Job setup is atomic while the shell is suspended; partial ownership is unsafe.
+ HANDLE hJob = CreateJobObjectW(nullptr, nullptr);
+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
+ limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
+ if (hJob == nullptr ||
+ !SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &limits, sizeof(limits)) ||
+ !AssignProcessToJobObject(hJob, piClient.hProcess)) {
+ const DWORD jobError = GetLastError();
+ TerminateProcess(piClient.hProcess, 1);
+ WaitForSingleObject(piClient.hProcess, INFINITE);
+ CloseHandle(piClient.hThread); CloseHandle(piClient.hProcess);
+ if (hJob != nullptr) CloseHandle(hJob);
+ SetLastError(jobError);
+ throw errorWithCode(info, "Cannot assign process to Job");
+ }
+ handle->hJob = hJob;
+
+ if (ResumeThread(piClient.hThread) == static_cast<DWORD>(-1)) {
+ const DWORD resumeError = GetLastError();
+ TerminateProcess(piClient.hProcess, 1);
+ WaitForSingleObject(piClient.hProcess, INFINITE);
+ CloseHandle(piClient.hThread);
+ CloseHandle(piClient.hProcess);
+ if (handle->hJob != nullptr) {
+ CloseHandle(handle->hJob);
+ handle->hJob = nullptr;
+ }
+ SetLastError(resumeError);
+ throw errorWithCode(info, "Cannot resume process");
+ }
+ }
+
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
bool fLoadedDll = hLibrary != nullptr;
if (useConptyDll && fLoadedDll)
@@ -567,6 +641,109 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
return env.Undefined();
}
+struct JobTerminationResult {
+ bool drained = false;
+};
+
+static Napi::Value PtyTerminateJobTree(const Napi::CallbackInfo& info) {
+ Napi::Env env(info.Env());
+ Napi::HandleScope scope(env);
+
+ if (info.Length() != 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
+ throw Napi::Error::New(env, "Usage: pty.terminateJobTree(id, timeoutMs)");
+ }
+
+ const int id = info[0].As<Napi::Number>().Int32Value();
+ const pty_baton* handle = get_pty_baton(id);
+ if (handle == nullptr || handle->hJob == nullptr) {
+ return env.Undefined();
+ }
+
+ const double timeoutValue = info[1].As<Napi::Number>().DoubleValue();
+ if (timeoutValue < 1 || timeoutValue > 60000) {
+ throw Napi::RangeError::New(env, "timeoutMs must be between 1 and 60000");
+ }
+
+ HANDLE hJob = nullptr;
+ if (!DuplicateHandle(
+ GetCurrentProcess(),
+ handle->hJob,
+ GetCurrentProcess(),
+ &hJob,
+ 0,
+ FALSE,
+ DUPLICATE_SAME_ACCESS)) {
+ return env.Undefined();
+ }
+ if (!TerminateJobObject(hJob, 1)) {
+ CloseHandle(hJob);
+ return env.Undefined();
+ }
+
+ // Why: termination is asynchronous. Keep a duplicate Job handle until the
+ // OS reports no members instead of treating the root process exit as proof.
+ auto deferred = Napi::Promise::Deferred::New(env);
+ std::thread *th = new std::thread;
+ auto tsfn = Napi::ThreadSafeFunction::New(
+ env,
+ Napi::Function::New(env, [](const Napi::CallbackInfo&) {}),
+ "PtyTerminateJobTree_resource",
+ 0,
+ 1,
+ [th](Napi::Env) {
+ th->join();
+ delete th;
+ });
+ const DWORD timeoutMs = static_cast<DWORD>(timeoutValue);
+ *th = std::thread([tsfn = std::move(tsfn), deferred, hJob, timeoutMs] {
+ auto *result = new JobTerminationResult;
+ const ULONGLONG startedAt = GetTickCount64();
+ while (true) {
+ JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting{};
+ if (!QueryInformationJobObject(
+ hJob,
+ JobObjectBasicAccountingInformation,
+ &accounting,
+ sizeof(accounting),
+ nullptr)) {
+ break;
+ }
+ if (accounting.ActiveProcesses == 0) {
+ result->drained = true;
+ break;
+ }
+ if (GetTickCount64() - startedAt >= timeoutMs) {
+ break;
+ }
+ Sleep(25);
+ }
+ CloseHandle(hJob);
+
+ const auto status = tsfn.BlockingCall(
+ result,
+ [deferred](Napi::Env env, Napi::Function, JobTerminationResult *result) {
+ deferred.Resolve(Napi::Boolean::New(env, result->drained));
+ delete result;
+ });
+ switch (status) {
+ case napi_closing:
+ delete result;
+ break;
+ case napi_queue_full:
+ Napi::Error::Fatal("PtyTerminateJobTree", "Queue was full");
+ case napi_ok:
+ if (tsfn.Release() != napi_ok) {
+ Napi::Error::Fatal("PtyTerminateJobTree", "ThreadSafeFunction.Release() failed");
+ }
+ break;
+ default:
+ Napi::Error::Fatal("PtyTerminateJobTree", "ThreadSafeFunction.BlockingCall() failed");
+ }
+ });
+
+ return deferred.Promise();
+}
+
/**
* Init
*/
@@ -577,6 +754,7 @@ Napi::Object init(Napi::Env env, Napi::Object exports) {
exports.Set("resize", Napi::Function::New(env, PtyResize));
exports.Set("clear", Napi::Function::New(env, PtyClear));
exports.Set("kill", Napi::Function::New(env, PtyKill));
+ exports.Set("terminateJobTree", Napi::Function::New(env, PtyTerminateJobTree));
return exports;
};
diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts
index d7054449516f0c9a62af351c2caa17331206d530..5cc9914ee1f1b8b40dadfb8164763357f5549300 100644
--- a/src/windowsPtyAgent.ts
+++ b/src/windowsPtyAgent.ts
@@ -34,6 +34,8 @@ export class WindowsPtyAgent {
private _innerPid: number = 0;
private _closeTimeout: NodeJS.Timer | undefined;
private _exitCode: number | undefined;
+ private _processExited: boolean = false;
+ private _processExitListeners: (() => void)[] = [];
private _conoutSocketWorker: ConoutConnection;
private _fd: any;
@@ -56,7 +58,8 @@ export class WindowsPtyAgent {
debug: boolean,
private _useConpty: boolean | undefined,
private _useConptyDll: boolean = false,
- conptyInheritCursor: boolean = false
+ conptyInheritCursor: boolean = false,
+ windowsAgentJob: boolean = false
) {
if (this._useConpty === undefined || this._useConpty === true) {
this._useConpty = this._getWindowsBuildNumber() >= 18309;
@@ -116,7 +119,7 @@ export class WindowsPtyAgent {
this._inSocket.setEncoding('utf8');
if (this._useConpty) {
- const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, this._useConptyDll, c => this._$onProcessExit(c));
+ const connect = (this._ptyNative as IConptyNative).connect(this._pty, commandLine, cwd, env, this._useConptyDll, windowsAgentJob, c => this._$onProcessExit(c));
this._innerPid = connect.pid;
}
}
@@ -138,6 +141,21 @@ export class WindowsPtyAgent {
}
}
+ public terminateJobTree(timeoutMs: number): Promise<boolean> | undefined {
+ if (!this._useConpty) {
+ return undefined;
+ }
+ return (this._ptyNative as IConptyNative).terminateJobTree?.(this._pty, timeoutMs);
+ }
+
+ public onProcessExit(listener: () => void): void {
+ if (this._processExited) {
+ listener();
+ return;
+ }
+ this._processExitListeners.push(listener);
+ }
+
public kill(): void {
// Tell the agent to kill the pty, this releases handles to the process
if (this._useConpty) {
@@ -223,6 +241,10 @@ export class WindowsPtyAgent {
*/
private _$onProcessExit(exitCode: number): void {
this._exitCode = exitCode;
+ this._processExited = true;
+ const listeners = this._processExitListeners;
+ this._processExitListeners = [];
+ listeners.forEach(listener => listener());
if (!this._useConptyDll) {
this._flushDataAndCleanUp();
this._outSocket.on('data', () => this._flushDataAndCleanUp());
diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts
index 13f6c6dbb3d7965c4699568f4704be3b6a33d947..23f2f2d5b7f95df60a22adad9c4d7cbdc1a3dd5f 100644
--- a/src/windowsTerminal.ts
+++ b/src/windowsTerminal.ts
@@ -48,8 +48,23 @@ export class WindowsTerminal extends Terminal {
this._deferreds = [];
// Create new termal.
- this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor);
+ this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConpty, opt.useConptyDll, opt.conptyInheritCursor, opt.windowsAgentJob);
this._socket = this._agent.outSocket;
+ let socketClosed = false;
+ let agentProcessExited = false;
+ let exitEmitted = false;
+ const emitExitWhenSafe = (): void => {
+ if (exitEmitted || !socketClosed || (opt.windowsAgentJob && !agentProcessExited)) {
+ return;
+ }
+ exitEmitted = true;
+ this.emit('exit', this._agent.exitCode);
+ this._close();
+ };
+ this._agent.onProcessExit(() => {
+ agentProcessExited = true;
+ emitExitWhenSafe();
+ });
// Not available until `ready` event emitted.
this._pid = this._agent.innerPid;
@@ -103,8 +118,10 @@ export class WindowsTerminal extends Terminal {
// Cleanup after the socket is closed.
this._socket.on('close', () => {
- this.emit('exit', this._agent.exitCode);
- this._close();
+ // Why: a Job-owned root can close ConPTY output before the native exit
+ // callback proves every descendant is gone; public exit needs both facts.
+ socketClosed = true;
+ emitExitWhenSafe();
});
});
@@ -161,6 +178,10 @@ export class WindowsTerminal extends Terminal {
});
}
+ public terminateJobTree(timeoutMs: number): Promise<boolean> | undefined {
+ return this._agent.terminateJobTree(timeoutMs);
+ }
+
public kill(signal?: string): void {
this._deferNoArgs(() => {
if (signal) {
diff --git a/typings/node-pty.d.ts b/typings/node-pty.d.ts
index 6f050ff1d97b8d3477be7ee95d2af516bd9517b7..b26831f60d2aba6cc352fad561cd0c3f7b9bfe02 100644
--- a/typings/node-pty.d.ts
+++ b/typings/node-pty.d.ts
@@ -108,6 +108,9 @@ declare module 'node-pty' {
* @see https://docs.microsoft.com/en-us/windows/console/createpseudoconsole
*/
conptyInheritCursor?: boolean;
+
+ /** Orca-specific: own this ConPTY and all descendants in a kill-on-close Job. */
+ windowsAgentJob?: boolean;
}
/**
@@ -183,6 +186,9 @@ declare module 'node-pty' {
*/
kill(signal?: string): void;
+ /** Terminates and waits for the Windows ConPTY Job Object to drain. */
+ terminateJobTree?(timeoutMs: number): Promise<boolean> | undefined;
+
/**
* Pauses the pty for customizable flow control.
*/