diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 11:09:59 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 11:10:37 -0300 |
| commit | 36a9bfbaba649da3d988608f1cc7834e567df95d (patch) | |
| tree | 432831507023d906f3f5ad46969202c92232bf15 | |
| parent | c5e5db988e59689b44feb42c59090c07687bcd42 (diff) | |
| download | pardes-36a9bfbaba649da3d988608f1cc7834e567df95d.tar.gz pardes-36a9bfbaba649da3d988608f1cc7834e567df95d.zip | |
Every pane pardes spawns gets $PARDES_MOUNT, the session's own directory under 9ns --mntgen ($NINE_MOUNT/pardes/<name>), unset when it is not known
9ns says --mntgen with NINE_MNTGEN=1 (cloud9). Under `9ns --unix` the
mount is the server it dialled, never this session (its socket does not
exist until it starts), so it stays unset there; scripts fall back to
`9p -a "unix!$PARDES_9P"`. Nothing is statted: the name comes from the
session's socket and the environment. selfmount checks both modes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | build.zig.zon | 4 | ||||
| -rw-r--r-- | src/9p_io.zig | 46 | ||||
| -rw-r--r-- | test/selfmount.py | 48 |
3 files changed, 96 insertions, 2 deletions
diff --git a/build.zig.zon b/build.zig.zon index 17ed9bde..5410776c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,8 +9,8 @@ // read-only HTTPS URL is what a manifest can carry. Re-pin with // `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`. .cloud9 = .{ - .url = "git+https://git.sr.ht/~gbrls/cloud9#552584ad79448988646231c7993a36e55b0e8c05", - .hash = "cloud9-0.1.0-yt86qlkdHgCC4CRZQA1ttzUYpZ07PoIxzIysTOSSbmqW", + .url = "git+https://git.sr.ht/~gbrls/cloud9#34834165e2b54dd6ac26babd1e9d0443f1f53ee4", + .hash = "cloud9-0.1.0-yt86qpUlHgC3vH0y1u4l0rSDo_lDBvuIu6tqETfVaJNZ", }, // Pinned to the exact libvaxis commit ghostty is built against, so it // is compatible with ghostty's uucode configuration (the one we share diff --git a/src/9p_io.zig b/src/9p_io.zig index cb345b00..0b3db274 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -1925,10 +1925,56 @@ pub fn exportPaneEnv(listener: ?*const Listener, serial: u32, adopts: bool) void var buf: [16]u8 = undefined; const id = std.fmt.bufPrintSentinel(&buf, "{d}", .{serial}, 0) catch break :exporting; if (setenv("PARDES_9P", path, 1) != 0) break :exporting; + exportMount(l.path()); if (setenv("PARDES_PANE", id, 1) == 0) return; } _ = unsetenv("PARDES_9P"); _ = unsetenv("PARDES_PANE"); + _ = unsetenv("PARDES_MOUNT"); +} + +/// $PARDES_MOUNT, the session's own directory, when the environment says +/// where it is -- never by looking at the mount, which this editor serves +/// (the self-mount deadlock). Under `9ns --mntgen` (NINE_MNTGEN=1) it is +/// `$NINE_MOUNT/pardes/<name>`, the name the registry gives the socket. +/// Any other $NINE_MOUNT is a mount of another server, made before this +/// session had a socket: unknown, so unset, and a script falls back to +/// `9p -a "unix!$PARDES_9P"`. A Tty9p shell sets its own (linux/v9fs.zig). +fn exportMount(sock_path: []const u8) void { + const mntgen = if (libc.getenv("NINE_MNTGEN")) |v| std.mem.eql(u8, std.mem.span(v), "1") else false; + const root = if (libc.getenv("NINE_MOUNT")) |v| std.mem.span(v) else ""; + const base = std.fs.path.basename(sock_path); + const sock_prefix = "pardes-9p-"; + const suffix = ".sock"; + if (mntgen and root.len > 0 and std.mem.startsWith(u8, base, sock_prefix) and std.mem.endsWith(u8, base, suffix) and base.len > sock_prefix.len + suffix.len) { + var buf: [4096]u8 = undefined; + if (std.fmt.bufPrintSentinel(&buf, "{s}/pardes/{s}", .{ std.mem.trimEnd(u8, root, "/"), base[sock_prefix.len .. base.len - suffix.len] }, 0)) |mount| { + if (setenv("PARDES_MOUNT", mount, 1) == 0) return; + } else |_| {} + } + _ = unsetenv("PARDES_MOUNT"); +} + +test "a pane's PARDES_MOUNT is the session's directory under 9ns --mntgen, and unset when not known" { + const names = [_][*:0]const u8{ "NINE_MNTGEN", "NINE_MOUNT", "PARDES_MOUNT" }; + var saved: [names.len]?[:0]u8 = @splat(null); + for (names, &saved) |name, *value| { + if (libc.getenv(name)) |old| value.* = try testing.allocator.dupeZ(u8, std.mem.span(old)); + } + defer for (names, saved) |name, value| { + if (value) |old| { + _ = setenv(name, old, 1); + testing.allocator.free(old); + } else _ = unsetenv(name); + }; + _ = setenv("NINE_MOUNT", "/mnt/9p", 1); + _ = setenv("NINE_MNTGEN", "1", 1); + exportMount("/run/user/1000/pardes-9p-work.sock"); + try testing.expectEqualStrings("/mnt/9p/pardes/work", std.mem.span(libc.getenv("PARDES_MOUNT").?)); + // A mount of another server (9ns --unix): not this session's. + _ = unsetenv("NINE_MNTGEN"); + exportMount("/run/user/1000/pardes-9p-work.sock"); + try testing.expect(libc.getenv("PARDES_MOUNT") == null); } test "9P shell environment states being inside pardes apart from how to reach it" { diff --git a/test/selfmount.py b/test/selfmount.py index 8d78487d..0111e6ac 100644 --- a/test/selfmount.py +++ b/test/selfmount.py @@ -27,6 +27,46 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from ninep import Client +def unix_mount(ns, binary, served, env, name, fixture): + """A second editor under `9ns --unix <the first's socket>`, on the + fixture (a command runs from a file's pane, not a terminal's): what its + command pane sees in $PARDES_MOUNT.""" + second = name + 'u' + sock = f'{os.environ["XDG_RUNTIME_DIR"]}/pardes-9p-{second}.sock' + out = Path(f'/tmp/{second}.mount') + env = dict(env, PARDES_DUMP=f'/tmp/{second}.dump.zon') + pid, master = pty.fork() + if pid == 0: + os.execve(ns, ['9ns', '--unix', served, '--', binary, '--tty', '--9p=' + second, str(fixture)], env) + fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack('HHHH', 30, 100, 0, 0)) + os.set_blocking(master, False) + try: + deadline = time.monotonic() + 30 + while time.monotonic() < deadline and not out.exists(): + try: + os.read(master, 65536) + except OSError: + pass + if os.path.exists(sock) and not out.with_suffix('.sent').exists(): + time.sleep(.3) + with Client(sock) as client: + client.write('/ctl', b'Shell sh\n') + client.write('/exec', f'echo "${{PARDES_MOUNT-unset}}" > {out}.tmp; mv {out}.tmp {out}\n'.encode()) + out.with_suffix('.sent').touch() + time.sleep(.1) + return out.read_text().strip() if out.exists() else 'timeout' + finally: + os.kill(pid, signal.SIGTERM) + time.sleep(.2) + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + os.waitpid(pid, 0) + for path in (out, out.with_suffix('.sent'), Path(f'/tmp/{second}.dump.zon')): + path.unlink(missing_ok=True) + + def run(binary): ns = shutil.which('9ns') or str(Path.home() / '.local/bin/9ns') runtime = os.environ.get('XDG_RUNTIME_DIR') @@ -128,6 +168,14 @@ def run(binary): return exits[-1].split()[2] time.sleep(.1) return 'timeout' + # A pane's command finds the session's own directory in + # $PARDES_MOUNT (9ns --mntgen says NINE_MNTGEN=1). + check(run(f'test "$PARDES_MOUNT" = {mount} && grep -q . "$PARDES_MOUNT/index"\n'.encode()) == '0', + '$PARDES_MOUNT is the session under the mntgen mount, and it answers') + # Under `9ns --unix` the mount is another server, never this + # session: a second editor started there leaves it unset. + seen_mount = unix_mount(ns, binary, sock, env, name, fixture) + check(seen_mount == 'unset', f'under 9ns --unix $PARDES_MOUNT is unset ({seen_mount!r})') # Followed from before the burst: a fresh open of the log reads # only what its 64 KiB ring still holds, which a burst this size # outgrows, and a follower the ring outruns reads `lost N`, so |
