diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 21:54:31 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 30011f7269d0febdefbb4c907f8a28d932f44f6f (patch) | |
| tree | 722c5752673169d28f10fcd5227deac7419ca371 /src | |
| parent | cc8b72c1b1fd19cd3ef159fe4f56c7b7916380ce (diff) | |
| download | pardes-30011f7269d0febdefbb4c907f8a28d932f44f6f.tar.gz pardes-30011f7269d0febdefbb4c907f8a28d932f44f6f.zip | |
An editor's private pardes-osc133 prompt files go at Exit and at a fatal signal, and a start sweeps the ones a dead editor left
/tmp had gathered thousands of private prompt files: Exit removed its
own, but an editor killed by a signal, or a crashed one, left them for
ever. They now carry their editor's pid in the name, a SIGTERM or
SIGHUP removes them before the signal's death, and each start removes
those whose pid kill(pid, 0) answers ESRCH for. Files of the older
shape carry no pid and are left alone, as a running older editor may
still read them; the shared ones in the runtime directory are kept,
being every session's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/9p_io.zig | 5 | ||||
| -rw-r--r-- | src/host_io.zig | 103 | ||||
| -rw-r--r-- | src/tty/tty.zig | 4 |
3 files changed, 109 insertions, 3 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index fe1a707f..0cb7b960 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -865,8 +865,13 @@ fn armSocketCleanup() void { } } +/// What else a fatal signal's handler removes (host_io's private prompt +/// files); async-signal-safe. +pub var fatal_cleanup: ?*const fn () void = null; + fn onFatalSignal(s: std.posix.SIG) callconv(.c) void { unlinkOwnSocket(); + if (fatal_cleanup) |cleanup| cleanup(); // SA_RESETHAND put the default back: the same signal, now fatal. _ = std.c.raise(s); } diff --git a/src/host_io.zig b/src/host_io.zig index 8cbdd2d4..ebe910aa 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -480,7 +480,76 @@ pub const Shell = struct { adoptSystemPath(); if (comptime builtin.os.tag.isDarwin()) _ = setenv("BASH_SILENCE_DEPRECATION_WARNING", "1", 1); - return PromptFiles.init(); + sweepDeadRcs(); + const rcs = PromptFiles.init(); + // A private file goes at a fatal signal too, not only at Exit. + for ([_]?[:0]const u8{ if (rcs.bash_owned) rcs.bashPath() else null, if (rcs.fish_owned) rcs.fishPath() else null }, 0..) |owned, i| { + const path = owned orelse continue; + @memcpy(fatal_rcs[i][0..path.len], path); + fatal_rcs[i][path.len] = 0; + fatal_rc_len[i] = path.len; + } + ninep_io.fatal_cleanup = unlinkOwnedRcs; + return rcs; + } + + /// This process's private prompt files, where a fatal signal's handler + /// can reach them (unlinkOwnedRcs). + var fatal_rcs: [2][rc_path_capacity:0]u8 = undefined; + var fatal_rc_len: [2]usize = .{ 0, 0 }; + + /// Removes this process's private prompt files. Async-signal-safe (two + /// unlinks): SIGTERM and SIGHUP call it before the signal's death. + pub fn unlinkOwnedRcs() void { + for (&fatal_rc_len, 0..) |*len, i| { + const n = len.*; + if (n == 0) continue; + len.* = 0; + _ = libc.unlink(fatal_rcs[i][0..n :0].ptr); + } + } + + /// Removes /tmp's `pardes-osc133-<shell>-<pid>-XXXXXX` files whose pid + /// is gone (kill(pid, 0) answers ESRCH): what a killed or crashed + /// editor left. A live pid's, one of another user's, or a name of + /// another shape is left alone. + fn sweepDeadRcs() void { + const dir = std.c.opendir("/tmp") orelse return; + defer _ = std.c.closedir(dir); + var dead: [64][64]u8 = undefined; + var dead_len: [64]usize = undefined; + var n: usize = 0; + while (std.c.readdir(dir)) |ent| { + const name = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&ent.name)), 0); + const pid = rcPid(name) orelse continue; + if (pid == std.c.getpid()) continue; + if (std.posix.errno(std.c.kill(pid, @enumFromInt(0))) != .SRCH) continue; + if (name.len >= 64 or n == dead.len) continue; + @memcpy(dead[n][0..name.len], name); + dead_len[n] = name.len; + n += 1; + } + for (dead[0..n], dead_len[0..n]) |*entry, len| { + var path_buf: [80:0]u8 = undefined; + const path = std.fmt.bufPrintSentinel(&path_buf, "/tmp/{s}", .{entry[0..len]}, 0) catch continue; + const facts = ninep_io.statNoFollow(path) orelse continue; + if (facts.mode & 0o170000 != 0o100000 or facts.uid != libc.getuid()) continue; + _ = libc.unlink(path.ptr); + } + } + + /// The pid in `pardes-osc133-<shell>-<pid>-XXXXXX`, or null. + fn rcPid(name: []const u8) ?std.c.pid_t { + const head = "pardes-osc133-"; + if (!std.mem.startsWith(u8, name, head)) return null; + var fields = std.mem.splitScalar(u8, name[head.len..], '-'); + _ = fields.next() orelse return null; // the shell + const digits = fields.next() orelse return null; + const rest = fields.next() orelse return null; + if (fields.next() != null or rest.len != 6 or digits.len == 0 or digits.len > 10) return null; + for (digits) |c| if (!std.ascii.isDigit(c)) return null; + const pid = std.fmt.parseInt(std.c.pid_t, digits, 10) catch return null; + return if (pid > 0) pid else null; } fn collectSystemPath(paths_file: [:0]const u8, paths_dir: []const u8, buf: []u8, len: *usize) void { @@ -745,6 +814,12 @@ pub const Shell = struct { } pub fn deinit(rcs: *PromptFiles) void { + // Removed here, not again by a signal's handler. + for (&fatal_rc_len, 0..) |*len, i| for ([_]?[:0]const u8{ rcs.bashPath(), rcs.fishPath() }) |mine| { + if (mine) |path| if (len.* == path.len and std.mem.eql(u8, fatal_rcs[i][0..len.*], path)) { + len.* = 0; + }; + }; if (rcs.bashPath()) |path| if (rcs.bash_owned) { _ = libc.unlink(path.ptr); }; @@ -794,7 +869,9 @@ pub const Shell = struct { 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 }; + // Its pid in the name: one a killed editor left is swept by the + // next (sweepDeadRcs). + const template = std.fmt.bufPrint(&tmp_template, "/tmp/pardes-osc133-{s}-{d}-XXXXXX", .{ name, @as(u32, @intCast(std.c.getpid())) }) catch return .{ .len = 0, .owned = false }; return .{ .len = stage(path_buf, template, contents), .owned = true }; } @@ -1028,6 +1105,27 @@ pub const Shell = struct { try std.testing.expect(libc.access(gone.path, X_OK) == 0); } + test "a private prompt file names its pid, and a dead pid's is swept while a live one's stays" { + if (builtin.os.tag == .windows) return; + const dead = "/tmp/pardes-osc133-bash-2147483647-AbCdEf"; + var live_buf: [80:0]u8 = undefined; + const live = try std.fmt.bufPrintSentinel(&live_buf, "/tmp/pardes-osc133-bash-{d}-AbCdEf", .{@as(u32, @intCast(std.c.getppid()))}, 0); + defer for ([_][:0]const u8{ dead, live }) |path| { + _ = libc.unlink(path.ptr); + }; + for ([_][:0]const u8{ dead, live }) |path| { + const fd = libc.open(path, .{ .CREAT = true, .ACCMODE = .WRONLY }, @as(libc.mode_t, 0o600)); + if (fd < 0) return error.SkipZigTest; + _ = libc.close(fd); + } + sweepDeadRcs(); + try std.testing.expect(libc.access(dead, 0) < 0); + try std.testing.expect(libc.access(live, 0) == 0); + // The pid is read only from that shape: an older name has none. + try std.testing.expectEqual(@as(?std.c.pid_t, 42), rcPid("pardes-osc133-fish-42-Xy12Zq")); + try std.testing.expectEqual(@as(?std.c.pid_t, null), rcPid("pardes-osc133-bash-01z3XK")); + } + 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 saved_buf: [std.fs.max_path_bytes:0]u8 = @splat(0); @@ -1073,6 +1171,7 @@ pub const Shell = struct { 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-")); + try std.testing.expectEqual(@as(?std.c.pid_t, std.c.getpid()), rcPid(std.fs.path.basename(d_bash))); @memcpy(kept[0..d_bash.len], d_bash); kept[d_bash.len] = 0; d.deinit(); diff --git a/src/tty/tty.zig b/src/tty/tty.zig index 8d121b1f..a01df94b 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -1150,8 +1150,10 @@ const Killed = struct { _ = std.c.write(fd, reset, reset.len); _ = std.c.tcsetattr(fd, .FLUSH, &cooked); } - // The 9P socket goes too: a killed editor leaves none behind. + // The 9P socket and private prompt files go too: a killed editor + // leaves none behind. ninep_io.unlinkOwnSocket(); + host_io.Shell.unlinkOwnedRcs(); // SA_RESETHAND put the default back: the same signal, now fatal. _ = std.c.raise(sig); } |
