summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/host_io.zig')
-rw-r--r--src/host_io.zig50
1 files changed, 41 insertions, 9 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 001446b1..6ffc890e 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -136,16 +136,43 @@ pub fn forkShell(
/// Truncate-or-create `path` and put `bytes` there. False on any failure, and
/// the caller reports it: a save that did not happen must not be announced as
/// one.
-pub fn writeFileBytes(path: []const u8, bytes: []const u8) bool {
+/// WHY it failed, and not merely that it did. A save is the one operation in
+/// this program whose failure a user must not be able to miss, and until this
+/// returned an error there was nothing for a host to put on the message row:
+/// the bool said "no" and every caller answered it with a bare `return`.
+/// `NoSpaceLeft` is the one that most needs saying — the file has already been
+/// truncated by the time it happens, so a save that reports nothing has
+/// destroyed the file it was asked to preserve.
+pub const WriteError = error{
+ PathTooLong,
+ PermissionDenied,
+ IsDirectory,
+ ReadOnlyFilesystem,
+ NoSpaceLeft,
+ OpenFailed,
+ WriteFailed,
+};
+
+pub fn writeFileBytes(path: []const u8, bytes: []const u8) WriteError!void {
var pathbuf: [4096:0]u8 = undefined;
- if (path.len >= pathbuf.len) return false;
+ if (path.len >= pathbuf.len) return error.PathTooLong;
@memcpy(pathbuf[0..path.len], path);
pathbuf[path.len] = 0;
const fd = libc.open(pathbuf[0..path.len :0], .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644));
- if (fd < 0) return false;
- writeFd(fd, bytes);
- _ = libc.close(fd);
- return true;
+ if (fd < 0) return switch (libc.errno(fd)) {
+ .ACCES, .PERM => error.PermissionDenied,
+ .ISDIR => error.IsDirectory,
+ .ROFS => error.ReadOnlyFilesystem,
+ .NOSPC, .DQUOT => error.NoSpaceLeft,
+ .NAMETOOLONG => error.PathTooLong,
+ else => error.OpenFailed,
+ };
+ const wrote = writeFd(fd, bytes);
+ // The close is part of the write. NFS and every write-back filesystem
+ // report a deferred error here and nowhere else, so a close that fails on a
+ // file we believe we wrote is a file we did not write.
+ const closed = libc.close(fd) == 0;
+ if (!wrote or !closed) return error.WriteFailed;
}
/// A whole-buffer write that finishes short writes, retries EINTR, and refuses
@@ -164,15 +191,20 @@ pub fn writeFileBytes(path: []const u8, bytes: []const u8) bool {
/// matters more now that detached/server.zig reaches this file from a
/// single-threaded poll loop: a blocked `write` is one syscall a signal can
/// interrupt, and a spin is 100% of a core with the whole session behind it.
-pub fn writeFd(fd: c_int, data: []const u8) void {
+/// True when every byte went. The answer is new: this used to return `void`, so
+/// a full disk and a completed write were the same event to every caller — and
+/// the one caller that matters had already truncated the file. A pty write
+/// ignores it, which is what `_ =` at those call sites means.
+pub fn writeFd(fd: c_int, data: []const u8) bool {
var off: usize = 0;
while (off < data.len) {
const n = libc.write(fd, data[off..].ptr, data.len - off);
if (n < 0) {
if (libc.errno(n) == .INTR) continue;
- return;
+ return false;
}
- if (n == 0) return;
+ if (n == 0) return false;
off += @intCast(n);
}
+ return true;
}