From e7cc89761e693833fe3fcabf022408741cf87709 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 3 Sep 2026 15:45:52 -0300 Subject: errors: a mistyped flag is a sentence, and a pipe is not a document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every argv refusal in main.zig was `return error.BadArgs` out of `main`, which std prints as `error: BadArgs` with a return trace under it — the same shape a real crash has, for the most ordinary thing a person can do. It also said `BadArgs` and nothing about which argument, at sites that knew exactly: pardes: no such option: --hepl pardes: -n takes 1 or 3, not 'abc' pardes: one file or directory at a time, and 'b' is the second pardes: --detach and --attach are opposites: one runs the session, the other joins one Try 'pardes --help'. stderr rather than the `+Errors` pane one function down, because argv is read before a core exists and the person who mistyped a flag is looking at the prompt they typed it into. `--attach`'s refusal already answered this way; now all eleven do. `getcwd` failing is no longer reported as an argument problem, and a `.url` or `@pN` positional says why a LAUNCH cannot act on it rather than being swept into the same word as a typo. AND `Look` ON A FIFO NO LONGER FREEZES THE EDITOR. `readFile` opened with a plain blocking `open`, so a named pipe with no writer waited forever — inside the keystroke that asked, with no frame, no message row and, in the tty shell, no Ctrl-C either, because the terminal is in raw mode. It is `O_NONBLOCK` now, the read loops answer `EAGAIN` rather than waiting, and `lseek` answering ESPIPE — a pipe, a socket, a terminal — is refused as `NotAFile`, which the boot pane spells "that is a pipe or a device, not a document". Without that last part an unwritten FIFO read as EOF and opened a silent empty pane, which says less than the hang did. The zero-size files worth streaming (procfs and its kin) seek fine and are untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf --- src/look.zig | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'src/look.zig') 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; -- cgit v1.3