diff options
Diffstat (limited to 'src/host_io.zig')
| -rw-r--r-- | src/host_io.zig | 76 |
1 files changed, 73 insertions, 3 deletions
diff --git a/src/host_io.zig b/src/host_io.zig index df78f73c..1db0dac9 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -25,6 +25,9 @@ pub const Host = struct { pty_signal: ?*const fn (ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void = null, close_pty: ?*const fn (ctx: ?*anyopaque, pane: u8) void = null, tty_taken: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null, + /// The name of the program holding the tty, when it is not the + /// shell (host_io.foregroundName), in `buf`. + fg_name: ?*const fn (ctx: ?*anyopaque, pane: u8, buf: []u8) ?[]const u8 = null, /// Answered at once, not queued as an effect: Kill asks while the /// job it saw running is still the one in the tty (host_io.killJob). kill_job: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null, @@ -834,7 +837,7 @@ pub const Shell = struct { @memcpy(buf[0..bin.len], bin); buf[bin.len] = 0; const p: [*:0]const u8 = @ptrCast(buf); - return if (libc.access(p, X_OK) == 0) p else null; + return if (libc.access(p, X_OK) == 0 and !isDirectory(p)) p else null; } for (bin_dirs) |dir| { if (dir.len + bin.len + 1 > buf.len) continue; @@ -847,6 +850,26 @@ pub const Shell = struct { return null; } + /// A directory passes access(X_OK) (it can be searched): no shell. + pub fn isDirectory(path: [*:0]const u8) bool { + const d = libc.opendir(path) orelse return false; + _ = libc.closedir(d); + return true; + } + + /// Why `bin` is no shell, for the words that name one (Tty, Shell); + /// null when it is one. + pub fn refusal(bin: []const u8, said: []u8) ?[]const u8 { + var buf: [std.fs.max_path_bytes]u8 = undefined; + if (find(bin, &buf) != null) return null; + if (std.mem.indexOfScalar(u8, bin, '/') != null and bin.len < buf.len) { + @memcpy(buf[0..bin.len], bin); + buf[bin.len] = 0; + if (isDirectory(@ptrCast(&buf))) return std.fmt.bufPrint(said, "not a shell: {s} is a directory", .{bin}) catch "not a shell: a directory"; + } + return std.fmt.bufPrint(said, "no shell \"{s}\" (a name on the usual paths, or a path to one)", .{bin}) catch "no such shell"; + } + fn fallback(buf: *[std.fs.max_path_bytes]u8) [*:0]const u8 { for (fallbacks) |f| { @memcpy(buf[0..f.len], f); @@ -1673,7 +1696,9 @@ fn reapShell(pid: libc.pid_t, grace_ms: u32) void { /// The tty's foreground job is signalled. A command pane's shell (`command`) /// is its command too: its line runs with job control on, so the job /// running is in a group of its own, and the shell's group goes as well, or -/// the rest of the line would run on after the job it was on. +/// the rest of the line would run on after the job it was on. A stop (TERM +/// or KILL) of a command reaches its `&` jobs too, each in a group of its +/// own in the shell's session: Kill leaves nothing the line started. pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal, command: bool) void { const sig = switch (which) { .int => libc.SIG.INT, @@ -1683,11 +1708,42 @@ pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySigna .kill => libc.SIG.KILL, }; const fg = tcgetpgrp(master_fd); + if (command and shell_pid > 0 and (which == .term or which == .kill)) signalSession(shell_pid, sig); if (fg > 0) _ = libc.kill(-fg, sig); if (command and shell_pid > 0 and fg != shell_pid) _ = libc.kill(-shell_pid, sig); if (fg <= 0 and !command and shell_pid > 0) _ = libc.kill(shell_pid, sig); } +/// Every process group in the session `sid` leads, but its own (which the +/// caller signals last). forkpty made the shell the session's leader, and +/// job control moves a job to a new group, never out of the session. +// ponytail: Linux only (a /proc scan); elsewhere a `&` job outlives Kill, +// add sysctl KERN_PROC_SESSION on macOS if that matters. +fn signalSession(sid: libc.pid_t, sig: libc.SIG) void { + if (comptime builtin.os.tag != .linux) return; + const d = libc.opendir("/proc") orelse return; + defer _ = libc.closedir(d); + var buf: [4096]u8 = undefined; + while (libc.readdir(d)) |ent| { + const name = std.mem.sliceTo(&ent.name, 0); + const pid = std.fmt.parseInt(libc.pid_t, name, 10) catch continue; + var path_buf: [64:0]u8 = undefined; + const path = std.fmt.bufPrintSentinel(&path_buf, "/proc/{d}/stat", .{pid}, 0) catch continue; + const ids = parseGroupSession(readProc(path, &buf) orelse continue) orelse continue; + if (ids.session == sid and ids.pgrp != sid) _ = libc.kill(-ids.pgrp, sig); + } +} + +fn parseGroupSession(stat: []const u8) ?struct { pgrp: libc.pid_t, session: libc.pid_t } { + const close = std.mem.lastIndexOfScalar(u8, stat, ')') orelse return null; + var fields = std.mem.tokenizeAny(u8, stat[close + 1 ..], " \t\n"); + _ = fields.next() orelse return null; // 3: state + _ = fields.next() orelse return null; // 4: ppid + const pgrp = std.fmt.parseInt(libc.pid_t, fields.next() orelse return null, 10) catch return null; + const session = std.fmt.parseInt(libc.pid_t, fields.next() orelse return null, 10) catch return null; + return .{ .pgrp = pgrp, .session = session }; +} + /// Kill: SIGTERM to the job in the tty's foreground, never to the shell. /// The shell leads its own group, and a job it runs has another; with job /// control off (`set +m`, a shell run as `sh -c`) the job shares the shell's @@ -1698,6 +1754,18 @@ pub fn killJob(shell_pid: libc.pid_t, master_fd: c_int) bool { return libc.kill(-fg, libc.SIG.TERM) == 0; } +/// The command name of the tty's foreground job, when that is not the +/// shell: what `pty/run` says is running. Linux only (/proc/<pgrp>/comm). +pub fn foregroundName(shell_pid: libc.pid_t, master_fd: c_int, buf: []u8) ?[]const u8 { + if (comptime builtin.os.tag != .linux) return null; + const fg = tcgetpgrp(master_fd); + if (fg <= 0 or fg == shell_pid) return null; + var name: [64:0]u8 = undefined; + const path = std.fmt.bufPrintSentinel(&name, "/proc/{d}/comm", .{@as(u32, @intCast(fg))}, 0) catch return null; + const comm = std.mem.trimEnd(u8, readProc(path, buf) orelse return null, "\n"); + return if (comm.len > 0) comm else null; +} + fn readProc(path: [*:0]const u8, buf: []u8) ?[]const u8 { const fd = libc.open(path, .{ .ACCMODE = .RDONLY }); if (fd < 0) return null; @@ -2076,7 +2144,7 @@ test "a command's exit is its process's, told by its watcher, whether or not its } } -test "a command's background job outlives it and its pty, and Kill stops its whole line" { +test "a command's background job outlives it and its pty, and Kill stops its whole line, its & jobs too" { if (comptime !tty_probe_platform) return error.SkipZigTest; if (!haveFile("/bin/sh")) return error.SkipZigTest; const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); @@ -2094,6 +2162,8 @@ test "a command's background job outlives it and its pty, and Kill stops its who .{ .line = "(sleep 0.4; touch left) & exit 0", .kill = false, .made = true }, // The job running is in its own group; the shell goes too. .{ .line = "sleep 0.4; touch killed", .kill = true, .made = false }, + // Kill stops the whole command, its `&` jobs as well. + .{ .line = "(sleep 0.4; touch killed) & sleep 5", .kill = true, .made = false }, }) |c| { const id = core.freeSlot().?; _ = try core.newCommand(id, dir, c.line); |
