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%': '= 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 | 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 #include #include +#include #include +#include #include #include @@ -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().Int32Value(); @@ -348,7 +387,8 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) { const std::wstring cwd(path_util::to_wstring(info[2].As())); const Napi::Array envValues = info[3].As(); const bool useConptyDll = info[4].As().Value(); - Napi::Function exitCallback = info[5].As(); + const bool windowsAgentJob = info[5].As().Value(); + Napi::Function exitCallback = info[6].As(); // 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(-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().Int32Value(); + const pty_baton* handle = get_pty_baton(id); + if (handle == nullptr || handle->hJob == nullptr) { + return env.Undefined(); + } + + const double timeoutValue = info[1].As().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(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 | 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 | 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 | undefined; + /** * Pauses the pty for customizable flow control. */