From e7aaeca8e12887e2b90aae257c41efce6847c26d Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 04:52:47 -0300 Subject: A command pane an exec open was answered is leased to it while that open stays open: another client's command in the same directory gets a pane of its own A finished command pane was the next command's for its directory, from any client, so with four clients scripting one session, one read another's output as its own last run. An open of a look or exec file whose write was answered a pane's serial now holds that pane: reuse passes over it and a new command pane runs the line. The open's own next command may run there again, and once it is closed the directory's one command pane is reused as before. Co-Authored-By: Claude Opus 5.5 --- src/exec.zig | 16 ++++++++++++++-- src/ninep/ctl.zig | 28 ++++++++++++++++++++++++++++ src/ninep/tree.zig | 3 +++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/exec.zig b/src/exec.zig index 47484105..ca48bed1 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -766,10 +766,13 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { // would hang that job up. // From a column's tag, only a finished pane in that column is reused, // as the new one goes in that column (placeNew). - const reuse: ?usize = if (!p.exec_header and p.exec_column == null and src != null and src.?.command != null and src.?.command_done and !src.?.command_pty) from else for (p.panes, 0..) |slot, i| { + // Nor one leased: an open that was answered its serial and is still + // open reads that pane's output, which another client's command must + // not replace; a new pane runs it instead. + const reuse: ?usize = if (!p.exec_header and p.exec_column == null and src != null and src.?.command != null and src.?.command_done and !src.?.command_pty and !leased(p, src.?.serial)) from else for (p.panes, 0..) |slot, i| { const other = slot orelse continue; if (p.exec_column) |c| if (if (layout.findPane(p, i)) |f| f.col != c else true) continue; - if (other.command != null and other.command_done and !other.command_pty and std.mem.eql(u8, other.cwdSlice(), dir)) break i; + if (other.command != null and other.command_done and !other.command_pty and std.mem.eql(u8, other.cwdSlice(), dir) and !leased(p, other.serial)) break i; } else null; if (reuse) |id| { const pane = p.panes[id].?; @@ -812,6 +815,15 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { return free; } +/// Whether an open of a look or exec file holds `serial` from its own write +/// (tree.Open.results): its client is reading that pane. +fn leased(p: *const Pardes, serial: u32) bool { + for (p.fs.opens) |o| if (o.node != 0 and o.results.wrote) { + if (std.mem.indexOfScalar(u32, o.results.list[0..o.results.len], serial) != null) return true; + }; + return false; +} + /// The serial a command's `$winid` names: the pane it was run from, none /// (0) from a column's or the workspace's tag. fn winid(p: *const Pardes, src: ?*Pane) u32 { diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index bcf9ffd6..c88eb328 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -4028,3 +4028,31 @@ test "pager answers its +Pager on the open that asked, whatever another client e // One directory a write. try testing.expectEqual(E.INVAL, wr(p, pager, "/a\n/b\n").errno()); } + +test "a command pane an exec open was answered is leased to it while it stays open: another client's command gets a pane of its own" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const exec = Node.of(serialOf(p), .exec); + const Client = struct { + fn run(pp: *Pardes, node: u64, h: u32, line: []const u8) !usize { + try testing.expectEqual(Status.ok, call(pp, .{ .tag = 2, .op = .write, .node = node, .handle = h, .data = line }).reply.status); + const id = pp.paneBySerial(pp.fs.results[0]).?; + // Its command finishes and its output ends. + pp.panes[id].?.command_done = true; + pp.panes[id].?.command_pty = false; + return id; + } + }; + const a = call(p, .{ .tag = 1, .op = .open, .node = exec, .omode = 2 }).reply.handle; + const b = call(p, .{ .tag = 1, .op = .open, .node = exec, .omode = 2 }).reply.handle; + const a_pane = try Client.run(p, exec, a, "echo a\n"); + const b_pane = try Client.run(p, exec, b, "echo b\n"); + try testing.expect(a_pane != b_pane); + // Each one's next command runs in its own pane again. + try testing.expectEqual(a_pane, try Client.run(p, exec, a, "echo a2\n")); + try testing.expectEqual(b_pane, try Client.run(p, exec, b, "echo b2\n")); + // Closed, a lease ends: the directory's one command pane is reused. + _ = call(p, .{ .tag = 3, .op = .release, .node = exec, .handle = a, .opened = true }); + try testing.expectEqual(Status.ok, wr(p, exec, "echo c\n").reply.status); + try testing.expectEqual(a_pane, p.paneBySerial(p.fs.results[0]).?); +} diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 9c3e1e9c..e5d1c848 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -1212,6 +1212,9 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { o.pending.shrinkRetainingCapacity(rest); var w = req; w.data = whole; + // The command pane this open was answered holds no lease against its + // own next command (exec.leased): that one may run there again. + if (resultsFile(target)) o.results.len = 0; var reply = writeNow(p, w, target); if (resultsFile(target)) { @memcpy(o.results.list[0..p.fs.results_len], p.fs.results[0..p.fs.results_len]); -- cgit v1.3