From 55e859929479050e2a3b71a614a388f3b6c2a197 Mon Sep 17 00:00:00 2001 From: equa Date: Tue, 8 Mar 2022 22:04:55 +0000 Subject: basic editor watcher, soon to be several editors --- bot.lua | 39 ++++++++++++++----- fs.lua | 19 +++++++--- mom.lua | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 14 deletions(-) create mode 100644 mom.lua diff --git a/bot.lua b/bot.lua index f957fd3..ac0a10c 100644 --- a/bot.lua +++ b/bot.lua @@ -8,6 +8,7 @@ local dkjson = require("dkjson") local fs = require("fs") local fifo = require("fifo") +local mom = require("mom") function irc_dump(fifo, socket) for data in fifo:iter() do @@ -128,6 +129,7 @@ function irc_connect(loop, host, config) local state = { queue = fifo.new(), nick = config.nick or "ed1bot", + command = config.command, } -- populate channels @@ -169,19 +171,38 @@ function irc_connect(loop, host, config) end end) - state.to_ed, state.from_ed = fifo.new(), fifo.new() - handle_ed(loop, state.to_ed, state.from_ed, { "red" }) + -- state.to_ed, state.from_ed = fifo.new(), fifo.new() + -- handle_ed(loop, state.to_ed, state.from_ed, { "red" }) + state.mom = mom.new(loop) + state.to_ed, state.from_ed = select(2, state.mom:create(state.command)) loop:wrap(function() while true do local line = fifo.get(state.from_ed) - fifo.put(state.queue, { - command = "PRIVMSG", - params = { - state.channel, - line - } - }) + if type(line) == "string" then + fifo.put(state.queue, { + command = "PRIVMSG", + params = { + state.channel, + line + } + }) + else -- TODO other possible types + fifo.put(state.queue, { + command = "PART", + params = { + state.channel + }, + }) + -- be annoying + cqueues.poll(10.0) + fifo.put(state.queue, { + command = "JOIN", + params = { + state.channel + }, + }) + end end end) diff --git a/fs.lua b/fs.lua index 2e7638f..153e611 100644 --- a/fs.lua +++ b/fs.lua @@ -1,5 +1,7 @@ -- cqueue filesystem with luaposix -- TODO: closing files +-- TODO: error handling. right now it *always* returns errno instead of +-- throwing it, which may be what we want. code depends on this local unistd = require("posix.unistd") local cqueues = require("cqueues") local dkjson = require("dkjson") @@ -85,8 +87,9 @@ function try_read(buffer, fd, max) local data, _, errno = unistd.read(fd, max) if not data then return nil, errno end + if #data == 0 then return nil end - table.insert(buffer, { data = data, index = 1}) + table.insert(buffer, { data = data, index = 1 }) return #data end @@ -96,7 +99,7 @@ function file.read(f, what) if (type(what) == "number" and what > 0) then while buffer_length(f.rbuf) < what do - if try_read(f.rbuf, f.pollfd, what - buffer_length(f.rbuf)) == 0 then + if not try_read(f.rbuf, f.pollfd, what - buffer_length(f.rbuf)) then break end end @@ -104,13 +107,14 @@ function file.read(f, what) return buffer_get(f.rbuf, math.min(what, buffer_length(f.rbuf))) elseif (type(what) == "number" and what < 0) then while buffer_length(f.rbuf) == 0 do - if try_read(f.rbuf, f.pollfd, 0 - what) == 0 then return nil end + if not try_read(f.rbuf, f.pollfd, 0 - what) then return nil end end return buffer_get(f.rbuf, math.min(0 - what, buffer_length(f.rbuf))) elseif what == "*l" or what == "*L" then while not buffer_char_index(f.rbuf, "\n") do -- TODO: constantize this - if try_read(f.rbuf, f.pollfd, 1024) == 0 then + local x = try_read(f.rbuf, f.pollfd, 1024) + if not x then break end end @@ -125,6 +129,7 @@ function file.read(f, what) "\n", (what == "*l" and "" or "\n") ) + return ret else -- TODO @@ -140,7 +145,7 @@ function file.write(f, data) while index <= #data do cqueues.poll({ pollfd = f.pollfd, events = function () return "w" end }) local nb, _, errno = unistd.write(f.pollfd, string.sub(data, index)) - if nb >= 0 then + if nb then index = index + nb else return nil, errno -- TODO? @@ -150,4 +155,8 @@ function file.write(f, data) return f end +function file.close(f) + cqueues.cancel(f.pollfd) +end + return file diff --git a/mom.lua b/mom.lua new file mode 100644 index 0000000..86bfacc --- /dev/null +++ b/mom.lua @@ -0,0 +1,133 @@ +-- 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 cqueues = require("cqueues") +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 +function mom.create(m, command) + local id = {} + + m.clients[id] = { + command = command, + tx_queue = fifo.new(), + rx_queue = fifo.new(), + 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 + +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.execp(m.clients[id].command[1], { select(2, table.unpack(m.clients[id].command)) }) -- meh + os.exit(100) -- TODO + 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(line) + end + + inp:close() + client.rx_queue:put({ "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 + client_id = m.pids[pid] + m.pids[pid] = nil + -- TODO: only restart sometimes + mom.start_process(m, client_id) + m.clients[client_id].rx_queue:put({ "quit" }) + end + end +end + +return mom -- cgit 1.3.0-6-gf8a5