about summary refs log tree commit diff
path: root/mom.lua
blob: 3cc5696f76a0aeec19b8c81ea8bac55562e721c3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
-- 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