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.zig88
1 files changed, 79 insertions, 9 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 25bd61e1..df78f73c 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -792,7 +792,7 @@ pub const Shell = struct {
pub const Spawn = struct {
path: [*:0]const u8,
- argv: [4:null]?[*:0]const u8,
+ argv: [6:null]?[*:0]const u8,
};
// Resolve in the parent. The path buffer and prompt files must survive through exec.
@@ -803,7 +803,24 @@ pub const Shell = struct {
.fish => if (prompt_rcs.fishCommand()) |command| .{ "-C", command.ptr } else .{ null, null },
.none => .{ null, null },
};
- return .{ .path = path, .argv = .{ path, marks[0], marks[1], null } };
+ return .{ .path = path, .argv = .{ path, marks[0], marks[1], null, null, null } };
+ }
+
+ /// A command pane's shell runs its one line with job control on, so a
+ /// job the line puts in the background has a process group of its own.
+ /// The shell leads the terminal's session, and when it exits the kernel
+ /// hangs up the terminal's foreground group: with job control off that
+ /// is the shell's own, the job's too, and the job died with the command
+ /// (nohup'd or not; only setsid got out, and not always in time). A
+ /// shell pardes does not know runs the line as it is.
+ pub fn commandArgv(path: [*:0]const u8, line: [*:0]const u8) [6:null]?[*:0]const u8 {
+ const bin = std.mem.span(path);
+ const base = if (std.mem.lastIndexOfScalar(u8, bin, '/')) |s| bin[s + 1 ..] else bin;
+ const posix_shells = [_][]const u8{ "bash", "sh", "dash", "zsh", "ksh", "mksh", "yash" };
+ for (posix_shells) |name| if (std.mem.eql(u8, base, name))
+ return .{ path, "-m", "-c", line, null, null };
+ if (family(bin) == .fish) return .{ path, "-C", "status job-control full", "-c", line, null };
+ return .{ path, "-c", line, null, null, null };
}
/// A shell by its path or its name in the usual directories, or null.
@@ -840,6 +857,20 @@ pub const Shell = struct {
return @ptrCast(buf);
}
+ test "a command pane's line runs with job control on in the shells that have it" {
+ const want = struct {
+ fn check(path: [*:0]const u8, words: []const []const u8) !void {
+ const argv = commandArgv(path, "sleep 9 &");
+ for (words, 0..) |w, i| try std.testing.expectEqualStrings(w, std.mem.span(argv[i].?));
+ try std.testing.expect(argv[words.len] == null);
+ }
+ };
+ try want.check("/usr/bin/bash", &.{ "/usr/bin/bash", "-m", "-c", "sleep 9 &" });
+ try want.check("/bin/sh", &.{ "/bin/sh", "-m", "-c", "sleep 9 &" });
+ try want.check("/usr/bin/fish", &.{ "/usr/bin/fish", "-C", "status job-control full", "-c", "sleep 9 &" });
+ try want.check("/usr/bin/nu", &.{ "/usr/bin/nu", "-c", "sleep 9 &" });
+ }
+
test "a path is taken at its word, a name is looked up, and both pick their own marks" {
if (builtin.os.tag == .windows) return;
var buf: [std.fs.max_path_bytes]u8 = undefined;
@@ -1070,7 +1101,7 @@ pub fn forkShell(
if (line.len > exec_command_max or std.mem.indexOfScalar(u8, line, 0) != null) return error.InvalidCommand;
@memcpy(command_buf[0..line.len], line);
command_buf[line.len] = 0;
- spawn.argv = .{ spawn.path, "-c", command_buf[0..line.len :0].ptr, null };
+ spawn.argv = Shell.commandArgv(spawn.path, command_buf[0..line.len :0].ptr);
}
var helper_buf: [4096]u8 = undefined;
const mounted = one_line == null and if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false;
@@ -1639,7 +1670,11 @@ fn reapShell(pid: libc.pid_t, grace_ms: u32) void {
while (libc.waitpid(pid, null, 0) < 0 and libc.errno(-1) == .INTR) {}
}
-pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal) 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.
+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,
.term => libc.SIG.TERM,
@@ -1648,11 +1683,9 @@ 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 (fg > 0) {
- _ = libc.kill(-fg, sig);
- return;
- }
- if (shell_pid > 0) _ = libc.kill(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);
}
/// Kill: SIGTERM to the job in the tty's foreground, never to the shell.
@@ -2043,6 +2076,43 @@ 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" {
+ 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 });
+ defer core.deinit();
+ while (core.nextEffect()) |_| {}
+ const rcs: Shell.PromptFiles = .{};
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ var dir_buf: [4096]u8 = undefined;
+ const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)];
+ const Case = struct { line: []const u8, kill: bool, made: bool };
+ for ([_]Case{
+ // The shell leads the pty's session; its exit hangs up only the
+ // foreground group, which the job, in a group of its own, is not.
+ .{ .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 },
+ }) |c| {
+ const id = core.freeSlot().?;
+ _ = try core.newCommand(id, dir, c.line);
+ while (core.nextEffect()) |_| {}
+ const child = try forkShell(core, id, &rcs, "/bin/sh", dir, 24, 80, null);
+ if (c.kill) {
+ sleepMs(150);
+ signalTty(child.pid, child.file.handle, .term, true);
+ }
+ _ = exitStatus(child.pid, 2000);
+ _ = libc.close(child.file.handle); // the pane closes: the pty hangs up
+ sleepMs(800);
+ const name = if (c.kill) "killed" else "left";
+ const made = if (tmp.dir.statFile(std.testing.io, name, .{})) |_| true else |_| false;
+ try std.testing.expectEqual(c.made, made);
+ try core.removePane(id, null);
+ }
+}
+
test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" {
const pid = libc.fork();
if (pid == 0) {