From c0952e62fd0ee16a3275bb69e0de04c836b39015 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 7 Jun 2023 13:52:23 +0100 Subject: feat(lua): add `vim.system()` feat(lua): add vim.system() Problem: Handling system commands in Lua is tedious and error-prone: - vim.fn.jobstart() is vimscript and comes with all limitations attached to typval. - vim.loop.spawn is too low level Solution: Add vim.system(). Partly inspired by Python's subprocess module Does not expose any libuv objects. --- runtime/lua/vim/_system.lua | 342 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 runtime/lua/vim/_system.lua (limited to 'runtime/lua/vim/_system.lua') diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua new file mode 100644 index 0000000000..eadf801a31 --- /dev/null +++ b/runtime/lua/vim/_system.lua @@ -0,0 +1,342 @@ +local uv = vim.uv + +--- @class SystemOpts +--- @field cmd string[] +--- @field stdin string|string[]|true +--- @field stdout fun(err:string, data: string)|false +--- @field stderr fun(err:string, data: string)|false +--- @field cwd? string +--- @field env? table +--- @field clear_env? boolean +--- @field text boolean? +--- @field timeout? integer Timeout in ms +--- @field detach? boolean + +--- @class SystemCompleted +--- @field code integer +--- @field signal integer +--- @field stdout? string +--- @field stderr? string + +--- @class SystemState +--- @field handle uv_process_t +--- @field timer uv_timer_t +--- @field pid integer +--- @field timeout? integer +--- @field done boolean +--- @field stdin uv_stream_t? +--- @field stdout uv_stream_t? +--- @field stderr uv_stream_t? +--- @field cmd string[] +--- @field result? SystemCompleted + +---@private +---@param state SystemState +local function close_handles(state) + for _, handle in pairs({ state.handle, state.stdin, state.stdout, state.stderr }) do + if not handle:is_closing() then + handle:close() + end + end +end + +--- @param cmd string[] +--- @return SystemCompleted +local function timeout_result(cmd) + local cmd_str = table.concat(cmd, ' ') + local err = string.format("Command timed out: '%s'", cmd_str) + return { code = 0, signal = 2, stdout = '', stderr = err } +end + +--- @class SystemObj +--- @field pid integer +--- @field private _state SystemState +--- @field wait fun(self: SystemObj, timeout?: integer): SystemCompleted +--- @field kill fun(self: SystemObj, signal: integer) +--- @field write fun(self: SystemObj, data?: string|string[]) +--- @field is_closing fun(self: SystemObj): boolean? +local SystemObj = {} + +--- @param state SystemState +--- @return SystemObj +local function new_systemobj(state) + return setmetatable({ + pid = state.pid, + _state = state, + }, { __index = SystemObj }) +end + +--- @param signal integer +function SystemObj:kill(signal) + local state = self._state + state.handle:kill(signal) + close_handles(state) +end + +local MAX_TIMEOUT = 2 ^ 31 + +--- @param timeout? integer +--- @return SystemCompleted +function SystemObj:wait(timeout) + local state = self._state + + vim.wait(timeout or state.timeout or MAX_TIMEOUT, function() + return state.done + end) + + if not state.done then + self:kill(6) -- 'sigint' + state.result = timeout_result(state.cmd) + end + + return state.result +end + +--- @param data string[]|string|nil +function SystemObj:write(data) + local stdin = self._state.stdin + + if not stdin then + error('stdin has not been opened on this object') + end + + if type(data) == 'table' then + for _, v in ipairs(data) do + stdin:write(v) + stdin:write('\n') + end + elseif type(data) == 'string' then + stdin:write(data) + elseif data == nil then + -- Shutdown the write side of the duplex stream and then close the pipe. + -- Note shutdown will wait for all the pending write requests to complete + -- TODO(lewis6991): apparently shutdown doesn't behave this way. + -- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616) + stdin:write('', function() + stdin:shutdown(function() + if stdin then + stdin:close() + end + end) + end) + end +end + +--- @return boolean +function SystemObj:is_closing() + local handle = self._state.handle + return handle == nil or handle:is_closing() +end + +---@private +---@param output function|'false' +---@return uv_stream_t? +---@return function? Handler +local function setup_output(output) + if output == nil then + return assert(uv.new_pipe(false)), nil + end + + if type(output) == 'function' then + return assert(uv.new_pipe(false)), output + end + + assert(output == false) + return nil, nil +end + +---@private +---@param input string|string[]|true|nil +---@return uv_stream_t? +---@return string|string[]? +local function setup_input(input) + if not input then + return + end + + local towrite --- @type string|string[]? + if type(input) == 'string' or type(input) == 'table' then + towrite = input + end + + return assert(uv.new_pipe(false)), towrite +end + +--- @return table +local function base_env() + local env = vim.fn.environ() + env['NVIM'] = vim.v.servername + env['NVIM_LISTEN_ADDRESS'] = nil + return env +end + +--- uv.spawn will completely overwrite the environment +--- when we just want to modify the existing one, so +--- make sure to prepopulate it with the current env. +--- @param env? table +--- @param clear_env? boolean +--- @return string[]? +local function setup_env(env, clear_env) + if clear_env then + return env + end + + --- @type table + env = vim.tbl_extend('force', base_env(), env or {}) + + local renv = {} --- @type string[] + for k, v in pairs(env) do + renv[#renv + 1] = string.format('%s=%s', k, tostring(v)) + end + + return renv +end + +--- @param stream uv_stream_t +--- @param text? boolean +--- @param bucket string[] +--- @return fun(err: string?, data: string?) +local function default_handler(stream, text, bucket) + return function(err, data) + if err then + error(err) + end + if data ~= nil then + if text then + bucket[#bucket + 1] = data:gsub('\r\n', '\n') + else + bucket[#bucket + 1] = data + end + else + stream:read_stop() + stream:close() + end + end +end + +local M = {} + +--- @param cmd string +--- @param opts uv.aliases.spawn_options +--- @param on_exit fun(code: integer, signal: integer) +--- @param on_error fun() +--- @return uv_process_t, integer +local function spawn(cmd, opts, on_exit, on_error) + local handle, pid_or_err = uv.spawn(cmd, opts, on_exit) + if not handle then + on_error() + error(pid_or_err) + end + return handle, pid_or_err --[[@as integer]] +end + +--- Run a system command +--- +--- @param cmd string[] +--- @param opts? SystemOpts +--- @param on_exit? fun(out: SystemCompleted) +--- @return SystemObj +function M.run(cmd, opts, on_exit) + vim.validate({ + cmd = { cmd, 'table' }, + opts = { opts, 'table', true }, + on_exit = { on_exit, 'function', true }, + }) + + opts = opts or {} + + local stdout, stdout_handler = setup_output(opts.stdout) + local stderr, stderr_handler = setup_output(opts.stderr) + local stdin, towrite = setup_input(opts.stdin) + + --- @type SystemState + local state = { + done = false, + cmd = cmd, + timeout = opts.timeout, + stdin = stdin, + stdout = stdout, + stderr = stderr, + } + + -- Define data buckets as tables and concatenate the elements at the end as + -- one operation. + --- @type string[], string[] + local stdout_data, stderr_data + + state.handle, state.pid = spawn(cmd[1], { + args = vim.list_slice(cmd, 2), + stdio = { stdin, stdout, stderr }, + cwd = opts.cwd, + env = setup_env(opts.env, opts.clear_env), + detached = opts.detach, + hide = true, + }, function(code, signal) + close_handles(state) + if state.timer then + state.timer:stop() + state.timer:close() + end + + local check = assert(uv.new_check()) + + check:start(function() + for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do + if not pipe:is_closing() then + return + end + end + check:stop() + + state.done = true + state.result = { + code = code, + signal = signal, + stdout = stdout_data and table.concat(stdout_data) or nil, + stderr = stderr_data and table.concat(stderr_data) or nil, + } + + if on_exit then + on_exit(state.result) + end + end) + end, function() + close_handles(state) + end) + + if stdout then + stdout_data = {} + stdout:read_start(stdout_handler or default_handler(stdout, opts.text, stdout_data)) + end + + if stderr then + stderr_data = {} + stderr:read_start(stderr_handler or default_handler(stderr, opts.text, stderr_data)) + end + + local obj = new_systemobj(state) + + if towrite then + obj:write(towrite) + obj:write(nil) -- close the stream + end + + if opts.timeout then + state.timer = assert(uv.new_timer()) + state.timer:start(opts.timeout, 0, function() + state.timer:stop() + state.timer:close() + if state.handle and state.handle:is_active() then + obj:kill(6) --- 'sigint' + state.result = timeout_result(state.cmd) + if on_exit then + on_exit(state.result) + end + end + end) + end + + return obj +end + +return M -- cgit From 2afb04758c341e17c70b8d2e3869c901c8cdb7d2 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 6 Jul 2023 12:56:19 +0800 Subject: fix(vim.system): close check handle (#24270) Fix hang after running vim.system() with sanitizers. --- runtime/lua/vim/_system.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim/_system.lua') diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index eadf801a31..ff566866c0 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -287,6 +287,7 @@ function M.run(cmd, opts, on_exit) end end check:stop() + check:close() state.done = true state.result = { -- cgit From be74807eef13ff8c90d55cf8b22b01d6d33b1641 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 18 Jul 2023 15:42:30 +0100 Subject: docs(lua): more improvements (#24387) * docs(lua): teach lua2dox how to table * docs(lua): teach gen_vimdoc.py about local functions No more need to mark local functions with @private * docs(lua): mention @nodoc and @meta in dev-lua-doc * fixup! Co-authored-by: Justin M. Keyes --------- Co-authored-by: Justin M. Keyes --- runtime/lua/vim/_system.lua | 3 --- 1 file changed, 3 deletions(-) (limited to 'runtime/lua/vim/_system.lua') diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index ff566866c0..e6dab90425 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -30,7 +30,6 @@ local uv = vim.uv --- @field cmd string[] --- @field result? SystemCompleted ----@private ---@param state SystemState local function close_handles(state) for _, handle in pairs({ state.handle, state.stdin, state.stdout, state.stderr }) do @@ -128,7 +127,6 @@ function SystemObj:is_closing() return handle == nil or handle:is_closing() end ----@private ---@param output function|'false' ---@return uv_stream_t? ---@return function? Handler @@ -145,7 +143,6 @@ local function setup_output(output) return nil, nil end ----@private ---@param input string|string[]|true|nil ---@return uv_stream_t? ---@return string|string[]? -- cgit From c43c745a14dced87a23227d7be4f1c33d4455193 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Wed, 9 Aug 2023 11:06:13 +0200 Subject: fix(lua): improve annotations for stricter luals diagnostics (#24609) Problem: luals returns stricter diagnostics with bundled luarc.json Solution: Improve some function and type annotations: * use recognized uv.* types * disable diagnostic for global `vim` in shared.lua * docs: don't start comment lines with taglink (otherwise LuaLS will interpret it as a type) * add type alias for lpeg pattern * fix return annotation for `vim.secure.trust` * rename local Range object in vim.version (shadows `Range` in vim.treesitter) * fix some "missing fields" warnings * add missing required fields for test functions in eval.lua * rename lsp meta files for consistency --- runtime/lua/vim/_system.lua | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/_system.lua') diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index e6dab90425..6f5e95eb24 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -1,14 +1,13 @@ local uv = vim.uv --- @class SystemOpts ---- @field cmd string[] ---- @field stdin string|string[]|true ---- @field stdout fun(err:string, data: string)|false ---- @field stderr fun(err:string, data: string)|false +--- @field stdin? string|string[]|true +--- @field stdout? fun(err:string, data: string)|false +--- @field stderr? fun(err:string, data: string)|false --- @field cwd? string --- @field env? table --- @field clear_env? boolean ---- @field text boolean? +--- @field text? boolean --- @field timeout? integer Timeout in ms --- @field detach? boolean @@ -19,15 +18,14 @@ local uv = vim.uv --- @field stderr? string --- @class SystemState ---- @field handle uv_process_t ---- @field timer uv_timer_t ---- @field pid integer +--- @field handle? uv.uv_process_t +--- @field timer? uv.uv_timer_t +--- @field pid? integer --- @field timeout? integer ---- @field done boolean ---- @field stdin uv_stream_t? ---- @field stdout uv_stream_t? ---- @field stderr uv_stream_t? ---- @field cmd string[] +--- @field done? boolean +--- @field stdin? uv.uv_stream_t +--- @field stdout? uv.uv_stream_t +--- @field stderr? uv.uv_stream_t --- @field result? SystemCompleted ---@param state SystemState @@ -128,7 +126,7 @@ function SystemObj:is_closing() end ---@param output function|'false' ----@return uv_stream_t? +---@return uv.uv_stream_t? ---@return function? Handler local function setup_output(output) if output == nil then @@ -144,7 +142,7 @@ local function setup_output(output) end ---@param input string|string[]|true|nil ----@return uv_stream_t? +---@return uv.uv_stream_t? ---@return string|string[]? local function setup_input(input) if not input then @@ -189,7 +187,7 @@ local function setup_env(env, clear_env) return renv end ---- @param stream uv_stream_t +--- @param stream uv.uv_stream_t --- @param text? boolean --- @param bucket string[] --- @return fun(err: string?, data: string?) @@ -217,7 +215,7 @@ local M = {} --- @param opts uv.aliases.spawn_options --- @param on_exit fun(code: integer, signal: integer) --- @param on_error fun() ---- @return uv_process_t, integer +--- @return uv.uv_process_t, integer local function spawn(cmd, opts, on_exit, on_error) local handle, pid_or_err = uv.spawn(cmd, opts, on_exit) if not handle then -- cgit From a44521f46e6f79171d034e5cce1a4dc266d23e49 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sun, 3 Sep 2023 10:17:24 +0100 Subject: fix(vim.system): let on_exit handle cleanup after kill Fixes #25000 --- runtime/lua/vim/_system.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/_system.lua') diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index 6f5e95eb24..ba3fc34e78 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -18,6 +18,7 @@ local uv = vim.uv --- @field stderr? string --- @class SystemState +--- @field cmd string[] --- @field handle? uv.uv_process_t --- @field timer? uv.uv_timer_t --- @field pid? integer @@ -63,11 +64,9 @@ local function new_systemobj(state) }, { __index = SystemObj }) end ---- @param signal integer +--- @param signal integer|string function SystemObj:kill(signal) - local state = self._state - state.handle:kill(signal) - close_handles(state) + self._state.handle:kill(signal) end local MAX_TIMEOUT = 2 ^ 31 @@ -159,7 +158,7 @@ end --- @return table local function base_env() - local env = vim.fn.environ() + local env = vim.fn.environ() --- @type table env['NVIM'] = vim.v.servername env['NVIM_LISTEN_ADDRESS'] = nil return env @@ -212,7 +211,7 @@ end local M = {} --- @param cmd string ---- @param opts uv.aliases.spawn_options +--- @param opts uv.spawn.options --- @param on_exit fun(code: integer, signal: integer) --- @param on_error fun() --- @return uv.uv_process_t, integer -- cgit From 6d5f12efd286c684de8608c07bb0f76a9d594b5b Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 4 Sep 2023 11:30:16 +0100 Subject: fix(vim.system): make timeout work properly Mimic the behaviour of timeout(1) from coreutils. --- runtime/lua/vim/_system.lua | 102 +++++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 38 deletions(-) (limited to 'runtime/lua/vim/_system.lua') diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index ba3fc34e78..7bd7ff91f9 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -2,8 +2,8 @@ local uv = vim.uv --- @class SystemOpts --- @field stdin? string|string[]|true ---- @field stdout? fun(err:string, data: string)|false ---- @field stderr? fun(err:string, data: string)|false +--- @field stdout? fun(err:string?, data: string?)|false +--- @field stderr? fun(err:string?, data: string?)|false --- @field cwd? string --- @field env? table --- @field clear_env? boolean @@ -18,39 +18,46 @@ local uv = vim.uv --- @field stderr? string --- @class SystemState ---- @field cmd string[] --- @field handle? uv.uv_process_t --- @field timer? uv.uv_timer_t --- @field pid? integer --- @field timeout? integer ---- @field done? boolean +--- @field done? boolean|'timeout' --- @field stdin? uv.uv_stream_t --- @field stdout? uv.uv_stream_t --- @field stderr? uv.uv_stream_t --- @field result? SystemCompleted ----@param state SystemState -local function close_handles(state) - for _, handle in pairs({ state.handle, state.stdin, state.stdout, state.stderr }) do - if not handle:is_closing() then - handle:close() - end +--- @enum vim.SystemSig +local SIG = { + HUP = 1, -- Hangup + INT = 2, -- Interrupt from keyboard + KILL = 9, -- Kill signal + TERM = 15, -- Termination signal + -- STOP = 17,19,23 -- Stop the process +} + +---@param handle uv.uv_handle_t? +local function close_handle(handle) + if handle and not handle:is_closing() then + handle:close() end end ---- @param cmd string[] ---- @return SystemCompleted -local function timeout_result(cmd) - local cmd_str = table.concat(cmd, ' ') - local err = string.format("Command timed out: '%s'", cmd_str) - return { code = 0, signal = 2, stdout = '', stderr = err } +---@param state SystemState +local function close_handles(state) + close_handle(state.handle) + close_handle(state.stdin) + close_handle(state.stdout) + close_handle(state.stderr) + close_handle(state.timer) end --- @class SystemObj --- @field pid integer --- @field private _state SystemState --- @field wait fun(self: SystemObj, timeout?: integer): SystemCompleted ---- @field kill fun(self: SystemObj, signal: integer) +--- @field kill fun(self: SystemObj, signal: integer|string) --- @field write fun(self: SystemObj, data?: string|string[]) --- @field is_closing fun(self: SystemObj): boolean? local SystemObj = {} @@ -69,6 +76,13 @@ function SystemObj:kill(signal) self._state.handle:kill(signal) end +--- @package +--- @param signal? vim.SystemSig +function SystemObj:_timeout(signal) + self._state.done = 'timeout' + self:kill(signal or SIG.TERM) +end + local MAX_TIMEOUT = 2 ^ 31 --- @param timeout? integer @@ -76,13 +90,16 @@ local MAX_TIMEOUT = 2 ^ 31 function SystemObj:wait(timeout) local state = self._state - vim.wait(timeout or state.timeout or MAX_TIMEOUT, function() - return state.done + local done = vim.wait(timeout or state.timeout or MAX_TIMEOUT, function() + return state.result ~= nil end) - if not state.done then - self:kill(6) -- 'sigint' - state.result = timeout_result(state.cmd) + if not done then + -- Send sigkill since this cannot be caught + self:_timeout(SIG.KILL) + vim.wait(timeout or state.timeout or MAX_TIMEOUT, function() + return state.result ~= nil + end) end return state.result @@ -124,9 +141,9 @@ function SystemObj:is_closing() return handle == nil or handle:is_closing() end ----@param output function|'false' +---@param output fun(err:string?, data: string?)|false ---@return uv.uv_stream_t? ----@return function? Handler +---@return fun(err:string?, data: string?)? Handler local function setup_output(output) if output == nil then return assert(uv.new_pipe(false)), nil @@ -224,6 +241,19 @@ local function spawn(cmd, opts, on_exit, on_error) return handle, pid_or_err --[[@as integer]] end +---@param timeout integer +---@param cb fun() +---@return uv.uv_timer_t +local function timer_oneshot(timeout, cb) + local timer = assert(uv.new_timer()) + timer:start(timeout, 0, function() + timer:stop() + timer:close() + cb() + end) + return timer +end + --- Run a system command --- --- @param cmd string[] @@ -267,10 +297,6 @@ function M.run(cmd, opts, on_exit) hide = true, }, function(code, signal) close_handles(state) - if state.timer then - state.timer:stop() - state.timer:close() - end local check = assert(uv.new_check()) @@ -283,7 +309,14 @@ function M.run(cmd, opts, on_exit) check:stop() check:close() - state.done = true + if state.done == nil then + state.done = true + end + + if code == 0 and state.done == 'timeout' then + code = 124 + end + state.result = { code = code, signal = signal, @@ -317,16 +350,9 @@ function M.run(cmd, opts, on_exit) end if opts.timeout then - state.timer = assert(uv.new_timer()) - state.timer:start(opts.timeout, 0, function() - state.timer:stop() - state.timer:close() + state.timer = timer_oneshot(opts.timeout, function() if state.handle and state.handle:is_active() then - obj:kill(6) --- 'sigint' - state.result = timeout_result(state.cmd) - if on_exit then - on_exit(state.result) - end + obj:_timeout() end end) end -- cgit From 80d1333b7317460c562a982ac21f900d9fbd89f6 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 4 Sep 2023 12:03:03 +0100 Subject: refactor(vim.system): factor out on_exit handling --- runtime/lua/vim/_system.lua | 121 +++++++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 57 deletions(-) (limited to 'runtime/lua/vim/_system.lua') diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index 7bd7ff91f9..5ea0071215 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -11,13 +11,13 @@ local uv = vim.uv --- @field timeout? integer Timeout in ms --- @field detach? boolean ---- @class SystemCompleted +--- @class vim.SystemCompleted --- @field code integer --- @field signal integer --- @field stdout? string --- @field stderr? string ---- @class SystemState +--- @class vim.SystemState --- @field handle? uv.uv_process_t --- @field timer? uv.uv_timer_t --- @field pid? integer @@ -26,7 +26,9 @@ local uv = vim.uv --- @field stdin? uv.uv_stream_t --- @field stdout? uv.uv_stream_t --- @field stderr? uv.uv_stream_t ---- @field result? SystemCompleted +--- @field stdout_data? string[] +--- @field stderr_data? string[] +--- @field result? vim.SystemCompleted --- @enum vim.SystemSig local SIG = { @@ -44,7 +46,7 @@ local function close_handle(handle) end end ----@param state SystemState +---@param state vim.SystemState local function close_handles(state) close_handle(state.handle) close_handle(state.stdin) @@ -53,17 +55,17 @@ local function close_handles(state) close_handle(state.timer) end ---- @class SystemObj +--- @class vim.SystemObj --- @field pid integer ---- @field private _state SystemState ---- @field wait fun(self: SystemObj, timeout?: integer): SystemCompleted ---- @field kill fun(self: SystemObj, signal: integer|string) ---- @field write fun(self: SystemObj, data?: string|string[]) ---- @field is_closing fun(self: SystemObj): boolean? +--- @field private _state vim.SystemState +--- @field wait fun(self: vim.SystemObj, timeout?: integer): vim.SystemCompleted +--- @field kill fun(self: vim.SystemObj, signal: integer|string) +--- @field write fun(self: vim.SystemObj, data?: string|string[]) +--- @field is_closing fun(self: vim.SystemObj): boolean? local SystemObj = {} ---- @param state SystemState ---- @return SystemObj +--- @param state vim.SystemState +--- @return vim.SystemObj local function new_systemobj(state) return setmetatable({ pid = state.pid, @@ -86,7 +88,7 @@ end local MAX_TIMEOUT = 2 ^ 31 --- @param timeout? integer ---- @return SystemCompleted +--- @return vim.SystemCompleted function SystemObj:wait(timeout) local state = self._state @@ -254,12 +256,53 @@ local function timer_oneshot(timeout, cb) return timer end +--- @param state vim.SystemState +--- @param code integer +--- @param signal integer +--- @param on_exit fun(result: vim.SystemCompleted)? +local function _on_exit(state, code, signal, on_exit) + close_handles(state) + + local check = assert(uv.new_check()) + check:start(function() + for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do + if not pipe:is_closing() then + return + end + end + check:stop() + check:close() + + if state.done == nil then + state.done = true + end + + if code == 0 and state.done == 'timeout' then + code = 124 + end + + local stdout_data = state.stdout_data + local stderr_data = state.stderr_data + + state.result = { + code = code, + signal = signal, + stdout = stdout_data and table.concat(stdout_data) or nil, + stderr = stderr_data and table.concat(stderr_data) or nil, + } + + if on_exit then + on_exit(state.result) + end + end) +end + --- Run a system command --- --- @param cmd string[] --- @param opts? SystemOpts ---- @param on_exit? fun(out: SystemCompleted) ---- @return SystemObj +--- @param on_exit? fun(out: vim.SystemCompleted) +--- @return vim.SystemObj function M.run(cmd, opts, on_exit) vim.validate({ cmd = { cmd, 'table' }, @@ -273,7 +316,7 @@ function M.run(cmd, opts, on_exit) local stderr, stderr_handler = setup_output(opts.stderr) local stdin, towrite = setup_input(opts.stdin) - --- @type SystemState + --- @type vim.SystemState local state = { done = false, cmd = cmd, @@ -283,11 +326,6 @@ function M.run(cmd, opts, on_exit) stderr = stderr, } - -- Define data buckets as tables and concatenate the elements at the end as - -- one operation. - --- @type string[], string[] - local stdout_data, stderr_data - state.handle, state.pid = spawn(cmd[1], { args = vim.list_slice(cmd, 2), stdio = { stdin, stdout, stderr }, @@ -296,50 +334,19 @@ function M.run(cmd, opts, on_exit) detached = opts.detach, hide = true, }, function(code, signal) - close_handles(state) - - local check = assert(uv.new_check()) - - check:start(function() - for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do - if not pipe:is_closing() then - return - end - end - check:stop() - check:close() - - if state.done == nil then - state.done = true - end - - if code == 0 and state.done == 'timeout' then - code = 124 - end - - state.result = { - code = code, - signal = signal, - stdout = stdout_data and table.concat(stdout_data) or nil, - stderr = stderr_data and table.concat(stderr_data) or nil, - } - - if on_exit then - on_exit(state.result) - end - end) + _on_exit(state, code, signal, on_exit) end, function() close_handles(state) end) if stdout then - stdout_data = {} - stdout:read_start(stdout_handler or default_handler(stdout, opts.text, stdout_data)) + state.stdout_data = {} + stdout:read_start(stdout_handler or default_handler(stdout, opts.text, state.stdout_data)) end if stderr then - stderr_data = {} - stderr:read_start(stderr_handler or default_handler(stderr, opts.text, stderr_data)) + state.stderr_data = {} + stderr:read_start(stderr_handler or default_handler(stderr, opts.text, state.stderr_data)) end local obj = new_systemobj(state) -- cgit From be8b15200d7093726b0999ccfd4a3e9952656d47 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 5 Sep 2023 14:38:57 +0100 Subject: fix: windows timeouts have exit code 1 --- runtime/lua/vim/_system.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/_system.lua') diff --git a/runtime/lua/vim/_system.lua b/runtime/lua/vim/_system.lua index 5ea0071215..9279febddf 100644 --- a/runtime/lua/vim/_system.lua +++ b/runtime/lua/vim/_system.lua @@ -277,7 +277,9 @@ local function _on_exit(state, code, signal, on_exit) state.done = true end - if code == 0 and state.done == 'timeout' then + if (code == 0 or code == 1) and state.done == 'timeout' then + -- Unix: code == 0 + -- Windows: code == 1 code = 124 end @@ -326,10 +328,12 @@ function M.run(cmd, opts, on_exit) stderr = stderr, } + --- @diagnostic disable-next-line:missing-fields state.handle, state.pid = spawn(cmd[1], { args = vim.list_slice(cmd, 2), stdio = { stdin, stdout, stderr }, cwd = opts.cwd, + --- @diagnostic disable-next-line:assign-type-mismatch env = setup_env(opts.env, opts.clear_env), detached = opts.detach, hide = true, -- cgit