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.zig39
1 files changed, 37 insertions, 2 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index df78f73c..f83c7d47 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1673,7 +1673,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 +1685,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
@@ -2076,7 +2109,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 +2127,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);