mirror of https://github.com/folke/lazy.nvim.git
refactor: async processes
This commit is contained in:
parent
4319846b8c
commit
a36ebd2a75
|
@ -1,78 +1,122 @@
|
||||||
---@class AsyncOpts
|
|
||||||
---@field on_done? fun()
|
|
||||||
---@field on_error? fun(err:string)
|
|
||||||
---@field on_yield? fun(res:any)
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
---@type Async[]
|
---@type Async[]
|
||||||
M._queue = {}
|
M._queue = {}
|
||||||
M._executor = assert(vim.loop.new_check())
|
M._executor = assert(vim.loop.new_check())
|
||||||
M._running = false
|
M._running = false
|
||||||
---@type Async
|
|
||||||
M.current = nil
|
---@type table<thread, Async>
|
||||||
|
M._threads = setmetatable({}, { __mode = "k" })
|
||||||
|
|
||||||
|
---@alias AsyncEvent "done" | "error" | "yield" | "ok"
|
||||||
|
|
||||||
---@class Async
|
---@class Async
|
||||||
---@field co thread
|
---@field _co thread
|
||||||
---@field opts AsyncOpts
|
---@field _fn fun()
|
||||||
---@field sleeping? boolean
|
---@field _suspended? boolean
|
||||||
|
---@field _on table<AsyncEvent, fun(res:any, async:Async)[]>
|
||||||
local Async = {}
|
local Async = {}
|
||||||
|
|
||||||
---@param fn async fun()
|
---@param fn async fun()
|
||||||
---@param opts? AsyncOpts
|
|
||||||
---@return Async
|
---@return Async
|
||||||
function Async.new(fn, opts)
|
function Async.new(fn)
|
||||||
local self = setmetatable({}, { __index = Async })
|
local self = setmetatable({}, { __index = Async })
|
||||||
self.co = coroutine.create(fn)
|
return self:init(fn)
|
||||||
self.opts = opts or {}
|
end
|
||||||
|
|
||||||
|
---@param fn async fun()
|
||||||
|
---@return Async
|
||||||
|
function Async:init(fn)
|
||||||
|
self._fn = fn
|
||||||
|
self._on = {}
|
||||||
|
self._co = coroutine.create(function()
|
||||||
|
local ok, err = pcall(self._fn)
|
||||||
|
if not ok then
|
||||||
|
self:_emit("error", err)
|
||||||
|
end
|
||||||
|
self:_emit("done")
|
||||||
|
end)
|
||||||
|
M._threads[self._co] = self
|
||||||
|
return M.add(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Async:restart()
|
||||||
|
assert(not self:running(), "Cannot restart a running async")
|
||||||
|
self:init(self._fn)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param event AsyncEvent
|
||||||
|
---@param cb async fun(res:any, async:Async)
|
||||||
|
function Async:on(event, cb)
|
||||||
|
self._on[event] = self._on[event] or {}
|
||||||
|
table.insert(self._on[event], cb)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
function Async:running()
|
---@private
|
||||||
return coroutine.status(self.co) ~= "dead"
|
---@param event AsyncEvent
|
||||||
|
---@param res any
|
||||||
|
function Async:_emit(event, res)
|
||||||
|
for _, cb in ipairs(self._on[event] or {}) do
|
||||||
|
cb(res, self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Async:running()
|
||||||
|
return coroutine.status(self._co) ~= "dead"
|
||||||
|
end
|
||||||
|
|
||||||
|
---@async
|
||||||
function Async:sleep(ms)
|
function Async:sleep(ms)
|
||||||
self.sleeping = true
|
self._suspended = true
|
||||||
vim.defer_fn(function()
|
vim.defer_fn(function()
|
||||||
self.sleeping = false
|
self._suspended = false
|
||||||
end, ms)
|
end, ms)
|
||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@async
|
||||||
function Async:suspend()
|
function Async:suspend()
|
||||||
self.sleeping = true
|
self._suspended = true
|
||||||
|
if coroutine.running() == self._co then
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Async:resume()
|
function Async:resume()
|
||||||
self.sleeping = false
|
self._suspended = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Async:wait()
|
||||||
|
local async = M.running()
|
||||||
|
if coroutine.running() == self._co then
|
||||||
|
error("Cannot wait on self")
|
||||||
|
end
|
||||||
|
|
||||||
|
while self:running() do
|
||||||
|
if async then
|
||||||
|
coroutine.yield()
|
||||||
|
else
|
||||||
|
vim.wait(10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
function Async:step()
|
function Async:step()
|
||||||
if self.sleeping then
|
if self._suspended then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
local status = coroutine.status(self.co)
|
local status = coroutine.status(self._co)
|
||||||
if status == "suspended" then
|
if status == "suspended" then
|
||||||
M.current = self
|
local ok, res = coroutine.resume(self._co)
|
||||||
local ok, res = coroutine.resume(self.co)
|
|
||||||
M.current = nil
|
|
||||||
if not ok then
|
if not ok then
|
||||||
if self.opts.on_error then
|
error(res)
|
||||||
self.opts.on_error(tostring(res))
|
|
||||||
end
|
|
||||||
elseif res then
|
elseif res then
|
||||||
if self.opts.on_yield then
|
self:_emit("yield", res)
|
||||||
self.opts.on_yield(res)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
return self:running()
|
||||||
if self:running() then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if self.opts.on_done then
|
|
||||||
self.opts.on_done()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.step()
|
function M.step()
|
||||||
|
@ -107,32 +151,24 @@ function M.add(async)
|
||||||
return async
|
return async
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param fn async fun()
|
function M.running()
|
||||||
---@param opts? AsyncOpts
|
local co = coroutine.running()
|
||||||
function M.run(fn, opts)
|
if co then
|
||||||
return M.add(Async.new(fn, opts))
|
local async = M._threads[co]
|
||||||
end
|
assert(async, "In coroutine without async context")
|
||||||
|
return async
|
||||||
---@generic T: async fun()
|
|
||||||
---@param fn T
|
|
||||||
---@param opts? AsyncOpts
|
|
||||||
---@return T
|
|
||||||
function M.wrap(fn, opts)
|
|
||||||
return function(...)
|
|
||||||
local args = { ... }
|
|
||||||
---@async
|
|
||||||
local wrapped = function()
|
|
||||||
return fn(unpack(args))
|
|
||||||
end
|
|
||||||
return M.run(wrapped, opts)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@async
|
---@async
|
||||||
---@param ms number
|
---@param ms number
|
||||||
function M.sleep(ms)
|
function M.sleep(ms)
|
||||||
assert(M.current, "Not in an async context")
|
local async = M.running()
|
||||||
M.current:sleep(ms)
|
assert(async, "Not in an async context")
|
||||||
|
async:sleep(ms)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M.Async = Async
|
||||||
|
M.new = Async.new
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
@ -236,7 +236,7 @@ function M.clear(plugins)
|
||||||
if plugin._.tasks then
|
if plugin._.tasks then
|
||||||
---@param task LazyTask
|
---@param task LazyTask
|
||||||
plugin._.tasks = vim.tbl_filter(function(task)
|
plugin._.tasks = vim.tbl_filter(function(task)
|
||||||
return task:is_running() or task:has_errors()
|
return task:running() or task:has_errors()
|
||||||
end, plugin._.tasks)
|
end, plugin._.tasks)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,67 +1,133 @@
|
||||||
|
local Async = require("lazy.async")
|
||||||
local Config = require("lazy.core.config")
|
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
|
---@diagnostic disable-next-line: no-unknown
|
||||||
local uv = vim.uv
|
local uv = vim.uv
|
||||||
|
|
||||||
---@class ProcessOpts
|
---@class ProcessOpts
|
||||||
---@field args string[]
|
---@field args string[]
|
||||||
---@field cwd? 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_exit? fun(ok:boolean, output:string)
|
||||||
---@field on_data? fun(string)
|
---@field on_data? fun(data:string, is_stderr?:boolean)
|
||||||
---@field timeout? number
|
---@field timeout? number
|
||||||
---@field env? table<string,string>
|
---@field env? table<string,string>
|
||||||
|
|
||||||
---@param opts? ProcessOpts
|
local M = {}
|
||||||
---@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)
|
|
||||||
|
|
||||||
|
---@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>
|
---@type table<string, string>
|
||||||
local env = vim.tbl_extend("force", {
|
local env = vim.tbl_extend("force", {
|
||||||
GIT_SSH_COMMAND = "ssh -oBatchMode=yes",
|
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_DIR = nil
|
||||||
env.GIT_WORK_TREE = nil
|
env.GIT_WORK_TREE = nil
|
||||||
env.GIT_TERMINAL_PROMPT = "0"
|
env.GIT_TERMINAL_PROMPT = "0"
|
||||||
|
@ -72,147 +138,105 @@ function M.spawn(cmd, opts)
|
||||||
for k, v in pairs(env) do
|
for k, v in pairs(env) do
|
||||||
env_flat[#env_flat + 1] = k .. "=" .. v
|
env_flat[#env_flat + 1] = k .. "=" .. v
|
||||||
end
|
end
|
||||||
|
return env_flat
|
||||||
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
|
end
|
||||||
|
|
||||||
-- make sure the cwd is valid
|
---@param signals uv.aliases.signals|uv.aliases.signals[]|nil
|
||||||
if not opts.cwd and type(uv.cwd()) ~= "string" then
|
function Process:kill(signals)
|
||||||
opts.cwd = uv.os_homedir()
|
if not self.handle or self.handle:is_closing() then
|
||||||
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
|
return
|
||||||
end
|
end
|
||||||
check:stop()
|
signals = signals or { "sigterm", "sigkill" }
|
||||||
if opts.on_exit then
|
signals = type(signals) == "table" and signals or { signals }
|
||||||
output = output:gsub("[^\r\n]+\r", "")
|
---@cast signals uv.aliases.signals[]
|
||||||
if killed then
|
local timer = assert(uv.new_timer())
|
||||||
output = output .. "\n" .. "Process was killed because it reached the timeout"
|
timer:start(0, 1000, function()
|
||||||
elseif signal ~= 0 then
|
if self.handle and not self.handle:is_closing() and #signals > 0 then
|
||||||
output = output .. "\n" .. "Process was killed with SIG" .. M.signals[signal]
|
self.handle:kill(table.remove(signals, 1))
|
||||||
end
|
else
|
||||||
|
timer:stop()
|
||||||
vim.schedule(function()
|
|
||||||
opts.on_exit(exit_code == 0 and signal == 0, output)
|
|
||||||
end)
|
|
||||||
end
|
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
|
end
|
||||||
return
|
|
||||||
end
|
|
||||||
M.running[handle] = true
|
|
||||||
|
|
||||||
|
---@param err? string
|
||||||
---@param data? string
|
---@param data? string
|
||||||
local function on_output(err, data)
|
---@param is_stderr? boolean
|
||||||
|
function Process:on_data(err, data, is_stderr)
|
||||||
assert(not err, err)
|
assert(not err, err)
|
||||||
|
if not data then
|
||||||
if data then
|
return
|
||||||
if opts.on_data then
|
|
||||||
vim.schedule(function()
|
|
||||||
opts.on_data(data)
|
|
||||||
end)
|
|
||||||
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
|
if self.opts.on_data then
|
||||||
vim.schedule(function()
|
self.opts.on_data(data, is_stderr)
|
||||||
opts.on_line(lines[#lines])
|
|
||||||
end)
|
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
uv.read_start(stdout, on_output)
|
M.signals = {
|
||||||
uv.read_start(stderr, on_output)
|
"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",
|
||||||
|
}
|
||||||
|
|
||||||
return handle
|
---@param cmd string|string[]
|
||||||
end
|
---@param opts? ProcessOpts
|
||||||
|
function M.spawn(cmd, opts)
|
||||||
function M.kill(handle)
|
return Process.new(cmd, opts)
|
||||||
if handle and not handle:is_closing() then
|
|
||||||
M.running[handle] = nil
|
|
||||||
uv.process_kill(handle, "sigint")
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.abort()
|
function M.abort()
|
||||||
for handle in pairs(M.running) do
|
for _, proc in pairs(M.running) do
|
||||||
M.kill(handle)
|
proc:kill()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param cmd string[]
|
---@async
|
||||||
---@param opts? {cwd:string, env:table}
|
---@param cmd string|string[]
|
||||||
|
---@param opts? ProcessOpts
|
||||||
function M.exec(cmd, opts)
|
function M.exec(cmd, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
---@type string[]
|
local proc = M.spawn(cmd, opts)
|
||||||
local lines
|
proc:wait()
|
||||||
local job = vim.fn.jobstart(cmd, {
|
if proc.code ~= 0 then
|
||||||
cwd = opts.cwd,
|
error("Process failed with code " .. proc.code)
|
||||||
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))
|
|
||||||
end
|
end
|
||||||
|
return vim.split(proc.data, "\n")
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
@ -16,7 +16,6 @@ local Task = require("lazy.manage.task")
|
||||||
---@class Runner
|
---@class Runner
|
||||||
---@field _plugins table<string,LazyPlugin>
|
---@field _plugins table<string,LazyPlugin>
|
||||||
---@field _pipeline PipelineStep[]
|
---@field _pipeline PipelineStep[]
|
||||||
---@field _on_done fun()[]
|
|
||||||
---@field _opts RunnerOpts
|
---@field _opts RunnerOpts
|
||||||
---@field _running? Async
|
---@field _running? Async
|
||||||
local Runner = {}
|
local Runner = {}
|
||||||
|
@ -38,7 +37,6 @@ function Runner.new(opts)
|
||||||
for _, plugin in ipairs(pp) do
|
for _, plugin in ipairs(pp) do
|
||||||
self._plugins[plugin.name] = plugin
|
self._plugins[plugin.name] = plugin
|
||||||
end
|
end
|
||||||
self._on_done = {}
|
|
||||||
|
|
||||||
---@param step string|(TaskOptions|{[1]:string})
|
---@param step string|(TaskOptions|{[1]:string})
|
||||||
self._pipeline = vim.tbl_map(function(step)
|
self._pipeline = vim.tbl_map(function(step)
|
||||||
|
@ -61,15 +59,9 @@ end
|
||||||
|
|
||||||
function Runner:start()
|
function Runner:start()
|
||||||
---@async
|
---@async
|
||||||
self._running = Async.run(function()
|
self._running = Async.new(function()
|
||||||
self:_start()
|
self:_start()
|
||||||
end, {
|
end)
|
||||||
on_done = function()
|
|
||||||
for _, cb in ipairs(self._on_done) do
|
|
||||||
cb()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@async
|
---@async
|
||||||
|
@ -97,7 +89,7 @@ function Runner:_start()
|
||||||
for _, name in ipairs(names) do
|
for _, name in ipairs(names) do
|
||||||
state[name] = state[name] or { step = 0 }
|
state[name] = state[name] or { step = 0 }
|
||||||
local s = state[name]
|
local s = state[name]
|
||||||
local is_running = s.task and s.task:is_running()
|
local is_running = s.task and s.task:running()
|
||||||
local step = self._pipeline[s.step]
|
local step = self._pipeline[s.step]
|
||||||
|
|
||||||
if is_running then
|
if is_running then
|
||||||
|
@ -185,14 +177,10 @@ function Runner:wait(cb)
|
||||||
end
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
if cb then
|
if cb then
|
||||||
table.insert(self._on_done, cb)
|
self._running:on("done", cb)
|
||||||
else
|
else
|
||||||
-- sync wait
|
self._running:wait()
|
||||||
while self:is_running() do
|
|
||||||
vim.wait(10)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,16 +15,15 @@ local colors = Config.options.headless.colors
|
||||||
---@field msg string
|
---@field msg string
|
||||||
---@field level? number
|
---@field level? number
|
||||||
|
|
||||||
---@class LazyTask
|
---@class LazyTask: Async
|
||||||
---@field plugin LazyPlugin
|
---@field plugin LazyPlugin
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field private _log LazyMsg[]
|
---@field private _log LazyMsg[]
|
||||||
---@field private _started? number
|
---@field private _started number
|
||||||
---@field private _ended? number
|
---@field private _ended? number
|
||||||
---@field private _opts TaskOptions
|
---@field private _opts TaskOptions
|
||||||
---@field private _running Async
|
|
||||||
---@field private _level number
|
---@field private _level number
|
||||||
local Task = {}
|
local Task = setmetatable({}, { __index = Async.Async })
|
||||||
|
|
||||||
---@class TaskOptions: {[string]:any}
|
---@class TaskOptions: {[string]:any}
|
||||||
---@field on_done? fun(task:LazyTask)
|
---@field on_done? fun(task:LazyTask)
|
||||||
|
@ -35,17 +34,21 @@ local Task = {}
|
||||||
---@param task LazyTaskFn
|
---@param task LazyTaskFn
|
||||||
function Task.new(plugin, name, task, opts)
|
function Task.new(plugin, name, task, opts)
|
||||||
local self = setmetatable({}, { __index = Task })
|
local self = setmetatable({}, { __index = Task })
|
||||||
|
---@async
|
||||||
|
Task.init(self, function()
|
||||||
|
self:_run(task)
|
||||||
|
end)
|
||||||
|
self:set_level()
|
||||||
self._opts = opts or {}
|
self._opts = opts or {}
|
||||||
self._log = {}
|
self._log = {}
|
||||||
self:set_level()
|
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self._started = vim.uv.hrtime()
|
||||||
---@param other LazyTask
|
---@param other LazyTask
|
||||||
plugin._.tasks = vim.tbl_filter(function(other)
|
plugin._.tasks = vim.tbl_filter(function(other)
|
||||||
return other.name ~= name or other:is_running()
|
return other.name ~= name or other:running()
|
||||||
end, plugin._.tasks or {})
|
end, plugin._.tasks or {})
|
||||||
table.insert(plugin._.tasks, self)
|
table.insert(plugin._.tasks, self)
|
||||||
self:_start(task)
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -75,10 +78,6 @@ function Task:status()
|
||||||
return msg ~= "" and msg or nil
|
return msg ~= "" and msg or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function Task:is_running()
|
|
||||||
return self._ended == nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function Task:has_errors()
|
function Task:has_errors()
|
||||||
return self._level >= vim.log.levels.ERROR
|
return self._level >= vim.log.levels.ERROR
|
||||||
end
|
end
|
||||||
|
@ -92,31 +91,24 @@ function Task:set_level(level)
|
||||||
self._level = level or vim.log.levels.TRACE
|
self._level = level or vim.log.levels.TRACE
|
||||||
end
|
end
|
||||||
|
|
||||||
---@private
|
---@async
|
||||||
---@param task LazyTaskFn
|
---@param task LazyTaskFn
|
||||||
function Task:_start(task)
|
function Task:_run(task)
|
||||||
assert(not self._started, "task already started")
|
|
||||||
assert(not self._ended, "task already done")
|
|
||||||
|
|
||||||
if Config.headless() and Config.options.headless.task then
|
if Config.headless() and Config.options.headless.task then
|
||||||
self:log("Running task " .. self.name, vim.log.levels.INFO)
|
self:log("Running task " .. self.name, vim.log.levels.INFO)
|
||||||
end
|
end
|
||||||
|
|
||||||
self._started = vim.uv.hrtime()
|
self
|
||||||
---@async
|
:on("done", function()
|
||||||
self._running = Async.run(function()
|
|
||||||
task(self, self._opts)
|
|
||||||
end, {
|
|
||||||
on_done = function()
|
|
||||||
self:_done()
|
self:_done()
|
||||||
end,
|
end)
|
||||||
on_error = function(err)
|
:on("error", function(err)
|
||||||
self:error(err)
|
self:error(err)
|
||||||
end,
|
end)
|
||||||
on_yield = function(res)
|
:on("yield", function(msg)
|
||||||
self:log(res)
|
self:log(msg)
|
||||||
end,
|
end)
|
||||||
})
|
task(self, self._opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param msg string|string[]
|
---@param msg string|string[]
|
||||||
|
@ -163,13 +155,6 @@ end
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
function Task:_done()
|
function Task:_done()
|
||||||
assert(self._started, "task not started")
|
|
||||||
assert(not self._ended, "task already done")
|
|
||||||
|
|
||||||
if self._running and self._running:running() then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if Config.headless() and Config.options.headless.task then
|
if Config.headless() and Config.options.headless.task then
|
||||||
local ms = math.floor(self:time() + 0.5)
|
local ms = math.floor(self:time() + 0.5)
|
||||||
self:log("Finished task " .. self.name .. " in " .. ms .. "ms", vim.log.levels.INFO)
|
self:log("Finished task " .. self.name .. " in " .. ms .. "ms", vim.log.levels.INFO)
|
||||||
|
@ -186,13 +171,7 @@ function Task:_done()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Task:time()
|
function Task:time()
|
||||||
if not self._started then
|
return ((self._ended or vim.uv.hrtime()) - self._started) / 1e6
|
||||||
return 0
|
|
||||||
end
|
|
||||||
if not self._ended then
|
|
||||||
return (vim.uv.hrtime() - self._started) / 1e6
|
|
||||||
end
|
|
||||||
return (self._ended - self._started) / 1e6
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@async
|
---@async
|
||||||
|
@ -201,7 +180,6 @@ end
|
||||||
function Task:spawn(cmd, opts)
|
function Task:spawn(cmd, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local on_line = opts.on_line
|
local on_line = opts.on_line
|
||||||
local on_exit = opts.on_exit
|
|
||||||
|
|
||||||
local headless = Config.headless() and Config.options.headless.process
|
local headless = Config.headless() and Config.options.headless.process
|
||||||
|
|
||||||
|
@ -214,35 +192,28 @@ function Task:spawn(cmd, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
self._running:suspend()
|
|
||||||
|
|
||||||
local running = true
|
|
||||||
local ret = { ok = true, output = "" }
|
|
||||||
---@param output string
|
|
||||||
function opts.on_exit(ok, output)
|
|
||||||
if not headless then
|
|
||||||
self:log(vim.trim(output), ok and vim.log.levels.DEBUG or vim.log.levels.ERROR)
|
|
||||||
end
|
|
||||||
ret = { ok = ok, output = output }
|
|
||||||
running = false
|
|
||||||
self._running:resume()
|
|
||||||
end
|
|
||||||
|
|
||||||
if headless then
|
if headless then
|
||||||
opts.on_data = function(data)
|
opts.on_data = function(data)
|
||||||
-- prefix with plugin name
|
-- prefix with plugin name
|
||||||
local prefix = self:prefix()
|
io.write(Terminal.prefix(data, self:prefix()))
|
||||||
io.write(Terminal.prefix(data, prefix))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Process.spawn(cmd, opts)
|
|
||||||
coroutine.yield()
|
local proc = Process.spawn(cmd, opts)
|
||||||
assert(not running, "process still running?")
|
proc:wait()
|
||||||
if on_exit then
|
|
||||||
pcall(on_exit, ret.ok, ret.output)
|
local ok = proc.code == 0 and proc.signal == 0
|
||||||
|
if not headless then
|
||||||
|
local msg = vim.trim(proc.data)
|
||||||
|
if #msg > 0 then
|
||||||
|
self:log(vim.trim(proc.data), ok and vim.log.levels.DEBUG or vim.log.levels.ERROR)
|
||||||
end
|
end
|
||||||
coroutine.yield()
|
end
|
||||||
return ret.ok
|
|
||||||
|
if opts.on_exit then
|
||||||
|
pcall(opts.on_exit, ok, proc.data)
|
||||||
|
end
|
||||||
|
return ok
|
||||||
end
|
end
|
||||||
|
|
||||||
function Task:prefix()
|
function Task:prefix()
|
||||||
|
@ -253,10 +224,4 @@ function Task:prefix()
|
||||||
or plugin .. " " .. task .. " | "
|
or plugin .. " " .. task .. " | "
|
||||||
end
|
end
|
||||||
|
|
||||||
function Task:wait()
|
|
||||||
while self:is_running() do
|
|
||||||
vim.wait(10)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Task
|
return Task
|
||||||
|
|
|
@ -76,6 +76,13 @@ function M.throttle(ms, fn)
|
||||||
local timer = vim.uv.new_timer()
|
local timer = vim.uv.new_timer()
|
||||||
local pending = false
|
local pending = false
|
||||||
|
|
||||||
|
---@type Async
|
||||||
|
local async
|
||||||
|
|
||||||
|
local function running()
|
||||||
|
return async and async:running()
|
||||||
|
end
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
if timer:is_active() then
|
if timer:is_active() then
|
||||||
pending = true
|
pending = true
|
||||||
|
@ -85,7 +92,10 @@ function M.throttle(ms, fn)
|
||||||
0,
|
0,
|
||||||
ms,
|
ms,
|
||||||
vim.schedule_wrap(function()
|
vim.schedule_wrap(function()
|
||||||
fn()
|
if running() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
async = require("lazy.async").new(fn)
|
||||||
if pending then
|
if pending then
|
||||||
pending = false
|
pending = false
|
||||||
else
|
else
|
||||||
|
|
|
@ -51,7 +51,7 @@ function M:update()
|
||||||
if plugin._.tasks then
|
if plugin._.tasks then
|
||||||
for _, task in ipairs(plugin._.tasks) do
|
for _, task in ipairs(plugin._.tasks) do
|
||||||
self.progress.total = self.progress.total + 1
|
self.progress.total = self.progress.total + 1
|
||||||
if not task:is_running() then
|
if not task:running() then
|
||||||
self.progress.done = self.progress.done + 1
|
self.progress.done = self.progress.done + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -356,7 +356,7 @@ end
|
||||||
function M:diagnostics(plugin)
|
function M:diagnostics(plugin)
|
||||||
local skip = false
|
local skip = false
|
||||||
for _, task in ipairs(plugin._.tasks or {}) do
|
for _, task in ipairs(plugin._.tasks or {}) do
|
||||||
if task:is_running() then
|
if task:running() then
|
||||||
self:diagnostic({
|
self:diagnostic({
|
||||||
severity = vim.diagnostic.severity.WARN,
|
severity = vim.diagnostic.severity.WARN,
|
||||||
message = task.name .. (task:status() and (": " .. task:status()) or ""),
|
message = task.name .. (task:status() and (": " .. task:status()) or ""),
|
||||||
|
|
|
@ -28,7 +28,7 @@ return {
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return has_task(plugin, function(task)
|
return has_task(plugin, function(task)
|
||||||
return task:is_running()
|
return task:running()
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
title = "Working",
|
title = "Working",
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
---@module 'luassert'
|
||||||
|
local Async = require("lazy.async")
|
||||||
|
local Process = require("lazy.manage.process")
|
||||||
|
|
||||||
|
describe("process", function()
|
||||||
|
it("runs sync", function()
|
||||||
|
local lines = Process.exec({ "echo", "-n", "hello" })
|
||||||
|
assert.are.same({ "hello" }, lines)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("runs sync from async context", function()
|
||||||
|
local lines ---@type string[]
|
||||||
|
local async = Async.new(function()
|
||||||
|
lines = Process.exec({ "echo", "-n", "hello" })
|
||||||
|
end)
|
||||||
|
async:wait()
|
||||||
|
|
||||||
|
assert.are.same({ "hello" }, lines)
|
||||||
|
end)
|
||||||
|
end)
|
|
@ -21,9 +21,9 @@ describe("task", function()
|
||||||
|
|
||||||
it("simple function", function()
|
it("simple function", function()
|
||||||
local task = Task.new(plugin, "test", function() end, opts)
|
local task = Task.new(plugin, "test", function() end, opts)
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
task:wait()
|
task:wait()
|
||||||
assert(not task:is_running())
|
assert(not task:running())
|
||||||
assert(task_result.done)
|
assert(task_result.done)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -31,9 +31,9 @@ describe("task", function()
|
||||||
local task = Task.new(plugin, "test", function()
|
local task = Task.new(plugin, "test", function()
|
||||||
error("test")
|
error("test")
|
||||||
end, opts)
|
end, opts)
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
task:wait()
|
task:wait()
|
||||||
assert(not task:is_running())
|
assert(not task:running())
|
||||||
assert(task_result.done)
|
assert(task_result.done)
|
||||||
assert(task_result.error)
|
assert(task_result.error)
|
||||||
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("test"))
|
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("test"))
|
||||||
|
@ -46,12 +46,12 @@ describe("task", function()
|
||||||
coroutine.yield()
|
coroutine.yield()
|
||||||
running = false
|
running = false
|
||||||
end, opts)
|
end, opts)
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
assert(running)
|
assert(running)
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
task:wait()
|
task:wait()
|
||||||
assert(not running)
|
assert(not running)
|
||||||
assert(not task:is_running())
|
assert(not task:running())
|
||||||
assert(task_result.done)
|
assert(task_result.done)
|
||||||
assert(not task:has_errors())
|
assert(not task:has_errors())
|
||||||
end)
|
end)
|
||||||
|
@ -60,19 +60,19 @@ describe("task", function()
|
||||||
local task = Task.new(plugin, "spawn_errors", function(task)
|
local task = Task.new(plugin, "spawn_errors", function(task)
|
||||||
task:spawn("foobar")
|
task:spawn("foobar")
|
||||||
end, opts)
|
end, opts)
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
task:wait()
|
task:wait()
|
||||||
assert(not task:is_running())
|
assert(not task:running())
|
||||||
assert(task_result.done)
|
assert(task_result.done)
|
||||||
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("Failed to spawn"), task.output)
|
assert(task:has_errors() and task:output(vim.log.levels.ERROR):find("Failed to spawn"), task:output())
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("spawn", function()
|
it("spawn", function()
|
||||||
local task = Task.new(plugin, "test", function(task)
|
local task = Task.new(plugin, "test", function(task)
|
||||||
task:spawn("echo", { args = { "foo" } })
|
task:spawn("echo", { args = { "foo" } })
|
||||||
end, opts)
|
end, opts)
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
task:wait()
|
task:wait()
|
||||||
assert.same(task:output(), "foo")
|
assert.same(task:output(), "foo")
|
||||||
assert(task_result.done)
|
assert(task_result.done)
|
||||||
|
@ -84,8 +84,8 @@ describe("task", function()
|
||||||
task:spawn("echo", { args = { "foo" } })
|
task:spawn("echo", { args = { "foo" } })
|
||||||
task:spawn("echo", { args = { "bar" } })
|
task:spawn("echo", { args = { "bar" } })
|
||||||
end, opts)
|
end, opts)
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
assert(task:is_running())
|
assert(task:running())
|
||||||
task:wait()
|
task:wait()
|
||||||
assert(task:output() == "foo\nbar" or task:output() == "bar\nfoo", task:output())
|
assert(task:output() == "foo\nbar" or task:output() == "bar\nfoo", task:output())
|
||||||
assert(task_result.done)
|
assert(task_result.done)
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
nvim -l tests/busted.lua tests -o utfTerminal
|
nvim -l tests/busted.lua tests -o utfTerminal "$@"
|
||||||
|
|
42
vim.toml
42
vim.toml
|
@ -8,42 +8,14 @@ any = true
|
||||||
[jit]
|
[jit]
|
||||||
any = true
|
any = true
|
||||||
|
|
||||||
[[describe.args]]
|
[assert]
|
||||||
type = "string"
|
|
||||||
[[describe.args]]
|
|
||||||
type = "function"
|
|
||||||
|
|
||||||
[[it.args]]
|
|
||||||
type = "string"
|
|
||||||
[[it.args]]
|
|
||||||
type = "function"
|
|
||||||
|
|
||||||
[[before_each.args]]
|
|
||||||
type = "function"
|
|
||||||
[[after_each.args]]
|
|
||||||
type = "function"
|
|
||||||
|
|
||||||
[assert.is_not]
|
|
||||||
any = true
|
any = true
|
||||||
|
|
||||||
[[assert.equals.args]]
|
[describe]
|
||||||
type = "any"
|
any = true
|
||||||
[[assert.equals.args]]
|
|
||||||
type = "any"
|
|
||||||
[[assert.equals.args]]
|
|
||||||
type = "any"
|
|
||||||
required = false
|
|
||||||
|
|
||||||
[[assert.same.args]]
|
[it]
|
||||||
type = "any"
|
any = true
|
||||||
[[assert.same.args]]
|
|
||||||
type = "any"
|
|
||||||
|
|
||||||
[[assert.truthy.args]]
|
[before_each.args]
|
||||||
type = "any"
|
any = true
|
||||||
|
|
||||||
[[assert.spy.args]]
|
|
||||||
type = "any"
|
|
||||||
|
|
||||||
[[assert.stub.args]]
|
|
||||||
type = "any"
|
|
||||||
|
|
Loading…
Reference in New Issue