Skip to content

Commit

Permalink
feat(process): return errors
Browse files Browse the repository at this point in the history
Rather than throwing, running a process will now return any error
messages.
  • Loading branch information
rcarriga committed Feb 8, 2024
1 parent 0456ad7 commit c037b0a
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 68 deletions.
26 changes: 15 additions & 11 deletions doc/nio.txt
Original file line number Diff line number Diff line change
Expand Up @@ -355,14 +355,13 @@ Return~
nio.process *nio.process*


*nio.process.Process*
Wrapper for a running process, providing access to its stdio streams and
methods to interact with it.

*nio.process.Process*
Fields~
{pid} `(integer)` ID of the invoked process
{signal} `(fun(signal: integer|uv.aliases.signals))` Send a signal to
the process
{signal} `(fun(signal: integer|uv.aliases.signals))` Send a signal to the
process
{result} `(async fun(): number)` Wait for the process to exit and return the
exit code
{stdin} `(nio.streams.OSStreamWriter)` Stream to write to the process stdin.
Expand Down Expand Up @@ -390,7 +389,9 @@ Run a process asynchronously.
Parameters~
{opts} `(nio.process.RunOpts)`
Return~
`(nio.process.Process)`
`(nio.process.Process?)` Process object for the running process
Return~
`(string?)` Error message if an error occurred

*nio.process.RunOpts*
Fields~
Expand All @@ -417,20 +418,23 @@ nio.streams *nio.streams*

*nio.streams.Stream*
Fields~
{close} `(async fun(): nil)` Close the stream
{close} `(async fun(): string|nil)` Close the stream. Returns an error message
if an error occurred.

*nio.streams.Reader*
Inherits: `nio.streams.Stream`

Fields~
{read} `(async fun(n?: integer): string)` Read data from the stream,
optionally up to n bytes otherwise until EOF is reached
{read} `(async fun(n?: integer): string,string)` Read data from the stream,
optionally up to n bytes otherwise until EOF is reached. Returns the data read
or error message if an error occurred.

*nio.streams.Writer*
Inherits: `nio.streams.Stream`

Fields~
{write} `(async fun(data: string): nil)` Write data to the stream
{write} `(async fun(data: string): string|nil)` Write data to the stream.
Returns an error message if an error occurred.

*nio.streams.OSStream*
Inherits: `nio.streams.Stream`
Expand Down Expand Up @@ -480,8 +484,8 @@ information.

Fields~
{close} `(async fun(handle: uv_handle_t))`
{fs_open} `(async fun(path: any, flags: any, mode: any):
(string|nil,integer|nil))`
{fs_open} `(async fun(path: any, flags: uv.aliases.fs_access_flags|integer,
mode: any): (string|nil,integer|nil))`
{fs_read} `(async fun(fd: integer, size: integer, offset?: integer):
(string|nil,string|nil))`
{fs_close} `(async fun(fd: integer): (string|nil,boolean|nil))`
Expand Down
60 changes: 40 additions & 20 deletions lua/nio/process.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,15 @@ local nio = {}
---@class nio.process
nio.process = {}

---@class nio.process.Process
--- Wrapper for a running process, providing access to its stdio streams and
--- methods to interact with it.
---
---@class nio.process.Process
---@field pid integer ID of the invoked process
---@field signal fun(signal: integer|uv.aliases.signals) Send a signal to
--- the process
---@field result async fun(): number Wait for the process to exit and return the
--- exit code
---@field signal fun(signal: integer|uv.aliases.signals) Send a signal to the process
---@field result async fun(): number Wait for the process to exit and return the exit code
---@field stdin nio.streams.OSStreamWriter Stream to write to the process stdin.
---@field stdout nio.streams.OSStreamReader Stream to read from the process
--- stdout.
---@field stderr nio.streams.OSStreamReader Stream to read from the process
--- stderr.
---@field stdout nio.streams.OSStreamReader Stream to read from the process stdout.
---@field stderr nio.streams.OSStreamReader Stream to read from the process stderr.

--- Run a process asynchronously.
--- ```lua
Expand All @@ -35,7 +30,8 @@ nio.process = {}
--- print(output)
--- ```
---@param opts nio.process.RunOpts
---@return nio.process.Process
---@return nio.process.Process? Process object for the running process
---@return string? Error message if an error occurred
function nio.process.run(opts)
opts = vim.tbl_extend("force", { hide = true }, opts)

Expand All @@ -44,13 +40,22 @@ function nio.process.run(opts)

local exit_code_future = control.future()

local stdout = streams.reader(opts.stdout)
local stderr = streams.reader(opts.stderr)
local stdin = streams.writer(opts.stdin)
local stdout, stdout_err = streams.reader(opts.stdout)
if not stdout then
return nil, stdout_err
end
local stderr, stderr_err = streams.reader(opts.stderr)
if not stderr then
return nil, stderr_err
end
local stdin, stdin_err = streams.writer(opts.stdin)
if not stdin then
return nil, stdin_err
end

local stdio = { stdin.pipe, stdout.pipe, stderr.pipe }

local handle, pid, spawn_err = vim.loop.spawn(cmd, {
local handle, pid_or_error = vim.loop.spawn(cmd, {
args = args,
stdio = stdio,
env = opts.env,
Expand All @@ -64,26 +69,41 @@ function nio.process.run(opts)
exit_code_future.set(code)
end)

assert(not spawn_err, spawn_err)
if not handle then
return nil, pid_or_error
end
local stdin_fd, stdin_fd_err = stdin.pipe:fileno()
if not stdin_fd then
return nil, stdin_fd_err
end
local stdout_fd, stdout_fd_err = stdout.pipe:fileno()
if not stdout_fd then
return nil, stdout_fd_err
end
local stderr_fd, stderr_fd_err = stderr.pipe:fileno()
if not stderr_fd then
return nil, stderr_fd_err
end

---@type nio.process.Process
local process = {
pid = pid,
pid = pid_or_error,
signal = function(signal)
vim.loop.process_kill(handle, signal)
end,
stdin = {
write = stdin.write,
fd = stdin.pipe:fileno(),
fd = stdin_fd,
close = stdin.close,
},
stdout = {
read = stdout.read,
fd = stdout.pipe:fileno(),
fd = stdout_fd,
close = stdout.close,
},
stderr = {
read = stderr.read,
fd = stderr.pipe:fileno(),
fd = stderr_fd,
close = stderr.close,
},
result = exit_code_future.wait,
Expand Down
65 changes: 48 additions & 17 deletions lua/nio/streams.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ local nio = {}
nio.streams = {}

---@class nio.streams.Stream
---@field close async fun(): nil Close the stream
---@field close async fun(): string|nil Close the stream. Returns an error message if an error occurred.

---@class nio.streams.Reader : nio.streams.Stream
---@field read async fun(n?: integer): string Read data from the stream,
--- optionally up to n bytes otherwise until EOF is reached
---@field read async fun(n?: integer): string,string Read data from the stream, optionally up to n bytes otherwise until EOF is reached. Returns the data read or error message if an error occurred.

---@class nio.streams.Writer : nio.streams.Stream
---@field write async fun(data: string): nil Write data to the stream
---@field write async fun(data: string): string|nil Write data to the stream. Returns an error message if an error occurred.

---@class nio.streams.OSStream : nio.streams.Stream
---@field fd integer The file descriptor of the stream
Expand All @@ -28,7 +27,8 @@ nio.streams = {}
---@class nio.streams.OSStreamWriter : nio.streams.StreamWriter, nio.streams.OSStream

---@param input integer|uv.uv_pipe_t|uv_pipe_t|nio.streams.OSStream
---@return uv_pipe_t
---@return uv_pipe_t?
---@return string?
---@nodoc
local function create_pipe(input)
if type(input) == "userdata" then
Expand All @@ -37,21 +37,30 @@ local function create_pipe(input)
end

local pipe, err = vim.loop.new_pipe()
assert(not err and pipe, err)
if not pipe then
return nil, err
end

local fd = type(input) == "number" and input or input and input.fd
if fd then
-- File descriptor
pipe:open(fd)
local _, open_err = pipe:open(fd)
if open_err then
return nil, open_err
end
end

return pipe
end

---@param input integer|nio.streams.OSStreamReader|uv.uv_pipe_t|uv_pipe_t
---@return {pipe: uv_pipe_t, read: (fun(n?: integer):string,string), close: fun(): string|nil}|nil
---@return string|nil
---@private
function nio.streams.reader(input)
local pipe = create_pipe(input)
local pipe, create_err = create_pipe(input)
if not pipe then
return nil, create_err
end

local buffer = ""
local ready = control.event()
Expand All @@ -66,18 +75,24 @@ function nio.streams.reader(input)
complete.set()
ready.set()
end
local read_err = nil

local start = function()
started = true
pipe:read_start(function(err, data)
assert(not err, err)
local _, read_start_err = pipe:read_start(function(err, data)
if err then
read_err = err
ready.set()
return
end
if not data then
tasks.run(stop_reading)
return
end
buffer = buffer .. data
ready.set()
end)
return read_start_err
end

return {
Expand All @@ -88,17 +103,24 @@ function nio.streams.reader(input)
end,
read = function(n)
if not started then
start()
local start_err = start()
if start_err then
return "", start_err
end
end
if n == 0 then
return ""
return "", nil
end

while not complete.is_set() and (not n or #buffer < n) do
while not complete.is_set() and (not n or #buffer < n) and not read_err do
ready.wait()
ready.clear()
end

if read_err then
return "", read_err
end

local data = n and buffer:sub(1, n) or buffer
buffer = buffer:sub(#data + 1)
return data
Expand All @@ -107,17 +129,26 @@ function nio.streams.reader(input)
end

---@param input integer|nio.streams.OSStreamWriter|uv.uv_pipe_t|uv_pipe_t
---@return {pipe: uv_pipe_t, write: (fun(data: string): string|nil), close: fun(): string|nil}|nil
---@return string|nil
---@private
function nio.streams.writer(input)
local pipe = create_pipe(input)
local pipe, create_err = create_pipe(input)
if not pipe then
return nil, create_err
end

return {
pipe = pipe,
write = function(data)
uv.write(pipe, data)
local maybe_err = uv.write(pipe, data)
if type(maybe_err) == "string" then
return maybe_err
end
return nil
end,
close = function()
uv.shutdown(pipe)
return uv.shutdown(pipe)
end,
}
end
Expand Down
2 changes: 1 addition & 1 deletion lua/nio/uv.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ local nio = {}
---
---@class nio.uv
---@field close async fun(handle: uv_handle_t)
---@field fs_open async fun(path: any, flags: any, mode: any): (string|nil,integer|nil)
---@field fs_open async fun(path: any, flags: uv.aliases.fs_access_flags|integer, mode: any): (string|nil,integer|nil)
---@field fs_read async fun(fd: integer, size: integer, offset?: integer): (string|nil,string|nil)
---@field fs_close async fun(fd: integer): (string|nil,boolean|nil)
---@field fs_unlink async fun(path: string): (string|nil,boolean|nil)
Expand Down
Loading

0 comments on commit c037b0a

Please sign in to comment.