_system.lua 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. local uv = vim.uv
  2. --- @class vim.SystemOpts
  3. --- @field stdin? string|string[]|true
  4. --- @field stdout? fun(err:string?, data: string?)|false
  5. --- @field stderr? fun(err:string?, data: string?)|false
  6. --- @field cwd? string
  7. --- @field env? table<string,string|number>
  8. --- @field clear_env? boolean
  9. --- @field text? boolean
  10. --- @field timeout? integer Timeout in ms
  11. --- @field detach? boolean
  12. --- @class vim.SystemCompleted
  13. --- @field code integer
  14. --- @field signal integer
  15. --- @field stdout? string
  16. --- @field stderr? string
  17. --- @class vim.SystemState
  18. --- @field cmd string[]
  19. --- @field handle? uv.uv_process_t
  20. --- @field timer? uv.uv_timer_t
  21. --- @field pid? integer
  22. --- @field timeout? integer
  23. --- @field done? boolean|'timeout'
  24. --- @field stdin? uv.uv_stream_t
  25. --- @field stdout? uv.uv_stream_t
  26. --- @field stderr? uv.uv_stream_t
  27. --- @field stdout_data? string[]
  28. --- @field stderr_data? string[]
  29. --- @field result? vim.SystemCompleted
  30. --- @enum vim.SystemSig
  31. local SIG = {
  32. HUP = 1, -- Hangup
  33. INT = 2, -- Interrupt from keyboard
  34. KILL = 9, -- Kill signal
  35. TERM = 15, -- Termination signal
  36. -- STOP = 17,19,23 -- Stop the process
  37. }
  38. ---@param handle uv.uv_handle_t?
  39. local function close_handle(handle)
  40. if handle and not handle:is_closing() then
  41. handle:close()
  42. end
  43. end
  44. ---@param state vim.SystemState
  45. local function close_handles(state)
  46. close_handle(state.handle)
  47. close_handle(state.stdin)
  48. close_handle(state.stdout)
  49. close_handle(state.stderr)
  50. close_handle(state.timer)
  51. end
  52. --- @class vim.SystemObj
  53. --- @field cmd string[]
  54. --- @field pid integer
  55. --- @field private _state vim.SystemState
  56. --- @field wait fun(self: vim.SystemObj, timeout?: integer): vim.SystemCompleted
  57. --- @field kill fun(self: vim.SystemObj, signal: integer|string)
  58. --- @field write fun(self: vim.SystemObj, data?: string|string[])
  59. --- @field is_closing fun(self: vim.SystemObj): boolean
  60. local SystemObj = {}
  61. --- @param state vim.SystemState
  62. --- @return vim.SystemObj
  63. local function new_systemobj(state)
  64. return setmetatable({
  65. cmd = state.cmd,
  66. pid = state.pid,
  67. _state = state,
  68. }, { __index = SystemObj })
  69. end
  70. --- @param signal integer|string
  71. function SystemObj:kill(signal)
  72. self._state.handle:kill(signal)
  73. end
  74. --- @package
  75. --- @param signal? vim.SystemSig
  76. function SystemObj:_timeout(signal)
  77. self._state.done = 'timeout'
  78. self:kill(signal or SIG.TERM)
  79. end
  80. local MAX_TIMEOUT = 2 ^ 31
  81. --- @param timeout? integer
  82. --- @return vim.SystemCompleted
  83. function SystemObj:wait(timeout)
  84. local state = self._state
  85. local done = vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
  86. return state.result ~= nil
  87. end, nil, true)
  88. if not done then
  89. -- Send sigkill since this cannot be caught
  90. self:_timeout(SIG.KILL)
  91. vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
  92. return state.result ~= nil
  93. end, nil, true)
  94. end
  95. return state.result
  96. end
  97. --- @param data string[]|string|nil
  98. function SystemObj:write(data)
  99. local stdin = self._state.stdin
  100. if not stdin then
  101. error('stdin has not been opened on this object')
  102. end
  103. if type(data) == 'table' then
  104. for _, v in ipairs(data) do
  105. stdin:write(v)
  106. stdin:write('\n')
  107. end
  108. elseif type(data) == 'string' then
  109. stdin:write(data)
  110. elseif data == nil then
  111. -- Shutdown the write side of the duplex stream and then close the pipe.
  112. -- Note shutdown will wait for all the pending write requests to complete
  113. -- TODO(lewis6991): apparently shutdown doesn't behave this way.
  114. -- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616)
  115. stdin:write('', function()
  116. stdin:shutdown(function()
  117. if stdin then
  118. stdin:close()
  119. end
  120. end)
  121. end)
  122. end
  123. end
  124. --- @return boolean
  125. function SystemObj:is_closing()
  126. local handle = self._state.handle
  127. return handle == nil or handle:is_closing() or false
  128. end
  129. ---@param output fun(err:string?, data: string?)|false
  130. ---@return uv.uv_stream_t?
  131. ---@return fun(err:string?, data: string?)? Handler
  132. local function setup_output(output)
  133. if output == nil then
  134. return assert(uv.new_pipe(false)), nil
  135. end
  136. if type(output) == 'function' then
  137. return assert(uv.new_pipe(false)), output
  138. end
  139. assert(output == false)
  140. return nil, nil
  141. end
  142. ---@param input string|string[]|true|nil
  143. ---@return uv.uv_stream_t?
  144. ---@return string|string[]?
  145. local function setup_input(input)
  146. if not input then
  147. return
  148. end
  149. local towrite --- @type string|string[]?
  150. if type(input) == 'string' or type(input) == 'table' then
  151. towrite = input
  152. end
  153. return assert(uv.new_pipe(false)), towrite
  154. end
  155. --- @return table<string,string>
  156. local function base_env()
  157. local env = vim.fn.environ() --- @type table<string,string>
  158. env['NVIM'] = vim.v.servername
  159. env['NVIM_LISTEN_ADDRESS'] = nil
  160. return env
  161. end
  162. --- uv.spawn will completely overwrite the environment
  163. --- when we just want to modify the existing one, so
  164. --- make sure to prepopulate it with the current env.
  165. --- @param env? table<string,string|number>
  166. --- @param clear_env? boolean
  167. --- @return string[]?
  168. local function setup_env(env, clear_env)
  169. if clear_env then
  170. return env
  171. end
  172. --- @type table<string,string|number>
  173. env = vim.tbl_extend('force', base_env(), env or {})
  174. local renv = {} --- @type string[]
  175. for k, v in pairs(env) do
  176. renv[#renv + 1] = string.format('%s=%s', k, tostring(v))
  177. end
  178. return renv
  179. end
  180. --- @param stream uv.uv_stream_t
  181. --- @param text? boolean
  182. --- @param bucket string[]
  183. --- @return fun(err: string?, data: string?)
  184. local function default_handler(stream, text, bucket)
  185. return function(err, data)
  186. if err then
  187. error(err)
  188. end
  189. if data ~= nil then
  190. if text then
  191. bucket[#bucket + 1] = data:gsub('\r\n', '\n')
  192. else
  193. bucket[#bucket + 1] = data
  194. end
  195. else
  196. stream:read_stop()
  197. stream:close()
  198. end
  199. end
  200. end
  201. local is_win = vim.fn.has('win32') == 1
  202. local M = {}
  203. --- @param cmd string
  204. --- @param opts uv.spawn.options
  205. --- @param on_exit fun(code: integer, signal: integer)
  206. --- @param on_error fun()
  207. --- @return uv.uv_process_t, integer
  208. local function spawn(cmd, opts, on_exit, on_error)
  209. if is_win then
  210. local cmd1 = vim.fn.exepath(cmd)
  211. if cmd1 ~= '' then
  212. cmd = cmd1
  213. end
  214. end
  215. local handle, pid_or_err = uv.spawn(cmd, opts, on_exit)
  216. if not handle then
  217. on_error()
  218. error(pid_or_err)
  219. end
  220. return handle, pid_or_err --[[@as integer]]
  221. end
  222. ---@param timeout integer
  223. ---@param cb fun()
  224. ---@return uv.uv_timer_t
  225. local function timer_oneshot(timeout, cb)
  226. local timer = assert(uv.new_timer())
  227. timer:start(timeout, 0, function()
  228. timer:stop()
  229. timer:close()
  230. cb()
  231. end)
  232. return timer
  233. end
  234. --- @param state vim.SystemState
  235. --- @param code integer
  236. --- @param signal integer
  237. --- @param on_exit fun(result: vim.SystemCompleted)?
  238. local function _on_exit(state, code, signal, on_exit)
  239. close_handles(state)
  240. local check = assert(uv.new_check())
  241. check:start(function()
  242. for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do
  243. if not pipe:is_closing() then
  244. return
  245. end
  246. end
  247. check:stop()
  248. check:close()
  249. if state.done == nil then
  250. state.done = true
  251. end
  252. if (code == 0 or code == 1) and state.done == 'timeout' then
  253. -- Unix: code == 0
  254. -- Windows: code == 1
  255. code = 124
  256. end
  257. local stdout_data = state.stdout_data
  258. local stderr_data = state.stderr_data
  259. state.result = {
  260. code = code,
  261. signal = signal,
  262. stdout = stdout_data and table.concat(stdout_data) or nil,
  263. stderr = stderr_data and table.concat(stderr_data) or nil,
  264. }
  265. if on_exit then
  266. on_exit(state.result)
  267. end
  268. end)
  269. end
  270. --- Run a system command
  271. ---
  272. --- @param cmd string[]
  273. --- @param opts? vim.SystemOpts
  274. --- @param on_exit? fun(out: vim.SystemCompleted)
  275. --- @return vim.SystemObj
  276. function M.run(cmd, opts, on_exit)
  277. vim.validate('cmd', cmd, 'table')
  278. vim.validate('opts', opts, 'table', true)
  279. vim.validate('on_exit', on_exit, 'function', true)
  280. opts = opts or {}
  281. local stdout, stdout_handler = setup_output(opts.stdout)
  282. local stderr, stderr_handler = setup_output(opts.stderr)
  283. local stdin, towrite = setup_input(opts.stdin)
  284. --- @type vim.SystemState
  285. local state = {
  286. done = false,
  287. cmd = cmd,
  288. timeout = opts.timeout,
  289. stdin = stdin,
  290. stdout = stdout,
  291. stderr = stderr,
  292. }
  293. --- @diagnostic disable-next-line:missing-fields
  294. state.handle, state.pid = spawn(cmd[1], {
  295. args = vim.list_slice(cmd, 2),
  296. stdio = { stdin, stdout, stderr },
  297. cwd = opts.cwd,
  298. --- @diagnostic disable-next-line:assign-type-mismatch
  299. env = setup_env(opts.env, opts.clear_env),
  300. detached = opts.detach,
  301. hide = true,
  302. }, function(code, signal)
  303. _on_exit(state, code, signal, on_exit)
  304. end, function()
  305. close_handles(state)
  306. end)
  307. if stdout then
  308. state.stdout_data = {}
  309. stdout:read_start(stdout_handler or default_handler(stdout, opts.text, state.stdout_data))
  310. end
  311. if stderr then
  312. state.stderr_data = {}
  313. stderr:read_start(stderr_handler or default_handler(stderr, opts.text, state.stderr_data))
  314. end
  315. local obj = new_systemobj(state)
  316. if towrite then
  317. obj:write(towrite)
  318. obj:write(nil) -- close the stream
  319. end
  320. if opts.timeout then
  321. state.timer = timer_oneshot(opts.timeout, function()
  322. if state.handle and state.handle:is_active() then
  323. obj:_timeout()
  324. end
  325. end)
  326. end
  327. return obj
  328. end
  329. return M