summaryrefslogtreecommitdiff
path: root/src/nested.zig
blob: eb01b2e067ceb6b8f588541d731a4d48c2ef0916 (plain) (blame)
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
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
//! A pardes launched inside a pardes hands its file to the outer one.
//!
//! Every top-level instance listens on `<dir>/pardes-<pid>.sock`, where `<dir>`
//! is `$XDG_RUNTIME_DIR` or, when the session has none, `~/.local/state/pardes`
//! created 0700. NOT /tmp: this socket takes a command line and runs it, and a
//! world-writable directory means both that somebody else can plant a listener
//! at a pid we are about to guess and that a file they planted under the sticky
//! bit cannot be unlinked, so bind fails and the feature goes quietly off.
//!
//! An instance that finds an ancestor process running the same executable
//! resolves its positional argument, writes ONE line — `Look /abs/path` — to
//! that ancestor's socket and exits silently; the outer pardes runs the line
//! through executeBuiltinLine and opens a pane for it. The wire format is a
//! builtin command line because that is a language pardes already speaks: no
//! serialization, nothing to version. The receive side still filters it down
//! to `Look `, because executeBuiltinLine dispatches ANY builtin and this
//! socket sits at a path anyone can derive from a pid — `Exec …` arriving here
//! is not something this protocol is allowed to say.
//!
//! Linux and darwin. The two differ in every primitive this needs and in none
//! of the design: /proc against libproc for the ancestor walk, SOCK_CLOEXEC
//! and accept4 against a plain socket plus an fcntl, and a `sun_path` of 108
//! bytes against one of 104 — which is why no buffer below spells a number,
//! they are all sized from the field itself. Anywhere else the walk returns
//! null and a pardes inside a pardes opens a second session, as before.
//!
//! macOS also has a third executable in the family: the app bundle. Its binary
//! is named `pardes`, like the tty frontend, wherever the bundle is installed.
//! Executable identity therefore comes from the family name rather than its
//! path — see samePardesExecutable.
const std = @import("std");
const builtin = @import("builtin");
const libc = std.c;

// std.c has getenv but neither setter; the tests below need both
extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
extern "c" fn unsetenv(name: [*:0]const u8) c_int;

const darwin = switch (builtin.os.tag) {
    .macos, .ios, .tvos, .watchos, .visionos => true,
    else => false,
};

/// This module is only as portable as its two ingredients: a way to name the
/// executable and parent of an arbitrary pid, and unix sockets.
const supported = builtin.os.tag == .linux or darwin;

/// `sun_path` is 108 bytes on linux and 104 on darwin, and it is the hard
/// limit on this whole feature: a path that does not fit is not a socket
/// address, it is a truncated one pointing somewhere else. Taken from the
/// struct so that the buffers, the fit checks and the memcpy below cannot
/// disagree with the kernel or with each other.
const sun_path_len = @typeInfo(@FieldType(libc.sockaddr.un, "path")).array.len;

/// libproc, darwin's answer to /proc. `proc_pidpath` is readlink of
/// `/proc/<pid>/exe`; `PROC_PIDTBSDINFO` carries the parent pid that linux
/// spells `PPid:`. Both are same-uid readable, which is the only permission
/// an ancestor walk through one's own processes needs.
const PROC_PIDTBSDINFO: c_int = 3;
const proc_bsdinfo = extern struct {
    flags: u32,
    status: u32,
    xstatus: u32,
    pid: u32,
    ppid: u32,
    /// uids, gids, comm, name, the tty and the start time: filled by the
    /// kernel and unread here, but the call fails unless the buffer is the
    /// whole 136-byte record.
    rest: [116]u8,
};
extern "c" fn proc_pidpath(pid: c_int, buffer: *anyopaque, buffersize: u32) c_int;
extern "c" fn proc_pidinfo(pid: c_int, flavor: c_int, arg: u64, buffer: *anyopaque, buffersize: c_int) c_int;

/// Linux opens sockets CLOEXEC in one call; darwin has to set it afterwards.
/// The gap is a race only against a fork on another thread, and both callers
/// are past that: `listen` runs before the first pane exists, and `acceptLine`
/// runs on a thread of its own long after spawning has settled.
fn setCloexec(fd: c_int) void {
    const FD_CLOEXEC: c_int = 1;
    _ = libc.fcntl(fd, libc.F.SETFD, FD_CLOEXEC);
}

/// The longest command line this protocol carries or accepts. `Look ` plus a
/// PATH_MAX path fits with room over; anything longer cannot have come from
/// the client and is dropped rather than truncated into a different command.
pub const max_line = 4200;

/// Where the sockets live. `$XDG_RUNTIME_DIR` first — a per-user 0700 tmpfs
/// the login session already cleans up — else `~/.local/state/pardes`, which
/// is per-user for the same reason a home directory is. Asked by the client
/// (to derive the path), by the listener (to create and vet it) and by the
/// sweeper (to scan it), so it is written once.
fn socketDir(buf: *[sun_path_len:0]u8) ?[:0]const u8 {
    if (libc.getenv("XDG_RUNTIME_DIR")) |x|
        return std.fmt.bufPrintSentinel(buf, "{s}", .{std.mem.span(x)}, 0) catch null;
    const home = libc.getenv("HOME") orelse return null;
    return std.fmt.bufPrintSentinel(buf, "{s}/.local/state/pardes", .{std.mem.span(home)}, 0) catch null;
}

/// `<dir>/pardes-<pid>.sock`. `<pid>` is the LISTENING instance's own pid, so
/// two pardes never collide and a nested child derives the exact path from the
/// ancestor pid its tree walk found. The buffer is sun_path-sized: a longer
/// path is not a socket address at all.
pub fn socketPath(buf: *[sun_path_len]u8, pid: libc.pid_t) ?[:0]const u8 {
    var dir_buf: [sun_path_len:0]u8 = undefined;
    const dir = socketDir(&dir_buf) orelse return null;
    // unsigned: {d} prints a leading '+' for a positive SIGNED int
    return std.fmt.bufPrintSentinel(buf, "{s}/pardes-{d}.sock", .{ dir, @as(u32, @intCast(pid)) }, 0) catch null;
}

/// Normalize the kernel suffix left on a running executable after its file is
/// replaced. `zig build` does this routinely while an outer session is live.
fn stripDeleted(link: []const u8) []const u8 {
    const suffix = " (deleted)";
    return if (std.mem.endsWith(u8, link, suffix)) link[0 .. link.len - suffix.len] else link;
}

/// The tty, SDL and macOS builds are sibling frontends of the same program.
/// Their installed names differ only by `-gui` (and, for cross builds, share
/// the same `-os-arch` tail), or not at all when one of them is the app bundle
/// — so any of them must recognise any other as an outer pardes. Paths are
/// deliberately ignored: the GUI may be installed system-wide while the tty
/// frontend is installed in the user's bin directory.
fn samePardesExecutable(a_raw: []const u8, b_raw: []const u8) bool {
    const a = stripDeleted(a_raw);
    const b = stripDeleted(b_raw);
    return sameFamily(std.fs.path.basename(a), std.fs.path.basename(b));
}

/// What is left of a family name after the frontend part: `` for `pardes` and
/// `pardes-gui`, `-linux-aarch64` for the cross-built spellings of both. Null
/// when the name is not in the family at all — `not-pardes`, `pardesfoo`, and
/// helper binaries such as `pardes-snap` are other programs.
fn familyTail(name: []const u8) ?[]const u8 {
    const rest = if (std.mem.startsWith(u8, name, "pardes-gui"))
        name["pardes-gui".len..]
    else if (std.mem.startsWith(u8, name, "pardes"))
        name["pardes".len..]
    else
        return null;
    if (rest.len == 0) return rest;
    // Build names have exactly `-os-arch` after the frontend. Validating both
    // fields keeps sibling installs flexible without mistaking pardes-snap,
    // pardes-perf, and the other helper executables for editor frontends.
    if (rest[0] != '-') return null;
    var fields = std.mem.splitScalar(u8, rest[1..], '-');
    const os = fields.next() orelse return null;
    const arch = fields.next() orelse return null;
    if (fields.next() != null) return null;
    if (std.meta.stringToEnum(std.Target.Os.Tag, os) == null) return null;
    if (std.meta.stringToEnum(std.Target.Cpu.Arch, arch) == null) return null;
    return rest;
}

/// Two executable names in the same family. The tails have to agree — a linux
/// binary and an x86_64 one are two builds — unless one of them has no tail at
/// all, which is the untagged name the default build and, unavoidably, the app
/// bundle both produce: CFBundleExecutable is a fixed string, so the bundled
/// copy of `pardes-macos-aarch64` is called `pardes` and nothing in the name
/// records what it was. A foreign-arch ancestor cannot be running here.
fn sameFamily(a: []const u8, b: []const u8) bool {
    const a_tail = familyTail(a) orelse return false;
    const b_tail = familyTail(b) orelse return false;
    if (std.mem.eql(u8, a, b)) return true;
    return a_tail.len == 0 or b_tail.len == 0 or std.mem.eql(u8, a_tail, b_tail);
}

/// The `PPid:` field of a /proc/<pid>/status blob. Deliberately NOT field 4 of
/// /proc/<pid>/stat: that field is positional after `comm`, and a comm may
/// contain spaces and parentheses — a process named `sh (a b)` shifts every
/// field after it and the parse silently reads the wrong number.
fn parsePPid(status: []const u8) ?libc.pid_t {
    var lines = std.mem.splitScalar(u8, status, '\n');
    while (lines.next()) |line| {
        if (!std.mem.startsWith(u8, line, "PPid:")) continue;
        return std.fmt.parseInt(libc.pid_t, std.mem.trim(u8, line["PPid:".len..], " \t\r"), 10) catch null;
    }
    return null;
}

/// The pid in a `pardes-<pid>.sock` filename, for the startup sweep. Strictly
/// digits: parseInt alone would take `pardes-+7.sock` and `pardes--7.sock`,
/// and the sweep unlinks what this answers about.
fn sweepPid(name: []const u8) ?libc.pid_t {
    if (!std.mem.startsWith(u8, name, "pardes-") or !std.mem.endsWith(u8, name, ".sock")) return null;
    const digits = name["pardes-".len .. name.len - ".sock".len];
    if (digits.len == 0) return null;
    for (digits) |ch| if (!std.ascii.isDigit(ch)) return null;
    return std.fmt.parseInt(libc.pid_t, digits, 10) catch null;
}

/// Name the executable behind a pid, the way this OS spells it.
fn exeOf(pid: libc.pid_t, buf: *[4096]u8) ?[]const u8 {
    switch (builtin.os.tag) {
        .linux => {
            var name: [64:0]u8 = undefined;
            const link = std.fmt.bufPrintSentinel(&name, "/proc/{d}/exe", .{@as(u32, @intCast(pid))}, 0) catch return null;
            const n = libc.readlink(link, buf, buf.len);
            if (n <= 0) return null;
            return buf[0..@intCast(n)];
        },
        else => {
            if (comptime !darwin) return null;
            // Documented to want a PROC_PIDPATHINFO_MAXSIZE buffer, which is
            // exactly this one, and to return the length it wrote.
            const n = proc_pidpath(pid, buf, @intCast(buf.len));
            if (n <= 0) return null;
            return buf[0..@intCast(n)];
        },
    }
}

/// ...and its parent.
fn parentOf(pid: libc.pid_t) ?libc.pid_t {
    switch (builtin.os.tag) {
        .linux => {
            var name: [64:0]u8 = undefined;
            var buf: [4096]u8 = undefined;
            const status = std.fmt.bufPrintSentinel(&name, "/proc/{d}/status", .{@as(u32, @intCast(pid))}, 0) catch return null;
            const fd = libc.open(status, .{ .ACCMODE = .RDONLY });
            if (fd < 0) return null;
            const got = libc.read(fd, &buf, buf.len);
            _ = libc.close(fd);
            if (got <= 0) return null;
            return parsePPid(buf[0..@intCast(got)]);
        },
        else => {
            if (comptime !darwin) return null;
            var info: proc_bsdinfo = undefined;
            const n = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, @sizeOf(proc_bsdinfo));
            // A short answer means the record this was compiled against is not
            // the one the kernel filled, and `ppid` is then some other field.
            if (n < @as(c_int, @sizeOf(proc_bsdinfo))) return null;
            return @intCast(info.ppid);
        },
    }
}

/// The pid of the nearest ancestor running a pardes executable, or null.
/// Identity is that ancestor's executable path against our own; the tty, SDL
/// and app-bundle siblings also match when they were installed together. A
/// name alone would call every unrelated `pardes` ancestor an outer instance.
/// The hop cap is not for the process tree, which cannot loop, but because the
/// walk is driven by numbers read out of the kernel and should not be able to
/// spin on a surprising one.
pub fn outer() ?libc.pid_t {
    if (comptime !supported) return null;
    var self_buf: [4096]u8 = undefined;
    const self_exe = exeOf(libc.getpid(), &self_buf) orelse return null;
    // A process harness may deliberately launch a fresh top-level pardes from
    // inside another one. Its pid is a process-tree boundary, not an opt-out
    // for the new session itself: pane shells below the child still detect it.
    // This is what lets the snapshot harness exercise nested launches while
    // the harness happens to be running in a real pardes pane.
    const boundary = if (libc.getenv("PARDES_NESTED_BOUNDARY_PID")) |raw|
        std.fmt.parseInt(libc.pid_t, std.mem.span(raw), 10) catch 0
    else
        0;
    var pid = libc.getppid();
    var hops: usize = 0;
    while (pid > 1 and hops < 64) : (hops += 1) {
        if (pid == boundary) return null;
        var buf: [4096]u8 = undefined;
        if (exeOf(pid, &buf)) |exe| if (samePardesExecutable(exe, self_exe)) return pid;
        pid = parentOf(pid) orelse return null;
    }
    return null;
}

/// Hand `Look <path>[:<line>]` to the pardes listening as `pid` and say
/// whether it landed. False for every failure — no socket file, nobody
/// accepting, a path that does not fit — because an outer instance that
/// cannot be reached (an older build, a stale path) must never cost the
/// caller its own launch. Writes and returns: the answer is a pane appearing
/// on someone else's screen, and there is nothing to wait for.
pub fn sendLook(pid: libc.pid_t, path: []const u8, line: usize) bool {
    if (comptime !supported) return false;
    // The protocol is one line, so a path with a line break IN it says
    // something else entirely: `we\nird.txt` arrived as `Look .../we` and the
    // outer instance opened a different file that happened to exist. \r goes
    // too — the receive side trims a trailing one. Unsendable, not escaped:
    // the caller falls through and opens the file in its own session.
    if (std.mem.indexOfAny(u8, path, "\r\n") != null) return false;
    var cmd_buf: [max_line]u8 = undefined;
    const cmd = (if (line > 0)
        std.fmt.bufPrint(&cmd_buf, "Look {s}:{d}\n", .{ path, line })
    else
        std.fmt.bufPrint(&cmd_buf, "Look {s}\n", .{path})) catch return false;

    // sun_path-sized by construction, so `sock` cannot be longer than the
    // field it is about to be copied into — socketPath returns null instead.
    var path_buf: [sun_path_len]u8 = undefined;
    const sock = socketPath(&path_buf, pid) orelse return false;
    var addr: libc.sockaddr.un = .{ .path = @splat(0) };
    @memcpy(addr.path[0 .. sock.len + 1], sock[0 .. sock.len + 1]);
    const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0);
    if (fd < 0) return false;
    setCloexec(fd);
    defer _ = libc.close(fd);
    if (libc.connect(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) return false;
    var off: usize = 0;
    while (off < cmd.len) {
        const n = libc.write(fd, cmd.ptr + off, cmd.len - off);
        if (n < 0) {
            if (libc.errno(n) == .INTR) continue;
            return false;
        }
        if (n == 0) return false;
        off += @intCast(n);
    }
    return true;
}

/// The three things ensureSocketDir has to know about a path, from whichever
/// call the platform actually offers. Darwin has fstatat and no statx; on
/// linux std.c.fstatat is `void` — glibc hides it behind a versioned symbol
/// std cannot name — so linux asks statx for the same three fields. Both
/// spellings refuse to follow a symlink, which is the point of asking.
const DirFacts = struct { mode: u32, uid: libc.uid_t };

fn statNoFollow(path: [:0]const u8) ?DirFacts {
    if (comptime darwin) {
        var st: libc.Stat = undefined;
        if (libc.fstatat(libc.AT.FDCWD, path, &st, libc.AT.SYMLINK_NOFOLLOW) != 0) return null;
        return .{ .mode = st.mode, .uid = st.uid };
    } else {
        const linux = std.os.linux;
        var stx: linux.Statx = undefined;
        const want: linux.STATX = .{ .TYPE = true, .MODE = true, .UID = true };
        if (libc.statx(linux.AT.FDCWD, path, linux.AT.SYMLINK_NOFOLLOW, want, &stx) != 0) return null;
        return .{ .mode = stx.mode, .uid = stx.uid };
    }
}

/// Create the socket directory if it is missing and refuse it unless it is a
/// directory WE own with nothing granted to group or other. A planted path is
/// the whole attack on a socket that runs commands, and $XDG_RUNTIME_DIR
/// passes this untouched (the login session already makes it 0700).
fn ensureSocketDir(dir: [:0]const u8) bool {
    // mkdir -p, because the HOME branch is three levels deep and a machine
    // without ~/.local/state would otherwise switch the feature off in
    // silence. Under $XDG_RUNTIME_DIR every prefix already exists and simply
    // EEXISTs, which is the ordinary case for the leaf too.
    var partial: [sun_path_len:0]u8 = undefined;
    @memcpy(partial[0 .. dir.len + 1], dir[0 .. dir.len + 1]);
    for (1..dir.len) |i| {
        if (dir[i] != '/') continue;
        partial[i] = 0;
        _ = libc.mkdir(partial[0..i :0], 0o700);
        partial[i] = '/';
    }
    _ = libc.mkdir(dir, 0o700);
    // A symlink where the directory should be is exactly the plant this
    // guards against, so the stat above it does not follow one.
    const st = statNoFollow(dir) orelse return false;
    const IFMT: u32 = 0o170000;
    const IFDIR: u32 = 0o040000;
    if (st.mode & IFMT != IFDIR) return false;
    if (st.uid != libc.getuid()) return false;
    return st.mode & 0o077 == 0;
}

/// Unlink the socket files of pardes processes that are gone. A pardes killed
/// rather than quit runs no defer, so its file outlives it; harmless by
/// construction (bind unlinks first, a client's connect is refused) but it is
/// our own litter and the snapshot suite alone leaves ~90 behind per run.
/// Bounded: one readdir of a directory only we write to, one kill(0) each.
fn sweep(dir: [:0]const u8) void {
    const d = libc.opendir(dir) orelse return;
    defer _ = libc.closedir(d);
    const me = libc.getpid();
    while (libc.readdir(d)) |ent| {
        const pid = sweepPid(std.mem.sliceTo(&ent.name, 0)) orelse continue;
        if (pid == me) continue;
        // 0 = alive; EPERM = alive and someone else's. Only ESRCH is a corpse.
        const rc = libc.kill(pid, @enumFromInt(0));
        if (rc == 0 or libc.errno(rc) != .SRCH) continue;
        var pbuf: [sun_path_len]u8 = undefined;
        _ = libc.unlink(socketPath(&pbuf, pid) orelse continue);
    }
}

/// Bind and listen so nested instances can find us; -1 if anything fails, and
/// a pardes without a socket is simply one whose children open their own UI.
/// The path is always this process's own, so nobody outside holds a buffer of
/// it — the shells each kept one and passed it back to be unlinked, which is a
/// way for the two spellings to go out of step and for no other reason.
///
/// CLOEXEC matters more here than on any other fd in the program: pane shells
/// are forked with forkpty and inherit everything open, and an orphaned bash
/// holding this one would keep the socket bound long after we exit — the same
/// shape as the inherited lock fd that once held a flock forever.
pub fn listen() c_int {
    if (comptime !supported) return -1;
    var dir_buf: [sun_path_len:0]u8 = undefined;
    const dir = socketDir(&dir_buf) orelse return -1;
    if (!ensureSocketDir(dir)) return -1;
    sweep(dir);
    // Fits by construction: socketPath writes into a sun_path-sized buffer and
    // returns null rather than a truncated address.
    var path_buf: [sun_path_len]u8 = undefined;
    const path = socketPath(&path_buf, libc.getpid()) orelse return -1;
    var addr: libc.sockaddr.un = .{ .path = @splat(0) };
    @memcpy(addr.path[0 .. path.len + 1], path[0 .. path.len + 1]);
    const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0);
    if (fd < 0) return -1;
    setCloexec(fd);
    _ = libc.unlink(path); // pid reuse: a dead pardes' file would EADDRINUSE forever
    if (libc.bind(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) {
        _ = libc.close(fd);
        return -1;
    }
    // Owner-only, and BEFORE listen(2), which is the moment anyone could
    // connect: the directory is already private, this is the second wall.
    _ = libc.chmod(path, 0o600);
    if (libc.listen(fd, 8) != 0) {
        _ = libc.close(fd);
        return -1;
    }
    return fd;
}

/// Close the listener and take its file away. Guarded on the fd rather than on
/// the path, so a bind that FAILED cannot unlink a path this process never
/// created; anything else is a no-op, which is what --nested and every
/// unsupported build hand it.
pub fn unlisten(fd: c_int) void {
    if (fd < 0) return;
    _ = libc.close(fd);
    var path_buf: [sun_path_len]u8 = undefined;
    if (socketPath(&path_buf, libc.getpid())) |path| _ = libc.unlink(path);
}

/// Block until a nested instance sends a `Look` line, and return it inside
/// `buf`. Null only when the listening fd itself is gone — teardown closed it,
/// or it was never a socket — because anything else (EMFILE, ECONNABORTED)
/// would otherwise kill the listener thread for the life of the process while
/// the socket stayed bound, and every later launch would exit 0 having done
/// nothing. Every accepted connection is CLOEXEC for the reason the listener
/// is.
pub fn acceptLine(fd: c_int, buf: []u8) ?[]const u8 {
    if (comptime !supported) return null;
    while (true) {
        const conn = libc.accept(fd, null, null);
        if (conn < 0) {
            switch (libc.errno(conn)) {
                .INTR => continue,
                // the fd went away or never was one: nothing will ever arrive
                .BADF, .INVAL, .NOTSOCK => return null,
                // transient. Sleep first: EMFILE persists until some other fd
                // is freed, and a bare `continue` would spin a core on it.
                else => {
                    var ts: libc.timespec = .{ .sec = 0, .nsec = 100 * std.time.ns_per_ms };
                    _ = libc.nanosleep(&ts, null);
                    continue;
                },
            }
        }
        defer _ = libc.close(conn);
        setCloexec(conn);
        // A peer that connects and says nothing must not hold the listener:
        // this is a serial accept loop, and one silent connection used to
        // block every later launch until it let go. The client writes its one
        // short line immediately, so a second is already generous.
        const tv: libc.timeval = .{ .sec = 1, .usec = 0 };
        _ = libc.setsockopt(conn, libc.SOL.SOCKET, libc.SO.RCVTIMEO, &tv, @sizeOf(libc.timeval));
        var len: usize = 0;
        while (len < buf.len) {
            const n = libc.read(conn, buf.ptr + len, buf.len - len);
            if (n < 0 and libc.errno(n) == .INTR) continue;
            if (n <= 0) break; // EOF, or the receive timeout expired
            len += @intCast(n);
            if (std.mem.indexOfScalar(u8, buf[0..len], '\n') != null) break;
        }
        const end = std.mem.indexOfScalar(u8, buf[0..len], '\n') orelse len;
        // a full buffer with no newline is an overlong line: drop it whole
        // rather than run its truncation as some other command
        if (end == buf.len) continue;
        const line = std.mem.trimEnd(u8, buf[0..end], "\r");
        // one verb (see the file header): this socket may open things, and
        // that is all it may do
        if (!std.mem.startsWith(u8, line, "Look ")) continue;
        return line;
    }
}

test "socket path: XDG first, then a private dir under HOME, never /tmp" {
    var buf: [sun_path_len]u8 = undefined;
    // The environment is process-wide and every test in this binary shares it.
    // The last case below reaches the "no directory at all" branch by blanking
    // both variables, and without this every later test ran without a HOME.
    var xdg_buf: [4096:0]u8 = undefined;
    var home_buf: [4096:0]u8 = undefined;
    const xdg0 = if (libc.getenv("XDG_RUNTIME_DIR")) |v| std.fmt.bufPrintSentinel(&xdg_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
    const home0 = if (libc.getenv("HOME")) |v| std.fmt.bufPrintSentinel(&home_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
    defer {
        if (xdg0) |v| {
            _ = setenv("XDG_RUNTIME_DIR", v, 1);
        } else _ = unsetenv("XDG_RUNTIME_DIR");
        if (home0) |v| {
            _ = setenv("HOME", v, 1);
        } else _ = unsetenv("HOME");
    }
    _ = setenv("XDG_RUNTIME_DIR", "/run/user/1000", 1);
    try std.testing.expectEqualStrings("/run/user/1000/pardes-4242.sock", socketPath(&buf, 4242).?);
    _ = unsetenv("XDG_RUNTIME_DIR");
    _ = setenv("HOME", "/home/who", 1);
    try std.testing.expectEqualStrings("/home/who/.local/state/pardes/pardes-4242.sock", socketPath(&buf, 4242).?);
    // sun_path holds the NUL, so a directory that fills it has no socket
    // address at all — say so instead of binding a truncated one. Sized from
    // the field: the limit is 108 on linux and 104 on darwin, and a literal
    // here would test nothing on whichever platform it was not written for.
    _ = setenv("XDG_RUNTIME_DIR", "/" ++ ("x" ** (sun_path_len - 8)), 1);
    try std.testing.expect(socketPath(&buf, 4242) == null);
    _ = unsetenv("XDG_RUNTIME_DIR");
    _ = unsetenv("HOME");
    try std.testing.expect(socketPath(&buf, 4242) == null);
}

test "a rebuilt binary still matches its own running instance" {
    // `zig build` under a live pardes: the outer's exe link gains the suffix,
    // the new process's does not, and before this the two stopped comparing
    // equal — every nested launch opened a second UI.
    try std.testing.expectEqualStrings("/usr/bin/pardes", stripDeleted("/usr/bin/pardes (deleted)"));
    try std.testing.expectEqualStrings("/usr/bin/pardes", stripDeleted("/usr/bin/pardes"));
    try std.testing.expectEqualStrings("", stripDeleted(" (deleted)"));
    // only a SUFFIX, and only the whole one
    try std.testing.expectEqualStrings("/x (deleted) y", stripDeleted("/x (deleted) y"));
    try std.testing.expectEqualStrings("/x (delete)", stripDeleted("/x (delete)"));
}

test "tty and GUI sibling executables recognise each other" {
    try std.testing.expect(samePardesExecutable(
        "/work/zig-out/bin/pardes",
        "/work/zig-out/bin/pardes-gui",
    ));
    try std.testing.expect(samePardesExecutable(
        "/work/zig-out/bin/pardes-linux-aarch64",
        "/work/zig-out/bin/pardes-gui-linux-aarch64 (deleted)",
    ));
    // Installation paths do not define the family. This is the ordinary
    // system-GUI/user-TTY pairing and the reason this comparison uses names.
    try std.testing.expect(samePardesExecutable(
        "/home/who/.local/bin/pardes",
        "/usr/bin/pardes-gui",
    ));
    try std.testing.expect(!samePardesExecutable(
        "/work/zig-out/bin/pardes-linux-aarch64",
        "/work/zig-out/bin/pardes-gui-linux-x86_64",
    ));
    try std.testing.expect(!samePardesExecutable(
        "/work/zig-out/bin/not-pardes",
        "/work/zig-out/bin/not-pardes-gui",
    ));
    try std.testing.expect(!samePardesExecutable(
        "/one/bin/not-pardes",
        "/two/bin/not-pardes",
    ));
    try std.testing.expect(!samePardesExecutable(
        "/work/zig-out/bin/pardes-snap",
        "/usr/bin/pardes",
    ));
}

test "the app bundle is in the same executable family" {
    // What `pardes foo.zig` typed into the bundle's own shell has to resolve:
    // the ancestor is zig-out/pardes.app/..., this process is zig-out/bin/...,
    // and nothing below zig-out is shared.
    try std.testing.expect(samePardesExecutable(
        "/work/zig-out/pardes.app/Contents/MacOS/pardes",
        "/work/zig-out/bin/pardes",
    ));
    // ...and the SDL sibling, which reaches it by the name rule instead.
    try std.testing.expect(samePardesExecutable(
        "/work/zig-out/pardes.app/Contents/MacOS/pardes",
        "/work/zig-out/bin/pardes-gui",
    ));
    // The case this machine actually produces: `zig build` installs the tty
    // binary under its os-arch tail, and the bundle carries the same build
    // under the one name CFBundleExecutable can spell.
    try std.testing.expect(samePardesExecutable(
        "/work/zig-out/pardes.app/Contents/MacOS/pardes",
        "/work/zig-out/bin/pardes-macos-aarch64",
    ));
    // Installation location does not matter here either.
    try std.testing.expect(samePardesExecutable(
        "/work/zig-out/pardes.app/Contents/MacOS/pardes",
        "/opt/zig-out/bin/pardes",
    ));
    try std.testing.expect(samePardesExecutable(
        "/work/zig-out/pardes/Contents/MacOS/pardes",
        "/work/zig-out/bin/pardes",
    ));
    // Nothing here may loosen the rule for two unrelated programs that merely
    // sit in a bin and a bundle of the same tree.
    try std.testing.expect(!samePardesExecutable(
        "/work/zig-out/other.app/Contents/MacOS/other",
        "/work/zig-out/bin/pardes",
    ));
}

test "the ancestor walk reads this process's own parent" {
    // The one thing a hand-written `struct proc_bsdinfo` gets wrong silently:
    // a field ordering that puts something else where ppid should be still
    // returns a plausible number. getppid knows the answer, so compare.
    //
    // Also the only check that libproc answers us at all — every caller of
    // outer() treats a failure as "no outer instance", which is exactly what a
    // permission problem would look like.
    if (comptime !supported) return error.SkipZigTest;
    try std.testing.expectEqual(libc.getppid(), parentOf(libc.getpid()).?);
    // ...and that the walk terminates rather than spinning on pid 1's parent.
    try std.testing.expect(parentOf(1) == null or parentOf(1).? <= 1);

    var buf: [4096]u8 = undefined;
    const exe = exeOf(libc.getpid(), &buf).?;
    try std.testing.expect(exe.len > 0);
    try std.testing.expect(exe[0] == '/');
    // The test binary is not a pardes, so the walk must come back empty rather
    // than matching some ancestor by accident.
    try std.testing.expect(outer() == null);
}

extern "c" fn mkdtemp(template: [*:0]u8) ?[*:0]u8;
extern "c" fn rmdir(path: [*:0]const u8) c_int;

test "a Look line survives the socket round trip" {
    // Everything the protocol actually does, against a real kernel: bind,
    // chmod, connect, write, accept, read, and the one-verb filter. The pure
    // functions above cannot see any of it, and every primitive here is
    // spelled differently on the two platforms this now supports.
    if (comptime !supported) return error.SkipZigTest;

    // A private directory of our own. Not the developer's real state dir: this
    // binds a socket named after a pid that is the TEST's, and sweep() unlinks
    // what it finds beside it.
    var tmpl: [64:0]u8 = undefined;
    _ = std.fmt.bufPrintSentinel(&tmpl, "/tmp/pardes-nested-XXXXXX", .{}, 0) catch unreachable;
    if (mkdtemp(&tmpl) == null) return error.SkipZigTest;
    const dir = std.mem.sliceTo(&tmpl, 0);
    defer _ = rmdir(tmpl[0..dir.len :0]);

    var xdg_buf: [4096:0]u8 = undefined;
    const xdg0 = if (libc.getenv("XDG_RUNTIME_DIR")) |v| std.fmt.bufPrintSentinel(&xdg_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
    defer {
        if (xdg0) |v| {
            _ = setenv("XDG_RUNTIME_DIR", v, 1);
        } else _ = unsetenv("XDG_RUNTIME_DIR");
    }
    _ = setenv("XDG_RUNTIME_DIR", tmpl[0..dir.len :0], 1);

    const fd = listen();
    try std.testing.expect(fd >= 0);
    defer unlisten(fd);

    // Sent to our own pid, which is the pid listen() named the socket after.
    // The client closes as it returns, and the line is already queued, so the
    // single-threaded accept below finds a complete connection waiting — no
    // thread and no timeout needed to prove the protocol.
    try std.testing.expect(sendLook(libc.getpid(), "/etc/hosts", 42));
    var buf: [max_line]u8 = undefined;
    try std.testing.expectEqualStrings("Look /etc/hosts:42", acceptLine(fd, &buf).?);

    // ...and without a line number, which is the directory and image case.
    try std.testing.expect(sendLook(libc.getpid(), "/etc", 0));
    try std.testing.expectEqualStrings("Look /etc", acceptLine(fd, &buf).?);

    // The socket takes one verb. Anything else is dropped rather than run, so
    // the next Look is what comes back — proving the filter skipped it without
    // dropping the connection after it.
    try std.testing.expect(writeLine(libc.getpid(), "Exec rm -rf /\n"));
    try std.testing.expect(sendLook(libc.getpid(), "/etc/passwd", 0));
    try std.testing.expectEqualStrings("Look /etc/passwd", acceptLine(fd, &buf).?);

    // A path that cannot be one line is not escaped, it is refused.
    try std.testing.expect(!sendLook(libc.getpid(), "/etc/ho\nsts", 0));
}

/// sendLook with the framing bypassed, so a test can put something on the wire
/// that the client would never send.
fn writeLine(pid: libc.pid_t, line: []const u8) bool {
    var path_buf: [sun_path_len]u8 = undefined;
    const sock = socketPath(&path_buf, pid) orelse return false;
    var addr: libc.sockaddr.un = .{ .path = @splat(0) };
    @memcpy(addr.path[0 .. sock.len + 1], sock[0 .. sock.len + 1]);
    const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0);
    if (fd < 0) return false;
    defer _ = libc.close(fd);
    if (libc.connect(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) return false;
    return libc.write(fd, line.ptr, line.len) == @as(isize, @intCast(line.len));
}

test "the sweep only recognises its own socket names" {
    try std.testing.expectEqual(@as(libc.pid_t, 7), sweepPid("pardes-7.sock").?);
    try std.testing.expectEqual(@as(libc.pid_t, 4194304), sweepPid("pardes-4194304.sock").?);
    try std.testing.expect(sweepPid("pardes-.sock") == null);
    try std.testing.expect(sweepPid("pardes-7.sockx") == null);
    try std.testing.expect(sweepPid("pardes-7") == null);
    try std.testing.expect(sweepPid("bus") == null);
    try std.testing.expect(sweepPid("pardes-osc133.bash") == null);
    // parseInt alone would take these, and the sweep UNLINKS what it answers
    try std.testing.expect(sweepPid("pardes-+7.sock") == null);
    try std.testing.expect(sweepPid("pardes--7.sock") == null);
    try std.testing.expect(sweepPid("pardes- 7.sock") == null);
}

test "PPid comes off the status field, not a comm-shifted stat line" {
    // the comm here contains a space AND parentheses — the exact shape that
    // breaks `field 4 of /proc/<pid>/stat`
    const status = "Name:\tsh (a b)\nUmask:\t0022\nState:\tS (sleeping)\n" ++
        "Tgid:\t1234\nNgid:\t0\nPid:\t1234\nPPid:\t991\nTracerPid:\t0\n";
    try std.testing.expectEqual(@as(libc.pid_t, 991), parsePPid(status).?);
    try std.testing.expectEqual(@as(libc.pid_t, 0), parsePPid("PPid:\t0\n").?);
    try std.testing.expect(parsePPid("Name:\tinit\nTracerPid:\t0\n") == null);
    try std.testing.expect(parsePPid("PPid:\tnotanumber\n") == null);
    // a truncated read must not answer from a half line
    try std.testing.expect(parsePPid("Name:\tsh\nPPi") == null);
}