mirror of
https://github.com/folke/lazy.nvim.git
synced 2025-04-19 04:46:45 +00:00
refactor: async processes
This commit is contained in:
parent
4319846b8c
commit
a36ebd2a75
12 changed files with 394 additions and 379 deletions
|
@ -1,67 +1,133 @@
|
|||
local Async = require("lazy.async")
|
||||
local Config = require("lazy.core.config")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<uv_process_t, true>
|
||||
M.running = {}
|
||||
|
||||
M.signals = {
|
||||
"HUP",
|
||||
"INT",
|
||||
"QUIT",
|
||||
"ILL",
|
||||
"TRAP",
|
||||
"ABRT",
|
||||
"BUS",
|
||||
"FPE",
|
||||
"KILL",
|
||||
"USR1",
|
||||
"SEGV",
|
||||
"USR2",
|
||||
"PIPE",
|
||||
"ALRM",
|
||||
"TERM",
|
||||
"CHLD",
|
||||
"CONT",
|
||||
"STOP",
|
||||
"TSTP",
|
||||
"TTIN",
|
||||
"TTOU",
|
||||
"URG",
|
||||
"XCPU",
|
||||
"XFSZ",
|
||||
"VTALRM",
|
||||
"PROF",
|
||||
"WINCH",
|
||||
"IO",
|
||||
"PWR",
|
||||
"EMT",
|
||||
"SYS",
|
||||
"INFO",
|
||||
}
|
||||
|
||||
---@diagnostic disable-next-line: no-unknown
|
||||
local uv = vim.uv
|
||||
|
||||
---@class ProcessOpts
|
||||
---@field args string[]
|
||||
---@field cwd? string
|
||||
---@field on_line? fun(string)
|
||||
---@field on_line? fun(line:string)
|
||||
---@field on_exit? fun(ok:boolean, output:string)
|
||||
---@field on_data? fun(string)
|
||||
---@field on_data? fun(data:string, is_stderr?:boolean)
|
||||
---@field timeout? number
|
||||
---@field env? table<string,string>
|
||||
|
||||
---@param opts? ProcessOpts
|
||||
---@param cmd string
|
||||
function M.spawn(cmd, opts)
|
||||
opts = opts or {}
|
||||
opts.timeout = opts.timeout or (Config.options.git and Config.options.git.timeout * 1000)
|
||||
local M = {}
|
||||
|
||||
---@type table<uv_process_t, LazyProcess>
|
||||
M.running = setmetatable({}, { __mode = "k" })
|
||||
|
||||
---@class LazyProcess: Async
|
||||
---@field handle? uv_process_t
|
||||
---@field pid? number
|
||||
---@field cmd string
|
||||
---@field opts ProcessOpts
|
||||
---@field timeout? uv_timer_t
|
||||
---@field timedout? boolean
|
||||
---@field data string
|
||||
---@field check? uv_check_t
|
||||
---@field code? number
|
||||
---@field signal? number
|
||||
local Process = setmetatable({}, { __index = Async.Async })
|
||||
|
||||
---@param cmd string|string[]
|
||||
---@param opts? ProcessOpts
|
||||
function Process.new(cmd, opts)
|
||||
local self = setmetatable({}, { __index = Process })
|
||||
---@async
|
||||
Process.init(self, function()
|
||||
self:_run()
|
||||
end)
|
||||
opts = opts or {}
|
||||
opts.args = opts.args or {}
|
||||
if type(cmd) == "table" then
|
||||
self.cmd = table.remove(cmd, 1)
|
||||
vim.list_extend(opts.args, cmd)
|
||||
else
|
||||
self.cmd = cmd
|
||||
end
|
||||
opts.timeout = opts.timeout or (Config.options.git and Config.options.git.timeout * 1000)
|
||||
-- make sure the cwd is valid
|
||||
if not opts.cwd and type(uv.cwd()) ~= "string" then
|
||||
opts.cwd = uv.os_homedir()
|
||||
end
|
||||
opts.on_line = opts.on_line and vim.schedule_wrap(opts.on_line) or nil
|
||||
opts.on_data = opts.on_data and vim.schedule_wrap(opts.on_data) or nil
|
||||
self.data = ""
|
||||
self.opts = opts
|
||||
self.code = 1
|
||||
self.signal = 0
|
||||
return self
|
||||
end
|
||||
|
||||
---@async
|
||||
function Process:_run()
|
||||
self:guard()
|
||||
local stdout = assert(uv.new_pipe())
|
||||
local stderr = assert(uv.new_pipe())
|
||||
self.handle = uv.spawn(self.cmd, {
|
||||
stdio = { nil, stdout, stderr },
|
||||
args = self.opts.args,
|
||||
cwd = self.opts.cwd,
|
||||
env = self:env(),
|
||||
}, function(code, signal)
|
||||
self.code = code
|
||||
self.signal = signal
|
||||
if self.timeout then
|
||||
self.timeout:stop()
|
||||
end
|
||||
self.handle:close()
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
self:resume()
|
||||
end)
|
||||
|
||||
if self.handle then
|
||||
M.running[self.handle] = self
|
||||
stdout:read_start(function(err, data)
|
||||
self:on_data(err, data)
|
||||
end)
|
||||
stderr:read_start(function(err, data)
|
||||
self:on_data(err, data, true)
|
||||
end)
|
||||
self:suspend()
|
||||
while not (self.handle:is_closing() and stdout:is_closing() and stderr:is_closing()) do
|
||||
coroutine.yield()
|
||||
end
|
||||
else
|
||||
self.data = "Failed to spawn process " .. self.cmd .. " " .. vim.inspect(self.opts)
|
||||
end
|
||||
self:on_exit()
|
||||
end
|
||||
|
||||
function Process:on_exit()
|
||||
self.data = self.data:gsub("[^\r\n]+\r", "")
|
||||
if self.timedout then
|
||||
self.data = self.data .. "\n" .. "Process was killed because it reached the timeout"
|
||||
elseif self.signal ~= 0 then
|
||||
self.data = self.data .. "\n" .. "Process was killed with SIG" .. M.signals[self.signal]:upper()
|
||||
end
|
||||
if self.opts.on_exit then
|
||||
self.opts.on_exit(self.code == 0 and self.signal == 0, self.data)
|
||||
end
|
||||
end
|
||||
|
||||
function Process:guard()
|
||||
if self.opts.timeout then
|
||||
self.timeout = assert(uv.new_timer())
|
||||
self.timeout:start(self.opts.timeout, 0, function()
|
||||
self.timedout = true
|
||||
self:kill()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function Process:env()
|
||||
---@type table<string, string>
|
||||
local env = vim.tbl_extend("force", {
|
||||
GIT_SSH_COMMAND = "ssh -oBatchMode=yes",
|
||||
}, uv.os_environ(), opts.env or {})
|
||||
}, uv.os_environ(), self.opts.env or {})
|
||||
env.GIT_DIR = nil
|
||||
env.GIT_WORK_TREE = nil
|
||||
env.GIT_TERMINAL_PROMPT = "0"
|
||||
|
@ -72,147 +138,105 @@ function M.spawn(cmd, opts)
|
|||
for k, v in pairs(env) do
|
||||
env_flat[#env_flat + 1] = k .. "=" .. v
|
||||
end
|
||||
|
||||
local stdout = assert(uv.new_pipe())
|
||||
local stderr = assert(uv.new_pipe())
|
||||
|
||||
local output = ""
|
||||
---@type uv_process_t?
|
||||
local handle = nil
|
||||
|
||||
---@type uv_timer_t
|
||||
local timeout
|
||||
local killed = false
|
||||
if opts.timeout then
|
||||
timeout = assert(uv.new_timer())
|
||||
timeout:start(opts.timeout, 0, function()
|
||||
if M.kill(handle) then
|
||||
killed = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- make sure the cwd is valid
|
||||
if not opts.cwd and type(uv.cwd()) ~= "string" then
|
||||
opts.cwd = uv.os_homedir()
|
||||
end
|
||||
|
||||
handle = uv.spawn(cmd, {
|
||||
stdio = { nil, stdout, stderr },
|
||||
args = opts.args,
|
||||
cwd = opts.cwd,
|
||||
env = env_flat,
|
||||
}, function(exit_code, signal)
|
||||
---@cast handle uv_process_t
|
||||
M.running[handle] = nil
|
||||
if timeout then
|
||||
timeout:stop()
|
||||
timeout:close()
|
||||
end
|
||||
handle:close()
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
local check = assert(uv.new_check())
|
||||
check:start(function()
|
||||
if not stdout:is_closing() or not stderr:is_closing() then
|
||||
return
|
||||
end
|
||||
check:stop()
|
||||
if opts.on_exit then
|
||||
output = output:gsub("[^\r\n]+\r", "")
|
||||
if killed then
|
||||
output = output .. "\n" .. "Process was killed because it reached the timeout"
|
||||
elseif signal ~= 0 then
|
||||
output = output .. "\n" .. "Process was killed with SIG" .. M.signals[signal]
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
opts.on_exit(exit_code == 0 and signal == 0, output)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
if not handle then
|
||||
if opts.on_exit then
|
||||
opts.on_exit(false, "Failed to spawn process " .. cmd .. " " .. vim.inspect(opts))
|
||||
end
|
||||
return
|
||||
end
|
||||
M.running[handle] = true
|
||||
|
||||
---@param data? string
|
||||
local function on_output(err, data)
|
||||
assert(not err, err)
|
||||
|
||||
if data then
|
||||
if opts.on_data then
|
||||
vim.schedule(function()
|
||||
opts.on_data(data)
|
||||
end)
|
||||
end
|
||||
output = output .. data:gsub("\r\n", "\n")
|
||||
local lines = vim.split(vim.trim(output:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n")
|
||||
|
||||
if opts.on_line then
|
||||
vim.schedule(function()
|
||||
opts.on_line(lines[#lines])
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
uv.read_start(stdout, on_output)
|
||||
uv.read_start(stderr, on_output)
|
||||
|
||||
return handle
|
||||
return env_flat
|
||||
end
|
||||
|
||||
function M.kill(handle)
|
||||
if handle and not handle:is_closing() then
|
||||
M.running[handle] = nil
|
||||
uv.process_kill(handle, "sigint")
|
||||
return true
|
||||
---@param signals uv.aliases.signals|uv.aliases.signals[]|nil
|
||||
function Process:kill(signals)
|
||||
if not self.handle or self.handle:is_closing() then
|
||||
return
|
||||
end
|
||||
signals = signals or { "sigterm", "sigkill" }
|
||||
signals = type(signals) == "table" and signals or { signals }
|
||||
---@cast signals uv.aliases.signals[]
|
||||
local timer = assert(uv.new_timer())
|
||||
timer:start(0, 1000, function()
|
||||
if self.handle and not self.handle:is_closing() and #signals > 0 then
|
||||
self.handle:kill(table.remove(signals, 1))
|
||||
else
|
||||
timer:stop()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param err? string
|
||||
---@param data? string
|
||||
---@param is_stderr? boolean
|
||||
function Process:on_data(err, data, is_stderr)
|
||||
assert(not err, err)
|
||||
if not data then
|
||||
return
|
||||
end
|
||||
|
||||
if self.opts.on_data then
|
||||
self.opts.on_data(data, is_stderr)
|
||||
end
|
||||
self.data = self.data .. data:gsub("\r\n", "\n")
|
||||
local lines = vim.split(vim.trim(self.data:gsub("\r$", "")):gsub("[^\n\r]+\r", ""), "\n")
|
||||
|
||||
if self.opts.on_line then
|
||||
self.opts.on_line(lines[#lines])
|
||||
end
|
||||
end
|
||||
|
||||
M.signals = {
|
||||
"hup",
|
||||
"int",
|
||||
"quit",
|
||||
"ill",
|
||||
"trap",
|
||||
"abrt",
|
||||
"bus",
|
||||
"fpe",
|
||||
"kill",
|
||||
"usr1",
|
||||
"segv",
|
||||
"usr2",
|
||||
"pipe",
|
||||
"alrm",
|
||||
"term",
|
||||
"chld",
|
||||
"cont",
|
||||
"stop",
|
||||
"tstp",
|
||||
"ttin",
|
||||
"ttou",
|
||||
"urg",
|
||||
"xcpu",
|
||||
"xfsz",
|
||||
"vtalrm",
|
||||
"prof",
|
||||
"winch",
|
||||
"io",
|
||||
"pwr",
|
||||
"emt",
|
||||
"sys",
|
||||
"info",
|
||||
}
|
||||
|
||||
---@param cmd string|string[]
|
||||
---@param opts? ProcessOpts
|
||||
function M.spawn(cmd, opts)
|
||||
return Process.new(cmd, opts)
|
||||
end
|
||||
|
||||
function M.abort()
|
||||
for handle in pairs(M.running) do
|
||||
M.kill(handle)
|
||||
for _, proc in pairs(M.running) do
|
||||
proc:kill()
|
||||
end
|
||||
end
|
||||
|
||||
---@param cmd string[]
|
||||
---@param opts? {cwd:string, env:table}
|
||||
---@async
|
||||
---@param cmd string|string[]
|
||||
---@param opts? ProcessOpts
|
||||
function M.exec(cmd, opts)
|
||||
opts = opts or {}
|
||||
---@type string[]
|
||||
local lines
|
||||
local job = vim.fn.jobstart(cmd, {
|
||||
cwd = opts.cwd,
|
||||
pty = false,
|
||||
env = opts.env,
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, _lines)
|
||||
lines = _lines
|
||||
end,
|
||||
})
|
||||
|
||||
if job <= 0 then
|
||||
error("Failed to start job: " .. vim.inspect(cmd))
|
||||
local proc = M.spawn(cmd, opts)
|
||||
proc:wait()
|
||||
if proc.code ~= 0 then
|
||||
error("Process failed with code " .. proc.code)
|
||||
end
|
||||
|
||||
local Async = require("lazy.async")
|
||||
local async = Async.current
|
||||
if async then
|
||||
while vim.fn.jobwait({ job }, 0)[1] == -1 do
|
||||
async:sleep(10)
|
||||
end
|
||||
else
|
||||
vim.fn.jobwait({ job })
|
||||
end
|
||||
|
||||
return lines
|
||||
return vim.split(proc.data, "\n")
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue