summaryrefslogtreecommitdiff
path: root/src/nested.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-26 13:27:46 -0300
committerGabriel Schneider <[email protected]>2026-08-27 09:47:39 -0300
commit11f380f6d7222f2cad93c2cdf13701ea1f903d47 (patch)
tree803194ee5853a6b4cda93f90a95e28d1f02e69ae /src/nested.zig
parentfbc194068687e49a8490c85c9f1257a2f2bb9079 (diff)
downloadpardes-11f380f6d7222f2cad93c2cdf13701ea1f903d47.tar.gz
pardes-11f380f6d7222f2cad93c2cdf13701ea1f903d47.zip
One core behind N frontends, the board's own runner moved in, and every board cap on one screen
## The wire is the effect stream, not a new protocol `pardes --detach` leaves a core running with no terminal; `pardes --attach` is a frontend that owns a terminal and a socket and nothing else. N frontends on one core all look at the same screen — `screen -x`, not N sessions. The codec (`src/detached/wire.zig`) carries exactly one `Event` or one `Host.VTable` call per message. That is not a coincidence and it is why there is no third vocabulary to keep in step: the core's IO seam was already a struct of function pointers with plain-data arguments, so a socket is a legal implementation of it. `nested.zig`'s socket could not be reused — it carries a builtin command line, and a command line cannot carry a frame. ARCHITECTURE-NEUTRAL on purpose, not as decoration. The frontend on the far end may be riscv32-freestanding on the ESP32-P4 while the core is x86_64 Linux, so every field is an explicit little-endian fixed width and no message is a blit of a native struct. A protocol that only works between two builds of the same compiler would have thrown away the one frontend that motivated it. ## The board comes in; its toolchain stays out `src/p4.zig` becomes `src/esp32p4.zig`, and the pardes half of `../05-zig-p4` — the vaxis-over- serial runner, the UART editor terminal, the keystroke rescue ring, the on-die test suite — moves into `src/esp32p4/`. `build.zig.zon` gains `.zig_p4 = .{ .path = "../05-zig-p4" }`, so `zig build -Dplatform=esp32p4 -Desp32p4-firmware` builds, flashes, monitors and self-tests the board from this repo's `build.zig`. The DIVISION is the point. What moved is what only pardes wants: the runner that drives a pardes core over a serial line. What stayed is everything a second project would also want — the HAL, the register/radio/oracle layers, the linker script, `_start`. `zig_p4` declares no dependencies of its own and its `build()` early-returns when it is not the root package, so this costs the package graph exactly zero packages and the editor's own builds nothing at all. ## limits.zig: nine forgettable places become one budget Nine `platform == .esp32p4` capacity tests lived in nine files. They were never nine decisions — they are ONE decision, how much memory this build may spend, taken nine times where no reader could see the total. `src/limits.zig` puts the whole budget on one screen with every cap named against what it is measured against, derived from two booleans. The payoff is testability on a machine that is not the board: the caps are ordinary comptime values, so a host build can be compiled against the board's numbers and the parking, eviction and clamping paths a 240 KiB core takes get exercised by the normal test suite instead of only over a UART. ## A bare `zig build` `zig build` with no arguments now builds the tty and GUI binaries and installs them into `~/.local/bin`, and says so once on stdout with the flag that overrides it. The old default built one binary into `zig-out` — a path nothing on a `PATH` ever looks at, which made "build it" and "use it" two different commands for no reason.
Diffstat (limited to 'src/nested.zig')
-rw-r--r--src/nested.zig61
1 files changed, 41 insertions, 20 deletions
diff --git a/src/nested.zig b/src/nested.zig
index eb01b2e0..887d0703 100644
--- a/src/nested.zig
+++ b/src/nested.zig
@@ -36,21 +36,28 @@ const libc = std.c;
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) {
+/// THE SOCKET CONVENTIONS BELOW ARE SHARED, and the ones marked `pub` are
+/// shared with src/detached/server.zig — a second unix socket in the same
+/// per-user directory, under a different name (`pardes-detached-<name>.sock`
+/// rather than `pardes-<pid>.sock`). They were copied into that file when it
+/// landed; one directory vetted by two different predicates is exactly the
+/// divergence the reasoning here is meant to prevent, so there is one of each.
+pub 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;
+/// executable and parent of an arbitrary pid, and unix sockets. The detached
+/// transport needs the second alone, and the same answer.
+pub 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;
+pub 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
@@ -72,10 +79,15 @@ extern "c" fn proc_pidpath(pid: c_int, buffer: *anyopaque, buffersize: u32) c_in
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 {
+/// The gap is a race only against a fork on another thread, and every caller
+/// is past that: `listen` runs before the first pane exists, `acceptLine` runs
+/// on a thread of its own long after spawning has settled, and the detached
+/// session forks nothing at all (its ptys live in its frontends).
+///
+/// CLOEXEC still matters for both: a `--detach` session is long-lived, and an
+/// inherited listener would keep its socket bound long after it ended — the
+/// same shape as the inherited lock fd that once held a flock forever.
+pub fn setCloexec(fd: c_int) void {
const FD_CLOEXEC: c_int = 1;
_ = libc.fcntl(fd, libc.F.SETFD, FD_CLOEXEC);
}
@@ -87,10 +99,12 @@ 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 {
+/// is per-user for the same reason a home directory is. NEVER /tmp: these
+/// sockets take a command line, or keystrokes into a live editor. Asked by the
+/// client (to derive the path), by the listener (to create and vet it), by the
+/// sweeper (to scan it) and by the detached transport (all three, for its own
+/// name), so it is written once.
+pub 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;
@@ -311,14 +325,19 @@ pub fn sendLook(pid: libc.pid_t, path: []const u8, line: usize) bool {
return true;
}
-/// The three things ensureSocketDir has to know about a path, from whichever
+/// The two 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 };
+/// std cannot name — so linux asks statx for the same fields. Both spellings
+/// refuse to follow a symlink, which is the point of asking.
+///
+/// `pub` for the detached transport, which vets the same directory and also
+/// vets the SOCKET FILE with it (src/detached/server.zig `vetted`): `mode`
+/// carries the type bits, so one call answers "is this a socket, ours, and
+/// private" as well as it answers it for a directory.
+pub const DirFacts = struct { mode: u32, uid: libc.uid_t };
-fn statNoFollow(path: [:0]const u8) ?DirFacts {
+pub 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;
@@ -334,9 +353,11 @@ fn statNoFollow(path: [:0]const u8) ?DirFacts {
/// 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 {
+/// the whole attack on a socket that runs commands — or, for the detached
+/// transport that shares this, on one that carries keystrokes into a live
+/// editor — and $XDG_RUNTIME_DIR passes this untouched (the login session
+/// already makes it 0700).
+pub 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