summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/dump.zig2
-rw-r--r--src/exec.zig2
-rw-r--r--src/host_io.zig4
-rw-r--r--src/ninep/pty.zig2
-rw-r--r--src/pardes.zig43
5 files changed, 46 insertions, 7 deletions
diff --git a/src/dump.zig b/src/dump.zig
index 3d0f0f0e..954a3270 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -748,7 +748,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo
p.installPane(i, restored);
try restored.setOwnedCwd(t.cwd);
if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.body.mode = .tty;
- if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .cwd = .from(t.cwd) } });
+ if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .serial = restored.serial, .cwd = .from(t.cwd) } });
break :terminal restored;
},
.file => try pardes.panes.File.restore(p, i, src),
diff --git a/src/exec.zig b/src/exec.zig
index e2b9f880..86adfeeb 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -430,7 +430,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
var buf: [command_max + 8]u8 = undefined;
const lead = if (panes.Terminal.gridCursor(pane).x != 0) "\r\n" else "";
panes.Terminal.feedOutput(p, pane, std.fmt.bufPrint(&buf, "{s}% {s}\r\n", .{ lead, line }) catch "%\r\n");
- p.emit(.{ .spawn = .{ .pane = @intCast(id), .cwd = .from(pane.cwdSlice()) } });
+ p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } });
noteRun(p, pane, "run", line);
return id;
}
diff --git a/src/host_io.zig b/src/host_io.zig
index 63b907ac..b167ef8a 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -965,6 +965,7 @@ pub fn forkShell(
const native_cwd = filesystem.localPath(cwd) orelse cwd;
if (std.mem.indexOfScalar(u8, native_cwd, 0) != null) return error.InvalidPath;
var cwd_buf: [4096]u8 = undefined;
+ const serial = if (core) |c| if (c.panes[pane]) |pn| pn.serial else 0 else 0;
const cwd_z: ?[:0]const u8 = if (native_cwd.len == 0) null else dir: {
const path = std.fmt.bufPrintSentinel(&cwd_buf, "{s}", .{native_cwd}, 0) catch return error.NameTooLong;
// A shell's directory may be inside a mount this editor serves.
@@ -974,6 +975,9 @@ pub fn forkShell(
if (stat.kind != .directory) return error.NotDir;
break :dir path;
};
+ // The turn was given up for the stat: a 9P client may have closed the
+ // pane, and another taken its slot, whose command this is not.
+ if (core) |c| if ((if (c.panes[pane]) |pn| pn.serial else 0) != serial) return error.PaneGone;
var master: c_int = -1;
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
// A terminal opened on a shell of its own (`Tty fish`) runs that one.
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index dcf9a8a7..6065dab2 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -71,7 +71,7 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool {
if (words.next() != null) return false;
if (pane.cwdSlice().len > pardes.effect_path_cap) return false;
if (!apply) return true;
- p.emit(.{ .spawn = .{ .pane = @intCast(id), .cwd = .from(pane.cwdSlice()) } });
+ p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } });
},
}
return true;
diff --git a/src/pardes.zig b/src/pardes.zig
index e92fb3f7..7863e272 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1361,6 +1361,36 @@ test "Exec in a terminal whose tty is taken runs as a command pane instead of ty
try std.testing.expect(spawned);
}
+test "a spawn is for the pane it was made for, not whatever took the slot since" {
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .tty_only = true });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const Count = struct {
+ var spawns: usize = 0;
+ var command: [32]u8 = undefined;
+ var core: *Pardes = undefined;
+ const vtable: Host.VTable = .{ .spawn = spawn };
+ fn spawn(_: ?*anyopaque, pane: u8, _: []const u8) void {
+ spawns += 1;
+ const line = core.panes[pane].?.command.?;
+ @memcpy(command[0..line.len], line);
+ }
+ };
+ Count.core = p;
+ p.host = .{ .vtable = &Count.vtable };
+ // A command pane closed and its slot taken, in one step, before the
+ // effects run: only the second's spawn is performed.
+ const slot = p.freeSlot().?;
+ _ = try p.newCommand(slot, "", "first");
+ try p.removePane(slot, null);
+ try std.testing.expectEqual(slot, p.freeSlot().?);
+ _ = try p.newCommand(slot, "", "second");
+ while (p.nextEffect()) |effect| p.perform(effect);
+ try std.testing.expectEqual(@as(usize, 1), Count.spawns);
+ try std.testing.expectEqualStrings("second", Count.command[0.."second".len]);
+}
+
test "Tty+fish, one word a tag can hold, opens a terminal on that shell" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
@@ -3136,7 +3166,10 @@ pub const WatchMode = enum { reconcile, baseline_disk };
pub const effect_path_cap = 256;
pub const Effect = union(enum) {
- spawn: struct { pane: u8, cwd: Buf(effect_path_cap) },
+ /// A child for the pane in slot `pane` -- the one whose serial is
+ /// `serial`: a pane closed and its slot taken before the effect is
+ /// performed is not the one it was for (perform).
+ spawn: struct { pane: u8, serial: u32, cwd: Buf(effect_path_cap) },
write: struct { pane: u8, bytes: Buf(64) },
resize_pty: struct { pane: u8, cols: u16, rows: u16 },
signal_pty: struct { pane: u8, sig: PtySignal },
@@ -4243,7 +4276,7 @@ pub const Pardes = struct {
const pane = try panes.Terminal.create(p.gpa, p.screen_w, p.screen_h);
panes.Terminal.armShellSpawn(pane);
p.installPane(id, pane);
- p.emit(.{ .spawn = .{ .pane = @intCast(id), .cwd = .from(cwd) } });
+ p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(cwd) } });
return pane;
}
@@ -4262,7 +4295,7 @@ pub const Pardes = struct {
// directory's commands reuse it; the child's own cd does not move it.
pane.setOwnedCwd(cwd) catch {};
p.installPane(id, pane);
- p.emit(.{ .spawn = .{ .pane = @intCast(id), .cwd = .from(cwd) } });
+ p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(cwd) } });
return pane;
}
@@ -4608,7 +4641,9 @@ pub const Pardes = struct {
pub fn perform(p: *Pardes, e: Effect) void {
const v = p.host.vtable;
switch (e) {
- .spawn => |s| if (v.spawn) |f| f(p.host.ctx, s.pane, s.cwd.slice()) else {
+ // The host reads the pane (its command, its shell) as it forks,
+ // so the slot must still hold the pane the spawn was for.
+ .spawn => |s| if (if (p.panes[s.pane]) |pane| pane.serial != s.serial else true) {} else if (v.spawn) |f| f(p.host.ctx, s.pane, s.cwd.slice()) else {
p.fallback.spawned[s.pane] = true;
},
// A pane with no child is silent: nothing invents output on its