about summary refs log tree commit diff
path: root/mom.lua
diff options
context:
space:
mode:
Diffstat (limited to 'mom.lua')
-rw-r--r--mom.lua133
1 files changed, 133 insertions, 0 deletions
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