mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-01-24 05:40:05 +08:00
324 lines
6.6 KiB
Lua
324 lines
6.6 KiB
Lua
local uv = vim.loop
|
|
|
|
local Object = require "plenary.class"
|
|
local log = require "plenary.log"
|
|
|
|
local async = require "plenary.async"
|
|
local channel = require("plenary.async").control.channel
|
|
|
|
local M = {}
|
|
|
|
local AsyncJob = {}
|
|
AsyncJob.__index = AsyncJob
|
|
|
|
function AsyncJob.new(opts)
|
|
local self = setmetatable({}, AsyncJob)
|
|
|
|
self.command, self.uv_opts = M.convert_opts(opts)
|
|
|
|
self.stdin = opts.stdin or M.NullPipe()
|
|
self.stdout = opts.stdout or M.NullPipe()
|
|
self.stderr = opts.stderr or M.NullPipe()
|
|
|
|
if opts.cwd and opts.cwd ~= "" then
|
|
self.uv_opts.cwd = vim.fn.expand(vim.fn.escape(opts.cwd, "$"))
|
|
-- this is a "illegal" hack for windows. E.g. If the git command returns `/` rather than `\` as delimiter,
|
|
-- vim.fn.expand might just end up returning an empty string. Weird
|
|
-- Because empty string is not allowed in libuv the job will not spawn. Solution is we just set it to opts.cwd
|
|
if self.uv_opts.cwd == "" then
|
|
self.uv_opts.cwd = opts.cwd
|
|
end
|
|
end
|
|
|
|
self.uv_opts.stdio = {
|
|
self.stdin.handle,
|
|
self.stdout.handle,
|
|
self.stderr.handle,
|
|
}
|
|
|
|
return self
|
|
end
|
|
|
|
function AsyncJob:_for_each_pipe(f, ...)
|
|
for _, pipe in ipairs { self.stdin, self.stdout, self.stderr } do
|
|
f(pipe, ...)
|
|
end
|
|
end
|
|
|
|
function AsyncJob:close(force)
|
|
if force == nil then
|
|
force = true
|
|
end
|
|
|
|
self:_for_each_pipe(function(p)
|
|
p:close(force)
|
|
end)
|
|
|
|
uv.process_kill(self.handle, "SIGTERM")
|
|
|
|
log.debug "[async_job] closed"
|
|
end
|
|
|
|
M.spawn = function(opts)
|
|
local self = AsyncJob.new(opts)
|
|
self.handle, self.pid = uv.spawn(
|
|
self.command,
|
|
self.uv_opts,
|
|
async.void(function()
|
|
self:close(false)
|
|
if not self.handle:is_closing() then
|
|
self.handle:close()
|
|
end
|
|
end)
|
|
)
|
|
|
|
if not self.handle then
|
|
error(debug.traceback("Failed to spawn process: " .. vim.inspect(self)))
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
---@class uv_pipe_t
|
|
--- A pipe handle from libuv
|
|
---@field read_start function: Start reading
|
|
---@field read_stop function: Stop reading
|
|
---@field close function: Close the handle
|
|
---@field is_closing function: Whether handle is currently closing
|
|
---@field is_active function: Whether the handle is currently reading
|
|
|
|
---@class BasePipe
|
|
---@field super Object: Always available
|
|
---@field handle uv_pipe_t: A pipe handle
|
|
---@field extend function: Extend
|
|
local BasePipe = Object:extend()
|
|
|
|
function BasePipe:new()
|
|
self.eof_tx, self.eof_rx = channel.oneshot()
|
|
end
|
|
|
|
function BasePipe:close(force)
|
|
if force == nil then
|
|
force = true
|
|
end
|
|
|
|
assert(self.handle, "Must have a pipe to close. Otherwise it's weird!")
|
|
|
|
if self.handle:is_closing() then
|
|
return
|
|
end
|
|
|
|
-- If we're not forcing the stop, allow waiting for eof
|
|
-- This ensures that we don't end up with weird race conditions
|
|
if not force then
|
|
self.eof_rx()
|
|
end
|
|
|
|
self.handle:read_stop()
|
|
if not self.handle:is_closing() then
|
|
self.handle:close()
|
|
end
|
|
|
|
self._closed = true
|
|
end
|
|
|
|
---@class LinesPipe : BasePipe
|
|
local LinesPipe = BasePipe:extend()
|
|
|
|
function LinesPipe:new()
|
|
LinesPipe.super.new(self)
|
|
self.handle = uv.new_pipe(false)
|
|
end
|
|
|
|
function LinesPipe:read()
|
|
local read_tx, read_rx = channel.oneshot()
|
|
|
|
self.handle:read_start(function(err, data)
|
|
assert(not err, err)
|
|
self.handle:read_stop()
|
|
|
|
read_tx(data)
|
|
if data == nil then
|
|
self.eof_tx()
|
|
end
|
|
end)
|
|
|
|
return read_rx()
|
|
end
|
|
|
|
function LinesPipe:iter(schedule)
|
|
if schedule == nil then
|
|
schedule = true
|
|
end
|
|
|
|
local text = nil
|
|
local index = nil
|
|
|
|
local get_next_text = function(previous)
|
|
index = nil
|
|
|
|
local read = self:read()
|
|
if previous == nil and read == nil then
|
|
return
|
|
end
|
|
|
|
read = string.gsub(read or "", "\r", "")
|
|
return (previous or "") .. read
|
|
end
|
|
|
|
local next_value = nil
|
|
next_value = function()
|
|
if schedule then
|
|
async.util.scheduler()
|
|
end
|
|
|
|
if text == nil or (text == "" and index == nil) then
|
|
return nil
|
|
end
|
|
|
|
local start = index
|
|
index = string.find(text, "\n", index, true)
|
|
|
|
if index == nil then
|
|
text = get_next_text(string.sub(text, start or 1))
|
|
return next_value()
|
|
end
|
|
|
|
index = index + 1
|
|
|
|
return string.sub(text, start or 1, index - 2)
|
|
end
|
|
|
|
text = get_next_text()
|
|
|
|
return function()
|
|
return next_value()
|
|
end
|
|
end
|
|
|
|
---@class NullPipe : BasePipe
|
|
local NullPipe = BasePipe:extend()
|
|
|
|
function NullPipe:new()
|
|
NullPipe.super.new(self)
|
|
self.start = function() end
|
|
self.read_start = function() end
|
|
self.close = function() end
|
|
|
|
-- This always has eof tx done, so can just call it now
|
|
self.eof_tx()
|
|
end
|
|
|
|
---@class ChunkPipe : BasePipe
|
|
local ChunkPipe = BasePipe:extend()
|
|
|
|
function ChunkPipe:new()
|
|
ChunkPipe.super.new(self)
|
|
self.handle = uv.new_pipe(false)
|
|
end
|
|
|
|
function ChunkPipe:read()
|
|
local read_tx, read_rx = channel.oneshot()
|
|
|
|
self.handle:read_start(function(err, data)
|
|
assert(not err, err)
|
|
self.handle:read_stop()
|
|
|
|
read_tx(data)
|
|
if data == nil then
|
|
self.eof_tx()
|
|
end
|
|
end)
|
|
|
|
return read_rx()
|
|
end
|
|
|
|
function ChunkPipe:iter()
|
|
return function()
|
|
if self._closed then
|
|
return nil
|
|
end
|
|
|
|
return self:read()
|
|
end
|
|
end
|
|
|
|
---@class ErrorPipe : BasePipe
|
|
local ErrorPipe = BasePipe:extend()
|
|
|
|
function ErrorPipe:new()
|
|
ErrorPipe.super.new(self)
|
|
self.handle = uv.new_pipe(false)
|
|
end
|
|
|
|
function ErrorPipe:start()
|
|
self.handle:read_start(function(err, data)
|
|
if not err and not data then
|
|
return
|
|
end
|
|
|
|
self.handle:read_stop()
|
|
self.handle:close()
|
|
|
|
error(string.format("Err: %s, Data: '%s'", err, data))
|
|
end)
|
|
end
|
|
|
|
M.NullPipe = NullPipe
|
|
M.LinesPipe = LinesPipe
|
|
M.ChunkPipe = ChunkPipe
|
|
M.ErrorPipe = ErrorPipe
|
|
|
|
M.convert_opts = function(o)
|
|
if not o then
|
|
error(debug.traceback "Options are required for Job:new")
|
|
end
|
|
|
|
local command = o.command
|
|
if not command then
|
|
if o[1] then
|
|
command = o[1]
|
|
else
|
|
error(debug.traceback "'command' is required for Job:new")
|
|
end
|
|
elseif o[1] then
|
|
error(debug.traceback "Cannot pass both 'command' and array args")
|
|
end
|
|
|
|
local args = o.args
|
|
if not args then
|
|
if #o > 1 then
|
|
args = { select(2, unpack(o)) }
|
|
end
|
|
end
|
|
|
|
local ok, is_exe = pcall(vim.fn.executable, command)
|
|
if not o.skip_validation and ok and 1 ~= is_exe then
|
|
error(debug.traceback(command .. ": Executable not found"))
|
|
end
|
|
|
|
local obj = {}
|
|
|
|
obj.args = args
|
|
|
|
if o.env then
|
|
if type(o.env) ~= "table" then
|
|
error(debug.traceback "'env' has to be a table")
|
|
end
|
|
|
|
local transform = {}
|
|
for k, v in pairs(o.env) do
|
|
if type(k) == "number" then
|
|
table.insert(transform, v)
|
|
elseif type(k) == "string" then
|
|
table.insert(transform, k .. "=" .. tostring(v))
|
|
end
|
|
end
|
|
obj.env = transform
|
|
end
|
|
|
|
return command, obj
|
|
end
|
|
|
|
return M
|