mirror of
https://github.com/neovim/neovim.git
synced 2024-12-19 10:45:16 -07:00
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.
This commit is contained in:
parent
4ecc71f6fc
commit
c0952e62fd
@ -1566,6 +1566,77 @@ schedule_wrap({cb}) *vim.schedule_wrap()*
|
||||
• |vim.schedule()|
|
||||
• |vim.in_fast_event()|
|
||||
|
||||
system({cmd}, {opts}, {on_exit}) *vim.system()*
|
||||
Run a system command
|
||||
|
||||
Examples: >lua
|
||||
|
||||
local on_exit = function(obj)
|
||||
print(obj.code)
|
||||
print(obj.signal)
|
||||
print(obj.stdout)
|
||||
print(obj.stderr)
|
||||
end
|
||||
|
||||
-- Run asynchronously
|
||||
vim.system({'echo', 'hello'}, { text = true }, on_exit)
|
||||
|
||||
-- Run synchronously
|
||||
local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
|
||||
-- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
|
||||
<
|
||||
|
||||
See |uv.spawn()| for more details.
|
||||
|
||||
Parameters: ~
|
||||
• {cmd} (string[]) Command to execute
|
||||
• {opts} (SystemOpts|nil) Options:
|
||||
• cwd: (string) Set the current working directory for the
|
||||
sub-process.
|
||||
• env: table<string,string> Set environment variables for
|
||||
the new process. Inherits the current environment with
|
||||
`NVIM` set to |v:servername|.
|
||||
• clear_env: (boolean) `env` defines the job environment
|
||||
exactly, instead of merging current environment.
|
||||
• stdin: (string|string[]|boolean) If `true`, then a pipe
|
||||
to stdin is opened and can be written to via the
|
||||
`write()` method to SystemObj. If string or string[] then
|
||||
will be written to stdin and closed. Defaults to `false`.
|
||||
• stdout: (boolean|function) Handle output from stdout.
|
||||
When passed as a function must have the signature
|
||||
`fun(err: string, data: string)`. Defaults to `true`
|
||||
• stderr: (boolean|function) Handle output from stdout.
|
||||
When passed as a function must have the signature
|
||||
`fun(err: string, data: string)`. Defaults to `true`.
|
||||
• text: (boolean) Handle stdout and stderr as text.
|
||||
Replaces `\r\n` with `\n`.
|
||||
• timeout: (integer)
|
||||
• detach: (boolean) If true, spawn the child process in a
|
||||
detached state - this will make it a process group
|
||||
leader, and will effectively enable the child to keep
|
||||
running after the parent exits. Note that the child
|
||||
process will still keep the parent's event loop alive
|
||||
unless the parent process calls |uv.unref()| on the
|
||||
child's process handle.
|
||||
• {on_exit} (function|nil) Called when subprocess exits. When provided,
|
||||
the command runs asynchronously. Receives SystemCompleted
|
||||
object, see return of SystemObj:wait().
|
||||
|
||||
Return: ~
|
||||
SystemObj Object with the fields:
|
||||
• pid (integer) Process ID
|
||||
• wait (fun(timeout: integer|nil): SystemCompleted)
|
||||
• SystemCompleted is an object with the fields:
|
||||
• code: (integer)
|
||||
• signal: (integer)
|
||||
• stdout: (string), nil if stdout argument is passed
|
||||
• stderr: (string), nil if stderr argument is passed
|
||||
|
||||
• kill (fun(signal: integer))
|
||||
• write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to
|
||||
close the stream.
|
||||
• is_closing (fun(): boolean)
|
||||
|
||||
|
||||
==============================================================================
|
||||
Lua module: inspector *lua-inspector*
|
||||
|
@ -76,6 +76,8 @@ The following new APIs or features were added.
|
||||
is resized horizontally). Note: Lines that are not visible and kept in
|
||||
|'scrollback'| are not reflown.
|
||||
|
||||
• |vim.system()| for running system commands.
|
||||
|
||||
==============================================================================
|
||||
CHANGED FEATURES *news-changed*
|
||||
|
||||
|
@ -14,79 +14,19 @@ local function man_error(msg)
|
||||
end
|
||||
|
||||
-- Run a system command and timeout after 30 seconds.
|
||||
---@param cmd_ string[]
|
||||
---@param cmd string[]
|
||||
---@param silent boolean?
|
||||
---@param env string[]
|
||||
---@param env? table<string,string|number>
|
||||
---@return string
|
||||
local function system(cmd_, silent, env)
|
||||
local stdout_data = {} ---@type string[]
|
||||
local stderr_data = {} ---@type string[]
|
||||
local stdout = assert(vim.uv.new_pipe(false))
|
||||
local stderr = assert(vim.uv.new_pipe(false))
|
||||
local function system(cmd, silent, env)
|
||||
local r = vim.system(cmd, { env = env, timeout = 10000 }):wait()
|
||||
|
||||
local done = false
|
||||
local exit_code ---@type integer?
|
||||
|
||||
-- We use the `env` command here rather than the env option to vim.uv.spawn since spawn will
|
||||
-- completely overwrite the environment when we just want to modify the existing one.
|
||||
--
|
||||
-- Overwriting mainly causes problems NixOS which relies heavily on a non-standard environment.
|
||||
local cmd = cmd_
|
||||
if env then
|
||||
cmd = { 'env' }
|
||||
vim.list_extend(cmd, env)
|
||||
vim.list_extend(cmd, cmd_)
|
||||
end
|
||||
|
||||
local handle
|
||||
handle = vim.uv.spawn(cmd[1], {
|
||||
args = vim.list_slice(cmd, 2),
|
||||
stdio = { nil, stdout, stderr },
|
||||
}, function(code)
|
||||
exit_code = code
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
handle:close()
|
||||
done = true
|
||||
end)
|
||||
|
||||
if handle then
|
||||
stdout:read_start(function(_, data)
|
||||
stdout_data[#stdout_data + 1] = data
|
||||
end)
|
||||
stderr:read_start(function(_, data)
|
||||
stderr_data[#stderr_data + 1] = data
|
||||
end)
|
||||
else
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
if not silent then
|
||||
local cmd_str = table.concat(cmd, ' ')
|
||||
man_error(string.format('command error: %s', cmd_str))
|
||||
end
|
||||
return ''
|
||||
end
|
||||
|
||||
vim.wait(30000, function()
|
||||
return done
|
||||
end)
|
||||
|
||||
if not done then
|
||||
if handle then
|
||||
handle:close()
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
end
|
||||
if r.code ~= 0 and not silent then
|
||||
local cmd_str = table.concat(cmd, ' ')
|
||||
man_error(string.format('command timed out: %s', cmd_str))
|
||||
man_error(string.format("command error '%s': %s", cmd_str, r.stderr))
|
||||
end
|
||||
|
||||
if exit_code ~= 0 and not silent then
|
||||
local cmd_str = table.concat(cmd, ' ')
|
||||
man_error(string.format("command error '%s': %s", cmd_str, table.concat(stderr_data)))
|
||||
end
|
||||
|
||||
return table.concat(stdout_data)
|
||||
return assert(r.stdout)
|
||||
end
|
||||
|
||||
---@param line string
|
||||
@ -312,7 +252,7 @@ local function get_path(sect, name, silent)
|
||||
end
|
||||
|
||||
local lines = system(cmd, silent)
|
||||
local results = vim.split(lines or {}, '\n', { trimempty = true })
|
||||
local results = vim.split(lines, '\n', { trimempty = true })
|
||||
|
||||
if #results == 0 then
|
||||
return
|
||||
@ -505,9 +445,9 @@ local function get_page(path, silent)
|
||||
-- http://comments.gmane.org/gmane.editors.vim.devel/29085
|
||||
-- Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces.
|
||||
return system(cmd, silent, {
|
||||
'MANPAGER=cat',
|
||||
'MANWIDTH=' .. manwidth,
|
||||
'MAN_KEEP_FORMATTING=1',
|
||||
MANPAGER = 'cat',
|
||||
MANWIDTH = manwidth,
|
||||
MAN_KEEP_FORMATTING = 1,
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -42,10 +42,6 @@ for k, v in pairs({
|
||||
vim._submodules[k] = v
|
||||
end
|
||||
|
||||
-- Remove at Nvim 1.0
|
||||
---@deprecated
|
||||
vim.loop = vim.uv
|
||||
|
||||
-- There are things which have special rules in vim._init_packages
|
||||
-- for legacy reasons (uri) or for performance (_inspector).
|
||||
-- most new things should go into a submodule namespace ( vim.foobar.do_thing() )
|
||||
@ -69,13 +65,73 @@ vim.log = {
|
||||
},
|
||||
}
|
||||
|
||||
-- Internal-only until comments in #8107 are addressed.
|
||||
-- Returns:
|
||||
-- {errcode}, {output}
|
||||
function vim._system(cmd)
|
||||
local out = vim.fn.system(cmd)
|
||||
local err = vim.v.shell_error
|
||||
return err, out
|
||||
-- TODO(lewis6991): document that the signature is system({cmd}, [{opts},] {on_exit})
|
||||
--- Run a system command
|
||||
---
|
||||
--- Examples:
|
||||
--- <pre>lua
|
||||
---
|
||||
--- local on_exit = function(obj)
|
||||
--- print(obj.code)
|
||||
--- print(obj.signal)
|
||||
--- print(obj.stdout)
|
||||
--- print(obj.stderr)
|
||||
--- end
|
||||
---
|
||||
--- -- Run asynchronously
|
||||
--- vim.system({'echo', 'hello'}, { text = true }, on_exit)
|
||||
---
|
||||
--- -- Run synchronously
|
||||
--- local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
|
||||
--- -- { code = 0, signal = 0, stdout = 'hello', stderr = '' }
|
||||
---
|
||||
--- </pre>
|
||||
---
|
||||
--- See |uv.spawn()| for more details.
|
||||
---
|
||||
--- @param cmd (string[]) Command to execute
|
||||
--- @param opts (SystemOpts|nil) Options:
|
||||
--- - cwd: (string) Set the current working directory for the sub-process.
|
||||
--- - env: table<string,string> Set environment variables for the new process. Inherits the
|
||||
--- current environment with `NVIM` set to |v:servername|.
|
||||
--- - clear_env: (boolean) `env` defines the job environment exactly, instead of merging current
|
||||
--- environment.
|
||||
--- - stdin: (string|string[]|boolean) If `true`, then a pipe to stdin is opened and can be written
|
||||
--- to via the `write()` method to SystemObj. If string or string[] then will be written to stdin
|
||||
--- and closed. Defaults to `false`.
|
||||
--- - stdout: (boolean|function)
|
||||
--- Handle output from stdout. When passed as a function must have the signature `fun(err: string, data: string)`.
|
||||
--- Defaults to `true`
|
||||
--- - stderr: (boolean|function)
|
||||
--- Handle output from stdout. When passed as a function must have the signature `fun(err: string, data: string)`.
|
||||
--- Defaults to `true`.
|
||||
--- - text: (boolean) Handle stdout and stderr as text. Replaces `\r\n` with `\n`.
|
||||
--- - timeout: (integer)
|
||||
--- - detach: (boolean) If true, spawn the child process in a detached state - this will make it
|
||||
--- a process group leader, and will effectively enable the child to keep running after the
|
||||
--- parent exits. Note that the child process will still keep the parent's event loop alive
|
||||
--- unless the parent process calls |uv.unref()| on the child's process handle.
|
||||
---
|
||||
--- @param on_exit (function|nil) Called when subprocess exits. When provided, the command runs
|
||||
--- asynchronously. Receives SystemCompleted object, see return of SystemObj:wait().
|
||||
---
|
||||
--- @return SystemObj Object with the fields:
|
||||
--- - pid (integer) Process ID
|
||||
--- - wait (fun(timeout: integer|nil): SystemCompleted)
|
||||
--- - SystemCompleted is an object with the fields:
|
||||
--- - code: (integer)
|
||||
--- - signal: (integer)
|
||||
--- - stdout: (string), nil if stdout argument is passed
|
||||
--- - stderr: (string), nil if stderr argument is passed
|
||||
--- - kill (fun(signal: integer))
|
||||
--- - write (fun(data: string|nil)) Requires `stdin=true`. Pass `nil` to close the stream.
|
||||
--- - is_closing (fun(): boolean)
|
||||
function vim.system(cmd, opts, on_exit)
|
||||
if type(opts) == 'function' then
|
||||
on_exit = opts
|
||||
opts = nil
|
||||
end
|
||||
return require('vim._system').run(cmd, opts, on_exit)
|
||||
end
|
||||
|
||||
-- Gets process info from the `ps` command.
|
||||
@ -85,13 +141,14 @@ function vim._os_proc_info(pid)
|
||||
error('invalid pid')
|
||||
end
|
||||
local cmd = { 'ps', '-p', pid, '-o', 'comm=' }
|
||||
local err, name = vim._system(cmd)
|
||||
if 1 == err and vim.trim(name) == '' then
|
||||
local r = vim.system(cmd):wait()
|
||||
local name = assert(r.stdout)
|
||||
if r.code == 1 and vim.trim(name) == '' then
|
||||
return {} -- Process not found.
|
||||
elseif 0 ~= err then
|
||||
elseif r.code ~= 0 then
|
||||
error('command failed: ' .. vim.fn.string(cmd))
|
||||
end
|
||||
local _, ppid = vim._system({ 'ps', '-p', pid, '-o', 'ppid=' })
|
||||
local ppid = assert(vim.system({ 'ps', '-p', pid, '-o', 'ppid=' }):wait().stdout)
|
||||
-- Remove trailing whitespace.
|
||||
name = vim.trim(name):gsub('^.*/', '')
|
||||
ppid = tonumber(ppid) or -1
|
||||
@ -109,14 +166,14 @@ function vim._os_proc_children(ppid)
|
||||
error('invalid ppid')
|
||||
end
|
||||
local cmd = { 'pgrep', '-P', ppid }
|
||||
local err, rv = vim._system(cmd)
|
||||
if 1 == err and vim.trim(rv) == '' then
|
||||
local r = vim.system(cmd):wait()
|
||||
if r.code == 1 and vim.trim(r.stdout) == '' then
|
||||
return {} -- Process not found.
|
||||
elseif 0 ~= err then
|
||||
elseif r.code ~= 0 then
|
||||
error('command failed: ' .. vim.fn.string(cmd))
|
||||
end
|
||||
local children = {}
|
||||
for s in rv:gmatch('%S+') do
|
||||
for s in r.stdout:gmatch('%S+') do
|
||||
local i = tonumber(s)
|
||||
if i ~= nil then
|
||||
table.insert(children, i)
|
||||
@ -1006,4 +1063,8 @@ end
|
||||
|
||||
require('vim._meta')
|
||||
|
||||
-- Remove at Nvim 1.0
|
||||
---@deprecated
|
||||
vim.loop = vim.uv
|
||||
|
||||
return vim
|
||||
|
342
runtime/lua/vim/_system.lua
Normal file
342
runtime/lua/vim/_system.lua
Normal file
@ -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<string,string|number>
|
||||
--- @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<string,string>
|
||||
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<string,string|number>
|
||||
--- @param clear_env? boolean
|
||||
--- @return string[]?
|
||||
local function setup_env(env, clear_env)
|
||||
if clear_env then
|
||||
return env
|
||||
end
|
||||
|
||||
--- @type table<string,string|number>
|
||||
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
|
@ -14,32 +14,6 @@ local function is_dir(filename)
|
||||
return stat and stat.type == 'directory' or false
|
||||
end
|
||||
|
||||
---@private
|
||||
--- Merges current process env with the given env and returns the result as
|
||||
--- a list of "k=v" strings.
|
||||
---
|
||||
--- <pre>
|
||||
--- Example:
|
||||
---
|
||||
--- in: { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", }
|
||||
--- out: { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", }
|
||||
--- </pre>
|
||||
---@param env (table) table of environment variable assignments
|
||||
---@returns (table) list of `"k=v"` strings
|
||||
local function env_merge(env)
|
||||
if env == nil then
|
||||
return env
|
||||
end
|
||||
-- Merge.
|
||||
env = vim.tbl_extend('force', vim.fn.environ(), env)
|
||||
local final_env = {}
|
||||
for k, v in pairs(env) do
|
||||
assert(type(k) == 'string', 'env must be a dict')
|
||||
table.insert(final_env, k .. '=' .. tostring(v))
|
||||
end
|
||||
return final_env
|
||||
end
|
||||
|
||||
---@private
|
||||
--- Embeds the given string into a table and correctly computes `Content-Length`.
|
||||
---
|
||||
@ -658,89 +632,85 @@ end
|
||||
--- - `is_closing()` returns a boolean indicating if the RPC is closing.
|
||||
--- - `terminate()` terminates the RPC client.
|
||||
local function start(cmd, cmd_args, dispatchers, extra_spawn_params)
|
||||
local _ = log.info()
|
||||
and log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
|
||||
if log.info() then
|
||||
log.info('Starting RPC client', { cmd = cmd, args = cmd_args, extra = extra_spawn_params })
|
||||
end
|
||||
|
||||
validate({
|
||||
cmd = { cmd, 's' },
|
||||
cmd_args = { cmd_args, 't' },
|
||||
dispatchers = { dispatchers, 't', true },
|
||||
})
|
||||
|
||||
if extra_spawn_params and extra_spawn_params.cwd then
|
||||
extra_spawn_params = extra_spawn_params or {}
|
||||
|
||||
if extra_spawn_params.cwd then
|
||||
assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
|
||||
end
|
||||
|
||||
dispatchers = merge_dispatchers(dispatchers)
|
||||
local stdin = uv.new_pipe(false)
|
||||
local stdout = uv.new_pipe(false)
|
||||
local stderr = uv.new_pipe(false)
|
||||
local handle, pid
|
||||
|
||||
local sysobj ---@type SystemObj
|
||||
|
||||
local client = new_client(dispatchers, {
|
||||
write = function(msg)
|
||||
stdin:write(msg)
|
||||
sysobj:write(msg)
|
||||
end,
|
||||
is_closing = function()
|
||||
return handle == nil or handle:is_closing()
|
||||
return sysobj == nil or sysobj:is_closing()
|
||||
end,
|
||||
terminate = function()
|
||||
if handle then
|
||||
handle:kill(15)
|
||||
end
|
||||
sysobj:kill(15)
|
||||
end,
|
||||
})
|
||||
|
||||
---@private
|
||||
--- Callback for |vim.uv.spawn()| Closes all streams and runs the `on_exit` dispatcher.
|
||||
---@param code (integer) Exit code
|
||||
---@param signal (integer) Signal that was used to terminate (if any)
|
||||
local function onexit(code, signal)
|
||||
stdin:close()
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
handle:close()
|
||||
dispatchers.on_exit(code, signal)
|
||||
local handle_body = function(body)
|
||||
client:handle_body(body)
|
||||
end
|
||||
local spawn_params = {
|
||||
args = cmd_args,
|
||||
stdio = { stdin, stdout, stderr },
|
||||
detached = not is_win,
|
||||
}
|
||||
if extra_spawn_params then
|
||||
spawn_params.cwd = extra_spawn_params.cwd
|
||||
spawn_params.env = env_merge(extra_spawn_params.env)
|
||||
if extra_spawn_params.detached ~= nil then
|
||||
spawn_params.detached = extra_spawn_params.detached
|
||||
|
||||
local stdout_handler = create_read_loop(handle_body, nil, function(err)
|
||||
client:on_error(client_errors.READ_ERROR, err)
|
||||
end)
|
||||
|
||||
local stderr_handler = function(_, chunk)
|
||||
if chunk and log.error() then
|
||||
log.error('rpc', cmd, 'stderr', chunk)
|
||||
end
|
||||
end
|
||||
handle, pid = uv.spawn(cmd, spawn_params, onexit)
|
||||
if handle == nil then
|
||||
stdin:close()
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
|
||||
local detached = not is_win
|
||||
if extra_spawn_params.detached ~= nil then
|
||||
detached = extra_spawn_params.detached
|
||||
end
|
||||
|
||||
local cmd1 = { cmd }
|
||||
vim.list_extend(cmd1, cmd_args)
|
||||
|
||||
local ok, sysobj_or_err = pcall(vim.system, cmd1, {
|
||||
stdin = true,
|
||||
stdout = stdout_handler,
|
||||
stderr = stderr_handler,
|
||||
cwd = extra_spawn_params.cwd,
|
||||
env = extra_spawn_params.env,
|
||||
detach = detached,
|
||||
}, function(obj)
|
||||
dispatchers.on_exit(obj.code, obj.signal)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
local err = sysobj_or_err --[[@as string]]
|
||||
local msg = string.format('Spawning language server with cmd: `%s` failed', cmd)
|
||||
if string.match(pid, 'ENOENT') then
|
||||
if string.match(err, 'ENOENT') then
|
||||
msg = msg
|
||||
.. '. The language server is either not installed, missing from PATH, or not executable.'
|
||||
else
|
||||
msg = msg .. string.format(' with error message: %s', pid)
|
||||
msg = msg .. string.format(' with error message: %s', err)
|
||||
end
|
||||
vim.notify(msg, vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
stderr:read_start(function(_, chunk)
|
||||
if chunk then
|
||||
local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk)
|
||||
end
|
||||
end)
|
||||
|
||||
local handle_body = function(body)
|
||||
client:handle_body(body)
|
||||
end
|
||||
stdout:read_start(create_read_loop(handle_body, nil, function(err)
|
||||
client:on_error(client_errors.READ_ERROR, err)
|
||||
end))
|
||||
sysobj = sysobj_or_err --[[@as SystemObj]]
|
||||
|
||||
return public_client(client)
|
||||
end
|
||||
|
@ -340,6 +340,7 @@ function TLua2DoX_filter.filter(this, AppStamp, Filename)
|
||||
|
||||
if vim.startswith(line, '---@cast')
|
||||
or vim.startswith(line, '---@diagnostic')
|
||||
or vim.startswith(line, '---@overload')
|
||||
or vim.startswith(line, '---@type') then
|
||||
-- Ignore LSP directives
|
||||
outStream:writeln('// gg:"' .. line .. '"')
|
||||
|
57
test/functional/lua/system_spec.lua
Normal file
57
test/functional/lua/system_spec.lua
Normal file
@ -0,0 +1,57 @@
|
||||
local helpers = require('test.functional.helpers')(after_each)
|
||||
local clear = helpers.clear
|
||||
local exec_lua = helpers.exec_lua
|
||||
local eq = helpers.eq
|
||||
|
||||
local function system_sync(cmd, opts)
|
||||
return exec_lua([[
|
||||
return vim.system(...):wait()
|
||||
]], cmd, opts)
|
||||
end
|
||||
|
||||
local function system_async(cmd, opts)
|
||||
exec_lua([[
|
||||
local cmd, opts = ...
|
||||
_G.done = false
|
||||
vim.system(cmd, opts, function(obj)
|
||||
_G.done = true
|
||||
_G.ret = obj
|
||||
end)
|
||||
]], cmd, opts)
|
||||
|
||||
while true do
|
||||
if exec_lua[[return _G.done]] then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return exec_lua[[return _G.ret]]
|
||||
end
|
||||
|
||||
describe('vim.system', function()
|
||||
before_each(function()
|
||||
clear()
|
||||
end)
|
||||
|
||||
for name, system in pairs{ sync = system_sync, async = system_async, } do
|
||||
describe('('..name..')', function()
|
||||
it('can run simple commands', function()
|
||||
eq('hello\n', system({'echo', 'hello' }, { text = true }).stdout)
|
||||
end)
|
||||
|
||||
it('handle input', function()
|
||||
eq('hellocat', system({ 'cat' }, { stdin = 'hellocat', text = true }).stdout)
|
||||
end)
|
||||
|
||||
it ('supports timeout', function()
|
||||
eq({
|
||||
code = 0,
|
||||
signal = 2,
|
||||
stdout = '',
|
||||
stderr = "Command timed out: 'sleep 10'"
|
||||
}, system({ 'sleep', '10' }, { timeout = 1 }))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
end)
|
Loading…
Reference in New Issue
Block a user