diff options
Diffstat (limited to 'src')
| -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 |
5 files changed, 158 insertions, 11 deletions
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 |
