diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 09:03:41 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 78d6f64e72be2a22c40feaf012636bc8f04d46de (patch) | |
| tree | ae2f5828eb32f78e1536a75235706828067e6e0b | |
| parent | 039ae6aa70d963f942d8a5747060c67599664af9 (diff) | |
| download | pardes-78d6f64e72be2a22c40feaf012636bc8f04d46de.tar.gz pardes-78d6f64e72be2a22c40feaf012636bc8f04d46de.zip | |
A shell given a directory is not taken to be in this process's before it has spoken, so it logs no rename to the session's and back
Each host read /proc/<pid>/cwd right after forking the shell and at every
frame after; before the shell's chdir that says this process's directory,
a rename to it, then the real one a frame later, a rename back. A shell
given a directory is named by it already (newShell) and is no longer
asked at fork; and until it has printed anything, an answer naming this
process's directory is not taken.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | src/detached/server.zig | 5 | ||||
| -rw-r--r-- | src/macos.zig | 5 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 6 | ||||
| -rw-r--r-- | src/panes.zig | 4 | ||||
| -rw-r--r-- | src/pardes.zig | 6 | ||||
| -rw-r--r-- | src/tty/tty.zig | 5 | ||||
| -rw-r--r-- | test/fs.py | 21 |
7 files changed, 49 insertions, 3 deletions
diff --git a/src/detached/server.zig b/src/detached/server.zig index 7ce8a1d0..4d2d3882 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -496,7 +496,10 @@ pub const Session = struct { 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); + // Only a shell given no directory is asked where it is: one given a + // directory is named by it already (newShell), and asked now, before + // its chdir, it would answer this process's, a rename to undo. + if (cwd.len == 0) if (host_io.shellCwd(child.pid, &lbuf)) |wd| s.core.setCwd(pane, wd); } fn ptyWrite(ctx: ?*anyopaque, pane: u8, bytes: []const u8) void { diff --git a/src/macos.zig b/src/macos.zig index 75834844..9b17803b 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -2450,7 +2450,10 @@ fn spawnShell(ctx: ?*anyopaque, pane: u8, cwd: []const u8) void { .cmd = .{ .watched = (if (core.panes[pane]) |pn| pn.command != null else false) and host_io.watchExit(child.pid), .fd = child.file.handle }, }; var lbuf: [1024]u8 = undefined; - if (host_io.shellCwd(child.pid, &lbuf)) |wd| core.setCwd(pane, wd); + // Only a shell given no directory is asked where it is: one given a + // directory is named by it already (newShell), and asked now, before + // its chdir, it would answer this process's, a rename to undo. + if (cwd.len == 0) if (host_io.shellCwd(child.pid, &lbuf)) |wd| core.setCwd(pane, wd); if (st.started) if (st.ptys[pane]) |*pt| startReader(st, pt, pane); } diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 95d81e3b..66092451 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1881,6 +1881,12 @@ test "a terminal told the directory it already goes by logs no rename" { p.setCwd(term, "/tmp"); var rec: [32]u8 = undefined; try testing.expect(!th.logHas(p, try std.fmt.bufPrint(&rec, "rename {d} ", .{p.panes[term].?.serial}))); + // This process's directory, asked before the shell has said anything + // (so before its chdir): not taken. + const session = p.sessionDir(); + p.setCwd(term, session); + try testing.expect(!th.logHas(p, try std.fmt.bufPrint(&rec, "rename {d} ", .{p.panes[term].?.serial}))); + p.update(.{ .output = .{ .pane = @intCast(term), .bytes = "$ " } }); // Somewhere else is a rename. p.setCwd(term, "/usr"); try testing.expect(th.logHas(p, try std.fmt.bufPrint(&rec, "rename {d} /usr", .{p.panes[term].?.serial}))); diff --git a/src/panes.zig b/src/panes.zig index 41ba636f..f2fe5037 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -109,6 +109,10 @@ pub const Pane = struct { /// Its shell could not be started (Pardes.shellFailed): a run hears /// `error shell gone` at once. shell_failed: bool = false, + /// Its shell has printed something, so it has made its chdir: until + /// then /proc says this process's directory, not the shell's + /// (Pardes.setCwd keeps the one it was started in). + shell_spoke: 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 c6e68517..24595045 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4999,6 +4999,11 @@ pub const Pardes = struct { else => "", }; if (std.mem.eql(u8, cur, cwd)) return; + // A shell started in a directory is asked where it is before it has + // said anything: its chdir may not have happened yet, and the answer + // is this process's directory, a rename to undo. That answer, then, + // is not taken. + if (pane.isTerminal() and !pane.shell_spoke and pane.cwd == .owned and std.mem.eql(u8, cwd, p.sessionDir())) return; pane.setOwnedCwd(cwd) catch |err| return p.reportError(id, "directory", err); if (look.lookHoverPane(p) == id) look.cancelLookHover(p); // A terminal is named by its directory, so /log hears it renamed; one @@ -5560,6 +5565,7 @@ pub const Pardes = struct { }, .output => |o| { const pane = p.panes[o.pane] orelse return; + pane.shell_spoke = true; ctlfs.events.notePtyOutput(p, o.pane, o.bytes); panes.Terminal.feedOutput(p, pane, o.bytes); ctlfs.pty.noteMarks(p, o.pane, pane); diff --git a/src/tty/tty.zig b/src/tty/tty.zig index a41e42e2..762616f5 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -1249,7 +1249,10 @@ const Shell = struct { 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; - if (host_io.shellCwd(child.pid, &lbuf)) |wd| s.core.setCwd(pane, wd); + // Only a shell given no directory is asked where it is: one given a + // directory is named by it already (newShell), and asked now, before + // its chdir, it would answer this process's, a rename to undo. + if (cwd.len == 0) if (host_io.shellCwd(child.pid, &lbuf)) |wd| s.core.setCwd(pane, wd); if (s.threads_ok) { if (s.ptys[pane]) |*pt| { pt.reader = s.io.concurrent(readPty, .{ s.io, s.gpa, pt.file, @as(usize, pane), s.gens[pane], s.loop }) catch pt.reader; @@ -494,6 +494,26 @@ def unattended_messages_expire(binary): assert not shown(), 'an unattended message outlived MessageLinger' +def new_terminals_named_once(binary): + """A terminal started outside the session's directory is named by that + directory from its `new` on: no rename to the session's and back.""" + with tempfile.TemporaryDirectory(prefix='pardes-names-') as directory: + root = Path(directory) + (root / 'sub').mkdir() + with session(binary, root, 'names') as (client, _): + made = [look(client, str(root / 'sub'))] + before = {int(row.split()[0]) for row in client.read('/index').splitlines()} + client.write('/pane/1/ctl', b'Tty\n') + if shutil.which('fish'): + client.write('/pane/1/ctl', b'Tty fish\n') + made += [s for s in (int(row.split()[0]) for row in client.read('/index').splitlines()) if s not in before] + time.sleep(1.5) # their shells start and say where they are + log = client.read('/log').decode() + for serial in made: + assert f'\nrename {serial} ' not in '\n' + log, (serial, log) + assert f'\nnew {serial} {root}' in '\n' + log, (serial, log) + + def python_repl_runs_one_line(binary): """A one-line exec sent to Python's own REPL (3.13's pyrepl, which asks for bracketed paste) runs at once, with no second Enter: the Enter @@ -610,6 +630,7 @@ def test(binary, quic=False): run_file(binary) exit_answers(binary) unattended_messages_expire(binary) + new_terminals_named_once(binary) python_repl_runs_one_line(binary) signal_cleans_up(binary) for options, message in [ |
