diff options
Diffstat (limited to 'src/host_io.zig')
| -rw-r--r-- | src/host_io.zig | 159 |
1 files changed, 104 insertions, 55 deletions
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); } }; |
