-- a cqueues utility for handling a bunch of editor processes -- it runs in the same process as the main program to make it easier -- to pass around lua data. as a result of this, it greedily blocks -- SIGCHILD; since this is the only time we make subprocesses in the scope -- of this program, this is fine. local signal = require("cqueues.signal") local wait = require("posix.sys.wait") local std = require("posix.unistd") local fifo = require("fifo") local fs = require("fs") local mom = {} -- initialize a new mom -- this starts the sigblocking handler; only one will work at a time -- otherwise the blocking system is not portable function mom.new(loop) local m = setmetatable({}, { __index = mom }) m.loop = loop m.pids = {} -- actual clients. each of the keys is a unique table that gets returned to -- the caller when they make a new ed process -- you could argue this is unnecessary, and that the caller should be in charge of -- making its own process. i don't really care either way; this is easier and does -- what we want m.clients = {} loop:wrap(function () mom.tend(m) end) return m end -- create a new process in it -- returns process (unique id object), tx_queue, rx_queue -- you need to give it an identifier (this can be anything, and it isn't the id -- that it uses internally). things pushed to the rx_queue will look like -- { IDENTIFIER, "line", "meow" } -- this is to make multiplexing easier -- you must also give it a queue to send to (the rx_queue) function mom.create(m, command, rx_id, rx_queue, directory) local id = {} m.clients[id] = { command = assert(command), directory = assert(directory), rx_id = assert(rx_id), tx_queue = fifo.new(), rx_queue = assert(rx_queue), tx_fds = fifo.new(), rx_fds = fifo.new(), } mom.start_process(m, id) m.loop:wrap(function() mom.handle_from_ed(m, id) end) m.loop:wrap(function() mom.handle_to_ed(m, id) end) return id, m.clients[id].tx_queue, m.clients[id].rx_queue end local function error_wait(err) print("error: " .. err) print("say something to try again") io.read("*l") os.exit(100) end function mom.start_process(m, id) local proc_out_rx, proc_out_tx = std.pipe() if not (proc_out_rx and proc_out_tx) then return nil, "can't create pipe" end local proc_in_rx, proc_in_tx = std.pipe() if not (proc_in_rx and proc_in_tx) then return nil, "can't create pipe" end local pid, err = std.fork() if not pid then return nil, "couldn't create process: " .. err end if pid == 0 then std.close(proc_out_rx) std.close(proc_in_tx) std.dup2(proc_in_rx, std.STDIN_FILENO) std.dup2(proc_out_tx, std.STDOUT_FILENO) std.dup2(proc_out_tx, std.STDERR_FILENO) local ok, err = std.chdir(m.clients[id].directory) if not ok then error_wait(err) end local _, err = std.execp(m.clients[id].command[1], { select(2, table.unpack(m.clients[id].command)) }) -- meh error_wait(err) else m.pids[pid] = id std.close(proc_in_rx) std.close(proc_out_tx) m.clients[id].tx_fds:put(fs.new(proc_in_tx, "w")) m.clients[id].rx_fds:put(fs.new(proc_out_rx, "r")) end return m end function mom.handle_from_ed(m, id) local client = m.clients[id] for inp in client.rx_fds:iter() do for line in inp:lines() do client.rx_queue:put({ client.rx_id, "line", line }) end inp:close() client.rx_queue:put({ client.rx_id, "quit" }) end end function mom.handle_to_ed(m, id) local client = m.clients[id] local out = client.tx_fds:get() while true do local line = client.tx_queue:get() while true do local good, err = out:write(line .. "\n") if good then break end out:close() out = client.tx_fds:get() end end end function mom.tend(m) signal.block(signal.SIGCHLD) local l = signal.listen(signal.SIGCHLD) while true do l:wait() local pid, status = wait.wait(-1, wait.WNOHANG | wait.WUNTRACED) if pid and status ~= "running" then local client_id = m.pids[pid] m.pids[pid] = nil m.clients[client_id].rx_queue:put({ "quit" }) end end end return mom