diff options
| -rw-r--r-- | docs/fs.md | 6 | ||||
| -rw-r--r-- | src/detached/server.zig | 2 | ||||
| -rw-r--r-- | src/exec.zig | 22 | ||||
| -rw-r--r-- | src/gui/gui.zig | 2 | ||||
| -rw-r--r-- | src/macos.zig | 2 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 31 | ||||
| -rw-r--r-- | src/ninep/pty.zig | 5 | ||||
| -rw-r--r-- | src/panes.zig | 3 | ||||
| -rw-r--r-- | src/pardes.zig | 11 | ||||
| -rw-r--r-- | src/tty/tty.zig | 2 |
10 files changed, 78 insertions, 8 deletions
@@ -885,8 +885,10 @@ carries no reason or code, and the shell's own complaint is in the pane's body (`tail body`); `exit N` and what it printed when the line ended the shell itself (`exit 3`, or `echo bye; exit 3`): its terminal closes, and a read of the run's open still answers after the pane is gone; `error shell gone` -when the pane closed or its shell was replaced, or the shell went without -an exit status to tell; `error no prompt marks` for a shell pardes could not instrument; +when the pane closed or its shell was replaced, the shell went without +an exit status to tell, or it never started (a `Tty` in a directory that is +not there is refused before, `Tty: <dir>: no such directory`, and makes no +pane); `error no prompt marks` for a shell pardes could not instrument; `error command done; not a shell` (or `error a command runs here, not a shell`) on a command pane, whose child is its command. It relies on the OSC 133 marks pardes injects into bash and fish, tagged diff --git a/src/detached/server.zig b/src/detached/server.zig index 13cf4488..7ce8a1d0 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -491,7 +491,7 @@ pub const Session = struct { s.core.screen_h, s.core.screen_w, s.ninep, - ) catch |err| return s.core.reportError(pane, "shell", err); + ) catch |err| return s.core.shellFailed(pane, err); 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); diff --git a/src/exec.zig b/src/exec.zig index 0bce67d2..6e437093 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -341,11 +341,20 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void { fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { if (p.panes[from] == null) return null; + // A directory that is not there makes no terminal: its shell could not + // start in it, and a dead pane would answer no run. Asked only of a host + // that starts shells (none, as in the core's tests, starts none). + const dir = p.execDir(from); + if (comptime hosted) if (p.host.vtable.spawn != null) if (@import("fs.zig").localPath(dir)) |local| if (!isDirectory(local)) { + var said: [pardes.memory.limits.host_path_cap + 48]u8 = undefined; + p.reportFailure(from, std.fmt.bufPrint(&said, "Tty: {s}: no such directory", .{dir}) catch "Tty: no such directory"); + return null; + }; const free = p.freeSlot() orelse { p.reportError(from, "Tty", error.NoPaneSlots); return null; }; - const nt = p.newShell(free, p.execDir(from)) catch |err| { + const nt = p.newShell(free, dir) catch |err| { if (err == error.PathTooLong) { p.reportFailure(from, std.fmt.comptimePrint("Tty: its directory is longer than a path may be ({d} bytes)", .{pardes.memory.limits.host_path_cap})); return null; @@ -360,6 +369,17 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { return nt; } +/// Whether `path` is a directory on this host, the turn out while it asks +/// (it may be inside a mount this editor serves). +fn isDirectory(path: []const u8) bool { + var buf: [4096]u8 = undefined; + const z = std.fmt.bufPrintSentinel(&buf, "{s}", .{path}, 0) catch return false; + pardes.turn.yield(); + defer pardes.turn.back(); + const stat = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), z, .{}) catch return false; + return stat.kind == .directory; +} + /// With `BootShell replace`, a doc landing in `col` closes a lone pristine /// shell there, the boot's placeholder, and absorbVWeight hands its space to /// the doc. With `keep`, the default, the shell stays like any other pane. diff --git a/src/gui/gui.zig b/src/gui/gui.zig index bd2dca69..d36cf187 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -4519,7 +4519,7 @@ fn spawnPane(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void { s.closePty(pane); if (s.ptys[pane] != null) return s.core.reportError(pane, "shell", error.WorkersBusy); s.gens[pane] +%= 1; - const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.reportError(pane, "shell", err); + const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.shellFailed(pane, err); const command = s.core.panes[pane].?.command != null; const pt: Pty = .{ .fd = child.file.handle, .pid = child.pid, .serial = s.core.panes[pane].?.serial, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } }; s.ptys[pane] = pt; diff --git a/src/macos.zig b/src/macos.zig index 4ff13b23..75834844 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -2441,7 +2441,7 @@ fn spawnShell(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void { st.gens[pane] +%= 1; const gen = st.gens[pane]; - const child = host_io.forkShell(core, pane, &st.prompt_rcs, core.shellBin(), cwd, core.screen_h, core.screen_w, st.ninep) catch |err| return core.reportError(pane, "shell", err); + const child = host_io.forkShell(core, pane, &st.prompt_rcs, core.shellBin(), cwd, core.screen_h, core.screen_w, st.ninep) catch |err| return core.shellFailed(pane, err); st.ptys[pane] = .{ .file = child.file, .pid = child.pid, diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 6a8a37be..9bb9c044 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1746,6 +1746,37 @@ test "a root exec that fails logs its err against the pane it ran at" { try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "err {d} exec: ", .{serialOf(p)}))); } +test "Tty in a directory that is not there fails and makes no pane; a shell that never started answers a run" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + _ = wr(p, Node.of(serial, .name), "/tmp/pardes-no-such-dir-zz/f.txt\n"); + // A host that starts shells (the core alone starts none, and asks + // nothing of a directory). + const Starting = struct { + fn spawn(_: ?*anyopaque, _: u8, _: []const u8) void {} + }; + p.host = .{ .vtable = &.{ .spawn = Starting.spawn } }; + defer p.host = .{}; + var count: usize = 0; + for (p.panes) |slot| count += @intFromBool(slot != null); + const refused = wr(p, Node.of(serial, .ctl), "Tty\n"); + try testing.expectEqual(E.IO, refused.errno()); + try testing.expectStringStartsWith(refused.reply.ename, "Tty: /tmp/pardes-no-such-dir-zz: no such directory"); + var after: usize = 0; + for (p.panes) |slot| after += @intFromBool(slot != null); + try testing.expectEqual(count, after); + // A terminal whose shell the host could not start. + _ = wr(p, Node.of(serial, .name), "/tmp/f.txt\n"); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Tty\n").reply.status); + const term = p.active; + p.shellFailed(@intCast(term), error.FileNotFound); + const run_node = Node.of(p.panes[term].?.serial, .pty_run); + const h = call(p, .{ .tag = 1, .op = .open, .node = run_node, .omode = 2 }).reply.handle; + _ = call(p, .{ .tag = 2, .op = .write, .node = run_node, .handle = h, .data = "true\n" }); + try testing.expectEqualStrings("error shell gone\n", call(p, .{ .tag = 3, .op = .read, .node = run_node, .handle = h, .size = 64 }).bytes); +} + test "size is monotonic: growing is never refused, and a size once taken is taken again" { const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 }); defer p.deinit(); diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 4009af49..68f98554 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -196,7 +196,10 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { const pf = &pane.fs; const state = pane.terminal orelse return tree.failText(req.tag, E.INVAL, e_bad_line); const marks = &state.stream.handler; - if (pane.command != null) { + if (pane.shell_failed) { + // Its shell never started: no prompt is coming to wait for. + answer(p, slot, "error shell gone", .{}); + } else if (pane.command != null) { // Its child is the command, and no shell is there to take a line. answer(p, slot, "error {s}", .{if (pane.command_done) "command done; not a shell" else "a command runs here, not a shell"}); } else if (pf.unmarked) { diff --git a/src/panes.zig b/src/panes.zig index 7aea5898..41ba636f 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -106,6 +106,9 @@ pub const Pane = struct { cols: u16, rows: u16, greet: bool = false, + /// Its shell could not be started (Pardes.shellFailed): a run hears + /// `error shell gone` at once. + shell_failed: bool = false, pending_command: Terminal.PendingCommand = .{}, /// The last command line pardes typed into this terminal (an exec, a /// middle click, a pty/run): its first word, which Kill matches, and diff --git a/src/pardes.zig b/src/pardes.zig index 002ff6f9..3a574dc9 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5010,6 +5010,7 @@ pub const Pardes = struct { pub fn acknowledgeShell(p: *Pardes, id: usize, executable: []const u8, prompt_marks: bool) void { if (id < MAX_PANES) if (p.panes[id]) |pane| { ctlfs.pty.shellGone(p, pane, true); // a respawn replaced whatever ran + pane.shell_failed = false; pane.fs.unmarked = !prompt_marks; // The tag's Tty word names the shell the host ran, and a click // on it (`Tty+fish`) opens another of the same. @@ -5230,6 +5231,16 @@ pub const Pardes = struct { p.emit(.{ .watch = .{ .pane = id, .on = true } }); } + /// A terminal whose shell could not be started: said, and a run waiting + /// on it, or written to it from now on, hears `error shell gone` + /// rather than wait for a prompt that never comes. + pub fn shellFailed(p: *Pardes, id: u8, err: anyerror) void { + const pane = p.panes[id] orelse return; + pane.shell_failed = true; + ctlfs.pty.shellGone(p, pane, false); + p.reportError(id, "shell", err); + } + pub fn saveFailed(p: *Pardes, id: u8, path: []const u8, err: anyerror) void { if (p.panes[id]) |pane| if (pane.file) |*f| { // The `-%` spelling fs.zig already uses for "make this dirty". diff --git a/src/tty/tty.zig b/src/tty/tty.zig index e7eddec4..a41e42e2 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -1245,7 +1245,7 @@ const Shell = struct { const s = of(ctx); closePty(ctx, pane); // a shell still in the slot goes first, reaped s.gens[pane] +%= 1; - const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.reportError(pane, "shell", err); + const child = host_io.forkShell(s.core, pane, s.prompt_rcs, s.core.shellBin(), cwd, s.core.screen_h, s.core.screen_w, s.fs) catch |err| return s.core.shellFailed(pane, err); const command = if (s.core.panes[pane]) |pn| pn.command != null else false; s.ptys[pane] = .{ .file = child.file, .pid = child.pid, .reader = .{ .any_future = null, .result = {} }, .cmd = .{ .watched = command and host_io.watchExit(child.pid), .fd = child.file.handle } }; var lbuf: [pardes.memory.limits.host_path_cap + 1]u8 = undefined; |
