fix(pty): prevent orphan conhost.exe on Windows 10 #38244

Problem:
On Windows 10, the ConPTY closing order is strict. Calling
ClosePseudoConsole after closing the output stream can easily
lead to accidental deadlocks, leaving orphan conhost.exe processes.
See https://learn.microsoft.com/en-us/windows/console/closepseudoconsole#remarks
and https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#ending-the-pseudoconsole-session

Solution:
Based on the warning in the docs above, we need to call ClosePseudoConsole
on a separate thread first so that the output pipe can fully drain on
the main thread. Also, `wait_eof_timer` is outdated, so I removed it
to avoid extra cleanup logic around it (it was introduced in the
early winpty days).

Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
This commit is contained in:
tao
2026-03-21 06:42:41 +08:00
committed by GitHub
parent cd71221250
commit 7be4e4dd65
5 changed files with 34 additions and 40 deletions

View File

@@ -93,7 +93,7 @@ struct stream {
uv_file fd; ///< When the stream is a file, this is its file descriptor
int64_t fpos; ///< When the stream is a file, this is the position in file
void *cb_data;
stream_close_cb close_cb, internal_close_cb;
stream_close_cb before_close_cb, close_cb, internal_close_cb;
void *close_cb_data, *internal_data;
size_t pending_reqs;
MultiQueue *events;

View File

@@ -140,6 +140,11 @@ void stream_close_handle(Stream *stream)
assert(handle != NULL);
if (stream->before_close_cb) {
stream->pending_reqs++;
stream->before_close_cb(stream, stream->close_cb_data);
stream->pending_reqs--;
}
if (!uv_is_closing(handle)) {
uv_close(handle, close_cb);
}

View File

@@ -174,8 +174,9 @@ void os_conpty_set_size(conpty_t *conpty_object, uint16_t width, uint16_t height
}
}
void os_conpty_free(conpty_t *conpty_object)
void os_conpty_free(void *data)
{
conpty_t *conpty_object = data;
if (conpty_object != NULL) {
if (conpty_object->si_ex.lpAttributeList != NULL) {
DeleteProcThreadAttributeList(conpty_object->si_ex.lpAttributeList);

View File

@@ -17,35 +17,35 @@
static void CALLBACK pty_proc_terminate_cb(void *context, BOOLEAN unused)
FUNC_ATTR_NONNULL_ALL
{
PtyProc *ptyproc = (PtyProc *)context;
Proc *proc = (Proc *)ptyproc;
os_conpty_free(ptyproc->conpty);
Proc *proc = (Proc *)context;
// NB: pty_proc_terminate_cb() is called on a separate thread,
// but finishing up the process needs to be done on the main thread.
loop_schedule_fast(proc->loop, event_create(pty_proc_finish_when_eof, ptyproc));
loop_schedule_fast(proc->loop, event_create(pty_proc_finish, context));
}
static void pty_proc_finish_when_eof(void **argv)
FUNC_ATTR_NONNULL_ALL
{
PtyProc *ptyproc = (PtyProc *)argv[0];
if (ptyproc->finish_wait != NULL) {
if (pty_proc_can_finish(ptyproc)) {
pty_proc_finish(ptyproc);
} else {
uv_timer_start(&ptyproc->wait_eof_timer, wait_eof_timer_cb, 200, 200);
}
}
}
static bool pty_proc_can_finish(PtyProc *ptyproc)
/// Create a separate thread to call ClosePseudoConsole,
/// which allows us to flush the final frame on the main thread.
/// See https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#ending-the-pseudoconsole-session
static void pty_proc_close_console(Stream *stream, void *data)
{
PtyProc *ptyproc = stream->internal_data;
Proc *proc = (Proc *)ptyproc;
assert(ptyproc->finish_wait != NULL);
return proc->out.s.closed || proc->out.did_eof || !uv_is_readable(proc->out.s.uvstream);
if (ptyproc->conpty == NULL) {
return;
}
// On Windows 11, closing a terminal immediately after opening it can leave orphan conhost
// and shell processes, e.g. `during TermClose event` case in autocmd_spec.lua
// Checking `num_bytes` is an attempt to detect that, until we find a better way.
if (proc->out.num_bytes == 0) {
TerminateProcess(ptyproc->proc_handle, 0);
}
uv_thread_t tid;
uv_thread_create(&tid, os_conpty_free, ptyproc->conpty);
while (!proc->out.did_eof && WaitForSingleObject(tid, 0) == WAIT_TIMEOUT) {
uv_run(&proc->loop->uv, UV_RUN_ONCE);
}
uv_thread_detach(&tid);
ptyproc->conpty = NULL;
}
/// @returns zero on success, or negative error code.
@@ -126,8 +126,6 @@ int pty_proc_spawn(PtyProc *ptyproc)
}
proc->pid = (int)GetProcessId(proc_handle);
uv_timer_init(&proc->loop->uv, &ptyproc->wait_eof_timer);
ptyproc->wait_eof_timer.data = (void *)ptyproc;
if (!RegisterWaitForSingleObject(&ptyproc->finish_wait,
proc_handle,
pty_proc_terminate_cb,
@@ -143,6 +141,7 @@ int pty_proc_spawn(PtyProc *ptyproc)
uv_run(&proc->loop->uv, UV_RUN_ONCE);
}
proc->out.s.before_close_cb = pty_proc_close_console;
ptyproc->conpty = conpty_object;
ptyproc->proc_handle = proc_handle;
conpty_object = NULL;
@@ -200,7 +199,6 @@ void pty_proc_close(PtyProc *ptyproc)
if (ptyproc->finish_wait != NULL) {
UnregisterWaitEx(ptyproc->finish_wait, NULL);
ptyproc->finish_wait = NULL;
uv_close((uv_handle_t *)&ptyproc->wait_eof_timer, NULL);
}
if (ptyproc->proc_handle != NULL) {
CloseHandle(ptyproc->proc_handle);
@@ -229,19 +227,10 @@ static void pty_proc_connect_cb(uv_connect_t *req, int status)
req->handle = NULL;
}
static void wait_eof_timer_cb(uv_timer_t *wait_eof_timer)
FUNC_ATTR_NONNULL_ALL
{
PtyProc *ptyproc = wait_eof_timer->data;
if (pty_proc_can_finish(ptyproc)) {
uv_timer_stop(&ptyproc->wait_eof_timer);
pty_proc_finish(ptyproc);
}
}
static void pty_proc_finish(PtyProc *ptyproc)
static void pty_proc_finish(void **argv)
FUNC_ATTR_NONNULL_ALL
{
PtyProc *ptyproc = (PtyProc *)argv[0];
Proc *proc = (Proc *)ptyproc;
DWORD exit_code = 0;

View File

@@ -13,7 +13,6 @@ typedef struct pty_process {
conpty_t *conpty;
HANDLE finish_wait;
HANDLE proc_handle;
uv_timer_t wait_eof_timer;
} PtyProc;
// Structure used by build_cmd_line()