From 608d0e01baf0ac69ee90efba481a0cc41ed077c8 Mon Sep 17 00:00:00 2001 From: Sanzhar Kuandyk Date: Wed, 1 Apr 2026 11:38:49 +0500 Subject: [PATCH] refactor(channel): defer hidden console allocation to :detach Refactor #37977: instead of allocating a hidden console at startup, borrow the parent's console via AttachConsole() and only create an isolated hidden console when :detach is called, with fd 0/1/2 re-bound to the new CONIN$/CONOUT$. (cherry picked from commit 789741bb83d1f31b7162cd8944bc2d846fa2fa51) --- src/nvim/channel.c | 22 ++++++++++++++++------ src/nvim/ex_docmd.c | 27 ++++++++++++++++++++++++++- src/nvim/os/os_win_console.c | 13 +++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/nvim/channel.c b/src/nvim/channel.c index aa0d067927..92b78492fe 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -38,6 +38,7 @@ #include "nvim/msgpack_rpc/channel.h" #include "nvim/msgpack_rpc/server.h" #include "nvim/os/fs.h" +#include "nvim/os/os.h" #include "nvim/os/os_defs.h" #include "nvim/os/shell.h" #include "nvim/terminal.h" @@ -555,13 +556,22 @@ uint64_t channel_from_stdio(bool rpc, CallbackReader on_output, const char **err os_set_cloexec(stdin_dup_fd); stdout_dup_fd = os_dup(STDOUT_FILENO); os_set_cloexec(stdout_dup_fd); - - // The server may have no console (spawned with UV_PROCESS_DETACHED for - // :detach support). Allocate a hidden one so CONIN$/CONOUT$ and ConPTY - // (:terminal) work. + const bool restart_alloc_console = os_env_exists("__NVIM_RESTART_ALLOC_CONSOLE", true); + if (restart_alloc_console) { + os_unsetenv("__NVIM_RESTART_ALLOC_CONSOLE"); + } if (!GetConsoleWindow()) { - AllocConsole(); - ShowWindow(GetConsoleWindow(), SW_HIDE); + if (restart_alloc_console) { + AllocConsole(); + ShowWindow(GetConsoleWindow(), SW_HIDE); + } else if (!AttachConsole(ATTACH_PARENT_PROCESS)) { + // Borrow the parent's console so CONOUT$ resolves to the real terminal, + // preserving io.stdout rendering (e.g. SIXEL/Kitty images). Only fall + // back to a hidden AllocConsole when there is no parent console (e.g. + // launched from a non-console parent). + AllocConsole(); + ShowWindow(GetConsoleWindow(), SW_HIDE); + } } os_replace_stdin_to_conin(); os_replace_stdout_and_stderr_to_conout(); diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 8a8f9df413..d9b3eb0138 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -17,6 +17,7 @@ #include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" #include "nvim/api/ui.h" +#include "nvim/api/vim.h" #include "nvim/api/vimscript.h" #include "nvim/arglist.h" #include "nvim/ascii_defs.h" @@ -89,6 +90,9 @@ #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/os/os_defs.h" +#ifdef MSWIN +# include "nvim/os/os_win_console.h" +#endif #include "nvim/os/shell.h" #include "nvim/path.h" #include "nvim/plines.h" @@ -5022,6 +5026,13 @@ static void ex_restart(exarg_T *eap) server_stopped = server_stop(listen_arg, true); } +#ifdef MSWIN + bool restart_alloc_console_env = false; + if (os_setenv("__NVIM_RESTART_ALLOC_CONSOLE", "1", 1) == 0) { + restart_alloc_console_env = true; + } +#endif + CallbackReader on_err = CALLBACK_READER_INIT; #ifdef MSWIN // On Windows, don't forward stderr as it won't work after the current server exits. @@ -5037,6 +5048,11 @@ static void ex_restart(exarg_T *eap) CALLBACK_READER_INIT, on_err, CALLBACK_NONE, false, true, true, detach, kChannelStdinPipe, NULL, 0, 0, NULL, &exit_status); +#ifdef MSWIN + if (restart_alloc_console_env) { + os_unsetenv("__NVIM_RESTART_ALLOC_CONSOLE"); + } +#endif if (!channel) { emsg("cannot create a channel job"); goto fail_1; @@ -5917,7 +5933,10 @@ static void ex_detach(exarg_T *eap) emsg(e_invchan); return; } - chan->detach = true; // Prevent self-exit on channel-close. + // Prevent self-exit on channel-close. + Error detach_err = ERROR_INIT; + nvim__chan_set_detach(chan->id, true, &detach_err); + api_clear_error(&detach_err); // Server-side UI detach. Doesn't close the channel. Error err2 = ERROR_INIT; @@ -5938,6 +5957,12 @@ static void ex_detach(exarg_T *eap) // XXX: Can't do this, channel_decref() is async... // assert(!find_channel(chan->id)); +#ifdef MSWIN + // After UI/channel detach, move this server off the parent's console so it + // survives terminal closure and still has working CONIN$/CONOUT$. + os_swap_to_hidden_console(); +#endif + ILOG("detach current_ui=%" PRId64, chan->id); } } diff --git a/src/nvim/os/os_win_console.c b/src/nvim/os/os_win_console.c index af5515e2f2..47c715287e 100644 --- a/src/nvim/os/os_win_console.c +++ b/src/nvim/os/os_win_console.c @@ -56,6 +56,19 @@ void os_replace_stdout_and_stderr_to_conout(void) assert(conerr_fd == STDERR_FILENO); } +/// Detach from the current console and switch stdio to a hidden private one. +/// +/// Used when an embedded server must outlive its parent console, while keeping +/// CONIN$/CONOUT$ and ConPTY functional for :terminal and stdio writes. +void os_swap_to_hidden_console(void) +{ + FreeConsole(); + AllocConsole(); + ShowWindow(GetConsoleWindow(), SW_HIDE); + os_replace_stdin_to_conin(); + os_replace_stdout_and_stderr_to_conout(); +} + /// Resets Windows console icon if we got an original one on startup. void os_icon_reset(void) {