123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- local uv = vim.uv
- --- @class vim.SystemOpts
- --- @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 vim.SystemCompleted
- --- @field code integer
- --- @field signal integer
- --- @field stdout? string
- --- @field stderr? string
- --- @class vim.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|'timeout'
- --- @field stdin? uv.uv_stream_t
- --- @field stdout? uv.uv_stream_t
- --- @field stderr? uv.uv_stream_t
- --- @field stdout_data? string[]
- --- @field stderr_data? string[]
- --- @field result? vim.SystemCompleted
- --- @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
- --- @class vim.SystemObj
- --- @field cmd string[]
- --- @field pid integer
- --- @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 vim.SystemState
- --- @return vim.SystemObj
- local function new_systemobj(state)
- return setmetatable({
- cmd = state.cmd,
- pid = state.pid,
- _state = state,
- }, { __index = SystemObj })
- end
- --- @param signal integer|string
- 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
- --- @return vim.SystemCompleted
- function SystemObj:wait(timeout)
- local state = self._state
- local done = vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
- return state.result ~= nil
- end, nil, true)
- 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, nil, true)
- 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()
- close_handle(stdin)
- end)
- end)
- end
- end
- --- @return boolean
- function SystemObj:is_closing()
- local handle = self._state.handle
- return handle == nil or handle:is_closing() or false
- end
- --- @param output? uv.read_start.callback|false
- --- @param text? boolean
- --- @return uv.uv_stream_t? pipe
- --- @return uv.read_start.callback? handler
- --- @return string[]? data
- local function setup_output(output, text)
- if output == false then
- return
- end
- local bucket --- @type string[]?
- local handler --- @type uv.read_start.callback
- if type(output) == 'function' then
- handler = output
- else
- bucket = {}
- handler = function(err, data)
- if err then
- error(err)
- end
- if text and data then
- bucket[#bucket + 1] = data:gsub('\r\n', '\n')
- else
- bucket[#bucket + 1] = data
- end
- end
- end
- local pipe = assert(uv.new_pipe(false))
- --- @type uv.read_start.callback
- local function handler_with_close(err, data)
- handler(err, data)
- if data == nil then
- pipe:read_stop()
- pipe:close()
- end
- end
- return pipe, handler_with_close, bucket
- end
- --- @param input? string|string[]|boolean
- --- @return uv.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() --- @type table<string,string>
- 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
- local is_win = vim.fn.has('win32') == 1
- local M = {}
- --- @param cmd string
- --- @param opts uv.spawn.options
- --- @param on_exit fun(code: integer, signal: integer)
- --- @param on_error fun()
- --- @return uv.uv_process_t, integer
- local function spawn(cmd, opts, on_exit, on_error)
- if is_win then
- local cmd1 = vim.fn.exepath(cmd)
- if cmd1 ~= '' then
- cmd = cmd1
- end
- end
- 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
- --- @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
- --- @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_handle(state.handle)
- close_handle(state.stdin)
- close_handle(state.timer)
- -- #30846: Do not close stdout/stderr here, as they may still have data to
- -- read. They will be closed in uv.read_start on EOF.
- 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 or code == 1) and state.done == 'timeout' then
- -- Unix: code == 0
- -- Windows: code == 1
- 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
- --- @param state vim.SystemState
- local function _on_error(state)
- close_handle(state.handle)
- close_handle(state.stdin)
- close_handle(state.stdout)
- close_handle(state.stderr)
- close_handle(state.timer)
- end
- --- Run a system command
- ---
- --- @param cmd string[]
- --- @param opts? vim.SystemOpts
- --- @param on_exit? fun(out: vim.SystemCompleted)
- --- @return vim.SystemObj
- function M.run(cmd, opts, on_exit)
- vim.validate('cmd', cmd, 'table')
- vim.validate('opts', opts, 'table', true)
- vim.validate('on_exit', on_exit, 'function', true)
- opts = opts or {}
- local stdout, stdout_handler, stdout_data = setup_output(opts.stdout, opts.text)
- local stderr, stderr_handler, stderr_data = setup_output(opts.stderr, opts.text)
- local stdin, towrite = setup_input(opts.stdin)
- --- @type vim.SystemState
- local state = {
- done = false,
- cmd = cmd,
- timeout = opts.timeout,
- stdin = stdin,
- stdout = stdout,
- stdout_data = stdout_data,
- stderr = stderr,
- stderr_data = stderr_data,
- }
- --- @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,
- }, function(code, signal)
- _on_exit(state, code, signal, on_exit)
- end, function()
- _on_error(state)
- end)
- if stdout and stdout_handler then
- stdout:read_start(stdout_handler)
- end
- if stderr and stderr_handler then
- stderr:read_start(stderr_handler)
- 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 = timer_oneshot(opts.timeout, function()
- if state.handle and state.handle:is_active() then
- obj:_timeout()
- end
- end)
- end
- return obj
- end
- return M
|