From 4354669aa560c5504b046413f771d02c4216dd45 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 18:13:42 -0300 Subject: Shell prompt files are one per content under $XDG_RUNTIME_DIR, so a killed pardes leaves none behind; tests remove their /tmp dirs Every pardes host wrote its own pardes-osc133-bash-* and -fish-* files to /tmp and removed them only at a clean teardown, so each killed session, test and crash left two: 72K of them had piled up. They are now written once per content, named by its hash, in the user's private runtime directory, renamed into place whole and shared by every pardes; without that directory the old private /tmp files remain. fish's -C source is quoted. The 9p_io tests remove their runtime dirs with what the listener left in them, and the snapshot runner removes its retry captures when every retry passed. Co-Authored-By: Claude Opus 5.5 --- src/9p_io.zig | 9 ++-- src/host_io.zig | 161 ++++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 111 insertions(+), 59 deletions(-) (limited to 'src') diff --git a/src/9p_io.zig b/src/9p_io.zig index 8f56ff13..651b3d0a 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -1303,7 +1303,8 @@ test "Unix TCP and QUIC share one listener through reads writes reconnects and r var directory: [64:0]u8 = undefined; _ = try std.fmt.bufPrintSentinel(&directory, "/tmp/pardes-tcp-XXXXXX", .{}, 0); if (mkdtemp(&directory) == null) return error.TempDirectoryFailed; - defer _ = rmdir(&directory); + // What the listener posted and left inside goes with it. + defer std.Io.Dir.cwd().deleteTree(testing.io, std.mem.sliceTo(&directory, 0)) catch |err| std.debug.print("deleteTree: {s}\n", .{@errorName(err)}); 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| { @@ -1414,7 +1415,8 @@ test "a change waits while the editor is out mid-step, a read does not, and the var directory: [64:0]u8 = undefined; _ = try std.fmt.bufPrintSentinel(&directory, "/tmp/pardes-park-XXXXXX", .{}, 0); if (mkdtemp(&directory) == null) return error.TempDirectoryFailed; - defer _ = rmdir(&directory); + // What the listener posted and left inside goes with it. + defer std.Io.Dir.cwd().deleteTree(testing.io, std.mem.sliceTo(&directory, 0)) catch |err| std.debug.print("deleteTree: {s}\n", .{@errorName(err)}); 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| { @@ -1483,7 +1485,8 @@ test "a held read is answered when the log has news, and a flushed one spends no var directory: [64:0]u8 = undefined; _ = try std.fmt.bufPrintSentinel(&directory, "/tmp/pardes-held-XXXXXX", .{}, 0); if (mkdtemp(&directory) == null) return error.TempDirectoryFailed; - defer _ = rmdir(&directory); + // What the listener posted and left inside goes with it. + defer std.Io.Dir.cwd().deleteTree(testing.io, std.mem.sliceTo(&directory, 0)) catch |err| std.debug.print("deleteTree: {s}\n", .{@errorName(err)}); 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| { diff --git a/src/host_io.zig b/src/host_io.zig index a019e15d..25bd61e1 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -637,35 +637,55 @@ pub const Shell = struct { \\ ; - const rc_path_capacity = 64; - - // Private files live until host teardown. Lengths keep this value movable. + const rc_path_capacity = 256; + + /// The prompt files a shell sources at start. Under $XDG_RUNTIME_DIR + /// (private to the user) there is one file per content, named by its + /// hash and shared by every pardes, so none is left behind by a pardes + /// that was killed: 72K of them piled up in /tmp when each host wrote + /// its own. Without that directory each host writes private mkstemp + /// files in /tmp and removes them at teardown, as before. pub const PromptFiles = struct { bash_path: [rc_path_capacity:0]u8 = @splat(0), - bash_len: u8 = 0, + bash_len: u16 = 0, + bash_owned: bool = false, fish_path: [rc_path_capacity:0]u8 = @splat(0), - fish_len: u8 = 0, - fish_command: [rc_path_capacity + "source ".len:0]u8 = @splat(0), - fish_command_len: u8 = 0, + fish_len: u16 = 0, + fish_owned: bool = false, + fish_command: [rc_path_capacity + "source ''".len:0]u8 = @splat(0), + fish_command_len: u16 = 0, pub fn init() PromptFiles { var rcs: PromptFiles = .{}; - rcs.bash_len = stage(&rcs.bash_path, "/tmp/pardes-osc133-bash-XXXXXX", bash_rc); - rcs.fish_len = stage(&rcs.fish_path, "/tmp/pardes-osc133-fish-XXXXXX", fish_rc); + const bash = place(&rcs.bash_path, "bash", bash_rc); + rcs.bash_len = bash.len; + rcs.bash_owned = bash.owned; + const fish = place(&rcs.fish_path, "fish", fish_rc); + rcs.fish_len = fish.len; + rcs.fish_owned = fish.owned; if (rcs.fishPath()) |path| { - const command = std.fmt.bufPrintSentinel(&rcs.fish_command, "source {s}", .{path}, 0) catch { - _ = libc.unlink(path.ptr); + // Quoted for fish; a path a quote would break is not used. + const command = if (std.mem.indexOfAny(u8, path, "'\\") != null) + error.NoSpaceLeft + else + std.fmt.bufPrintSentinel(&rcs.fish_command, "source '{s}'", .{path}, 0); + const ok = command catch { + if (rcs.fish_owned) _ = libc.unlink(path.ptr); rcs.fish_len = 0; return rcs; }; - rcs.fish_command_len = @intCast(command.len); + rcs.fish_command_len = @intCast(ok.len); } return rcs; } pub fn deinit(rcs: *PromptFiles) void { - if (rcs.bashPath()) |path| _ = libc.unlink(path.ptr); - if (rcs.fishPath()) |path| _ = libc.unlink(path.ptr); + if (rcs.bashPath()) |path| if (rcs.bash_owned) { + _ = libc.unlink(path.ptr); + }; + if (rcs.fishPath()) |path| if (rcs.fish_owned) { + _ = libc.unlink(path.ptr); + }; rcs.bash_len = 0; rcs.fish_len = 0; rcs.fish_command_len = 0; @@ -687,8 +707,34 @@ pub const Shell = struct { } }; + /// The shared file for `contents` under $XDG_RUNTIME_DIR, written + /// beside it and renamed into place when missing or different, so a + /// shell never reads half of one; failing that, a private /tmp file. + fn place(path_buf: *[rc_path_capacity:0]u8, name: []const u8, contents: []const u8) struct { len: u16, owned: bool } { + shared: { + const dir = std.mem.span(std.c.getenv("XDG_RUNTIME_DIR") orelse break :shared); + if (dir.len == 0 or dir[0] != '/') break :shared; + const final = std.fmt.bufPrintSentinel(path_buf, "{s}/pardes-osc133-{s}-{x:0>16}", .{ dir, name, std.hash.Wyhash.hash(0, contents) }, 0) catch break :shared; + var have: [2048]u8 = undefined; + if (readSmall(final, &have)) |got| if (std.mem.eql(u8, got, contents)) return .{ .len = @intCast(final.len), .owned = false }; + var tmp_buf: [rc_path_capacity:0]u8 = @splat(0); + var template_buf: [rc_path_capacity]u8 = undefined; + const tmp_template = std.fmt.bufPrint(&template_buf, "{s}.XXXXXX", .{final}) catch break :shared; + const n = stage(&tmp_buf, tmp_template, contents); + if (n == 0) break :shared; + if (libc.rename(tmp_buf[0..n :0].ptr, final.ptr) != 0) { + _ = libc.unlink(tmp_buf[0..n :0].ptr); + break :shared; + } + return .{ .len = @intCast(final.len), .owned = false }; + } + var tmp_template: [64]u8 = undefined; + const template = std.fmt.bufPrint(&tmp_template, "/tmp/pardes-osc133-{s}-XXXXXX", .{name}) catch return .{ .len = 0, .owned = false }; + return .{ .len = stage(path_buf, template, contents), .owned = true }; + } + // Publish a path only after its private 0600 file is fully written and closed. - fn stage(path_buf: *[rc_path_capacity:0]u8, template: []const u8, contents: []const u8) u8 { + fn stage(path_buf: *[rc_path_capacity:0]u8, template: []const u8, contents: []const u8) u16 { const path = std.fmt.bufPrintSentinel(path_buf, "{s}", .{template}, 0) catch return 0; const fd = mkstemp(path.ptr); if (fd < 0) return 0; @@ -817,52 +863,55 @@ pub const Shell = struct { try std.testing.expect(libc.access(gone.path, X_OK) == 0); } - test "prompt rc owners have private complete files and clean them up" { + test "prompt files are one per content under the runtime dir, or private and cleaned up without it" { if (builtin.os.tag == .windows) return; - var original = PromptFiles.init(); - var a = original; - original = .{}; - original.deinit(); - defer a.deinit(); + var saved_buf: [std.fs.max_path_bytes:0]u8 = @splat(0); + const saved: ?[:0]const u8 = if (std.c.getenv("XDG_RUNTIME_DIR")) |v| std.fmt.bufPrintSentinel(&saved_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null; + defer _ = if (saved) |v| setenv("XDG_RUNTIME_DIR", v.ptr, 1) else unsetenv("XDG_RUNTIME_DIR"); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var dir_buf: [std.fs.max_path_bytes:0]u8 = @splat(0); + const dir_len = try tmp.dir.realPath(std.testing.io, &dir_buf); + _ = setenv("XDG_RUNTIME_DIR", dir_buf[0..dir_len :0].ptr, 1); + + // Shared: the same file for every owner, left in place, rewritten + // whole when what is there is not its content. + var a = PromptFiles.init(); var b = PromptFiles.init(); - defer b.deinit(); const a_bash = a.bashPath() orelse return error.TempCreateFailed; - const b_bash = b.bashPath() orelse return error.TempCreateFailed; - const a_fish = a.fishPath() orelse return error.TempCreateFailed; - try std.testing.expect(!std.mem.eql(u8, a_bash, b_bash)); + try std.testing.expectEqualStrings(a_bash, b.bashPath().?); + try std.testing.expect(std.mem.startsWith(u8, a_bash, dir_buf[0..dir_len])); const fish_command = a.fishCommand() orelse return error.MissingFishCommand; - try std.testing.expectEqualStrings("source ", fish_command[0.."source ".len]); - try std.testing.expectEqualStrings(a_fish, fish_command["source ".len..]); - for ([_][]const u8{ a_bash, b_bash, a_fish }) |path| { - const stat = try std.Io.Dir.cwd().statFile(std.testing.io, path, .{}); - try std.testing.expectEqual(std.Io.File.Kind.file, stat.kind); - try std.testing.expectEqual(0, stat.permissions.toMode() & 0o077); - } - var fish_buf: [fish_rc.len]u8 = undefined; - try std.testing.expectEqualStrings(fish_rc, readSmall(a_fish, &fish_buf) orelse return error.ReadFailed); - - var buf: [bash_rc.len]u8 = undefined; - const fd = libc.open(a_bash.ptr, .{ .ACCMODE = .RDONLY }); - if (fd < 0) return error.OpenFailed; - defer _ = libc.close(fd); - var len: usize = 0; - while (len < buf.len) { - const n = libc.read(fd, buf[len..].ptr, buf.len - len); - if (n < 0) { - if (libc.errno(n) == .INTR) continue; - return error.ReadFailed; - } - if (n == 0) break; - len += @intCast(n); - } - try std.testing.expectEqualStrings(bash_rc, buf[0..len]); - - var removed: [rc_path_capacity:0]u8 = @splat(0); - @memcpy(removed[0..a_bash.len], a_bash); - removed[a_bash.len] = 0; + var want: [rc_path_capacity + 16]u8 = undefined; + try std.testing.expectEqualStrings(try std.fmt.bufPrint(&want, "source '{s}'", .{a.fishPath().?}), fish_command); + var buf: [2048]u8 = undefined; + try std.testing.expectEqualStrings(bash_rc, readSmall(a_bash, &buf) orelse return error.ReadFailed); + try std.testing.expectEqualStrings(fish_rc, readSmall(a.fishPath().?, &buf) orelse return error.ReadFailed); + const stat = try std.Io.Dir.cwd().statFile(std.testing.io, a_bash, .{}); + try std.testing.expectEqual(0, stat.permissions.toMode() & 0o077); + var kept: [rc_path_capacity:0]u8 = @splat(0); + @memcpy(kept[0..a_bash.len], a_bash); a.deinit(); - try std.testing.expect(libc.access(&removed, 0) < 0); - try std.testing.expectEqualStrings(bash_rc, readSmall(b_bash, &buf) orelse return error.ReadFailed); + b.deinit(); + try std.testing.expect(libc.access(&kept, 0) == 0); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = std.fs.path.basename(kept[0..a_bash.len]), .data = "half" }); + var c = PromptFiles.init(); + defer c.deinit(); + try std.testing.expectEqualStrings(bash_rc, readSmall(c.bashPath().?, &buf) orelse return error.ReadFailed); + + // No runtime dir: private files, one per owner, gone at teardown. + _ = unsetenv("XDG_RUNTIME_DIR"); + var d = PromptFiles.init(); + var e = PromptFiles.init(); + defer e.deinit(); + const d_bash = d.bashPath() orelse return error.TempCreateFailed; + try std.testing.expect(!std.mem.eql(u8, d_bash, e.bashPath().?)); + try std.testing.expect(std.mem.startsWith(u8, d_bash, "/tmp/pardes-osc133-bash-")); + @memcpy(kept[0..d_bash.len], d_bash); + kept[d_bash.len] = 0; + d.deinit(); + try std.testing.expect(libc.access(&kept, 0) < 0); } }; -- cgit v1.3