Skip to content

Commit ce2dde9

Browse files
committed
feat: process module
1 parent 48413f4 commit ce2dde9

File tree

7 files changed

+477
-8
lines changed

7 files changed

+477
-8
lines changed

doc/nio.txt

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ nvim-nio *nvim-nio*
66
nio....................................................................|nio|
77
nio.control....................................................|nio.control|
88
nio.lsp............................................................|nio.lsp|
9+
nio.process....................................................|nio.process|
10+
nio.streams....................................................|nio.streams|
911
nio.uv..............................................................|nio.uv|
1012
nio.ui..............................................................|nio.ui|
1113
nio.tests........................................................|nio.tests|
@@ -349,6 +351,108 @@ Return~
349351
`(nio.lsp.Client)`
350352

351353

354+
==============================================================================
355+
nio.process *nio.process*
356+
357+
358+
*nio.process.Process*
359+
Wrapper for a running process, providing access to its stdio streams and
360+
methods to interact with it.
361+
362+
Fields~
363+
{pid} `(integer)` ID of the invoked process
364+
{signal} `(fun(signal: integer|uv.aliases.signals))` Send a signal to
365+
the process
366+
{result} `(async fun(): number)` Wait for the process to exit and return the
367+
exit code
368+
{stdin} `(nio.streams.OSStreamWriter)` Stream to write to the process stdin.
369+
{stdout} `(nio.streams.OSStreamReader)` Stream to read from the process
370+
stdout.
371+
{stderr} `(nio.streams.OSStreamReader)` Stream to read from the process
372+
stderr.
373+
374+
*nio.process.run()*
375+
`run`({opts})
376+
377+
Run a process asynchronously.
378+
>lua
379+
local first = nio.process.run({
380+
cmd = "printf", args = { "hello" }
381+
})
382+
383+
local second = nio.process.run({
384+
cmd = "cat", stdin = first.stdout
385+
})
386+
387+
local output = second.stdout.read()
388+
print(output)
389+
<
390+
Parameters~
391+
{opts} `(nio.process.RunOpts)`
392+
Return~
393+
`(nio.process.Process)`
394+
395+
*nio.process.RunOpts*
396+
Fields~
397+
{cmd} `(string)` Command to run
398+
{args?} `(string[])` Arguments to pass to the command
399+
{stdin?} `(integer|nio.streams.OSStreamReader|uv.uv_pipe_t|uv_pipe_t)` Stream,
400+
pipe or file descriptor to use as stdin.
401+
{stdout?} `(integer|nio.streams.OSStreamWriter|uv.uv_pipe_t|uv_pipe_t)`
402+
Stream,
403+
pipe or file descriptor to use as stdout.
404+
{stderr?} `(integer|nio.streams.OSStreamWriter|uv.uv_pipe_t|uv_pipe_t)`
405+
Stream,
406+
pipe or file descriptor to use as stderr.
407+
{env?} `(table<string, string>)` Environment variables to pass to the
408+
process
409+
{cwd?} `(string)` Current working directory of the process
410+
{uid?} `(integer)` User ID of the process
411+
{gid?} `(integer)` Group ID of the process
412+
413+
414+
==============================================================================
415+
nio.streams *nio.streams*
416+
417+
418+
*nio.streams.Stream*
419+
Fields~
420+
{close} `(async fun(): nil)` Close the stream
421+
422+
*nio.streams.Reader*
423+
Inherits: `nio.streams.Stream`
424+
425+
Fields~
426+
{read} `(async fun(n?: integer): string)` Read data from the stream,
427+
optionally up to n bytes otherwise until EOF is reached
428+
429+
*nio.streams.Writer*
430+
Inherits: `nio.streams.Stream`
431+
432+
Fields~
433+
{write} `(async fun(data: string): nil)` Write data to the stream
434+
435+
*nio.streams.OSStream*
436+
Inherits: `nio.streams.Stream`
437+
438+
Fields~
439+
{fd} `(integer)` The file descriptor of the stream
440+
441+
*nio.streams.StreamReader*
442+
Inherits: `nio.streams.Reader, nio.streams.Stream`
443+
444+
Inherits: `nio.streams.Writer, nio.streams.Stream`
445+
*nio.streams.StreamWriter*
446+
447+
448+
*nio.streams.OSStreamReader*
449+
Inherits: `nio.streams.StreamReader, nio.streams.OSStream`
450+
451+
Inherits: `nio.streams.StreamWriter, nio.streams.OSStream`
452+
*nio.streams.OSStreamWriter*
453+
454+
455+
352456
==============================================================================
353457
nio.uv *nio.uv*
354458

@@ -436,7 +540,8 @@ length: integer): (string|nil,integer|nil))`
436540
{fs_scandir} `(async fun(path: string): (string|nil,uv_fs_t|nil))`
437541
{shutdown} `(async fun(stream: uv_stream_t): string|nil)`
438542
{listen} `(async fun(stream: uv_stream_t): string|nil)`
439-
{write} `(async fun(stream: uv_stream_t): string|nil)`
543+
{write} `(async fun(stream: uv_stream_t, data: string|string[]):
544+
uv.uv_write_t|nil)`
440545
{write2} `(async fun(stream: uv_stream_t, data: string|string[], send_handle:
441546
uv_stream_t): string|nil)`
442547

lua/nio/init.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ local uv = require("nio.uv")
55
local tests = require("nio.tests")
66
local ui = require("nio.ui")
77
local lsp = require("nio.lsp")
8+
local process = require("nio.process")
89

910
---@tag nvim-nio
1011

@@ -26,6 +27,7 @@ nio.ui = ui
2627
nio.tests = tests
2728
nio.tasks = tasks
2829
nio.lsp = lsp
30+
nio.process = process
2931

3032
--- Run a function in an async context. This is the entrypoint to all async
3133
--- functionality.

lua/nio/process.lua

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
local streams = require("nio.streams")
2+
local control = require("nio.control")
3+
4+
local nio = {}
5+
---@toc_entry nio.process
6+
---@class nio.process
7+
nio.process = {}
8+
9+
---@class nio.process.Process
10+
--- Wrapper for a running process, providing access to its stdio streams and
11+
--- methods to interact with it.
12+
---
13+
---@field pid integer ID of the invoked process
14+
---@field signal fun(signal: integer|uv.aliases.signals) Send a signal to
15+
--- the process
16+
---@field result async fun(): number Wait for the process to exit and return the
17+
--- exit code
18+
---@field stdin nio.streams.OSStreamWriter Stream to write to the process stdin.
19+
---@field stdout nio.streams.OSStreamReader Stream to read from the process
20+
--- stdout.
21+
---@field stderr nio.streams.OSStreamReader Stream to read from the process
22+
--- stderr.
23+
24+
--- Run a process asynchronously.
25+
--- ```lua
26+
--- local first = nio.process.run({
27+
--- cmd = "printf", args = { "hello" }
28+
--- })
29+
---
30+
--- local second = nio.process.run({
31+
--- cmd = "cat", stdin = first.stdout
32+
--- })
33+
---
34+
--- local output = second.stdout.read()
35+
--- print(output)
36+
--- ```
37+
---@param opts nio.process.RunOpts
38+
---@return nio.process.Process
39+
function nio.process.run(opts)
40+
opts = vim.tbl_extend("force", { hide = true }, opts)
41+
42+
local cmd = opts.cmd
43+
local args = opts.args
44+
45+
local exit_code_future = control.future()
46+
47+
local stdout = streams.reader(opts.stdout)
48+
local stderr = streams.reader(opts.stderr)
49+
local stdin = streams.writer(opts.stdin)
50+
51+
local stdio = { stdin.pipe, stdout.pipe, stderr.pipe }
52+
53+
local handle, pid, spawn_err = vim.loop.spawn(cmd, {
54+
args = args,
55+
stdio = stdio,
56+
env = opts.env,
57+
cwd = opts.cwd,
58+
uid = opts.uid,
59+
gid = opts.gid,
60+
verbatim = opts.verbatim,
61+
detached = opts.detached,
62+
hide = opts.hide,
63+
}, function(_, code)
64+
exit_code_future.set(code)
65+
end)
66+
67+
assert(not spawn_err, spawn_err)
68+
69+
local process = {
70+
pid = pid,
71+
signal = function(signal)
72+
vim.loop.process_kill(handle, signal)
73+
end,
74+
stdin = {
75+
write = stdin.write,
76+
fd = stdin.pipe:fileno(),
77+
close = stdin.close,
78+
},
79+
stdout = {
80+
read = stdout.read,
81+
fd = stdout.pipe:fileno(),
82+
close = stdout.close,
83+
},
84+
stderr = {
85+
read = stderr.read,
86+
fd = stderr.pipe:fileno(),
87+
close = stderr.close,
88+
},
89+
result = exit_code_future.wait,
90+
}
91+
return process
92+
end
93+
94+
---@class nio.process.RunOpts
95+
---@field cmd string Command to run
96+
---@field args? string[] Arguments to pass to the command
97+
---@field stdin? integer|nio.streams.OSStreamReader|uv.uv_pipe_t|uv_pipe_t Stream,
98+
--- pipe or file descriptor to use as stdin.
99+
---@field stdout? integer|nio.streams.OSStreamWriter|uv.uv_pipe_t|uv_pipe_t Stream,
100+
--- pipe or file descriptor to use as stdout.
101+
---@field stderr? integer|nio.streams.OSStreamWriter|uv.uv_pipe_t|uv_pipe_t Stream,
102+
--- pipe or file descriptor to use as stderr.
103+
---@field env? table<string, string> Environment variables to pass to the
104+
--- process
105+
---@field cwd? string Current working directory of the process
106+
---@field uid? integer User ID of the process
107+
---@field gid? integer Group ID of the process
108+
109+
return nio.process

lua/nio/streams.lua

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
local tasks = require("nio.tasks")
2+
local control = require("nio.control")
3+
local uv = require("nio.uv")
4+
5+
local nio = {}
6+
7+
---@toc_entry nio.streams
8+
---@class nio.streams
9+
nio.streams = {}
10+
11+
---@class nio.streams.Stream
12+
---@field close async fun(): nil Close the stream
13+
14+
---@class nio.streams.Reader : nio.streams.Stream
15+
---@field read async fun(n?: integer): string Read data from the stream,
16+
--- optionally up to n bytes otherwise until EOF is reached
17+
18+
---@class nio.streams.Writer : nio.streams.Stream
19+
---@field write async fun(data: string): nil Write data to the stream
20+
21+
---@class nio.streams.OSStream : nio.streams.Stream
22+
---@field fd integer The file descriptor of the stream
23+
24+
---@class nio.streams.StreamReader : nio.streams.Reader, nio.streams.Stream
25+
---@class nio.streams.StreamWriter : nio.streams.Writer, nio.streams.Stream
26+
27+
---@class nio.streams.OSStreamReader : nio.streams.StreamReader, nio.streams.OSStream
28+
---@class nio.streams.OSStreamWriter : nio.streams.StreamWriter, nio.streams.OSStream
29+
30+
---@param input integer|uv.uv_pipe_t|uv_pipe_t|nio.streams.OSStream
31+
---@return uv_pipe_t
32+
---@nodoc
33+
local function create_pipe(input)
34+
if type(input) == "userdata" then
35+
-- Existing pipe
36+
return input
37+
end
38+
39+
local pipe, err = vim.loop.new_pipe()
40+
assert(not err and pipe, err)
41+
42+
local fd = type(input) == "number" and input or input and input.fd
43+
if fd then
44+
-- File descriptor
45+
pipe:open(fd)
46+
end
47+
48+
return pipe
49+
end
50+
51+
---@param input integer|nio.streams.OSStreamReader|uv.uv_pipe_t|uv_pipe_t
52+
---@private
53+
function nio.streams.reader(input)
54+
local pipe = create_pipe(input)
55+
56+
local buffer = ""
57+
local ready = control.event()
58+
local complete = control.event()
59+
local started = false
60+
61+
local stop_reading = function()
62+
if not started or complete.is_set() then
63+
return
64+
end
65+
vim.loop.read_stop(pipe)
66+
complete.set()
67+
ready.set()
68+
end
69+
70+
local start = function()
71+
started = true
72+
pipe:read_start(function(err, data)
73+
assert(not err, err)
74+
if not data then
75+
tasks.run(stop_reading)
76+
return
77+
end
78+
buffer = buffer .. data
79+
ready.set()
80+
end)
81+
end
82+
83+
return {
84+
pipe = pipe,
85+
close = function()
86+
stop_reading()
87+
uv.close(pipe)
88+
end,
89+
read = function(n)
90+
if not started then
91+
start()
92+
end
93+
if n == 0 then
94+
return ""
95+
end
96+
97+
while not complete.is_set() and (not n or #buffer < n) do
98+
ready.wait()
99+
ready.clear()
100+
end
101+
102+
local data = n and buffer:sub(1, n) or buffer
103+
buffer = buffer:sub(#data + 1)
104+
return data
105+
end,
106+
}
107+
end
108+
109+
---@param input integer|nio.streams.OSStreamWriter|uv.uv_pipe_t|uv_pipe_t
110+
---@private
111+
function nio.streams.writer(input)
112+
local pipe = create_pipe(input)
113+
114+
return {
115+
pipe = pipe,
116+
write = function(data)
117+
uv.write(pipe, data)
118+
end,
119+
close = function()
120+
uv.shutdown(pipe)
121+
end,
122+
}
123+
end
124+
125+
return nio.streams

lua/nio/uv.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ local nio = {}
6363
---@field fs_scandir async fun(path: string): (string|nil,uv_fs_t|nil)
6464
---@field shutdown async fun(stream: uv_stream_t): string|nil
6565
---@field listen async fun(stream: uv_stream_t): string|nil
66-
---@field write async fun(stream: uv_stream_t): string|nil
66+
---@field write async fun(stream: uv_stream_t, data: string|string[]): uv.uv_write_t|nil
6767
---@field write2 async fun(stream: uv_stream_t, data: string|string[], send_handle: uv_stream_t): string|nil
6868
nio.uv = {}
6969

@@ -78,7 +78,7 @@ local function add(name, argc)
7878
nio.uv[name] = ret
7979
end
8080

81-
add("close", 4) -- close a handle
81+
add("close", 2) -- close a handle
8282
-- filesystem operations
8383
add("fs_open", 4)
8484
add("fs_read", 4)

0 commit comments

Comments
 (0)