Hi Xopxe
There are a few things about the selector.grab_stdout tool that don't work for me:
- Doesn't grab
stderr which many tools such as wget use extensively
- Doesn't provide feedback on when a long(ish) running command has completed and no way of finding the exit code of the program that has been run
- Leaks FDs (because
waitpid must be called on child processes otherwise they become zombie processes)
I've put together a demo of what a more comprehensive function might look like. Some things to note:
- I use a variation of the double fork technique. The grandchild process is the one that executes the program and its direct parent (the child process) calls a blocking
nixio.waitpid() on the grandchild's pid. Once this returns it sends the return code (the marker of success or failure) on a stream, much like the stdout and stderr streams
- This uses a more complex input table to avoid code repetition
- It should be easy to alter
grab_stdout to use a function like this under the hood
Okay the function looks like this:
M.grab_all = function(command, handles)
local function run_shell_nixio(command)
local out_r, out_w = nixio.pipe()
local err_r, err_w = nixio.pipe()
local ret_r, ret_w = nixio.pipe()
local childpid = nixio.fork()
assert(childpid ~= nil, errmsg)
if childpid > 0 then --parent
out_w:close()
err_w:close()
ret_w:close()
return {out_r=out_r, err_r=err_r, ret_r=ret_r}, pid
else
local grandchildpid = nixio.fork()
assert(grandchildpid ~= nil, errmsg)
if grandchildpid > 0 then -- child
out_r:close(); out_w:close()
err_r:close(); err_w:close()
ret_r:close()
local _, _, exit_code = nixio.waitpid(grandchildpid)
ret_w:write(exit_code)
ret_w:close()
os.exit()
else -- grandchild
nixio.dup(out_w, nixio.stdout)
nixio.dup(err_w, nixio.stderr)
out_r:close(); out_w:close()
err_r:close(); err_w:close()
ret_r:close(); ret_w:close()
nixio.exec("/bin/sh", "-c", command)
end
end
end
local descriptors, pid = run_shell_nixio(command)
local sockets = {}
for k, v in pairs(descriptors) do
local sktd=init_sktd({
pattern = normalize_pattern(handles[k].pattern),
handler = handles[k].handler,
fd = descriptors[k],
events = descriptors[k]
})
-- sktd.events = {data=sktd.fd}
if sktd.pattern=='*l' and handles[k].handler == 'stream' then sktd.pattern=nil end
register_client(sktd)
sockets[k] = sktd
end
return sockets, pid
end
An example of how it works is like this:
--look for packages one folder up.
package.path = package.path .. ";;;../../?.lua;../../?/init.lua"
local sched = require 'lumen.sched'
local stream = require 'lumen.stream'
local selector = require 'lumen.tasks.selector'
local nixio = require 'nixio'
selector.init({service='nixio'})
local handles = {
out_r = {
pattern = "*a",
handler = stream.new()
},
err_r = {
pattern = "*a",
handler = stream.new()
},
ret_r = {
pattern = "*a",
handler = stream.new()
},
}
-- task that issues command
sched.run(function()
local sktd, pid = selector.grab_all('ping -c 5 8.8.8.8', handles)
local a = handles.ret_r.handler:read()
if a ~= nil then
print("return code: "..a)
end
nixio.waitpid(pid)
print("function completed!")
end)
-- task receives ping std_out
sched.run(function()
while true do
local a, b, c = handles.out_r.handler:read()
if a ~= nil then
print("stdout: "..a)
else
print("returning from stdout wait")
return
end
end
end)
-- task receives ping std_err
sched.run(function()
while true do
local a, b, c = handles.err_r.handler:read()
if a ~= nil then
print("stderr: "..a)
else
print("returning from stderr wait")
return
end
end
end)
sched.loop()
This is very much a first attempt. Would love your input. I'm sure this could be made more elegant (through functional composition perhaps to execute the double fork and waiting on the grandchild pid), but it works well and allows for efficient execution of external commands (no need to poll on the pid to ascertain program completion). Would love your thoughts on this before I submit a PR.
Also a very cool addition would be to bind stdin to this process, perhaps through a stream as well so that we can take full input/output command of external programs. I tried extending this just by adding another handler specified the same as the other 3 but just managed to achieve 100% CPU utilisation, which is no fun - I suspect you understand posix sockets much better than me :)
Hi Xopxe
There are a few things about the
selector.grab_stdouttool that don't work for me:stderrwhich many tools such aswgetuse extensivelywaitpidmust be called on child processes otherwise they become zombie processes)I've put together a demo of what a more comprehensive function might look like. Some things to note:
nixio.waitpid()on the grandchild's pid. Once this returns it sends the return code (the marker of success or failure) on a stream, much like thestdoutandstderrstreamsgrab_stdoutto use a function like this under the hoodOkay the function looks like this:
An example of how it works is like this:
This is very much a first attempt. Would love your input. I'm sure this could be made more elegant (through functional composition perhaps to execute the double fork and waiting on the grandchild pid), but it works well and allows for efficient execution of external commands (no need to poll on the pid to ascertain program completion). Would love your thoughts on this before I submit a PR.
Also a very cool addition would be to bind
stdinto this process, perhaps through a stream as well so that we can take full input/output command of external programs. I tried extending this just by adding another handler specified the same as the other 3 but just managed to achieve 100% CPU utilisation, which is no fun - I suspect you understand posix sockets much better than me :)