summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/9p_io.zig59
-rw-r--r--src/fs-help.txt2
-rw-r--r--src/macos.zig1
-rw-r--r--src/main.zig31
-rw-r--r--src/pardes.zig6
-rw-r--r--src/tty/tty.zig31
6 files changed, 66 insertions, 64 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 3d019e0d..7f0134ed 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -671,7 +671,7 @@ pub const Listener = struct {
log.warn("registry post skipped: cannot create {s}", .{svc});
return;
}
- sweepRegistry(l.io, svc);
+ sweepRegistry(l.io, svc, xdg);
var entry_buf: [sun_path_len:0]u8 = undefined;
const entry = std.fmt.bufPrintSentinel(&entry_buf, "{s}/{s}", .{ svc, name }, 0) catch {
log.warn("registry post skipped: name too long: {s}", .{name});
@@ -903,7 +903,21 @@ fn probe(path: [:0]const u8) Probe {
/// a definite refusal counts as gone. The socket a stale entry points
/// at goes too, but not before a stat agrees it is a socket of ours: a
/// plain file answers a connect with the same refusal.
-fn sweepRegistry(io: std.Io, svc: [:0]const u8) void {
+/// Is this the kind of path this editor is allowed to delete? A registry
+/// entry is a symlink we wrote, but its target is just bytes on disk that
+/// anyone could have pointed anywhere, so the sweep only ever follows one
+/// into the directory our own sockets live in, and only to a name of the
+/// shape we give them. Everything else gets its entry removed and its target
+/// left strictly alone.
+fn ourSocket(target: []const u8, sockets: []const u8) bool {
+ if (sockets.len == 0 or !std.mem.startsWith(u8, target, sockets)) return false;
+ if (target.len <= sockets.len or target[sockets.len] != '/') return false;
+ const base = target[sockets.len + 1 ..];
+ if (std.mem.indexOfScalar(u8, base, '/') != null) return false;
+ return std.mem.startsWith(u8, base, "pardes-9p-") and std.mem.endsWith(u8, base, ".sock");
+}
+
+fn sweepRegistry(io: std.Io, svc: [:0]const u8, sockets: []const u8) void {
if (comptime !supported) return;
// The names are staged before anything is unlinked, so the sweep
@@ -941,12 +955,22 @@ fn sweepRegistry(io: std.Io, svc: [:0]const u8) void {
if (libc.unlink(entry) != 0) continue;
reaped += 1;
// A relative target would resolve against this editor's working
- // directory, which says nothing about what the entry named.
+ // directory, which says nothing about what the entry named, and a
+ // readlink that exactly filled the buffer was truncated, so the path
+ // it produced is some other file's.
if (state != .stale or n <= 0 or link_buf[0] != '/') continue;
+ if (@as(usize, @intCast(n)) >= link_buf.len) continue;
var target_buf: [sun_path_len:0]u8 = undefined;
const target = std.fmt.bufPrintSentinel(&target_buf, "{s}", .{link_buf[0..@intCast(n)]}, 0) catch continue;
+ if (!ourSocket(target, sockets)) continue;
const t = statNoFollow(target) orelse continue;
- if (t.mode & 0o170000 == 0o140000 and t.uid == libc.getuid()) _ = libc.unlink(target);
+ if (t.mode & 0o170000 != 0o140000 or t.uid != libc.getuid()) continue;
+ // Ask again, immediately before deleting. The first probe was of the
+ // ENTRY and is by now several syscalls old; a socket that is bound but
+ // has not reached listen(2) yet answers ECONNREFUSED exactly like a
+ // dead one, and that window is every server's startup.
+ if (probe(target) != .stale) continue;
+ _ = libc.unlink(target);
}
if (reaped != 0) log.info("reaped {d} stale registry entries under {s}", .{ reaped, svc });
}
@@ -997,14 +1021,23 @@ test "the registry sweep takes the dead entries and leaves everything else" {
return error.SkipZigTest;
if (libc.mkdir(svc, 0o700) != 0) return error.SkipZigTest;
- var paths: [5][sun_path_len:0]u8 = undefined;
- const live_sock = try std.fmt.bufPrintSentinel(&paths[0], "{s}/live.sock", .{svc}, 0);
- const dead_sock = try std.fmt.bufPrintSentinel(&paths[1], "{s}/dead.sock", .{svc}, 0);
+ // The sockets live where the real ones do -- beside the registry, not in
+ // it, and named the way a session names them -- because the sweep only
+ // follows an entry to a target of exactly that shape and place.
+ var paths: [6][sun_path_len:0]u8 = undefined;
+ const pid: u32 = @intCast(libc.getpid());
+ const live_sock = try std.fmt.bufPrintSentinel(&paths[0], "{s}/pardes-9p-sweeplive-{d}.sock", .{ base, pid }, 0);
+ const dead_sock = try std.fmt.bufPrintSentinel(&paths[1], "{s}/pardes-9p-sweepdead-{d}.sock", .{ base, pid }, 0);
const live = try std.fmt.bufPrintSentinel(&paths[2], "{s}/live", .{svc}, 0);
const dead = try std.fmt.bufPrintSentinel(&paths[3], "{s}/dead", .{svc}, 0);
const stranger = try std.fmt.bufPrintSentinel(&paths[4], "{s}/stranger", .{svc}, 0);
+ // A dead entry pointing at something that is NOT one of our sockets: the
+ // entry goes, the file it named must not.
+ const outsider_sock = try std.fmt.bufPrintSentinel(&paths[5], "{s}/sweep-outsider-{d}.sock", .{ base, pid }, 0);
+ var outsider_buf: [sun_path_len:0]u8 = undefined;
+ const outsider = try std.fmt.bufPrintSentinel(&outsider_buf, "{s}/outsider", .{svc}, 0);
defer {
- for ([_][:0]const u8{ live_sock, dead_sock, live, dead, stranger }) |p| _ = libc.unlink(p);
+ for ([_][:0]const u8{ live_sock, dead_sock, live, dead, stranger, outsider_sock, outsider }) |p| _ = libc.unlink(p);
_ = libc.rmdir(svc);
}
@@ -1021,6 +1054,12 @@ test "the registry sweep takes the dead entries and leaves everything else" {
try testing.expectEqual(@as(c_int, 0), libc.symlink(live_sock, live));
try testing.expectEqual(@as(c_int, 0), libc.symlink(dead_sock, dead));
+ // Dead too, but its target is not one of our sockets by name, so the
+ // entry must go and the file it named must survive untouched.
+ const outside = bindSocket(outsider_sock);
+ try testing.expect(outside >= 0);
+ defer _ = libc.close(outside);
+ try testing.expectEqual(@as(c_int, 0), libc.symlink(outsider_sock, outsider));
// Not a symlink, so not this program's to reason about, even though
// connecting to it is refused exactly like the dead socket.
try std.Io.Dir.cwd().writeFile(testing.io, .{ .sub_path = stranger, .data = "" });
@@ -1058,7 +1097,7 @@ test "the registry sweep takes the dead entries and leaves everything else" {
try testing.expectEqual(Probe.live, probe(live));
try testing.expectEqual(Probe.stale, probe(dead));
- sweepRegistry(testing.io, svc);
+ sweepRegistry(testing.io, svc, base);
// Promptly, and not "eventually": a blocking probe never comes back
// at all, so any wall-clock bound at all is the assertion that
@@ -1071,6 +1110,8 @@ test "the registry sweep takes the dead entries and leaves everything else" {
try testing.expect(statNoFollow(live) != null);
try testing.expect(statNoFollow(live_sock) != null);
try testing.expect(statNoFollow(stranger) != null);
+ try testing.expect(statNoFollow(outsider) == null);
+ try testing.expect(statNoFollow(outsider_sock) != null);
try testing.expectEqual(Probe.live, probe(live));
}
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 3113590a..7f55b3af 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -23,7 +23,7 @@ Below, $m is the mount point (PARDES_MOUNT in a Tty9p shell; 9ns and 9p work too
cat $m/pane/$n/name; echo notes.txt > $m/pane/$n/name read, then rename
echo Save > $m/pane/$n/exec save it; rmdir $m/pane/$n closes it
echo 'Msg hello' > $m/exec show text in the editor
- echo '#0,#5' > $m/pane/$n/addr; echo NEW > $m/pane/$n/data replace bytes 0..5
+ echo '#0,#5' > $m/pane/$n/addr; printf NEW >> $m/pane/$n/data replace bytes 0..5
cp $m/pane/$n/addr $m/pane/$n/dot; cat $m/pane/$n/sel select the range, read it
cat $m/pane/$n/dirty; echo 0 > $m/pane/$n/dirty is it modified? say it is not
cat $m/log block until a pane is made, renamed, saved or closed
diff --git a/src/macos.zig b/src/macos.zig
index 83b5eac9..d3b33c55 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -2594,6 +2594,7 @@ test "pardes.h declares every export the way it is defined" {
try expectSameAbi(@TypeOf(c.pardes_gui_tagline_font_percent), @TypeOf(pardes_gui_tagline_font_percent));
try expectSameAbi(@TypeOf(c.pardes_tagline_band_offset), @TypeOf(pardes_tagline_band_offset));
try expectSameAbi(@TypeOf(c.pardes_topbar_pane_border_rgb), @TypeOf(pardes_topbar_pane_border_rgb));
+ try expectSameAbi(@TypeOf(c.pardes_topbar_pane_border_px), @TypeOf(pardes_topbar_pane_border_px));
try expectSameAbi(@TypeOf(c.pardes_tag_active_bg), @TypeOf(pardes_tag_active_bg));
try expectSameAbi(@TypeOf(c.pardes_tagline_origin_col), @TypeOf(pardes_tagline_origin_col));
try expectSameAbi(@TypeOf(c.pardes_grid_col_at), @TypeOf(pardes_grid_col_at));
diff --git a/src/main.zig b/src/main.zig
index 9abc04bf..050cee60 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -112,16 +112,6 @@ const nested_text =
\\
;
-/// $PARDES_PID named a live editor, so this shell IS inside one, but the Look
-/// never got there. Saying so beats quietly opening the second editor that
-/// $PARDES_PID exists to prevent.
-const unreachable_text =
- \\pardes: this shell is inside pardes, but that session did not take the
- \\file. Check that it is still running, or pass --nested to start a second
- \\editor in here anyway.
- \\
-;
-
// The browser runtime calls a C main (exported below); everything else keeps
// the std.process.Init entry.
pub const main = if (is_emscripten) webMain else nativeMain;
@@ -304,10 +294,13 @@ fn nativeMain(init: std.process.Init) !void {
if (serial == 0) break :reaching null;
break :reaching .{ .dial = dial, .serial = serial };
};
- const parent = found orelse {
- try std.Io.File.stderr().writeStreamingAll(init.io, unreachable_text);
- std.process.exit(1);
- };
+ // A pid we cannot reach is a session that is not answering: the shell
+ // says it is inside one, but there is nothing there to take the file.
+ // Starting an ordinary editor is what this did before the pid existed
+ // and is the more useful of the two answers -- refusing to start would
+ // leave a stale environment variable able to lock someone out of their
+ // own editor.
+ const parent = found orelse break :forwarding;
// The pane's `look` file: one line, and the line is the clicked text
// itself, which is what a right click in that pane would have been.
var look_buf: [64]u8 = undefined;
@@ -315,10 +308,7 @@ fn nativeMain(init: std.process.Init) !void {
const word = positional orelse {
var tag_buf: [64]u8 = undefined;
const tag = try std.fmt.bufPrint(&tag_buf, "/pane/{d}/tag", .{parent.serial});
- const contents = ninep_io.Client.read(arena, parent.dial, tag, tag) catch {
- try std.Io.File.stderr().writeStreamingAll(init.io, unreachable_text);
- std.process.exit(1);
- };
+ const contents = ninep_io.Client.read(arena, parent.dial, tag, tag) catch break :forwarding;
arena.free(contents);
try std.Io.File.stderr().writeStreamingAll(init.io, nested_text);
std.process.exit(1);
@@ -332,10 +322,7 @@ fn nativeMain(init: std.process.Init) !void {
const path = if (pardes.filesystem.isVirtual(target.path)) target.path else (pardes.filesystem.resolveOs(target.path, &realbuf) orelse break :forwarding).path;
var command_buf: [8192]u8 = undefined;
const command = std.fmt.bufPrint(&command_buf, "{s}{s}\n", .{ path, word[target.path.len..] }) catch break :forwarding;
- ninep_io.Client.write(arena, parent.dial, look, command) catch {
- try std.Io.File.stderr().writeStreamingAll(init.io, unreachable_text);
- std.process.exit(1);
- };
+ ninep_io.Client.write(arena, parent.dial, look, command) catch break :forwarding;
return;
}
if (positional) |a| {
diff --git a/src/pardes.zig b/src/pardes.zig
index e9908d65..5c6d2422 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -8368,7 +8368,11 @@ pub const Pardes = struct {
// A host that folds Shift into the letter says the same thing.
if (key.ctrl and !key.alt and (key.cp == 'v' or key.cp == 'V')) {
if (key.shift or key.cp == 'V') return p.clipRequest(p.active, .after);
- return p.typeToTty(p.active, pane, p.yank orelse return);
+ // With something in the register this is a paste. With nothing
+ // in it the chord is the program's -- vim's visual block,
+ // readline's quoted-insert -- and swallowing it would make
+ // Ctrl-V a black hole in every full-screen application.
+ if (p.yank) |text| return p.typeToTty(p.active, pane, text);
}
return panes.Terminal.forwardKey(p, p.active, key);
}
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index a1917ba9..23b7c09b 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -71,21 +71,6 @@ fn inputReader(loop: *Loop, tty: anytype, cache: *vaxis.GraphemeCache) void {
loop.postEvent(.quit) catch {};
}
-/// How long a lone ESC waits for the rest of a sequence before it counts as
-/// the Escape key. Long enough for the remainder of a real sequence to arrive
-/// even over a slow link, short enough that nobody sees the delay.
-const escape_hold_ms = 25;
-
-/// Is there more input right behind what we have already read? Only a real
-/// terminal has an fd to ask; the test readers hand their parts over whole, so
-/// for them the answer is always no.
-fn morePending(tty: anytype) bool {
- const Reader = @typeInfo(@TypeOf(tty)).pointer.child;
- if (!@hasField(Reader, "fd") or @FieldType(Reader, "fd") != std.Io.File) return false;
- var fds = [_]std.posix.pollfd{.{ .fd = tty.fd.handle, .events = std.posix.POLL.IN, .revents = 0 }};
- return (std.posix.poll(&fds, escape_hold_ms) catch return false) > 0;
-}
-
fn readInput(loop: *Loop, tty: anytype, cache: *vaxis.GraphemeCache) !void {
try loop.postEvent(.{ .winsize = try tty.getWinsize() });
var parser: vaxis.Parser = .{};
@@ -116,22 +101,6 @@ fn readInput(loop: *Loop, tty: anytype, cache: *vaxis.GraphemeCache) !void {
}
carried = end - consumed;
std.mem.copyForwards(u8, buf[0..carried], buf[consumed..end]);
- // A lone ESC opens most sequences and is also the Escape key, so the
- // parser holds it for a remainder that a keypress never sends: the
- // press would only land when the NEXT key arrived. Wait a beat, and
- // when nothing follows it was the key. A terminal speaking the kitty
- // protocol never reaches here — it spells Escape out in full.
- if (carried == 1 and buf[0] == 0x1b and !morePending(tty)) {
- carried = 0;
- try vaxis.loop.handleEventGeneric(
- loop,
- loop.vaxis,
- cache,
- @TypeOf(Command.value),
- @as(vaxis.Event, .{ .key_press = .{ .codepoint = vaxis.Key.escape } }),
- loop.vaxis.opts.system_clipboard_allocator,
- );
- }
}
}