Commit 4232d74

Eric Bower  ·  2026-07-23 11:42:37 -0400 EDT
parent 93cd148
refactor: upgrade to zig v0.16.0

The biggest challenge was the deprecation of std.posix.  In an effort to upgrade
to 0.16 without a major rewrite I decided to port the functions from std.posix
that we were heavily using.  We can continue to chip away at some of the posix
functions but it's likely many will remain until std.Io matures and we can migrate
off of using `poll(2)` directly.

Build is passing, tests are passing.
10 files changed,  +1640, -433
+1, -1
......@@ -2,7 +2,7 @@ FROM debian:12
22
33 RUN apt-get update && apt-get install -y curl git bats coreutils && rm -rf /var/lib/apt/lists/*
44
5-ARG ZIG_VERSION=0.15.2
5+ARG ZIG_VERSION=0.16.0
66 RUN curl -L -o /tmp/zig.tar.xz https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz && \
77 cd /tmp && \
88 tar -xvf zig.tar.xz && \
+1, -1
......@@ -80,7 +80,7 @@ nix run github:neurosnap/zmx
8080
8181 ### src
8282
83-- Requires zig `v0.15`
83+- Requires zig `v0.16`
8484 - Clone the repo
8585 - Run build cmd
8686
+6, -5
......@@ -27,6 +27,7 @@ pub fn build(b: *std.Build) void {
2727 .root_source_file = b.path("src/main.zig"),
2828 .target = target,
2929 .optimize = optimize,
30+ .link_libc = true,
3031 });
3132 exe_mod.addOptions("build_options", options);
3233
......@@ -54,7 +55,7 @@ pub fn build(b: *std.Build) void {
5455 .use_lld = !is_macos,
5556 .root_module = exe_mod,
5657 });
57- exe.linkLibC();
58+
5859 b.installArtifact(exe);
5960 const run_cmd = b.addRunArtifact(exe);
6061 run_cmd.step.dependOn(b.getInstallStep());
......@@ -69,6 +70,7 @@ pub fn build(b: *std.Build) void {
6970 .root_source_file = b.path("src/test.zig"),
7071 .target = target,
7172 .optimize = optimize,
73+ .link_libc = true,
7274 });
7375 const test_dep = b.dependency("ghostty", .{
7476 .target = target,
......@@ -86,7 +88,7 @@ pub fn build(b: *std.Build) void {
8688 .use_llvm = true,
8789 .use_lld = !is_macos,
8890 });
89- exe_unit_tests.linkLibC();
91+
9092 const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
9193 test_step.dependOn(&run_exe_unit_tests.step);
9294 }
......@@ -100,7 +102,6 @@ pub fn build(b: *std.Build) void {
100102 .use_lld = !is_macos,
101103 .root_module = exe_mod,
102104 });
103- exe_check.linkLibC();
104105
105106 // Finally we add the "check" step which will be detected
106107 // by ZLS and automatically enable Build-On-Save.
......@@ -121,6 +122,7 @@ pub fn build(b: *std.Build) void {
121122 .root_source_file = b.path("src/main.zig"),
122123 .target = resolved,
123124 .optimize = .ReleaseSafe,
125+ .link_libc = true,
124126 });
125127 release_mod.addOptions("build_options", options);
126128
......@@ -141,7 +143,6 @@ pub fn build(b: *std.Build) void {
141143 .use_lld = !is_local_macos,
142144 .root_module = release_mod,
143145 });
144- release_exe.linkLibC();
145146
146147 const os_name = @tagName(release_target.os_tag orelse .linux);
147148 const arch_name = @tagName(release_target.cpu_arch orelse .x86_64);
......@@ -156,7 +157,7 @@ pub fn build(b: *std.Build) void {
156157
157158 const shasum = b.addSystemCommand(&.{"sha256sum"});
158159 shasum.addFileArg(tarball);
159- const shasum_output = shasum.captureStdOut();
160+ const shasum_output = shasum.captureStdOut(.{});
160161
161162 const install_tar = b.addInstallFile(tarball, b.fmt("dist/{s}", .{tarball_name}));
162163 const install_sha = b.addInstallFile(
+3, -3
......@@ -2,11 +2,11 @@
22 .name = .zmx,
33 .version = "0.7.0",
44 .fingerprint = 0x28aad87005052b4e, // Changing this has security and trust implications.
5- .minimum_zig_version = "0.15.2",
5+ .minimum_zig_version = "0.16.0",
66 .dependencies = .{
77 .ghostty = .{
8- .url = "git+https://github.com/ghostty-org/ghostty#30e1f3bb8c3d2949e9ae4aefc1c2b76142569cfb",
9- .hash = "ghostty-1.3.2-dev-5UdBC5gHIgWG-_Voonf5F76Vt2I2KljI5lIuvI-7eyBI",
8+ .url = "git+https://github.com/ghostty-org/ghostty.git/#15484b607eb5a518dedf1548247c923b8abaae7c",
9+ .hash = "ghostty-1.3.2-dev-5UdBCwfRJAX2W3lpOWUOrkLvsqJckxucSrjECoDjq9kL",
1010 },
1111 },
1212 .paths = .{
+5, -4
......@@ -2,6 +2,7 @@ const std = @import("std");
22 const posix = std.posix;
33 const cross = @import("cross.zig");
44 const socket = @import("socket.zig");
5+const lib_posix = @import("posix.zig");
56
67 pub const Tag = enum(u8) {
78 Input = 0,
......@@ -115,7 +116,7 @@ pub fn appendMessage(
115116 fn writeAll(fd: i32, data: []const u8) !void {
116117 var index: usize = 0;
117118 while (index < data.len) {
118- const n = try posix.write(fd, data[index..]);
119+ const n = try lib_posix.write(fd, data[index..]);
119120 if (n == 0) return error.DiskQuota;
120121 index += n;
121122 }
......@@ -223,7 +224,7 @@ const SessionProbeResult = struct {
223224
224225 pub fn deinit(self: *const SessionProbeResult) void {
225226 if (self.labels) |lbl| self.alloc.free(lbl);
226- posix.close(self.fd);
227+ lib_posix.close(self.fd);
227228 }
228229 };
229230
......@@ -233,7 +234,7 @@ pub fn probeSession(
233234 ) SessionProbeError!SessionProbeResult {
234235 const timeout_ms = 1000;
235236 const fd = try connectSession(socket_path);
236- errdefer posix.close(fd);
237+ errdefer lib_posix.close(fd);
237238
238239 send(fd, .Info, "") catch return error.Unexpected;
239240 send(fd, .LabelGet, "") catch {};
......@@ -320,7 +321,7 @@ pub fn roundTripForTag(
320321 ) SessionProbeError![]u8 {
321322 const timeout_ms = 1000;
322323 const fd = try connectSession(socket_path);
323- defer posix.close(fd);
324+ defer lib_posix.close(fd);
324325
325326 send(fd, request_tag, payload) catch return error.Unexpected;
326327
+31, -24
......@@ -1,50 +1,52 @@
11 const std = @import("std");
2-const posix = std.posix;
32
43 pub const LogSystem = struct {
5- file: ?std.fs.File = null,
6- mutex: std.Thread.Mutex = .{},
4+ file: ?std.Io.File = null,
5+ mutex: std.Io.Mutex = .init,
76 current_size: u64 = 0,
87 max_size: u64 = 5 * 1024 * 1024, // 5MB
98 path: []const u8 = "",
109 alloc: std.mem.Allocator = undefined,
11- mode: u32 = 0o640,
10+ io: std.Io = undefined,
11+ mode: std.Io.File.Permissions = std.Io.File.Permissions.fromMode(0o640),
1212
13- pub fn init(self: *LogSystem, alloc: std.mem.Allocator, path: []const u8, mode: u32) !void {
13+ pub fn init(self: *LogSystem, alloc: std.mem.Allocator, io: std.Io, path: []const u8, mode: std.Io.File.Permissions) !void {
1414 self.alloc = alloc;
15+ self.io = io;
1516 self.path = try alloc.dupe(u8, path);
1617 self.mode = mode;
1718
18- const file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) {
19- error.FileNotFound => try std.fs.createFileAbsolute(
19+ const file = std.Io.Dir.openFileAbsolute(self.io, path, .{ .mode = .read_write }) catch |err| switch (err) {
20+ error.FileNotFound => try std.Io.Dir.createFileAbsolute(
21+ self.io,
2022 path,
21- .{ .read = true, .mode = @intCast(self.mode) },
23+ .{ .read = true, .permissions = self.mode },
2224 ),
2325 else => return err,
2426 };
2527
26- // fstat (not getEndPos) to avoid the statx syscall; see #186.
27- const st = try posix.fstat(file.handle);
28- const end_pos: u64 = @intCast(st.size);
29- try file.seekTo(end_pos);
28+ const end_pos = try std.Io.File.length(file, self.io);
29+ var buf: [1]u8 = undefined;
30+ var w = std.Io.File.writer(file, self.io, &buf);
31+ try w.seekTo(end_pos);
3032 self.current_size = end_pos;
3133 self.file = file;
3234 }
3335
3436 pub fn deinit(self: *LogSystem) void {
35- if (self.file) |f| f.close();
37+ if (self.file) |f| std.Io.File.close(f, self.io);
3638 if (self.path.len > 0) self.alloc.free(self.path);
3739 }
3840
3941 pub fn log(
4042 self: *LogSystem,
4143 comptime level: std.log.Level,
42- comptime scope: @Type(.enum_literal),
44+ comptime scope: anytype,
4345 comptime format: []const u8,
4446 args: anytype,
45- ) void {
46- self.mutex.lock();
47- defer self.mutex.unlock();
47+ ) !void {
48+ try self.mutex.lock(self.io);
49+ defer self.mutex.unlock(self.io);
4850
4951 if (self.file == null) {
5052 std.log.defaultLog(level, scope, format, args);
......@@ -57,7 +59,7 @@ pub const LogSystem = struct {
5759 };
5860 }
5961
60- const now = std.time.milliTimestamp();
62+ const now: i64 = @intCast(@divTrunc(std.Io.Timestamp.now(self.io, .real).nanoseconds, std.time.ns_per_ms));
6163 const prefix = "[{d}] [{s}] ({s}): ";
6264 const scope_name = @tagName(scope);
6365 const level_name = level.asText();
......@@ -76,29 +78,34 @@ pub const LogSystem = struct {
7678 self.current_size += total_len;
7779
7880 var buf: [4096]u8 = undefined;
79- var w = f.writerStreaming(&buf);
80- w.interface.print(prefix ++ format ++ "\n", prefix_args ++ args) catch {};
81+ var w = f.writerStreaming(self.io, &buf);
82+ std.Io.Writer.print(&w.interface, prefix ++ format ++ "\n", prefix_args ++ args) catch {};
8183 w.interface.flush() catch {};
8284 }
8385 }
8486
8587 fn rotate(self: *LogSystem) !void {
8688 if (self.file) |f| {
87- f.close();
89+ std.Io.File.close(f, self.io);
8890 self.file = null;
8991 }
9092
9193 const old_path = try std.fmt.allocPrint(self.alloc, "{s}.old", .{self.path});
9294 defer self.alloc.free(old_path);
9395
94- std.fs.renameAbsolute(self.path, old_path) catch |err| switch (err) {
96+ std.Io.Dir.renameAbsolute(self.path, old_path, self.io) catch |err| switch (err) {
9597 error.FileNotFound => {},
9698 else => return err,
9799 };
98100
99- self.file = try std.fs.createFileAbsolute(
101+ self.file = try std.Io.Dir.createFileAbsolute(
102+ self.io,
100103 self.path,
101- .{ .truncate = true, .read = true, .mode = @intCast(self.mode) },
104+ .{
105+ .truncate = true,
106+ .read = true,
107+ .permissions = self.mode,
108+ },
102109 );
103110 self.current_size = 0;
104111 }
+363, -340
......@@ -9,6 +9,7 @@ const util = @import("util.zig");
99 const cross = @import("cross.zig");
1010 const socket = @import("socket.zig");
1111 const label = @import("label.zig");
12+const lib_posix = @import("posix.zig");
1213
1314 pub const version = build_options.version;
1415 pub const ghostty_version = build_options.ghostty_version;
......@@ -22,11 +23,11 @@ pub const std_options: std.Options = .{
2223
2324 fn zmxLogFn(
2425 comptime level: std.log.Level,
25- comptime scope: @Type(.enum_literal),
26+ comptime scope: anytype,
2627 comptime format: []const u8,
2728 args: anytype,
2829 ) void {
29- log_system.log(level, scope, format, args);
30+ log_system.log(level, scope, format, args) catch {};
3031 }
3132
3233 /// Self-pipe woken by signal handlers. std.posix.poll loops on .INTR internally
......@@ -47,13 +48,13 @@ const SessionMatch = struct {
4748 }
4849 };
4950
50-fn resolveSessionOrEnv(alloc: std.mem.Allocator, session_name: ?[]const u8) ![]const u8 {
51+fn resolveSessionOrEnv(alloc: std.mem.Allocator, io: std.Io, session_name: ?[]const u8) ![]const u8 {
5152 const sesh_env = socket.getSeshNameFromEnv();
5253 const raw = if (session_name) |name|
5354 if (std.mem.eql(u8, name, ".")) blk: {
5455 if (sesh_env.len > 0) break :blk sesh_env;
5556 var buf: [4096]u8 = undefined;
56- var w = std.fs.File.stderr().writer(&buf);
57+ var w = std.Io.File.stderr().writer(io, &buf);
5758 w.interface.print("error: \".\" requires ZMX_SESSION (are you inside a zmx session?)\n", .{}) catch {};
5859 w.interface.flush() catch {};
5960 return error.SessionNameRequired;
......@@ -76,7 +77,7 @@ fn parseSessionArg(alloc: std.mem.Allocator, raw: []const u8) !SessionMatch {
7677 }
7778
7879 fn openSignalPipe() !void {
79- sig_pipe = try posix.pipe2(.{ .CLOEXEC = true, .NONBLOCK = true });
80+ sig_pipe = try lib_posix.pipe2(.{ .CLOEXEC = true, .NONBLOCK = true });
8081 }
8182
8283 fn drainSignalPipe() void {
......@@ -91,9 +92,9 @@ fn detectHelp(arg: []const u8) bool {
9192 return (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h"));
9293 }
9394
94-pub fn main() !void {
95- // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
96- const alloc = std.heap.c_allocator;
95+pub fn main(init: std.process.Init) !void {
96+ const gpa = init.gpa;
97+ const io = init.io;
9798
9899 // Every subcommand may write to a Unix-domain socket; a peer that
99100 // disappears between probe and send would otherwise kill us before
......@@ -101,76 +102,79 @@ pub fn main() !void {
101102 // covers the daemon.
102103 ignoreSigpipe();
103104
104- var args = try std.process.argsWithAllocator(alloc);
105+ var args = init.minimal.args.iterate();
105106 defer args.deinit();
106- _ = args.skip(); // skip program name
107+ _ = args.next(); // skip program name
107108
108- var cfg = try Cfg.init(alloc);
109- defer cfg.deinit(alloc);
109+ var cfg = try Cfg.init(gpa, io);
110+ defer cfg.deinit(gpa);
110111
111- const log_path = try std.fs.path.join(alloc, &.{ cfg.log_dir, "zmx.log" });
112- defer alloc.free(log_path);
113- try log_system.init(alloc, log_path, cfg.log_mode);
112+ const log_path = try std.fs.path.join(gpa, &.{ cfg.log_dir, "zmx.log" });
113+ defer gpa.free(log_path);
114+ const log_mode = std.Io.File.Permissions.fromMode(cfg.log_mode);
115+ try log_system.init(gpa, io, log_path, log_mode);
114116 defer log_system.deinit();
115117
118+ const shell_env = init.environ_map.get("SHELL") orelse "/bin/sh";
119+
116120 const cmd = args.next() orelse {
117- return list(&cfg, false);
121+ return list(gpa, io, &cfg, false);
118122 };
119123
120124 if (std.mem.eql(u8, cmd, "version") or std.mem.eql(u8, cmd, "v") or std.mem.eql(u8, cmd, "-v") or std.mem.eql(u8, cmd, "--version")) {
121- return printVersion(&cfg);
125+ return printVersion(io, &cfg);
122126 } else if (std.mem.eql(u8, cmd, "help") or std.mem.eql(u8, cmd, "h") or std.mem.eql(u8, cmd, "-h")) {
123- return help();
127+ return help(io);
124128 } else if (std.mem.eql(u8, cmd, "list") or std.mem.eql(u8, cmd, "l") or std.mem.eql(u8, cmd, "ls")) {
125129 var short = false;
126130 while (args.next()) |arg| {
127- if (detectHelp(arg)) return help();
131+ if (detectHelp(arg)) return help(io);
128132 if (std.mem.eql(u8, arg, "--short")) short = true;
129133 }
130- return list(&cfg, short);
134+ return list(gpa, io, &cfg, short);
131135 } else if (std.mem.eql(u8, cmd, "get") or std.mem.eql(u8, cmd, "g")) {
132136 const sesh_name = args.next() orelse return error.SessionNameRequired;
133- if (detectHelp(sesh_name)) return help();
134- const sesh = try resolveSessionOrEnv(alloc, sesh_name);
135- defer alloc.free(sesh);
137+ if (detectHelp(sesh_name)) return help(io);
138+ const sesh = try resolveSessionOrEnv(gpa, io, sesh_name);
139+ defer gpa.free(sesh);
136140 const single_kv = args.next() orelse "";
137- return labelGet(&cfg, sesh, single_kv);
141+ return labelGet(gpa, io, &cfg, sesh, single_kv);
138142 } else if (std.mem.eql(u8, cmd, "set")) {
139143 const sesh_name = args.next() orelse return error.SessionNameRequired;
140- if (detectHelp(sesh_name)) return help();
141- const sesh = try resolveSessionOrEnv(alloc, sesh_name);
142- defer alloc.free(sesh);
144+ if (detectHelp(sesh_name)) return help(io);
145+ const sesh = try resolveSessionOrEnv(gpa, io, sesh_name);
146+ defer gpa.free(sesh);
143147
144148 var kvs = std.ArrayList(u8).empty;
145- defer kvs.deinit(alloc);
149+ defer kvs.deinit(gpa);
146150 var first = true;
147151 while (args.next()) |arg| {
148- if (!first) try kvs.append(alloc, ' ');
149- try kvs.appendSlice(alloc, arg);
152+ if (!first) try kvs.append(gpa, ' ');
153+ try kvs.appendSlice(gpa, arg);
150154 first = false;
151155 }
152- return labelSet(&cfg, sesh, kvs.items);
156+ return labelSet(gpa, io, &cfg, sesh, kvs.items);
153157 } else if (std.mem.eql(u8, cmd, "clear")) {
154158 const sesh_name = args.next() orelse return error.SessionNameRequired;
155- if (detectHelp(sesh_name)) return help();
156- const sesh = try resolveSessionOrEnv(alloc, sesh_name);
157- defer alloc.free(sesh);
158- return labelClear(&cfg, sesh);
159+ if (detectHelp(sesh_name)) return help(io);
160+ const sesh = try resolveSessionOrEnv(gpa, io, sesh_name);
161+ defer gpa.free(sesh);
162+ return labelClear(gpa, io, &cfg, sesh);
159163 } else if (std.mem.eql(u8, cmd, "completions") or std.mem.eql(u8, cmd, "c")) {
160164 const arg = args.next() orelse return;
161165 if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
162- return help();
166+ return help(io);
163167 }
164168 const shell = completions.Shell.fromString(arg) orelse return;
165- return printCompletions(shell);
169+ return printCompletions(io, shell);
166170 } else if (std.mem.eql(u8, cmd, "detach") or std.mem.eql(u8, cmd, "d")) {
167- return detachAll(&cfg);
171+ return detachAll(gpa, io, &cfg);
168172 } else if (std.mem.eql(u8, cmd, "history") or std.mem.eql(u8, cmd, "hi")) {
169173 var session_name: ?[]const u8 = null;
170174 var format: util.HistoryFormat = .plain;
171175 while (args.next()) |arg| {
172176 if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
173- return help();
177+ return help(io);
174178 } else if (std.mem.eql(u8, arg, "--vt")) {
175179 format = .vt;
176180 } else if (std.mem.eql(u8, arg, "--html")) {
......@@ -180,47 +184,50 @@ pub fn main() !void {
180184 }
181185 }
182186 const sesh_env = socket.getSeshNameFromEnv();
183- const sesh = try socket.getSeshName(alloc, session_name orelse sesh_env);
184- defer alloc.free(sesh);
185- return history(&cfg, sesh, format);
187+ const sesh = try socket.getSeshName(gpa, session_name orelse sesh_env);
188+ defer gpa.free(sesh);
189+ return history(gpa, io, &cfg, sesh, format);
186190 } else if (std.mem.eql(u8, cmd, "attach") or std.mem.eql(u8, cmd, "a")) {
187191 const session_name = args.next() orelse "";
188192 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
189- return help();
193+ return help(io);
190194 }
191195
192196 var command_args: std.ArrayList([]const u8) = .empty;
193- defer command_args.deinit(alloc);
197+ defer command_args.deinit(gpa);
194198 while (args.next()) |arg| {
195- try command_args.append(alloc, arg);
199+ try command_args.append(gpa, arg);
196200 }
197201
198- const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
202+ const clients = try std.ArrayList(*Client).initCapacity(gpa, 10);
199203 var command: ?[][]const u8 = null;
200204 if (command_args.items.len > 0) {
201205 command = command_args.items;
202206 }
203207
204208 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
205- const cwd = std.posix.getcwd(&cwd_buf) catch "";
209+ const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0;
210+ const cwd = cwd_buf[0..cwd_len];
206211
207- const sesh = try socket.getSeshName(alloc, session_name);
208- defer alloc.free(sesh);
212+ const sesh = try socket.getSeshName(gpa, session_name);
213+ defer gpa.free(sesh);
209214 var daemon = Daemon{
215+ .io = io,
210216 .running = true,
211217 .cfg = &cfg,
212- .alloc = alloc,
218+ .alloc = std.heap.c_allocator,
213219 .clients = clients,
214220 .session_name = sesh,
215221 .socket_path = undefined,
216222 .pid = undefined,
217223 .command = command,
218224 .cwd = cwd,
219- .created_at = @intCast(std.time.timestamp()),
225+ .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds),
220226 .leader_client_fd = null,
227+ .shell = shell_env,
221228 };
222- daemon.socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
223- error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
229+ daemon.socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
230+ error.NameTooLong => return socket.printSessionNameTooLong(daemon.io, sesh, cfg.socket_dir),
224231 error.OutOfMemory => return err,
225232 };
226233 std.log.info("socket path={s}", .{daemon.socket_path});
......@@ -228,42 +235,45 @@ pub fn main() !void {
228235 } else if (std.mem.eql(u8, cmd, "run") or std.mem.eql(u8, cmd, "r")) {
229236 const session_name = args.next() orelse "";
230237 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
231- return help();
238+ return help(io);
232239 }
233240
234241 var cmd_args_raw: std.ArrayList([]const u8) = .empty;
235- defer cmd_args_raw.deinit(alloc);
242+ defer cmd_args_raw.deinit(gpa);
236243 var detached = false;
237244 while (args.next()) |arg| {
238245 if (std.mem.startsWith(u8, arg, "-d")) {
239246 detached = true;
240247 } else {
241- try cmd_args_raw.append(alloc, arg);
248+ try cmd_args_raw.append(gpa, arg);
242249 }
243250 }
244- const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
251+ const clients = try std.ArrayList(*Client).initCapacity(gpa, 10);
245252
246253 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
247- const cwd = std.posix.getcwd(&cwd_buf) catch "";
254+ const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0;
255+ const cwd = cwd_buf[0..cwd_len];
248256
249- const sesh = try socket.getSeshName(alloc, session_name);
250- defer alloc.free(sesh);
257+ const sesh = try socket.getSeshName(gpa, session_name);
258+ defer gpa.free(sesh);
251259 var daemon = Daemon{
260+ .io = io,
252261 .running = true,
253262 .cfg = &cfg,
254- .alloc = alloc,
263+ .alloc = std.heap.c_allocator,
255264 .clients = clients,
256265 .session_name = sesh,
257266 .socket_path = undefined,
258267 .pid = undefined,
259268 .command = null,
260269 .cwd = cwd,
261- .created_at = @intCast(std.time.timestamp()),
270+ .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds),
262271 .is_task_mode = true,
263272 .leader_client_fd = null,
273+ .shell = shell_env,
264274 };
265- daemon.socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
266- error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
275+ daemon.socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
276+ error.NameTooLong => return socket.printSessionNameTooLong(daemon.io, sesh, cfg.socket_dir),
267277 error.OutOfMemory => return err,
268278 };
269279 std.log.info("socket path={s}", .{daemon.socket_path});
......@@ -271,76 +281,76 @@ pub fn main() !void {
271281 } else if (std.mem.eql(u8, cmd, "send") or std.mem.eql(u8, cmd, "s")) {
272282 const session_name = args.next() orelse "";
273283 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
274- return help();
284+ return help(io);
275285 }
276286 if (session_name.len == 0) return error.SessionNameRequired;
277287
278288 var text_parts: std.ArrayList([]const u8) = .empty;
279- defer text_parts.deinit(alloc);
289+ defer text_parts.deinit(gpa);
280290 while (args.next()) |arg| {
281- try text_parts.append(alloc, arg);
291+ try text_parts.append(gpa, arg);
282292 }
283293
284- const sesh = try socket.getSeshName(alloc, session_name);
285- defer alloc.free(sesh);
286- const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
287- error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
294+ const sesh = try socket.getSeshName(gpa, session_name);
295+ defer gpa.free(sesh);
296+ const socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
297+ error.NameTooLong => return socket.printSessionNameTooLong(io, sesh, cfg.socket_dir),
288298 error.OutOfMemory => return err,
289299 };
290- return send(&cfg, sesh, socket_path, text_parts.items, .Send);
300+ return send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Send);
291301 } else if (std.mem.eql(u8, cmd, "print") or std.mem.eql(u8, cmd, "p")) {
292302 const session_name = args.next() orelse "";
293303 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
294- return help();
304+ return help(io);
295305 }
296306 if (session_name.len == 0) return error.SessionNameRequired;
297307
298308 var text_parts: std.ArrayList([]const u8) = .empty;
299- defer text_parts.deinit(alloc);
309+ defer text_parts.deinit(gpa);
300310 while (args.next()) |arg| {
301- try text_parts.append(alloc, arg);
311+ try text_parts.append(gpa, arg);
302312 }
303313
304- const sesh = try socket.getSeshName(alloc, session_name);
305- defer alloc.free(sesh);
306- const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
307- error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
314+ const sesh = try socket.getSeshName(gpa, session_name);
315+ defer gpa.free(sesh);
316+ const socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
317+ error.NameTooLong => return socket.printSessionNameTooLong(io, sesh, cfg.socket_dir),
308318 error.OutOfMemory => return err,
309319 };
310- return send(&cfg, sesh, socket_path, text_parts.items, .Output);
320+ return send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Output);
311321 } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) {
312322 var stderr_buffer: [1024]u8 = undefined;
313- var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
323+ var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
314324 const stderr = &stderr_writer.interface;
315325
316326 var matchers: std.ArrayList(SessionMatch) = .empty;
317327 defer {
318328 for (matchers.items) |m| {
319- alloc.free(m.name);
329+ gpa.free(m.name);
320330 }
321- matchers.deinit(alloc);
331+ matchers.deinit(gpa);
322332 }
323333 var force = false;
324334 while (args.next()) |session_name| {
325335 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
326- return help();
336+ return help(io);
327337 }
328338 if (std.mem.eql(u8, session_name, "--force")) {
329339 force = true;
330340 continue;
331341 }
332- const m = try parseSessionArg(alloc, session_name);
333- try matchers.append(alloc, m);
342+ const m = try parseSessionArg(gpa, session_name);
343+ try matchers.append(gpa, m);
334344 }
335345 if (matchers.items.len == 0) {
336346 return error.SessionNameRequired;
337347 }
338- var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
348+ var sessions = try util.get_session_entries(gpa, io, cfg.socket_dir);
339349 defer {
340350 for (sessions.items) |session| {
341- session.deinit(alloc);
351+ session.deinit(gpa);
342352 }
343- sessions.deinit(alloc);
353+ sessions.deinit(gpa);
344354 }
345355
346356 for (sessions.items) |session| {
......@@ -349,7 +359,7 @@ pub fn main() !void {
349359 continue;
350360 }
351361
352- kill(&cfg, session.name, force) catch |err| {
362+ kill(gpa, io, &cfg, session.name, force) catch |err| {
353363 try stderr.print(
354364 "failed to kill session={s}: {s}\n",
355365 .{ session.name, @errorName(err) },
......@@ -363,35 +373,35 @@ pub fn main() !void {
363373 var matchers: std.ArrayList(SessionMatch) = .empty;
364374 defer {
365375 for (matchers.items) |m| {
366- alloc.free(m.name);
376+ gpa.free(m.name);
367377 }
368- matchers.deinit(alloc);
378+ matchers.deinit(gpa);
369379 }
370380 while (args.next()) |session_name| {
371381 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
372- return help();
382+ return help(io);
373383 }
374- const m = try parseSessionArg(alloc, session_name);
375- try matchers.append(alloc, m);
384+ const m = try parseSessionArg(gpa, session_name);
385+ try matchers.append(gpa, m);
376386 }
377387 if (matchers.items.len == 0) {
378388 return error.SessionNameRequired;
379389 }
380- return wait(&cfg, matchers);
390+ return wait(gpa, io, &cfg, matchers);
381391 } else if (std.mem.eql(u8, cmd, "tail") or std.mem.eql(u8, cmd, "t")) {
382392 var matchers: std.ArrayList(SessionMatch) = .empty;
383393 defer {
384394 for (matchers.items) |m| {
385- alloc.free(m.name);
395+ gpa.free(m.name);
386396 }
387- matchers.deinit(alloc);
397+ matchers.deinit(gpa);
388398 }
389399 while (args.next()) |session_name| {
390400 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
391- return help();
401+ return help(io);
392402 }
393- const m = try parseSessionArg(alloc, session_name);
394- try matchers.append(alloc, m);
403+ const m = try parseSessionArg(gpa, session_name);
404+ try matchers.append(gpa, m);
395405 }
396406 if (matchers.items.len == 0) {
397407 return error.SessionNameRequired;
......@@ -401,9 +411,9 @@ pub fn main() !void {
401411 var resolved_names: std.ArrayList([]const u8) = .empty;
402412 defer {
403413 for (resolved_names.items) |name| {
404- alloc.free(name);
414+ gpa.free(name);
405415 }
406- resolved_names.deinit(alloc);
416+ resolved_names.deinit(gpa);
407417 }
408418
409419 var any_prefix = false;
......@@ -415,17 +425,17 @@ pub fn main() !void {
415425 }
416426
417427 if (any_prefix) {
418- var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
428+ var sessions = try util.get_session_entries(gpa, io, cfg.socket_dir);
419429 defer {
420430 for (sessions.items) |session| {
421- session.deinit(alloc);
431+ session.deinit(gpa);
422432 }
423- sessions.deinit(alloc);
433+ sessions.deinit(gpa);
424434 }
425435 for (sessions.items) |session| {
426436 for (matchers.items) |m| {
427437 if (m.matches(session.name)) {
428- try resolved_names.append(alloc, try alloc.dupe(u8, session.name));
438+ try resolved_names.append(gpa, try gpa.dupe(u8, session.name));
429439 break;
430440 }
431441 }
......@@ -434,66 +444,69 @@ pub fn main() !void {
434444 // Add exact-match names directly.
435445 for (matchers.items) |m| {
436446 if (!m.is_prefix) {
437- try resolved_names.append(alloc, try alloc.dupe(u8, m.name));
447+ try resolved_names.append(gpa, try gpa.dupe(u8, m.name));
438448 }
439449 }
440450
441- var client_socket_fds = try std.ArrayList(i32).initCapacity(alloc, resolved_names.items.len);
451+ var client_socket_fds = try std.ArrayList(i32).initCapacity(gpa, resolved_names.items.len);
442452 defer {
443453 for (client_socket_fds.items) |client_fd| {
444- posix.close(client_fd);
454+ lib_posix.close(client_fd);
445455 }
446- client_socket_fds.deinit(alloc);
456+ client_socket_fds.deinit(gpa);
447457 }
448458
449459 for (resolved_names.items) |session_name| {
450- const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
451- error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
460+ const socket_path = socket.getSocketPath(gpa, cfg.socket_dir, session_name) catch |err| switch (err) {
461+ error.NameTooLong => return socket.printSessionNameTooLong(init.io, session_name, cfg.socket_dir),
452462 error.OutOfMemory => return err,
453463 };
454464 const client_sock = try socket.sessionConnect(socket_path);
455- try client_socket_fds.append(alloc, client_sock);
465+ try client_socket_fds.append(gpa, client_sock);
456466 }
457- _ = try tail(client_socket_fds, false, false);
467+ _ = try tail(gpa, client_socket_fds, false, false);
458468 } else if (std.mem.eql(u8, cmd, "write") or std.mem.eql(u8, cmd, "wr")) {
459469 const session_name = args.next() orelse "";
460470 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
461- return help();
471+ return help(io);
462472 }
463473 if (session_name.len == 0) return error.SessionNameRequired;
464474 const file_path = args.next() orelse "";
465475 if (std.mem.eql(u8, file_path, "--help") or std.mem.eql(u8, file_path, "-h")) {
466- return help();
476+ return help(io);
467477 }
468478 if (file_path.len == 0) return error.FilePathRequired;
469479
470480 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
471- const cwd = std.posix.getcwd(&cwd_buf) catch "";
472- const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
473- const sesh = try socket.getSeshName(alloc, session_name);
474- defer alloc.free(sesh);
481+ const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0;
482+ const cwd = cwd_buf[0..cwd_len];
483+ const clients = try std.ArrayList(*Client).initCapacity(gpa, 10);
484+ const sesh = try socket.getSeshName(gpa, session_name);
485+ defer gpa.free(sesh);
475486 var daemon = Daemon{
487+ .io = io,
476488 .running = true,
477489 .cfg = &cfg,
478- .alloc = alloc,
490+ .alloc = std.heap.c_allocator,
479491 .clients = clients,
480492 .session_name = sesh,
481493 .socket_path = undefined,
482494 .pid = undefined,
483495 .command = null,
484496 .cwd = cwd,
485- .created_at = @intCast(std.time.timestamp()),
497+ .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds),
486498 .is_task_mode = true,
487499 .leader_client_fd = null,
500+ .shell = shell_env,
488501 };
489- daemon.socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
490- error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
502+ daemon.socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
503+ error.NameTooLong => return socket.printSessionNameTooLong(daemon.io, sesh, cfg.socket_dir),
491504 error.OutOfMemory => return err,
492505 };
493506 std.log.info("socket path={s}", .{daemon.socket_path});
494507 try writeFile(&daemon, file_path);
495508 } else {
496- return help();
509+ return help(io);
497510 }
498511 }
499512
......@@ -508,7 +521,7 @@ const Client = struct {
508521 write_buf: std.ArrayList(u8),
509522
510523 pub fn deinit(self: *Client) void {
511- posix.close(self.socket_fd);
524+ lib_posix.close(self.socket_fd);
512525 self.read_buf.deinit();
513526 self.write_buf.deinit(self.alloc);
514527 }
......@@ -524,18 +537,18 @@ const Cfg = struct {
524537 dir_mode: u32 = 0o750,
525538 log_mode: u32 = 0o640,
526539
527- pub fn init(alloc: std.mem.Allocator) !Cfg {
540+ pub fn init(alloc: std.mem.Allocator, io: std.Io) !Cfg {
528541 const socket_dir = try socketDir(alloc);
529542 errdefer alloc.free(socket_dir);
530543 const log_dir = try logDir(alloc);
531544 errdefer alloc.free(log_dir);
532545
533- const dir_mode = if (std.posix.getenv("ZMX_DIR_MODE")) |m|
546+ const dir_mode = if (lib_posix.getenv("ZMX_DIR_MODE")) |m|
534547 std.fmt.parseInt(u32, m, 8) catch 0o750
535548 else
536549 0o750;
537550
538- const log_mode = if (std.posix.getenv("ZMX_LOG_MODE")) |m|
551+ const log_mode = if (lib_posix.getenv("ZMX_LOG_MODE")) |m|
539552 std.fmt.parseInt(u32, m, 8) catch 0o640
540553 else
541554 0o640;
......@@ -547,18 +560,18 @@ const Cfg = struct {
547560 .log_mode = log_mode,
548561 };
549562
550- try cfg.mkdir();
563+ try cfg.mkdir(io);
551564
552565 return cfg;
553566 }
554567
555568 fn socketDir(alloc: std.mem.Allocator) ![]const u8 {
556- const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/");
557- const uid = posix.getuid();
569+ const tmpdir = std.mem.trimEnd(u8, lib_posix.getenv("TMPDIR") orelse "/tmp", "/");
570+ const uid = lib_posix.getuid();
558571
559- const socket_dir: []const u8 = if (posix.getenv("ZMX_DIR")) |zmxdir|
572+ const socket_dir: []const u8 = if (lib_posix.getenv("ZMX_DIR")) |zmxdir|
560573 try alloc.dupe(u8, zmxdir)
561- else if (posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime|
574+ else if (lib_posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime|
562575 try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime})
563576 else
564577 try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
......@@ -567,16 +580,16 @@ const Cfg = struct {
567580 }
568581
569582 fn logDir(alloc: std.mem.Allocator) ![]const u8 {
570- const log_dir = if (posix.getenv("ZMX_DIR")) |zmxdir|
583+ const log_dir = if (lib_posix.getenv("ZMX_DIR")) |zmxdir|
571584 try std.fmt.allocPrint(alloc, "{s}/logs", .{zmxdir})
572- else if (posix.getenv("XDG_STATE_HOME")) |xdg_state_home|
585+ else if (lib_posix.getenv("XDG_STATE_HOME")) |xdg_state_home|
573586 try std.fmt.allocPrint(alloc, "{s}/zmx/logs", .{xdg_state_home})
574- else if (posix.getenv("HOME")) |home_dir|
587+ else if (lib_posix.getenv("HOME")) |home_dir|
575588 try std.fmt.allocPrint(alloc, "{s}/.local/state/zmx/logs", .{home_dir})
576589 else fallback: {
577590 // This is the last resort: falling back to /tmp/$UID if HOME is unset.
578- const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/");
579- const uid = posix.getuid();
591+ const tmpdir = std.mem.trimEnd(u8, lib_posix.getenv("TMPDIR") orelse "/tmp", "/");
592+ const uid = lib_posix.getuid();
580593 break :fallback try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
581594 };
582595
......@@ -588,16 +601,18 @@ const Cfg = struct {
588601 if (self.log_dir.len > 0) alloc.free(self.log_dir);
589602 }
590603
591- pub fn mkdir(self: *Cfg) !void {
592- try mkdirAll(self.socket_dir, @intCast(self.dir_mode));
593- try mkdirAll(self.log_dir, @intCast(self.dir_mode));
604+ pub fn mkdir(self: *Cfg, io: std.Io) !void {
605+ const sock_perms = std.Io.Dir.Permissions.fromMode(@intCast(self.dir_mode));
606+ try mkdirAll(io, self.socket_dir, sock_perms);
607+ const log_perms = std.Io.Dir.Permissions.fromMode(@intCast(self.dir_mode));
608+ try mkdirAll(io, self.log_dir, log_perms);
594609 }
595610
596- fn mkdirAll(sub_dir_path: []const u8, mode: posix.mode_t) !void {
597- var it = try std.fs.path.componentIterator(sub_dir_path);
611+ fn mkdirAll(io: std.Io, sub_dir_path: []const u8, permissions: std.Io.Dir.Permissions) !void {
612+ var it = std.fs.path.componentIterator(sub_dir_path);
598613 var component = it.last() orelse return error.BadPathName;
599614 while (true) {
600- posix.mkdirat(posix.AT.FDCWD, component.path, mode) catch |err| switch (err) {
615+ std.Io.Dir.createDirAbsolute(io, component.path, permissions) catch |err| switch (err) {
601616 error.PathAlreadyExists => {},
602617 error.FileNotFound => |e| {
603618 component = it.previous() orelse return e;
......@@ -617,7 +632,7 @@ test "Cfg.init uses default modes when env vars are not set" {
617632 _ = cross.c.unsetenv("ZMX_DIR_MODE");
618633 _ = cross.c.unsetenv("ZMX_LOG_MODE");
619634
620- var cfg = try Cfg.init(alloc);
635+ var cfg = try Cfg.init(alloc, std.testing.io);
621636 defer cfg.deinit(alloc);
622637
623638 try std.testing.expectEqual(@as(u32, 0o750), cfg.dir_mode);
......@@ -635,7 +650,7 @@ test "Cfg.init uses custom modes from env vars" {
635650 _ = cross.c.unsetenv("ZMX_LOG_MODE");
636651 }
637652
638- var cfg = try Cfg.init(alloc);
653+ var cfg = try Cfg.init(alloc, std.testing.io);
639654 defer cfg.deinit(alloc);
640655
641656 try std.testing.expectEqual(@as(u32, 0o770), cfg.dir_mode);
......@@ -651,6 +666,7 @@ test "Cfg.init uses custom modes from env vars" {
651666 ///
652667 /// Conceptually it's also much simpler to reason about.
653668 const Daemon = struct {
669+ io: std.Io,
654670 cfg: *Cfg,
655671 alloc: std.mem.Allocator,
656672 clients: std.ArrayList(*Client),
......@@ -673,6 +689,7 @@ const Daemon = struct {
673689 task_ended_at: ?u64 = null, // timestamp when task exited
674690 pty_fd: i32 = -1, // set by daemonLoop so handleRun can probe the foreground process
675691 pty_write_buf: std.ArrayList(u8) = .empty,
692+ shell: []const u8 = "/bin/sh",
676693
677694 const EnsureSessionResult = struct {
678695 created: bool,
......@@ -808,12 +825,14 @@ const Daemon = struct {
808825 for (cmd_args, 0..) |arg, i| {
809826 argv[i] = try alloc.dupeZ(u8, arg);
810827 }
811- const err = std.posix.execvpeZ(argv[0].?, argv.ptr, std.c.environ);
828+ const err = lib_posix.execvpeZ(argv[0].?, argv.ptr, std.c.environ);
812829 std.log.err("execvpe failed: cmd={s} err={s}", .{ cmd_args[0], @errorName(err) });
813- std.posix.exit(1);
830+ lib_posix.exit(1);
814831 }
815832
816- const shell: [:0]const u8 = if (self.is_task_mode) "bash" else util.detectShell();
833+ var buf: [256]u8 = undefined;
834+ const z = try std.fmt.bufPrintZ(&buf, "{s}", .{self.shell});
835+ const shell: [:0]const u8 = if (self.is_task_mode) "bash" else z;
817836 // Use "-shellname" as argv[0] to signal login shell (traditional method)
818837 const login_shell = try std.fmt.allocPrintSentinel(
819838 alloc,
......@@ -822,9 +841,9 @@ const Daemon = struct {
822841 0,
823842 );
824843 const argv = [_:null]?[*:0]const u8{ login_shell, null };
825- const err = std.posix.execvpeZ(shell, &argv, std.c.environ);
844+ const err = lib_posix.execvpeZ(shell, &argv, std.c.environ);
826845 std.log.err("execvpe failed: shell={s} err={s}", .{ shell, @errorName(err) });
827- std.posix.exit(1);
846+ lib_posix.exit(1);
828847 }
829848
830849 /// spawnPty runs forkpty() and executes the shell or shell command the user provides.
......@@ -850,7 +869,7 @@ const Daemon = struct {
850869 // errdefers that delete the parent's socket file).
851870 execChild(self) catch |err| {
852871 std.log.err("child setup failed: {s}", .{@errorName(err)});
853- std.posix.exit(1);
872+ lib_posix.exit(1);
854873 };
855874 unreachable; // execChild either execs or exits, never returns ok
856875 }
......@@ -859,8 +878,8 @@ const Daemon = struct {
859878 std.log.info("pty spawned session={s} pid={d}", .{ self.session_name, pid });
860879
861880 // make pty non-blocking
862- const flags = try posix.fcntl(master_fd, posix.F.GETFL, 0);
863- _ = try posix.fcntl(master_fd, posix.F.SETFL, flags | O_NONBLOCK);
881+ const flags = try lib_posix.fcntl(master_fd, posix.F.GETFL, 0);
882+ _ = try lib_posix.fcntl(master_fd, posix.F.SETFL, flags | O_NONBLOCK);
864883 return master_fd;
865884 }
866885
......@@ -868,15 +887,15 @@ const Daemon = struct {
868887 /// If not it creates one and spawns the daemon.
869888 fn ensureSession(self: *Daemon) !EnsureSessionResult {
870889 std.log.info("ensure session session={s}", .{self.session_name});
871- var dir = try std.fs.openDirAbsolute(self.cfg.socket_dir, .{});
872- defer dir.close();
890+ var dir = try std.Io.Dir.openDirAbsolute(self.io, self.cfg.socket_dir, .{});
891+ defer dir.close(self.io);
873892
874- const exists = try socket.sessionExists(dir, self.session_name);
893+ const exists = try socket.sessionExists(self.io, dir, self.session_name);
875894 var should_create = !exists;
876895
877896 if (exists) {
878897 if (ipc.connectSession(self.socket_path)) |fd| {
879- posix.close(fd);
898+ lib_posix.close(fd);
880899 if (self.command != null) {
881900 std.log.warn(
882901 "session already exists, ignoring command session={s}",
......@@ -886,7 +905,7 @@ const Daemon = struct {
886905 } else |err| switch (err) {
887906 // Daemon is definitively gone: safe to replace.
888907 error.ConnectionRefused => {
889- socket.cleanupStaleSocket(dir, self.session_name);
908+ socket.cleanupStaleSocket(self.io, dir, self.session_name);
890909 should_create = true;
891910 },
892911 // Connect failed for an unusual reason. The check is only to
......@@ -906,10 +925,10 @@ const Daemon = struct {
906925 const server_sock_fd = try socket.createSocket(self.socket_path);
907926
908927 // creates the daemon
909- const pid = try posix.fork();
928+ const pid = try lib_posix.fork();
910929 if (pid == 0) { // child (daemon)
911930 // becomes the session leader and detaches process from its controlling terminal
912- _ = try posix.setsid();
931+ _ = try lib_posix.setsid();
913932
914933 log_system.deinit();
915934
......@@ -919,7 +938,7 @@ const Daemon = struct {
919938 // keyword) stays open for the daemon's lifetime, causing
920939 // the caller to hang waiting for EOF.
921940 {
922- const devnull = std.posix.open(
941+ const devnull = lib_posix.open(
923942 "/dev/null",
924943 .{ .ACCMODE = .RDWR },
925944 0,
......@@ -928,12 +947,12 @@ const Daemon = struct {
928947 return err;
929948 };
930949 inline for (.{ posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO }) |fd| {
931- _ = posix.dup2(devnull, fd) catch |err| {
950+ _ = lib_posix.dup2(devnull, fd) catch |err| {
932951 std.log.warn("dup2 /dev/null -> {d}: {s}", .{ fd, @errorName(err) });
933952 return err;
934953 };
935954 }
936- if (devnull > 2) posix.close(devnull);
955+ if (devnull > 2) lib_posix.close(devnull);
937956 }
938957
939958 // Close file descriptors inherited from the parent that the
......@@ -948,7 +967,7 @@ const Daemon = struct {
948967 // Skip server_sock_fd (needed for IPC) and dir.fd (needed to
949968 // delete the socket file on shutdown).
950969 {
951- const dir_fd = @as(i32, @intCast(dir.fd));
970+ const dir_fd = @as(i32, @intCast(dir.handle));
952971 var fd: i32 = 3;
953972 while (fd < 64) : (fd += 1) {
954973 if (fd == server_sock_fd or fd == dir_fd) continue;
......@@ -967,14 +986,15 @@ const Daemon = struct {
967986 &.{ self.cfg.log_dir, session_log_name },
968987 );
969988 defer self.alloc.free(session_log_path);
970- try log_system.init(self.alloc, session_log_path, self.cfg.log_mode);
989+ const log_mode = std.Io.File.Permissions.fromMode(self.cfg.log_mode);
990+ try log_system.init(self.alloc, self.io, session_log_path, log_mode);
971991
972992 // If spawnPty fails, clean up here. Once it succeeds,
973993 // the inner block's defer takes ownership of cleanup to
974994 // avoid double-closing server_sock_fd on daemonLoop error.
975995 const pty_fd = self.spawnPty() catch |err| {
976- posix.close(server_sock_fd);
977- dir.deleteFile(self.session_name) catch {};
996+ lib_posix.close(server_sock_fd);
997+ dir.deleteFile(self.io, self.session_name) catch {};
978998 return err;
979999 };
9801000
......@@ -983,23 +1003,23 @@ const Daemon = struct {
9831003 // 500ms SIGHUP->SIGKILL grace sleep. Otherwise a `zmx run`
9841004 // for the same name issued in that window will hang waiting
9851005 // for a connect.
986- posix.close(server_sock_fd);
1006+ lib_posix.close(server_sock_fd);
9871007 std.log.info("deleting socket file session={s}", .{self.session_name});
988- dir.deleteFile(self.session_name) catch |err| {
1008+ dir.deleteFile(self.io, self.session_name) catch |err| {
9891009 std.log.warn("failed to delete socket file err={s}", .{@errorName(err)});
9901010 };
9911011 self.handleKill();
9921012 self.deinit();
993- posix.close(pty_fd);
994- _ = posix.waitpid(self.pid, 0);
1013+ lib_posix.close(pty_fd);
1014+ _ = lib_posix.waitpid(self.pid, 0);
9951015 }
9961016
9971017 try daemonLoop(self, server_sock_fd, pty_fd);
9981018 std.log.info("daemon loop shutdown", .{});
9991019 return .{ .created = true, .is_daemon = true };
10001020 }
1001- posix.close(server_sock_fd);
1002- std.Thread.sleep(10 * std.time.ns_per_ms);
1021+ lib_posix.close(server_sock_fd);
1022+ std.Io.sleep(self.io, std.Io.Duration.fromMilliseconds(10), .real) catch unreachable;
10031023 return .{ .created = true, .is_daemon = false };
10041024 }
10051025
......@@ -1204,7 +1224,7 @@ const Daemon = struct {
12041224 posix.kill(-self.pid, posix.SIG.HUP) catch |err| {
12051225 std.log.warn("failed to send SIGHUP to pty child err={s}", .{@errorName(err)});
12061226 };
1207- std.Thread.sleep(500 * std.time.ns_per_ms);
1227+ std.Io.sleep(self.io, std.Io.Duration.fromMilliseconds(500), .real) catch unreachable;
12081228 posix.kill(-self.pid, posix.SIG.KILL) catch |err| {
12091229 std.log.warn("failed to send SIGKILL to pty child err={s}", .{@errorName(err)});
12101230 };
......@@ -1265,7 +1285,7 @@ const Daemon = struct {
12651285 payload: []const u8,
12661286 ) !void {
12671287 const format: util.HistoryFormat = if (payload.len > 0)
1268- std.meta.intToEnum(util.HistoryFormat, payload[0]) catch .plain
1288+ @enumFromInt(payload[0])
12691289 else
12701290 .plain;
12711291 if (util.serializeTerminal(self.alloc, term, format)) |output| {
......@@ -1385,6 +1405,7 @@ test "send queues PTY input without changing leader" {
13851405 .leader_client_fd = 42,
13861406 .session_name = "test",
13871407 .socket_path = "",
1408+ .io = std.testing.io,
13881409 .running = true,
13891410 .pid = 0,
13901411 .created_at = 0,
......@@ -1397,9 +1418,9 @@ test "send queues PTY input without changing leader" {
13971418 try std.testing.expectEqualStrings("hello", daemon.pty_write_buf.items);
13981419 }
13991420
1400-fn printVersion(cfg: *Cfg) !void {
1421+fn printVersion(io: std.Io, cfg: *Cfg) !void {
14011422 var buf: [256]u8 = undefined;
1402- var w = std.fs.File.stdout().writer(&buf);
1423+ var w = std.Io.File.stdout().writer(io, &buf);
14031424 try w.interface.print(
14041425 "zmx\t\t{s}\nghostty_vt\t{s}\nsocket_dir\t{s}\nlog_dir\t\t{s}\n",
14051426 .{ version, ghostty_version, cfg.socket_dir, cfg.log_dir },
......@@ -1407,15 +1428,15 @@ fn printVersion(cfg: *Cfg) !void {
14071428 try w.interface.flush();
14081429 }
14091430
1410-fn printCompletions(shell: completions.Shell) !void {
1431+fn printCompletions(io: std.Io, shell: completions.Shell) !void {
14111432 const script = shell.getCompletionScript();
14121433 var buf: [8192]u8 = undefined;
1413- var w = std.fs.File.stdout().writer(&buf);
1434+ var w = std.Io.File.stdout().writer(io, &buf);
14141435 try w.interface.print("{s}\n", .{script});
14151436 try w.interface.flush();
14161437 }
14171438
1418-fn help() !void {
1439+fn help(io: std.Io) !void {
14191440 const help_text =
14201441 \\zmx - session persistence for terminal processes
14211442 \\
......@@ -1553,16 +1574,12 @@ fn help() !void {
15531574 \\
15541575 ;
15551576 var buf: [8192]u8 = undefined;
1556- var w = std.fs.File.stdout().writer(&buf);
1577+ var w = std.Io.File.stdout().writer(io, &buf);
15571578 try w.interface.print(help_text, .{});
15581579 try w.interface.flush();
15591580 }
15601581
1561-fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) !u8 {
1562- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1563- defer _ = gpa.deinit();
1564- const alloc = gpa.allocator();
1565-
1582+fn tail(alloc: std.mem.Allocator, client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) !u8 {
15661583 var poll_fds = try std.ArrayList(posix.pollfd).initCapacity(alloc, 4);
15671584 defer poll_fds.deinit(alloc);
15681585
......@@ -1625,7 +1642,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool)
16251642 switch (msg.header.tag) {
16261643 .Ack => {
16271644 if (detached) {
1628- _ = posix.write(posix.STDOUT_FILENO, "command sent!\n") catch |err| blk: {
1645+ _ = lib_posix.write(posix.STDOUT_FILENO, "command sent!\n") catch |err| blk: {
16291646 if (err == error.WouldBlock) break :blk 0;
16301647 return err;
16311648 };
......@@ -1687,7 +1704,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool)
16871704 if (task_complete_code) |exit_code| {
16881705 // Flush any remaining output before returning
16891706 flush_loop: while (stdout_buf.items.len > 0) {
1690- const n = posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| {
1707+ const n = lib_posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| {
16911708 if (err == error.WouldBlock) break :flush_loop;
16921709 return err;
16931710 };
......@@ -1697,7 +1714,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool)
16971714 }
16981715
16991716 if (stdout_buf.items.len > 0) {
1700- const n = posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| blk: {
1717+ const n = lib_posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| blk: {
17011718 if (err == error.WouldBlock) break :blk 0;
17021719 return err;
17031720 };
......@@ -1715,17 +1732,13 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool)
17151732 }
17161733 }
17171734
1718-fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
1719- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1720- defer _ = gpa.deinit();
1721- const alloc = gpa.allocator();
1722-
1735+fn wait(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
17231736 var stdout_buffer: [1024]u8 = undefined;
1724- var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
1737+ var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
17251738 const stdout = &stdout_writer.interface;
17261739
17271740 var stderr_buffer: [1024]u8 = undefined;
1728- var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
1741+ var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
17291742 const stderr = &stderr_writer.interface;
17301743
17311744 // Highest match count seen so far. Lets us distinguish "sessions haven't
......@@ -1735,11 +1748,11 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
17351748 var zero_match_iters: u32 = 0;
17361749
17371750 var agg_exit_code: u8 = 0;
1738- var last_print: i64 = 0;
1751+ var last_print: i96 = 0;
17391752 var prev_done: i32 = 0;
17401753 while (true) {
17411754 agg_exit_code = 0;
1742- var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
1755+ var sessions = try util.get_session_entries(alloc, io, cfg.socket_dir);
17431756 var total: i32 = 0;
17441757 var done: i32 = 0;
17451758
......@@ -1763,7 +1776,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
17631776 // waiting". Count it as done+failed so wait terminates.
17641777 try stderr.print(
17651778 "[{d}] task unreachable: {s} ({s})\n",
1766- .{ std.time.timestamp(), session.name, session.error_name orelse "unknown" },
1779+ .{ std.Io.Timestamp.now(io, .real).nanoseconds, session.name, session.error_name orelse "unknown" },
17671780 );
17681781 try stderr.flush();
17691782 agg_exit_code = 1;
......@@ -1771,7 +1784,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
17711784 continue;
17721785 }
17731786 if (session.task_ended_at == 0) {
1774- const now = std.time.timestamp();
1787+ const now = std.Io.Timestamp.now(io, .real).nanoseconds;
17751788 if (now - last_print >= 5) {
17761789 try stdout.print(
17771790 "[{d}] waiting task={s}\n",
......@@ -1834,7 +1847,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
18341847 }
18351848
18361849 prev_done = done;
1837- std.Thread.sleep(1000 * std.time.ns_per_ms);
1850+ std.Io.sleep(io, std.Io.Duration.fromMilliseconds(1000), .real) catch unreachable;
18381851 }
18391852
18401853 if (agg_exit_code == 0) {
......@@ -1844,7 +1857,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
18441857 }
18451858 try stdout.flush();
18461859
1847- const sessions = try util.get_session_entries(alloc, cfg.socket_dir);
1860+ const sessions = try util.get_session_entries(alloc, io, cfg.socket_dir);
18481861 for (sessions.items) |session| {
18491862 var found = false;
18501863 for (matchers.items) |m| {
......@@ -1866,7 +1879,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
18661879
18671880 // Fetch and print the last 20 lines of history for debugging
18681881 const history_lines: usize = 20;
1869- const history_text = fetchHistory(alloc, cfg, session.name) catch null;
1882+ const history_text = fetchHistory(alloc, io, cfg, session.name) catch null;
18701883 if (history_text) |text| {
18711884 defer alloc.free(text);
18721885 try stdout.print("\nLast {d} lines of {s} history:\n", .{ history_lines, session.name });
......@@ -1897,15 +1910,11 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
18971910 std.process.exit(agg_exit_code);
18981911 }
18991912
1900-fn list(cfg: *Cfg, short: bool) !void {
1901- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1902- defer _ = gpa.deinit();
1903- const alloc = gpa.allocator();
1904-
1913+fn list(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, short: bool) !void {
19051914 const current_session = socket.getSeshNameFromEnv();
19061915 var buf: [4096]u8 = undefined;
1907- var stdout = std.fs.File.stdout().writer(&buf);
1908- var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
1916+ var stdout = std.Io.File.stdout().writer(io, &buf);
1917+ var sessions = try util.get_session_entries(alloc, io, cfg.socket_dir);
19091918 defer {
19101919 for (sessions.items) |session| {
19111920 session.deinit(alloc);
......@@ -1916,7 +1925,7 @@ fn list(cfg: *Cfg, short: bool) !void {
19161925 if (sessions.items.len == 0) {
19171926 if (short) return;
19181927 var errbuf: [4096]u8 = undefined;
1919- var stderr = std.fs.File.stderr().writer(&errbuf);
1928+ var stderr = std.Io.File.stderr().writer(io, &errbuf);
19201929 try stderr.interface.print("no sessions found in {s}\n", .{cfg.socket_dir});
19211930 try stderr.interface.flush();
19221931 return;
......@@ -1936,10 +1945,7 @@ fn list(cfg: *Cfg, short: bool) !void {
19361945 }
19371946 }
19381947
1939-fn detachAll(cfg: *Cfg) !void {
1940- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1941- defer _ = gpa.deinit();
1942- const alloc = gpa.allocator();
1948+fn detachAll(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg) !void {
19431949 const session_name = socket.getSeshNameFromEnv();
19441950 if (session_name.len == 0) {
19451951 std.log.err("ZMX_SESSION env var not found: are you inside a zmx session?", .{});
......@@ -1947,45 +1953,41 @@ fn detachAll(cfg: *Cfg) !void {
19471953 }
19481954 std.log.info("detach all session={s}", .{session_name});
19491955
1950- var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
1951- defer dir.close();
1956+ var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
1957+ defer dir.close(io);
19521958
19531959 const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1954- error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
1960+ error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
19551961 error.OutOfMemory => return err,
19561962 };
19571963 defer alloc.free(socket_path);
19581964 const fd = ipc.connectSession(socket_path) catch |err| {
19591965 std.log.err("session unresponsive: {s}", .{@errorName(err)});
1960- if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
1966+ if (err == error.ConnectionRefused) socket.cleanupStaleSocket(io, dir, session_name);
19611967 return;
19621968 };
1963- defer posix.close(fd);
1969+ defer lib_posix.close(fd);
19641970 ipc.send(fd, .DetachAll, "") catch |err| switch (err) {
19651971 error.BrokenPipe, error.ConnectionResetByPeer => return,
19661972 else => return err,
19671973 };
19681974 }
19691975
1970-fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
1976+fn kill(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, force: bool) !void {
19711977 std.log.info("kill session={s}", .{session_name});
1972- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1973- defer _ = gpa.deinit();
1974- const alloc = gpa.allocator();
1975-
19761978 const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1977- error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
1979+ error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
19781980 error.OutOfMemory => return err,
19791981 };
19801982 defer alloc.free(socket_path);
19811983
1982- var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
1983- defer dir.close();
1984+ var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
1985+ defer dir.close(io);
19841986
1985- const exists = try socket.sessionExists(dir, session_name);
1987+ const exists = try socket.sessionExists(io, dir, session_name);
19861988 if (!exists) {
19871989 var buf: [4096]u8 = undefined;
1988- var w = std.fs.File.stderr().writer(&buf);
1990+ var w = std.Io.File.stderr().writer(io, &buf);
19891991 w.interface.print("error: session \"{s}\" does not exist\n", .{session_name}) catch {};
19901992 w.interface.flush() catch {};
19911993 return error.SessionNotFound;
......@@ -1993,9 +1995,9 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
19931995 const fd = ipc.connectSession(socket_path) catch |err| {
19941996 std.log.err("session unresponsive: {s}", .{@errorName(err)});
19951997 var buf: [4096]u8 = undefined;
1996- var w = std.fs.File.stdout().writer(&buf);
1998+ var w = std.Io.File.stdout().writer(io, &buf);
19971999 if (force or err == error.ConnectionRefused) {
1998- socket.cleanupStaleSocket(dir, session_name);
2000+ socket.cleanupStaleSocket(io, dir, session_name);
19992001 w.interface.print("cleaned up stale session {s}\n", .{session_name}) catch {};
20002002 } else {
20012003 w.interface.print(
......@@ -2007,7 +2009,7 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
20072009 return;
20082010 };
20092011
2010- defer posix.close(fd);
2012+ defer lib_posix.close(fd);
20112013 ipc.send(fd, .Kill, "") catch |err| switch (err) {
20122014 error.BrokenPipe, error.ConnectionResetByPeer => return,
20132015 else => return err,
......@@ -2025,14 +2027,14 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
20252027 }
20262028
20272029 var buf: [100]u8 = undefined;
2028- var w = std.fs.File.stdout().writer(&buf);
2030+ var w = std.Io.File.stdout().writer(io, &buf);
20292031 try w.interface.print("killed session {s}\n", .{session_name});
20302032 try w.interface.flush();
20312033 }
20322034
2033-fn printLabelError(session_name: []const u8, err: anyerror) noreturn {
2035+fn printLabelError(io: std.Io, session_name: []const u8, err: anyerror) noreturn {
20342036 var buf: [4096]u8 = undefined;
2035- var w = std.fs.File.stderr().writer(&buf);
2037+ var w = std.Io.File.stderr().writer(io, &buf);
20362038 switch (err) {
20372039 error.Timeout => w.interface.print(
20382040 "error: session \"{s}\" does not support labels (daemon too old?)\n",
......@@ -2051,25 +2053,22 @@ fn printLabelError(session_name: []const u8, err: anyerror) noreturn {
20512053 std.process.exit(1);
20522054 }
20532055
2054-fn labelGet(cfg: *Cfg, session_name: []const u8, single_kv: []const u8) !void {
2056+fn labelGet(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, single_kv: []const u8) !void {
20552057 std.log.info("label get session={s}", .{session_name});
2056- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
2057- defer _ = gpa.deinit();
2058- const alloc = gpa.allocator();
20592058
20602059 const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
2061- error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
2060+ error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
20622061 error.OutOfMemory => return err,
20632062 };
20642063 defer alloc.free(socket_path);
20652064
20662065 const payload = ipc.roundTripForTag(alloc, socket_path, .LabelGet, "", .LabelData) catch |err| {
2067- printLabelError(session_name, err);
2066+ printLabelError(io, session_name, err);
20682067 };
20692068 defer alloc.free(payload);
20702069
20712070 var buf: [4096]u8 = undefined;
2072- var stdout = std.fs.File.stdout().writer(&buf);
2071+ var stdout = std.Io.File.stdout().writer(io, &buf);
20732072 if (single_kv.len == 0) {
20742073 try stdout.interface.print("{s}", .{payload});
20752074 try stdout.interface.flush();
......@@ -2081,17 +2080,14 @@ fn labelGet(cfg: *Cfg, session_name: []const u8, single_kv: []const u8) !void {
20812080 try stdout.interface.flush();
20822081 }
20832082
2084-fn labelSet(cfg: *Cfg, session_name: []const u8, labels: []const u8) !void {
2083+fn labelSet(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, labels: []const u8) !void {
20852084 std.log.info("label set session={s}", .{session_name});
2086- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
2087- defer _ = gpa.deinit();
2088- const alloc = gpa.allocator();
20892085
20902086 var kvs = label.LabelIterator.init(labels);
20912087 while (kvs.next()) |kv| {
20922088 label.assertLabel(kv.key, kv.value) catch |err| {
20932089 var buf: [4096]u8 = undefined;
2094- var w = std.fs.File.stderr().writer(&buf);
2090+ var w = std.Io.File.stderr().writer(io, &buf);
20952091 const msg = "error: key-value kvs can only contain [a-z, A-Z, 0-9, -_.] characters";
20962092 switch (err) {
20972093 error.LabelKeyEmpty => {
......@@ -2113,30 +2109,27 @@ fn labelSet(cfg: *Cfg, session_name: []const u8, labels: []const u8) !void {
21132109 }
21142110
21152111 const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
2116- error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
2112+ error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
21172113 error.OutOfMemory => return err,
21182114 };
21192115 defer alloc.free(socket_path);
21202116
21212117 _ = ipc.roundTripForTag(alloc, socket_path, .LabelSet, labels, .Ack) catch |err| {
2122- printLabelError(session_name, err);
2118+ printLabelError(io, session_name, err);
21232119 };
21242120 }
21252121
2126-fn labelClear(cfg: *Cfg, session_name: []const u8) !void {
2122+fn labelClear(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8) !void {
21272123 std.log.info("label clear session={s}", .{session_name});
2128- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
2129- defer _ = gpa.deinit();
2130- const alloc = gpa.allocator();
21312124
21322125 const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
2133- error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
2126+ error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
21342127 error.OutOfMemory => return err,
21352128 };
21362129 defer alloc.free(socket_path);
21372130
21382131 _ = ipc.roundTripForTag(alloc, socket_path, .LabelClear, "", .Ack) catch |err| {
2139- printLabelError(session_name, err);
2132+ printLabelError(io, session_name, err);
21402133 };
21412134 }
21422135
......@@ -2144,32 +2137,33 @@ fn labelClear(cfg: *Cfg, session_name: []const u8) !void {
21442137 /// string. Caller owns the returned memory and must free it.
21452138 fn fetchHistory(
21462139 alloc: std.mem.Allocator,
2140+ io: std.Io,
21472141 cfg: *Cfg,
21482142 session_name: []const u8,
21492143 ) ![]const u8 {
21502144 std.log.info("fetch history session={s}", .{session_name});
21512145 const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
21522146 error.NameTooLong => {
2153- socket.printSessionNameTooLong(session_name, cfg.socket_dir);
2147+ socket.printSessionNameTooLong(io, session_name, cfg.socket_dir);
21542148 return error.NameTooLong;
21552149 },
21562150 error.OutOfMemory => return err,
21572151 };
21582152 defer alloc.free(socket_path);
21592153
2160- var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
2161- defer dir.close();
2154+ var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
2155+ defer dir.close(io);
21622156
2163- const exists = try socket.sessionExists(dir, session_name);
2157+ const exists = try socket.sessionExists(io, dir, session_name);
21642158 if (!exists) {
21652159 return error.SessionNotFound;
21662160 }
21672161
21682162 const fd = ipc.connectSession(socket_path) catch |err| {
2169- if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
2163+ if (err == error.ConnectionRefused) socket.cleanupStaleSocket(io, dir, session_name);
21702164 return err;
21712165 };
2172- defer posix.close(fd);
2166+ defer lib_posix.close(fd);
21732167
21742168 const format_byte: u8 = @intFromEnum(util.HistoryFormat.plain);
21752169 const payload = [_]u8{format_byte};
......@@ -2205,35 +2199,32 @@ fn fetchHistory(
22052199 return error.NoHistoryResponse;
22062200 }
22072201
2208-fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !void {
2202+fn history(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !void {
22092203 std.log.info("history session={s}", .{session_name});
2210- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
2211- defer _ = gpa.deinit();
2212- const alloc = gpa.allocator();
22132204
22142205 const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
2215- error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
2206+ error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
22162207 error.OutOfMemory => return err,
22172208 };
22182209 defer alloc.free(socket_path);
22192210
2220- var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
2221- defer dir.close();
2211+ var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
2212+ defer dir.close(io);
22222213
2223- const exists = try socket.sessionExists(dir, session_name);
2214+ const exists = try socket.sessionExists(io, dir, session_name);
22242215 if (!exists) {
22252216 var buf: [4096]u8 = undefined;
2226- var w = std.fs.File.stderr().writer(&buf);
2217+ var w = std.Io.File.stderr().writer(io, &buf);
22272218 w.interface.print("error: session \"{s}\" does not exist\n", .{session_name}) catch {};
22282219 w.interface.flush() catch {};
22292220 return error.SessionNotFound;
22302221 }
22312222 const fd = ipc.connectSession(socket_path) catch |err| {
22322223 std.log.err("session unresponsive: {s}", .{@errorName(err)});
2233- if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
2224+ if (err == error.ConnectionRefused) socket.cleanupStaleSocket(io, dir, session_name);
22342225 return;
22352226 };
2236- defer posix.close(fd);
2227+ defer lib_posix.close(fd);
22372228
22382229 const format_byte = [_]u8{@intFromEnum(format)};
22392230 ipc.send(fd, .History, &format_byte) catch |err| switch (err) {
......@@ -2257,7 +2248,7 @@ fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !voi
22572248
22582249 while (sb.next()) |msg| {
22592250 if (msg.header.tag == .History) {
2260- _ = posix.write(posix.STDOUT_FILENO, msg.payload) catch return;
2251+ _ = lib_posix.write(posix.STDOUT_FILENO, msg.payload) catch return;
22612252 return;
22622253 }
22632254 }
......@@ -2271,28 +2262,28 @@ fn switchSesh(daemon: *Daemon, current_sesh: []const u8) !void {
22712262 std.log.info("switch session cur={s} next={s}", .{ current_sesh, next_session });
22722263
22732264 const socket_path = socket.getSocketPath(daemon.alloc, daemon.cfg.socket_dir, current_sesh) catch |err| switch (err) {
2274- error.NameTooLong => return socket.printSessionNameTooLong(current_sesh, daemon.cfg.socket_dir),
2265+ error.NameTooLong => return socket.printSessionNameTooLong(daemon.io, current_sesh, daemon.cfg.socket_dir),
22752266 error.OutOfMemory => return err,
22762267 };
22772268 defer daemon.alloc.free(socket_path);
22782269
2279- var dir = try std.fs.openDirAbsolute(daemon.cfg.socket_dir, .{});
2280- defer dir.close();
2270+ var dir = try std.Io.Dir.openDirAbsolute(daemon.io, daemon.cfg.socket_dir, .{});
2271+ defer dir.close(daemon.io);
22812272
2282- const exists = try socket.sessionExists(dir, current_sesh);
2273+ const exists = try socket.sessionExists(daemon.io, dir, current_sesh);
22832274 if (!exists) {
22842275 var buf: [4096]u8 = undefined;
2285- var w = std.fs.File.stderr().writer(&buf);
2276+ var w = std.Io.File.stderr().writer(daemon.io, &buf);
22862277 w.interface.print("error: session \"{s}\" does not exist\n", .{current_sesh}) catch {};
22872278 w.interface.flush() catch {};
22882279 return error.SessionNotFound;
22892280 }
22902281 const fd = ipc.connectSession(socket_path) catch |err| {
22912282 std.log.err("session unresponsive: {s}", .{@errorName(err)});
2292- if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, current_sesh);
2283+ if (err == error.ConnectionRefused) socket.cleanupStaleSocket(daemon.io, dir, current_sesh);
22932284 return;
22942285 };
2295- defer posix.close(fd);
2286+ defer lib_posix.close(fd);
22962287
22972288 ipc.send(fd, .Switch, next_session) catch |err| switch (err) {
22982289 error.BrokenPipe, error.ConnectionResetByPeer => return,
......@@ -2331,7 +2322,7 @@ fn attach(daemon: *Daemon) !void {
23312322 }
23322323 // Reset terminal modes on detach
23332324 const restore_seq = "\x1bc";
2334- _ = posix.write(posix.STDOUT_FILENO, restore_seq) catch {};
2325+ _ = lib_posix.write(posix.STDOUT_FILENO, restore_seq) catch {};
23352326 }
23362327
23372328 if (stdin_is_tty) {
......@@ -2355,7 +2346,7 @@ fn attach(daemon: *Daemon) !void {
23552346 // Clear screen before attaching. This provides a clean slate before
23562347 // the session restore.
23572348 const clear_seq = "\x1b[2J\x1b[H";
2358- _ = try posix.write(posix.STDOUT_FILENO, clear_seq);
2349+ _ = try lib_posix.write(posix.STDOUT_FILENO, clear_seq);
23592350
23602351 const looper = try clientLoop(client_sock);
23612352 switch (looper.kind) {
......@@ -2363,13 +2354,15 @@ fn attach(daemon: *Daemon) !void {
23632354 .switch_session => {
23642355 if (looper.session_name) |session_name| {
23652356 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
2366- const cwd = std.posix.getcwd(&cwd_buf) catch "";
2357+ const cwd_len = std.process.currentPath(daemon.io, &cwd_buf) catch 0;
2358+ const cwd = cwd_buf[0..cwd_len];
23672359 const target_path = socket.getSocketPath(
23682360 daemon.alloc,
23692361 daemon.cfg.socket_dir,
23702362 session_name,
23712363 ) catch |err| switch (err) {
23722364 error.NameTooLong => return socket.printSessionNameTooLong(
2365+ daemon.io,
23732366 session_name,
23742367 daemon.cfg.socket_dir,
23752368 ),
......@@ -2378,6 +2371,7 @@ fn attach(daemon: *Daemon) !void {
23782371
23792372 const clients = try std.ArrayList(*Client).initCapacity(daemon.alloc, 10);
23802373 var target_daemon = Daemon{
2374+ .io = daemon.io,
23812375 .running = true,
23822376 .cfg = daemon.cfg,
23832377 .alloc = daemon.alloc,
......@@ -2386,7 +2380,7 @@ fn attach(daemon: *Daemon) !void {
23862380 .socket_path = target_path,
23872381 .pid = undefined,
23882382 .cwd = cwd,
2389- .created_at = @intCast(std.time.timestamp()),
2383+ .created_at = @intCast(std.Io.Timestamp.now(daemon.io, .real).nanoseconds),
23902384 .leader_client_fd = null,
23912385 };
23922386 return attach(&target_daemon);
......@@ -2397,7 +2391,7 @@ fn attach(daemon: *Daemon) !void {
23972391
23982392 fn writeFile(daemon: *Daemon, file_path: []const u8) !void {
23992393 var buf: [4096]u8 = undefined;
2400- var w = std.fs.File.stdout().writer(&buf);
2394+ var w = std.Io.File.stdout().writer(daemon.io, &buf);
24012395 const sesh_result = try daemon.ensureSession();
24022396 if (sesh_result.is_daemon) return;
24032397
......@@ -2425,18 +2419,19 @@ fn writeFile(daemon: *Daemon, file_path: []const u8) !void {
24252419 daemon.session_name,
24262420 ) catch |err| switch (err) {
24272421 error.NameTooLong => return socket.printSessionNameTooLong(
2422+ daemon.io,
24282423 daemon.session_name,
24292424 daemon.cfg.socket_dir,
24302425 ),
24312426 error.OutOfMemory => return err,
24322427 };
2433- var dir = try std.fs.openDirAbsolute(daemon.cfg.socket_dir, .{});
2434- defer dir.close();
2428+ var dir = try std.Io.Dir.openDirAbsolute(daemon.io, daemon.cfg.socket_dir, .{});
2429+ defer dir.close(daemon.io);
24352430
24362431 const result = ipc.probeSession(daemon.alloc, socket_path) catch |err| {
24372432 std.log.err("session unresponsive: {s}", .{@errorName(err)});
24382433 if (err == error.ConnectionRefused) {
2439- socket.cleanupStaleSocket(dir, daemon.session_name);
2434+ socket.cleanupStaleSocket(daemon.io, dir, daemon.session_name);
24402435 w.interface.print("cleaned up stale session {s}\n", .{daemon.session_name}) catch {};
24412436 } else {
24422437 w.interface.print(
......@@ -2483,11 +2478,10 @@ fn writeFile(daemon: *Daemon, file_path: []const u8) !void {
24832478 return error.NoAckReceived;
24842479 }
24852480
2486-fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts: [][]const u8, tag: ipc.Tag) !void {
2481+fn send(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts: [][]const u8, tag: ipc.Tag) !void {
24872482 std.log.info("send session={s}", .{session_name});
2488- const alloc = std.heap.c_allocator;
24892483 var buf: [4096]u8 = undefined;
2490- var w = std.fs.File.stdout().writer(&buf);
2484+ var w = std.Io.File.stdout().writer(io, &buf);
24912485
24922486 var payload = std.ArrayList(u8).empty;
24932487 defer payload.deinit(alloc);
......@@ -2499,16 +2493,16 @@ fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts
24992493 }
25002494 } else {
25012495 // Read from stdin when no text arguments provided.
2502- const stdin_fd = posix.STDIN_FILENO;
2503- if (!std.posix.isatty(stdin_fd)) {
2496+ const stdin_file = std.Io.File.stdin();
2497+ defer stdin_file.close(io);
2498+ var stdin_buf: [4096]u8 = undefined;
2499+ var reader = stdin_file.reader(io, &stdin_buf);
2500+ if (!try stdin_file.isTty(io)) {
25042501 while (true) {
2505- var tmp: [4096]u8 = undefined;
2506- const n = posix.read(stdin_fd, &tmp) catch |err| {
2507- if (err == error.WouldBlock) break;
2508- return err;
2509- };
2510- if (n == 0) break;
2511- try payload.appendSlice(alloc, tmp[0..n]);
2502+ var dest: [1024]u8 = undefined;
2503+ const n = try reader.interface.readSliceShort(&dest);
2504+ if (n == 0) break; // EOF
2505+ try payload.appendSlice(alloc, dest[0..n]);
25122506 }
25132507 // Strip trailing newline from piped input; the caller is
25142508 // responsible for including \r when submission is desired.
......@@ -2521,13 +2515,13 @@ fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts
25212515
25222516 if (payload.items.len == 0) return error.TextRequired;
25232517
2524- var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
2525- defer dir.close();
2518+ var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
2519+ defer dir.close(io);
25262520
25272521 const probe_result = ipc.probeSession(alloc, socket_path) catch |err| {
25282522 std.log.err("session unresponsive: {s}", .{@errorName(err)});
25292523 if (err == error.ConnectionRefused) {
2530- socket.cleanupStaleSocket(dir, session_name);
2524+ socket.cleanupStaleSocket(io, dir, session_name);
25312525 try w.interface.print("cleaned up stale session {s}\n", .{session_name});
25322526 } else {
25332527 try w.interface.print(
......@@ -2549,7 +2543,7 @@ fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts
25492543 fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
25502544 const alloc = daemon.alloc;
25512545 var buf: [4096]u8 = undefined;
2552- var w = std.fs.File.stdout().writer(&buf);
2546+ var w = std.Io.File.stdout().writer(daemon.io, &buf);
25532547
25542548 var cmd_to_send: ?[]const u8 = null;
25552549 var allocated_cmd: ?[]u8 = null;
......@@ -2587,19 +2581,19 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
25872581 cmd_to_send = try cmd_list.toOwnedSlice(alloc);
25882582 allocated_cmd = @constCast(cmd_to_send.?);
25892583 } else {
2590- const stdin_fd = posix.STDIN_FILENO;
2591- if (!std.posix.isatty(stdin_fd)) {
2592- var stdin_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
2593- defer stdin_buf.deinit(alloc);
2594-
2584+ // Read from stdin when no text arguments provided.
2585+ const stdin_file = std.Io.File.stdin();
2586+ defer stdin_file.close(daemon.io);
2587+ var stdin_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
2588+ defer stdin_buf.deinit(alloc);
2589+ var stdbuf: [4096]u8 = undefined;
2590+ var reader = stdin_file.reader(daemon.io, &stdbuf);
2591+ if (!try stdin_file.isTty(daemon.io)) {
25952592 while (true) {
2596- var tmp: [4096]u8 = undefined;
2597- const n = posix.read(stdin_fd, &tmp) catch |err| {
2598- if (err == error.WouldBlock) break;
2599- return err;
2600- };
2601- if (n == 0) break;
2602- try stdin_buf.appendSlice(alloc, tmp[0..n]);
2593+ var dest: [1024]u8 = undefined;
2594+ const n = try reader.interface.readSliceShort(&dest);
2595+ if (n == 0) break; // EOF
2596+ try stdin_buf.appendSlice(alloc, dest[0..n]);
26032597 }
26042598
26052599 if (stdin_buf.items.len > 0) {
......@@ -2615,6 +2609,35 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
26152609 allocated_cmd = @constCast(cmd_to_send.?);
26162610 }
26172611 }
2612+
2613+ // const stdin_fd = posix.STDIN_FILENO;
2614+ // if (!lib_posix.isatty(stdin_fd)) {
2615+ // var stdin_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
2616+ // defer stdin_buf.deinit(alloc);
2617+
2618+ // while (true) {
2619+ // var tmp: [4096]u8 = undefined;
2620+ // const n = posix.read(stdin_fd, &tmp) catch |err| {
2621+ // if (err == error.WouldBlock) break;
2622+ // return err;
2623+ // };
2624+ // if (n == 0) break;
2625+ // try stdin_buf.appendSlice(alloc, tmp[0..n]);
2626+ // }
2627+
2628+ // if (stdin_buf.items.len > 0) {
2629+ // // Normalize any trailing newline to CR so readline (raw mode)
2630+ // // accepts each line.
2631+ // if (stdin_buf.items[stdin_buf.items.len - 1] == '\n') {
2632+ // stdin_buf.items[stdin_buf.items.len - 1] = '\r';
2633+ // } else {
2634+ // try stdin_buf.append(alloc, '\r');
2635+ // }
2636+
2637+ // cmd_to_send = try alloc.dupe(u8, stdin_buf.items);
2638+ // allocated_cmd = @constCast(cmd_to_send.?);
2639+ // }
2640+ // }
26182641 }
26192642
26202643 if (cmd_to_send == null) {
......@@ -2625,7 +2648,7 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
26252648 std.log.err("session not ready: {s}", .{@errorName(err)});
26262649 return error.SessionNotReady;
26272650 };
2628- defer posix.close(client_sock);
2651+ defer lib_posix.close(client_sock);
26292652
26302653 var fds = try std.ArrayList(i32).initCapacity(alloc, 1);
26312654 defer fds.deinit(alloc);
......@@ -2636,8 +2659,8 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
26362659 else => return err,
26372660 };
26382661
2639- const exit_code = try tail(fds, detached, true);
2640- posix.exit(exit_code);
2662+ const exit_code = try tail(daemon.alloc, fds, detached, true);
2663+ lib_posix.exit(exit_code);
26412664 }
26422665
26432666 const ClientResult = struct {
......@@ -2654,15 +2677,15 @@ fn clientLoop(client_sock_fd: i32) !ClientResult {
26542677 std.log.info("client loop fd={d}", .{client_sock_fd});
26552678 // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
26562679 const alloc = std.heap.c_allocator;
2657- defer posix.close(client_sock_fd);
2680+ defer lib_posix.close(client_sock_fd);
26582681
26592682 try openSignalPipe();
2660- installWakeHandler(posix.SIG.WINCH);
2683+ installWakeHandler(@intFromEnum(posix.SIG.WINCH));
26612684
26622685 // Make socket non-blocking to avoid blocking on writes
2663- var sock_flags = try posix.fcntl(client_sock_fd, posix.F.GETFL, 0);
2686+ var sock_flags = try lib_posix.fcntl(client_sock_fd, posix.F.GETFL, 0);
26642687 sock_flags |= O_NONBLOCK;
2665- _ = try posix.fcntl(client_sock_fd, posix.F.SETFL, sock_flags);
2688+ _ = try lib_posix.fcntl(client_sock_fd, posix.F.SETFL, sock_flags);
26662689
26672690 // Buffer for outgoing socket writes
26682691 var sock_write_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
......@@ -2686,9 +2709,9 @@ fn clientLoop(client_sock_fd: i32) !ClientResult {
26862709 // Make stdin non-blocking. O_NONBLOCK is set on the open file description,
26872710 // which is shared with the parent shell; restore on exit to avoid
26882711 // corrupting the parent's stdin.
2689- const stdin_orig_flags = try posix.fcntl(stdin_fd, posix.F.GETFL, 0);
2690- _ = try posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags | O_NONBLOCK);
2691- defer _ = posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags) catch {};
2712+ const stdin_orig_flags = try lib_posix.fcntl(stdin_fd, posix.F.GETFL, 0);
2713+ _ = try lib_posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags | O_NONBLOCK);
2714+ defer _ = lib_posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags) catch {};
26922715
26932716 while (true) {
26942717 poll_fds.clearRetainingCapacity();
......@@ -2800,7 +2823,7 @@ fn clientLoop(client_sock_fd: i32) !ClientResult {
28002823 // Handle socket write (flush buffered messages to daemon)
28012824 if (poll_fds.items[1].revents & posix.POLL.OUT != 0) {
28022825 if (sock_write_buf.items.len > 0) {
2803- const n = posix.write(client_sock_fd, sock_write_buf.items) catch |err| blk: {
2826+ const n = lib_posix.write(client_sock_fd, sock_write_buf.items) catch |err| blk: {
28042827 if (err == error.WouldBlock) break :blk 0;
28052828 if (err == error.ConnectionResetByPeer or err == error.BrokenPipe) {
28062829 std.log.info("connection reset or broken pipe", .{});
......@@ -2815,7 +2838,7 @@ fn clientLoop(client_sock_fd: i32) !ClientResult {
28152838 }
28162839
28172840 if (stdout_buf.items.len > 0) {
2818- const n = posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| blk: {
2841+ const n = lib_posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| blk: {
28192842 if (err == error.WouldBlock) break :blk 0;
28202843 return err;
28212844 };
......@@ -2837,12 +2860,12 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
28372860 std.log.info("daemon started session={s} pty_fd={d}", .{ daemon.session_name, pty_fd });
28382861 daemon.pty_fd = pty_fd;
28392862 try openSignalPipe();
2840- installWakeHandler(posix.SIG.TERM);
2863+ installWakeHandler(@intFromEnum(lib_posix.SIG.TERM));
28412864 var poll_fds = try std.ArrayList(posix.pollfd).initCapacity(daemon.alloc, 8);
28422865 defer poll_fds.deinit(daemon.alloc);
28432866
28442867 const init_size = ipc.getTerminalSize(pty_fd);
2845- var term = try ghostty_vt.Terminal.init(daemon.alloc, .{
2868+ var term = try ghostty_vt.Terminal.init(daemon.io, daemon.alloc, .{
28462869 .cols = init_size.cols,
28472870 .rows = init_size.rows,
28482871 .max_scrollback = daemon.cfg.max_scrollback,
......@@ -2905,7 +2928,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
29052928 std.log.err("server socket error revents={d}", .{poll_fds.items[0].revents});
29062929 break :daemon_loop;
29072930 } else if (poll_fds.items[0].revents & posix.POLL.IN != 0) {
2908- const client_fd = try posix.accept(
2931+ const client_fd = try lib_posix.accept(
29092932 server_sock_fd,
29102933 null,
29112934 null,
......@@ -2979,7 +3002,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
29793002
29803003 if (util.findTaskExitMarker(scan_buf[0..scan_len])) |exit_code| {
29813004 daemon.task_exit_code = exit_code;
2982- daemon.task_ended_at = @intCast(std.time.timestamp());
3005+ daemon.task_ended_at = @intCast(std.Io.Timestamp.now(daemon.io, .real).nanoseconds);
29833006
29843007 std.log.info("task completed exit_code={d}", .{exit_code});
29853008
......@@ -3018,7 +3041,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
30183041
30193042 if (poll_fds.items[1].revents & posix.POLL.OUT != 0) {
30203043 while (daemon.pty_write_buf.items.len > 0) {
3021- const n = posix.write(pty_fd, daemon.pty_write_buf.items) catch |err| {
3044+ const n = lib_posix.write(pty_fd, daemon.pty_write_buf.items) catch |err| {
30223045 if (err != error.WouldBlock) {
30233046 std.log.warn("pty write failed: {s}", .{@errorName(err)});
30243047 daemon.pty_write_buf.clearRetainingCapacity();
......@@ -3102,7 +3125,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
31023125
31033126 if (revents & posix.POLL.OUT != 0) {
31043127 // Flush pending output buffers
3105- const n = posix.write(client.socket_fd, client.write_buf.items) catch |err| blk: {
3128+ const n = lib_posix.write(client.socket_fd, client.write_buf.items) catch |err| blk: {
31063129 if (err == error.WouldBlock) break :blk 0;
31073130 // Error on write, close client
31083131 const last = daemon.closeClient(client, i, false);
......@@ -3127,7 +3150,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
31273150 }
31283151 }
31293152
3130-fn wakeSignalPipe(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void {
3153+fn wakeSignalPipe(_: std.os.linux.SIG, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void {
31313154 const saved = std.c._errno().*;
31323155 _ = std.c.write(sig_pipe[1], "x", 1);
31333156 std.c._errno().* = saved;
......@@ -3142,7 +3165,7 @@ fn installWakeHandler(sig: u6) void {
31423165 .mask = posix.sigemptyset(),
31433166 .flags = posix.SA.SIGINFO,
31443167 };
3145- posix.sigaction(sig, &act, null);
3168+ posix.sigaction(@as(posix.SIG, @enumFromInt(sig)), &act, null);
31463169 }
31473170
31483171 fn ignoreSigpipe() void {
+1170, -0
......@@ -0,0 +1,1170 @@
1+const builtin = @import("builtin");
2+const std = @import("std");
3+const maxInt = std.math.maxInt;
4+const assert = std.debug.assert;
5+const mem = std.mem;
6+const native_os = builtin.os.tag;
7+const use_libc = builtin.link_libc;
8+const linux = std.os.linux;
9+
10+/// A libc-compatible API layer.
11+const system = if (use_libc)
12+ std.c
13+else switch (native_os) {
14+ .linux => linux,
15+ .plan9 => std.os.plan9,
16+ else => struct {
17+ pub const ucontext_t = void;
18+ pub const pid_t = void;
19+ pub const pollfd = void;
20+ pub const fd_t = void;
21+ pub const uid_t = void;
22+ pub const gid_t = void;
23+ },
24+};
25+
26+pub const SIG = system.SIG;
27+const E = system.E;
28+const PATH_MAX = system.PATH_MAX;
29+const pid_t = system.pid_t;
30+const AT = system.AT;
31+const lfs64_abi = native_os == .linux and builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid());
32+const uid_t = system.uid_t;
33+const fd_t = system.fd_t;
34+const mode_t = system.mode_t;
35+const socket_t = fd_t;
36+const SOCK = system.SOCK;
37+const F = system.F;
38+const O = system.O;
39+const AF = system.AF;
40+const FD_CLOEXEC = system.FD_CLOEXEC;
41+const sockaddr = system.sockaddr;
42+pub const socklen_t = system.socklen_t;
43+
44+pub fn getuid() uid_t {
45+ return system.getuid();
46+}
47+
48+/// Get an environment variable.
49+/// See also `getenvZ`.
50+pub fn getenv(key: []const u8) ?[:0]const u8 {
51+ if (mem.indexOfScalar(u8, key, '=') != null) {
52+ return null;
53+ }
54+ if (builtin.link_libc) {
55+ var ptr = std.c.environ;
56+ while (ptr[0]) |line| : (ptr += 1) {
57+ var line_i: usize = 0;
58+ while (line[line_i] != 0) : (line_i += 1) {
59+ if (line_i == key.len) break;
60+ if (line[line_i] != key[line_i]) break;
61+ }
62+ if ((line_i != key.len) or (line[line_i] != '=')) continue;
63+
64+ return mem.sliceTo(line + line_i + 1, 0);
65+ }
66+ return null;
67+ }
68+ // The simplified start logic doesn't populate environ.
69+ if (std.start.simplified_logic) return null;
70+ // TODO see https://github.com/ziglang/zig/issues/4524
71+ for (std.os.environ) |ptr| {
72+ var line_i: usize = 0;
73+ while (ptr[line_i] != 0) : (line_i += 1) {
74+ if (line_i == key.len) break;
75+ if (ptr[line_i] != key[line_i]) break;
76+ }
77+ if ((line_i != key.len) or (ptr[line_i] != '=')) continue;
78+
79+ return mem.sliceTo(ptr + line_i + 1, 0);
80+ }
81+ return null;
82+}
83+
84+const UnexpectedError = error{
85+ /// The Operating System returned an undocumented error code.
86+ ///
87+ /// This error is in theory not possible, but it would be better
88+ /// to handle this error than to invoke undefined behavior.
89+ ///
90+ /// When this error code is observed, it usually means the Zig Standard
91+ /// Library needs a small patch to add the error code to the error set for
92+ /// the respective function.
93+ Unexpected,
94+};
95+
96+const SocketError = error{
97+ /// Permission to create a socket of the specified type and/or
98+ /// pro‐tocol is denied.
99+ AccessDenied,
100+
101+ /// The implementation does not support the specified address family.
102+ AddressFamilyNotSupported,
103+
104+ /// Unknown protocol, or protocol family not available.
105+ ProtocolFamilyNotAvailable,
106+
107+ /// The per-process limit on the number of open file descriptors has been reached.
108+ ProcessFdQuotaExceeded,
109+
110+ /// The system-wide limit on the total number of open files has been reached.
111+ SystemFdQuotaExceeded,
112+
113+ /// Insufficient memory is available. The socket cannot be created until sufficient
114+ /// resources are freed.
115+ SystemResources,
116+
117+ /// The protocol type or the specified protocol is not supported within this domain.
118+ ProtocolNotSupported,
119+
120+ /// The socket type is not supported by the protocol.
121+ SocketTypeNotSupported,
122+} || UnexpectedError;
123+
124+pub fn socket(domain: u32, socket_type: u32, protocol: u32) SocketError!socket_t {
125+ const have_sock_flags = !builtin.target.os.tag.isDarwin() and native_os != .haiku;
126+ const filtered_sock_type = if (!have_sock_flags)
127+ socket_type & ~@as(u32, SOCK.NONBLOCK | SOCK.CLOEXEC)
128+ else
129+ socket_type;
130+ const rc = system.socket(domain, filtered_sock_type, protocol);
131+ switch (errno(rc)) {
132+ .SUCCESS => {
133+ const fd: fd_t = @intCast(rc);
134+ errdefer close(fd);
135+ if (!have_sock_flags) {
136+ try setSockFlags(fd, socket_type);
137+ }
138+ return fd;
139+ },
140+ .ACCES => return error.AccessDenied,
141+ .AFNOSUPPORT => return error.AddressFamilyNotSupported,
142+ .INVAL => return error.ProtocolFamilyNotAvailable,
143+ .MFILE => return error.ProcessFdQuotaExceeded,
144+ .NFILE => return error.SystemFdQuotaExceeded,
145+ .NOBUFS => return error.SystemResources,
146+ .NOMEM => return error.SystemResources,
147+ .PROTONOSUPPORT => return error.ProtocolNotSupported,
148+ .PROTOTYPE => return error.SocketTypeNotSupported,
149+ else => |err| return unexpectedErrno(err),
150+ }
151+}
152+
153+pub fn close(fd: fd_t) void {
154+ return std.Io.Threaded.closeFd(fd);
155+ // switch (errno(system.close(fd))) {
156+ // .BADF => unreachable, // Always a race condition.
157+ // .SUCCESS, .INTR => return, // This is still a success. See https://github.com/ziglang/zig/issues/2425
158+ // else => return,
159+ // }
160+}
161+
162+const ConnectError = error{
163+ /// For UNIX domain sockets, which are identified by pathname: Write permission is denied on the socket
164+ /// file, or search permission is denied for one of the directories in the path prefix.
165+ /// or
166+ /// The user tried to connect to a broadcast address without having the socket broadcast flag enabled or
167+ /// the connection request failed because of a local firewall rule.
168+ AccessDenied,
169+
170+ /// See AccessDenied
171+ PermissionDenied,
172+
173+ /// Local address is already in use.
174+ AddressInUse,
175+
176+ /// (Internet domain sockets) The socket referred to by sockfd had not previously been bound to an
177+ /// address and, upon attempting to bind it to an ephemeral port, it was determined that all port numbers
178+ /// in the ephemeral port range are currently in use. See the discussion of
179+ /// /proc/sys/net/ipv4/ip_local_port_range in ip(7).
180+ AddressNotAvailable,
181+
182+ /// The passed address didn't have the correct address family in its sa_family field.
183+ AddressFamilyNotSupported,
184+
185+ /// Insufficient entries in the routing cache.
186+ SystemResources,
187+
188+ /// A connect() on a stream socket found no one listening on the remote address.
189+ ConnectionRefused,
190+
191+ /// Network is unreachable.
192+ NetworkUnreachable,
193+
194+ /// Timeout while attempting connection. The server may be too busy to accept new connections. Note
195+ /// that for IP sockets the timeout may be very long when syncookies are enabled on the server.
196+ ConnectionTimedOut,
197+
198+ /// This error occurs when no global event loop is configured,
199+ /// and connecting to the socket would block.
200+ WouldBlock,
201+
202+ /// The given path for the unix socket does not exist.
203+ FileNotFound,
204+
205+ /// Connection was reset by peer before connect could complete.
206+ ConnectionResetByPeer,
207+
208+ /// Socket is non-blocking and already has a pending connection in progress.
209+ ConnectionPending,
210+} || UnexpectedError;
211+
212+/// Initiate a connection on a socket.
213+/// If `sockfd` is opened in non blocking mode, the function will
214+/// return error.WouldBlock when EAGAIN or EINPROGRESS is received.
215+pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) ConnectError!void {
216+ while (true) {
217+ switch (errno(system.connect(sock, sock_addr, len))) {
218+ .SUCCESS => return,
219+ .ACCES => return error.AccessDenied,
220+ .PERM => return error.PermissionDenied,
221+ .ADDRINUSE => return error.AddressInUse,
222+ .ADDRNOTAVAIL => return error.AddressNotAvailable,
223+ .AFNOSUPPORT => return error.AddressFamilyNotSupported,
224+ .AGAIN, .INPROGRESS => return error.WouldBlock,
225+ .ALREADY => return error.ConnectionPending,
226+ .BADF => unreachable, // sockfd is not a valid open file descriptor.
227+ .CONNREFUSED => return error.ConnectionRefused,
228+ .CONNRESET => return error.ConnectionResetByPeer,
229+ .FAULT => unreachable, // The socket structure address is outside the user's address space.
230+ .INTR => continue,
231+ .ISCONN => unreachable, // The socket is already connected.
232+ .HOSTUNREACH => return error.NetworkUnreachable,
233+ .NETUNREACH => return error.NetworkUnreachable,
234+ .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket.
235+ .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol.
236+ .TIMEDOUT => return error.ConnectionTimedOut,
237+ .NOENT => return error.FileNotFound, // Returned when socket is AF.UNIX and the given path does not exist.
238+ .CONNABORTED => unreachable, // Tried to reuse socket that previously received error.ConnectionRefused.
239+ else => |err| return unexpectedErrno(err),
240+ }
241+ }
242+}
243+
244+const BindError = error{
245+ /// The address is protected, and the user is not the superuser.
246+ /// For UNIX domain sockets: Search permission is denied on a component
247+ /// of the path prefix.
248+ AccessDenied,
249+
250+ /// The given address is already in use, or in the case of Internet domain sockets,
251+ /// The port number was specified as zero in the socket
252+ /// address structure, but, upon attempting to bind to an ephemeral port, it was
253+ /// determined that all port numbers in the ephemeral port range are currently in
254+ /// use. See the discussion of /proc/sys/net/ipv4/ip_local_port_range ip(7).
255+ AddressInUse,
256+
257+ /// A nonexistent interface was requested or the requested address was not local.
258+ AddressNotAvailable,
259+
260+ /// The address is not valid for the address family of socket.
261+ AddressFamilyNotSupported,
262+
263+ /// Too many symbolic links were encountered in resolving addr.
264+ SymLinkLoop,
265+
266+ /// addr is too long.
267+ NameTooLong,
268+
269+ /// A component in the directory prefix of the socket pathname does not exist.
270+ FileNotFound,
271+
272+ /// Insufficient kernel memory was available.
273+ SystemResources,
274+
275+ /// A component of the path prefix is not a directory.
276+ NotDir,
277+
278+ /// The socket inode would reside on a read-only filesystem.
279+ ReadOnlyFileSystem,
280+
281+ /// The network subsystem has failed.
282+ NetworkSubsystemFailed,
283+
284+ FileDescriptorNotASocket,
285+
286+ AlreadyBound,
287+} || UnexpectedError;
288+
289+/// addr is `*const T` where T is one of the sockaddr
290+pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!void {
291+ const rc = system.bind(sock, addr, len);
292+ switch (errno(rc)) {
293+ .SUCCESS => return,
294+ .ACCES, .PERM => return error.AccessDenied,
295+ .ADDRINUSE => return error.AddressInUse,
296+ .BADF => unreachable, // always a race condition if this error is returned
297+ .INVAL => unreachable, // invalid parameters
298+ .NOTSOCK => unreachable, // invalid `sockfd`
299+ .AFNOSUPPORT => return error.AddressFamilyNotSupported,
300+ .ADDRNOTAVAIL => return error.AddressNotAvailable,
301+ .FAULT => unreachable, // invalid `addr` pointer
302+ .LOOP => return error.SymLinkLoop,
303+ .NAMETOOLONG => return error.NameTooLong,
304+ .NOENT => return error.FileNotFound,
305+ .NOMEM => return error.SystemResources,
306+ .NOTDIR => return error.NotDir,
307+ .ROFS => return error.ReadOnlyFileSystem,
308+ else => |err| return unexpectedErrno(err),
309+ }
310+}
311+
312+const ListenError = error{
313+ /// Another socket is already listening on the same port.
314+ /// For Internet domain sockets, the socket referred to by sockfd had not previously
315+ /// been bound to an address and, upon attempting to bind it to an ephemeral port, it
316+ /// was determined that all port numbers in the ephemeral port range are currently in
317+ /// use. See the discussion of /proc/sys/net/ipv4/ip_local_port_range in ip(7).
318+ AddressInUse,
319+
320+ /// The file descriptor sockfd does not refer to a socket.
321+ FileDescriptorNotASocket,
322+
323+ /// The socket is not of a type that supports the listen() operation.
324+ OperationNotSupported,
325+
326+ /// The network subsystem has failed.
327+ NetworkSubsystemFailed,
328+
329+ /// Ran out of system resources
330+ /// On Windows it can either run out of socket descriptors or buffer space
331+ SystemResources,
332+
333+ /// Already connected
334+ AlreadyConnected,
335+
336+ /// Socket has not been bound yet
337+ SocketNotBound,
338+} || UnexpectedError;
339+
340+pub fn listen(sock: socket_t, backlog: u31) ListenError!void {
341+ const rc = system.listen(sock, backlog);
342+ switch (errno(rc)) {
343+ .SUCCESS => return,
344+ .ADDRINUSE => return error.AddressInUse,
345+ .BADF => unreachable,
346+ .NOTSOCK => return error.FileDescriptorNotASocket,
347+ .OPNOTSUPP => return error.OperationNotSupported,
348+ else => |err| return unexpectedErrno(err),
349+ }
350+}
351+
352+/// Obtains errno from the return value of a system function call.
353+///
354+/// For some systems this will obtain the value directly from the syscall return value;
355+/// for others it will use a thread-local errno variable. Therefore, this
356+/// function only returns a well-defined value when it is called directly after
357+/// the system function call whose errno value is intended to be observed.
358+fn errno(rc: anytype) E {
359+ if (use_libc) {
360+ return if (rc == -1) @enumFromInt(std.c._errno().*) else .SUCCESS;
361+ }
362+ const signed: isize = @bitCast(rc);
363+ const int = if (signed > -4096 and signed < 0) -signed else 0;
364+ return @enumFromInt(int);
365+}
366+
367+fn setSockFlags(sock: socket_t, flags: u32) !void {
368+ if ((flags & SOCK.CLOEXEC) != 0) {
369+ var fd_flags = fcntl(sock, F.GETFD, 0) catch |err| switch (err) {
370+ error.FileBusy => unreachable,
371+ error.Locked => unreachable,
372+ error.PermissionDenied => unreachable,
373+ error.DeadLock => unreachable,
374+ error.LockedRegionLimitExceeded => unreachable,
375+ else => |e| return e,
376+ };
377+ fd_flags |= FD_CLOEXEC;
378+ _ = fcntl(sock, F.SETFD, fd_flags) catch |err| switch (err) {
379+ error.FileBusy => unreachable,
380+ error.Locked => unreachable,
381+ error.PermissionDenied => unreachable,
382+ error.DeadLock => unreachable,
383+ error.LockedRegionLimitExceeded => unreachable,
384+ else => |e| return e,
385+ };
386+ }
387+ if ((flags & SOCK.NONBLOCK) != 0) {
388+ var fl_flags = fcntl(sock, F.GETFL, 0) catch |err| switch (err) {
389+ error.FileBusy => unreachable,
390+ error.Locked => unreachable,
391+ error.PermissionDenied => unreachable,
392+ error.DeadLock => unreachable,
393+ error.LockedRegionLimitExceeded => unreachable,
394+ else => |e| return e,
395+ };
396+ fl_flags |= 1 << @bitOffsetOf(O, "NONBLOCK");
397+ _ = fcntl(sock, F.SETFL, fl_flags) catch |err| switch (err) {
398+ error.FileBusy => unreachable,
399+ error.Locked => unreachable,
400+ error.PermissionDenied => unreachable,
401+ error.DeadLock => unreachable,
402+ error.LockedRegionLimitExceeded => unreachable,
403+ else => |e| return e,
404+ };
405+ }
406+}
407+
408+const FcntlError = error{
409+ PermissionDenied,
410+ FileBusy,
411+ ProcessFdQuotaExceeded,
412+ Locked,
413+ DeadLock,
414+ LockedRegionLimitExceeded,
415+} || UnexpectedError;
416+
417+pub fn fcntl(fd: fd_t, cmd: i32, arg: usize) FcntlError!usize {
418+ while (true) {
419+ const rc = system.fcntl(fd, cmd, arg);
420+ switch (errno(rc)) {
421+ .SUCCESS => return @intCast(rc),
422+ .INTR => continue,
423+ .AGAIN, .ACCES => return error.Locked,
424+ .BADF => unreachable,
425+ .BUSY => return error.FileBusy,
426+ .INVAL => unreachable, // invalid parameters
427+ .PERM => return error.PermissionDenied,
428+ .MFILE => return error.ProcessFdQuotaExceeded,
429+ .NOTDIR => unreachable, // invalid parameter
430+ .DEADLK => return error.DeadLock,
431+ .NOLCK => return error.LockedRegionLimitExceeded,
432+ else => |err| return unexpectedErrno(err),
433+ }
434+ }
435+}
436+
437+const WriteError = error{
438+ DiskQuota,
439+ FileTooBig,
440+ InputOutput,
441+ NoSpaceLeft,
442+ DeviceBusy,
443+ InvalidArgument,
444+
445+ /// File descriptor does not hold the required rights to write to it.
446+ AccessDenied,
447+ PermissionDenied,
448+ BrokenPipe,
449+ SystemResources,
450+ OperationAborted,
451+ NotOpenForWriting,
452+
453+ /// The process cannot access the file because another process has locked
454+ /// a portion of the file. Windows-only.
455+ LockViolation,
456+
457+ /// This error occurs when no global event loop is configured,
458+ /// and reading from the file descriptor would block.
459+ WouldBlock,
460+
461+ /// Connection reset by peer.
462+ ConnectionResetByPeer,
463+
464+ /// This error occurs in Linux if the process being written to
465+ /// no longer exists.
466+ ProcessNotFound,
467+ /// This error occurs when a device gets disconnected before or mid-flush
468+ /// while it's being written to - errno(6): No such device or address.
469+ NoDevice,
470+
471+ /// The socket type requires that message be sent atomically, and the size of the message
472+ /// to be sent made this impossible. The message is not transmitted.
473+ MessageTooBig,
474+} || UnexpectedError;
475+
476+/// Write to a file descriptor.
477+/// Retries when interrupted by a signal.
478+/// Returns the number of bytes written. If nonzero bytes were supplied, this will be nonzero.
479+///
480+/// Note that a successful write() may transfer fewer than count bytes. Such partial writes can
481+/// occur for various reasons; for example, because there was insufficient space on the disk
482+/// device to write all of the requested bytes, or because a blocked write() to a socket, pipe, or
483+/// similar was interrupted by a signal handler after it had transferred some, but before it had
484+/// transferred all of the requested bytes. In the event of a partial write, the caller can make
485+/// another write() call to transfer the remaining bytes. The subsequent call will either
486+/// transfer further bytes or may result in an error (e.g., if the disk is now full).
487+///
488+/// For POSIX systems, if `fd` is opened in non blocking mode, the function will
489+/// return error.WouldBlock when EAGAIN is received.
490+/// On Windows, if the application has a global event loop enabled, I/O Completion Ports are
491+/// used to perform the I/O. `error.WouldBlock` is not possible on Windows.
492+///
493+/// Linux has a limit on how many bytes may be transferred in one `write` call, which is `0x7ffff000`
494+/// on both 64-bit and 32-bit systems. This is due to using a signed C int as the return value, as
495+/// well as stuffing the errno codes into the last `4096` values. This is noted on the `write` man page.
496+/// The limit on Darwin is `0x7fffffff`, trying to read more than that returns EINVAL.
497+/// The corresponding POSIX limit is `maxInt(isize)`.
498+pub fn write(fd: fd_t, bytes: []const u8) WriteError!usize {
499+ if (bytes.len == 0) return 0;
500+ const max_count = switch (native_os) {
501+ .linux => 0x7ffff000,
502+ .macos, .ios, .watchos, .tvos, .visionos => maxInt(i32),
503+ else => maxInt(isize),
504+ };
505+ while (true) {
506+ const rc = system.write(fd, bytes.ptr, @min(bytes.len, max_count));
507+ switch (errno(rc)) {
508+ .SUCCESS => return @intCast(rc),
509+ .INTR => continue,
510+ .INVAL => return error.InvalidArgument,
511+ .FAULT => unreachable,
512+ .SRCH => return error.ProcessNotFound,
513+ .AGAIN => return error.WouldBlock,
514+ .BADF => return error.NotOpenForWriting, // can be a race condition.
515+ .DESTADDRREQ => unreachable, // `connect` was never called.
516+ .DQUOT => return error.DiskQuota,
517+ .FBIG => return error.FileTooBig,
518+ .IO => return error.InputOutput,
519+ .NOSPC => return error.NoSpaceLeft,
520+ .ACCES => return error.AccessDenied,
521+ .PERM => return error.PermissionDenied,
522+ .PIPE => return error.BrokenPipe,
523+ .CONNRESET => return error.ConnectionResetByPeer,
524+ .BUSY => return error.DeviceBusy,
525+ .NXIO => return error.NoDevice,
526+ .MSGSIZE => return error.MessageTooBig,
527+ else => |err| return unexpectedErrno(err),
528+ }
529+ }
530+}
531+
532+pub const ForkError = error{SystemResources} || UnexpectedError;
533+
534+pub fn fork() ForkError!pid_t {
535+ const rc = system.fork();
536+ switch (errno(rc)) {
537+ .SUCCESS => return @intCast(rc),
538+ .AGAIN => return error.SystemResources,
539+ .NOMEM => return error.SystemResources,
540+ else => |err| return unexpectedErrno(err),
541+ }
542+}
543+
544+const SetSidError = error{
545+ /// The calling process is already a process group leader, or the process group ID of a process other than the calling process matches the process ID of the calling process.
546+ PermissionDenied,
547+} || UnexpectedError;
548+
549+pub fn setsid() SetSidError!pid_t {
550+ const rc = system.setsid();
551+ switch (errno(rc)) {
552+ .SUCCESS => return rc,
553+ .PERM => return error.PermissionDenied,
554+ else => |err| return unexpectedErrno(err),
555+ }
556+}
557+
558+/// Open and possibly create a file. Keeps trying if it gets interrupted.
559+/// On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
560+/// On WASI, `file_path` should be encoded as valid UTF-8.
561+/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
562+/// See also `open`.
563+fn openZ(file_path: [*:0]const u8, flags: O, perm: mode_t) OpenError!fd_t {
564+ const open_sym = if (lfs64_abi) system.open64 else system.open;
565+ while (true) {
566+ const rc = open_sym(file_path, flags, perm);
567+ switch (errno(rc)) {
568+ .SUCCESS => return @intCast(rc),
569+ .INTR => continue,
570+
571+ .FAULT => unreachable,
572+ .INVAL => return error.BadPathName,
573+ .ACCES => return error.AccessDenied,
574+ .FBIG => return error.FileTooBig,
575+ .OVERFLOW => return error.FileTooBig,
576+ .ISDIR => return error.IsDir,
577+ .LOOP => return error.SymLinkLoop,
578+ .MFILE => return error.ProcessFdQuotaExceeded,
579+ .NAMETOOLONG => return error.NameTooLong,
580+ .NFILE => return error.SystemFdQuotaExceeded,
581+ .NODEV => return error.NoDevice,
582+ .NOENT => return error.FileNotFound,
583+ .SRCH => return error.ProcessNotFound,
584+ .NOMEM => return error.SystemResources,
585+ .NOSPC => return error.NoSpaceLeft,
586+ .NOTDIR => return error.NotDir,
587+ .PERM => return error.PermissionDenied,
588+ .EXIST => return error.PathAlreadyExists,
589+ .BUSY => return error.DeviceBusy,
590+ .ILSEQ => |err| if (native_os == .wasi)
591+ return error.InvalidUtf8
592+ else
593+ return unexpectedErrno(err),
594+ else => |err| return unexpectedErrno(err),
595+ }
596+ }
597+}
598+
599+/// Open and possibly create a file. Keeps trying if it gets interrupted.
600+/// `file_path` is relative to the open directory handle `dir_fd`.
601+/// On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
602+/// On WASI, `file_path` should be encoded as valid UTF-8.
603+/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
604+/// See also `openat`.
605+fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: O, mode: mode_t) OpenError!fd_t {
606+ const openat_sym = if (lfs64_abi) system.openat64 else system.openat;
607+ while (true) {
608+ const rc = openat_sym(dir_fd, file_path, flags, mode);
609+ switch (errno(rc)) {
610+ .SUCCESS => return @intCast(rc),
611+ .INTR => continue,
612+
613+ .FAULT => unreachable,
614+ .INVAL => return error.BadPathName,
615+ .BADF => unreachable,
616+ .ACCES => return error.AccessDenied,
617+ .FBIG => return error.FileTooBig,
618+ .OVERFLOW => return error.FileTooBig,
619+ .ISDIR => return error.IsDir,
620+ .LOOP => return error.SymLinkLoop,
621+ .MFILE => return error.ProcessFdQuotaExceeded,
622+ .NAMETOOLONG => return error.NameTooLong,
623+ .NFILE => return error.SystemFdQuotaExceeded,
624+ .NODEV => return error.NoDevice,
625+ .NOENT => return error.FileNotFound,
626+ .SRCH => return error.ProcessNotFound,
627+ .NOMEM => return error.SystemResources,
628+ .NOSPC => return error.NoSpaceLeft,
629+ .NOTDIR => return error.NotDir,
630+ .PERM => return error.PermissionDenied,
631+ .EXIST => return error.PathAlreadyExists,
632+ .BUSY => return error.DeviceBusy,
633+ .OPNOTSUPP => return error.FileLocksNotSupported,
634+ .AGAIN => return error.WouldBlock,
635+ .TXTBSY => return error.FileBusy,
636+ .NXIO => return error.NoDevice,
637+ .ILSEQ => |err| if (native_os == .wasi)
638+ return error.InvalidUtf8
639+ else
640+ return unexpectedErrno(err),
641+ else => |err| return unexpectedErrno(err),
642+ }
643+ }
644+}
645+
646+/// Open and possibly create a file. Keeps trying if it gets interrupted.
647+/// `file_path` is relative to the open directory handle `dir_fd`.
648+/// On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
649+/// On WASI, `file_path` should be encoded as valid UTF-8.
650+/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
651+/// See also `openatZ`.
652+fn openat(dir_fd: fd_t, file_path: []const u8, flags: O, mode: mode_t) OpenError!fd_t {
653+ const file_path_c = try toPosixPath(file_path);
654+ return openatZ(dir_fd, &file_path_c, flags, mode);
655+}
656+
657+const OpenError = error{
658+ /// In WASI, this error may occur when the file descriptor does
659+ /// not hold the required rights to open a new resource relative to it.
660+ AccessDenied,
661+ PermissionDenied,
662+ SymLinkLoop,
663+ ProcessFdQuotaExceeded,
664+ SystemFdQuotaExceeded,
665+ NoDevice,
666+ /// Either:
667+ /// * One of the path components does not exist.
668+ /// * Cwd was used, but cwd has been deleted.
669+ /// * The path associated with the open directory handle has been deleted.
670+ /// * On macOS, multiple processes or threads raced to create the same file
671+ /// with `O.EXCL` set to `false`.
672+ FileNotFound,
673+
674+ /// The path exceeded `max_path_bytes` bytes.
675+ NameTooLong,
676+
677+ /// Insufficient kernel memory was available, or
678+ /// the named file is a FIFO and per-user hard limit on
679+ /// memory allocation for pipes has been reached.
680+ SystemResources,
681+
682+ /// The file is too large to be opened. This error is unreachable
683+ /// for 64-bit targets, as well as when opening directories.
684+ FileTooBig,
685+
686+ /// The path refers to directory but the `DIRECTORY` flag was not provided.
687+ IsDir,
688+
689+ /// A new path cannot be created because the device has no room for the new file.
690+ /// This error is only reachable when the `CREAT` flag is provided.
691+ NoSpaceLeft,
692+
693+ /// A component used as a directory in the path was not, in fact, a directory, or
694+ /// `DIRECTORY` was specified and the path was not a directory.
695+ NotDir,
696+
697+ /// The path already exists and the `CREAT` and `EXCL` flags were provided.
698+ PathAlreadyExists,
699+ DeviceBusy,
700+
701+ /// The underlying filesystem does not support file locks
702+ FileLocksNotSupported,
703+
704+ /// Path contains characters that are disallowed by the underlying filesystem.
705+ BadPathName,
706+
707+ /// WASI-only; file paths must be valid UTF-8.
708+ InvalidUtf8,
709+
710+ /// Windows-only; file paths provided by the user must be valid WTF-8.
711+ /// https://simonsapin.github.io/wtf-8/
712+ InvalidWtf8,
713+
714+ /// On Windows, `\\server` or `\\server\share` was not found.
715+ NetworkNotFound,
716+
717+ /// This error occurs in Linux if the process to be open was not found.
718+ ProcessNotFound,
719+
720+ /// One of these three things:
721+ /// * pathname refers to an executable image which is currently being
722+ /// executed and write access was requested.
723+ /// * pathname refers to a file that is currently in use as a swap
724+ /// file, and the O_TRUNC flag was specified.
725+ /// * pathname refers to a file that is currently being read by the
726+ /// kernel (e.g., for module/firmware loading), and write access was
727+ /// requested.
728+ FileBusy,
729+
730+ WouldBlock,
731+} || UnexpectedError;
732+
733+/// Open and possibly create a file. Keeps trying if it gets interrupted.
734+/// On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
735+/// On WASI, `file_path` should be encoded as valid UTF-8.
736+/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
737+/// See also `openZ`.
738+pub fn open(file_path: []const u8, flags: O, perm: mode_t) OpenError!fd_t {
739+ const file_path_c = try toPosixPath(file_path);
740+ return openZ(&file_path_c, flags, perm);
741+}
742+
743+pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void {
744+ while (true) {
745+ switch (errno(system.dup2(old_fd, new_fd))) {
746+ .SUCCESS => return,
747+ .BUSY, .INTR => continue,
748+ .MFILE => return error.ProcessFdQuotaExceeded,
749+ .INVAL => unreachable, // invalid parameters passed to dup2
750+ .BADF => unreachable, // invalid file descriptor
751+ else => |err| return unexpectedErrno(err),
752+ }
753+ }
754+}
755+
756+/// This function ignores PATH environment variable. See `execvpeZ` for that.
757+fn execveZ(
758+ path: [*:0]const u8,
759+ child_argv: [*:null]const ?[*:0]const u8,
760+ envp: [*:null]const ?[*:0]const u8,
761+) ExecveError {
762+ switch (errno(system.execve(path, child_argv, envp))) {
763+ .SUCCESS => unreachable,
764+ .FAULT => unreachable,
765+ .@"2BIG" => return error.SystemResources,
766+ .MFILE => return error.ProcessFdQuotaExceeded,
767+ .NAMETOOLONG => return error.NameTooLong,
768+ .NFILE => return error.SystemFdQuotaExceeded,
769+ .NOMEM => return error.SystemResources,
770+ .ACCES => return error.AccessDenied,
771+ .PERM => return error.PermissionDenied,
772+ .INVAL => return error.InvalidExe,
773+ .NOEXEC => return error.InvalidExe,
774+ .IO => return error.FileSystem,
775+ .LOOP => return error.FileSystem,
776+ .ISDIR => return error.IsDir,
777+ .NOENT => return error.FileNotFound,
778+ .NOTDIR => return error.NotDir,
779+ .TXTBSY => return error.FileBusy,
780+ else => |err| switch (native_os) {
781+ .macos, .ios, .tvos, .watchos, .visionos => switch (err) {
782+ .BADEXEC => return error.InvalidExe,
783+ .BADARCH => return error.InvalidExe,
784+ else => return unexpectedErrno(err),
785+ },
786+ .linux => switch (err) {
787+ .LIBBAD => return error.InvalidExe,
788+ else => return unexpectedErrno(err),
789+ },
790+ else => return unexpectedErrno(err),
791+ },
792+ }
793+}
794+
795+/// Get an environment variable with a null-terminated name.
796+/// See also `getenv`.
797+fn getenvZ(key: [*:0]const u8) ?[:0]const u8 {
798+ if (builtin.link_libc) {
799+ const value = system.getenv(key) orelse return null;
800+ return mem.sliceTo(value, 0);
801+ }
802+ return getenv(mem.sliceTo(key, 0));
803+}
804+
805+const Arg0Expand = enum {
806+ expand,
807+ no_expand,
808+};
809+
810+/// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable,
811+/// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall.
812+/// If this function returns with an error, `argv[0]` will be restored to the value it was when it was passed in.
813+fn execvpeZ_expandArg0(
814+ comptime arg0_expand: Arg0Expand,
815+ file: [*:0]const u8,
816+ child_argv: switch (arg0_expand) {
817+ .expand => [*:null]?[*:0]const u8,
818+ .no_expand => [*:null]const ?[*:0]const u8,
819+ },
820+ envp: [*:null]const ?[*:0]const u8,
821+) ExecveError {
822+ const file_slice = mem.sliceTo(file, 0);
823+ if (mem.indexOfScalar(u8, file_slice, '/') != null) return execveZ(file, child_argv, envp);
824+
825+ const PATH = getenvZ("PATH") orelse "/usr/local/bin:/bin/:/usr/bin";
826+ // Use of PATH_MAX here is valid as the path_buf will be passed
827+ // directly to the operating system in execveZ.
828+ var path_buf: [PATH_MAX]u8 = undefined;
829+ var it = mem.tokenizeScalar(u8, PATH, ':');
830+ var seen_eacces = false;
831+ var err: ExecveError = error.FileNotFound;
832+
833+ // In case of expanding arg0 we must put it back if we return with an error.
834+ const prev_arg0 = child_argv[0];
835+ defer switch (arg0_expand) {
836+ .expand => child_argv[0] = prev_arg0,
837+ .no_expand => {},
838+ };
839+
840+ while (it.next()) |search_path| {
841+ const path_len = search_path.len + file_slice.len + 1;
842+ if (path_buf.len < path_len + 1) return error.NameTooLong;
843+ @memcpy(path_buf[0..search_path.len], search_path);
844+ path_buf[search_path.len] = '/';
845+ @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice);
846+ path_buf[path_len] = 0;
847+ const full_path = path_buf[0..path_len :0].ptr;
848+ switch (arg0_expand) {
849+ .expand => child_argv[0] = full_path,
850+ .no_expand => {},
851+ }
852+ err = execveZ(full_path, child_argv, envp);
853+ switch (err) {
854+ error.AccessDenied => seen_eacces = true,
855+ error.FileNotFound, error.NotDir => {},
856+ else => |e| return e,
857+ }
858+ }
859+ if (seen_eacces) return error.AccessDenied;
860+ return err;
861+}
862+
863+const ExecveError = error{
864+ SystemResources,
865+ AccessDenied,
866+ PermissionDenied,
867+ InvalidExe,
868+ FileSystem,
869+ IsDir,
870+ FileNotFound,
871+ NotDir,
872+ FileBusy,
873+ ProcessFdQuotaExceeded,
874+ SystemFdQuotaExceeded,
875+ NameTooLong,
876+} || UnexpectedError;
877+
878+/// This function also uses the PATH environment variable to get the full path to the executable.
879+/// If `file` is an absolute path, this is the same as `execveZ`.
880+pub fn execvpeZ(
881+ file: [*:0]const u8,
882+ argv_ptr: [*:null]const ?[*:0]const u8,
883+ envp: [*:null]const ?[*:0]const u8,
884+) ExecveError {
885+ return execvpeZ_expandArg0(.no_expand, file, argv_ptr, envp);
886+}
887+
888+/// Exits all threads of the program with the specified status code.
889+pub fn exit(status: u8) noreturn {
890+ if (builtin.link_libc) {
891+ std.c.exit(status);
892+ }
893+ if (native_os == .linux and !builtin.single_threaded) {
894+ linux.exit_group(status);
895+ }
896+ if (native_os == .uefi) {
897+ const uefi = std.os.uefi;
898+ // exit() is only available if exitBootServices() has not been called yet.
899+ // This call to exit should not fail, so we catch-ignore errors.
900+ if (uefi.system_table.boot_services) |bs| {
901+ bs.exit(uefi.handle, @enumFromInt(status), null) catch {};
902+ }
903+ // If we can't exit, reboot the system instead.
904+ uefi.system_table.runtime_services.resetSystem(.cold, @enumFromInt(status), null);
905+ }
906+ system.exit(status);
907+}
908+
909+/// Creates a unidirectional data channel that can be used for interprocess communication.
910+fn pipe() PipeError![2]fd_t {
911+ var fds: [2]fd_t = undefined;
912+ switch (errno(system.pipe(&fds))) {
913+ .SUCCESS => return fds,
914+ .INVAL => unreachable, // Invalid parameters to pipe()
915+ .FAULT => unreachable, // Invalid fds pointer
916+ .NFILE => return error.SystemFdQuotaExceeded,
917+ .MFILE => return error.ProcessFdQuotaExceeded,
918+ else => |err| return unexpectedErrno(err),
919+ }
920+}
921+
922+const PipeError = error{
923+ SystemFdQuotaExceeded,
924+ ProcessFdQuotaExceeded,
925+} || UnexpectedError;
926+
927+pub fn pipe2(flags: O) PipeError![2]fd_t {
928+ if (@TypeOf(system.pipe2) != void) {
929+ var fds: [2]fd_t = undefined;
930+ switch (errno(system.pipe2(&fds, flags))) {
931+ .SUCCESS => return fds,
932+ .INVAL => unreachable, // Invalid flags
933+ .FAULT => unreachable, // Invalid fds pointer
934+ .NFILE => return error.SystemFdQuotaExceeded,
935+ .MFILE => return error.ProcessFdQuotaExceeded,
936+ else => |err| return unexpectedErrno(err),
937+ }
938+ }
939+
940+ const fds: [2]fd_t = try pipe();
941+ errdefer {
942+ close(fds[0]);
943+ close(fds[1]);
944+ }
945+
946+ // https://github.com/ziglang/zig/issues/18882
947+ if (@as(u32, @bitCast(flags)) == 0)
948+ return fds;
949+
950+ // CLOEXEC is special, it's a file descriptor flag and must be set using
951+ // F.SETFD.
952+ if (flags.CLOEXEC) {
953+ for (fds) |fd| {
954+ switch (errno(system.fcntl(fd, F.SETFD, @as(u32, FD_CLOEXEC)))) {
955+ .SUCCESS => {},
956+ .INVAL => unreachable, // Invalid flags
957+ .BADF => unreachable, // Always a race condition
958+ else => |err| return unexpectedErrno(err),
959+ }
960+ }
961+ }
962+
963+ const new_flags: u32 = f: {
964+ var new_flags = flags;
965+ new_flags.CLOEXEC = false;
966+ break :f @bitCast(new_flags);
967+ };
968+ // Set every other flag affecting the file status using F.SETFL.
969+ if (new_flags != 0) {
970+ for (fds) |fd| {
971+ switch (errno(system.fcntl(fd, F.SETFL, new_flags))) {
972+ .SUCCESS => {},
973+ .INVAL => unreachable, // Invalid flags
974+ .BADF => unreachable, // Always a race condition
975+ else => |err| return unexpectedErrno(err),
976+ }
977+ }
978+ }
979+
980+ return fds;
981+}
982+
983+const AcceptError = error{
984+ ConnectionAborted,
985+
986+ /// The file descriptor sockfd does not refer to a socket.
987+ FileDescriptorNotASocket,
988+
989+ /// The per-process limit on the number of open file descriptors has been reached.
990+ ProcessFdQuotaExceeded,
991+
992+ /// The system-wide limit on the total number of open files has been reached.
993+ SystemFdQuotaExceeded,
994+
995+ /// Not enough free memory. This often means that the memory allocation is limited
996+ /// by the socket buffer limits, not by the system memory.
997+ SystemResources,
998+
999+ /// Socket is not listening for new connections.
1000+ SocketNotListening,
1001+
1002+ ProtocolFailure,
1003+
1004+ /// Firewall rules forbid connection.
1005+ BlockedByFirewall,
1006+
1007+ /// This error occurs when no global event loop is configured,
1008+ /// and accepting from the socket would block.
1009+ WouldBlock,
1010+
1011+ /// An incoming connection was indicated, but was subsequently terminated by the
1012+ /// remote peer prior to accepting the call.
1013+ ConnectionResetByPeer,
1014+
1015+ /// The network subsystem has failed.
1016+ NetworkSubsystemFailed,
1017+
1018+ /// The referenced socket is not a type that supports connection-oriented service.
1019+ OperationNotSupported,
1020+} || UnexpectedError;
1021+
1022+/// Accept a connection on a socket.
1023+/// If `sockfd` is opened in non blocking mode, the function will
1024+/// return error.WouldBlock when EAGAIN is received.
1025+pub fn accept(
1026+ /// This argument is a socket that has been created with `socket`, bound to a local address
1027+ /// with `bind`, and is listening for connections after a `listen`.
1028+ sock: socket_t,
1029+ /// This argument is a pointer to a sockaddr structure. This structure is filled in with the
1030+ /// address of the peer socket, as known to the communications layer. The exact format of the
1031+ /// address returned addr is determined by the socket's address family (see `socket` and the
1032+ /// respective protocol man pages).
1033+ addr: ?*sockaddr,
1034+ /// This argument is a value-result argument: the caller must initialize it to contain the
1035+ /// size (in bytes) of the structure pointed to by addr; on return it will contain the actual size
1036+ /// of the peer address.
1037+ ///
1038+ /// The returned address is truncated if the buffer provided is too small; in this case, `addr_size`
1039+ /// will return a value greater than was supplied to the call.
1040+ addr_size: ?*socklen_t,
1041+ /// The following values can be bitwise ORed in flags to obtain different behavior:
1042+ /// * `SOCK.NONBLOCK` - Set the `NONBLOCK` file status flag on the open file description (see `open`)
1043+ /// referred to by the new file descriptor. Using this flag saves extra calls to `fcntl` to achieve
1044+ /// the same result.
1045+ /// * `SOCK.CLOEXEC` - Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor. See the
1046+ /// description of the `CLOEXEC` flag in `open` for reasons why this may be useful.
1047+ flags: u32,
1048+) AcceptError!socket_t {
1049+ const have_accept4 = !builtin.target.os.tag.isDarwin();
1050+ assert(0 == (flags & ~@as(u32, SOCK.NONBLOCK | SOCK.CLOEXEC))); // Unsupported flag(s)
1051+
1052+ const accepted_sock: socket_t = while (true) {
1053+ const rc = if (have_accept4)
1054+ system.accept4(sock, addr, addr_size, flags)
1055+ else
1056+ system.accept(sock, addr, addr_size);
1057+
1058+ switch (errno(rc)) {
1059+ .SUCCESS => break @intCast(rc),
1060+ .INTR => continue,
1061+ .AGAIN => return error.WouldBlock,
1062+ .BADF => unreachable, // always a race condition
1063+ .CONNABORTED => return error.ConnectionAborted,
1064+ .FAULT => unreachable,
1065+ .INVAL => return error.SocketNotListening,
1066+ .NOTSOCK => unreachable,
1067+ .MFILE => return error.ProcessFdQuotaExceeded,
1068+ .NFILE => return error.SystemFdQuotaExceeded,
1069+ .NOBUFS => return error.SystemResources,
1070+ .NOMEM => return error.SystemResources,
1071+ .OPNOTSUPP => unreachable,
1072+ .PROTO => return error.ProtocolFailure,
1073+ .PERM => return error.BlockedByFirewall,
1074+ else => |err| return unexpectedErrno(err),
1075+ }
1076+ };
1077+
1078+ errdefer close(accepted_sock);
1079+ if (!have_accept4) {
1080+ try setSockFlags(accepted_sock, flags);
1081+ }
1082+ return accepted_sock;
1083+}
1084+
1085+const WaitPidResult = struct {
1086+ pid: pid_t,
1087+ status: u32,
1088+};
1089+
1090+/// Use this version of the `waitpid` wrapper if you spawned your child process using explicit
1091+/// `fork` and `execve` method.
1092+pub fn waitpid(pid: pid_t, flags: u32) WaitPidResult {
1093+ var status: if (builtin.link_libc) c_int else u32 = undefined;
1094+ while (true) {
1095+ const rc = system.waitpid(pid, &status, @intCast(flags));
1096+ switch (errno(rc)) {
1097+ .SUCCESS => return .{
1098+ .pid = @intCast(rc),
1099+ .status = @bitCast(status),
1100+ },
1101+ .INTR => continue,
1102+ .CHILD => unreachable, // The process specified does not exist. It would be a race condition to handle this error.
1103+ .INVAL => unreachable, // Invalid flags.
1104+ else => unreachable,
1105+ }
1106+ }
1107+}
1108+
1109+/// Call this when you made a syscall or something that sets errno
1110+/// and you get an unexpected error.
1111+fn unexpectedErrno(err: E) UnexpectedError {
1112+ if (unexpected_error_tracing) {
1113+ std.debug.print("unexpected errno: {d}\n", .{@intFromEnum(err)});
1114+ std.debug.dumpCurrentStackTrace(std.debug.StackUnwindOptions{});
1115+ }
1116+ return error.Unexpected;
1117+}
1118+
1119+/// Whether or not `error.Unexpected` will print its value and a stack trace.
1120+///
1121+/// If this happens the fix is to add the error code to the corresponding
1122+/// switch expression, possibly introduce a new error in the error set, and
1123+/// send a patch to Zig.
1124+const unexpected_error_tracing = builtin.mode == .Debug and switch (builtin.zig_backend) {
1125+ .stage2_llvm, .stage2_x86_64 => true,
1126+ else => false,
1127+};
1128+
1129+/// Used to convert a slice to a null terminated slice on the stack.
1130+fn toPosixPath(file_path: []const u8) error{NameTooLong}![PATH_MAX - 1:0]u8 {
1131+ if (std.debug.runtime_safety) assert(mem.indexOfScalar(u8, file_path, 0) == null);
1132+ var path_with_null: [PATH_MAX - 1:0]u8 = undefined;
1133+ // >= rather than > to make room for the null byte
1134+ if (file_path.len >= PATH_MAX) return error.NameTooLong;
1135+ @memcpy(path_with_null[0..file_path.len], file_path);
1136+ path_with_null[file_path.len] = 0;
1137+ return path_with_null;
1138+}
1139+
1140+const Address = extern union {
1141+ any: sockaddr,
1142+ un: sockaddr.un,
1143+
1144+ pub fn getOsSockLen(_: Address) socklen_t {
1145+ // Using the full length of the structure here is more portable than returning
1146+ // the number of bytes actually used by the currently stored path.
1147+ // This also is correct regardless if we are passing a socket address to the kernel
1148+ // (e.g. in bind, connect, sendto) since we ensure the path is 0 terminated in
1149+ // initUnix() or if we are receiving a socket address from the kernel and must
1150+ // provide the full buffer size (e.g. getsockname, getpeername, recvfrom, accept).
1151+ //
1152+ // To access the path, std.mem.sliceTo(&address.un.path, 0) should be used.
1153+ return @as(socklen_t, @intCast(@sizeOf(sockaddr.un)));
1154+ }
1155+};
1156+
1157+pub fn initUnix(path: []const u8) !Address {
1158+ var sock_addr = sockaddr.un{
1159+ .family = AF.UNIX,
1160+ .path = undefined,
1161+ };
1162+
1163+ // Add 1 to ensure a terminating 0 is present in the path array for maximum portability.
1164+ if (path.len + 1 > sock_addr.path.len) return error.NameTooLong;
1165+
1166+ @memset(&sock_addr.path, 0);
1167+ @memcpy(sock_addr.path[0..path.len], path);
1168+
1169+ return Address{ .un = sock_addr };
1170+}
+24, -24
......@@ -1,12 +1,13 @@
11 const std = @import("std");
22 const posix = std.posix;
3+const lib_posix = @import("posix.zig");
34
45 pub fn getSeshPrefix() []const u8 {
5- return std.posix.getenv("ZMX_SESSION_PREFIX") orelse "";
6+ return lib_posix.getenv("ZMX_SESSION_PREFIX") orelse "";
67 }
78
89 pub fn getSeshNameFromEnv() []const u8 {
9- return std.posix.getenv("ZMX_SESSION") orelse "";
10+ return lib_posix.getenv("ZMX_SESSION") orelse "";
1011 }
1112
1213 pub fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 {
......@@ -29,48 +30,47 @@ pub fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 {
2930 }
3031
3132 pub fn sessionConnect(sesh: []const u8) !i32 {
32- var unix_addr = try std.net.Address.initUnix(sesh);
33- const socket_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0);
34- errdefer posix.close(socket_fd);
35- try posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen());
33+ var unix_addr = try lib_posix.initUnix(sesh);
34+ const socket_fd = try lib_posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0);
35+ errdefer lib_posix.close(socket_fd);
36+ try lib_posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen());
3637 return socket_fd;
3738 }
3839
39-pub fn cleanupStaleSocket(dir: std.fs.Dir, session_name: []const u8) void {
40+pub fn cleanupStaleSocket(io: std.Io, dir: std.Io.Dir, session_name: []const u8) void {
4041 std.log.warn("stale socket found, cleaning up session={s}", .{session_name});
41- dir.deleteFile(session_name) catch |err| {
42+ dir.deleteFile(io, session_name) catch |err| {
4243 std.log.warn("failed to delete stale socket err={s}", .{@errorName(err)});
4344 };
4445 }
4546
46-pub fn sessionExists(dir: std.fs.Dir, name: []const u8) !bool {
47- // fstatatZ (not statFile) to avoid the statx syscall
48- // https://github.com/neurosnap/zmx/issues/186
49- const name_c = try posix.toPosixPath(name);
50- const stat = posix.fstatatZ(dir.fd, &name_c, posix.AT.SYMLINK_NOFOLLOW) catch |err| switch (err) {
51- error.FileNotFound => return false,
52- else => return err,
47+pub fn sessionExists(io: std.Io, dir: std.Io.Dir, name: []const u8) !bool {
48+ const stat = dir.statFile(io, name, std.Io.Dir.StatFileOptions{}) catch |err| {
49+ switch (err) {
50+ error.FileNotFound => return false,
51+ else => return err,
52+ }
5353 };
54- if (!posix.S.ISSOCK(stat.mode)) {
54+ if (stat.kind != .unix_domain_socket) {
5555 return error.FileNotUnixSocket;
5656 }
5757 return true;
5858 }
5959
60-pub fn createSocket(fname: []const u8) !i32 {
60+pub fn createSocket(sesh: []const u8) !i32 {
6161 // AF.UNIX: Unix domain socket for local IPC with client processes
6262 // SOCK.STREAM: Reliable, bidirectional communication
6363 // SOCK.NONBLOCK: Set socket to non-blocking
64- const fd = try posix.socket(
64+ const fd = try lib_posix.socket(
6565 posix.AF.UNIX,
6666 posix.SOCK.STREAM | posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC,
6767 0,
6868 );
69- errdefer posix.close(fd);
69+ errdefer lib_posix.close(fd);
7070
71- var unix_addr = try std.net.Address.initUnix(fname);
72- try posix.bind(fd, &unix_addr.any, unix_addr.getOsSockLen());
73- try posix.listen(fd, 128);
71+ var unix_addr = try lib_posix.initUnix(sesh);
72+ try lib_posix.bind(fd, &unix_addr.any, unix_addr.getOsSockLen());
73+ try lib_posix.listen(fd, 128);
7474 return fd;
7575 }
7676
......@@ -96,9 +96,9 @@ pub fn getSocketPath(
9696 return fname;
9797 }
9898
99-pub fn printSessionNameTooLong(session_name: []const u8, socket_dir: []const u8) void {
99+pub fn printSessionNameTooLong(io: std.Io, session_name: []const u8, socket_dir: []const u8) void {
100100 var buf: [4096]u8 = undefined;
101- var w = std.fs.File.stderr().writer(&buf);
101+ var w = std.Io.File.stderr().writer(io, &buf);
102102 if (maxSessionNameLen(socket_dir)) |max_len| {
103103 w.interface.print(
104104 "error: session name is too long ({d} bytes, max {d} for socket directory \"{s}\")\n",
+36, -31
......@@ -33,17 +33,18 @@ pub const SessionEntry = struct {
3333
3434 pub fn get_session_entries(
3535 alloc: std.mem.Allocator,
36+ io: std.Io,
3637 socket_dir: []const u8,
3738 ) !std.ArrayList(SessionEntry) {
3839 std.log.info("get session entries socket_dir={s}", .{socket_dir});
39- var dir = try std.fs.openDirAbsolute(socket_dir, .{ .iterate = true });
40- defer dir.close();
40+ var dir = try std.Io.Dir.openDirAbsolute(io, socket_dir, .{ .iterate = true });
41+ defer dir.close(io);
4142 var iter = dir.iterate();
4243
4344 var sessions = try std.ArrayList(SessionEntry).initCapacity(alloc, 30);
4445
45- while (try iter.next()) |entry| {
46- const exists = socket.sessionExists(dir, entry.name) catch continue;
46+ while (try iter.next(io)) |entry| {
47+ const exists = socket.sessionExists(io, dir, entry.name) catch continue;
4748 if (exists) {
4849 const name = try alloc.dupe(u8, entry.name);
4950 errdefer alloc.free(name);
......@@ -70,7 +71,7 @@ pub fn get_session_entries(
7071 // daemon can miss the probe timeout; deleting its socket
7172 // orphans it permanently.
7273 if (err == error.ConnectionRefused) {
73- socket.cleanupStaleSocket(dir, entry.name);
74+ socket.cleanupStaleSocket(io, dir, entry.name);
7475 }
7576 continue;
7677 };
......@@ -404,7 +405,7 @@ pub fn stripAnsi(alloc: std.mem.Allocator, data: []const u8) ![]const u8 {
404405 return result.toOwnedSlice(alloc);
405406 }
406407
407-/// Detects Ctrl+\ across raw, Kitty CSI u, and xterm modifyOtherKeys encodings.
408+/// Dcts Ctrl+\ across raw, Kitty CSI u, and xterm modifyOtherKeys encodings.
408409 pub fn isCtrlBackslash(buf: []const u8) bool {
409410 if (buf.len == 0) return false;
410411 return buf[0] == 0x1C or isKeyPressed(buf, 0x5c, 0b100) or isModifyOtherKey(buf, 0x5c, 0b100);
......@@ -765,10 +766,6 @@ pub fn serializeTerminal(
765766 };
766767 }
767768
768-pub fn detectShell() [:0]const u8 {
769- return std.posix.getenv("SHELL") orelse "/bin/sh";
770-}
771-
772769 /// Formats a session entry for list output (only the name when `short` is
773770 /// true), adding a prefix to indicate the current session, if there is one.
774771 pub fn writeSessionLine(
......@@ -1168,8 +1165,9 @@ test "isCtrlBackslash xterm modifyOtherKeys" {
11681165
11691166 test "serializeTerminalState excludes synchronized output replay" {
11701167 const alloc = testing.allocator;
1168+ const io = testing.io;
11711169
1172- var term = try ghostty_vt.Terminal.init(alloc, .{
1170+ var term = try ghostty_vt.Terminal.init(io, alloc, .{
11731171 .cols = 80,
11741172 .rows = 24,
11751173 });
......@@ -1194,8 +1192,8 @@ test "serializeTerminalState excludes synchronized output replay" {
11941192 try testing.expect(std.mem.indexOf(u8, output, "\x1b[?2026h") == null);
11951193 }
11961194
1197-fn testCreateTerminal(alloc: std.mem.Allocator, cols: u16, rows: u16, vt_data: []const u8) !ghostty_vt.Terminal {
1198- var term = try ghostty_vt.Terminal.init(alloc, .{
1195+fn testCreateTerminal(alloc: std.mem.Allocator, io: std.Io, cols: u16, rows: u16, vt_data: []const u8) !ghostty_vt.Terminal {
1196+ var term = try ghostty_vt.Terminal.init(io, alloc, .{
11991197 .cols = cols,
12001198 .rows = rows,
12011199 .max_scrollback = 10_000_000,
......@@ -1222,12 +1220,12 @@ fn expectCursorAt(term: *ghostty_vt.Terminal, row: usize, col: usize) !void {
12221220 try testing.expectEqual(row, cursor.y);
12231221 }
12241222
1225-fn serializeRoundtrip(alloc: std.mem.Allocator, source: *ghostty_vt.Terminal) !ghostty_vt.Terminal {
1223+fn serializeRoundtrip(alloc: std.mem.Allocator, io: std.Io, source: *ghostty_vt.Terminal) !ghostty_vt.Terminal {
12261224 const serialized = serializeTerminalState(alloc, source) orelse
12271225 return error.SerializationFailed;
12281226 defer alloc.free(serialized);
12291227
1230- var dest = try ghostty_vt.Terminal.init(alloc, .{
1228+ var dest = try ghostty_vt.Terminal.init(io, alloc, .{
12311229 .cols = source.screens.active.pages.cols,
12321230 .rows = source.screens.active.pages.rows,
12331231 .max_scrollback = 10_000_000,
......@@ -1256,15 +1254,16 @@ fn expectMarkerAtRow(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal, marke
12561254
12571255 test "serializeTerminalState roundtrip preserves cursor position" {
12581256 const alloc = testing.allocator;
1257+ const io = testing.io;
12591258
1260- var term = try testCreateTerminal(alloc, 80, 24, "\x1b[2J" ++ // clear
1259+ var term = try testCreateTerminal(alloc, io, 80, 24, "\x1b[2J" ++ // clear
12611260 "\x1b[10;20H" // cursor at row 10, col 20 (1-indexed)
12621261 );
12631262 defer term.deinit(alloc);
12641263
12651264 try expectCursorAt(&term, 9, 19); // 0-indexed
12661265
1267- var client = try serializeRoundtrip(alloc, &term);
1266+ var client = try serializeRoundtrip(alloc, io, &term);
12681267 defer client.deinit(alloc);
12691268
12701269 try expectCursorAt(&client, 9, 19);
......@@ -1272,8 +1271,9 @@ test "serializeTerminalState roundtrip preserves cursor position" {
12721271
12731272 test "serializeTerminalState roundtrip preserves CUP-positioned markers" {
12741273 const alloc = testing.allocator;
1274+ const io = testing.io;
12751275
1276- var term = try testCreateTerminal(alloc, 80, 24, "\x1b[2J" ++
1276+ var term = try testCreateTerminal(alloc, io, 80, 24, "\x1b[2J" ++
12771277 "\x1b[2;5HMARK_A" ++
12781278 "\x1b[6;15HMARK_B" ++
12791279 "\x1b[10;30HMARK_C" ++
......@@ -1281,7 +1281,7 @@ test "serializeTerminalState roundtrip preserves CUP-positioned markers" {
12811281 "\x1b[16;20H");
12821282 defer term.deinit(alloc);
12831283
1284- var client = try serializeRoundtrip(alloc, &term);
1284+ var client = try serializeRoundtrip(alloc, io, &term);
12851285 defer client.deinit(alloc);
12861286
12871287 try expectScreensMatch(alloc, &term, &client);
......@@ -1294,8 +1294,9 @@ test "serializeTerminalState roundtrip preserves CUP-positioned markers" {
12941294
12951295 test "serializeTerminalState with scrollback preserves visible content" {
12961296 const alloc = testing.allocator;
1297+ const io = testing.io;
12971298
1298- var term = try testCreateTerminal(alloc, 80, 24, "");
1299+ var term = try testCreateTerminal(alloc, io, 80, 24, "");
12991300 defer term.deinit(alloc);
13001301
13011302 var stream = term.vtStream();
......@@ -1321,7 +1322,7 @@ test "serializeTerminalState with scrollback preserves visible content" {
13211322 try testing.expect(has_scrollback);
13221323
13231324 // Roundtrip: serialize → feed into fresh terminal
1324- var client = try serializeRoundtrip(alloc, &term);
1325+ var client = try serializeRoundtrip(alloc, io, &term);
13251326 defer client.deinit(alloc);
13261327
13271328 // Visible content must match (this is the core cursor corruption test)
......@@ -1336,9 +1337,10 @@ test "serializeTerminalState nested roundtrip preserves content" {
13361337 // Simulates: inner zmx → serialized state → outer ghostty-vt → serialized again → client
13371338 // This is the exact nested session scenario (zmx → SSH → zmx).
13381339 const alloc = testing.allocator;
1340+ const io = testing.io;
13391341
13401342 // "Inner" terminal with scrollback + markers
1341- var inner = try testCreateTerminal(alloc, 80, 24, "");
1343+ var inner = try testCreateTerminal(alloc, io, 80, 24, "");
13421344 defer inner.deinit(alloc);
13431345
13441346 {
......@@ -1365,7 +1367,7 @@ test "serializeTerminalState nested roundtrip preserves content" {
13651367 defer alloc.free(inner_serialized);
13661368
13671369 // "Outer" terminal processes inner's serialized output
1368- var outer = try testCreateTerminal(alloc, 80, 24, "");
1370+ var outer = try testCreateTerminal(alloc, io, 80, 24, "");
13691371 defer outer.deinit(alloc);
13701372
13711373 {
......@@ -1375,7 +1377,7 @@ test "serializeTerminalState nested roundtrip preserves content" {
13751377 }
13761378
13771379 // Serialize outer (simulates outer daemon re-attach after detach)
1378- var client = try serializeRoundtrip(alloc, &outer);
1380+ var client = try serializeRoundtrip(alloc, io, &outer);
13791381 defer client.deinit(alloc);
13801382
13811383 // Client must see the same content as inner's visible screen
......@@ -1387,15 +1389,16 @@ test "serializeTerminalState nested roundtrip preserves content" {
13871389
13881390 test "serializeTerminalState alternate screen not leaked" {
13891391 const alloc = testing.allocator;
1392+ const io = testing.io;
13901393
1391- var term = try testCreateTerminal(alloc, 80, 24, "\x1b[?1049h" ++ // enter alt screen
1394+ var term = try testCreateTerminal(alloc, io, 80, 24, "\x1b[?1049h" ++ // enter alt screen
13921395 "\x1b[2J\x1b[3;10HALT_MARK" ++ // write on alt screen
13931396 "\x1b[?1049l" ++ // exit alt screen
13941397 "\x1b[2J\x1b[2;5HMAIN_MARK\x1b[8;20H" // write on main screen
13951398 );
13961399 defer term.deinit(alloc);
13971400
1398- var client = try serializeRoundtrip(alloc, &term);
1401+ var client = try serializeRoundtrip(alloc, io, &term);
13991402 defer client.deinit(alloc);
14001403
14011404 try expectScreensMatch(alloc, &term, &client);
......@@ -1408,8 +1411,9 @@ test "serializeTerminalState alternate screen not leaked" {
14081411
14091412 test "serializeTerminalState size mismatch roundtrip" {
14101413 const alloc = testing.allocator;
1414+ const io = testing.io;
14111415
1412- var term = try testCreateTerminal(alloc, 80, 30, "\x1b[2J" ++
1416+ var term = try testCreateTerminal(alloc, io, 80, 30, "\x1b[2J" ++
14131417 "\x1b[3;10HSIZE_A" ++
14141418 "\x1b[12;20HSIZE_B" ++
14151419 "\x1b[20;40HSIZE_C" ++
......@@ -1419,7 +1423,7 @@ test "serializeTerminalState size mismatch roundtrip" {
14191423 // Resize to 24 rows (simulates outer terminal being smaller)
14201424 try term.resize(alloc, ghostty_vt.Terminal.Resize{ .cols = 80, .rows = 24 });
14211425
1422- var client = try serializeRoundtrip(alloc, &term);
1426+ var client = try serializeRoundtrip(alloc, io, &term);
14231427 defer client.deinit(alloc);
14241428
14251429 try expectScreensMatch(alloc, &term, &client);
......@@ -1428,8 +1432,9 @@ test "serializeTerminalState size mismatch roundtrip" {
14281432
14291433 test "serializeTerminalState scrollback + size mismatch nested roundtrip" {
14301434 const alloc = testing.allocator;
1435+ const io = testing.io;
14311436
1432- var inner = try testCreateTerminal(alloc, 80, 30, "");
1437+ var inner = try testCreateTerminal(alloc, io, 80, 30, "");
14331438 defer inner.deinit(alloc);
14341439
14351440 {
......@@ -1457,7 +1462,7 @@ test "serializeTerminalState scrollback + size mismatch nested roundtrip" {
14571462 return error.SerializationFailed;
14581463 defer alloc.free(inner_ser);
14591464
1460- var outer = try testCreateTerminal(alloc, 80, 24, "");
1465+ var outer = try testCreateTerminal(alloc, io, 80, 24, "");
14611466 defer outer.deinit(alloc);
14621467 {
14631468 var outer_stream = outer.vtStream();
......@@ -1465,7 +1470,7 @@ test "serializeTerminalState scrollback + size mismatch nested roundtrip" {
14651470 outer_stream.nextSlice(inner_ser);
14661471 }
14671472
1468- var client = try serializeRoundtrip(alloc, &outer);
1473+ var client = try serializeRoundtrip(alloc, io, &outer);
14691474 defer client.deinit(alloc);
14701475
14711476 try expectScreensMatch(alloc, &inner, &client);