summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig.zon4
-rw-r--r--src/9p_io.zig46
-rw-r--r--test/selfmount.py48
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