neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit 057d27a9d6ef0bb2ee5130704c45b9e9197e7c36
parent 5792546777332361a9ac49107e46149c703de90e
Author: Justin M. Keyes <justinkz@gmail.com>
Date:   Sun, 15 Sep 2024 12:20:58 -0700

refactor: rename "process" => "proc" #30387

Problem:
- "process" is often used as a verb (`multiqueue_process_events`), which
  is ambiguous for cases where it's used as a topic.
- The documented naming convention for processes is "proc".
  - `:help dev-name-common`
- Shorter is better, when it doesn't harm readability or
  discoverability.

Solution:
Rename "process" => "proc" in all C symbols and module names.
Diffstat:
Msrc/.valgrind.supp | 4++--
Msrc/clint.py | 4++--
Msrc/nvim/CMakeLists.txt | 10+++++-----
Msrc/nvim/api/vim.c | 2+-
Msrc/nvim/channel.c | 36++++++++++++++++++------------------
Msrc/nvim/channel.h | 10+++++-----
Msrc/nvim/eval.c | 4++--
Msrc/nvim/eval/funcs.c | 24++++++++++++------------
Msrc/nvim/event/defs.h | 25+++++++++++++------------
Asrc/nvim/event/libuv_proc.c | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/nvim/event/libuv_proc.h | 16++++++++++++++++
Dsrc/nvim/event/libuv_process.c | 139-------------------------------------------------------------------------------
Dsrc/nvim/event/libuv_process.h | 16----------------
Asrc/nvim/event/proc.c | 451+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/nvim/event/proc.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/nvim/event/process.c | 451-------------------------------------------------------------------------------
Dsrc/nvim/event/process.h | 49-------------------------------------------------
Msrc/nvim/main.c | 6+++---
Msrc/nvim/memline.c | 22+++++++++++-----------
Msrc/nvim/msgpack_rpc/channel.c | 2+-
Msrc/nvim/os/env.c | 2+-
Asrc/nvim/os/proc.c | 286+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/nvim/os/proc.h | 11+++++++++++
Dsrc/nvim/os/process.c | 286-------------------------------------------------------------------------------
Dsrc/nvim/os/process.h | 11-----------
Msrc/nvim/os/pty_conpty_win.c | 4++--
Asrc/nvim/os/pty_proc.h | 7+++++++
Asrc/nvim/os/pty_proc_unix.c | 417+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/nvim/os/pty_proc_unix.h | 18++++++++++++++++++
Asrc/nvim/os/pty_proc_win.c | 440+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/nvim/os/pty_proc_win.h | 27+++++++++++++++++++++++++++
Dsrc/nvim/os/pty_process.h | 7-------
Dsrc/nvim/os/pty_process_unix.c | 417-------------------------------------------------------------------------------
Dsrc/nvim/os/pty_process_unix.h | 18------------------
Dsrc/nvim/os/pty_process_win.c | 440-------------------------------------------------------------------------------
Dsrc/nvim/os/pty_process_win.h | 27---------------------------
Msrc/nvim/os/shell.c | 14+++++++-------
Msrc/nvim/profile.c | 6+++---
Mtest/README.md | 2+-
39 files changed, 1950 insertions(+), 1949 deletions(-)

diff --git a/src/.valgrind.supp b/src/.valgrind.supp @@ -10,7 +10,7 @@ Memcheck:Leak fun:malloc fun:uv_spawn - fun:libuv_process_spawn - fun:process_spawn + fun:libuv_proc_spawn + fun:proc_spawn fun:job_start } diff --git a/src/clint.py b/src/clint.py @@ -848,7 +848,7 @@ def CheckIncludes(filename, lines, error): or filename.endswith('.in.h') or FileInfo(filename).RelativePath() in { 'func_attr.h', - 'os/pty_process.h', + 'os/pty_proc.h', }): return @@ -869,7 +869,7 @@ def CheckIncludes(filename, lines, error): "src/nvim/msgpack_rpc/unpacker.h", "src/nvim/option.h", "src/nvim/os/pty_conpty_win.h", - "src/nvim/os/pty_process_win.h", + "src/nvim/os/pty_proc_win.h", ] skip_headers = [ diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt @@ -417,10 +417,10 @@ list(SORT NVIM_HEADERS) foreach(sfile ${NVIM_SOURCES}) get_filename_component(f ${sfile} NAME) - if(WIN32 AND ${f} MATCHES "^(pty_process_unix.c)$") + if(WIN32 AND ${f} MATCHES "^(pty_proc_unix.c)$") list(REMOVE_ITEM NVIM_SOURCES ${sfile}) endif() - if(NOT WIN32 AND ${f} MATCHES "^(pty_process_win.c)$") + if(NOT WIN32 AND ${f} MATCHES "^(pty_proc_win.c)$") list(REMOVE_ITEM NVIM_SOURCES ${sfile}) endif() if(NOT WIN32 AND ${f} MATCHES "^(pty_conpty_win.c)$") @@ -436,7 +436,7 @@ foreach(hfile ${NVIM_HEADERS}) if(WIN32 AND ${f} MATCHES "^(unix_defs.h)$") list(REMOVE_ITEM NVIM_HEADERS ${hfile}) endif() - if(WIN32 AND ${f} MATCHES "^(pty_process_unix.h)$") + if(WIN32 AND ${f} MATCHES "^(pty_proc_unix.h)$") list(REMOVE_ITEM NVIM_HEADERS ${hfile}) endif() if(NOT WIN32 AND ${f} MATCHES "^(win_defs.h)$") @@ -832,12 +832,12 @@ find_program(CLANG_TIDY_PRG clang-tidy) set(EXCLUDE_CLANG_TIDY typval_encode.c.h ui_events.in.h) if(WIN32) list(APPEND EXCLUDE_CLANG_TIDY - os/pty_process_unix.h + os/pty_proc_unix.h os/unix_defs.h) else() list(APPEND EXCLUDE_CLANG_TIDY os/win_defs.h - os/pty_process_win.h + os/pty_proc_win.h os/pty_conpty_win.h os/os_win_console.h) endif() diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c @@ -70,7 +70,7 @@ #include "nvim/optionstr.h" #include "nvim/os/input.h" #include "nvim/os/os_defs.h" -#include "nvim/os/process.h" +#include "nvim/os/proc.h" #include "nvim/popupmenu.h" #include "nvim/pos_defs.h" #include "nvim/runtime.h" diff --git a/src/nvim/channel.c b/src/nvim/channel.c @@ -19,7 +19,7 @@ #include "nvim/eval/typval.h" #include "nvim/event/loop.h" #include "nvim/event/multiqueue.h" -#include "nvim/event/process.h" +#include "nvim/event/proc.h" #include "nvim/event/rstream.h" #include "nvim/event/socket.h" #include "nvim/event/stream.h" @@ -88,7 +88,7 @@ void channel_free_all_mem(void) bool channel_close(uint64_t id, ChannelPart part, const char **error) { Channel *chan; - Process *proc; + Proc *proc; const char *dummy; if (!error) { @@ -139,8 +139,8 @@ bool channel_close(uint64_t id, ChannelPart part, const char **error) if (part == kChannelPartStderr || part == kChannelPartAll) { rstream_may_close(&proc->err); } - if (proc->type == kProcessTypePty && part == kChannelPartAll) { - pty_process_close_master(&chan->stream.pty); + if (proc->type == kProcTypePty && part == kChannelPartAll) { + pty_proc_close_master(&chan->stream.pty); } break; @@ -289,7 +289,7 @@ static void channel_destroy(Channel *chan) } if (chan->streamtype == kChannelStreamProc) { - process_free(&chan->stream.proc); + proc_free(&chan->stream.proc); } callback_reader_free(&chan->on_data); @@ -376,7 +376,7 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s *status_out = 0; return NULL; } - chan->stream.pty = pty_process_init(&main_loop, chan); + chan->stream.pty = pty_proc_init(&main_loop, chan); if (pty_width > 0) { chan->stream.pty.width = pty_width; } @@ -384,22 +384,22 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s chan->stream.pty.height = pty_height; } } else { - chan->stream.uv = libuv_process_init(&main_loop, chan); + chan->stream.uv = libuv_proc_init(&main_loop, chan); } - Process *proc = &chan->stream.proc; + Proc *proc = &chan->stream.proc; proc->argv = argv; proc->exepath = exepath; - proc->cb = channel_process_exit_cb; + proc->cb = channel_proc_exit_cb; proc->events = chan->events; proc->detach = detach; proc->cwd = cwd; proc->env = env; proc->overlapped = overlapped; - char *cmd = xstrdup(process_get_exepath(proc)); + char *cmd = xstrdup(proc_get_exepath(proc)); bool has_out, has_err; - if (proc->type == kProcessTypePty) { + if (proc->type == kProcTypePty) { has_out = true; has_err = false; } else { @@ -410,7 +410,7 @@ Channel *channel_job_start(char **argv, const char *exepath, CallbackReader on_s bool has_in = stdin_mode == kChannelStdinPipe; - int status = process_spawn(proc, has_in, has_out, has_err); + int status = proc_spawn(proc, has_in, has_out, has_err); if (status) { semsg(_(e_jobspawn), os_strerror(status), cmd); xfree(cmd); @@ -760,7 +760,7 @@ void channel_reader_callbacks(Channel *chan, CallbackReader *reader) } } -static void channel_process_exit_cb(Process *proc, int status, void *data) +static void channel_proc_exit_cb(Proc *proc, int status, void *data) { Channel *chan = data; if (chan->term) { @@ -847,7 +847,7 @@ static void term_write(const char *buf, size_t size, void *data) static void term_resize(uint16_t width, uint16_t height, void *data) { Channel *chan = data; - pty_process_resize(&chan->stream.pty, width, height); + pty_proc_resize(&chan->stream.pty, width, height); } static inline void term_delayed_free(void **argv) @@ -867,7 +867,7 @@ static inline void term_delayed_free(void **argv) static void term_close(void *data) { Channel *chan = data; - process_stop(&chan->stream.proc); + proc_stop(&chan->stream.proc); multiqueue_put(chan->events, term_delayed_free, data); } @@ -907,7 +907,7 @@ bool channel_job_running(uint64_t id) Channel *chan = find_channel(id); return (chan && chan->streamtype == kChannelStreamProc - && !process_is_stopped(&chan->stream.proc)); + && !proc_is_stopped(&chan->stream.proc)); } Dictionary channel_info(uint64_t id, Arena *arena) @@ -924,8 +924,8 @@ Dictionary channel_info(uint64_t id, Arena *arena) switch (chan->streamtype) { case kChannelStreamProc: { stream_desc = "job"; - if (chan->stream.proc.type == kProcessTypePty) { - const char *name = pty_process_tty_name(&chan->stream.pty); + if (chan->stream.proc.type == kProcTypePty) { + const char *name = pty_proc_tty_name(&chan->stream.pty); PUT_C(info, "pty", CSTR_TO_ARENA_OBJ(arena, name)); } diff --git a/src/nvim/channel.h b/src/nvim/channel.h @@ -7,11 +7,11 @@ #include "nvim/channel_defs.h" // IWYU pragma: keep #include "nvim/eval/typval_defs.h" #include "nvim/event/defs.h" -#include "nvim/event/libuv_process.h" +#include "nvim/event/libuv_proc.h" #include "nvim/macros_defs.h" #include "nvim/map_defs.h" #include "nvim/msgpack_rpc/channel_defs.h" -#include "nvim/os/pty_process.h" +#include "nvim/os/pty_proc.h" #include "nvim/types_defs.h" struct Channel { @@ -21,9 +21,9 @@ struct Channel { ChannelStreamType streamtype; union { - Process proc; - LibuvProcess uv; - PtyProcess pty; + Proc proc; + LibuvProc uv; + PtyProc pty; RStream socket; StdioPair stdio; StderrState err; diff --git a/src/nvim/eval.c b/src/nvim/eval.c @@ -32,7 +32,7 @@ #include "nvim/eval/vars.h" #include "nvim/event/loop.h" #include "nvim/event/multiqueue.h" -#include "nvim/event/process.h" +#include "nvim/event/proc.h" #include "nvim/event/time.h" #include "nvim/ex_cmds.h" #include "nvim/ex_docmd.h" @@ -8506,7 +8506,7 @@ Channel *find_job(uint64_t id, bool show_error) { Channel *data = find_channel(id); if (!data || data->streamtype != kChannelStreamProc - || process_is_stopped(&data->stream.proc)) { + || proc_is_stopped(&data->stream.proc)) { if (show_error) { if (data && data->streamtype != kChannelStreamProc) { emsg(_(e_invchanjob)); diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c @@ -49,7 +49,7 @@ #include "nvim/event/defs.h" #include "nvim/event/loop.h" #include "nvim/event/multiqueue.h" -#include "nvim/event/process.h" +#include "nvim/event/proc.h" #include "nvim/event/time.h" #include "nvim/ex_cmds.h" #include "nvim/ex_cmds_defs.h" @@ -101,7 +101,7 @@ #include "nvim/os/fs.h" #include "nvim/os/os.h" #include "nvim/os/os_defs.h" -#include "nvim/os/pty_process.h" +#include "nvim/os/pty_proc.h" #include "nvim/os/shell.h" #include "nvim/os/stdpaths_defs.h" #include "nvim/os/time.h" @@ -3770,7 +3770,7 @@ static void f_jobpid(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) return; } - Process *proc = &data->stream.proc; + Proc *proc = &data->stream.proc; rettv->vval.v_number = proc->pid; } @@ -3796,13 +3796,13 @@ static void f_jobresize(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) return; } - if (data->stream.proc.type != kProcessTypePty) { + if (data->stream.proc.type != kProcTypePty) { emsg(_(e_channotpty)); return; } - pty_process_resize(&data->stream.pty, (uint16_t)argvars[1].vval.v_number, - (uint16_t)argvars[2].vval.v_number); + pty_proc_resize(&data->stream.pty, (uint16_t)argvars[1].vval.v_number, + (uint16_t)argvars[2].vval.v_number); rettv->vval.v_number = 1; } @@ -4077,7 +4077,7 @@ static void f_jobstop(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) // Ignore return code, but show error later. channel_close(data->id, kChannelPartRpc, &error); } - process_stop(&data->stream.proc); + proc_stop(&data->stream.proc); rettv->vval.v_number = 1; if (error) { emsg(error); @@ -4113,10 +4113,10 @@ static void f_jobwait(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) || !(chan = find_channel((uint64_t)TV_LIST_ITEM_TV(arg)->vval.v_number)) || chan->streamtype != kChannelStreamProc) { jobs[i] = NULL; // Invalid job. - } else if (process_is_stopped(&chan->stream.proc)) { + } else if (proc_is_stopped(&chan->stream.proc)) { // Job is stopped but not fully destroyed. // Ensure all callbacks on its event queue are executed. #15402 - process_wait(&chan->stream.proc, -1, NULL); + proc_wait(&chan->stream.proc, -1, NULL); jobs[i] = NULL; // Invalid job. } else { jobs[i] = chan; @@ -4144,8 +4144,8 @@ static void f_jobwait(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) if (jobs[i] == NULL) { continue; // Invalid job, will assign status=-3 below. } - int status = process_wait(&jobs[i]->stream.proc, remaining, - waiting_jobs); + int status = proc_wait(&jobs[i]->stream.proc, remaining, + waiting_jobs); if (status < 0) { break; // Interrupted (CTRL-C) or timeout, skip remaining jobs. } @@ -8207,7 +8207,7 @@ static void f_termopen(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) return; } - int pid = chan->stream.pty.process.pid; + int pid = chan->stream.pty.proc.pid; // "./…" => "/home/foo/…" vim_FullName(cwd, NameBuff, sizeof(NameBuff), false); diff --git a/src/nvim/event/defs.h b/src/nvim/event/defs.h @@ -142,30 +142,31 @@ struct socket_watcher { }; typedef enum { - kProcessTypeUv, - kProcessTypePty, -} ProcessType; + kProcTypeUv, + kProcTypePty, +} ProcType; -typedef struct process Process; -typedef void (*process_exit_cb)(Process *proc, int status, void *data); -typedef void (*internal_process_cb)(Process *proc); +/// OS process +typedef struct proc Proc; +typedef void (*proc_exit_cb)(Proc *proc, int status, void *data); +typedef void (*internal_proc_cb)(Proc *proc); -struct process { - ProcessType type; +struct proc { + ProcType type; Loop *loop; void *data; int pid, status, refcount; uint8_t exit_signal; // Signal used when killing (on Windows). - uint64_t stopped_time; // process_stop() timestamp + uint64_t stopped_time; // proc_stop() timestamp const char *cwd; char **argv; const char *exepath; dict_T *env; Stream in; RStream out, err; - /// Exit handler. If set, user must call process_free(). - process_exit_cb cb; - internal_process_cb internal_exit_cb, internal_close_cb; + /// Exit handler. If set, user must call proc_free(). + proc_exit_cb cb; + internal_proc_cb internal_exit_cb, internal_close_cb; bool closed, detach, overlapped, fwd_err; MultiQueue *events; }; diff --git a/src/nvim/event/libuv_proc.c b/src/nvim/event/libuv_proc.c @@ -0,0 +1,139 @@ +#include <assert.h> +#include <locale.h> +#include <stdint.h> +#include <uv.h> + +#include "nvim/eval/typval.h" +#include "nvim/event/defs.h" +#include "nvim/event/libuv_proc.h" +#include "nvim/event/loop.h" +#include "nvim/event/proc.h" +#include "nvim/log.h" +#include "nvim/os/os.h" +#include "nvim/os/os_defs.h" +#include "nvim/types_defs.h" +#include "nvim/ui_client.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "event/libuv_proc.c.generated.h" +#endif + +/// @returns zero on success, or negative error code +int libuv_proc_spawn(LibuvProc *uvproc) + FUNC_ATTR_NONNULL_ALL +{ + Proc *proc = (Proc *)uvproc; + uvproc->uvopts.file = proc_get_exepath(proc); + uvproc->uvopts.args = proc->argv; + uvproc->uvopts.flags = UV_PROCESS_WINDOWS_HIDE; +#ifdef MSWIN + // libuv collapses the argv to a CommandLineToArgvW()-style string. cmd.exe + // expects a different syntax (must be prepared by the caller before now). + if (os_shell_is_cmdexe(proc->argv[0])) { + uvproc->uvopts.flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; + } + if (proc->detach) { + uvproc->uvopts.flags |= UV_PROCESS_DETACHED; + } +#else + // Always setsid() on unix-likes. #8107 + uvproc->uvopts.flags |= UV_PROCESS_DETACHED; +#endif + uvproc->uvopts.exit_cb = exit_cb; + uvproc->uvopts.cwd = proc->cwd; + + uvproc->uvopts.stdio = uvproc->uvstdio; + uvproc->uvopts.stdio_count = 3; + uvproc->uvstdio[0].flags = UV_IGNORE; + uvproc->uvstdio[1].flags = UV_IGNORE; + uvproc->uvstdio[2].flags = UV_IGNORE; + + if (ui_client_forward_stdin) { + assert(UI_CLIENT_STDIN_FD == 3); + uvproc->uvopts.stdio_count = 4; + uvproc->uvstdio[3].data.fd = 0; + uvproc->uvstdio[3].flags = UV_INHERIT_FD; + } + uvproc->uv.data = proc; + + if (proc->env) { + uvproc->uvopts.env = tv_dict_to_env(proc->env); + } else { + uvproc->uvopts.env = NULL; + } + + if (!proc->in.closed) { + uvproc->uvstdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; +#ifdef MSWIN + uvproc->uvstdio[0].flags |= proc->overlapped ? UV_OVERLAPPED_PIPE : 0; +#endif + uvproc->uvstdio[0].data.stream = (uv_stream_t *)(&proc->in.uv.pipe); + } + + if (!proc->out.s.closed) { + uvproc->uvstdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; +#ifdef MSWIN + // pipe must be readable for IOCP to work on Windows. + uvproc->uvstdio[1].flags |= proc->overlapped + ? (UV_READABLE_PIPE | UV_OVERLAPPED_PIPE) : 0; +#endif + uvproc->uvstdio[1].data.stream = (uv_stream_t *)(&proc->out.s.uv.pipe); + } + + if (!proc->err.s.closed) { + uvproc->uvstdio[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + uvproc->uvstdio[2].data.stream = (uv_stream_t *)(&proc->err.s.uv.pipe); + } else if (proc->fwd_err) { + uvproc->uvstdio[2].flags = UV_INHERIT_FD; + uvproc->uvstdio[2].data.fd = STDERR_FILENO; + } + + int status; + if ((status = uv_spawn(&proc->loop->uv, &uvproc->uv, &uvproc->uvopts))) { + ILOG("uv_spawn(%s) failed: %s", uvproc->uvopts.file, uv_strerror(status)); + if (uvproc->uvopts.env) { + os_free_fullenv(uvproc->uvopts.env); + } + return status; + } + + proc->pid = uvproc->uv.pid; + return status; +} + +void libuv_proc_close(LibuvProc *uvproc) + FUNC_ATTR_NONNULL_ARG(1) +{ + uv_close((uv_handle_t *)&uvproc->uv, close_cb); +} + +static void close_cb(uv_handle_t *handle) +{ + Proc *proc = handle->data; + if (proc->internal_close_cb) { + proc->internal_close_cb(proc); + } + LibuvProc *uvproc = (LibuvProc *)proc; + if (uvproc->uvopts.env) { + os_free_fullenv(uvproc->uvopts.env); + } +} + +static void exit_cb(uv_process_t *handle, int64_t status, int term_signal) +{ + Proc *proc = handle->data; +#if defined(MSWIN) + // Use stored/expected signal. + term_signal = proc->exit_signal; +#endif + proc->status = term_signal ? 128 + term_signal : (int)status; + proc->internal_exit_cb(proc); +} + +LibuvProc libuv_proc_init(Loop *loop, void *data) +{ + LibuvProc rv = { + .proc = proc_init(loop, kProcTypeUv, data) + }; + return rv; +} diff --git a/src/nvim/event/libuv_proc.h b/src/nvim/event/libuv_proc.h @@ -0,0 +1,16 @@ +#pragma once + +#include <uv.h> + +#include "nvim/event/defs.h" + +typedef struct { + Proc proc; + uv_process_t uv; + uv_process_options_t uvopts; + uv_stdio_container_t uvstdio[4]; +} LibuvProc; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "event/libuv_proc.h.generated.h" +#endif diff --git a/src/nvim/event/libuv_process.c b/src/nvim/event/libuv_process.c @@ -1,139 +0,0 @@ -#include <assert.h> -#include <locale.h> -#include <stdint.h> -#include <uv.h> - -#include "nvim/eval/typval.h" -#include "nvim/event/defs.h" -#include "nvim/event/libuv_process.h" -#include "nvim/event/loop.h" -#include "nvim/event/process.h" -#include "nvim/log.h" -#include "nvim/os/os.h" -#include "nvim/os/os_defs.h" -#include "nvim/types_defs.h" -#include "nvim/ui_client.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "event/libuv_process.c.generated.h" -#endif - -/// @returns zero on success, or negative error code -int libuv_process_spawn(LibuvProcess *uvproc) - FUNC_ATTR_NONNULL_ALL -{ - Process *proc = (Process *)uvproc; - uvproc->uvopts.file = process_get_exepath(proc); - uvproc->uvopts.args = proc->argv; - uvproc->uvopts.flags = UV_PROCESS_WINDOWS_HIDE; -#ifdef MSWIN - // libuv collapses the argv to a CommandLineToArgvW()-style string. cmd.exe - // expects a different syntax (must be prepared by the caller before now). - if (os_shell_is_cmdexe(proc->argv[0])) { - uvproc->uvopts.flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; - } - if (proc->detach) { - uvproc->uvopts.flags |= UV_PROCESS_DETACHED; - } -#else - // Always setsid() on unix-likes. #8107 - uvproc->uvopts.flags |= UV_PROCESS_DETACHED; -#endif - uvproc->uvopts.exit_cb = exit_cb; - uvproc->uvopts.cwd = proc->cwd; - - uvproc->uvopts.stdio = uvproc->uvstdio; - uvproc->uvopts.stdio_count = 3; - uvproc->uvstdio[0].flags = UV_IGNORE; - uvproc->uvstdio[1].flags = UV_IGNORE; - uvproc->uvstdio[2].flags = UV_IGNORE; - - if (ui_client_forward_stdin) { - assert(UI_CLIENT_STDIN_FD == 3); - uvproc->uvopts.stdio_count = 4; - uvproc->uvstdio[3].data.fd = 0; - uvproc->uvstdio[3].flags = UV_INHERIT_FD; - } - uvproc->uv.data = proc; - - if (proc->env) { - uvproc->uvopts.env = tv_dict_to_env(proc->env); - } else { - uvproc->uvopts.env = NULL; - } - - if (!proc->in.closed) { - uvproc->uvstdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; -#ifdef MSWIN - uvproc->uvstdio[0].flags |= proc->overlapped ? UV_OVERLAPPED_PIPE : 0; -#endif - uvproc->uvstdio[0].data.stream = (uv_stream_t *)(&proc->in.uv.pipe); - } - - if (!proc->out.s.closed) { - uvproc->uvstdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; -#ifdef MSWIN - // pipe must be readable for IOCP to work on Windows. - uvproc->uvstdio[1].flags |= proc->overlapped - ? (UV_READABLE_PIPE | UV_OVERLAPPED_PIPE) : 0; -#endif - uvproc->uvstdio[1].data.stream = (uv_stream_t *)(&proc->out.s.uv.pipe); - } - - if (!proc->err.s.closed) { - uvproc->uvstdio[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; - uvproc->uvstdio[2].data.stream = (uv_stream_t *)(&proc->err.s.uv.pipe); - } else if (proc->fwd_err) { - uvproc->uvstdio[2].flags = UV_INHERIT_FD; - uvproc->uvstdio[2].data.fd = STDERR_FILENO; - } - - int status; - if ((status = uv_spawn(&proc->loop->uv, &uvproc->uv, &uvproc->uvopts))) { - ILOG("uv_spawn(%s) failed: %s", uvproc->uvopts.file, uv_strerror(status)); - if (uvproc->uvopts.env) { - os_free_fullenv(uvproc->uvopts.env); - } - return status; - } - - proc->pid = uvproc->uv.pid; - return status; -} - -void libuv_process_close(LibuvProcess *uvproc) - FUNC_ATTR_NONNULL_ARG(1) -{ - uv_close((uv_handle_t *)&uvproc->uv, close_cb); -} - -static void close_cb(uv_handle_t *handle) -{ - Process *proc = handle->data; - if (proc->internal_close_cb) { - proc->internal_close_cb(proc); - } - LibuvProcess *uvproc = (LibuvProcess *)proc; - if (uvproc->uvopts.env) { - os_free_fullenv(uvproc->uvopts.env); - } -} - -static void exit_cb(uv_process_t *handle, int64_t status, int term_signal) -{ - Process *proc = handle->data; -#if defined(MSWIN) - // Use stored/expected signal. - term_signal = proc->exit_signal; -#endif - proc->status = term_signal ? 128 + term_signal : (int)status; - proc->internal_exit_cb(proc); -} - -LibuvProcess libuv_process_init(Loop *loop, void *data) -{ - LibuvProcess rv = { - .process = process_init(loop, kProcessTypeUv, data) - }; - return rv; -} diff --git a/src/nvim/event/libuv_process.h b/src/nvim/event/libuv_process.h @@ -1,16 +0,0 @@ -#pragma once - -#include <uv.h> - -#include "nvim/event/defs.h" - -typedef struct { - Process process; - uv_process_t uv; - uv_process_options_t uvopts; - uv_stdio_container_t uvstdio[4]; -} LibuvProcess; - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "event/libuv_process.h.generated.h" -#endif diff --git a/src/nvim/event/proc.c b/src/nvim/event/proc.c @@ -0,0 +1,451 @@ +#include <assert.h> +#include <inttypes.h> +#include <signal.h> +#include <uv.h> + +#include "klib/klist.h" +#include "nvim/event/libuv_proc.h" +#include "nvim/event/loop.h" +#include "nvim/event/multiqueue.h" +#include "nvim/event/proc.h" +#include "nvim/event/rstream.h" +#include "nvim/event/stream.h" +#include "nvim/event/wstream.h" +#include "nvim/globals.h" +#include "nvim/log.h" +#include "nvim/main.h" +#include "nvim/os/proc.h" +#include "nvim/os/pty_proc.h" +#include "nvim/os/shell.h" +#include "nvim/os/time.h" +#include "nvim/ui_client.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "event/proc.c.generated.h" +#endif + +// Time for a process to exit cleanly before we send KILL. +// For PTY processes SIGTERM is sent first (in case SIGHUP was not enough). +#define KILL_TIMEOUT_MS 2000 + +/// Externally defined with gcov. +#ifdef USE_GCOV +void __gcov_flush(void); +#endif + +static bool proc_is_tearing_down = false; + +// Delay exit until handles are closed, to avoid deadlocks +static int exit_need_delay = 0; + +/// @returns zero on success, or negative error code +int proc_spawn(Proc *proc, bool in, bool out, bool err) + FUNC_ATTR_NONNULL_ALL +{ + // forwarding stderr contradicts with processing it internally + assert(!(err && proc->fwd_err)); + + if (in) { + uv_pipe_init(&proc->loop->uv, &proc->in.uv.pipe, 0); + } else { + proc->in.closed = true; + } + + if (out) { + uv_pipe_init(&proc->loop->uv, &proc->out.s.uv.pipe, 0); + } else { + proc->out.s.closed = true; + } + + if (err) { + uv_pipe_init(&proc->loop->uv, &proc->err.s.uv.pipe, 0); + } else { + proc->err.s.closed = true; + } + +#ifdef USE_GCOV + // Flush coverage data before forking, to avoid "Merge mismatch" errors. + __gcov_flush(); +#endif + + int status; + switch (proc->type) { + case kProcTypeUv: + status = libuv_proc_spawn((LibuvProc *)proc); + break; + case kProcTypePty: + status = pty_proc_spawn((PtyProc *)proc); + break; + } + + if (status) { + if (in) { + uv_close((uv_handle_t *)&proc->in.uv.pipe, NULL); + } + if (out) { + uv_close((uv_handle_t *)&proc->out.s.uv.pipe, NULL); + } + if (err) { + uv_close((uv_handle_t *)&proc->err.s.uv.pipe, NULL); + } + + if (proc->type == kProcTypeUv) { + uv_close((uv_handle_t *)&(((LibuvProc *)proc)->uv), NULL); + } else { + proc_close(proc); + } + proc_free(proc); + proc->status = -1; + return status; + } + + if (in) { + stream_init(NULL, &proc->in, -1, (uv_stream_t *)&proc->in.uv.pipe); + proc->in.internal_data = proc; + proc->in.internal_close_cb = on_proc_stream_close; + proc->refcount++; + } + + if (out) { + stream_init(NULL, &proc->out.s, -1, (uv_stream_t *)&proc->out.s.uv.pipe); + proc->out.s.internal_data = proc; + proc->out.s.internal_close_cb = on_proc_stream_close; + proc->refcount++; + } + + if (err) { + stream_init(NULL, &proc->err.s, -1, (uv_stream_t *)&proc->err.s.uv.pipe); + proc->err.s.internal_data = proc; + proc->err.s.internal_close_cb = on_proc_stream_close; + proc->refcount++; + } + + proc->internal_exit_cb = on_proc_exit; + proc->internal_close_cb = decref; + proc->refcount++; + kl_push(WatcherPtr, proc->loop->children, proc); + DLOG("new: pid=%d exepath=[%s]", proc->pid, proc_get_exepath(proc)); + return 0; +} + +void proc_teardown(Loop *loop) FUNC_ATTR_NONNULL_ALL +{ + proc_is_tearing_down = true; + kl_iter(WatcherPtr, loop->children, current) { + Proc *proc = (*current)->data; + if (proc->detach || proc->type == kProcTypePty) { + // Close handles to process without killing it. + CREATE_EVENT(loop->events, proc_close_handles, proc); + } else { + proc_stop(proc); + } + } + + // Wait until all children exit and all close events are processed. + LOOP_PROCESS_EVENTS_UNTIL(loop, loop->events, -1, + kl_empty(loop->children) && multiqueue_empty(loop->events)); + pty_proc_teardown(loop); +} + +void proc_close_streams(Proc *proc) FUNC_ATTR_NONNULL_ALL +{ + wstream_may_close(&proc->in); + rstream_may_close(&proc->out); + rstream_may_close(&proc->err); +} + +/// Synchronously wait for a process to finish +/// +/// @param process Process instance +/// @param ms Time in milliseconds to wait for the process. +/// 0 for no wait. -1 to wait until the process quits. +/// @return Exit code of the process. proc->status will have the same value. +/// -1 if the timeout expired while the process is still running. +/// -2 if the user interrupted the wait. +int proc_wait(Proc *proc, int ms, MultiQueue *events) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (!proc->refcount) { + int status = proc->status; + LOOP_PROCESS_EVENTS(proc->loop, proc->events, 0); + return status; + } + + if (!events) { + events = proc->events; + } + + // Increase refcount to stop the exit callback from being called (and possibly + // freed) before we have a chance to get the status. + proc->refcount++; + LOOP_PROCESS_EVENTS_UNTIL(proc->loop, events, ms, + // Until... + got_int // interrupted by the user + || proc->refcount == 1); // job exited + + // Assume that a user hitting CTRL-C does not like the current job. Kill it. + if (got_int) { + got_int = false; + proc_stop(proc); + if (ms == -1) { + // We can only return if all streams/handles are closed and the job + // exited. + LOOP_PROCESS_EVENTS_UNTIL(proc->loop, events, -1, + proc->refcount == 1); + } else { + LOOP_PROCESS_EVENTS(proc->loop, events, 0); + } + + proc->status = -2; + } + + if (proc->refcount == 1) { + // Job exited, free its resources. + decref(proc); + if (proc->events) { + // decref() created an exit event, process it now. + multiqueue_process_events(proc->events); + } + } else { + proc->refcount--; + } + + return proc->status; +} + +/// Ask a process to terminate and eventually kill if it doesn't respond +void proc_stop(Proc *proc) FUNC_ATTR_NONNULL_ALL +{ + bool exited = (proc->status >= 0); + if (exited || proc->stopped_time) { + return; + } + proc->stopped_time = os_hrtime(); + proc->exit_signal = SIGTERM; + + switch (proc->type) { + case kProcTypeUv: + os_proc_tree_kill(proc->pid, SIGTERM); + break; + case kProcTypePty: + // close all streams for pty processes to send SIGHUP to the process + proc_close_streams(proc); + pty_proc_close_master((PtyProc *)proc); + break; + } + + // (Re)start timer to verify that stopped process(es) died. + uv_timer_start(&proc->loop->children_kill_timer, children_kill_cb, + KILL_TIMEOUT_MS, 0); +} + +/// Frees process-owned resources. +void proc_free(Proc *proc) FUNC_ATTR_NONNULL_ALL +{ + if (proc->argv != NULL) { + shell_free_argv(proc->argv); + proc->argv = NULL; + } +} + +/// Sends SIGKILL (or SIGTERM..SIGKILL for PTY jobs) to processes that did +/// not terminate after proc_stop(). +static void children_kill_cb(uv_timer_t *handle) +{ + Loop *loop = handle->loop->data; + + kl_iter(WatcherPtr, loop->children, current) { + Proc *proc = (*current)->data; + bool exited = (proc->status >= 0); + if (exited || !proc->stopped_time) { + continue; + } + uint64_t term_sent = UINT64_MAX == proc->stopped_time; + if (kProcTypePty != proc->type || term_sent) { + proc->exit_signal = SIGKILL; + os_proc_tree_kill(proc->pid, SIGKILL); + } else { + proc->exit_signal = SIGTERM; + os_proc_tree_kill(proc->pid, SIGTERM); + proc->stopped_time = UINT64_MAX; // Flag: SIGTERM was sent. + // Restart timer. + uv_timer_start(&proc->loop->children_kill_timer, children_kill_cb, + KILL_TIMEOUT_MS, 0); + } + } +} + +static void proc_close_event(void **argv) +{ + Proc *proc = argv[0]; + if (proc->cb) { + // User (hint: channel_job_start) is responsible for calling + // proc_free(). + proc->cb(proc, proc->status, proc->data); + } else { + proc_free(proc); + } +} + +static void decref(Proc *proc) +{ + if (--proc->refcount != 0) { + return; + } + + Loop *loop = proc->loop; + kliter_t(WatcherPtr) **node = NULL; + kl_iter(WatcherPtr, loop->children, current) { + if ((*current)->data == proc) { + node = current; + break; + } + } + assert(node); + kl_shift_at(WatcherPtr, loop->children, node); + CREATE_EVENT(proc->events, proc_close_event, proc); +} + +static void proc_close(Proc *proc) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (proc_is_tearing_down && (proc->detach || proc->type == kProcTypePty) + && proc->closed) { + // If a detached/pty process dies while tearing down it might get closed + // twice. + return; + } + assert(!proc->closed); + proc->closed = true; + + if (proc->detach) { + if (proc->type == kProcTypeUv) { + uv_unref((uv_handle_t *)&(((LibuvProc *)proc)->uv)); + } + } + + switch (proc->type) { + case kProcTypeUv: + libuv_proc_close((LibuvProc *)proc); + break; + case kProcTypePty: + pty_proc_close((PtyProc *)proc); + break; + } +} + +/// Flush output stream. +/// +/// @param proc Process, for which an output stream should be flushed. +/// @param stream Stream to flush. +static void flush_stream(Proc *proc, RStream *stream) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (!stream || stream->s.closed) { + return; + } + + // Maximal remaining data size of terminated process is system + // buffer size. + // Also helps with a child process that keeps the output streams open. If it + // keeps sending data, we only accept as much data as the system buffer size. + // Otherwise this would block cleanup/teardown. + int system_buffer_size = 0; + int err = uv_recv_buffer_size((uv_handle_t *)&stream->s.uv.pipe, + &system_buffer_size); + if (err) { + system_buffer_size = ARENA_BLOCK_SIZE; + } + + size_t max_bytes = stream->num_bytes + (size_t)system_buffer_size; + + // Read remaining data. + while (!stream->s.closed && stream->num_bytes < max_bytes) { + // Remember number of bytes before polling + size_t num_bytes = stream->num_bytes; + + // Poll for data and process the generated events. + loop_poll_events(proc->loop, 0); + if (stream->s.events) { + multiqueue_process_events(stream->s.events); + } + + // Stream can be closed if it is empty. + if (num_bytes == stream->num_bytes) { + if (stream->read_cb && !stream->did_eof) { + // Stream callback could miss EOF handling if a child keeps the stream + // open. But only send EOF if we haven't already. + stream->read_cb(stream, stream->buffer, 0, stream->s.cb_data, true); + } + break; + } + } +} + +static void proc_close_handles(void **argv) +{ + Proc *proc = argv[0]; + + exit_need_delay++; + flush_stream(proc, &proc->out); + flush_stream(proc, &proc->err); + + proc_close_streams(proc); + proc_close(proc); + exit_need_delay--; +} + +static void exit_delay_cb(uv_timer_t *handle) +{ + uv_timer_stop(&main_loop.exit_delay_timer); + multiqueue_put(main_loop.fast_events, exit_event, main_loop.exit_delay_timer.data); +} + +static void exit_event(void **argv) +{ + int status = (int)(intptr_t)argv[0]; + if (exit_need_delay) { + main_loop.exit_delay_timer.data = argv[0]; + uv_timer_start(&main_loop.exit_delay_timer, exit_delay_cb, 0, 0); + return; + } + + if (!exiting) { + if (ui_client_channel_id) { + ui_client_exit_status = status; + os_exit(status); + } else { + assert(status == 0); // Called from rpc_close(), which passes 0 as status. + preserve_exit(NULL); + } + } +} + +void exit_from_channel(int status) +{ + multiqueue_put(main_loop.fast_events, exit_event, (void *)(intptr_t)status); +} + +static void on_proc_exit(Proc *proc) +{ + Loop *loop = proc->loop; + ILOG("exited: pid=%d status=%d stoptime=%" PRIu64, proc->pid, proc->status, + proc->stopped_time); + + if (ui_client_channel_id) { + exit_from_channel(proc->status); + } + + // Process has terminated, but there could still be data to be read from the + // OS. We are still in the libuv loop, so we cannot call code that polls for + // more data directly. Instead delay the reading after the libuv loop by + // queueing proc_close_handles() as an event. + MultiQueue *queue = proc->events ? proc->events : loop->events; + CREATE_EVENT(queue, proc_close_handles, proc); +} + +static void on_proc_stream_close(Stream *stream, void *data) +{ + Proc *proc = data; + decref(proc); +} diff --git a/src/nvim/event/proc.h b/src/nvim/event/proc.h @@ -0,0 +1,49 @@ +#pragma once + +#include <stdbool.h> +#include <stddef.h> + +#include "nvim/event/defs.h" // IWYU pragma: keep +#include "nvim/types_defs.h" + +static inline Proc proc_init(Loop *loop, ProcType type, void *data) +{ + return (Proc) { + .type = type, + .data = data, + .loop = loop, + .events = NULL, + .pid = 0, + .status = -1, + .refcount = 0, + .stopped_time = 0, + .cwd = NULL, + .argv = NULL, + .exepath = NULL, + .in = { .closed = false }, + .out = { .s.closed = false }, + .err = { .s.closed = false }, + .cb = NULL, + .closed = false, + .internal_close_cb = NULL, + .internal_exit_cb = NULL, + .detach = false, + .fwd_err = false, + }; +} + +/// Get the path to the executable of the process. +static inline const char *proc_get_exepath(Proc *proc) +{ + return proc->exepath != NULL ? proc->exepath : proc->argv[0]; +} + +static inline bool proc_is_stopped(Proc *proc) +{ + bool exited = (proc->status >= 0); + return exited || (proc->stopped_time != 0); +} + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "event/proc.h.generated.h" +#endif diff --git a/src/nvim/event/process.c b/src/nvim/event/process.c @@ -1,451 +0,0 @@ -#include <assert.h> -#include <inttypes.h> -#include <signal.h> -#include <uv.h> - -#include "klib/klist.h" -#include "nvim/event/libuv_process.h" -#include "nvim/event/loop.h" -#include "nvim/event/multiqueue.h" -#include "nvim/event/process.h" -#include "nvim/event/rstream.h" -#include "nvim/event/stream.h" -#include "nvim/event/wstream.h" -#include "nvim/globals.h" -#include "nvim/log.h" -#include "nvim/main.h" -#include "nvim/os/process.h" -#include "nvim/os/pty_process.h" -#include "nvim/os/shell.h" -#include "nvim/os/time.h" -#include "nvim/ui_client.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "event/process.c.generated.h" -#endif - -// Time for a process to exit cleanly before we send KILL. -// For PTY processes SIGTERM is sent first (in case SIGHUP was not enough). -#define KILL_TIMEOUT_MS 2000 - -/// Externally defined with gcov. -#ifdef USE_GCOV -void __gcov_flush(void); -#endif - -static bool process_is_tearing_down = false; - -// Delay exit until handles are closed, to avoid deadlocks -static int exit_need_delay = 0; - -/// @returns zero on success, or negative error code -int process_spawn(Process *proc, bool in, bool out, bool err) - FUNC_ATTR_NONNULL_ALL -{ - // forwarding stderr contradicts with processing it internally - assert(!(err && proc->fwd_err)); - - if (in) { - uv_pipe_init(&proc->loop->uv, &proc->in.uv.pipe, 0); - } else { - proc->in.closed = true; - } - - if (out) { - uv_pipe_init(&proc->loop->uv, &proc->out.s.uv.pipe, 0); - } else { - proc->out.s.closed = true; - } - - if (err) { - uv_pipe_init(&proc->loop->uv, &proc->err.s.uv.pipe, 0); - } else { - proc->err.s.closed = true; - } - -#ifdef USE_GCOV - // Flush coverage data before forking, to avoid "Merge mismatch" errors. - __gcov_flush(); -#endif - - int status; - switch (proc->type) { - case kProcessTypeUv: - status = libuv_process_spawn((LibuvProcess *)proc); - break; - case kProcessTypePty: - status = pty_process_spawn((PtyProcess *)proc); - break; - } - - if (status) { - if (in) { - uv_close((uv_handle_t *)&proc->in.uv.pipe, NULL); - } - if (out) { - uv_close((uv_handle_t *)&proc->out.s.uv.pipe, NULL); - } - if (err) { - uv_close((uv_handle_t *)&proc->err.s.uv.pipe, NULL); - } - - if (proc->type == kProcessTypeUv) { - uv_close((uv_handle_t *)&(((LibuvProcess *)proc)->uv), NULL); - } else { - process_close(proc); - } - process_free(proc); - proc->status = -1; - return status; - } - - if (in) { - stream_init(NULL, &proc->in, -1, (uv_stream_t *)&proc->in.uv.pipe); - proc->in.internal_data = proc; - proc->in.internal_close_cb = on_process_stream_close; - proc->refcount++; - } - - if (out) { - stream_init(NULL, &proc->out.s, -1, (uv_stream_t *)&proc->out.s.uv.pipe); - proc->out.s.internal_data = proc; - proc->out.s.internal_close_cb = on_process_stream_close; - proc->refcount++; - } - - if (err) { - stream_init(NULL, &proc->err.s, -1, (uv_stream_t *)&proc->err.s.uv.pipe); - proc->err.s.internal_data = proc; - proc->err.s.internal_close_cb = on_process_stream_close; - proc->refcount++; - } - - proc->internal_exit_cb = on_process_exit; - proc->internal_close_cb = decref; - proc->refcount++; - kl_push(WatcherPtr, proc->loop->children, proc); - DLOG("new: pid=%d exepath=[%s]", proc->pid, process_get_exepath(proc)); - return 0; -} - -void process_teardown(Loop *loop) FUNC_ATTR_NONNULL_ALL -{ - process_is_tearing_down = true; - kl_iter(WatcherPtr, loop->children, current) { - Process *proc = (*current)->data; - if (proc->detach || proc->type == kProcessTypePty) { - // Close handles to process without killing it. - CREATE_EVENT(loop->events, process_close_handles, proc); - } else { - process_stop(proc); - } - } - - // Wait until all children exit and all close events are processed. - LOOP_PROCESS_EVENTS_UNTIL(loop, loop->events, -1, - kl_empty(loop->children) && multiqueue_empty(loop->events)); - pty_process_teardown(loop); -} - -void process_close_streams(Process *proc) FUNC_ATTR_NONNULL_ALL -{ - wstream_may_close(&proc->in); - rstream_may_close(&proc->out); - rstream_may_close(&proc->err); -} - -/// Synchronously wait for a process to finish -/// -/// @param process Process instance -/// @param ms Time in milliseconds to wait for the process. -/// 0 for no wait. -1 to wait until the process quits. -/// @return Exit code of the process. proc->status will have the same value. -/// -1 if the timeout expired while the process is still running. -/// -2 if the user interrupted the wait. -int process_wait(Process *proc, int ms, MultiQueue *events) - FUNC_ATTR_NONNULL_ARG(1) -{ - if (!proc->refcount) { - int status = proc->status; - LOOP_PROCESS_EVENTS(proc->loop, proc->events, 0); - return status; - } - - if (!events) { - events = proc->events; - } - - // Increase refcount to stop the exit callback from being called (and possibly - // freed) before we have a chance to get the status. - proc->refcount++; - LOOP_PROCESS_EVENTS_UNTIL(proc->loop, events, ms, - // Until... - got_int // interrupted by the user - || proc->refcount == 1); // job exited - - // Assume that a user hitting CTRL-C does not like the current job. Kill it. - if (got_int) { - got_int = false; - process_stop(proc); - if (ms == -1) { - // We can only return if all streams/handles are closed and the job - // exited. - LOOP_PROCESS_EVENTS_UNTIL(proc->loop, events, -1, - proc->refcount == 1); - } else { - LOOP_PROCESS_EVENTS(proc->loop, events, 0); - } - - proc->status = -2; - } - - if (proc->refcount == 1) { - // Job exited, free its resources. - decref(proc); - if (proc->events) { - // decref() created an exit event, process it now. - multiqueue_process_events(proc->events); - } - } else { - proc->refcount--; - } - - return proc->status; -} - -/// Ask a process to terminate and eventually kill if it doesn't respond -void process_stop(Process *proc) FUNC_ATTR_NONNULL_ALL -{ - bool exited = (proc->status >= 0); - if (exited || proc->stopped_time) { - return; - } - proc->stopped_time = os_hrtime(); - proc->exit_signal = SIGTERM; - - switch (proc->type) { - case kProcessTypeUv: - os_proc_tree_kill(proc->pid, SIGTERM); - break; - case kProcessTypePty: - // close all streams for pty processes to send SIGHUP to the process - process_close_streams(proc); - pty_process_close_master((PtyProcess *)proc); - break; - } - - // (Re)start timer to verify that stopped process(es) died. - uv_timer_start(&proc->loop->children_kill_timer, children_kill_cb, - KILL_TIMEOUT_MS, 0); -} - -/// Frees process-owned resources. -void process_free(Process *proc) FUNC_ATTR_NONNULL_ALL -{ - if (proc->argv != NULL) { - shell_free_argv(proc->argv); - proc->argv = NULL; - } -} - -/// Sends SIGKILL (or SIGTERM..SIGKILL for PTY jobs) to processes that did -/// not terminate after process_stop(). -static void children_kill_cb(uv_timer_t *handle) -{ - Loop *loop = handle->loop->data; - - kl_iter(WatcherPtr, loop->children, current) { - Process *proc = (*current)->data; - bool exited = (proc->status >= 0); - if (exited || !proc->stopped_time) { - continue; - } - uint64_t term_sent = UINT64_MAX == proc->stopped_time; - if (kProcessTypePty != proc->type || term_sent) { - proc->exit_signal = SIGKILL; - os_proc_tree_kill(proc->pid, SIGKILL); - } else { - proc->exit_signal = SIGTERM; - os_proc_tree_kill(proc->pid, SIGTERM); - proc->stopped_time = UINT64_MAX; // Flag: SIGTERM was sent. - // Restart timer. - uv_timer_start(&proc->loop->children_kill_timer, children_kill_cb, - KILL_TIMEOUT_MS, 0); - } - } -} - -static void process_close_event(void **argv) -{ - Process *proc = argv[0]; - if (proc->cb) { - // User (hint: channel_job_start) is responsible for calling - // process_free(). - proc->cb(proc, proc->status, proc->data); - } else { - process_free(proc); - } -} - -static void decref(Process *proc) -{ - if (--proc->refcount != 0) { - return; - } - - Loop *loop = proc->loop; - kliter_t(WatcherPtr) **node = NULL; - kl_iter(WatcherPtr, loop->children, current) { - if ((*current)->data == proc) { - node = current; - break; - } - } - assert(node); - kl_shift_at(WatcherPtr, loop->children, node); - CREATE_EVENT(proc->events, process_close_event, proc); -} - -static void process_close(Process *proc) - FUNC_ATTR_NONNULL_ARG(1) -{ - if (process_is_tearing_down && (proc->detach || proc->type == kProcessTypePty) - && proc->closed) { - // If a detached/pty process dies while tearing down it might get closed - // twice. - return; - } - assert(!proc->closed); - proc->closed = true; - - if (proc->detach) { - if (proc->type == kProcessTypeUv) { - uv_unref((uv_handle_t *)&(((LibuvProcess *)proc)->uv)); - } - } - - switch (proc->type) { - case kProcessTypeUv: - libuv_process_close((LibuvProcess *)proc); - break; - case kProcessTypePty: - pty_process_close((PtyProcess *)proc); - break; - } -} - -/// Flush output stream. -/// -/// @param proc Process, for which an output stream should be flushed. -/// @param stream Stream to flush. -static void flush_stream(Process *proc, RStream *stream) - FUNC_ATTR_NONNULL_ARG(1) -{ - if (!stream || stream->s.closed) { - return; - } - - // Maximal remaining data size of terminated process is system - // buffer size. - // Also helps with a child process that keeps the output streams open. If it - // keeps sending data, we only accept as much data as the system buffer size. - // Otherwise this would block cleanup/teardown. - int system_buffer_size = 0; - int err = uv_recv_buffer_size((uv_handle_t *)&stream->s.uv.pipe, - &system_buffer_size); - if (err) { - system_buffer_size = ARENA_BLOCK_SIZE; - } - - size_t max_bytes = stream->num_bytes + (size_t)system_buffer_size; - - // Read remaining data. - while (!stream->s.closed && stream->num_bytes < max_bytes) { - // Remember number of bytes before polling - size_t num_bytes = stream->num_bytes; - - // Poll for data and process the generated events. - loop_poll_events(proc->loop, 0); - if (stream->s.events) { - multiqueue_process_events(stream->s.events); - } - - // Stream can be closed if it is empty. - if (num_bytes == stream->num_bytes) { - if (stream->read_cb && !stream->did_eof) { - // Stream callback could miss EOF handling if a child keeps the stream - // open. But only send EOF if we haven't already. - stream->read_cb(stream, stream->buffer, 0, stream->s.cb_data, true); - } - break; - } - } -} - -static void process_close_handles(void **argv) -{ - Process *proc = argv[0]; - - exit_need_delay++; - flush_stream(proc, &proc->out); - flush_stream(proc, &proc->err); - - process_close_streams(proc); - process_close(proc); - exit_need_delay--; -} - -static void exit_delay_cb(uv_timer_t *handle) -{ - uv_timer_stop(&main_loop.exit_delay_timer); - multiqueue_put(main_loop.fast_events, exit_event, main_loop.exit_delay_timer.data); -} - -static void exit_event(void **argv) -{ - int status = (int)(intptr_t)argv[0]; - if (exit_need_delay) { - main_loop.exit_delay_timer.data = argv[0]; - uv_timer_start(&main_loop.exit_delay_timer, exit_delay_cb, 0, 0); - return; - } - - if (!exiting) { - if (ui_client_channel_id) { - ui_client_exit_status = status; - os_exit(status); - } else { - assert(status == 0); // Called from rpc_close(), which passes 0 as status. - preserve_exit(NULL); - } - } -} - -void exit_from_channel(int status) -{ - multiqueue_put(main_loop.fast_events, exit_event, (void *)(intptr_t)status); -} - -static void on_process_exit(Process *proc) -{ - Loop *loop = proc->loop; - ILOG("exited: pid=%d status=%d stoptime=%" PRIu64, proc->pid, proc->status, - proc->stopped_time); - - if (ui_client_channel_id) { - exit_from_channel(proc->status); - } - - // Process has terminated, but there could still be data to be read from the - // OS. We are still in the libuv loop, so we cannot call code that polls for - // more data directly. Instead delay the reading after the libuv loop by - // queueing process_close_handles() as an event. - MultiQueue *queue = proc->events ? proc->events : loop->events; - CREATE_EVENT(queue, process_close_handles, proc); -} - -static void on_process_stream_close(Stream *stream, void *data) -{ - Process *proc = data; - decref(proc); -} diff --git a/src/nvim/event/process.h b/src/nvim/event/process.h @@ -1,49 +0,0 @@ -#pragma once - -#include <stdbool.h> -#include <stddef.h> - -#include "nvim/event/defs.h" // IWYU pragma: keep -#include "nvim/types_defs.h" - -static inline Process process_init(Loop *loop, ProcessType type, void *data) -{ - return (Process) { - .type = type, - .data = data, - .loop = loop, - .events = NULL, - .pid = 0, - .status = -1, - .refcount = 0, - .stopped_time = 0, - .cwd = NULL, - .argv = NULL, - .exepath = NULL, - .in = { .closed = false }, - .out = { .s.closed = false }, - .err = { .s.closed = false }, - .cb = NULL, - .closed = false, - .internal_close_cb = NULL, - .internal_exit_cb = NULL, - .detach = false, - .fwd_err = false, - }; -} - -/// Get the path to the executable of the process. -static inline const char *process_get_exepath(Process *proc) -{ - return proc->exepath != NULL ? proc->exepath : proc->argv[0]; -} - -static inline bool process_is_stopped(Process *proc) -{ - bool exited = (proc->status >= 0); - return exited || (proc->stopped_time != 0); -} - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "event/process.h.generated.h" -#endif diff --git a/src/nvim/main.c b/src/nvim/main.c @@ -43,7 +43,7 @@ #include "nvim/eval/userfunc.h" #include "nvim/event/loop.h" #include "nvim/event/multiqueue.h" -#include "nvim/event/process.h" +#include "nvim/event/proc.h" #include "nvim/event/stream.h" #include "nvim/ex_cmds.h" #include "nvim/ex_docmd.h" @@ -174,7 +174,7 @@ bool event_teardown(void) loop_poll_events(&main_loop, 0); // Drain thread_events, fast_events. input_stop(); channel_teardown(); - process_teardown(&main_loop); + proc_teardown(&main_loop); timer_teardown(); server_teardown(); signal_teardown(); @@ -2207,7 +2207,7 @@ static void usage(void) printf(_(" --headless Don't start a user interface\n")); printf(_(" --listen <address> Serve RPC API from this address\n")); printf(_(" --remote[-subcommand] Execute commands remotely on a server\n")); - printf(_(" --server <address> Specify RPC server to send commands to\n")); + printf(_(" --server <address> Connect to this Nvim server\n")); printf(_(" --startuptime <file> Write startup timing messages to <file>\n")); printf(_("\nSee \":help startup-options\" for all options.\n")); } diff --git a/src/nvim/memline.c b/src/nvim/memline.c @@ -84,7 +84,7 @@ #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/os/os_defs.h" -#include "nvim/os/process.h" +#include "nvim/os/proc.h" #include "nvim/os/time.h" #include "nvim/os/time_defs.h" #include "nvim/path.h" @@ -743,7 +743,7 @@ static void add_b0_fenc(ZeroBlock *b0p, buf_T *buf) /// @param swap_fname Name of the swapfile. If it's from before a reboot, the result is 0. /// /// @return PID, or 0 if process is not running or the swapfile is from before a reboot. -static int swapfile_process_running(const ZeroBlock *b0p, const char *swap_fname) +static int swapfile_proc_running(const ZeroBlock *b0p, const char *swap_fname) { FileInfo st; double uptime; @@ -1214,7 +1214,7 @@ void ml_recover(bool checkext) msg(_("Recovery completed. Buffer contents equals file contents."), 0); } msg_puts(_("\nYou may want to delete the .swp file now.")); - if (swapfile_process_running(b0p, fname_used)) { + if (swapfile_proc_running(b0p, fname_used)) { // Warn there could be an active Vim on the same file, the user may // want to kill it. msg_puts(_("\nNote: process STILL RUNNING: ")); @@ -1462,7 +1462,7 @@ char *make_percent_swname(char *dir, char *dir_end, const char *name) } // PID of swapfile owner, or zero if not running. -static int process_running; +static int proc_running; /// For Vimscript "swapinfo()". /// @@ -1488,7 +1488,7 @@ void swapfile_dict(const char *fname, dict_T *d) tv_dict_add_str_len(d, S_LEN("fname"), b0.b0_fname, B0_FNAME_SIZE_ORG); - tv_dict_add_nr(d, S_LEN("pid"), swapfile_process_running(&b0, fname)); + tv_dict_add_nr(d, S_LEN("pid"), swapfile_proc_running(&b0, fname)); tv_dict_add_nr(d, S_LEN("mtime"), char_to_long(b0.b0_mtime)); tv_dict_add_nr(d, S_LEN("dirty"), b0.b0_dirty ? 1 : 0); tv_dict_add_nr(d, S_LEN("inode"), char_to_long(b0.b0_ino)); @@ -1572,7 +1572,7 @@ static time_t swapfile_info(char *fname) if (char_to_long(b0.b0_pid) != 0) { msg_puts(_("\n process ID: ")); msg_outnum((int)char_to_long(b0.b0_pid)); - if ((process_running = swapfile_process_running(&b0, fname))) { + if ((proc_running = swapfile_proc_running(&b0, fname))) { msg_puts(_(" (STILL RUNNING)")); } } @@ -1640,7 +1640,7 @@ static bool swapfile_unchanged(char *fname) } // process must be known and not running. - if (char_to_long(b0.b0_pid) == 0 || swapfile_process_running(&b0, fname)) { + if (char_to_long(b0.b0_pid) == 0 || swapfile_proc_running(&b0, fname)) { ret = false; } @@ -3399,7 +3399,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_ fd = os_open(fname, O_RDONLY, 0); if (fd >= 0) { if (read_eintr(fd, &b0, sizeof(b0)) == sizeof(b0)) { - process_running = swapfile_process_running(&b0, fname); + proc_running = swapfile_proc_running(&b0, fname); // If the swapfile has the same directory as the // buffer don't compare the directory names, they can @@ -3459,7 +3459,7 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_ choice = SEA_CHOICE_READONLY; } - process_running = 0; // Set by attention_message..swapfile_info. + proc_running = 0; // Set by attention_message..swapfile_info. if (choice == SEA_CHOICE_NONE) { // Show info about the existing swapfile. attention_message(buf, fname); @@ -3491,12 +3491,12 @@ static char *findswapname(buf_T *buf, char **dirp, char *old_fname, bool *found_ = do_dialog(VIM_WARNING, _("VIM - ATTENTION"), name, - process_running + proc_running ? _("&Open Read-Only\n&Edit anyway\n&Recover\n&Quit\n&Abort") : _("&Open Read-Only\n&Edit anyway\n&Recover\n&Delete it\n&Quit\n&Abort"), 1, NULL, false); - if (process_running && dialog_result >= 4) { + if (proc_running && dialog_result >= 4) { // compensate for missing "Delete it" button dialog_result++; } diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c @@ -14,7 +14,7 @@ #include "nvim/event/defs.h" #include "nvim/event/loop.h" #include "nvim/event/multiqueue.h" -#include "nvim/event/process.h" +#include "nvim/event/proc.h" #include "nvim/event/rstream.h" #include "nvim/event/wstream.h" #include "nvim/globals.h" diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c @@ -344,7 +344,7 @@ char *os_getenvname_at_index(size_t index) #endif } -/// Get the process ID of the Neovim process. +/// Get the process ID of the Nvim process. /// /// @return the process ID. int64_t os_get_pid(void) diff --git a/src/nvim/os/proc.c b/src/nvim/os/proc.c @@ -0,0 +1,286 @@ +/// OS process functions +/// +/// psutil is a good reference for cross-platform syscall voodoo: +/// https://github.com/giampaolo/psutil/tree/master/psutil/arch + +// IWYU pragma: no_include <sys/param.h> + +#include <assert.h> +#include <signal.h> +#include <stdbool.h> +#include <stddef.h> +#include <uv.h> + +#ifdef MSWIN +# include <tlhelp32.h> +#endif + +#if defined(__FreeBSD__) +# include <string.h> +# include <sys/types.h> +# include <sys/user.h> +#endif + +#if defined(__NetBSD__) || defined(__OpenBSD__) +# include <sys/param.h> +#endif + +#if defined(__APPLE__) || defined(BSD) +# include <sys/sysctl.h> + +# include "nvim/macros_defs.h" +#endif + +#if defined(__linux__) +# include <stdio.h> +#endif + +#include "nvim/log.h" +#include "nvim/memory.h" +#include "nvim/os/proc.h" + +#ifdef MSWIN +# include "nvim/api/private/helpers.h" +#endif + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/proc.c.generated.h" +#endif + +#ifdef MSWIN +static bool os_proc_tree_kill_rec(HANDLE proc, int sig) +{ + if (proc == NULL) { + return false; + } + PROCESSENTRY32 pe; + DWORD pid = GetProcessId(proc); + + if (pid != 0) { + HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (h != INVALID_HANDLE_VALUE) { + pe.dwSize = sizeof(PROCESSENTRY32); + if (!Process32First(h, &pe)) { + goto theend; + } + do { + if (pe.th32ParentProcessID == pid) { + HANDLE ph = OpenProcess(PROCESS_ALL_ACCESS, false, pe.th32ProcessID); + if (ph != NULL) { + os_proc_tree_kill_rec(ph, sig); + CloseHandle(ph); + } + } + } while (Process32Next(h, &pe)); + CloseHandle(h); + } + } + +theend: + return (bool)TerminateProcess(proc, (unsigned)sig); +} +/// Kills process `pid` and its descendants recursively. +bool os_proc_tree_kill(int pid, int sig) +{ + assert(sig >= 0); + assert(sig == SIGTERM || sig == SIGKILL); + if (pid > 0) { + ILOG("terminating process tree: %d", pid); + HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, false, (DWORD)pid); + return os_proc_tree_kill_rec(h, sig); + } else { + ELOG("invalid pid: %d", pid); + } + return false; +} +#else +/// Kills process group where `pid` is the process group leader. +bool os_proc_tree_kill(int pid, int sig) +{ + assert(sig == SIGTERM || sig == SIGKILL); + if (pid == 0) { + // Never kill self (pid=0). + return false; + } + ILOG("sending %s to PID %d", sig == SIGTERM ? "SIGTERM" : "SIGKILL", -pid); + return uv_kill(-pid, sig) == 0; +} +#endif + +/// Gets the process ids of the immediate children of process `ppid`. +/// +/// @param ppid Process to inspect. +/// @param[out,allocated] proc_list Child process ids. +/// @param[out] proc_count Number of child processes. +/// @return 0 on success, 1 if process not found, 2 on other error. +int os_proc_children(int ppid, int **proc_list, size_t *proc_count) + FUNC_ATTR_NONNULL_ALL +{ + if (ppid < 0) { + return 2; + } + + int *temp = NULL; + *proc_list = NULL; + *proc_count = 0; + +#ifdef MSWIN + PROCESSENTRY32 pe; + + // Snapshot of all processes. + HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (h == INVALID_HANDLE_VALUE) { + return 2; + } + + pe.dwSize = sizeof(PROCESSENTRY32); + // Get root process. + if (!Process32First(h, &pe)) { + CloseHandle(h); + return 2; + } + // Collect processes whose parent matches `ppid`. + do { + if (pe.th32ParentProcessID == (DWORD)ppid) { + temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp)); + temp[*proc_count] = (int)pe.th32ProcessID; + (*proc_count)++; + } + } while (Process32Next(h, &pe)); + CloseHandle(h); + +#elif defined(__APPLE__) || defined(BSD) +# if defined(__APPLE__) +# define KP_PID(o) o.kp_proc.p_pid +# define KP_PPID(o) o.kp_eproc.e_ppid +# elif defined(__FreeBSD__) +# define KP_PID(o) o.ki_pid +# define KP_PPID(o) o.ki_ppid +# else +# define KP_PID(o) o.p_pid +# define KP_PPID(o) o.p_ppid +# endif +# ifdef __NetBSD__ + static int name[] = { + CTL_KERN, KERN_PROC2, KERN_PROC_ALL, 0, (int)(sizeof(struct kinfo_proc2)), 0 + }; +# else + static int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 }; +# endif + + // Get total process count. + size_t len = 0; + int rv = sysctl(name, ARRAY_SIZE(name) - 1, NULL, &len, NULL, 0); + if (rv) { + return 2; + } + + // Get ALL processes. +# ifdef __NetBSD__ + struct kinfo_proc2 *p_list = xmalloc(len); +# else + struct kinfo_proc *p_list = xmalloc(len); +# endif + rv = sysctl(name, ARRAY_SIZE(name) - 1, p_list, &len, NULL, 0); + if (rv) { + xfree(p_list); + return 2; + } + + // Collect processes whose parent matches `ppid`. + bool exists = false; + size_t p_count = len / sizeof(*p_list); + for (size_t i = 0; i < p_count; i++) { + exists = exists || KP_PID(p_list[i]) == ppid; + if (KP_PPID(p_list[i]) == ppid) { + temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp)); + temp[*proc_count] = KP_PID(p_list[i]); + (*proc_count)++; + } + } + xfree(p_list); + if (!exists) { + return 1; // Process not found. + } + +#elif defined(__linux__) + char proc_p[256] = { 0 }; + // Collect processes whose parent matches `ppid`. + // Rationale: children are defined in thread with same ID of process. + snprintf(proc_p, sizeof(proc_p), "/proc/%d/task/%d/children", ppid, ppid); + FILE *fp = fopen(proc_p, "r"); + if (fp == NULL) { + return 2; // Process not found, or /proc/…/children not supported. + } + int match_pid; + while (fscanf(fp, "%d", &match_pid) > 0) { + temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp)); + temp[*proc_count] = match_pid; + (*proc_count)++; + } + fclose(fp); +#endif + + *proc_list = temp; + return 0; +} + +#ifdef MSWIN +/// Gets various properties of the process identified by `pid`. +/// +/// @param pid Process to inspect. +/// @return Map of process properties, empty on error. +Dictionary os_proc_info(int pid, Arena *arena) +{ + Dictionary pinfo = ARRAY_DICT_INIT; + PROCESSENTRY32 pe; + + // Snapshot of all processes. This is used instead of: + // OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, …) + // to avoid ERROR_PARTIAL_COPY. https://stackoverflow.com/a/29942376 + HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (h == INVALID_HANDLE_VALUE) { + return pinfo; // Return empty. + } + + pe.dwSize = sizeof(PROCESSENTRY32); + // Get root process. + if (!Process32First(h, &pe)) { + CloseHandle(h); + return pinfo; // Return empty. + } + // Find the process. + do { + if (pe.th32ProcessID == (DWORD)pid) { + break; + } + } while (Process32Next(h, &pe)); + CloseHandle(h); + + if (pe.th32ProcessID == (DWORD)pid) { + pinfo = arena_dict(arena, 3); + PUT_C(pinfo, "pid", INTEGER_OBJ(pid)); + PUT_C(pinfo, "ppid", INTEGER_OBJ((int)pe.th32ParentProcessID)); + PUT_C(pinfo, "name", CSTR_TO_ARENA_OBJ(arena, pe.szExeFile)); + } + + return pinfo; +} +#endif + +/// Return true if process `pid` is running. +bool os_proc_running(int pid) +{ + int err = uv_kill(pid, 0); + // If there is no error the process must be running. + if (err == 0) { + return true; + } + // If the error is ESRCH then the process is not running. + if (err == UV_ESRCH) { + return false; + } + // If the process is running and owned by another user we get EPERM. With + // other errors the process might be running, assuming it is then. + return true; +} diff --git a/src/nvim/os/proc.h b/src/nvim/os/proc.h @@ -0,0 +1,11 @@ +#pragma once + +#include <stddef.h> // IWYU pragma: keep + +#ifdef MSWIN +# include "nvim/api/private/defs.h" // IWYU pragma: keep +#endif + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/proc.h.generated.h" +#endif diff --git a/src/nvim/os/process.c b/src/nvim/os/process.c @@ -1,286 +0,0 @@ -/// OS process functions -/// -/// psutil is a good reference for cross-platform syscall voodoo: -/// https://github.com/giampaolo/psutil/tree/master/psutil/arch - -// IWYU pragma: no_include <sys/param.h> - -#include <assert.h> -#include <signal.h> -#include <stdbool.h> -#include <stddef.h> -#include <uv.h> - -#ifdef MSWIN -# include <tlhelp32.h> -#endif - -#if defined(__FreeBSD__) -# include <string.h> -# include <sys/types.h> -# include <sys/user.h> -#endif - -#if defined(__NetBSD__) || defined(__OpenBSD__) -# include <sys/param.h> -#endif - -#if defined(__APPLE__) || defined(BSD) -# include <sys/sysctl.h> - -# include "nvim/macros_defs.h" -#endif - -#if defined(__linux__) -# include <stdio.h> -#endif - -#include "nvim/log.h" -#include "nvim/memory.h" -#include "nvim/os/process.h" - -#ifdef MSWIN -# include "nvim/api/private/helpers.h" -#endif - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "os/process.c.generated.h" -#endif - -#ifdef MSWIN -static bool os_proc_tree_kill_rec(HANDLE process, int sig) -{ - if (process == NULL) { - return false; - } - PROCESSENTRY32 pe; - DWORD pid = GetProcessId(process); - - if (pid != 0) { - HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (h != INVALID_HANDLE_VALUE) { - pe.dwSize = sizeof(PROCESSENTRY32); - if (!Process32First(h, &pe)) { - goto theend; - } - do { - if (pe.th32ParentProcessID == pid) { - HANDLE ph = OpenProcess(PROCESS_ALL_ACCESS, false, pe.th32ProcessID); - if (ph != NULL) { - os_proc_tree_kill_rec(ph, sig); - CloseHandle(ph); - } - } - } while (Process32Next(h, &pe)); - CloseHandle(h); - } - } - -theend: - return (bool)TerminateProcess(process, (unsigned)sig); -} -/// Kills process `pid` and its descendants recursively. -bool os_proc_tree_kill(int pid, int sig) -{ - assert(sig >= 0); - assert(sig == SIGTERM || sig == SIGKILL); - if (pid > 0) { - ILOG("terminating process tree: %d", pid); - HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, false, (DWORD)pid); - return os_proc_tree_kill_rec(h, sig); - } else { - ELOG("invalid pid: %d", pid); - } - return false; -} -#else -/// Kills process group where `pid` is the process group leader. -bool os_proc_tree_kill(int pid, int sig) -{ - assert(sig == SIGTERM || sig == SIGKILL); - if (pid == 0) { - // Never kill self (pid=0). - return false; - } - ILOG("sending %s to PID %d", sig == SIGTERM ? "SIGTERM" : "SIGKILL", -pid); - return uv_kill(-pid, sig) == 0; -} -#endif - -/// Gets the process ids of the immediate children of process `ppid`. -/// -/// @param ppid Process to inspect. -/// @param[out,allocated] proc_list Child process ids. -/// @param[out] proc_count Number of child processes. -/// @return 0 on success, 1 if process not found, 2 on other error. -int os_proc_children(int ppid, int **proc_list, size_t *proc_count) - FUNC_ATTR_NONNULL_ALL -{ - if (ppid < 0) { - return 2; - } - - int *temp = NULL; - *proc_list = NULL; - *proc_count = 0; - -#ifdef MSWIN - PROCESSENTRY32 pe; - - // Snapshot of all processes. - HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (h == INVALID_HANDLE_VALUE) { - return 2; - } - - pe.dwSize = sizeof(PROCESSENTRY32); - // Get root process. - if (!Process32First(h, &pe)) { - CloseHandle(h); - return 2; - } - // Collect processes whose parent matches `ppid`. - do { - if (pe.th32ParentProcessID == (DWORD)ppid) { - temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp)); - temp[*proc_count] = (int)pe.th32ProcessID; - (*proc_count)++; - } - } while (Process32Next(h, &pe)); - CloseHandle(h); - -#elif defined(__APPLE__) || defined(BSD) -# if defined(__APPLE__) -# define KP_PID(o) o.kp_proc.p_pid -# define KP_PPID(o) o.kp_eproc.e_ppid -# elif defined(__FreeBSD__) -# define KP_PID(o) o.ki_pid -# define KP_PPID(o) o.ki_ppid -# else -# define KP_PID(o) o.p_pid -# define KP_PPID(o) o.p_ppid -# endif -# ifdef __NetBSD__ - static int name[] = { - CTL_KERN, KERN_PROC2, KERN_PROC_ALL, 0, (int)(sizeof(struct kinfo_proc2)), 0 - }; -# else - static int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 }; -# endif - - // Get total process count. - size_t len = 0; - int rv = sysctl(name, ARRAY_SIZE(name) - 1, NULL, &len, NULL, 0); - if (rv) { - return 2; - } - - // Get ALL processes. -# ifdef __NetBSD__ - struct kinfo_proc2 *p_list = xmalloc(len); -# else - struct kinfo_proc *p_list = xmalloc(len); -# endif - rv = sysctl(name, ARRAY_SIZE(name) - 1, p_list, &len, NULL, 0); - if (rv) { - xfree(p_list); - return 2; - } - - // Collect processes whose parent matches `ppid`. - bool exists = false; - size_t p_count = len / sizeof(*p_list); - for (size_t i = 0; i < p_count; i++) { - exists = exists || KP_PID(p_list[i]) == ppid; - if (KP_PPID(p_list[i]) == ppid) { - temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp)); - temp[*proc_count] = KP_PID(p_list[i]); - (*proc_count)++; - } - } - xfree(p_list); - if (!exists) { - return 1; // Process not found. - } - -#elif defined(__linux__) - char proc_p[256] = { 0 }; - // Collect processes whose parent matches `ppid`. - // Rationale: children are defined in thread with same ID of process. - snprintf(proc_p, sizeof(proc_p), "/proc/%d/task/%d/children", ppid, ppid); - FILE *fp = fopen(proc_p, "r"); - if (fp == NULL) { - return 2; // Process not found, or /proc/…/children not supported. - } - int match_pid; - while (fscanf(fp, "%d", &match_pid) > 0) { - temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp)); - temp[*proc_count] = match_pid; - (*proc_count)++; - } - fclose(fp); -#endif - - *proc_list = temp; - return 0; -} - -#ifdef MSWIN -/// Gets various properties of the process identified by `pid`. -/// -/// @param pid Process to inspect. -/// @return Map of process properties, empty on error. -Dictionary os_proc_info(int pid, Arena *arena) -{ - Dictionary pinfo = ARRAY_DICT_INIT; - PROCESSENTRY32 pe; - - // Snapshot of all processes. This is used instead of: - // OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, …) - // to avoid ERROR_PARTIAL_COPY. https://stackoverflow.com/a/29942376 - HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (h == INVALID_HANDLE_VALUE) { - return pinfo; // Return empty. - } - - pe.dwSize = sizeof(PROCESSENTRY32); - // Get root process. - if (!Process32First(h, &pe)) { - CloseHandle(h); - return pinfo; // Return empty. - } - // Find the process. - do { - if (pe.th32ProcessID == (DWORD)pid) { - break; - } - } while (Process32Next(h, &pe)); - CloseHandle(h); - - if (pe.th32ProcessID == (DWORD)pid) { - pinfo = arena_dict(arena, 3); - PUT_C(pinfo, "pid", INTEGER_OBJ(pid)); - PUT_C(pinfo, "ppid", INTEGER_OBJ((int)pe.th32ParentProcessID)); - PUT_C(pinfo, "name", CSTR_TO_ARENA_OBJ(arena, pe.szExeFile)); - } - - return pinfo; -} -#endif - -/// Return true if process `pid` is running. -bool os_proc_running(int pid) -{ - int err = uv_kill(pid, 0); - // If there is no error the process must be running. - if (err == 0) { - return true; - } - // If the error is ESRCH then the process is not running. - if (err == UV_ESRCH) { - return false; - } - // If the process is running and owned by another user we get EPERM. With - // other errors the process might be running, assuming it is then. - return true; -} diff --git a/src/nvim/os/process.h b/src/nvim/os/process.h @@ -1,11 +0,0 @@ -#pragma once - -#include <stddef.h> // IWYU pragma: keep - -#ifdef MSWIN -# include "nvim/api/private/defs.h" // IWYU pragma: keep -#endif - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "os/process.h.generated.h" -#endif diff --git a/src/nvim/os/pty_conpty_win.c b/src/nvim/os/pty_conpty_win.c @@ -143,7 +143,7 @@ finished: return conpty_object; } -bool os_conpty_spawn(conpty_t *conpty_object, HANDLE *process_handle, wchar_t *name, +bool os_conpty_spawn(conpty_t *conpty_object, HANDLE *proc_handle, wchar_t *name, wchar_t *cmd_line, wchar_t *cwd, wchar_t *env) { PROCESS_INFORMATION pi = { 0 }; @@ -159,7 +159,7 @@ bool os_conpty_spawn(conpty_t *conpty_object, HANDLE *process_handle, wchar_t *n &pi)) { return false; } - *process_handle = pi.hProcess; + *proc_handle = pi.hProcess; return true; } diff --git a/src/nvim/os/pty_proc.h b/src/nvim/os/pty_proc.h @@ -0,0 +1,7 @@ +#pragma once + +#ifdef MSWIN +# include "nvim/os/pty_proc_win.h" +#else +# include "nvim/os/pty_proc_unix.h" +#endif diff --git a/src/nvim/os/pty_proc_unix.c b/src/nvim/os/pty_proc_unix.c @@ -0,0 +1,417 @@ +// Some of the code came from pangoterm and libuv + +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <uv.h> + +// forkpty is not in POSIX, so headers are platform-specific +#if defined(__FreeBSD__) || defined(__DragonFly__) +# include <libutil.h> +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) +# include <util.h> +#elif defined(__sun) +# include <fcntl.h> +# include <signal.h> +# include <sys/stream.h> +# include <sys/syscall.h> +# include <unistd.h> +#else +# include <pty.h> +#endif + +#ifdef __APPLE__ +# include <crt_externs.h> +#endif + +#include "auto/config.h" +#include "klib/klist.h" +#include "nvim/eval/typval.h" +#include "nvim/event/defs.h" +#include "nvim/event/loop.h" +#include "nvim/event/proc.h" +#include "nvim/log.h" +#include "nvim/os/fs.h" +#include "nvim/os/os_defs.h" +#include "nvim/os/pty_proc.h" +#include "nvim/os/pty_proc_unix.h" +#include "nvim/types_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/pty_proc_unix.c.generated.h" +#endif + +#if defined(__sun) && !defined(HAVE_FORKPTY) + +// this header defines STR, just as nvim.h, but it is defined as ('S'<<8), +// to avoid #undef STR, #undef STR, #define STR ('S'<<8) just delay the +// inclusion of the header even though it gets include out of order. +# include <sys/stropts.h> + +static int openpty(int *amaster, int *aslave, char *name, struct termios *termp, + struct winsize *winp) +{ + int slave = -1; + int master = open("/dev/ptmx", O_RDWR); + if (master == -1) { + goto error; + } + + // grantpt will invoke a setuid program to change permissions + // and might fail if SIGCHLD handler is set, temporarily reset + // while running + void (*sig_saved)(int) = signal(SIGCHLD, SIG_DFL); + int res = grantpt(master); + signal(SIGCHLD, sig_saved); + + if (res == -1 || unlockpt(master) == -1) { + goto error; + } + + char *slave_name = ptsname(master); + if (slave_name == NULL) { + goto error; + } + + slave = open(slave_name, O_RDWR|O_NOCTTY); + if (slave == -1) { + goto error; + } + + // ptem emulates a terminal when used on a pseudo terminal driver, + // must be pushed before ldterm + ioctl(slave, I_PUSH, "ptem"); + // ldterm provides most of the termio terminal interface + ioctl(slave, I_PUSH, "ldterm"); + // ttcompat compatibility with older terminal ioctls + ioctl(slave, I_PUSH, "ttcompat"); + + if (termp) { + tcsetattr(slave, TCSAFLUSH, termp); + } + if (winp) { + ioctl(slave, TIOCSWINSZ, winp); + } + + *amaster = master; + *aslave = slave; + // ignoring name, not passed and size is unknown in the API + + return 0; + +error: + if (slave != -1) { + close(slave); + } + if (master != -1) { + close(master); + } + return -1; +} + +static int login_tty(int fd) +{ + setsid(); + if (ioctl(fd, TIOCSCTTY, NULL) == -1) { + return -1; + } + + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > STDERR_FILENO) { + close(fd); + } + + return 0; +} + +static pid_t forkpty(int *amaster, char *name, struct termios *termp, struct winsize *winp) +{ + int master, slave; + if (openpty(&master, &slave, name, termp, winp) == -1) { + return -1; + } + + pid_t pid = fork(); + switch (pid) { + case -1: + close(master); + close(slave); + return -1; + case 0: + close(master); + login_tty(slave); + return 0; + default: + close(slave); + *amaster = master; + return pid; + } +} + +#endif + +/// @returns zero on success, or negative error code +int pty_proc_spawn(PtyProc *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + // termios initialized at first use + static struct termios termios_default; + if (!termios_default.c_cflag) { + init_termios(&termios_default); + } + + int status = 0; // zero or negative error code (libuv convention) + Proc *proc = (Proc *)ptyproc; + assert(proc->err.s.closed); + uv_signal_start(&proc->loop->children_watcher, chld_handler, SIGCHLD); + ptyproc->winsize = (struct winsize){ ptyproc->height, ptyproc->width, 0, 0 }; + uv_disable_stdio_inheritance(); + int master; + int pid = forkpty(&master, NULL, &termios_default, &ptyproc->winsize); + + if (pid < 0) { + status = -errno; + ELOG("forkpty failed: %s", strerror(errno)); + return status; + } else if (pid == 0) { + init_child(ptyproc); // never returns + } + + // make sure the master file descriptor is non blocking + int master_status_flags = fcntl(master, F_GETFL); + if (master_status_flags == -1) { + status = -errno; + ELOG("Failed to get master descriptor status flags: %s", strerror(errno)); + goto error; + } + if (fcntl(master, F_SETFL, master_status_flags | O_NONBLOCK) == -1) { + status = -errno; + ELOG("Failed to make master descriptor non-blocking: %s", strerror(errno)); + goto error; + } + + // Other jobs and providers should not get a copy of this file descriptor. + if (os_set_cloexec(master) == -1) { + status = -errno; + ELOG("Failed to set CLOEXEC on ptmx file descriptor"); + goto error; + } + + if (!proc->in.closed + && (status = set_duplicating_descriptor(master, &proc->in.uv.pipe))) { + goto error; + } + if (!proc->out.s.closed + && (status = set_duplicating_descriptor(master, &proc->out.s.uv.pipe))) { + goto error; + } + + ptyproc->tty_fd = master; + proc->pid = pid; + return 0; + +error: + close(master); + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + return status; +} + +const char *pty_proc_tty_name(PtyProc *ptyproc) +{ + return ptsname(ptyproc->tty_fd); +} + +void pty_proc_resize(PtyProc *ptyproc, uint16_t width, uint16_t height) + FUNC_ATTR_NONNULL_ALL +{ + ptyproc->winsize = (struct winsize){ height, width, 0, 0 }; + ioctl(ptyproc->tty_fd, TIOCSWINSZ, &ptyproc->winsize); +} + +void pty_proc_close(PtyProc *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + pty_proc_close_master(ptyproc); + Proc *proc = (Proc *)ptyproc; + if (proc->internal_close_cb) { + proc->internal_close_cb(proc); + } +} + +void pty_proc_close_master(PtyProc *ptyproc) FUNC_ATTR_NONNULL_ALL +{ + if (ptyproc->tty_fd >= 0) { + close(ptyproc->tty_fd); + ptyproc->tty_fd = -1; + } +} + +void pty_proc_teardown(Loop *loop) +{ + uv_signal_stop(&loop->children_watcher); +} + +static void init_child(PtyProc *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ +#if defined(HAVE__NSGETENVIRON) +# define environ (*_NSGetEnviron()) +#else + extern char **environ; +#endif + // New session/process-group. #6530 + setsid(); + + signal(SIGCHLD, SIG_DFL); + signal(SIGHUP, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGALRM, SIG_DFL); + + Proc *proc = (Proc *)ptyproc; + if (proc->cwd && os_chdir(proc->cwd) != 0) { + ELOG("chdir(%s) failed: %s", proc->cwd, strerror(errno)); + return; + } + + const char *prog = proc_get_exepath(proc); + + assert(proc->env); + environ = tv_dict_to_env(proc->env); + execvp(prog, proc->argv); + ELOG("execvp(%s) failed: %s", prog, strerror(errno)); + + _exit(122); // 122 is EXEC_FAILED in the Vim source. +} + +static void init_termios(struct termios *termios) FUNC_ATTR_NONNULL_ALL +{ + // Taken from pangoterm + termios->c_iflag = ICRNL|IXON; + termios->c_oflag = OPOST|ONLCR; +#ifdef TAB0 + termios->c_oflag |= TAB0; +#endif + termios->c_cflag = CS8|CREAD; + termios->c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK; + + // not using cfsetspeed, not available on all platforms + cfsetispeed(termios, 38400); + cfsetospeed(termios, 38400); + +#ifdef IUTF8 + termios->c_iflag |= IUTF8; +#endif +#ifdef NL0 + termios->c_oflag |= NL0; +#endif +#ifdef CR0 + termios->c_oflag |= CR0; +#endif +#ifdef BS0 + termios->c_oflag |= BS0; +#endif +#ifdef VT0 + termios->c_oflag |= VT0; +#endif +#ifdef FF0 + termios->c_oflag |= FF0; +#endif +#ifdef ECHOCTL + termios->c_lflag |= ECHOCTL; +#endif +#ifdef ECHOKE + termios->c_lflag |= ECHOKE; +#endif + + termios->c_cc[VINTR] = 0x1f & 'C'; + termios->c_cc[VQUIT] = 0x1f & '\\'; + termios->c_cc[VERASE] = 0x7f; + termios->c_cc[VKILL] = 0x1f & 'U'; + termios->c_cc[VEOF] = 0x1f & 'D'; + termios->c_cc[VEOL] = _POSIX_VDISABLE; + termios->c_cc[VEOL2] = _POSIX_VDISABLE; + termios->c_cc[VSTART] = 0x1f & 'Q'; + termios->c_cc[VSTOP] = 0x1f & 'S'; + termios->c_cc[VSUSP] = 0x1f & 'Z'; + termios->c_cc[VREPRINT] = 0x1f & 'R'; + termios->c_cc[VWERASE] = 0x1f & 'W'; + termios->c_cc[VLNEXT] = 0x1f & 'V'; + termios->c_cc[VMIN] = 1; + termios->c_cc[VTIME] = 0; +} + +static int set_duplicating_descriptor(int fd, uv_pipe_t *pipe) + FUNC_ATTR_NONNULL_ALL +{ + int status = 0; // zero or negative error code (libuv convention) + int fd_dup = dup(fd); + if (fd_dup < 0) { + status = -errno; + ELOG("Failed to dup descriptor %d: %s", fd, strerror(errno)); + return status; + } + + if (os_set_cloexec(fd_dup) == -1) { + status = -errno; + ELOG("Failed to set CLOEXEC on duplicate fd"); + goto error; + } + + status = uv_pipe_open(pipe, fd_dup); + if (status) { + ELOG("Failed to set pipe to descriptor %d: %s", + fd_dup, uv_strerror(status)); + goto error; + } + return status; + +error: + close(fd_dup); + return status; +} + +static void chld_handler(uv_signal_t *handle, int signum) +{ + int stat = 0; + int pid; + + Loop *loop = handle->loop->data; + + kl_iter(WatcherPtr, loop->children, current) { + Proc *proc = (*current)->data; + do { + pid = waitpid(proc->pid, &stat, WNOHANG); + } while (pid < 0 && errno == EINTR); + + if (pid <= 0) { + continue; + } + + if (WIFEXITED(stat)) { + proc->status = WEXITSTATUS(stat); + } else if (WIFSIGNALED(stat)) { + proc->status = 128 + WTERMSIG(stat); + } + proc->internal_exit_cb(proc); + } +} + +PtyProc pty_proc_init(Loop *loop, void *data) +{ + PtyProc rv; + rv.proc = proc_init(loop, kProcTypePty, data); + rv.width = 80; + rv.height = 24; + rv.tty_fd = -1; + return rv; +} diff --git a/src/nvim/os/pty_proc_unix.h b/src/nvim/os/pty_proc_unix.h @@ -0,0 +1,18 @@ +#pragma once +// IWYU pragma: private, include "nvim/os/pty_proc.h" + +#include <stdint.h> +#include <sys/ioctl.h> + +#include "nvim/event/defs.h" + +typedef struct { + Proc proc; + uint16_t width, height; + struct winsize winsize; + int tty_fd; +} PtyProc; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/pty_proc_unix.h.generated.h" +#endif diff --git a/src/nvim/os/pty_proc_win.c b/src/nvim/os/pty_proc_win.c @@ -0,0 +1,440 @@ +#include <assert.h> +#include <stdbool.h> +#include <stdlib.h> + +#include "nvim/ascii_defs.h" +#include "nvim/eval/typval.h" +#include "nvim/event/loop.h" +#include "nvim/log.h" +#include "nvim/mbyte.h" +#include "nvim/memory.h" +#include "nvim/os/os.h" +#include "nvim/os/pty_conpty_win.h" +#include "nvim/os/pty_proc_win.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/pty_proc_win.c.generated.h" +#endif + +static void CALLBACK pty_proc_finish1(void *context, BOOLEAN unused) + FUNC_ATTR_NONNULL_ALL +{ + PtyProc *ptyproc = (PtyProc *)context; + Proc *proc = (Proc *)ptyproc; + + os_conpty_free(ptyproc->conpty); + // NB: pty_proc_finish1() is called on a separate thread, + // but the timer only works properly if it's started by the main thread. + loop_schedule_fast(proc->loop, event_create(start_wait_eof_timer, ptyproc)); +} + +static void start_wait_eof_timer(void **argv) + FUNC_ATTR_NONNULL_ALL +{ + PtyProc *ptyproc = (PtyProc *)argv[0]; + + if (ptyproc->finish_wait != NULL) { + uv_timer_start(&ptyproc->wait_eof_timer, wait_eof_timer_cb, 200, 200); + } +} + +/// @returns zero on success, or negative error code. +int pty_proc_spawn(PtyProc *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + Proc *proc = (Proc *)ptyproc; + int status = 0; + conpty_t *conpty_object = NULL; + char *in_name = NULL; + char *out_name = NULL; + HANDLE proc_handle = NULL; + uv_connect_t *in_req = NULL; + uv_connect_t *out_req = NULL; + wchar_t *cmd_line = NULL; + wchar_t *cwd = NULL; + wchar_t *env = NULL; + const char *emsg = NULL; + + assert(proc->err.s.closed); + + if (!os_has_conpty_working() || (conpty_object = os_conpty_init(&in_name, + &out_name, ptyproc->width, + ptyproc->height)) == NULL) { + status = UV_ENOSYS; + goto cleanup; + } + + if (!proc->in.closed) { + in_req = xmalloc(sizeof(uv_connect_t)); + uv_pipe_connect(in_req, + &proc->in.uv.pipe, + in_name, + pty_proc_connect_cb); + } + + if (!proc->out.s.closed) { + out_req = xmalloc(sizeof(uv_connect_t)); + uv_pipe_connect(out_req, + &proc->out.s.uv.pipe, + out_name, + pty_proc_connect_cb); + } + + if (proc->cwd != NULL) { + status = utf8_to_utf16(proc->cwd, -1, &cwd); + if (status != 0) { + emsg = "utf8_to_utf16(proc->cwd) failed"; + goto cleanup; + } + } + + status = build_cmd_line(proc->argv, &cmd_line, + os_shell_is_cmdexe(proc->argv[0])); + if (status != 0) { + emsg = "build_cmd_line failed"; + goto cleanup; + } + + if (proc->env != NULL) { + status = build_env_block(proc->env, &env); + } + + if (status != 0) { + emsg = "build_env_block failed"; + goto cleanup; + } + + if (!os_conpty_spawn(conpty_object, + &proc_handle, + NULL, + cmd_line, + cwd, + env)) { + emsg = "os_conpty_spawn failed"; + status = (int)GetLastError(); + goto cleanup; + } + 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_finish1, + ptyproc, + INFINITE, + WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE)) { + abort(); + } + + // Wait until pty_proc_connect_cb is called. + while ((in_req != NULL && in_req->handle != NULL) + || (out_req != NULL && out_req->handle != NULL)) { + uv_run(&proc->loop->uv, UV_RUN_ONCE); + } + + ptyproc->conpty = conpty_object; + ptyproc->proc_handle = proc_handle; + conpty_object = NULL; + proc_handle = NULL; + +cleanup: + if (status) { + // In the case of an error of MultiByteToWideChar or CreateProcessW. + ELOG("pty_proc_spawn(%s): %s: error code: %d", + proc->argv[0], emsg, status); + status = os_translate_sys_error(status); + } + os_conpty_free(conpty_object); + xfree(in_name); + xfree(out_name); + if (proc_handle != NULL) { + CloseHandle(proc_handle); + } + xfree(in_req); + xfree(out_req); + xfree(cmd_line); + xfree(env); + xfree(cwd); + return status; +} + +const char *pty_proc_tty_name(PtyProc *ptyproc) +{ + return "?"; +} + +void pty_proc_resize(PtyProc *ptyproc, uint16_t width, uint16_t height) + FUNC_ATTR_NONNULL_ALL +{ + os_conpty_set_size(ptyproc->conpty, width, height); +} + +void pty_proc_close(PtyProc *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + Proc *proc = (Proc *)ptyproc; + + pty_proc_close_master(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); + ptyproc->proc_handle = NULL; + } + + if (proc->internal_close_cb) { + proc->internal_close_cb(proc); + } +} + +void pty_proc_close_master(PtyProc *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ +} + +void pty_proc_teardown(Loop *loop) + FUNC_ATTR_NONNULL_ALL +{ +} + +static void pty_proc_connect_cb(uv_connect_t *req, int status) + FUNC_ATTR_NONNULL_ALL +{ + assert(status == 0); + 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; + Proc *proc = (Proc *)ptyproc; + + assert(ptyproc->finish_wait != NULL); + if (proc->out.s.closed || proc->out.did_eof || !uv_is_readable(proc->out.s.uvstream)) { + uv_timer_stop(&ptyproc->wait_eof_timer); + pty_proc_finish2(ptyproc); + } +} + +static void pty_proc_finish2(PtyProc *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + Proc *proc = (Proc *)ptyproc; + + DWORD exit_code = 0; + GetExitCodeProcess(ptyproc->proc_handle, &exit_code); + proc->status = proc->exit_signal ? 128 + proc->exit_signal : (int)exit_code; + + proc->internal_exit_cb(proc); +} + +/// Build the command line to pass to CreateProcessW. +/// +/// @param[in] argv Array with string arguments. +/// @param[out] cmd_line Location where saved built cmd line. +/// +/// @returns zero on success, or error code of MultiByteToWideChar function. +/// +static int build_cmd_line(char **argv, wchar_t **cmd_line, bool is_cmdexe) + FUNC_ATTR_NONNULL_ALL +{ + size_t utf8_cmd_line_len = 0; + size_t argc = 0; + QUEUE args_q; + + QUEUE_INIT(&args_q); + while (*argv) { + size_t buf_len = is_cmdexe ? (strlen(*argv) + 1) : (strlen(*argv) * 2 + 3); + ArgNode *arg_node = xmalloc(sizeof(*arg_node)); + arg_node->arg = xmalloc(buf_len); + if (is_cmdexe) { + xstrlcpy(arg_node->arg, *argv, buf_len); + } else { + quote_cmd_arg(arg_node->arg, buf_len, *argv); + } + utf8_cmd_line_len += strlen(arg_node->arg); + QUEUE_INIT(&arg_node->node); + QUEUE_INSERT_TAIL(&args_q, &arg_node->node); + argc++; + argv++; + } + + utf8_cmd_line_len += argc; + char *utf8_cmd_line = xmalloc(utf8_cmd_line_len); + *utf8_cmd_line = NUL; + QUEUE *q; + QUEUE_FOREACH(q, &args_q, { + ArgNode *arg_node = QUEUE_DATA(q, ArgNode, node); + xstrlcat(utf8_cmd_line, arg_node->arg, utf8_cmd_line_len); + QUEUE_REMOVE(q); + xfree(arg_node->arg); + xfree(arg_node); + if (!QUEUE_EMPTY(&args_q)) { + xstrlcat(utf8_cmd_line, " ", utf8_cmd_line_len); + } + }) + + int result = utf8_to_utf16(utf8_cmd_line, -1, cmd_line); + xfree(utf8_cmd_line); + return result; +} + +/// Emulate quote_cmd_arg of libuv and quotes command line argument. +/// Most of the code came from libuv. +/// +/// @param[out] dest Location where saved quotes argument. +/// @param dest_remaining Destination buffer size. +/// @param[in] src Pointer to argument. +/// +static void quote_cmd_arg(char *dest, size_t dest_remaining, const char *src) + FUNC_ATTR_NONNULL_ALL +{ + size_t src_len = strlen(src); + bool quote_hit = true; + char *start = dest; + + if (src_len == 0) { + // Need double quotation for empty argument. + snprintf(dest, dest_remaining, "\"\""); + return; + } + + if (NULL == strpbrk(src, " \t\"")) { + // No quotation needed. + xstrlcpy(dest, src, dest_remaining); + return; + } + + if (NULL == strpbrk(src, "\"\\")) { + // No embedded double quotes or backlashes, so I can just wrap quote marks. + // around the whole thing. + snprintf(dest, dest_remaining, "\"%s\"", src); + return; + } + + // Expected input/output: + // input : 'hello"world' + // output: '"hello\"world"' + // input : 'hello""world' + // output: '"hello\"\"world"' + // input : 'hello\world' + // output: 'hello\world' + // input : 'hello\\world' + // output: 'hello\\world' + // input : 'hello\"world' + // output: '"hello\\\"world"' + // input : 'hello\\"world' + // output: '"hello\\\\\"world"' + // input : 'hello world\' + // output: '"hello world\\"' + + assert(dest_remaining--); + *(dest++) = NUL; + assert(dest_remaining--); + *(dest++) = '"'; + for (size_t i = src_len; i > 0; i--) { + assert(dest_remaining--); + *(dest++) = src[i - 1]; + if (quote_hit && src[i - 1] == '\\') { + assert(dest_remaining--); + *(dest++) = '\\'; + } else if (src[i - 1] == '"') { + quote_hit = true; + assert(dest_remaining--); + *(dest++) = '\\'; + } else { + quote_hit = false; + } + } + assert(dest_remaining); + *dest = '"'; + + while (start < dest) { + char tmp = *start; + *start = *dest; + *dest = tmp; + start++; + dest--; + } +} + +typedef struct EnvNode { + wchar_t *str; + size_t len; + QUEUE node; +} EnvNode; + +/// Build the environment block to pass to CreateProcessW. +/// +/// @param[in] denv Dict of environment name/value pairs +/// @param[out] env Allocated environment block +/// +/// @returns zero on success or error code of MultiByteToWideChar function. +static int build_env_block(dict_T *denv, wchar_t **env_block) +{ + const size_t denv_size = (size_t)tv_dict_len(denv); + size_t env_block_len = 0; + int rc = 0; + char **env = tv_dict_to_env(denv); + + QUEUE *q; + QUEUE env_q; + QUEUE_INIT(&env_q); + // Convert env vars to wchar_t and calculate how big the final env block + // needs to be + for (size_t i = 0; i < denv_size; i++) { + EnvNode *env_node = xmalloc(sizeof(*env_node)); + rc = utf8_to_utf16(env[i], -1, &env_node->str); + if (rc != 0) { + goto cleanup; + } + env_node->len = wcslen(env_node->str) + 1; + env_block_len += env_node->len; + QUEUE_INSERT_TAIL(&env_q, &env_node->node); + } + + // Additional NUL after the final entry + env_block_len++; + + *env_block = xmalloc(sizeof(**env_block) * env_block_len); + wchar_t *pos = *env_block; + + QUEUE_FOREACH(q, &env_q, { + EnvNode *env_node = QUEUE_DATA(q, EnvNode, node); + memcpy(pos, env_node->str, env_node->len * sizeof(*pos)); + pos += env_node->len; + }) + + *pos = L'\0'; + +cleanup: + q = QUEUE_HEAD(&env_q); + while (q != &env_q) { + QUEUE *next = q->next; + EnvNode *env_node = QUEUE_DATA(q, EnvNode, node); + XFREE_CLEAR(env_node->str); + QUEUE_REMOVE(q); + xfree(env_node); + q = next; + } + + return rc; +} + +PtyProc pty_proc_init(Loop *loop, void *data) +{ + PtyProc rv; + rv.proc = proc_init(loop, kProcTypePty, data); + rv.width = 80; + rv.height = 24; + rv.conpty = NULL; + rv.finish_wait = NULL; + rv.proc_handle = NULL; + return rv; +} diff --git a/src/nvim/os/pty_proc_win.h b/src/nvim/os/pty_proc_win.h @@ -0,0 +1,27 @@ +#pragma once +// IWYU pragma: private, include "nvim/os/pty_proc.h" + +#include <uv.h> + +#include "nvim/event/proc.h" +#include "nvim/lib/queue_defs.h" +#include "nvim/os/pty_conpty_win.h" + +typedef struct pty_process { + Proc proc; + uint16_t width, height; + conpty_t *conpty; + HANDLE finish_wait; + HANDLE proc_handle; + uv_timer_t wait_eof_timer; +} PtyProc; + +// Structure used by build_cmd_line() +typedef struct arg_node { + char *arg; // pointer to argument. + QUEUE node; // QUEUE structure. +} ArgNode; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/pty_proc_win.h.generated.h" +#endif diff --git a/src/nvim/os/pty_process.h b/src/nvim/os/pty_process.h @@ -1,7 +0,0 @@ -#pragma once - -#ifdef MSWIN -# include "nvim/os/pty_process_win.h" -#else -# include "nvim/os/pty_process_unix.h" -#endif diff --git a/src/nvim/os/pty_process_unix.c b/src/nvim/os/pty_process_unix.c @@ -1,417 +0,0 @@ -// Some of the code came from pangoterm and libuv - -#include <assert.h> -#include <errno.h> -#include <fcntl.h> -#include <signal.h> -#include <stdlib.h> -#include <string.h> -#include <sys/ioctl.h> -#include <sys/wait.h> -#include <uv.h> - -// forkpty is not in POSIX, so headers are platform-specific -#if defined(__FreeBSD__) || defined(__DragonFly__) -# include <libutil.h> -#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) -# include <util.h> -#elif defined(__sun) -# include <fcntl.h> -# include <signal.h> -# include <sys/stream.h> -# include <sys/syscall.h> -# include <unistd.h> -#else -# include <pty.h> -#endif - -#ifdef __APPLE__ -# include <crt_externs.h> -#endif - -#include "auto/config.h" -#include "klib/klist.h" -#include "nvim/eval/typval.h" -#include "nvim/event/defs.h" -#include "nvim/event/loop.h" -#include "nvim/event/process.h" -#include "nvim/log.h" -#include "nvim/os/fs.h" -#include "nvim/os/os_defs.h" -#include "nvim/os/pty_process.h" -#include "nvim/os/pty_process_unix.h" -#include "nvim/types_defs.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "os/pty_process_unix.c.generated.h" -#endif - -#if defined(__sun) && !defined(HAVE_FORKPTY) - -// this header defines STR, just as nvim.h, but it is defined as ('S'<<8), -// to avoid #undef STR, #undef STR, #define STR ('S'<<8) just delay the -// inclusion of the header even though it gets include out of order. -# include <sys/stropts.h> - -static int openpty(int *amaster, int *aslave, char *name, struct termios *termp, - struct winsize *winp) -{ - int slave = -1; - int master = open("/dev/ptmx", O_RDWR); - if (master == -1) { - goto error; - } - - // grantpt will invoke a setuid program to change permissions - // and might fail if SIGCHLD handler is set, temporarily reset - // while running - void (*sig_saved)(int) = signal(SIGCHLD, SIG_DFL); - int res = grantpt(master); - signal(SIGCHLD, sig_saved); - - if (res == -1 || unlockpt(master) == -1) { - goto error; - } - - char *slave_name = ptsname(master); - if (slave_name == NULL) { - goto error; - } - - slave = open(slave_name, O_RDWR|O_NOCTTY); - if (slave == -1) { - goto error; - } - - // ptem emulates a terminal when used on a pseudo terminal driver, - // must be pushed before ldterm - ioctl(slave, I_PUSH, "ptem"); - // ldterm provides most of the termio terminal interface - ioctl(slave, I_PUSH, "ldterm"); - // ttcompat compatibility with older terminal ioctls - ioctl(slave, I_PUSH, "ttcompat"); - - if (termp) { - tcsetattr(slave, TCSAFLUSH, termp); - } - if (winp) { - ioctl(slave, TIOCSWINSZ, winp); - } - - *amaster = master; - *aslave = slave; - // ignoring name, not passed and size is unknown in the API - - return 0; - -error: - if (slave != -1) { - close(slave); - } - if (master != -1) { - close(master); - } - return -1; -} - -static int login_tty(int fd) -{ - setsid(); - if (ioctl(fd, TIOCSCTTY, NULL) == -1) { - return -1; - } - - dup2(fd, STDIN_FILENO); - dup2(fd, STDOUT_FILENO); - dup2(fd, STDERR_FILENO); - if (fd > STDERR_FILENO) { - close(fd); - } - - return 0; -} - -static pid_t forkpty(int *amaster, char *name, struct termios *termp, struct winsize *winp) -{ - int master, slave; - if (openpty(&master, &slave, name, termp, winp) == -1) { - return -1; - } - - pid_t pid = fork(); - switch (pid) { - case -1: - close(master); - close(slave); - return -1; - case 0: - close(master); - login_tty(slave); - return 0; - default: - close(slave); - *amaster = master; - return pid; - } -} - -#endif - -/// @returns zero on success, or negative error code -int pty_process_spawn(PtyProcess *ptyproc) - FUNC_ATTR_NONNULL_ALL -{ - // termios initialized at first use - static struct termios termios_default; - if (!termios_default.c_cflag) { - init_termios(&termios_default); - } - - int status = 0; // zero or negative error code (libuv convention) - Process *proc = (Process *)ptyproc; - assert(proc->err.s.closed); - uv_signal_start(&proc->loop->children_watcher, chld_handler, SIGCHLD); - ptyproc->winsize = (struct winsize){ ptyproc->height, ptyproc->width, 0, 0 }; - uv_disable_stdio_inheritance(); - int master; - int pid = forkpty(&master, NULL, &termios_default, &ptyproc->winsize); - - if (pid < 0) { - status = -errno; - ELOG("forkpty failed: %s", strerror(errno)); - return status; - } else if (pid == 0) { - init_child(ptyproc); // never returns - } - - // make sure the master file descriptor is non blocking - int master_status_flags = fcntl(master, F_GETFL); - if (master_status_flags == -1) { - status = -errno; - ELOG("Failed to get master descriptor status flags: %s", strerror(errno)); - goto error; - } - if (fcntl(master, F_SETFL, master_status_flags | O_NONBLOCK) == -1) { - status = -errno; - ELOG("Failed to make master descriptor non-blocking: %s", strerror(errno)); - goto error; - } - - // Other jobs and providers should not get a copy of this file descriptor. - if (os_set_cloexec(master) == -1) { - status = -errno; - ELOG("Failed to set CLOEXEC on ptmx file descriptor"); - goto error; - } - - if (!proc->in.closed - && (status = set_duplicating_descriptor(master, &proc->in.uv.pipe))) { - goto error; - } - if (!proc->out.s.closed - && (status = set_duplicating_descriptor(master, &proc->out.s.uv.pipe))) { - goto error; - } - - ptyproc->tty_fd = master; - proc->pid = pid; - return 0; - -error: - close(master); - kill(pid, SIGKILL); - waitpid(pid, NULL, 0); - return status; -} - -const char *pty_process_tty_name(PtyProcess *ptyproc) -{ - return ptsname(ptyproc->tty_fd); -} - -void pty_process_resize(PtyProcess *ptyproc, uint16_t width, uint16_t height) - FUNC_ATTR_NONNULL_ALL -{ - ptyproc->winsize = (struct winsize){ height, width, 0, 0 }; - ioctl(ptyproc->tty_fd, TIOCSWINSZ, &ptyproc->winsize); -} - -void pty_process_close(PtyProcess *ptyproc) - FUNC_ATTR_NONNULL_ALL -{ - pty_process_close_master(ptyproc); - Process *proc = (Process *)ptyproc; - if (proc->internal_close_cb) { - proc->internal_close_cb(proc); - } -} - -void pty_process_close_master(PtyProcess *ptyproc) FUNC_ATTR_NONNULL_ALL -{ - if (ptyproc->tty_fd >= 0) { - close(ptyproc->tty_fd); - ptyproc->tty_fd = -1; - } -} - -void pty_process_teardown(Loop *loop) -{ - uv_signal_stop(&loop->children_watcher); -} - -static void init_child(PtyProcess *ptyproc) - FUNC_ATTR_NONNULL_ALL -{ -#if defined(HAVE__NSGETENVIRON) -# define environ (*_NSGetEnviron()) -#else - extern char **environ; -#endif - // New session/process-group. #6530 - setsid(); - - signal(SIGCHLD, SIG_DFL); - signal(SIGHUP, SIG_DFL); - signal(SIGINT, SIG_DFL); - signal(SIGQUIT, SIG_DFL); - signal(SIGTERM, SIG_DFL); - signal(SIGALRM, SIG_DFL); - - Process *proc = (Process *)ptyproc; - if (proc->cwd && os_chdir(proc->cwd) != 0) { - ELOG("chdir(%s) failed: %s", proc->cwd, strerror(errno)); - return; - } - - const char *prog = process_get_exepath(proc); - - assert(proc->env); - environ = tv_dict_to_env(proc->env); - execvp(prog, proc->argv); - ELOG("execvp(%s) failed: %s", prog, strerror(errno)); - - _exit(122); // 122 is EXEC_FAILED in the Vim source. -} - -static void init_termios(struct termios *termios) FUNC_ATTR_NONNULL_ALL -{ - // Taken from pangoterm - termios->c_iflag = ICRNL|IXON; - termios->c_oflag = OPOST|ONLCR; -#ifdef TAB0 - termios->c_oflag |= TAB0; -#endif - termios->c_cflag = CS8|CREAD; - termios->c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK; - - // not using cfsetspeed, not available on all platforms - cfsetispeed(termios, 38400); - cfsetospeed(termios, 38400); - -#ifdef IUTF8 - termios->c_iflag |= IUTF8; -#endif -#ifdef NL0 - termios->c_oflag |= NL0; -#endif -#ifdef CR0 - termios->c_oflag |= CR0; -#endif -#ifdef BS0 - termios->c_oflag |= BS0; -#endif -#ifdef VT0 - termios->c_oflag |= VT0; -#endif -#ifdef FF0 - termios->c_oflag |= FF0; -#endif -#ifdef ECHOCTL - termios->c_lflag |= ECHOCTL; -#endif -#ifdef ECHOKE - termios->c_lflag |= ECHOKE; -#endif - - termios->c_cc[VINTR] = 0x1f & 'C'; - termios->c_cc[VQUIT] = 0x1f & '\\'; - termios->c_cc[VERASE] = 0x7f; - termios->c_cc[VKILL] = 0x1f & 'U'; - termios->c_cc[VEOF] = 0x1f & 'D'; - termios->c_cc[VEOL] = _POSIX_VDISABLE; - termios->c_cc[VEOL2] = _POSIX_VDISABLE; - termios->c_cc[VSTART] = 0x1f & 'Q'; - termios->c_cc[VSTOP] = 0x1f & 'S'; - termios->c_cc[VSUSP] = 0x1f & 'Z'; - termios->c_cc[VREPRINT] = 0x1f & 'R'; - termios->c_cc[VWERASE] = 0x1f & 'W'; - termios->c_cc[VLNEXT] = 0x1f & 'V'; - termios->c_cc[VMIN] = 1; - termios->c_cc[VTIME] = 0; -} - -static int set_duplicating_descriptor(int fd, uv_pipe_t *pipe) - FUNC_ATTR_NONNULL_ALL -{ - int status = 0; // zero or negative error code (libuv convention) - int fd_dup = dup(fd); - if (fd_dup < 0) { - status = -errno; - ELOG("Failed to dup descriptor %d: %s", fd, strerror(errno)); - return status; - } - - if (os_set_cloexec(fd_dup) == -1) { - status = -errno; - ELOG("Failed to set CLOEXEC on duplicate fd"); - goto error; - } - - status = uv_pipe_open(pipe, fd_dup); - if (status) { - ELOG("Failed to set pipe to descriptor %d: %s", - fd_dup, uv_strerror(status)); - goto error; - } - return status; - -error: - close(fd_dup); - return status; -} - -static void chld_handler(uv_signal_t *handle, int signum) -{ - int stat = 0; - int pid; - - Loop *loop = handle->loop->data; - - kl_iter(WatcherPtr, loop->children, current) { - Process *proc = (*current)->data; - do { - pid = waitpid(proc->pid, &stat, WNOHANG); - } while (pid < 0 && errno == EINTR); - - if (pid <= 0) { - continue; - } - - if (WIFEXITED(stat)) { - proc->status = WEXITSTATUS(stat); - } else if (WIFSIGNALED(stat)) { - proc->status = 128 + WTERMSIG(stat); - } - proc->internal_exit_cb(proc); - } -} - -PtyProcess pty_process_init(Loop *loop, void *data) -{ - PtyProcess rv; - rv.process = process_init(loop, kProcessTypePty, data); - rv.width = 80; - rv.height = 24; - rv.tty_fd = -1; - return rv; -} diff --git a/src/nvim/os/pty_process_unix.h b/src/nvim/os/pty_process_unix.h @@ -1,18 +0,0 @@ -#pragma once -// IWYU pragma: private, include "nvim/os/pty_process.h" - -#include <stdint.h> -#include <sys/ioctl.h> - -#include "nvim/event/defs.h" - -typedef struct { - Process process; - uint16_t width, height; - struct winsize winsize; - int tty_fd; -} PtyProcess; - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "os/pty_process_unix.h.generated.h" -#endif diff --git a/src/nvim/os/pty_process_win.c b/src/nvim/os/pty_process_win.c @@ -1,440 +0,0 @@ -#include <assert.h> -#include <stdbool.h> -#include <stdlib.h> - -#include "nvim/ascii_defs.h" -#include "nvim/eval/typval.h" -#include "nvim/event/loop.h" -#include "nvim/log.h" -#include "nvim/mbyte.h" -#include "nvim/memory.h" -#include "nvim/os/os.h" -#include "nvim/os/pty_conpty_win.h" -#include "nvim/os/pty_process_win.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "os/pty_process_win.c.generated.h" -#endif - -static void CALLBACK pty_process_finish1(void *context, BOOLEAN unused) - FUNC_ATTR_NONNULL_ALL -{ - PtyProcess *ptyproc = (PtyProcess *)context; - Process *proc = (Process *)ptyproc; - - os_conpty_free(ptyproc->conpty); - // NB: pty_process_finish1() is called on a separate thread, - // but the timer only works properly if it's started by the main thread. - loop_schedule_fast(proc->loop, event_create(start_wait_eof_timer, ptyproc)); -} - -static void start_wait_eof_timer(void **argv) - FUNC_ATTR_NONNULL_ALL -{ - PtyProcess *ptyproc = (PtyProcess *)argv[0]; - - if (ptyproc->finish_wait != NULL) { - uv_timer_start(&ptyproc->wait_eof_timer, wait_eof_timer_cb, 200, 200); - } -} - -/// @returns zero on success, or negative error code. -int pty_process_spawn(PtyProcess *ptyproc) - FUNC_ATTR_NONNULL_ALL -{ - Process *proc = (Process *)ptyproc; - int status = 0; - conpty_t *conpty_object = NULL; - char *in_name = NULL; - char *out_name = NULL; - HANDLE process_handle = NULL; - uv_connect_t *in_req = NULL; - uv_connect_t *out_req = NULL; - wchar_t *cmd_line = NULL; - wchar_t *cwd = NULL; - wchar_t *env = NULL; - const char *emsg = NULL; - - assert(proc->err.s.closed); - - if (!os_has_conpty_working() || (conpty_object = os_conpty_init(&in_name, - &out_name, ptyproc->width, - ptyproc->height)) == NULL) { - status = UV_ENOSYS; - goto cleanup; - } - - if (!proc->in.closed) { - in_req = xmalloc(sizeof(uv_connect_t)); - uv_pipe_connect(in_req, - &proc->in.uv.pipe, - in_name, - pty_process_connect_cb); - } - - if (!proc->out.s.closed) { - out_req = xmalloc(sizeof(uv_connect_t)); - uv_pipe_connect(out_req, - &proc->out.s.uv.pipe, - out_name, - pty_process_connect_cb); - } - - if (proc->cwd != NULL) { - status = utf8_to_utf16(proc->cwd, -1, &cwd); - if (status != 0) { - emsg = "utf8_to_utf16(proc->cwd) failed"; - goto cleanup; - } - } - - status = build_cmd_line(proc->argv, &cmd_line, - os_shell_is_cmdexe(proc->argv[0])); - if (status != 0) { - emsg = "build_cmd_line failed"; - goto cleanup; - } - - if (proc->env != NULL) { - status = build_env_block(proc->env, &env); - } - - if (status != 0) { - emsg = "build_env_block failed"; - goto cleanup; - } - - if (!os_conpty_spawn(conpty_object, - &process_handle, - NULL, - cmd_line, - cwd, - env)) { - emsg = "os_conpty_spawn failed"; - status = (int)GetLastError(); - goto cleanup; - } - proc->pid = (int)GetProcessId(process_handle); - - uv_timer_init(&proc->loop->uv, &ptyproc->wait_eof_timer); - ptyproc->wait_eof_timer.data = (void *)ptyproc; - if (!RegisterWaitForSingleObject(&ptyproc->finish_wait, - process_handle, - pty_process_finish1, - ptyproc, - INFINITE, - WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE)) { - abort(); - } - - // Wait until pty_process_connect_cb is called. - while ((in_req != NULL && in_req->handle != NULL) - || (out_req != NULL && out_req->handle != NULL)) { - uv_run(&proc->loop->uv, UV_RUN_ONCE); - } - - ptyproc->conpty = conpty_object; - ptyproc->process_handle = process_handle; - conpty_object = NULL; - process_handle = NULL; - -cleanup: - if (status) { - // In the case of an error of MultiByteToWideChar or CreateProcessW. - ELOG("pty_process_spawn(%s): %s: error code: %d", - proc->argv[0], emsg, status); - status = os_translate_sys_error(status); - } - os_conpty_free(conpty_object); - xfree(in_name); - xfree(out_name); - if (process_handle != NULL) { - CloseHandle(process_handle); - } - xfree(in_req); - xfree(out_req); - xfree(cmd_line); - xfree(env); - xfree(cwd); - return status; -} - -const char *pty_process_tty_name(PtyProcess *ptyproc) -{ - return "?"; -} - -void pty_process_resize(PtyProcess *ptyproc, uint16_t width, uint16_t height) - FUNC_ATTR_NONNULL_ALL -{ - os_conpty_set_size(ptyproc->conpty, width, height); -} - -void pty_process_close(PtyProcess *ptyproc) - FUNC_ATTR_NONNULL_ALL -{ - Process *proc = (Process *)ptyproc; - - pty_process_close_master(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->process_handle != NULL) { - CloseHandle(ptyproc->process_handle); - ptyproc->process_handle = NULL; - } - - if (proc->internal_close_cb) { - proc->internal_close_cb(proc); - } -} - -void pty_process_close_master(PtyProcess *ptyproc) - FUNC_ATTR_NONNULL_ALL -{ -} - -void pty_process_teardown(Loop *loop) - FUNC_ATTR_NONNULL_ALL -{ -} - -static void pty_process_connect_cb(uv_connect_t *req, int status) - FUNC_ATTR_NONNULL_ALL -{ - assert(status == 0); - req->handle = NULL; -} - -static void wait_eof_timer_cb(uv_timer_t *wait_eof_timer) - FUNC_ATTR_NONNULL_ALL -{ - PtyProcess *ptyproc = wait_eof_timer->data; - Process *proc = (Process *)ptyproc; - - assert(ptyproc->finish_wait != NULL); - if (proc->out.s.closed || proc->out.did_eof || !uv_is_readable(proc->out.s.uvstream)) { - uv_timer_stop(&ptyproc->wait_eof_timer); - pty_process_finish2(ptyproc); - } -} - -static void pty_process_finish2(PtyProcess *ptyproc) - FUNC_ATTR_NONNULL_ALL -{ - Process *proc = (Process *)ptyproc; - - DWORD exit_code = 0; - GetExitCodeProcess(ptyproc->process_handle, &exit_code); - proc->status = proc->exit_signal ? 128 + proc->exit_signal : (int)exit_code; - - proc->internal_exit_cb(proc); -} - -/// Build the command line to pass to CreateProcessW. -/// -/// @param[in] argv Array with string arguments. -/// @param[out] cmd_line Location where saved built cmd line. -/// -/// @returns zero on success, or error code of MultiByteToWideChar function. -/// -static int build_cmd_line(char **argv, wchar_t **cmd_line, bool is_cmdexe) - FUNC_ATTR_NONNULL_ALL -{ - size_t utf8_cmd_line_len = 0; - size_t argc = 0; - QUEUE args_q; - - QUEUE_INIT(&args_q); - while (*argv) { - size_t buf_len = is_cmdexe ? (strlen(*argv) + 1) : (strlen(*argv) * 2 + 3); - ArgNode *arg_node = xmalloc(sizeof(*arg_node)); - arg_node->arg = xmalloc(buf_len); - if (is_cmdexe) { - xstrlcpy(arg_node->arg, *argv, buf_len); - } else { - quote_cmd_arg(arg_node->arg, buf_len, *argv); - } - utf8_cmd_line_len += strlen(arg_node->arg); - QUEUE_INIT(&arg_node->node); - QUEUE_INSERT_TAIL(&args_q, &arg_node->node); - argc++; - argv++; - } - - utf8_cmd_line_len += argc; - char *utf8_cmd_line = xmalloc(utf8_cmd_line_len); - *utf8_cmd_line = NUL; - QUEUE *q; - QUEUE_FOREACH(q, &args_q, { - ArgNode *arg_node = QUEUE_DATA(q, ArgNode, node); - xstrlcat(utf8_cmd_line, arg_node->arg, utf8_cmd_line_len); - QUEUE_REMOVE(q); - xfree(arg_node->arg); - xfree(arg_node); - if (!QUEUE_EMPTY(&args_q)) { - xstrlcat(utf8_cmd_line, " ", utf8_cmd_line_len); - } - }) - - int result = utf8_to_utf16(utf8_cmd_line, -1, cmd_line); - xfree(utf8_cmd_line); - return result; -} - -/// Emulate quote_cmd_arg of libuv and quotes command line argument. -/// Most of the code came from libuv. -/// -/// @param[out] dest Location where saved quotes argument. -/// @param dest_remaining Destination buffer size. -/// @param[in] src Pointer to argument. -/// -static void quote_cmd_arg(char *dest, size_t dest_remaining, const char *src) - FUNC_ATTR_NONNULL_ALL -{ - size_t src_len = strlen(src); - bool quote_hit = true; - char *start = dest; - - if (src_len == 0) { - // Need double quotation for empty argument. - snprintf(dest, dest_remaining, "\"\""); - return; - } - - if (NULL == strpbrk(src, " \t\"")) { - // No quotation needed. - xstrlcpy(dest, src, dest_remaining); - return; - } - - if (NULL == strpbrk(src, "\"\\")) { - // No embedded double quotes or backlashes, so I can just wrap quote marks. - // around the whole thing. - snprintf(dest, dest_remaining, "\"%s\"", src); - return; - } - - // Expected input/output: - // input : 'hello"world' - // output: '"hello\"world"' - // input : 'hello""world' - // output: '"hello\"\"world"' - // input : 'hello\world' - // output: 'hello\world' - // input : 'hello\\world' - // output: 'hello\\world' - // input : 'hello\"world' - // output: '"hello\\\"world"' - // input : 'hello\\"world' - // output: '"hello\\\\\"world"' - // input : 'hello world\' - // output: '"hello world\\"' - - assert(dest_remaining--); - *(dest++) = NUL; - assert(dest_remaining--); - *(dest++) = '"'; - for (size_t i = src_len; i > 0; i--) { - assert(dest_remaining--); - *(dest++) = src[i - 1]; - if (quote_hit && src[i - 1] == '\\') { - assert(dest_remaining--); - *(dest++) = '\\'; - } else if (src[i - 1] == '"') { - quote_hit = true; - assert(dest_remaining--); - *(dest++) = '\\'; - } else { - quote_hit = false; - } - } - assert(dest_remaining); - *dest = '"'; - - while (start < dest) { - char tmp = *start; - *start = *dest; - *dest = tmp; - start++; - dest--; - } -} - -typedef struct EnvNode { - wchar_t *str; - size_t len; - QUEUE node; -} EnvNode; - -/// Build the environment block to pass to CreateProcessW. -/// -/// @param[in] denv Dict of environment name/value pairs -/// @param[out] env Allocated environment block -/// -/// @returns zero on success or error code of MultiByteToWideChar function. -static int build_env_block(dict_T *denv, wchar_t **env_block) -{ - const size_t denv_size = (size_t)tv_dict_len(denv); - size_t env_block_len = 0; - int rc = 0; - char **env = tv_dict_to_env(denv); - - QUEUE *q; - QUEUE env_q; - QUEUE_INIT(&env_q); - // Convert env vars to wchar_t and calculate how big the final env block - // needs to be - for (size_t i = 0; i < denv_size; i++) { - EnvNode *env_node = xmalloc(sizeof(*env_node)); - rc = utf8_to_utf16(env[i], -1, &env_node->str); - if (rc != 0) { - goto cleanup; - } - env_node->len = wcslen(env_node->str) + 1; - env_block_len += env_node->len; - QUEUE_INSERT_TAIL(&env_q, &env_node->node); - } - - // Additional NUL after the final entry - env_block_len++; - - *env_block = xmalloc(sizeof(**env_block) * env_block_len); - wchar_t *pos = *env_block; - - QUEUE_FOREACH(q, &env_q, { - EnvNode *env_node = QUEUE_DATA(q, EnvNode, node); - memcpy(pos, env_node->str, env_node->len * sizeof(*pos)); - pos += env_node->len; - }) - - *pos = L'\0'; - -cleanup: - q = QUEUE_HEAD(&env_q); - while (q != &env_q) { - QUEUE *next = q->next; - EnvNode *env_node = QUEUE_DATA(q, EnvNode, node); - XFREE_CLEAR(env_node->str); - QUEUE_REMOVE(q); - xfree(env_node); - q = next; - } - - return rc; -} - -PtyProcess pty_process_init(Loop *loop, void *data) -{ - PtyProcess rv; - rv.process = process_init(loop, kProcessTypePty, data); - rv.width = 80; - rv.height = 24; - rv.conpty = NULL; - rv.finish_wait = NULL; - rv.process_handle = NULL; - return rv; -} diff --git a/src/nvim/os/pty_process_win.h b/src/nvim/os/pty_process_win.h @@ -1,27 +0,0 @@ -#pragma once -// IWYU pragma: private, include "nvim/os/pty_process.h" - -#include <uv.h> - -#include "nvim/event/process.h" -#include "nvim/lib/queue_defs.h" -#include "nvim/os/pty_conpty_win.h" - -typedef struct pty_process { - Process process; - uint16_t width, height; - conpty_t *conpty; - HANDLE finish_wait; - HANDLE process_handle; - uv_timer_t wait_eof_timer; -} PtyProcess; - -// Structure used by build_cmd_line() -typedef struct arg_node { - char *arg; // pointer to argument. - QUEUE node; // QUEUE structure. -} ArgNode; - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "os/pty_process_win.h.generated.h" -#endif diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c @@ -14,10 +14,10 @@ #include "nvim/eval.h" #include "nvim/eval/typval_defs.h" #include "nvim/event/defs.h" -#include "nvim/event/libuv_process.h" +#include "nvim/event/libuv_proc.h" #include "nvim/event/loop.h" #include "nvim/event/multiqueue.h" -#include "nvim/event/process.h" +#include "nvim/event/proc.h" #include "nvim/event/rstream.h" #include "nvim/event/stream.h" #include "nvim/event/wstream.h" @@ -872,12 +872,12 @@ static int do_os_system(char **argv, const char *input, size_t len, char **outpu char prog[MAXPATHL]; xstrlcpy(prog, argv[0], MAXPATHL); - LibuvProcess uvproc = libuv_process_init(&main_loop, &buf); - Process *proc = &uvproc.process; + LibuvProc uvproc = libuv_proc_init(&main_loop, &buf); + Proc *proc = &uvproc.proc; MultiQueue *events = multiqueue_new_child(main_loop.events); proc->events = events; proc->argv = argv; - int status = process_spawn(proc, has_input, true, true); + int status = proc_spawn(proc, has_input, true, true); if (status) { loop_poll_events(&main_loop, 0); // Failed, probably 'shell' is not executable. @@ -910,7 +910,7 @@ static int do_os_system(char **argv, const char *input, size_t len, char **outpu if (!wstream_write(&proc->in, input_buffer)) { // couldn't write, stop the process and tell the user about it - process_stop(proc); + proc_stop(proc); return -1; } // close the input stream after everything is written @@ -927,7 +927,7 @@ static int do_os_system(char **argv, const char *input, size_t len, char **outpu msg_no_more = true; lines_left = -1; } - int exitcode = process_wait(proc, -1, NULL); + int exitcode = proc_wait(proc, -1, NULL); if (!got_int && out_data_decide_throttle(0)) { // Last chunk of output was skipped; display it now. out_data_ring(NULL, SIZE_MAX); diff --git a/src/nvim/profile.c b/src/nvim/profile.c @@ -950,8 +950,8 @@ void time_msg(const char *mesg, const proftime_T *start) /// Initializes the `time_fd` stream for the --startuptime report. /// /// @param fname startuptime report file path -/// @param process_name name of the current Nvim process to write in the report. -void time_init(const char *fname, const char *process_name) +/// @param proc_name name of the current Nvim process to write in the report. +void time_init(const char *fname, const char *proc_name) { const size_t bufsize = 8192; // Big enough for the entire --startuptime report. time_fd = fopen(fname, "a"); @@ -972,7 +972,7 @@ void time_init(const char *fname, const char *process_name) semsg("time_init: setvbuf failed: %d %s", r, uv_err_name(r)); return; } - fprintf(time_fd, "--- Startup times for process: %s ---\n", process_name); + fprintf(time_fd, "--- Startup times for process: %s ---\n", proc_name); } /// Flushes the startuptimes to disk for the current process diff --git a/test/README.md b/test/README.md @@ -103,7 +103,7 @@ Debugging tests DBG 2022-06-15T18:37:45.227 T57.58016.0/c UI: stop INF 2022-06-15T18:37:45.227 T57.58016.0/c os_exit:595: Nvim exit: 0 DBG 2022-06-15T18:37:45.229 T57.58016.0 read_cb:118: closing Stream (0x7fd5d700ea18): EOF (end of file) - INF 2022-06-15T18:37:45.229 T57.58016.0 on_process_exit:400: exited: pid=58017 status=0 stoptime=0 + INF 2022-06-15T18:37:45.229 T57.58016.0 on_proc_exit:400: exited: pid=58017 status=0 stoptime=0 ``` - You can set `$GDB` to [run functional tests under gdbserver](https://github.com/neovim/neovim/pull/1527):