diff options
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); } |
