diff options
Diffstat (limited to 'src/nested.zig')
| -rw-r--r-- | src/nested.zig | 75 |
1 files changed, 63 insertions, 12 deletions
diff --git a/src/nested.zig b/src/nested.zig index 0c065d1d..20c42dd0 100644 --- a/src/nested.zig +++ b/src/nested.zig @@ -59,17 +59,43 @@ pub fn socketPath(buf: *[108]u8, pid: libc.pid_t) ?[:0]const u8 { return std.fmt.bufPrintSentinel(buf, "{s}/pardes-{d}.sock", .{ dir, @as(u32, @intCast(pid)) }, 0) catch null; } -/// A `/proc/<pid>/exe` readlink with the kernel's `" (deleted)"` suffix taken -/// off. `zig build` replaces the binary under a running pardes — that is the -/// daily loop in this repo — and from that moment the OUTER instance's exe -/// link reads `/path/to/pardes (deleted)` while the freshly built child's -/// reads `/path/to/pardes`. Comparing them raw made every nested launch after -/// a rebuild open a second full-screen UI inside the pane. +/// 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 and SDL 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), so either one must recognise the other as an outer +/// pardes. Requiring the same directory retains the executable-identity check: +/// an unrelated ancestor merely named `pardes` is not enough. +fn samePardesExecutable(a_raw: []const u8, b_raw: []const u8) bool { + const a = stripDeleted(a_raw); + const b = stripDeleted(b_raw); + if (std.mem.eql(u8, a, b)) return true; + const a_dir = std.fs.path.dirname(a) orelse return false; + const b_dir = std.fs.path.dirname(b) orelse return false; + if (!std.mem.eql(u8, a_dir, b_dir)) return false; + + const a_name = std.fs.path.basename(a); + const b_name = std.fs.path.basename(b); + const gui = "pardes-gui"; + const tty = "pardes"; + const a_gui = std.mem.startsWith(u8, a_name, gui); + const b_gui = std.mem.startsWith(u8, b_name, gui); + if (a_gui == b_gui) return false; + const gui_name = if (a_gui) a_name else b_name; + const tty_name = if (a_gui) b_name else a_name; + if (!std.mem.startsWith(u8, tty_name, tty)) return false; + const gui_tail = gui_name[gui.len..]; + const tty_tail = tty_name[tty.len..]; + if ((gui_tail.len != 0 and gui_tail[0] != '-') or + (tty_tail.len != 0 and tty_tail[0] != '-')) return false; + return std.mem.eql(u8, gui_tail, tty_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 @@ -94,11 +120,13 @@ fn sweepPid(name: []const u8) ?libc.pid_t { return std.fmt.parseInt(libc.pid_t, digits, 10) catch null; } -/// The pid of the nearest ancestor running THIS executable, or null. Identity -/// is `readlink("/proc/<pid>/exe")` against our own, not a name: a name match -/// would call every `vim pardes.zig` an outer pardes. The hop cap is not for -/// /proc, which cannot loop, but because the walk is driven by numbers read -/// out of files and should not be able to spin on a surprising one. +/// The pid of the nearest ancestor running a pardes executable, or null. +/// Identity is `readlink("/proc/<pid>/exe")` against our own; the tty `pardes` +/// and SDL `pardes-gui` siblings also match when they live in the same +/// directory. A name alone would call every unrelated `pardes` ancestor an +/// outer instance. The hop cap is not for /proc, which cannot loop, but because +/// the walk is driven by numbers read out of files and should not be able to +/// spin on a surprising one. pub fn outer() ?libc.pid_t { if (comptime builtin.os.tag != .linux) return null; var self_buf: [4096]u8 = undefined; @@ -112,7 +140,7 @@ pub fn outer() ?libc.pid_t { var buf: [4096]u8 = undefined; const exe = std.fmt.bufPrintSentinel(&name, "/proc/{d}/exe", .{@as(u32, @intCast(pid))}, 0) catch return null; const n = libc.readlink(exe, &buf, buf.len); - if (n > 0 and std.mem.eql(u8, stripDeleted(buf[0..@intCast(n)]), self_exe)) return pid; + if (n > 0 and samePardesExecutable(buf[0..@intCast(n)], self_exe)) return pid; 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; @@ -356,6 +384,29 @@ test "a rebuilt binary still matches its own running instance" { 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)", + )); + try std.testing.expect(!samePardesExecutable( + "/usr/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-x86_64", + )); + try std.testing.expect(!samePardesExecutable( + "/work/zig-out/bin/not-pardes", + "/work/zig-out/bin/not-pardes-gui", + )); +} + 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").?); |
