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