diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 22:58:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 22:58:12 -0300 |
| commit | 3ddd34bcd94ba95d9cacbf6afcb9f89a16a414ce (patch) | |
| tree | e4fee3fc0c7e228f4ac0a5877eb44b997f7bdb11 | |
| parent | 1b654dfeb5b832d110beef0ec7dd8a3b7f66e431 (diff) | |
| download | pardes-3ddd34bcd94ba95d9cacbf6afcb9f89a16a414ce.tar.gz pardes-3ddd34bcd94ba95d9cacbf6afcb9f89a16a414ce.zip | |
Edit's w to a file changed on disk since read is refused once, as Save is (w again to overwrite), the rest of an X/'/w written and the refused named; Edit filters get a pane command's environment ($PARDES_MOUNT, $PARDES_9P, $PARDES_PID, $winid); an Edit with no commands runs beside one whose commands run unless it touches a file that one changes, refused busy (EBUSY) as is a second Edit with commands
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/typ/reference.typ | 12 | ||||
| -rw-r--r-- | src/9p_io.zig | 16 | ||||
| -rw-r--r-- | src/edit_cmd.zig | 117 | ||||
| -rw-r--r-- | src/host_io.zig | 14 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 3 | ||||
| -rw-r--r-- | src/selection_pipe.zig | 19 | ||||
| -rw-r--r-- | test/fs.py | 21 | ||||
| -rw-r--r-- | test/selfmount.py | 7 |
8 files changed, 195 insertions, 14 deletions
diff --git a/docs/typ/reference.typ b/docs/typ/reference.typ index efc08a9b..f094a224 100644 --- a/docs/typ/reference.typ +++ b/docs/typ/reference.typ @@ -699,19 +699,25 @@ open file whose line matches. Commands: step puts both back, and unsaved edits are asked about once; the file takes no other command in that Edit. `r` reads a file over dot. `w` writes all of it, or the address, to its file or a name; all of it to its own - name leaves it clean. `f` names the pane and prints its line. File names + name leaves it clean. A `w` to its own file changed on disk since it was + read is refused once, as #word("Save") is (`modified on disk since read (w + again to overwrite)`), and the next `w` writes it; in an `X` the other + files are written, and the refusal names those it left. `f` names the pane and prints its line. File names are relative to the file's directory, and `~` is the home directory. - `<cmd` replaces dot with what `cmd` writes, `|cmd` pipes dot through it, `>cmd` sends dot to it and prints what it writes. Each command runs in its file's directory through #word("Shell"), off the -loop, so the editor goes on. The write that ran the Edit is held and +loop, so the editor goes on, with what a pane's command is given: +`$PARDES_MOUNT`, `$PARDES_9P`, `$PARDES_PID`, and `$winid` the Edit's pane. The write that ran the Edit is held and answered when they are done, its connection serving other requests meanwhile (a command may read the session through its mount); a flush of that write (an interrupted writer) kills the commands and changes nothing. Such an Edit is a write of its own: one with other lines is refused before any runs. One Edit's commands run at a time in a session, at most 1024 of -them. A command that fails (an exit status, 10 seconds, 1 MiB of output) +them. While they run, an Edit with no commands runs as usual unless it +would change, undo, rename, close or write a file the running one changes; +that, and a second Edit with commands, is refused `busy` (EBUSY). A command that fails (an exit status, 10 seconds, 1 MiB of output) fails the Edit, its stderr in `+Errors`. Each file's changes are one undo step, applied only if every command succeeds; a failure changes nothing and fails the write with acme's words (`Edit: no substitution`), as does a file edited while the diff --git a/src/9p_io.zig b/src/9p_io.zig index a01eb491..18c51ece 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -1992,6 +1992,16 @@ pub fn start(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes) ?*Listene /// open to scripts and to mounted shells — while withholding `PARDES_PID`, /// which is the whole of what `--nested` means. pub fn exportPaneEnv(listener: ?*const Listener, serial: u32, adopts: bool) void { + exportEnv(if (listener) |l| l.path() else null, serial, adopts); +} + +/// exportPaneEnv for a command run off the loop (an Edit's filter), with +/// this process's own socket: what a pane's command would be given. +pub fn exportSessionEnv(serial: u32, adopts: bool) void { + exportEnv(if (own_socket_len > 0) own_socket[0..own_socket_len] else null, serial, adopts); +} + +fn exportEnv(socket: ?[]const u8, serial: u32, adopts: bool) void { var announced = false; if (adopts) announcing: { var buf: [16]u8 = undefined; @@ -2001,13 +2011,13 @@ pub fn exportPaneEnv(listener: ?*const Listener, serial: u32, adopts: bool) void } if (!announced) _ = unsetenv("PARDES_PID"); - if (listener) |l| exporting: { + if (socket) |own| exporting: { var sock: [sun_path_len]u8 = undefined; - const path = std.fmt.bufPrintSentinel(&sock, "{s}", .{l.path()}, 0) catch break :exporting; + const path = std.fmt.bufPrintSentinel(&sock, "{s}", .{own}, 0) catch break :exporting; var buf: [16]u8 = undefined; const id = std.fmt.bufPrintSentinel(&buf, "{d}", .{serial}, 0) catch break :exporting; if (setenv("PARDES_9P", path, 1) != 0) break :exporting; - exportMount(l.path()); + exportMount(own); if (setenv("PARDES_PANE", id, 1) == 0) return; } _ = unsetenv("PARDES_9P"); diff --git a/src/edit_cmd.zig b/src/edit_cmd.zig index 4e2c09fd..ebeae223 100644 --- a/src/edit_cmd.zig +++ b/src/edit_cmd.zig @@ -148,9 +148,6 @@ fn fileOf(pane: *panes.Pane, id: usize) sam.File { pub fn run(p: *Pardes, id: usize, command: []const u8) void { const pane = p.panes[id] orelse return; if (ninep_pane.fileOf(pane) == null) return p.reportFailure(id, "Edit: only a text pane is edited"); - // acme's Edit blocks the editor while its commands run; here the - // editor goes on, and a second Edit waits its turn. - if (p.pipe.edit_run != null) return p.reportFailure(id, "Edit: another Edit's commands are still running"); const pd = p.gpa.create(Pending) catch |err| return p.reportError(id, "Edit", err); pd.* = .{ .arena = .init(p.gpa), .origin = id, .origin_serial = pane.serial }; var keep = false; @@ -181,6 +178,22 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void { error.Edit => return p.reportFailure(id, std.fmt.bufPrint(&text, "Edit: {s}", .{why.text()}) catch "Edit: failed"), error.OutOfMemory => return p.reportError(id, "Edit", err), }; + // acme's Edit blocks the editor while its commands run; here the editor + // goes on, and so does any Edit that runs no command and leaves alone + // the files the running one will change. A second Edit with commands, + // or one that would change such a file, is refused, busy. + if (p.pipe.edit_run) |running| { + if (res.jobs.len > 0) return p.reportFailure(id, "Edit: busy: another Edit's commands are running; one Edit runs commands at a time"); + for (res.files, 0..) |f, i| { + const touches = if (res.undo != 0) f.id == id else changes(f) or for (res.writes) |w| { + if (w.file == i) break true; + } else false; + if (!touches) continue; + const q = p.panes[f.id] orelse continue; + for (running.res.files, 0..) |g, j| if (g.id == f.id and running.serials[j] == q.serial and changes(g)) + return p.reportFailure(id, std.fmt.bufPrint(&text, "Edit: busy: {s}: an Edit's commands are running on it", .{f.name}) catch "Edit: busy"); + } + } if (res.undo != 0) { for (0..@abs(res.undo)) |_| if (res.undo > 0) panes.File.undo(p, pane) else panes.File.redo(p, pane); return; @@ -220,6 +233,9 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void { .cwds = cwds, .shell = arena.dupe(u8, p.shellBin()) catch |err| return p.reportError(id, "Edit", err), .token = selection_pipe.newToken(), + // What a pane's command is given: $PARDES_MOUNT, $PARDES_9P, + // $PARDES_PID, $winid as the Edit's pane. + .env = if (comptime pardes.hosted) @import("host_io.zig").commandEnv(arena, pane.serial, !p.opts.nested) catch &.{} else &.{}, }; keep = true; p.pipe.edit_run = pd; @@ -229,6 +245,11 @@ pub fn run(p: *Pardes, id: usize, command: []const u8) void { if (p.fs.serving) p.fs.edit_started = true; } +/// Whether the Edit changes file `f`: its text, its name, or its pane. +fn changes(f: sam.File) bool { + return f.ops.items.len > 0 or f.get != null or f.close or f.renamed; +} + /// The Edit waiting on its commands, stopped: its write was flushed, or /// its connection hung up (9p_io). Its commands are killed, and nothing /// changes. @@ -314,10 +335,22 @@ fn commit(p: *Pardes, pd: *Pending, outputs: []const []const u8) void { const errno = @import("ninep/ctl.zig").getFile(p, p.panes[f.id].?, path, &failed); if (errno != 0) return p.reportFailure(at, std.fmt.bufPrint(&text, "Edit: e {s}: {s}", .{ path, if (errno == @import("ninep/tree.zig").E.NOENT) "no such file" else "cannot be read" }) catch "Edit: e failed"); } - // `w`'s bytes, written after this step, before anything else is. + // `w`'s bytes, written after this step, before anything else is. To + // its own file changed on disk since it was read, as Save is: refused + // once, said, and written by the next (exec.saveFile); the others go. + var refused: std.Io.Writer.Allocating = .init(p.gpa); + defer refused.deinit(); for (res.writes) |wr| { const f = res.files[wr.file]; const pane = p.panes[f.id].?; + if (pane.file) |*pf| if (!f.scratch and std.mem.eql(u8, wr.path, pf.path)) if (pf.disk_newer != null) { + if (!pf.disk_newer_warned) { + pf.disk_newer_warned = true; + refused.writer.print("{s}{s}", .{ if (refused.written().len > 0) ", " else "", pf.path }) catch {}; + continue; + } + pf.disk_newer = null; + }; const path = p.gpa.dupe(u8, wr.path) catch |err| return p.reportError(at, "Edit", err); const bytes = p.gpa.dupe(u8, wr.bytes) catch |err| { p.gpa.free(path); @@ -376,6 +409,10 @@ fn commit(p: *Pardes, pd: *Pending, outputs: []const []const u8) void { if (out.items.len > 0) _ = ninep_pane.appendErrors(p, at, out.items); if (p.panes[was] != null) p.active = was; } + if (refused.written().len > 0) { + var said: [320]u8 = undefined; + p.reportFailure(at, std.fmt.bufPrint(&said, "Edit: w: {s} modified on disk since read (w again to overwrite)", .{refused.written()[0..@min(refused.written().len, 200)]}) catch "Edit: w: modified on disk since read (w again to overwrite)"); + } // D's panes last, each as Del closes one: an edited one is warned about // once, and closes on the next. for (res.files) |f| { @@ -441,8 +478,8 @@ test "Edit , | sort runs off the loop, through the session's shell in the file's try testing.expectEqualStrings("b\na\nc\n", req.inputs[0].bytes); try testing.expectEqualStrings("/", req.cwds[0]); try testing.expectEqualStrings(p.shellBin(), req.shell); - // A second Edit waits its turn rather than run beside it. - try testing.expectEqual(tree.E.IO, th.wr(p, tree.Node.of(serial, .ctl), "Edit ,d\n").errno()); + // An Edit that would change the file being filtered is busy. + try testing.expectEqual(tree.E.BUSY, th.wr(p, tree.Node.of(serial, .ctl), "Edit ,d\n").errno()); answer(p, &.{"a\nb\nc\n"}); try testing.expect(p.pipe.edit_run == null); try testing.expectEqualStrings("a\nb\nc\n", p.panes[0].?.file.?.content); @@ -622,3 +659,71 @@ test "X over open files, w, e, r, f, B and D on panes and the disk" { try testing.expectEqual(tree.Status.ok, th.wr(p, a_ctl, "Edit D b.h\n").reply.status); try testing.expect(p.paneBySerial(b_serial) == null); } + +test "an Edit's w to a file changed on disk is refused once, as Save is, and the others are written" { + if (comptime !pardes.hosted or !filesystem.platform_has_fs) return error.SkipZigTest; + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "a.txt", .data = "a\n" }); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "b.txt", .data = "b\n" }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + const p = try th.withFile(testing.allocator, "scratch\n"); + defer p.deinit(); + var path: [4200]u8 = undefined; + look.lookAt(p, 0, try std.fmt.bufPrint(&path, "{s}/a.txt", .{dir})); + const a = p.active; + look.lookAt(p, 0, try std.fmt.bufPrint(&path, "{s}/b.txt", .{dir})); + const b = p.active; + p.active = 0; + for ([_]usize{ a, b }) |i| try testing.expectEqual(tree.Status.ok, th.wr(p, tree.Node.of(p.panes[i].?.serial, .ctl), "Edit $a/more/\n").reply.status); + // a.txt changed on disk since it was read (the watch says so). + p.panes[a].?.file.?.disk_newer = 1; + const ctl = tree.Node.of(th.serialOf(p), .ctl); + const first = th.wr(p, ctl, "Edit X/'/w\n"); + try testing.expect(first.reply.status == .err); + try testing.expect(std.mem.indexOf(u8, first.reply.ename, "a.txt modified on disk since read (w again to overwrite)") != null); + try testing.expect(std.mem.indexOf(u8, first.reply.ename, "b.txt") == null); + // b.txt is written all the same; a.txt is not. + try testing.expectEqual(@as(usize, 1), p.pipe.writes.items.len); + try testing.expect(std.mem.endsWith(u8, p.pipe.writes.items[0].path, "/b.txt")); + p.perform(.edit_write); + try testing.expect(ninep_pane.dirtyOf(p.panes[a].?)); + // The same w again overwrites it, as a second Save does. + try testing.expectEqual(tree.Status.ok, th.wr(p, tree.Node.of(p.panes[a].?.serial, .ctl), "Edit w\n").reply.status); + try testing.expectEqual(@as(usize, 1), p.pipe.writes.items.len); + p.perform(.edit_write); + try testing.expect(!ninep_pane.dirtyOf(p.panes[a].?)); +} + +test "an Edit with no command runs beside one whose commands run, unless it touches what that one changes" { + if (comptime !pardes.hosted or !filesystem.platform_has_fs) return error.SkipZigTest; + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "other.txt", .data = "x\n" }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + const p = try th.withFile(testing.allocator, "b\na\n"); + defer p.deinit(); + var path: [4200]u8 = undefined; + look.lookAt(p, 0, try std.fmt.bufPrint(&path, "{s}/other.txt", .{dir})); + const other = p.active; + p.active = 0; + const ctl = tree.Node.of(th.serialOf(p), .ctl); + const other_ctl = tree.Node.of(p.panes[other].?.serial, .ctl); + try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit , | sort\n").reply.status); + // Another pane's plain s runs while the filter does. + try testing.expectEqual(tree.Status.ok, th.wr(p, other_ctl, "Edit ,s/x/y/\n").reply.status); + try testing.expectEqualStrings("y\n", p.panes[other].?.file.?.content); + // One that changes the filtered file, or runs commands of its own, is + // busy, EBUSY; a print of the filtered file is no change, and runs. + for ([_][]const u8{ "Edit ,s/a/A/\n", "Edit u\n" }) |line| { + const busy = th.wr(p, ctl, line); + try testing.expectEqual(tree.E.BUSY, busy.errno()); + try testing.expect(std.mem.indexOf(u8, busy.reply.ename, "busy: ") != null); + } + try testing.expectEqual(tree.E.BUSY, th.wr(p, other_ctl, "Edit , | cat\n").errno()); + try testing.expectEqual(tree.Status.ok, th.wr(p, ctl, "Edit 1p\n").reply.status); + answer(p, &.{"a\nb\n"}); + try testing.expectEqualStrings("a\nb\n", p.panes[0].?.file.?.content); +} diff --git a/src/host_io.zig b/src/host_io.zig index ee503061..7314affc 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -1469,6 +1469,20 @@ test "the child environment replaces the launcher's terminal identity exactly on extern "c" fn unsetenv(name: [*:0]const u8) c_int; +/// The environment a pane's command is given (exportPaneEnv, `$winid`, +/// ChildEnv), as `NAME=value` lines in `arena`, for a command that is not a +/// pane's: an Edit's filters, run from pane `serial`. +pub fn commandEnv(arena: std.mem.Allocator, serial: u32, adopts: bool) ![]const []const u8 { + ninep_io.exportSessionEnv(serial, adopts); + exportWinid(serial); + var slots: ChildEnv.Slots = undefined; + const envp = ChildEnv.build(&slots, .command) orelse return error.EnvironmentTooLarge; + var out: std.ArrayList([]const u8) = .empty; + var i: usize = 0; + while (envp[i]) |entry| : (i += 1) try out.append(arena, try arena.dupe(u8, std.mem.span(entry))); + return out.items; +} + fn exportWinid(serial: u32) void { var buf: [16]u8 = undefined; if (serial != 0) if (std.fmt.bufPrintSentinel(&buf, "{d}", .{serial}, 0)) |text| { diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 8ece7ead..917c9c39 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -318,6 +318,9 @@ pub fn captured(p: *Pardes, req: Req, id: usize, exec: bool, text: []const u8, i /// there (`no such`, `not found`, the words 9ns reads as ENOENT too), else /// EIO. pub fn failureErrno(failure: []const u8) u16 { + // Refused while something else runs (an Edit's commands): EBUSY, the + // word 9ns reads as it. + if (std.mem.indexOf(u8, failure, "busy: ") != null) return E.BUSY; if (std.mem.indexOf(u8, failure, "no such") != null or std.mem.indexOf(u8, failure, "not found") != null) return E.NOENT; // A pattern refused (Edit's too) is malformed input, as 9ns reads it; // so is a word this build has not (`invalid: acme's Put ...`). diff --git a/src/selection_pipe.zig b/src/selection_pipe.zig index fb6587f9..66444b04 100644 --- a/src/selection_pipe.zig +++ b/src/selection_pipe.zig @@ -32,6 +32,9 @@ pub const Request = struct { shell: []const u8 = "", /// Nonzero: `stop(token)` from another thread kills its commands. token: u32 = 0, + /// The commands' environment, `NAME=value` each, when given; else this + /// process's. + env: []const []const u8 = &.{}, }; /// Worker-owned snapshot. `copy` is intentionally called synchronously while @@ -47,6 +50,7 @@ pub const Job = struct { cwds: [][]u8 = &.{}, shell: []u8 = &.{}, token: u32 = 0, + env: [][]u8 = &.{}, pub fn copy(gpa: std.mem.Allocator, request: Request) !*Job { const job = try gpa.create(Job); @@ -68,6 +72,8 @@ pub const Job = struct { job.cwds = try dupeAll(gpa, request.cwds.len, request.cwds, true); errdefer freeAll(gpa, job.cwds); job.shell = try gpa.dupe(u8, request.shell); + errdefer gpa.free(job.shell); + job.env = try dupeAll(gpa, request.env.len, request.env, false); return job; } @@ -100,6 +106,7 @@ pub const Job = struct { freeAll(gpa, job.commands); freeAll(gpa, job.cwds); gpa.free(job.shell); + freeAll(gpa, job.env); gpa.destroy(job); } }; @@ -232,7 +239,7 @@ pub fn runOne( cwd: []const u8, input: []const u8, ) Outcome { - return runIn(gpa, io, "", command, cwd, input, 0); + return runIn(gpa, io, "", command, cwd, input, 0, &.{}); } /// The commands running for a stoppable request (`Request.token`), so @@ -306,6 +313,7 @@ pub fn runIn( cwd: []const u8, input: []const u8, token: u32, + env: []const []const u8, ) Outcome { const fail = struct { fn k(kind: Failure.Kind) Outcome { @@ -320,6 +328,12 @@ pub fn runIn( // child inherits, is cleared across the fork (only the tty's SIGWINCH is // ever blocked, and its default is to be ignored). A handler resets at // exec by itself, and nothing here is ignored. + var environ: std.process.Environ.Map = .init(gpa); + defer environ.deinit(); + for (env) |line| { + const eq = std.mem.indexOfScalar(u8, line, '=') orelse continue; + environ.put(line[0..eq], line[eq + 1 ..]) catch return fail.k(.spawn); + } const none = std.posix.sigemptyset(); var kept: std.posix.sigset_t = undefined; std.posix.sigprocmask(std.posix.SIG.SETMASK, &none, &kept); @@ -327,6 +341,7 @@ pub fn runIn( var child = std.process.spawn(io, .{ .argv = &.{ if (shell.len == 0) "/bin/sh" else shell, "-c", command }, .cwd = if (cwd.len == 0) .inherit else .{ .path = cwd }, + .environ_map = if (env.len > 0) &environ else null, .stdin = .pipe, .stdout = .pipe, .stderr = .pipe, @@ -435,7 +450,7 @@ pub fn runJob(gpa: std.mem.Allocator, io: std.Io, job: *const Job) Response { response.failure = .{ .kind = .signal, .index = @intCast(i) }; break; } - const output = switch (runIn(gpa, io, job.shell, command, cwd, input, job.token)) { + const output = switch (runIn(gpa, io, job.shell, command, cwd, input, job.token, job.env)) { .ok => |bytes| bytes, .failed => |f| { // WHICH selection, because with several cursors "it failed" is @@ -1123,6 +1123,27 @@ def edit_commands(binary): assert (root / 'b.txt').read_bytes() == b'APPLE pie\n' client.write(f'/pane/{a}/ctl', b'Edit 1 w first.txt\n') assert (root / 'first.txt').read_bytes() == b'APPLE\n' + # A file changed on disk under unsaved edits: Edit's w refuses + # once, as Save does, and the next w overwrites it. + w1 = root / 'w1.txt' + w1.write_bytes(b'one\n') + w = look(client, str(w1)) + client.write(f'/pane/{w}/ctl', b'Edit $a/two/\n') + seen = client.read('/log').decode().count(f'\nchanged {w}') + with open(w1, 'ab') as f: + f.write(b'external\n') + deadline = time.monotonic() + 10 + while client.read('/log').decode().count(f'\nchanged {w}') == seen: + assert time.monotonic() < deadline, 'the change on disk was not heard' + time.sleep(.05) + try: + client.write(f'/pane/{w}/ctl', b'Edit w\n') + raise AssertionError('w overwrote a file changed on disk') + except OSError as why: + assert 'modified on disk since read (w again to overwrite)' in str(why), why + assert w1.read_bytes() == b'one\nexternal\n' + client.write(f'/pane/{w}/ctl', b'Edit w\n') + assert w1.read_bytes() == b'one\ntwo', w1.read_bytes() def test(binary, quic=False): diff --git a/test/selfmount.py b/test/selfmount.py index c8a81df9..347093e1 100644 --- a/test/selfmount.py +++ b/test/selfmount.py @@ -307,6 +307,13 @@ def run(binary): ran = run(f"echo 'Edit $ < cat {mount}/pane/{edited}/body' > {mount}/pane/{edited}/ctl\n".encode()) check(ran == '0' and client.read(f'/pane/{edited}/body') == b'apple\npear\napple\npear\n', f'an Edit whose command reads the session through its mount ({ran!r})') + # A filter gets what a pane's command gets: the session's mount, + # socket and pid. + env_pane = int(client.read('/pane/new')) + ran = run(f"echo 'Edit $ < printf \"%s|%s|%s\" \"$PARDES_MOUNT\" \"$PARDES_9P\" \"$PARDES_PID\"' > {mount}/pane/{env_pane}/ctl\n".encode()) + got_env = client.read(f'/pane/{env_pane}/body').decode().split('|') + check(ran == '0' and len(got_env) == 3 and got_env[0] == mount and got_env[1] == sock and got_env[2].isdigit(), + f'an Edit filter has $PARDES_MOUNT, $PARDES_9P and $PARDES_PID ({got_env!r})') # An interrupted Edit write is flushed: its command is stopped and # nothing changes, and the next Edit runs. flushed = Path(f'/tmp/{name}.flushed') |
