diff options
| -rw-r--r-- | src/look.zig | 30 | ||||
| -rw-r--r-- | src/main.zig | 64 | ||||
| -rw-r--r-- | src/pardes.zig | 1 |
3 files changed, 81 insertions, 14 deletions
diff --git a/src/look.zig b/src/look.zig index f5d1488c..8c7014f5 100644 --- a/src/look.zig +++ b/src/look.zig @@ -857,7 +857,15 @@ pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { } var pathbuf: [4096]u8 = undefined; const path_z = std.fmt.bufPrintSentinel(&pathbuf, "{s}", .{path}, 0) catch return error.PathTooLong; - const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY }); + // NONBLOCK, and it is the difference between an error and a dead editor. + // A plain blocking `open` of a FIFO waits for a writer that may never come, + // and this call runs INSIDE the keystroke that asked for it — no frame, no + // message row, and in the tty shell no Ctrl-C either, because the terminal + // is in raw mode. `Look` on a named pipe (or on a device that blocks until + // carrier) froze the whole program with nothing on screen to say why. The + // flag is cleared again below for the file kinds that are worth reading; + // the ones that are not are refused by name. + const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY, .NONBLOCK = true }); // WHY it would not open, not just that it would not. Every one of these is // an ordinary thing to do by accident — `pardes /root`, a file left at mode // 000, a name that was deleted between resolving and reading — and a caller @@ -872,7 +880,20 @@ pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { else => error.OpenFailed, }; defer _ = libc.close(fd); + + // The flag STAYS SET, and the read loops below answer `EAGAIN` with + // `NotAFile`. A regular file ignores `O_NONBLOCK` entirely — the kernel + // never short-reads one for it — so this costs ordinary opens nothing and + // turns the one case that would have hung into an error with a name. const end = libc.lseek(fd, 0, libc.SEEK.END); + // NOT SEEKABLE IS NOT A DOCUMENT. `lseek` answers `ESPIPE` for a pipe, a + // socket and a terminal, and those are exactly the things whose "contents" + // are a future rather than a file — with `O_NONBLOCK` above they no longer + // hang the editor, but an unwritten FIFO then reads as EOF and opened as a + // silent empty pane, which says even less than the hang did. The zero-size + // files that ARE worth streaming — procfs and its kin — seek fine and + // report 0, so they take the branch below untouched. + if (end < 0 and libc.errno(end) == .SPIPE) return error.NotAFile; const size: usize = if (end < 0) 0 else @intCast(end); if (end >= 0 and libc.lseek(fd, 0, libc.SEEK.SET) < 0) return error.ReadFailed; if (size == 0) { @@ -885,6 +906,9 @@ pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { const n = libc.read(fd, &first, first.len); if (n < 0) { if (libc.errno(n) == .INTR) continue; + // Nothing to read AND nothing that will end: an empty pipe with + // no writer. This is the hang, reported instead of waited on. + if (libc.errno(n) == .AGAIN) return error.NotAFile; return error.ReadFailed; } first_len = @intCast(n); @@ -899,6 +923,9 @@ pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { const n = libc.read(fd, stream[stream_len..].ptr, stream.len - stream_len); if (n < 0) { if (libc.errno(n) == .INTR) continue; + // A stream that has paused is a stream that has ended, as far + // as one keystroke is concerned: keep what came. + if (libc.errno(n) == .AGAIN) break; return error.ReadFailed; } if (n == 0) break; @@ -909,6 +936,7 @@ pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { while (true) { const n = libc.read(fd, &extra, 1); if (n < 0 and libc.errno(n) == .INTR) continue; + if (n < 0 and libc.errno(n) == .AGAIN) break; if (n < 0) return error.ReadFailed; if (n > 0) return error.FileTooLarge; break; diff --git a/src/main.zig b/src/main.zig index 00eff433..b08a64da 100644 --- a/src/main.zig +++ b/src/main.zig @@ -136,6 +136,26 @@ const help_text = \\ ; +/// A MISTAKE AT THE SHELL PROMPT, said in words and nothing else. +/// +/// Every one of these used to be `return error.BadArgs` out of `main`, which +/// std prints as `error: BadArgs` with a RETURN TRACE under it. That reads as a +/// crash — it is the same shape a real panic has — for the most ordinary thing +/// a person can do, which is mistype a flag. It also said `BadArgs` and nothing +/// about WHICH argument, when the site that returned it knew exactly. +/// +/// stderr and not an `+Errors` pane, which is the answer one line down in +/// `Pardes.init`: argv is read before any core exists, and a person who typed +/// a bad flag is looking at the prompt they typed it into rather than at an +/// editor. `--attach`'s refusal three functions down already answers this way. +fn badArgs(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn { + var buf: [1024]u8 = undefined; + const line = std.fmt.bufPrint(&buf, "pardes: " ++ fmt ++ "\nTry 'pardes --help'.\n", args) catch + "pardes: bad arguments\nTry 'pardes --help'.\n"; + std.Io.File.stderr().writeStreamingAll(io, line) catch {}; + std.process.exit(1); +} + /// Built at COMPTIME, because both halves are: `version` comes out of /// `build.zig.zon` through the options module and `commit` out of `git` at /// configure time, so there is nothing here to format at runtime and no buffer @@ -240,18 +260,21 @@ fn nativeMain(init: std.process.Init) !void { if (std.mem.eql(u8, a, "--tty")) { opts.tty_only = true; } else if (std.mem.startsWith(u8, a, "--tty-toggle=")) { - opts.tty_toggle = parseCtrlKey(a["--tty-toggle=".len..]) orelse return error.BadArgs; + opts.tty_toggle = parseCtrlKey(a["--tty-toggle=".len..]) orelse + badArgs(init.io, "--tty-toggle wants one letter, not '{s}'", .{a["--tty-toggle=".len..]}); } else if (std.mem.eql(u8, a, "--tty-toggle")) { i += 1; - if (i >= args.len) return error.BadArgs; - opts.tty_toggle = parseCtrlKey(args[i]) orelse return error.BadArgs; + if (i >= args.len) badArgs(init.io, "--tty-toggle needs a letter after it", .{}); + opts.tty_toggle = parseCtrlKey(args[i]) orelse + badArgs(init.io, "--tty-toggle wants one letter, not '{s}'", .{args[i]}); } else if (std.mem.eql(u8, a, "-n")) { i += 1; - if (i >= args.len) return error.BadArgs; - opts.shells = std.fmt.parseInt(u8, args[i], 10) catch return error.BadArgs; + if (i >= args.len) badArgs(init.io, "-n needs a count after it: 1 or 3", .{}); + opts.shells = std.fmt.parseInt(u8, args[i], 10) catch + badArgs(init.io, "-n takes 1 or 3, not '{s}'", .{args[i]}); } else if (std.mem.eql(u8, a, "-l")) { i += 1; - if (i >= args.len) return error.BadArgs; + if (i >= args.len) badArgs(init.io, "-l needs the path of a dump to load", .{}); opts.load_path = args[i]; } else if (std.mem.eql(u8, a, "--fs")) { opts.fs = ""; @@ -297,8 +320,12 @@ fn nativeMain(init: std.process.Init) !void { return; } else if (a.len > 0 and a[0] != '-' and positional == null) { positional = a; + } else if (a.len == 0) { + badArgs(init.io, "an empty argument names nothing", .{}); + } else if (a[0] == '-') { + badArgs(init.io, "no such option: {s}", .{a}); } else { - return error.BadArgs; + badArgs(init.io, "one file or directory at a time, and '{s}' is the second", .{a}); } } // Started INSIDE another pardes: hand it the file and get out of the way @@ -328,7 +355,8 @@ fn nativeMain(init: std.process.Init) !void { std.process.exit(1); }; var cwdbuf: [4096]u8 = undefined; - const cwd = std.c.getcwd(&cwdbuf, cwdbuf.len) orelse return error.BadArgs; + const cwd = std.c.getcwd(&cwdbuf, cwdbuf.len) orelse + badArgs(init.io, "this shell's working directory is gone; cd somewhere that exists", .{}); var realbuf: [4096]u8 = undefined; const sent = switch (@import("look.zig").resolve(word, std.mem.span(@as([*:0]u8, @ptrCast(cwd))), &realbuf)) { .dir => |d| nested.sendLook(outer_pid, d, 0), @@ -356,7 +384,8 @@ fn nativeMain(init: std.process.Init) !void { // dirs); anything else resolves as a file if (std.c.chdir(a.ptr) != 0) { var cwdbuf: [4096]u8 = undefined; - const cwd = std.c.getcwd(&cwdbuf, cwdbuf.len) orelse return error.BadArgs; + const cwd = std.c.getcwd(&cwdbuf, cwdbuf.len) orelse + badArgs(init.io, "this shell's working directory is gone; cd somewhere that exists", .{}); var realbuf: [4096]u8 = undefined; switch (@import("look.zig").resolve(a, std.mem.span(@as([*:0]u8, @ptrCast(cwd))), &realbuf)) { .file => |t| { @@ -390,7 +419,13 @@ fn nativeMain(init: std.process.Init) !void { .word = try arena.dupe(u8, a), .dir = try arena.dupe(u8, std.mem.span(@as([*:0]u8, @ptrCast(cwd)))), }, - else => return error.BadArgs, + // A URL, an `@pN` pane address, or a directory that resolves + // and `chdir` refused. None is a typo, and none is something a + // LAUNCH can act on — the first two are words to click once + // pardes is open, and the third is a permission problem. + .url => badArgs(init.io, "a URL is not something a launch can open; start pardes and click it", .{}), + .pane => badArgs(init.io, "@pN addresses a pane of a running pardes, so there is none yet", .{}), + else => badArgs(init.io, "cannot enter that directory: {s}", .{a}), } } } @@ -410,7 +445,8 @@ fn nativeMain(init: std.process.Init) !void { // with no core, so the two together are a contradiction with no useful // reading. Refused rather than resolved by declaration order, which would // silently drop whichever flag lost. - if (detach != null and attach != null) return error.BadArgs; + if (detach != null and attach != null) + badArgs(init.io, "--detach and --attach are opposites: one runs the session, the other joins one", .{}); // `--fs` mounts the acme control filesystem, and it needs a CORE to serve. // `--attach` has none — it is a terminal whose state lives in another // process — so the flag there would be parsed, stored, and served by @@ -427,13 +463,15 @@ fn nativeMain(init: std.process.Init) !void { // than the desktop shells pay. `--detach --fs` is now the configuration // that most wants a control filesystem, because it is the one whose panes // outlive every terminal that could otherwise have scripted them. - if (opts.fs != null and attach != null) return error.BadArgs; + if (opts.fs != null and attach != null) + badArgs(init.io, "--fs serves a session's own core, and --attach has none of its own", .{}); // ...and `--fs9` for exactly that reason and no other: it is the same tree // over a different transport, and an `--attach` has no core to serve it // from either. Checked separately rather than folded into the line above // so that neither flag's refusal is a side effect of the other's — they // are independent everywhere else. - if (opts.fs9 != null and attach != null) return error.BadArgs; + if (opts.fs9 != null and attach != null) + badArgs(init.io, "--fs9 serves a session's own core, and --attach has none of its own", .{}); // `--detach` replaces the frontend rather than choosing among them: the // core runs here, with no terminal, and the frontends are elsewhere on a // socket (src/detached/). It is checked before `platform` because it is not diff --git a/src/pardes.zig b/src/pardes.zig index 4956150a..d9a77c31 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -6401,6 +6401,7 @@ pub const Pardes = struct { error.IsDirectory => "that is a directory, and not one that could be read", error.PathTooLong => "that path is too long", error.FileTooLarge => "that file is too large to open", + error.NotAFile => "that is a pipe or a device, not a document", else => @errorName(err), }; const content = try std.fmt.allocPrint(gpa, "cannot open\n\n\t{s}\n\n{s}\n", .{ path, why }); |
