diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/host_io.zig | 39 |
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); |
