summaryrefslogtreecommitdiff
path: root/src/detached/server.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/detached/server.zig')
-rw-r--r--src/detached/server.zig77
1 files changed, 69 insertions, 8 deletions
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 9e18e125..5374bf73 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -70,8 +70,18 @@ const Pty = struct {
pid: posix.pid_t = 0,
kill_at: i64 = 0,
out: std.ArrayListUnmanaged(u8) = .empty,
+ /// A command pane's child, watched to its exit (host_io.watchExit);
+ /// the pty stays open, unpolled after its end of file, until both its
+ /// end of file and its exit.
+ cmd: host_io.CommandWatch = .{},
};
+/// A watched child exited: wake the loop, which takes it (`takeExits`).
+fn wakeForExit(ctx: ?*anyopaque) void {
+ const box: *Mailbox = @ptrCast(@alignCast(ctx.?));
+ box.signal();
+}
+
const RetiredShell = struct { pid: posix.pid_t = 0, kill_at: i64 = 0 };
const Completion = union(enum) {
@@ -190,6 +200,7 @@ pub const Session = struct {
}
s.mailbox.wake = pair;
pardes.lsp.setStatusSink(s, lspStatus);
+ host_io.setExitWake(&s.mailbox, wakeForExit);
}
fn cancelWorkers(s: *Session) void {
@@ -235,8 +246,8 @@ pub const Session = struct {
return batch.len != 0 or batch.status != null;
}
- pub fn restore(s: *Session, bytes: []const u8) !void {
- const replacement = try dump.restore(s.core, bytes);
+ pub fn restore(s: *Session, bytes: []const u8, from: []const u8) !void {
+ const replacement = try dump.restore(s.core, bytes, from);
s.cancelWorkers();
for (0..s.ptys.len) |pane| s.closePty(@intCast(pane));
s.harvest();
@@ -257,6 +268,7 @@ pub const Session = struct {
pub fn deinit(s: *Session) void {
if (s.mailbox.wake[0] >= 0) pardes.lsp.setStatusSink(null, null);
+ if (s.mailbox.wake[0] >= 0) host_io.setExitWake(null, null);
s.cancelWorkers();
for (s.mailbox.wake) |fd| if (fd >= 0) {
_ = libc.close(fd);
@@ -479,7 +491,8 @@ pub const Session = struct {
s.core.screen_w,
s.ninep,
) catch |err| return s.core.reportError(pane, "shell", err);
- s.ptys[pane] = .{ .fd = child.file.handle, .pid = child.pid };
+ const command = if (s.core.panes[pane]) |pn| pn.command != null else false;
+ s.ptys[pane] = .{ .fd = child.file.handle, .pid = child.pid, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } };
setNonblock(child.file.handle);
var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined;
if (host_io.shellCwd(child.pid, &lbuf)) |wd| s.core.setCwd(pane, wd);
@@ -518,7 +531,8 @@ pub const Session = struct {
const s = of(ctx);
if (pane >= s.ptys.len) return;
const pt = s.ptys[pane];
- if (pt.fd < 0) return;
+ // A command whose exit is recorded is reaped: its group may be another's.
+ if (pt.fd < 0 or pt.cmd.exited) return;
host_io.signalTty(pt.pid, pt.fd, sig);
}
@@ -687,11 +701,33 @@ pub const Session = struct {
}
fn paneEof(s: *Session, pane: u8) void {
+ const pt = &s.ptys[pane];
+ // A command's pty stays open until its child has exited: closing
+ // it would hang up one that runs on without it.
+ // Its exit, if it came first, is told now its output is in.
+ if (pt.cmd.watched) return host_io.commandEof(s.core, &s.ptys, pane, s, closeWatched);
+ // Unwatched, a command's exit is read here, as its end.
+ const unwatched = if (s.core.panes[pane]) |pn| pn.command != null else false;
+ var status: ?u8 = null;
+ if (unwatched) {
+ status = host_io.exitStatus(pt.pid, 100);
+ if (status != null) pt.pid = 0; // reaped: no shell to retire
+ }
s.closePty(pane);
s.harvest();
+ if (unwatched) s.core.update(.{ .exited = .{ .pane = pane, .status = status } });
s.core.update(.{ .eof = .{ .pane = pane } });
}
+ /// The command panes' exits, told once their output is in (host_io).
+ fn takeExits(s: *Session) void {
+ host_io.takeExits(s.core, &s.ptys, s, closeWatched);
+ }
+
+ fn closeWatched(s: *Session, id: usize) void {
+ s.closePty(@intCast(id));
+ }
+
fn harvest(s: *Session) void {
const now = monotonicMs();
for (&s.retired_shells) |*shell| reapShell(&shell.pid, &shell.kill_at, now);
@@ -780,6 +816,10 @@ pub const Session = struct {
const completed = s.ninep_wake.swap(false, .acq_rel) or drained;
for (&s.clients) |*c| if (c.fd >= 0) s.flush(c);
const regridded = s.reconcile();
+ // Nobody attached, nobody to dismiss a message: the loop expires
+ // them itself, between the editor's steps (expireUnattended).
+ s.core.unattended = s.primary() == null;
+ pardes.Messages.expireUnattended(s.core);
const now = monotonicMs();
var fds: [poll_slots]libc.pollfd = undefined;
@@ -807,7 +847,7 @@ pub const Session = struct {
n += 1;
}
for (&s.ptys, 0..) |*pt, pane| {
- if (pt.fd < 0) continue;
+ if (pt.fd < 0 or pt.cmd.eof) continue;
fds[n] = .{
.fd = pt.fd,
.events = if (pt.out.items.len != 0) poll_in | poll_out else poll_in,
@@ -854,6 +894,7 @@ pub const Session = struct {
virtual.* = @max(virtual.*, s.core.nextWake() orelse virtual.*);
};
_ = s.drainCompletions(true);
+ s.takeExits();
s.expire(monotonicMs());
s.harvest();
s.reloadWatched();
@@ -914,6 +955,11 @@ pub const Session = struct {
due = if (due) |d| @min(d, at9) else at9;
}
}
+ // Unattended messages go on the clock: look again in a while.
+ if (s.core.unattended) for (s.core.panes) |slot| if (slot) |pane| if (pane.msg_len > 0) {
+ due = if (due) |d| @min(d, now + 100) else now + 100;
+ break;
+ };
const at = due orelse return null;
return @intCast(@max(0, @min(at - now, std.math.maxInt(c_int))));
}
@@ -1135,6 +1181,17 @@ pub const Session = struct {
}
};
+/// SIGTERM, SIGINT or SIGHUP: quit as Exit does, through the loop, so its
+/// sockets are unlinked on the way out rather than left for the next
+/// session to trip on. The handler only sets the flag and wakes the loop.
+var quit_signalled = std.atomic.Value(bool).init(false);
+var quit_wake: c_int = -1;
+
+fn onQuitSignal(_: std.posix.SIG) callconv(.c) void {
+ quit_signalled.store(true, .release);
+ if (quit_wake >= 0) _ = libc.send(quit_wake, "q", 1, nosignal);
+}
+
pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void {
const gpa = init.gpa;
const allocs = pardes.memory.init(gpa);
@@ -1186,7 +1243,11 @@ pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void
core.host = h;
while (core.nextEffect()) |effect| core.perform(effect);
session.in_loop = true;
- while (!session.core.quit) {
+ quit_wake = session.mailbox.wake[1];
+ const on_quit: std.posix.Sigaction = .{ .handler = .{ .handler = onQuitSignal }, .mask = std.posix.sigemptyset(), .flags = 0 };
+ for ([_]std.posix.SIG{ .HUP, .INT, .TERM }) |sig| std.posix.sigaction(sig, &on_quit, null);
+ defer quit_wake = -1;
+ while (!session.core.quit and !quit_signalled.load(.acquire)) {
pardes.turn.restoreSettled();
try session.core.pump(h);
if (session.core.quit) break;
@@ -1196,7 +1257,7 @@ pub fn run(init: std.process.Init, opts: pardes.Options, name: []const u8) !void
break :restore;
};
defer gpa.free(bytes);
- session.restore(bytes) catch |err| session.core.reportError(session.core.active, "Restore", err);
+ session.restore(bytes, path) catch |err| session.core.reportError(session.core.active, "Restore", err);
}
}
}
@@ -1236,7 +1297,7 @@ test "detached queued results preserve current requests and are discarded before
try std.testing.expect(s.pipe_tasks.add(.{ .id = 77, .future = .{ .any_future = null, .result = {} } }));
s.mailbox.post(.{ .pipe = .{ .id = 77, .success = true, .outputs = outputs } });
Session.lspStatus(&s, "old status");
- try s.restore(saved);
+ try s.restore(saved, "test");
try std.testing.expect(s.lsp_task == null);
try std.testing.expectEqual(@as(usize, 0), s.pipe_tasks.len);
try std.testing.expect(!s.drainCompletions(true));