summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/9p_io.zig16
-rw-r--r--src/edit_cmd.zig117
-rw-r--r--src/host_io.zig14
-rw-r--r--src/ninep/ctl.zig3
-rw-r--r--src/selection_pipe.zig19
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