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$.
This commit is contained in:
Sanzhar Kuandyk
2026-04-01 11:38:49 +05:00
parent 8367340b05
commit 789741bb83
3 changed files with 55 additions and 7 deletions

View File

@@ -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"
@@ -549,13 +550,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();

View File

@@ -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"
@@ -5020,6 +5024,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;
// This temporary bootstrap channel is closed intentionally once we obtain
// the new server address. Don't forward child stderr to the current UI.
@@ -5031,6 +5042,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;
@@ -5907,7 +5923,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;
@@ -5928,6 +5947,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);
}
}

View File

@@ -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)
{