-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsys.lua
More file actions
352 lines (316 loc) · 14.8 KB
/
Copy pathsys.lua
File metadata and controls
352 lines (316 loc) · 14.8 KB
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
--- sys.lua -- the entire platform/syscall surface of lvi, quarantined.
--
-- ============================================================================
-- DECISION RECORD (why this file exists and why it looks like this)
-- ============================================================================
--
-- Runtime: LuaJIT.
-- We evaluated PUC Lua 5.4 + a C posix module (luaposix) against LuaJIT +
-- FFI. The deciding values were, in order: (1) extreme minimalism of *our
-- own* code, (2) a clean packaging story, (3) cross-UNIX portability
-- (Linux/macOS/BSD; WSL == Linux). LuaJIT wins because the entire C surface
-- we need lives in this one file with no build step and no external C
-- dependency: ship `luajit` + our .lua files, or bundle a single binary with
-- luastatic. Performance on scan/regex-heavy editor work is a free bonus.
--
-- Why FFI didn't become a portability nightmare:
-- The scary part of binding libc from Lua is termios -- a large struct with
-- divergent layouts and a wall of flag constants that differ per OS. We never
-- bind it. Raw mode is done by shelling out to stty (see raw_mode below),
-- which is pure Lua, zero ABI, and -- pleasingly -- is itself the project
-- philosophy in miniature: lean on the UNIX tools that already exist.
-- With termios gone, the only C surface left is socket + poll, which is tiny
-- and effectively frozen since the 1980s. The one real divergence is
-- `struct sockaddr_un` (macOS/BSD prepend a `sun_len` byte and use a shorter
-- path), handled by a single `ffi.os` branch below.
--
-- Why quarantine everything here:
-- The one genuine risk in betting on LuaJIT is its community-rolling release
-- model. By keeping every FFI call behind this module's small interface
-- (raw_mode/restore, listen/accept, poll, read/write/close, getuid/mkdir/
-- unlink), the PUC-vs-LuaJIT choice stays REVERSIBLE: if LuaJIT ever becomes
-- a liability, swap this single file for a luaposix-backed implementation and
-- nothing else in the codebase notices. This file is the only place in lvi
-- that is allowed to be unsafe or platform-specific.
--
-- Portability notes baked into the code below:
-- * errno values differ across OSes, so we never compare errno numbers.
-- Liveness of a stale socket is decided by try-connect, not by EADDRINUSE.
-- * poll() flag values (POLLIN/ERR/HUP/NVAL) and pollfd layout are uniform
-- across our targets; sockaddr_un is the only struct that branches.
-- * If poll() ever misbehaves on a target, select() belongs here too --
-- invisible to every caller.
--
-- Socket-path POLICY (where the socket lives) is deliberately NOT here. This
-- file only binds/listens on a path it is handed. Path construction
-- (XDG_RUNTIME_DIR -> TMPDIR -> /tmp, the lvi-$uid dir, the $wid view id, and
-- the 0700/ownership hardening) is the driver's job. getuid() and mkdir() are
-- exposed here because they are syscalls; composing them into a path is not.
-- ============================================================================
local ffi = require("ffi")
local bit = require("bit")
local C = ffi.C
ffi.cdef[[
/* LP64 is assumed on every target (Linux/macOS/BSD on 64-bit). */
typedef unsigned int socklen_t;
int getuid(void);
int getpid(void);
int isatty(int fd);
int setenv(const char *name, const char *value, int overwrite);
typedef void (*lvi_sighandler)(int);
lvi_sighandler signal(int signum, lvi_sighandler handler);
int raise(int sig);
int close(int fd);
int open(const char *pathname, int flags);
int dup2(int oldfd, int newfd);
long read(int fd, void *buf, unsigned long count); /* ssize_t/size_t */
long write(int fd, const void *buf, unsigned long count);
int unlink(const char *pathname);
int mkdir(const char *pathname, int mode); /* mode_t widened */
struct pollfd { int fd; short events; short revents; };
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
int socket(int domain, int type, int protocol);
int bind(int sockfd, const void *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, void *addr, socklen_t *addrlen);
int connect(int sockfd, const void *addr, socklen_t addrlen);
struct winsize { unsigned short ws_row, ws_col, ws_xpixel, ws_ypixel; };
int ioctl(int fd, unsigned long request, void *arg);
int fcntl(int fd, int cmd, ...);
]]
-- sockaddr_un: the one struct that diverges. AF_UNIX == 1 and SOCK_STREAM == 1
-- on all targets, so only the layout branches.
if ffi.os == "OSX" or ffi.os == "BSD" then
ffi.cdef[[
struct sockaddr_un { unsigned char sun_len; unsigned char sun_family; char sun_path[104]; };
]]
else -- Linux (and WSL)
ffi.cdef[[
struct sockaddr_un { unsigned short sun_family; char sun_path[108]; };
]]
end
local M = {}
-- Constants (uniform across Linux/macOS/BSD).
local AF_UNIX = 1
local SOCK_STREAM = 1
M.POLLIN = 0x01
M.POLLOUT = 0x04
M.POLLERR = 0x08
M.POLLHUP = 0x10
M.POLLNVAL= 0x20
-- Non-blocking I/O: O_NONBLOCK and EAGAIN are per-OS, handled like SIGTSTP /
-- TIOCGWINSZ below -- one constant per branch. F_GETFL/F_SETFL are uniform.
local F_GETFL, F_SETFL = 3, 4
local O_NONBLOCK = (ffi.os == "Linux") and 0x800 or 0x4
local EAGAIN = (ffi.os == "Linux") and 11 or 35 -- == EWOULDBLOCK on all targets
--- Identity / filesystem primitives (syscalls; path policy lives in the driver).
function M.getuid() return tonumber(C.getuid()) end
function M.getpid() return tonumber(C.getpid()) end
function M.isatty(fd) return C.isatty(fd) == 1 end
-- Set an environment variable in this process, so spawned children (os.execute
-- / io.popen) inherit it -- e.g. LVI_WID/LVI_SOCK so a picker can call back.
function M.setenv(name, value) C.setenv(name, tostring(value), 1) end
-- Ignore SIGPIPE so writing a reply to a client that already disconnected (e.g.
-- a --detach/fire-and-forget client) returns an error instead of killing us.
function M.ignore_sigpipe() C.signal(13, ffi.cast("lvi_sighandler", 1)) end -- SIGPIPE -> SIG_IGN
-- Suspend this process (Ctrl-Z / job control). Execution resumes here on `fg`.
-- SIGTSTP is the one number that diverges (Linux 20 vs BSD/macOS 18).
local SIGTSTP = (ffi.os == "Linux") and 20 or 18
function M.suspend() C.raise(SIGTSTP) end
-- Terminal size of fd (default stdout). struct winsize is uniform; only the
-- request number diverges (Linux vs the BSD _IOR encoding macOS/*BSD share).
local TIOCGWINSZ = (ffi.os == "Linux") and 0x5413 or 0x40087468
function M.winsize(fd)
local ws = ffi.new("struct winsize")
if C.ioctl(fd or 1, TIOCGWINSZ, ws) ~= 0 then return nil end
return ws.ws_row, ws.ws_col
end
function M.unlink(path) return C.unlink(path) == 0 end
--- Create a directory. Returns true on success. EEXIST and friends surface as
--- false; the driver is responsible for the lstat/owner 0700 verification when
--- it falls back to a shared /tmp.
function M.mkdir(path, mode) return C.mkdir(path, mode or 0x1c0) == 0 end -- 0700
-- Shell-quote one word for the /bin/sh one-liners below (and path.lua's reap).
local function shq(s) return "'" .. tostring(s):gsub("'", "'\\''") .. "'" end
M.shq = shq
--- Mirror src's mtime onto dst, creating dst (`touch -r`, POSIX). The mtime
--- primitive without stat(2), whose struct is as platform-divergent as termios
--- -- the same dodge as raw_mode's stty (see the decision record above). Fails
--- silently when src does not exist (dst is then not created/updated).
function M.stamp(dst, src)
os.execute(("touch -r %s %s 2>/dev/null"):format(shq(src), shq(dst)))
end
--- True when file `a` is strictly newer than `b` (POSIX `test -nt`; also true
--- when a exists and b does not -- which reads correctly for our caller: a
--- file that appeared after a failed read-stamp WAS changed under us).
function M.newer(a, b)
return os.execute(("[ %s -nt %s ]"):format(shq(a), shq(b))) == 0
end
--- Build a filled-in sockaddr_un for `path`. Internal.
local function sockaddr(path)
assert(#path < 104, "socket path too long: " .. path)
local addr = ffi.new("struct sockaddr_un")
addr.sun_family = AF_UNIX
if ffi.os == "OSX" or ffi.os == "BSD" then
addr.sun_len = ffi.sizeof("struct sockaddr_un")
end
ffi.copy(addr.sun_path, path)
return addr, ffi.sizeof("struct sockaddr_un")
end
--- Listen on a Unix-domain socket at `path`. Handles crash leftovers without
--- comparing errno: probe the path; if someone answers, the view is alive and
--- we refuse; otherwise the file is stale, so unlink and bind. Returns the
--- listening fd.
function M.listen(path, backlog)
local addr, len = sockaddr(path)
-- Liveness probe (try-connect, not errno).
local probe = C.socket(AF_UNIX, SOCK_STREAM, 0)
if probe >= 0 then
local alive = C.connect(probe, addr, len) == 0
C.close(probe)
if alive then error("lvi already listening at " .. path) end
end
C.unlink(path) -- remove stale file if present (no-op otherwise)
local fd = C.socket(AF_UNIX, SOCK_STREAM, 0)
if fd < 0 then error("socket() failed for " .. path) end
if C.bind(fd, addr, len) ~= 0 then C.close(fd); error("bind() failed for " .. path) end
-- Generous backlog: a tty shell-out (:!, :sh, a picker) freezes the poll
-- loop, and hook children keep connecting back meanwhile. Once the backlog
-- fills, further connect()s BLOCK (hanging even detached clients and
-- lvi -l's liveness probes), so cheap headroom here is what keeps a long
-- :sh session drivable. Observed live at the old backlog of 8.
if C.listen(fd, backlog or 64) ~= 0 then C.close(fd); error("listen() failed for " .. path) end
return fd
end
--- Accept one pending connection on a listening fd. Returns the connection fd,
--- or nil if none is ready. Client address is discarded (path is the selector).
function M.accept(fd)
local conn = C.accept(fd, nil, nil)
if conn < 0 then return nil end
return conn
end
--- Connect to a Unix-domain socket at `path`. Returns the connected fd, or nil
--- on failure (e.g. nothing listening). The client side of M.listen.
function M.connect(path)
local addr, len = sockaddr(path)
local fd = C.socket(AF_UNIX, SOCK_STREAM, 0)
if fd < 0 then return nil end
if C.connect(fd, addr, len) ~= 0 then C.close(fd); return nil end
return fd
end
--- Poll a list of fds for readability (and error/hangup); fds listed in the
--- optional `wfds` array are additionally watched for writability (an fd may
--- appear in both). Returns a table mapping ready fd -> revents bitmask; an
--- empty table on timeout; nil on error (EINTR after a suspend/resume or a
--- signal), so a caller with timeout-triggered work does not run it on
--- interruptions.
function M.poll(fds, timeout_ms, wfds)
local want = {}
for _, fd in ipairs(fds) do want[fd] = M.POLLIN end
for _, fd in ipairs(wfds or {}) do want[fd] = bit.bor(want[fd] or 0, M.POLLOUT) end
local list = {}
for fd in pairs(want) do list[#list + 1] = fd end
local n = #list
local pfds = ffi.new("struct pollfd[?]", n)
for i = 1, n do
pfds[i - 1].fd = list[i]
pfds[i - 1].events = want[list[i]]
pfds[i - 1].revents = 0
end
local rc = C.poll(pfds, n, timeout_ms or -1)
if rc < 0 then return nil end -- interrupted/error: NOT a timeout
local ready = {}
if rc == 0 then return ready end -- timeout
for i = 1, n do
local re = pfds[i - 1].revents
if re ~= 0 then ready[pfds[i - 1].fd] = re end
end
return ready
end
--- Read up to `n` bytes from `fd`. Returns a string, or nil on EOF/error. A
--- would-block read on a non-blocking fd (a spurious poll wakeup) returns ""
--- -- no data, but not a reason to hang up.
local RBUF_MAX = 4096
function M.read(fd, n)
n = n or RBUF_MAX
local buf = ffi.new("char[?]", n)
local r = tonumber(C.read(fd, buf, n))
if r > 0 then return ffi.string(buf, r) end
if r < 0 and ffi.errno() == EAGAIN then return "" end
return nil
end
--- Put an fd into non-blocking mode. Returns true on success. fcntl is
--- variadic, so the int arg must be cast explicitly (the FFI would otherwise
--- promote a Lua number to double).
function M.set_nonblock(fd)
local fl = C.fcntl(fd, F_GETFL, ffi.new("int", 0))
if fl < 0 then return false end
return C.fcntl(fd, F_SETFL, ffi.new("int", bit.bor(fl, O_NONBLOCK))) >= 0
end
--- One write attempt from byte offset `off` (0-based), for non-blocking fds.
--- Returns bytes written (0 == would-block: no progress, not fatal), or nil on
--- a real error. Unlike M.write it never loops -- the caller owns the buffer
--- and retries when poll() reports the fd writable again.
function M.write1(fd, s, off)
off = off or 0
local p = ffi.cast("const char *", s)
local w = tonumber(C.write(fd, p + off, #s - off))
if w >= 0 then return w end
if ffi.errno() == EAGAIN then return 0 end
return nil
end
--- Write the whole string `s` to `fd`, looping over partial writes. Returns
--- true on success, nil on error.
function M.write(fd, s)
local p, len = ffi.cast("const char *", s), #s
local off = 0
while off < len do
local w = tonumber(C.write(fd, p + off, len - off))
if w <= 0 then return nil end
off = off + w
end
return true
end
function M.close(fd) return C.close(fd) == 0 end
--- Read fd to EOF and return all its bytes (blocking). Used to slurp piped
--- stdin for `lvi -` before fd 0 is repurposed as the keyboard. Since nothing
--- has touched Lua's stdio for this fd, raw reads see the whole stream.
function M.slurp(fd)
local parts = {}
while true do
local chunk = M.read(fd) -- nil at EOF; "" only on EAGAIN (blocking here)
if not chunk then break end
if chunk ~= "" then parts[#parts + 1] = chunk end
end
return table.concat(parts)
end
--- Point fd 0 at the controlling terminal (/dev/tty). The keyboard path is
--- hardwired to fd 0 -- isatty(0), poll(0), read(0), and stty (which inherits
--- fd 0) -- so after stdin was consumed as a pipe (`lvi -`), swapping the tty
--- back onto fd 0 makes all of that work unchanged, no fd threading downstream.
--- Returns true on success; false when there is no controlling terminal (e.g.
--- `lvi - >out` in a pipeline, or a detached session), leaving fd 0 as-is so the
--- caller falls through to headless mode. O_RDWR is 0x2 on every target.
function M.reopen_stdin_from_tty()
local fd = C.open("/dev/tty", 2)
if fd < 0 then return false end
local ok = C.dup2(fd, 0) >= 0
C.close(fd)
return ok
end
--- Raw mode via stty -- no termios struct in our tree. Save the current
--- settings (stty -g returns an opaque, restorable blob), then drop canonical
--- mode and echo. restore() replays the saved blob. Both shell out once, at
--- startup/shutdown; the cost is irrelevant and the philosophy is on-brand.
function M.raw_mode()
local f = io.popen("stty -g 2>/dev/null")
local saved = f and f:read("*l") or nil
f:close()
os.execute("stty raw -echo 2>/dev/null")
return saved -- hand this back to restore()
end
function M.restore(saved)
if saved then os.execute("stty " .. saved .. " 2>/dev/null")
else os.execute("stty sane 2>/dev/null") end
end
return M