diff options
Diffstat (limited to 'src/9p_io.zig')
| -rw-r--r-- | src/9p_io.zig | 424 |
1 files changed, 409 insertions, 15 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index 090eb838..3d019e0d 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -241,6 +241,10 @@ pub const Listener = struct { paused_ms: i64 = 0, path_buf: [sun_path_len]u8 = undefined, path_len: usize = 0, + /// The registry entry this editor posted + /// (`$XDG_RUNTIME_DIR/9p/pardes/<name>`), zero when it did not. + posted_buf: [sun_path_len]u8 = undefined, + posted_len: usize = 0, conns: [quic_slots]Conn = @splat(.{}), control: [2]c_int = .{ -1, -1 }, watcher: ?std.Thread = null, @@ -592,7 +596,7 @@ pub const Listener = struct { } for (l.control) |fd| { setCloexec(fd); - setNonblock(fd); + _ = setNonblock(fd); } l.arm(); l.watcher = try std.Thread.spawn(.{}, watchQuic, .{l}); @@ -635,8 +639,92 @@ pub const Listener = struct { } } + /// Advertises this editor in the posted-9P registry so a + /// `9ns --mntgen` mount lists it and dials it on a walk: + /// `$XDG_RUNTIME_DIR/9p/pardes/<name>` is a symlink to the socket + /// pardes already binds. One directory for the program, one entry + /// per running editor — the layout zmx uses for its sessions, so + /// several editors group instead of crowding the registry root. + /// + /// Only the runtime-directory socket posts: an instance that fell + /// back to `~/.local/state/pardes` stays out of the user's + /// registry, the way a private `ZMX_DIR` does for zmx. + /// + /// The socket itself is still bound by `listen` above, not by + /// `cloud9.post`: `post` claims flat names only, and its claim + /// protocol derives its lock directory from the registry path, so + /// a name inside a subdirectory cannot go through it yet. See + /// docs/cloud9.md. + fn postToRegistry(l: *Listener, name: []const u8) void { + if (comptime !supported) return; + if (name.len == 0 or std.mem.indexOfAny(u8, name, "/\x00") != null) return; + const xdg_c = libc.getenv("XDG_RUNTIME_DIR") orelse return; + const xdg = std.mem.span(xdg_c); + if (xdg.len == 0) return; + + var reg_buf: [sun_path_len:0]u8 = undefined; + const reg = std.fmt.bufPrintSentinel(®_buf, "{s}/9p", .{xdg}, 0) catch return; + _ = libc.mkdir(reg, 0o750); + var svc_buf: [sun_path_len:0]u8 = undefined; + const svc = std.fmt.bufPrintSentinel(&svc_buf, "{s}/9p/pardes", .{xdg}, 0) catch return; + if (libc.mkdir(svc, 0o750) != 0 and statNoFollow(svc) == null) { + log.warn("registry post skipped: cannot create {s}", .{svc}); + return; + } + sweepRegistry(l.io, svc); + var entry_buf: [sun_path_len:0]u8 = undefined; + const entry = std.fmt.bufPrintSentinel(&entry_buf, "{s}/{s}", .{ svc, name }, 0) catch { + log.warn("registry post skipped: name too long: {s}", .{name}); + return; + }; + const target = l.path_buf[0..l.path_len]; + + // Replace only what is provably not live: our own entry, or a + // dead predecessor's symlink. `probe` treats uncertainty as + // live, so it is only asked about an entry that *is* a symlink; + // anything else is left strictly alone and the symlink below + // simply fails. + var link_buf: [sun_path_len]u8 = undefined; + const n = libc.readlink(entry, &link_buf, link_buf.len); + if (n >= 0) { + const had = link_buf[0..@intCast(n)]; + if (!std.mem.eql(u8, had, target) and probe(entry) == .live) { + log.warn("registry entry pardes/{s} is live; not re-posted", .{name}); + return; + } + _ = libc.unlink(entry); + } + var target_z: [sun_path_len:0]u8 = undefined; + const target_zs = std.fmt.bufPrintSentinel(&target_z, "{s}", .{target}, 0) catch return; + if (libc.symlink(target_zs, entry) != 0) { + log.warn("registry post skipped: pardes/{s} is occupied", .{name}); + return; + } + @memcpy(l.posted_buf[0..entry.len], entry); + l.posted_len = entry.len; + log.info("posted pardes/{s} -> {s}", .{ name, target }); + } + + /// Removes the registry entry, but only while it is still ours: a + /// name another editor has since claimed is never unlinked. + fn unpostFromRegistry(l: *Listener) void { + if (comptime !supported) return; + if (l.posted_len == 0) return; + var z: [sun_path_len:0]u8 = undefined; + @memcpy(z[0..l.posted_len], l.posted_buf[0..l.posted_len]); + z[l.posted_len] = 0; + const entry = z[0..l.posted_len :0]; + var link_buf: [sun_path_len]u8 = undefined; + const n = libc.readlink(entry, &link_buf, link_buf.len); + if (n >= 0 and std.mem.eql(u8, link_buf[0..@intCast(n)], l.path_buf[0..l.path_len])) { + _ = libc.unlink(entry); + } + l.posted_len = 0; + } + pub fn deinit(l: *Listener, gpa: std.mem.Allocator) void { l.stopping.store(true, .release); + l.unpostFromRegistry(); if (l.watcher) |thread| { l.watch_stop.store(true, .release); _ = libc.write(l.control[1], "q", 1); @@ -680,7 +768,8 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, named: []const u8, fallback: [ .handler = .{ .ctx = l, .serve = Listener.onServe, .opened = Listener.onOpened, .closed = Listener.onClosed }, .greet_timeout_ms = Listener.greet_deadline_ms, }); - const p = socketPath(&l.path_buf, dir, if (named.len != 0) named else fallback) orelse { + const entry_name = if (named.len != 0) named else fallback; + const p = socketPath(&l.path_buf, dir, entry_name) orelse { gpa.destroy(l); return null; }; @@ -705,6 +794,7 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, named: []const u8, fallback: [ l.deinit(gpa); return null; } + l.postToRegistry(entry_name); if (tcp_dial) |dial| { if (!std.mem.startsWith(u8, dial, "tcp!")) { l.deinit(gpa); @@ -753,12 +843,129 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, named: []const u8, fallback: [ const alive = transport.isListening; -fn setNonblock(fd: c_int) void { +/// What is at a socket path, in cloud9's three answers (`post.Probe`). +/// Asked here with libc rather than through `cloud9.post.probe`, whose +/// raw Linux syscalls darwin cannot compile, but the classification is +/// that file's and must not become a second opinion: the one definite +/// refusal is `.stale`, nothing at the path at all is `.none`, and +/// everything else — connected, busy, refused permission, a surprise — +/// is `.live`. Uncertainty belongs to the server that owns the socket, +/// never to a sweeper deciding what to delete. +const Probe = enum { none, stale, live }; + +/// `SOCK.STREAM`, and the kernel's own non-blocking bit where there is +/// one. Darwin's `SOCK.NONBLOCK` is a Zig shim for `std.posix.socket` +/// to unpack, not an ABI value, so handing it to the raw libc call +/// would ask a kernel that has never heard of it; there it is an +/// `fcntl` instead. +const probe_socket_kind: c_uint = libc.SOCK.STREAM | (if (darwin) 0 else libc.SOCK.NONBLOCK); + +fn probe(path: [:0]const u8) Probe { + if (path.len + 1 > sun_path_len) return .live; // cannot ask; assume occupied + 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, probe_socket_kind, 0); + if (fd < 0) return .live; + defer _ = libc.close(fd); + + // Non-blocking is the whole safety of this function, so it is read + // back rather than assumed: a BLOCKING connect to a live server + // whose backlog is full parks in the kernel with no timeout to end + // it — measured, it simply never returns — and a sweep that parks + // takes the editor's startup with it. An fd that cannot be proven + // non-blocking is never connected at all, which lands on `.live`, + // the answer that deletes nothing. + if (comptime darwin) _ = setNonblock(fd); + if (!isNonblocking(fd)) return .live; + + if (libc.connect(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) == 0) return .live; + // cloud9's `post.probe` mapping, answer for answer. The connection + // is never wanted: EAGAIN (a full Unix backlog) and EINPROGRESS say + // somebody is listening, which is all that was asked, and the fd + // closes without ever waiting for it to become writable. + return switch (libc.errno(-1)) { + .AGAIN, .INPROGRESS, .PERM, .ACCES => .live, + .CONNREFUSED => .stale, + .NOENT, .NOTDIR => .none, + else => .live, + }; +} + +/// Reaps the registry entries whose editor is gone, once per post. +/// +/// A clean exit unposts itself (`unpostFromRegistry`), so everything +/// left behind comes from an exit that could not: an aborted test, a +/// kill, a crash. No code in the dead process can ever run, so the cure +/// has to be somebody else's readdir, and the next editor to start is +/// the somebody. It is only ever a symlink that is followed or +/// unlinked, because that is the only thing `postToRegistry` makes and +/// anything else under a name belongs to whoever put it there; and only +/// a definite refusal counts as gone. The socket a stale entry points +/// at goes too, but not before a stat agrees it is a socket of ours: a +/// plain file answers a connect with the same refusal. +fn sweepRegistry(io: std.Io, svc: [:0]const u8) void { + if (comptime !supported) return; + + // The names are staged before anything is unlinked, so the sweep + // never asks a directory to keep reading while it is being edited. + var names: [4096]u8 = undefined; + var staged: usize = 0; + { + const dir = std.Io.Dir.openDirAbsolute(io, svc, .{ .iterate = true }) catch return; + defer dir.close(io); + var read_buf: [std.Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined; + var reader: std.Io.Dir.Reader = .init(dir, &read_buf); + while (true) { + const listed = (reader.next(io) catch break) orelse break; + if (listed.name.len == 0 or listed.name.len > 255) continue; + if (staged + 1 + listed.name.len > names.len) break; + names[staged] = @intCast(listed.name.len); + @memcpy(names[staged + 1 ..][0..listed.name.len], listed.name); + staged += 1 + listed.name.len; + } + } + + var reaped: usize = 0; + var i: usize = 0; + while (i < staged) { + const name = names[i + 1 ..][0..names[i]]; + i += 1 + name.len; + var entry_buf: [sun_path_len:0]u8 = undefined; + const entry = std.fmt.bufPrintSentinel(&entry_buf, "{s}/{s}", .{ svc, name }, 0) catch continue; + const facts = statNoFollow(entry) orelse continue; + if (facts.mode & 0o170000 != 0o120000) continue; // not a symlink: not ours to judge + const state = probe(entry); + if (state == .live) continue; + var link_buf: [sun_path_len]u8 = undefined; + const n = libc.readlink(entry, &link_buf, link_buf.len); + if (libc.unlink(entry) != 0) continue; + reaped += 1; + // A relative target would resolve against this editor's working + // directory, which says nothing about what the entry named. + if (state != .stale or n <= 0 or link_buf[0] != '/') continue; + var target_buf: [sun_path_len:0]u8 = undefined; + const target = std.fmt.bufPrintSentinel(&target_buf, "{s}", .{link_buf[0..@intCast(n)]}, 0) catch continue; + const t = statNoFollow(target) orelse continue; + if (t.mode & 0o170000 == 0o140000 and t.uid == libc.getuid()) _ = libc.unlink(target); + } + if (reaped != 0) log.info("reaped {d} stale registry entries under {s}", .{ reaped, svc }); +} + +/// Whether it worked, because `probe` is not allowed to find out the +/// hard way: a caller that needs the guarantee has to be able to check. +fn setNonblock(fd: c_int) bool { const flags = libc.fcntl(fd, libc.F.GETFL, @as(c_int, 0)); - if (flags < 0) return; + if (flags < 0) return false; var o: libc.O = @bitCast(@as(u32, @bitCast(flags))); o.NONBLOCK = true; - _ = libc.fcntl(fd, libc.F.SETFL, @as(c_int, @bitCast(@as(u32, @bitCast(o))))); + return libc.fcntl(fd, libc.F.SETFL, @as(c_int, @bitCast(@as(u32, @bitCast(o))))) >= 0; +} + +fn isNonblocking(fd: c_int) bool { + const flags = libc.fcntl(fd, libc.F.GETFL, @as(c_int, 0)); + if (flags < 0) return false; + const o: libc.O = @bitCast(@as(u32, @bitCast(flags))); + return o.NONBLOCK; } const testing = std.testing; @@ -777,6 +984,108 @@ test "a name that is not one path component is no address at all" { try testing.expect(socketPath(&buf, "/run", "a\x00b") == null); } +test "the registry sweep takes the dead entries and leaves everything else" { + if (comptime !supported) return error.SkipZigTest; + // Under the real runtime directory, because a sun_path is 108 bytes + // and the test cache's temporary directories are longer than that. + var dir_buf: [sun_path_len:0]u8 = undefined; + const base = socketDir(&dir_buf) orelse return error.SkipZigTest; + if (!ensureSocketDir(base)) return error.SkipZigTest; + + var svc_buf: [sun_path_len:0]u8 = undefined; + const svc = std.fmt.bufPrintSentinel(&svc_buf, "{s}/sweep-{d}", .{ base, @as(u32, @intCast(libc.getpid())) }, 0) catch + return error.SkipZigTest; + if (libc.mkdir(svc, 0o700) != 0) return error.SkipZigTest; + + var paths: [5][sun_path_len:0]u8 = undefined; + const live_sock = try std.fmt.bufPrintSentinel(&paths[0], "{s}/live.sock", .{svc}, 0); + const dead_sock = try std.fmt.bufPrintSentinel(&paths[1], "{s}/dead.sock", .{svc}, 0); + const live = try std.fmt.bufPrintSentinel(&paths[2], "{s}/live", .{svc}, 0); + const dead = try std.fmt.bufPrintSentinel(&paths[3], "{s}/dead", .{svc}, 0); + const stranger = try std.fmt.bufPrintSentinel(&paths[4], "{s}/stranger", .{svc}, 0); + defer { + for ([_][:0]const u8{ live_sock, dead_sock, live, dead, stranger }) |p| _ = libc.unlink(p); + _ = libc.rmdir(svc); + } + + const listening = bindSocket(live_sock); + try testing.expect(listening >= 0); + defer _ = libc.close(listening); + try testing.expectEqual(@as(c_int, 0), libc.listen(listening, 1)); + + // Bound and then dropped: the file stays, and nobody answers it — + // exactly what an aborted test leaves behind. + const abandoned = bindSocket(dead_sock); + try testing.expect(abandoned >= 0); + _ = libc.close(abandoned); + + try testing.expectEqual(@as(c_int, 0), libc.symlink(live_sock, live)); + try testing.expectEqual(@as(c_int, 0), libc.symlink(dead_sock, dead)); + // Not a symlink, so not this program's to reason about, even though + // connecting to it is refused exactly like the dead socket. + try std.Io.Dir.cwd().writeFile(testing.io, .{ .sub_path = stranger, .data = "" }); + + // The guarantee itself, asserted rather than inferred from the fact + // that the test finished: if a probe socket ever stops being + // non-blocking the connect below parks instead of failing, and a + // parked test costs whoever is building far more than a red one. + const checking = libc.socket(libc.AF.UNIX, probe_socket_kind, 0); + try testing.expect(checking >= 0); + if (comptime darwin) try testing.expect(setNonblock(checking)); + try testing.expect(isNonblocking(checking)); + _ = libc.close(checking); + + // The listener's backlog is filled before anything is asked, because + // a full backlog is the one place a blocking connect parks forever + // (`unix_wait_for_peer`, no timeout) and a sweep that only answers + // while nobody is queued is the sweep that hangs a build. Nothing + // below accepts any of these, so the queue stays full throughout. + var queued: [8]c_int = @splat(-1); + defer for (queued) |fd| { + if (fd >= 0) _ = libc.close(fd); + }; + for (&queued) |*slot| { + const fd = libc.socket(libc.AF.UNIX, probe_socket_kind, 0); + if (fd < 0) break; + if (comptime darwin) _ = setNonblock(fd); + var addr: libc.sockaddr.un = .{ .path = @splat(0) }; + @memcpy(addr.path[0 .. live_sock.len + 1], live_sock[0 .. live_sock.len + 1]); + _ = libc.connect(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))); + slot.* = fd; + } + + const started = transport.nowMs(); + try testing.expectEqual(Probe.live, probe(live)); + try testing.expectEqual(Probe.stale, probe(dead)); + + sweepRegistry(testing.io, svc); + + // Promptly, and not "eventually": a blocking probe never comes back + // at all, so any wall-clock bound at all is the assertion that + // matters. A second is several thousand times what three connects + // and a readdir cost. + try testing.expect(transport.nowMs() - started < 1000); + + try testing.expect(statNoFollow(dead) == null); + try testing.expect(statNoFollow(dead_sock) == null); + try testing.expect(statNoFollow(live) != null); + try testing.expect(statNoFollow(live_sock) != null); + try testing.expect(statNoFollow(stranger) != null); + try testing.expectEqual(Probe.live, probe(live)); +} + +fn bindSocket(path: [:0]const u8) c_int { + const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0); + if (fd < 0) return fd; + var addr: libc.sockaddr.un = .{ .path = @splat(0) }; + @memcpy(addr.path[0 .. path.len + 1], path[0 .. path.len + 1]); + if (libc.bind(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) { + _ = libc.close(fd); + return -1; + } + return fd; +} + test "one connection's buffers are sized from the one msize constant" { try testing.expect(msize >= ninep.min_msize); try testing.expectEqual(@as(u32, msize), Runner.msize); @@ -840,6 +1149,59 @@ test "same-session TCP mounts compare canonical endpoints and local wildcard des extern "c" fn mkdtemp(template: [*:0]u8) ?[*:0]u8; extern "c" fn rmdir(path: [*:0]const u8) c_int; +test "a listening editor posts itself into the 9P registry and unposts on stop" { + if (comptime !supported) return error.SkipZigTest; + const gpa = testing.allocator; + var directory: [64:0]u8 = undefined; + _ = try std.fmt.bufPrintSentinel(&directory, "/tmp/pardes-post-XXXXXX", .{}, 0); + if (mkdtemp(&directory) == null) return error.TempDirectoryFailed; + const dir = std.mem.span(@as([*:0]const u8, &directory)); + const old_runtime = if (libc.getenv("XDG_RUNTIME_DIR")) |v| try gpa.dupeZ(u8, std.mem.span(v)) else null; + defer { + if (old_runtime) |v| { + _ = setenv("XDG_RUNTIME_DIR", v, 1); + gpa.free(v); + } else _ = unsetenv("XDG_RUNTIME_DIR"); + } + try testing.expectEqual(@as(c_int, 0), setenv("XDG_RUNTIME_DIR", &directory, 1)); + + var entry_buf: [sun_path_len:0]u8 = undefined; + const entry = try std.fmt.bufPrintSentinel(&entry_buf, "{s}/9p/pardes/unit", .{dir}, 0); + var svc_buf: [sun_path_len:0]u8 = undefined; + const svc = try std.fmt.bufPrintSentinel(&svc_buf, "{s}/9p/pardes", .{dir}, 0); + var sock_buf: [sun_path_len:0]u8 = undefined; + const sock = try std.fmt.bufPrintSentinel(&sock_buf, "{s}/" ++ prefix ++ "unit.sock", .{dir}, 0); + defer { + _ = libc.unlink(entry); + _ = rmdir(svc); + var reg_buf: [sun_path_len:0]u8 = undefined; + if (std.fmt.bufPrintSentinel(®_buf, "{s}/9p", .{dir}, 0)) |reg| { + _ = rmdir(reg); + } else |_| {} + _ = libc.unlink(sock); + _ = rmdir(&directory); + } + + var link: [sun_path_len]u8 = undefined; + { + const l = listen(testing.io, gpa, "unit", "", null, null) orelse return error.ListenFailed; + defer l.deinit(gpa); + // The socket stays exactly where pardes has always bound it: + // adopting the registry moves nothing, it only advertises. + try testing.expect(statNoFollow(sock) != null); + // And the registry holds a symlink to it one directory down, so + // several editors group under /mnt/9p/pardes/ instead of + // crowding the registry root — the layout zmx posts its + // sessions in. + const n = libc.readlink(entry, &link, link.len); + try testing.expect(n > 0); + try testing.expectEqualStrings(sock, link[0..@intCast(n)]); + } + // Stopping takes the name back out, so the next editor of that name + // is not refused by its own corpse. + try testing.expect(libc.readlink(entry, &link, link.len) < 0); +} + test "Unix TCP and QUIC share one listener through reads writes reconnects and reset" { if (comptime !supported) return error.SkipZigTest; const gpa = testing.allocator; @@ -960,24 +1322,41 @@ pub fn start(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes) ?*Listene return listener; } -pub fn exportPaneEnv(listener: ?*const Listener, serial: u32, forward_look: bool) void { - _ = unsetenv("PARDES_FORWARD_LOOK"); +/// What a pane shell is told about the editor above it, set into this +/// process's environment just before `forkpty` so the child inherits it. +/// +/// Two independent facts, and they are separate variables because they answer +/// separate questions. `PARDES_PID` says "you are inside this editor, and it +/// will take a Look from you": a session whose 9P listener never came up still +/// owns its children, so the child says so rather than looking, from its own +/// side, exactly like no pardes at all. `PARDES_9P` and `PARDES_PANE` say how +/// to reach it, and a `--nested` session exports them too — its socket stays +/// open to scripts and to mounted shells — while withholding `PARDES_PID`, +/// which is the whole of what `--nested` means. +pub fn exportPaneEnv(listener: ?*const Listener, serial: u32, adopts: bool) void { + var announced = false; + if (adopts) announcing: { + var buf: [16]u8 = undefined; + const text = std.fmt.bufPrintSentinel(&buf, "{d}", .{@as(u32, @intCast(libc.getpid()))}, 0) catch + break :announcing; + announced = setenv("PARDES_PID", text, 1) == 0; + } + if (!announced) _ = unsetenv("PARDES_PID"); + if (listener) |l| exporting: { var sock: [sun_path_len]u8 = undefined; const path = std.fmt.bufPrintSentinel(&sock, "{s}", .{l.path()}, 0) catch break :exporting; 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; - if (setenv("PARDES_PANE", id, 1) != 0) break :exporting; - if (setenv("PARDES_FORWARD_LOOK", if (forward_look) "1" else "0", 1) == 0) return; + if (setenv("PARDES_PANE", id, 1) == 0) return; } _ = unsetenv("PARDES_9P"); _ = unsetenv("PARDES_PANE"); - _ = setenv("PARDES_FORWARD_LOOK", "0", 1); } -test "9P shell environment preserves identity when nested Look forwarding is disabled" { - const names = [_][*:0]const u8{ "XDG_RUNTIME_DIR", "HOME", "PARDES_9P", "PARDES_PANE", "PARDES_FORWARD_LOOK" }; +test "9P shell environment states being inside pardes apart from how to reach it" { + const names = [_][*:0]const u8{ "XDG_RUNTIME_DIR", "HOME", "PARDES_PID", "PARDES_9P", "PARDES_PANE" }; 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)); @@ -1004,17 +1383,32 @@ test "9P shell environment preserves identity when nested Look forwarding is dis const path = "/tmp/pardes-example.sock"; @memcpy(listener.path_buf[0..path.len], path); listener.path_len = path.len; + var own: [16]u8 = undefined; + const own_pid = try std.fmt.bufPrint(&own, "{d}", .{@as(u32, @intCast(libc.getpid()))}); + + // A `--nested` session: reachable for scripts and mounts, but nobody's + // parent, so a pardes started in one of its shells runs a session of + // its own instead of handing its argument over. exportPaneEnv(listener, 7, false); try testing.expectEqualStrings(path, std.mem.span(libc.getenv("PARDES_9P").?)); try testing.expectEqualStrings("7", std.mem.span(libc.getenv("PARDES_PANE").?)); - try testing.expectEqualStrings("0", std.mem.span(libc.getenv("PARDES_FORWARD_LOOK").?)); + try testing.expect(libc.getenv("PARDES_PID") == null); + exportPaneEnv(listener, 8, true); try testing.expectEqualStrings("8", std.mem.span(libc.getenv("PARDES_PANE").?)); - try testing.expectEqualStrings("1", std.mem.span(libc.getenv("PARDES_FORWARD_LOOK").?)); + try testing.expectEqualStrings(own_pid, std.mem.span(libc.getenv("PARDES_PID").?)); + + // A session whose listener never came up is still the session this shell + // is inside: the pid stands on its own, and only the address is missing. + exportPaneEnv(null, 0, true); + try testing.expect(libc.getenv("PARDES_9P") == null); + try testing.expect(libc.getenv("PARDES_PANE") == null); + try testing.expectEqualStrings(own_pid, std.mem.span(libc.getenv("PARDES_PID").?)); + exportPaneEnv(null, 0, false); try testing.expect(libc.getenv("PARDES_9P") == null); try testing.expect(libc.getenv("PARDES_PANE") == null); - try testing.expectEqualStrings("0", std.mem.span(libc.getenv("PARDES_FORWARD_LOOK").?)); + try testing.expect(libc.getenv("PARDES_PID") == null); } pub const Client = struct { |
