neovim

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

system.lua (14328B)


      1 local uv = vim.uv
      2 
      3 --- @class vim.SystemOpts
      4 --- @inlinedoc
      5 ---
      6 --- Set the current working directory for the sub-process.
      7 --- @field cwd? string
      8 ---
      9 --- Set environment variables for the new process. Inherits the current environment with `NVIM` set
     10 --- to |v:servername|.
     11 --- @field env? table<string,string|number>
     12 ---
     13 --- `env` defines the job environment exactly, instead of merging current environment. Note: if
     14 --- `env` is `nil`, the current environment is used but without `NVIM` set.
     15 --- @field clear_env? boolean
     16 ---
     17 --- If `true`, then a pipe to stdin is opened and can be written to via the `write()` method to
     18 --- SystemObj. If `string` or `string[]` then will be written to stdin and closed.
     19 --- @field stdin? string|string[]|true
     20 ---
     21 --- Handle output from stdout.
     22 --- (Default: `true`)
     23 --- @field stdout? fun(err:string?, data: string?)|boolean
     24 ---
     25 --- Handle output from stderr.
     26 --- (Default: `true`)
     27 --- @field stderr? fun(err:string?, data: string?)|boolean
     28 ---
     29 --- Handle stdout and stderr as text. Normalizes line endings by replacing `\r\n` with `\n`.
     30 --- @field text? boolean
     31 ---
     32 --- Run the command with a time limit in ms. Upon timeout the process is sent the TERM signal (15)
     33 --- and the exit code is set to 124.
     34 --- @field timeout? integer
     35 ---
     36 --- Spawn the child process in a detached state - this will make it a process group leader, and will
     37 --- effectively enable the child to keep running after the parent exits. Note that the child process
     38 --- will still keep the parent's event loop alive unless the parent process calls [uv.unref()] on
     39 --- the child's process handle.
     40 --- @field detach? boolean
     41 
     42 --- @class vim.SystemCompleted
     43 --- @field code integer
     44 --- @field signal integer
     45 --- @field stdout? string `nil` if stdout is disabled or has a custom handler.
     46 --- @field stderr? string `nil` if stderr is disabled or has a custom handler.
     47 
     48 --- @class (package) vim.SystemState
     49 --- @field cmd string[]
     50 --- @field handle? uv.uv_process_t
     51 --- @field timer?  uv.uv_timer_t
     52 --- @field pid? integer
     53 --- @field timeout? integer
     54 --- @field done? boolean|'timeout'
     55 --- @field stdin? uv.uv_stream_t
     56 --- @field stdout? uv.uv_stream_t
     57 --- @field stderr? uv.uv_stream_t
     58 --- @field stdout_data? string[]
     59 --- @field stderr_data? string[]
     60 --- @field result? vim.SystemCompleted
     61 
     62 --- @enum vim.SystemSig
     63 local SIG = {
     64  HUP = 1, -- Hangup
     65  INT = 2, -- Interrupt from keyboard
     66  KILL = 9, -- Kill signal
     67  TERM = 15, -- Termination signal
     68  -- STOP = 17,19,23  -- Stop the process
     69 }
     70 
     71 ---@param handle uv.uv_handle_t?
     72 local function close_handle(handle)
     73  if handle and not handle:is_closing() then
     74    handle:close()
     75  end
     76 end
     77 
     78 --- @class vim.SystemObj
     79 --- @field cmd string[] Command name and args
     80 --- @field pid integer Process ID
     81 --- @field private _state vim.SystemState
     82 local SystemObj = {}
     83 
     84 --- @param state vim.SystemState
     85 --- @return vim.SystemObj
     86 local function new_systemobj(state)
     87  return setmetatable({
     88    cmd = state.cmd,
     89    pid = state.pid,
     90    _state = state,
     91  }, { __index = SystemObj })
     92 end
     93 
     94 --- Sends a signal to the process.
     95 ---
     96 --- The signal can be specified as an integer or as a string.
     97 ---
     98 --- Example:
     99 --- ```lua
    100 --- local obj = vim.system({'sleep', '10'})
    101 --- obj:kill('sigterm') -- sends SIGTERM to the process
    102 --- ```
    103 ---
    104 --- @param signal integer|string Signal to send to the process. See |luv-constants|.
    105 function SystemObj:kill(signal)
    106  self._state.handle:kill(signal)
    107 end
    108 
    109 --- @package
    110 --- @param signal? vim.SystemSig
    111 function SystemObj:_timeout(signal)
    112  self._state.done = 'timeout'
    113  self:kill(signal or SIG.TERM)
    114 end
    115 
    116 --- Waits for the process to complete or until the specified timeout elapses.
    117 ---
    118 --- This method blocks execution until the associated process has exited or
    119 --- the optional `timeout` (in milliseconds) has been reached. If the process
    120 --- does not exit before the timeout, it is forcefully terminated with SIGKILL
    121 --- (signal 9), and the exit code is set to 124.
    122 ---
    123 --- If no `timeout` is provided, the method will wait indefinitely (or use the
    124 --- timeout specified in the options when the process was started).
    125 ---
    126 --- Example:
    127 --- ```lua
    128 --- local obj = vim.system({'echo', 'hello'}, { text = true })
    129 --- local result = obj:wait(1000) -- waits up to 1000ms
    130 --- print(result.code, result.signal, result.stdout, result.stderr)
    131 --- ```
    132 ---
    133 --- @param timeout? integer
    134 --- @return vim.SystemCompleted
    135 function SystemObj:wait(timeout)
    136  local state = self._state
    137 
    138  local done = vim.wait(timeout or state.timeout or vim._maxint, function()
    139    return state.result ~= nil
    140  end, nil, true)
    141 
    142  if not done then
    143    -- Send sigkill since this cannot be caught
    144    self:_timeout(SIG.KILL)
    145    vim.wait(timeout or state.timeout or vim._maxint, function()
    146      return state.result ~= nil
    147    end, nil, true)
    148  end
    149 
    150  return state.result
    151 end
    152 
    153 --- Writes data to the stdin of the process or closes stdin.
    154 ---
    155 --- If `data` is a list of strings, each string is written followed by a
    156 --- newline.
    157 ---
    158 --- If `data` is a string, it is written as-is.
    159 ---
    160 --- If `data` is `nil`, the write side of the stream is shut down and the pipe
    161 --- is closed.
    162 ---
    163 --- Example:
    164 --- ```lua
    165 --- local obj = vim.system({'cat'}, { stdin = true })
    166 --- obj:write({'hello', 'world'}) -- writes 'hello\nworld\n' to stdin
    167 --- obj:write(nil) -- closes stdin
    168 --- ```
    169 ---
    170 --- @param data string[]|string|nil
    171 function SystemObj:write(data)
    172  local stdin = self._state.stdin
    173 
    174  if not stdin then
    175    error('stdin has not been opened on this object')
    176  end
    177 
    178  if type(data) == 'table' then
    179    for _, v in ipairs(data) do
    180      stdin:write(v)
    181      stdin:write('\n')
    182    end
    183  elseif type(data) == 'string' then
    184    stdin:write(data)
    185  elseif data == nil then
    186    -- Shutdown the write side of the duplex stream and then close the pipe.
    187    -- Note shutdown will wait for all the pending write requests to complete
    188    -- TODO(lewis6991): apparently shutdown doesn't behave this way.
    189    -- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616)
    190    stdin:write('', function()
    191      stdin:shutdown(function()
    192        close_handle(stdin)
    193      end)
    194    end)
    195  end
    196 end
    197 
    198 --- Checks if the process handle is closing or already closed.
    199 ---
    200 --- This method returns `true` if the underlying process handle is either
    201 --- `nil` or is in the process of closing. It is useful for determining
    202 --- whether it is safe to perform operations on the process handle.
    203 ---
    204 --- @return boolean
    205 function SystemObj:is_closing()
    206  local handle = self._state.handle
    207  return handle == nil or handle:is_closing() or false
    208 end
    209 
    210 --- @param output? fun(err: string?, data: string?)|false
    211 --- @param text? boolean
    212 --- @return uv.uv_stream_t? pipe
    213 --- @return fun(err: string?, data: string?)? handler
    214 --- @return string[]? data
    215 local function setup_output(output, text)
    216  if output == false then
    217    return
    218  end
    219 
    220  local bucket --- @type string[]?
    221  local handler --- @type fun(err: string?, data: string?)
    222 
    223  if type(output) == 'function' then
    224    handler = output
    225  else
    226    bucket = {}
    227    handler = function(err, data)
    228      if err then
    229        error(err)
    230      end
    231      if text and data then
    232        bucket[#bucket + 1] = data:gsub('\r\n', '\n')
    233      else
    234        bucket[#bucket + 1] = data
    235      end
    236    end
    237  end
    238 
    239  local pipe = assert(uv.new_pipe(false))
    240 
    241  --- @param err? string
    242  --- @param data? string
    243  local function handler_with_close(err, data)
    244    handler(err, data)
    245    if data == nil then
    246      pipe:read_stop()
    247      pipe:close()
    248    end
    249  end
    250 
    251  return pipe, handler_with_close, bucket
    252 end
    253 
    254 --- @param input? string|string[]|boolean
    255 --- @return uv.uv_stream_t?
    256 --- @return string|string[]?
    257 local function setup_input(input)
    258  if not input then
    259    return
    260  end
    261 
    262  local towrite --- @type string|string[]?
    263  if type(input) == 'string' or type(input) == 'table' then
    264    towrite = input
    265  end
    266 
    267  return assert(uv.new_pipe(false)), towrite
    268 end
    269 
    270 --- @return table<string,string>
    271 local function base_env()
    272  local env = vim.fn.environ() --- @type table<string,string>
    273  env['NVIM'] = vim.v.servername
    274  env['NVIM_LISTEN_ADDRESS'] = nil
    275  return env
    276 end
    277 
    278 --- uv.spawn will completely overwrite the environment
    279 --- when we just want to modify the existing one, so
    280 --- make sure to prepopulate it with the current env.
    281 --- @param env? table<string,string|number>
    282 --- @param clear_env? boolean
    283 --- @return string[]?
    284 local function setup_env(env, clear_env)
    285  if not env and clear_env then
    286    return
    287  end
    288 
    289  env = env or {}
    290  if not clear_env then
    291    --- @type table<string,string|number>
    292    env = vim.tbl_extend('force', base_env(), env)
    293  end
    294 
    295  local renv = {} --- @type string[]
    296  for k, v in pairs(env) do
    297    renv[#renv + 1] = string.format('%s=%s', k, tostring(v))
    298  end
    299 
    300  return renv
    301 end
    302 
    303 local is_win = vim.fn.has('win32') == 1
    304 
    305 --- @param cmd string
    306 --- @param opts uv.spawn.options
    307 --- @param on_exit fun(code: integer, signal: integer)
    308 --- @param on_error fun()
    309 --- @return uv.uv_process_t?, integer?
    310 local function spawn(cmd, opts, on_exit, on_error)
    311  if is_win then
    312    local cmd1 = vim.fn.exepath(cmd)
    313    if cmd1 ~= '' then
    314      cmd = cmd1
    315    end
    316  end
    317 
    318  local handle, pid_or_err = uv.spawn(cmd, opts, on_exit)
    319  if not handle then
    320    on_error()
    321    if opts.cwd and not uv.fs_stat(opts.cwd) then
    322      error(("%s (cwd): '%s'"):format(pid_or_err, opts.cwd))
    323    elseif vim.fn.executable(cmd) == 0 then
    324      error(("%s (cmd): '%s'"):format(pid_or_err, cmd))
    325    else
    326      error(pid_or_err)
    327    end
    328  end
    329  return handle, pid_or_err --[[@as integer]]
    330 end
    331 
    332 --- @param timeout integer
    333 --- @param cb fun()
    334 --- @return uv.uv_timer_t
    335 local function timer_oneshot(timeout, cb)
    336  local timer = assert(uv.new_timer())
    337  timer:start(timeout, 0, function()
    338    timer:stop()
    339    timer:close()
    340    cb()
    341  end)
    342  return timer
    343 end
    344 
    345 --- @param state vim.SystemState
    346 --- @param code integer
    347 --- @param signal integer
    348 --- @param on_exit fun(result: vim.SystemCompleted)?
    349 local function _on_exit(state, code, signal, on_exit)
    350  close_handle(state.handle)
    351  close_handle(state.stdin)
    352  close_handle(state.timer)
    353 
    354  -- #30846: Do not close stdout/stderr here, as they may still have data to
    355  -- read. They will be closed in uv.read_start on EOF.
    356 
    357  local check = uv.new_check()
    358  check:start(function()
    359    for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do
    360      if not pipe:is_closing() then
    361        return
    362      end
    363    end
    364    check:stop()
    365    check:close()
    366 
    367    if state.done == nil then
    368      state.done = true
    369    end
    370 
    371    if (code == 0 or code == 1) and state.done == 'timeout' then
    372      -- Unix: code == 0
    373      -- Windows: code == 1
    374      code = 124
    375    end
    376 
    377    local stdout_data = state.stdout_data
    378    local stderr_data = state.stderr_data
    379 
    380    state.result = {
    381      code = code,
    382      signal = signal,
    383      stdout = stdout_data and table.concat(stdout_data) or nil,
    384      stderr = stderr_data and table.concat(stderr_data) or nil,
    385    }
    386 
    387    if on_exit then
    388      on_exit(state.result)
    389    end
    390  end)
    391 end
    392 
    393 --- @param state vim.SystemState
    394 local function _on_error(state)
    395  close_handle(state.handle)
    396  close_handle(state.stdin)
    397  close_handle(state.stdout)
    398  close_handle(state.stderr)
    399  close_handle(state.timer)
    400 end
    401 
    402 --- Run a system command
    403 ---
    404 --- @param cmd string[]
    405 --- @param opts? vim.SystemOpts
    406 --- @param on_exit? fun(out: vim.SystemCompleted)
    407 --- @return vim.SystemObj
    408 local function run(cmd, opts, on_exit)
    409  vim.validate('cmd', cmd, 'table')
    410  vim.validate('opts', opts, 'table', true)
    411  vim.validate('on_exit', on_exit, 'function', true)
    412 
    413  opts = opts or {}
    414 
    415  local stdout, stdout_handler, stdout_data = setup_output(opts.stdout, opts.text)
    416  local stderr, stderr_handler, stderr_data = setup_output(opts.stderr, opts.text)
    417  local stdin, towrite = setup_input(opts.stdin)
    418 
    419  --- @type vim.SystemState
    420  local state = {
    421    done = false,
    422    cmd = cmd,
    423    timeout = opts.timeout,
    424    stdin = stdin,
    425    stdout = stdout,
    426    stdout_data = stdout_data,
    427    stderr = stderr,
    428    stderr_data = stderr_data,
    429  }
    430 
    431  --- @diagnostic disable-next-line:missing-fields
    432  state.handle, state.pid = spawn(cmd[1], {
    433    args = vim.list_slice(cmd, 2),
    434    stdio = { stdin, stdout, stderr },
    435    cwd = opts.cwd,
    436    --- @diagnostic disable-next-line:assign-type-mismatch
    437    env = setup_env(opts.env, opts.clear_env),
    438    detached = opts.detach,
    439    hide = true,
    440  }, function(code, signal)
    441    _on_exit(state, code, signal, on_exit)
    442  end, function()
    443    _on_error(state)
    444  end)
    445 
    446  if stdout and stdout_handler then
    447    stdout:read_start(stdout_handler)
    448  end
    449 
    450  if stderr and stderr_handler then
    451    stderr:read_start(stderr_handler)
    452  end
    453 
    454  local obj = new_systemobj(state)
    455 
    456  if towrite then
    457    obj:write(towrite)
    458    obj:write(nil) -- close the stream
    459  end
    460 
    461  if opts.timeout then
    462    state.timer = timer_oneshot(opts.timeout, function()
    463      if state.handle and state.handle:is_active() then
    464        obj:_timeout()
    465      end
    466    end)
    467  end
    468 
    469  return obj
    470 end
    471 
    472 --- Runs a system command or throws an error if {cmd} cannot be run.
    473 ---
    474 --- The command runs directly (not in 'shell') so shell builtins such as "echo" in cmd.exe, cmdlets
    475 --- in powershell, or "help" in bash, will not work unless you actually invoke a shell:
    476 --- `vim.system({'bash', '-c', 'help'})`.
    477 ---
    478 --- Examples:
    479 ---
    480 --- ```lua
    481 --- local on_exit = function(obj)
    482 ---   print(obj.code)
    483 ---   print(obj.signal)
    484 ---   print(obj.stdout)
    485 ---   print(obj.stderr)
    486 --- end
    487 ---
    488 --- -- Runs asynchronously:
    489 --- vim.system({'echo', 'hello'}, { text = true }, on_exit)
    490 ---
    491 --- -- Runs synchronously:
    492 --- local obj = vim.system({'echo', 'hello'}, { text = true }):wait()
    493 --- -- { code = 0, signal = 0, stdout = 'hello\n', stderr = '' }
    494 ---
    495 --- ```
    496 ---
    497 --- See |uv.spawn()| for more details. Note: unlike |uv.spawn()|, vim.system
    498 --- throws an error if {cmd} cannot be run.
    499 ---
    500 --- @param cmd string[] Command to execute
    501 --- @param opts vim.SystemOpts?
    502 --- @param on_exit? fun(out: vim.SystemCompleted) Called when subprocess exits. When provided, the command runs
    503 ---   asynchronously. See return of SystemObj:wait().
    504 ---
    505 --- @return vim.SystemObj
    506 --- @overload fun(cmd: string[], on_exit: fun(out: vim.SystemCompleted)): vim.SystemObj
    507 function vim.system(cmd, opts, on_exit)
    508  if type(opts) == 'function' then
    509    on_exit = opts
    510    opts = nil
    511  end
    512  return run(cmd, opts, on_exit)
    513 end