mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
1081 lines
41 KiB
Diff
1081 lines
41 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..0b71eba1d0fdb312bd46e5f47c218bd5330f023a 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..24cbb71bce12f8e6ec617918df1e16bafd6adff1 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..187d72f22bb9ee8dbf444465d0febb302be4aa54 100644
|
|
--- a/lib/windowsPtyAgent.js
|
|
+++ b/lib/windowsPtyAgent.js
|
|
@@ -26,17 +26,23 @@ 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._windowsAgentJob = false;
|
|
+ this._processExitListeners = [];
|
|
if (this._useConpty === undefined || this._useConpty === true) {
|
|
this._useConpty = this._getWindowsBuildNumber() >= 18309;
|
|
}
|
|
+ // Why: a requested Job is effective only when runtime detection actually selects ConPTY.
|
|
+ this._windowsAgentJob = Boolean(this._useConpty && windowsAgentJob);
|
|
if (this._useConpty) {
|
|
if (!conptyNative) {
|
|
conptyNative = utils_1.loadNativeModule('conpty').module;
|
|
@@ -86,7 +92,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, this._windowsAgentJob, function (c) { return _this._$onProcessExit(c); });
|
|
this._innerPid = connect.pid;
|
|
}
|
|
}
|
|
@@ -115,6 +121,11 @@ var WindowsPtyAgent = /** @class */ (function () {
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
+ Object.defineProperty(WindowsPtyAgent.prototype, "windowsAgentJob", {
|
|
+ get: function () { return this._windowsAgentJob; },
|
|
+ enumerable: false,
|
|
+ configurable: true
|
|
+ });
|
|
WindowsPtyAgent.prototype.resize = function (cols, rows) {
|
|
if (this._useConpty) {
|
|
if (this._exitCode !== undefined) {
|
|
@@ -130,6 +141,23 @@ var WindowsPtyAgent = /** @class */ (function () {
|
|
this._ptyNative.clear(this._pty, this._useConptyDll);
|
|
}
|
|
};
|
|
+ WindowsPtyAgent.prototype.terminateJobTree = function (timeoutMs) {
|
|
+ var _a, _b;
|
|
+ if (!this._windowsAgentJob) {
|
|
+ return undefined;
|
|
+ }
|
|
+ if (this._processExited) {
|
|
+ return Promise.resolve(true);
|
|
+ }
|
|
+ return (_b = (_a = this._ptyNative).terminateJobTree) === null || _b === void 0 ? void 0 : _b.call(_a, 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 +249,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/windowsPtyAgent.test.js b/lib/windowsPtyAgent.test.js
|
|
index 15bbf5ba397ab02a5ec6a11b1c0c6f6ffd137be0..d9a0cf1af2da1cdd56ff6edae0bfa41580b51c60 100644
|
|
--- a/lib/windowsPtyAgent.test.js
|
|
+++ b/lib/windowsPtyAgent.test.js
|
|
@@ -10,6 +10,26 @@ function check(file, args, expected) {
|
|
assert.equal(windowsPtyAgent_1.argsToCommandLine(file, args), expected);
|
|
}
|
|
if (process.platform === 'win32') {
|
|
+ describe('Windows agent Job control', function () {
|
|
+ it('reports an already-exited owned Job as drained without another native lookup', function () {
|
|
+ var terminateJobTree = function () { return Promise.resolve(false); };
|
|
+ var agent = Object.create(windowsPtyAgent_1.WindowsPtyAgent.prototype);
|
|
+ agent._windowsAgentJob = true;
|
|
+ agent._processExited = true;
|
|
+ agent._pty = 1;
|
|
+ agent._ptyNative = { terminateJobTree: terminateJobTree };
|
|
+ assert.strictEqual(agent.windowsAgentJob, true);
|
|
+ return agent.terminateJobTree(1000).then(function (drained) {
|
|
+ assert.strictEqual(drained, true);
|
|
+ });
|
|
+ });
|
|
+ it('does not expose teardown when Job ownership was not established', function () {
|
|
+ var agent = Object.create(windowsPtyAgent_1.WindowsPtyAgent.prototype);
|
|
+ agent._windowsAgentJob = false;
|
|
+ assert.strictEqual(agent.windowsAgentJob, false);
|
|
+ assert.strictEqual(agent.terminateJobTree(1000), undefined);
|
|
+ });
|
|
+ });
|
|
describe('argsToCommandLine', function () {
|
|
describe('Plain strings', function () {
|
|
it('doesn\'t quote plain string', function () {
|
|
diff --git a/lib/windowsTerminal.js b/lib/windowsTerminal.js
|
|
index 3c38f89d14cece8081782a9600ebc582b7c2a62a..52e47c98194daf6f64b22fcaa647077acf989371 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 || (_this._agent.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/lib/windowsTerminal.test.js b/lib/windowsTerminal.test.js
|
|
index af5f343d17f012f330fe2292083f297768ff3b5e..6b558c789fc77b58c20cd9564d03e93e54164f54 100644
|
|
--- a/lib/windowsTerminal.test.js
|
|
+++ b/lib/windowsTerminal.test.js
|
|
@@ -88,6 +88,19 @@ function pollForProcessTreeSize(pid, size, intervalMs, timeoutMs) {
|
|
});
|
|
}
|
|
if (process.platform === 'win32') {
|
|
+ describe('WindowsTerminal Job capability fallback', function () {
|
|
+ it('does not wait for a Job callback when a requested Job falls back to winpty', function (done) {
|
|
+ this.timeout(10000);
|
|
+ var term = new windowsTerminal_1.WindowsTerminal('cmd.exe', '/C exit', {
|
|
+ useConpty: false,
|
|
+ windowsAgentJob: true
|
|
+ });
|
|
+ term.on('exit', function (code) {
|
|
+ assert.strictEqual(code, 0);
|
|
+ done();
|
|
+ });
|
|
+ });
|
|
+ });
|
|
[[false, false], [true, false], [true, true]].forEach(function (_a) {
|
|
var useConpty = _a[0], useConptyDll = _a[1];
|
|
describe("WindowsTerminal (useConpty = " + useConpty + ", useConptyDll = " + useConptyDll + ")", 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.test.ts b/src/windowsPtyAgent.test.ts
|
|
index dc2104b34fd396722a9e30821ff0b7dea18b4295..23510c3d7c85ecc911b94719832abe5f2b90c1e3 100644
|
|
--- a/src/windowsPtyAgent.test.ts
|
|
+++ b/src/windowsPtyAgent.test.ts
|
|
@@ -4,13 +4,37 @@
|
|
*/
|
|
|
|
import * as assert from 'assert';
|
|
-import { argsToCommandLine } from './windowsPtyAgent';
|
|
+import { argsToCommandLine, WindowsPtyAgent } from './windowsPtyAgent';
|
|
|
|
function check(file: string, args: string | string[], expected: string): void {
|
|
assert.equal(argsToCommandLine(file, args), expected);
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
+ describe('Windows agent Job control', () => {
|
|
+ it('reports an already-exited owned Job as drained without another native lookup', () => {
|
|
+ const terminateJobTree = () => Promise.resolve(false);
|
|
+ const agent = Object.create(WindowsPtyAgent.prototype) as any;
|
|
+ agent._windowsAgentJob = true;
|
|
+ agent._processExited = true;
|
|
+ agent._pty = 1;
|
|
+ agent._ptyNative = { terminateJobTree };
|
|
+
|
|
+ assert.strictEqual(agent.windowsAgentJob, true);
|
|
+ return agent.terminateJobTree(1000).then((drained: boolean) => {
|
|
+ assert.strictEqual(drained, true);
|
|
+ });
|
|
+ });
|
|
+
|
|
+ it('does not expose teardown when Job ownership was not established', () => {
|
|
+ const agent = Object.create(WindowsPtyAgent.prototype) as any;
|
|
+ agent._windowsAgentJob = false;
|
|
+
|
|
+ assert.strictEqual(agent.windowsAgentJob, false);
|
|
+ assert.strictEqual(agent.terminateJobTree(1000), undefined);
|
|
+ });
|
|
+ });
|
|
+
|
|
describe('argsToCommandLine', () => {
|
|
describe('Plain strings', () => {
|
|
it('doesn\'t quote plain string', () => {
|
|
diff --git a/src/windowsPtyAgent.ts b/src/windowsPtyAgent.ts
|
|
index d7054449516f0c9a62af351c2caa17331206d530..5cc174373171329f36860103edab95323da89ed7 100644
|
|
--- a/src/windowsPtyAgent.ts
|
|
+++ b/src/windowsPtyAgent.ts
|
|
@@ -34,6 +34,9 @@ export class WindowsPtyAgent {
|
|
private _innerPid: number = 0;
|
|
private _closeTimeout: NodeJS.Timer | undefined;
|
|
private _exitCode: number | undefined;
|
|
+ private _processExited: boolean = false;
|
|
+ private _windowsAgentJob: boolean = false;
|
|
+ private _processExitListeners: (() => void)[] = [];
|
|
private _conoutSocketWorker: ConoutConnection;
|
|
|
|
private _fd: any;
|
|
@@ -45,6 +48,7 @@ export class WindowsPtyAgent {
|
|
public get fd(): any { return this._fd; }
|
|
public get innerPid(): number { return this._innerPid; }
|
|
public get pty(): number { return this._pty; }
|
|
+ public get windowsAgentJob(): boolean { return this._windowsAgentJob; }
|
|
|
|
constructor(
|
|
file: string,
|
|
@@ -56,11 +60,14 @@ 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;
|
|
}
|
|
+ // Why: a requested Job is effective only when runtime detection actually selects ConPTY.
|
|
+ this._windowsAgentJob = Boolean(this._useConpty && windowsAgentJob);
|
|
if (this._useConpty) {
|
|
if (!conptyNative) {
|
|
conptyNative = loadNativeModule('conpty').module;
|
|
@@ -116,7 +123,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, this._windowsAgentJob, c => this._$onProcessExit(c));
|
|
this._innerPid = connect.pid;
|
|
}
|
|
}
|
|
@@ -138,6 +145,24 @@ export class WindowsPtyAgent {
|
|
}
|
|
}
|
|
|
|
+ public terminateJobTree(timeoutMs: number): Promise<boolean> | undefined {
|
|
+ if (!this._windowsAgentJob) {
|
|
+ return undefined;
|
|
+ }
|
|
+ if (this._processExited) {
|
|
+ return Promise.resolve(true);
|
|
+ }
|
|
+ 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 +248,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.test.ts b/src/windowsTerminal.test.ts
|
|
index 8f1274edf6dcf6e9257ba9d711099791391ec778..afc5813d26ce0027b826d326fffdbc3c242753aa 100644
|
|
--- a/src/windowsTerminal.test.ts
|
|
+++ b/src/windowsTerminal.test.ts
|
|
@@ -92,6 +92,20 @@ function pollForProcessTreeSize(pid: number, size: number, intervalMs: number =
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
+ describe('WindowsTerminal Job capability fallback', () => {
|
|
+ it('does not wait for a Job callback when a requested Job falls back to winpty', function(done) {
|
|
+ this.timeout(10000);
|
|
+ const term = new WindowsTerminal('cmd.exe', '/C exit', {
|
|
+ useConpty: false,
|
|
+ windowsAgentJob: true
|
|
+ });
|
|
+ term.on('exit', code => {
|
|
+ assert.strictEqual(code, 0);
|
|
+ done();
|
|
+ });
|
|
+ });
|
|
+ });
|
|
+
|
|
[[false, false], [true, false], [true, true]].forEach(([useConpty, useConptyDll]) => {
|
|
describe(`WindowsTerminal (useConpty = ${useConpty}, useConptyDll = ${useConptyDll})`, () => {
|
|
describe('kill', () => {
|
|
diff --git a/src/windowsTerminal.ts b/src/windowsTerminal.ts
|
|
index 13f6c6dbb3d7965c4699568f4704be3b6a33d947..db6ddd94a35b034c0fb998003e4332c28512809d 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 || (this._agent.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.
|
|
*/
|