summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/host_io.zig')
-rw-r--r--src/host_io.zig76
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);