summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/9p_io.zig9
-rw-r--r--src/host_io.zig159
-rw-r--r--test/snapshot.zig3
3 files changed, 113 insertions, 58 deletions
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;
+ const rc_path_capacity = 256;
- // Private files live until host teardown. Lengths keep this value movable.
+ /// 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);
}
};
diff --git a/test/snapshot.zig b/test/snapshot.zig
index 53553657..2fa8e89c 100644
--- a/test/snapshot.zig
+++ b/test/snapshot.zig
@@ -264,6 +264,9 @@ pub fn main(init: std.process.Init) !void {
});
std.process.exit(1);
}
+ // Every retry passed: the captures of the failed attempts explain
+ // nothing any more, and /tmp is not where they should pile up.
+ if (retry_dir) |d| std.Io.Dir.cwd().deleteTree(init.io, d) catch {};
std.debug.print("all {d} snapshot scripts ok\n", .{scripts.len});
}