fix(channel): fix Ctrl-C handling regression in terminal

Problem: Normal Windows builtin-TUI startup spawns the embedded server as DETACHED_PROCESS, which breaks Ctrl-C delivery to :terminal jobs.
Solution: Restores the default behavior once the embedded server has a
console so terminal jobs inherit it.

(cherry picked from commit 8bb7533639)
This commit is contained in:
Sanzhar Kuandyk
2026-04-02 09:41:03 +05:00
committed by github-actions[bot]
parent 608d0e01ba
commit 319c031820
3 changed files with 26 additions and 8 deletions

View File

@@ -556,23 +556,24 @@ 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);
// :restart spawns a replacement server that must not borrow the parent
// Nvim process console, because that parent process will soon exit.
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()) {
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).
// 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), or for the replacement server
// spawned by :restart, because the parent Nvim process will soon exit.
if (restart_alloc_console || !AttachConsole(ATTACH_PARENT_PROCESS)) {
AllocConsole();
ShowWindow(GetConsoleWindow(), SW_HIDE);
}
}
os_enable_ctrl_c();
os_replace_stdin_to_conin();
os_replace_stdout_and_stderr_to_conout();
}

View File

@@ -14,6 +14,16 @@ static HWND hWnd = NULL;
static HICON hOrigIconSmall = NULL;
static HICON hOrigIcon = NULL;
/// Re-enable normal Ctrl-C processing after detached startup.
///
/// On Windows, UV_PROCESS_DETACHED implies CREATE_NEW_PROCESS_GROUP, which
/// disables Ctrl-C handling for the new process. Restore the default behavior
/// once the embedded server has a console so terminal jobs inherit it.
void os_enable_ctrl_c(void)
{
SetConsoleCtrlHandler(NULL, FALSE);
}
int os_open_conin_fd(void)
{
const HANDLE conin_handle = CreateFile("CONIN$",
@@ -65,6 +75,7 @@ void os_swap_to_hidden_console(void)
FreeConsole();
AllocConsole();
ShowWindow(GetConsoleWindow(), SW_HIDE);
os_enable_ctrl_c();
os_replace_stdin_to_conin();
os_replace_stdout_and_stderr_to_conout();
}

View File

@@ -16,6 +16,8 @@ local ok = t.ok
local rmdir = n.rmdir
local new_pipename = n.new_pipename
local pesc = vim.pesc
local is_os = t.is_os
local skip = t.skip
local set_session = n.set_session
local async_meths = n.async_meths
local expect_msg_seq = n.expect_msg_seq
@@ -116,6 +118,10 @@ describe("preserve and (R)ecover with custom 'directory'", function()
end)
it('killing TUI process without :preserve #22096', function()
-- Windows(#38669): inner server could attach to the outer Nvim terminal's console
-- and die abruptly when the outer terminal job closed, leaving an unreadable swapfile
skip(is_os('win'), 'unreadable swapfile on Windows after TUI process exit')
local screen0 = Screen.new()
local child_server = new_pipename()
fn.jobstart({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--listen', child_server }, {