summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/9p_io.zig5
-rw-r--r--src/host_io.zig103
-rw-r--r--src/tty/tty.zig4
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);
}