From 974bc1b0444ba3a235f056ff16e038aaee34863f Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 12 Mar 2026 06:45:22 +0800 Subject: [PATCH] fix(process): wrong exit code for SIGHUP on Windows (#38242) Problem: When stopping a PTY process on Windows, the exit code indicates that the process is stopped by SIGTERM even when closing all streams is enough to terminate the process. This is inconsistent with other platforms. Solution: Set exit_signal to SIGHUP instead of SIGTERM when using SIGHUP. --- src/nvim/event/proc.c | 3 +- test/functional/api/vim_spec.lua | 54 +++++++++++++++++++------------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/nvim/event/proc.c b/src/nvim/event/proc.c index 009b8b2a7e..eee728679d 100644 --- a/src/nvim/event/proc.c +++ b/src/nvim/event/proc.c @@ -242,14 +242,15 @@ void proc_stop(Proc *proc) FUNC_ATTR_NONNULL_ALL return; } proc->stopped_time = os_hrtime(); - proc->exit_signal = SIGTERM; switch (proc->type) { case kProcTypeUv: + proc->exit_signal = SIGTERM; os_proc_tree_kill(proc->pid, SIGTERM); break; case kProcTypePty: // close all streams for pty processes to send SIGHUP to the process + proc->exit_signal = SIGHUP; proc_close_streams(proc); pty_proc_close_master((PtyProc *)proc); break; diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 56ac0ec205..196165ea90 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -2828,6 +2828,18 @@ describe('API', function() eq(info, eval('rpcrequest(3, "nvim_get_chan_info", 0)')) end) + local function term_channel_info(id, buffer, argv) + return { + stream = 'job', + id = id, + argv = argv, + mode = 'terminal', + buffer = buffer, + pty = '?', + exitcode = -1, + } + end + it('stream=job :terminal channel', function() local screen = Screen.new(80, 24) @@ -2835,15 +2847,7 @@ describe('API', function() eq(1, api.nvim_get_current_buf()) eq(3, api.nvim_get_option_value('channel', { buf = 1 })) - local info = { - stream = 'job', - id = 3, - argv = { eval('exepath(&shell)') }, - mode = 'terminal', - buffer = 1, - pty = '?', - exitcode = -1, - } + local info = term_channel_info(3, 1, { eval('exepath(&shell)') }) local event = api.nvim_get_var('opened_event') if not is_os('win') then info.pty = event.info.pty @@ -2854,7 +2858,7 @@ describe('API', function() eq({ [1] = testinfo, [2] = stderr, [3] = info }, api.nvim_list_chans()) eq(info, api.nvim_get_chan_info(3)) - -- :terminal with args + running process. + -- :terminal with args + running process (Nvim TUI). -- Don't use a shell here, so that SIGHUP handling doesn't depend on the shell. command('enew') local argv = { n.nvim_prog, '-u', 'NONE', '-i', 'NONE' } @@ -2863,15 +2867,7 @@ describe('API', function() env = { VIMRUNTIME = os.getenv('VIMRUNTIME') }, }) eq(-1, eval('jobwait([&channel], 0)[0]')) -- Running? - local expected2 = { - stream = 'job', - id = 4, - argv = argv, - mode = 'terminal', - buffer = 2, - pty = '?', - exitcode = -1, - } + local expected2 = term_channel_info(4, 2, argv) local actual2 = eval('nvim_get_chan_info(&channel)') expected2.pty = actual2.pty eq(expected2, actual2) @@ -2879,12 +2875,28 @@ describe('API', function() -- Make sure Nvim TUI is started (which is after registering SIGHUP handler). screen:expect({ any = 'Nvim is open source and freely distributable' }) - -- :terminal with args + stopped process. + -- :terminal with args + stopped process (Nvim TUI). eq(1, eval('jobstop(&channel)')) eval('jobwait([&channel], 1000)') -- Wait. expected2.pty = (is_os('win') and '?' or '') -- pty stream was closed. - expected2.exitcode = (is_os('win') and 143 or 1) + -- On Unix, SIGHUP is handled by Nvim TUI, so exit code is 1. + -- On Windows, even though Nvim TUI handles SIGHUP, it's not possible for the + -- parent process to know that, so exit code reflects SIGHUP. + expected2.exitcode = (is_os('win') and 129 or 1) eq(expected2, eval('nvim_get_chan_info(&channel)')) + + -- :terminal with args + stopped process (shell-test). + command('enew') + argv = { n.testprg('shell-test'), 'INTERACT' } + fn.jobstart(argv, { term = true }) + screen:expect({ any = { vim.pesc('interact $') } }) + eq(1, eval('jobstop(&channel)')) + eval('jobwait([&channel], 1000)') -- Wait. + local expected3 = term_channel_info(5, 3, argv) + expected3.pty = (is_os('win') and '?' or '') -- pty stream was closed. + -- Exit code should reflect SIGHUP as shell-test doesn't handle it. + expected3.exitcode = 129 + eq(expected3, eval('nvim_get_chan_info(&channel)')) end) end)