mirror of
https://github.com/neovim/neovim.git
synced 2026-03-29 03:42:11 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user