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 | |
| 2 | 2 | ||
| 3 | 3 | RUN apt-get update && apt-get install -y curl git bats coreutils && rm -rf /var/lib/apt/lists/* | |
| 4 | 4 | ||
| 5 | - | ARG ZIG_VERSION=0.15.2 | |
| 5 | + | ARG ZIG_VERSION=0.16.0 | |
| 6 | 6 | RUN curl -L -o /tmp/zig.tar.xz https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz && \ | |
| 7 | 7 | cd /tmp && \ | |
| 8 | 8 | tar -xvf zig.tar.xz && \ |
+1,
-1
| ... | ... | @@ -80,7 +80,7 @@ nix run github:neurosnap/zmx | |
| 80 | 80 | ||
| 81 | 81 | ### src | |
| 82 | 82 | ||
| 83 | - | - Requires zig `v0.15` | |
| 83 | + | - Requires zig `v0.16` | |
| 84 | 84 | - Clone the repo | |
| 85 | 85 | - Run build cmd | |
| 86 | 86 |
+6,
-5
| ... | ... | @@ -27,6 +27,7 @@ pub fn build(b: *std.Build) void { | |
| 27 | 27 | .root_source_file = b.path("src/main.zig"), | |
| 28 | 28 | .target = target, | |
| 29 | 29 | .optimize = optimize, | |
| 30 | + | .link_libc = true, | |
| 30 | 31 | }); | |
| 31 | 32 | exe_mod.addOptions("build_options", options); | |
| 32 | 33 |
| ... | ... | @@ -54,7 +55,7 @@ pub fn build(b: *std.Build) void { | |
| 54 | 55 | .use_lld = !is_macos, | |
| 55 | 56 | .root_module = exe_mod, | |
| 56 | 57 | }); | |
| 57 | - | exe.linkLibC(); | |
| 58 | + | ||
| 58 | 59 | b.installArtifact(exe); | |
| 59 | 60 | const run_cmd = b.addRunArtifact(exe); | |
| 60 | 61 | run_cmd.step.dependOn(b.getInstallStep()); |
| ... | ... | @@ -69,6 +70,7 @@ pub fn build(b: *std.Build) void { | |
| 69 | 70 | .root_source_file = b.path("src/test.zig"), | |
| 70 | 71 | .target = target, | |
| 71 | 72 | .optimize = optimize, | |
| 73 | + | .link_libc = true, | |
| 72 | 74 | }); | |
| 73 | 75 | const test_dep = b.dependency("ghostty", .{ | |
| 74 | 76 | .target = target, |
| ... | ... | @@ -86,7 +88,7 @@ pub fn build(b: *std.Build) void { | |
| 86 | 88 | .use_llvm = true, | |
| 87 | 89 | .use_lld = !is_macos, | |
| 88 | 90 | }); | |
| 89 | - | exe_unit_tests.linkLibC(); | |
| 91 | + | ||
| 90 | 92 | const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); | |
| 91 | 93 | test_step.dependOn(&run_exe_unit_tests.step); | |
| 92 | 94 | } |
| ... | ... | @@ -100,7 +102,6 @@ pub fn build(b: *std.Build) void { | |
| 100 | 102 | .use_lld = !is_macos, | |
| 101 | 103 | .root_module = exe_mod, | |
| 102 | 104 | }); | |
| 103 | - | exe_check.linkLibC(); | |
| 104 | 105 | ||
| 105 | 106 | // Finally we add the "check" step which will be detected | |
| 106 | 107 | // by ZLS and automatically enable Build-On-Save. |
| ... | ... | @@ -121,6 +122,7 @@ pub fn build(b: *std.Build) void { | |
| 121 | 122 | .root_source_file = b.path("src/main.zig"), | |
| 122 | 123 | .target = resolved, | |
| 123 | 124 | .optimize = .ReleaseSafe, | |
| 125 | + | .link_libc = true, | |
| 124 | 126 | }); | |
| 125 | 127 | release_mod.addOptions("build_options", options); | |
| 126 | 128 |
| ... | ... | @@ -141,7 +143,6 @@ pub fn build(b: *std.Build) void { | |
| 141 | 143 | .use_lld = !is_local_macos, | |
| 142 | 144 | .root_module = release_mod, | |
| 143 | 145 | }); | |
| 144 | - | release_exe.linkLibC(); | |
| 145 | 146 | ||
| 146 | 147 | const os_name = @tagName(release_target.os_tag orelse .linux); | |
| 147 | 148 | const arch_name = @tagName(release_target.cpu_arch orelse .x86_64); |
| ... | ... | @@ -156,7 +157,7 @@ pub fn build(b: *std.Build) void { | |
| 156 | 157 | ||
| 157 | 158 | const shasum = b.addSystemCommand(&.{"sha256sum"}); | |
| 158 | 159 | shasum.addFileArg(tarball); | |
| 159 | - | const shasum_output = shasum.captureStdOut(); | |
| 160 | + | const shasum_output = shasum.captureStdOut(.{}); | |
| 160 | 161 | ||
| 161 | 162 | const install_tar = b.addInstallFile(tarball, b.fmt("dist/{s}", .{tarball_name})); | |
| 162 | 163 | const install_sha = b.addInstallFile( |
+3,
-3
| ... | ... | @@ -2,11 +2,11 @@ | |
| 2 | 2 | .name = .zmx, | |
| 3 | 3 | .version = "0.7.0", | |
| 4 | 4 | .fingerprint = 0x28aad87005052b4e, // Changing this has security and trust implications. | |
| 5 | - | .minimum_zig_version = "0.15.2", | |
| 5 | + | .minimum_zig_version = "0.16.0", | |
| 6 | 6 | .dependencies = .{ | |
| 7 | 7 | .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", | |
| 10 | 10 | }, | |
| 11 | 11 | }, | |
| 12 | 12 | .paths = .{ |
+5,
-4
| ... | ... | @@ -2,6 +2,7 @@ const std = @import("std"); | |
| 2 | 2 | const posix = std.posix; | |
| 3 | 3 | const cross = @import("cross.zig"); | |
| 4 | 4 | const socket = @import("socket.zig"); | |
| 5 | + | const lib_posix = @import("posix.zig"); | |
| 5 | 6 | ||
| 6 | 7 | pub const Tag = enum(u8) { | |
| 7 | 8 | Input = 0, |
| ... | ... | @@ -115,7 +116,7 @@ pub fn appendMessage( | |
| 115 | 116 | fn writeAll(fd: i32, data: []const u8) !void { | |
| 116 | 117 | var index: usize = 0; | |
| 117 | 118 | while (index < data.len) { | |
| 118 | - | const n = try posix.write(fd, data[index..]); | |
| 119 | + | const n = try lib_posix.write(fd, data[index..]); | |
| 119 | 120 | if (n == 0) return error.DiskQuota; | |
| 120 | 121 | index += n; | |
| 121 | 122 | } |
| ... | ... | @@ -223,7 +224,7 @@ const SessionProbeResult = struct { | |
| 223 | 224 | ||
| 224 | 225 | pub fn deinit(self: *const SessionProbeResult) void { | |
| 225 | 226 | if (self.labels) |lbl| self.alloc.free(lbl); | |
| 226 | - | posix.close(self.fd); | |
| 227 | + | lib_posix.close(self.fd); | |
| 227 | 228 | } | |
| 228 | 229 | }; | |
| 229 | 230 |
| ... | ... | @@ -233,7 +234,7 @@ pub fn probeSession( | |
| 233 | 234 | ) SessionProbeError!SessionProbeResult { | |
| 234 | 235 | const timeout_ms = 1000; | |
| 235 | 236 | const fd = try connectSession(socket_path); | |
| 236 | - | errdefer posix.close(fd); | |
| 237 | + | errdefer lib_posix.close(fd); | |
| 237 | 238 | ||
| 238 | 239 | send(fd, .Info, "") catch return error.Unexpected; | |
| 239 | 240 | send(fd, .LabelGet, "") catch {}; |
| ... | ... | @@ -320,7 +321,7 @@ pub fn roundTripForTag( | |
| 320 | 321 | ) SessionProbeError![]u8 { | |
| 321 | 322 | const timeout_ms = 1000; | |
| 322 | 323 | const fd = try connectSession(socket_path); | |
| 323 | - | defer posix.close(fd); | |
| 324 | + | defer lib_posix.close(fd); | |
| 324 | 325 | ||
| 325 | 326 | send(fd, request_tag, payload) catch return error.Unexpected; | |
| 326 | 327 |
+31,
-24
| ... | ... | @@ -1,50 +1,52 @@ | |
| 1 | 1 | const std = @import("std"); | |
| 2 | - | const posix = std.posix; | |
| 3 | 2 | ||
| 4 | 3 | 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, | |
| 7 | 6 | current_size: u64 = 0, | |
| 8 | 7 | max_size: u64 = 5 * 1024 * 1024, // 5MB | |
| 9 | 8 | path: []const u8 = "", | |
| 10 | 9 | 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), | |
| 12 | 12 | ||
| 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 { | |
| 14 | 14 | self.alloc = alloc; | |
| 15 | + | self.io = io; | |
| 15 | 16 | self.path = try alloc.dupe(u8, path); | |
| 16 | 17 | self.mode = mode; | |
| 17 | 18 | ||
| 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, | |
| 20 | 22 | path, | |
| 21 | - | .{ .read = true, .mode = @intCast(self.mode) }, | |
| 23 | + | .{ .read = true, .permissions = self.mode }, | |
| 22 | 24 | ), | |
| 23 | 25 | else => return err, | |
| 24 | 26 | }; | |
| 25 | 27 | ||
| 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); | |
| 30 | 32 | self.current_size = end_pos; | |
| 31 | 33 | self.file = file; | |
| 32 | 34 | } | |
| 33 | 35 | ||
| 34 | 36 | 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); | |
| 36 | 38 | if (self.path.len > 0) self.alloc.free(self.path); | |
| 37 | 39 | } | |
| 38 | 40 | ||
| 39 | 41 | pub fn log( | |
| 40 | 42 | self: *LogSystem, | |
| 41 | 43 | comptime level: std.log.Level, | |
| 42 | - | comptime scope: @Type(.enum_literal), | |
| 44 | + | comptime scope: anytype, | |
| 43 | 45 | comptime format: []const u8, | |
| 44 | 46 | 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); | |
| 48 | 50 | ||
| 49 | 51 | if (self.file == null) { | |
| 50 | 52 | std.log.defaultLog(level, scope, format, args); |
| ... | ... | @@ -57,7 +59,7 @@ pub const LogSystem = struct { | |
| 57 | 59 | }; | |
| 58 | 60 | } | |
| 59 | 61 | ||
| 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)); | |
| 61 | 63 | const prefix = "[{d}] [{s}] ({s}): "; | |
| 62 | 64 | const scope_name = @tagName(scope); | |
| 63 | 65 | const level_name = level.asText(); |
| ... | ... | @@ -76,29 +78,34 @@ pub const LogSystem = struct { | |
| 76 | 78 | self.current_size += total_len; | |
| 77 | 79 | ||
| 78 | 80 | 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 {}; | |
| 81 | 83 | w.interface.flush() catch {}; | |
| 82 | 84 | } | |
| 83 | 85 | } | |
| 84 | 86 | ||
| 85 | 87 | fn rotate(self: *LogSystem) !void { | |
| 86 | 88 | if (self.file) |f| { | |
| 87 | - | f.close(); | |
| 89 | + | std.Io.File.close(f, self.io); | |
| 88 | 90 | self.file = null; | |
| 89 | 91 | } | |
| 90 | 92 | ||
| 91 | 93 | const old_path = try std.fmt.allocPrint(self.alloc, "{s}.old", .{self.path}); | |
| 92 | 94 | defer self.alloc.free(old_path); | |
| 93 | 95 | ||
| 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) { | |
| 95 | 97 | error.FileNotFound => {}, | |
| 96 | 98 | else => return err, | |
| 97 | 99 | }; | |
| 98 | 100 | ||
| 99 | - | self.file = try std.fs.createFileAbsolute( | |
| 101 | + | self.file = try std.Io.Dir.createFileAbsolute( | |
| 102 | + | self.io, | |
| 100 | 103 | self.path, | |
| 101 | - | .{ .truncate = true, .read = true, .mode = @intCast(self.mode) }, | |
| 104 | + | .{ | |
| 105 | + | .truncate = true, | |
| 106 | + | .read = true, | |
| 107 | + | .permissions = self.mode, | |
| 108 | + | }, | |
| 102 | 109 | ); | |
| 103 | 110 | self.current_size = 0; | |
| 104 | 111 | } |
+363,
-340
| ... | ... | @@ -9,6 +9,7 @@ const util = @import("util.zig"); | |
| 9 | 9 | const cross = @import("cross.zig"); | |
| 10 | 10 | const socket = @import("socket.zig"); | |
| 11 | 11 | const label = @import("label.zig"); | |
| 12 | + | const lib_posix = @import("posix.zig"); | |
| 12 | 13 | ||
| 13 | 14 | pub const version = build_options.version; | |
| 14 | 15 | pub const ghostty_version = build_options.ghostty_version; |
| ... | ... | @@ -22,11 +23,11 @@ pub const std_options: std.Options = .{ | |
| 22 | 23 | ||
| 23 | 24 | fn zmxLogFn( | |
| 24 | 25 | comptime level: std.log.Level, | |
| 25 | - | comptime scope: @Type(.enum_literal), | |
| 26 | + | comptime scope: anytype, | |
| 26 | 27 | comptime format: []const u8, | |
| 27 | 28 | args: anytype, | |
| 28 | 29 | ) void { | |
| 29 | - | log_system.log(level, scope, format, args); | |
| 30 | + | log_system.log(level, scope, format, args) catch {}; | |
| 30 | 31 | } | |
| 31 | 32 | ||
| 32 | 33 | /// Self-pipe woken by signal handlers. std.posix.poll loops on .INTR internally |
| ... | ... | @@ -47,13 +48,13 @@ const SessionMatch = struct { | |
| 47 | 48 | } | |
| 48 | 49 | }; | |
| 49 | 50 | ||
| 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 { | |
| 51 | 52 | const sesh_env = socket.getSeshNameFromEnv(); | |
| 52 | 53 | const raw = if (session_name) |name| | |
| 53 | 54 | if (std.mem.eql(u8, name, ".")) blk: { | |
| 54 | 55 | if (sesh_env.len > 0) break :blk sesh_env; | |
| 55 | 56 | var buf: [4096]u8 = undefined; | |
| 56 | - | var w = std.fs.File.stderr().writer(&buf); | |
| 57 | + | var w = std.Io.File.stderr().writer(io, &buf); | |
| 57 | 58 | w.interface.print("error: \".\" requires ZMX_SESSION (are you inside a zmx session?)\n", .{}) catch {}; | |
| 58 | 59 | w.interface.flush() catch {}; | |
| 59 | 60 | return error.SessionNameRequired; |
| ... | ... | @@ -76,7 +77,7 @@ fn parseSessionArg(alloc: std.mem.Allocator, raw: []const u8) !SessionMatch { | |
| 76 | 77 | } | |
| 77 | 78 | ||
| 78 | 79 | fn openSignalPipe() !void { | |
| 79 | - | sig_pipe = try posix.pipe2(.{ .CLOEXEC = true, .NONBLOCK = true }); | |
| 80 | + | sig_pipe = try lib_posix.pipe2(.{ .CLOEXEC = true, .NONBLOCK = true }); | |
| 80 | 81 | } | |
| 81 | 82 | ||
| 82 | 83 | fn drainSignalPipe() void { |
| ... | ... | @@ -91,9 +92,9 @@ fn detectHelp(arg: []const u8) bool { | |
| 91 | 92 | return (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")); | |
| 92 | 93 | } | |
| 93 | 94 | ||
| 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; | |
| 97 | 98 | ||
| 98 | 99 | // Every subcommand may write to a Unix-domain socket; a peer that | |
| 99 | 100 | // disappears between probe and send would otherwise kill us before |
| ... | ... | @@ -101,76 +102,79 @@ pub fn main() !void { | |
| 101 | 102 | // covers the daemon. | |
| 102 | 103 | ignoreSigpipe(); | |
| 103 | 104 | ||
| 104 | - | var args = try std.process.argsWithAllocator(alloc); | |
| 105 | + | var args = init.minimal.args.iterate(); | |
| 105 | 106 | defer args.deinit(); | |
| 106 | - | _ = args.skip(); // skip program name | |
| 107 | + | _ = args.next(); // skip program name | |
| 107 | 108 | ||
| 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); | |
| 110 | 111 | ||
| 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); | |
| 114 | 116 | defer log_system.deinit(); | |
| 115 | 117 | ||
| 118 | + | const shell_env = init.environ_map.get("SHELL") orelse "/bin/sh"; | |
| 119 | + | ||
| 116 | 120 | const cmd = args.next() orelse { | |
| 117 | - | return list(&cfg, false); | |
| 121 | + | return list(gpa, io, &cfg, false); | |
| 118 | 122 | }; | |
| 119 | 123 | ||
| 120 | 124 | 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); | |
| 122 | 126 | } 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); | |
| 124 | 128 | } else if (std.mem.eql(u8, cmd, "list") or std.mem.eql(u8, cmd, "l") or std.mem.eql(u8, cmd, "ls")) { | |
| 125 | 129 | var short = false; | |
| 126 | 130 | while (args.next()) |arg| { | |
| 127 | - | if (detectHelp(arg)) return help(); | |
| 131 | + | if (detectHelp(arg)) return help(io); | |
| 128 | 132 | if (std.mem.eql(u8, arg, "--short")) short = true; | |
| 129 | 133 | } | |
| 130 | - | return list(&cfg, short); | |
| 134 | + | return list(gpa, io, &cfg, short); | |
| 131 | 135 | } else if (std.mem.eql(u8, cmd, "get") or std.mem.eql(u8, cmd, "g")) { | |
| 132 | 136 | 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); | |
| 136 | 140 | const single_kv = args.next() orelse ""; | |
| 137 | - | return labelGet(&cfg, sesh, single_kv); | |
| 141 | + | return labelGet(gpa, io, &cfg, sesh, single_kv); | |
| 138 | 142 | } else if (std.mem.eql(u8, cmd, "set")) { | |
| 139 | 143 | 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); | |
| 143 | 147 | ||
| 144 | 148 | var kvs = std.ArrayList(u8).empty; | |
| 145 | - | defer kvs.deinit(alloc); | |
| 149 | + | defer kvs.deinit(gpa); | |
| 146 | 150 | var first = true; | |
| 147 | 151 | 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); | |
| 150 | 154 | first = false; | |
| 151 | 155 | } | |
| 152 | - | return labelSet(&cfg, sesh, kvs.items); | |
| 156 | + | return labelSet(gpa, io, &cfg, sesh, kvs.items); | |
| 153 | 157 | } else if (std.mem.eql(u8, cmd, "clear")) { | |
| 154 | 158 | 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); | |
| 159 | 163 | } else if (std.mem.eql(u8, cmd, "completions") or std.mem.eql(u8, cmd, "c")) { | |
| 160 | 164 | const arg = args.next() orelse return; | |
| 161 | 165 | if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { | |
| 162 | - | return help(); | |
| 166 | + | return help(io); | |
| 163 | 167 | } | |
| 164 | 168 | const shell = completions.Shell.fromString(arg) orelse return; | |
| 165 | - | return printCompletions(shell); | |
| 169 | + | return printCompletions(io, shell); | |
| 166 | 170 | } 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); | |
| 168 | 172 | } else if (std.mem.eql(u8, cmd, "history") or std.mem.eql(u8, cmd, "hi")) { | |
| 169 | 173 | var session_name: ?[]const u8 = null; | |
| 170 | 174 | var format: util.HistoryFormat = .plain; | |
| 171 | 175 | while (args.next()) |arg| { | |
| 172 | 176 | if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { | |
| 173 | - | return help(); | |
| 177 | + | return help(io); | |
| 174 | 178 | } else if (std.mem.eql(u8, arg, "--vt")) { | |
| 175 | 179 | format = .vt; | |
| 176 | 180 | } else if (std.mem.eql(u8, arg, "--html")) { |
| ... | ... | @@ -180,47 +184,50 @@ pub fn main() !void { | |
| 180 | 184 | } | |
| 181 | 185 | } | |
| 182 | 186 | 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); | |
| 186 | 190 | } else if (std.mem.eql(u8, cmd, "attach") or std.mem.eql(u8, cmd, "a")) { | |
| 187 | 191 | const session_name = args.next() orelse ""; | |
| 188 | 192 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 189 | - | return help(); | |
| 193 | + | return help(io); | |
| 190 | 194 | } | |
| 191 | 195 | ||
| 192 | 196 | var command_args: std.ArrayList([]const u8) = .empty; | |
| 193 | - | defer command_args.deinit(alloc); | |
| 197 | + | defer command_args.deinit(gpa); | |
| 194 | 198 | while (args.next()) |arg| { | |
| 195 | - | try command_args.append(alloc, arg); | |
| 199 | + | try command_args.append(gpa, arg); | |
| 196 | 200 | } | |
| 197 | 201 | ||
| 198 | - | const clients = try std.ArrayList(*Client).initCapacity(alloc, 10); | |
| 202 | + | const clients = try std.ArrayList(*Client).initCapacity(gpa, 10); | |
| 199 | 203 | var command: ?[][]const u8 = null; | |
| 200 | 204 | if (command_args.items.len > 0) { | |
| 201 | 205 | command = command_args.items; | |
| 202 | 206 | } | |
| 203 | 207 | ||
| 204 | 208 | 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]; | |
| 206 | 211 | ||
| 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); | |
| 209 | 214 | var daemon = Daemon{ | |
| 215 | + | .io = io, | |
| 210 | 216 | .running = true, | |
| 211 | 217 | .cfg = &cfg, | |
| 212 | - | .alloc = alloc, | |
| 218 | + | .alloc = std.heap.c_allocator, | |
| 213 | 219 | .clients = clients, | |
| 214 | 220 | .session_name = sesh, | |
| 215 | 221 | .socket_path = undefined, | |
| 216 | 222 | .pid = undefined, | |
| 217 | 223 | .command = command, | |
| 218 | 224 | .cwd = cwd, | |
| 219 | - | .created_at = @intCast(std.time.timestamp()), | |
| 225 | + | .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds), | |
| 220 | 226 | .leader_client_fd = null, | |
| 227 | + | .shell = shell_env, | |
| 221 | 228 | }; | |
| 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), | |
| 224 | 231 | error.OutOfMemory => return err, | |
| 225 | 232 | }; | |
| 226 | 233 | std.log.info("socket path={s}", .{daemon.socket_path}); |
| ... | ... | @@ -228,42 +235,45 @@ pub fn main() !void { | |
| 228 | 235 | } else if (std.mem.eql(u8, cmd, "run") or std.mem.eql(u8, cmd, "r")) { | |
| 229 | 236 | const session_name = args.next() orelse ""; | |
| 230 | 237 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 231 | - | return help(); | |
| 238 | + | return help(io); | |
| 232 | 239 | } | |
| 233 | 240 | ||
| 234 | 241 | var cmd_args_raw: std.ArrayList([]const u8) = .empty; | |
| 235 | - | defer cmd_args_raw.deinit(alloc); | |
| 242 | + | defer cmd_args_raw.deinit(gpa); | |
| 236 | 243 | var detached = false; | |
| 237 | 244 | while (args.next()) |arg| { | |
| 238 | 245 | if (std.mem.startsWith(u8, arg, "-d")) { | |
| 239 | 246 | detached = true; | |
| 240 | 247 | } else { | |
| 241 | - | try cmd_args_raw.append(alloc, arg); | |
| 248 | + | try cmd_args_raw.append(gpa, arg); | |
| 242 | 249 | } | |
| 243 | 250 | } | |
| 244 | - | const clients = try std.ArrayList(*Client).initCapacity(alloc, 10); | |
| 251 | + | const clients = try std.ArrayList(*Client).initCapacity(gpa, 10); | |
| 245 | 252 | ||
| 246 | 253 | 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]; | |
| 248 | 256 | ||
| 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); | |
| 251 | 259 | var daemon = Daemon{ | |
| 260 | + | .io = io, | |
| 252 | 261 | .running = true, | |
| 253 | 262 | .cfg = &cfg, | |
| 254 | - | .alloc = alloc, | |
| 263 | + | .alloc = std.heap.c_allocator, | |
| 255 | 264 | .clients = clients, | |
| 256 | 265 | .session_name = sesh, | |
| 257 | 266 | .socket_path = undefined, | |
| 258 | 267 | .pid = undefined, | |
| 259 | 268 | .command = null, | |
| 260 | 269 | .cwd = cwd, | |
| 261 | - | .created_at = @intCast(std.time.timestamp()), | |
| 270 | + | .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds), | |
| 262 | 271 | .is_task_mode = true, | |
| 263 | 272 | .leader_client_fd = null, | |
| 273 | + | .shell = shell_env, | |
| 264 | 274 | }; | |
| 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), | |
| 267 | 277 | error.OutOfMemory => return err, | |
| 268 | 278 | }; | |
| 269 | 279 | std.log.info("socket path={s}", .{daemon.socket_path}); |
| ... | ... | @@ -271,76 +281,76 @@ pub fn main() !void { | |
| 271 | 281 | } else if (std.mem.eql(u8, cmd, "send") or std.mem.eql(u8, cmd, "s")) { | |
| 272 | 282 | const session_name = args.next() orelse ""; | |
| 273 | 283 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 274 | - | return help(); | |
| 284 | + | return help(io); | |
| 275 | 285 | } | |
| 276 | 286 | if (session_name.len == 0) return error.SessionNameRequired; | |
| 277 | 287 | ||
| 278 | 288 | var text_parts: std.ArrayList([]const u8) = .empty; | |
| 279 | - | defer text_parts.deinit(alloc); | |
| 289 | + | defer text_parts.deinit(gpa); | |
| 280 | 290 | while (args.next()) |arg| { | |
| 281 | - | try text_parts.append(alloc, arg); | |
| 291 | + | try text_parts.append(gpa, arg); | |
| 282 | 292 | } | |
| 283 | 293 | ||
| 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), | |
| 288 | 298 | error.OutOfMemory => return err, | |
| 289 | 299 | }; | |
| 290 | - | return send(&cfg, sesh, socket_path, text_parts.items, .Send); | |
| 300 | + | return send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Send); | |
| 291 | 301 | } else if (std.mem.eql(u8, cmd, "print") or std.mem.eql(u8, cmd, "p")) { | |
| 292 | 302 | const session_name = args.next() orelse ""; | |
| 293 | 303 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 294 | - | return help(); | |
| 304 | + | return help(io); | |
| 295 | 305 | } | |
| 296 | 306 | if (session_name.len == 0) return error.SessionNameRequired; | |
| 297 | 307 | ||
| 298 | 308 | var text_parts: std.ArrayList([]const u8) = .empty; | |
| 299 | - | defer text_parts.deinit(alloc); | |
| 309 | + | defer text_parts.deinit(gpa); | |
| 300 | 310 | while (args.next()) |arg| { | |
| 301 | - | try text_parts.append(alloc, arg); | |
| 311 | + | try text_parts.append(gpa, arg); | |
| 302 | 312 | } | |
| 303 | 313 | ||
| 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), | |
| 308 | 318 | error.OutOfMemory => return err, | |
| 309 | 319 | }; | |
| 310 | - | return send(&cfg, sesh, socket_path, text_parts.items, .Output); | |
| 320 | + | return send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Output); | |
| 311 | 321 | } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) { | |
| 312 | 322 | 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); | |
| 314 | 324 | const stderr = &stderr_writer.interface; | |
| 315 | 325 | ||
| 316 | 326 | var matchers: std.ArrayList(SessionMatch) = .empty; | |
| 317 | 327 | defer { | |
| 318 | 328 | for (matchers.items) |m| { | |
| 319 | - | alloc.free(m.name); | |
| 329 | + | gpa.free(m.name); | |
| 320 | 330 | } | |
| 321 | - | matchers.deinit(alloc); | |
| 331 | + | matchers.deinit(gpa); | |
| 322 | 332 | } | |
| 323 | 333 | var force = false; | |
| 324 | 334 | while (args.next()) |session_name| { | |
| 325 | 335 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 326 | - | return help(); | |
| 336 | + | return help(io); | |
| 327 | 337 | } | |
| 328 | 338 | if (std.mem.eql(u8, session_name, "--force")) { | |
| 329 | 339 | force = true; | |
| 330 | 340 | continue; | |
| 331 | 341 | } | |
| 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); | |
| 334 | 344 | } | |
| 335 | 345 | if (matchers.items.len == 0) { | |
| 336 | 346 | return error.SessionNameRequired; | |
| 337 | 347 | } | |
| 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); | |
| 339 | 349 | defer { | |
| 340 | 350 | for (sessions.items) |session| { | |
| 341 | - | session.deinit(alloc); | |
| 351 | + | session.deinit(gpa); | |
| 342 | 352 | } | |
| 343 | - | sessions.deinit(alloc); | |
| 353 | + | sessions.deinit(gpa); | |
| 344 | 354 | } | |
| 345 | 355 | ||
| 346 | 356 | for (sessions.items) |session| { |
| ... | ... | @@ -349,7 +359,7 @@ pub fn main() !void { | |
| 349 | 359 | continue; | |
| 350 | 360 | } | |
| 351 | 361 | ||
| 352 | - | kill(&cfg, session.name, force) catch |err| { | |
| 362 | + | kill(gpa, io, &cfg, session.name, force) catch |err| { | |
| 353 | 363 | try stderr.print( | |
| 354 | 364 | "failed to kill session={s}: {s}\n", | |
| 355 | 365 | .{ session.name, @errorName(err) }, |
| ... | ... | @@ -363,35 +373,35 @@ pub fn main() !void { | |
| 363 | 373 | var matchers: std.ArrayList(SessionMatch) = .empty; | |
| 364 | 374 | defer { | |
| 365 | 375 | for (matchers.items) |m| { | |
| 366 | - | alloc.free(m.name); | |
| 376 | + | gpa.free(m.name); | |
| 367 | 377 | } | |
| 368 | - | matchers.deinit(alloc); | |
| 378 | + | matchers.deinit(gpa); | |
| 369 | 379 | } | |
| 370 | 380 | while (args.next()) |session_name| { | |
| 371 | 381 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 372 | - | return help(); | |
| 382 | + | return help(io); | |
| 373 | 383 | } | |
| 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); | |
| 376 | 386 | } | |
| 377 | 387 | if (matchers.items.len == 0) { | |
| 378 | 388 | return error.SessionNameRequired; | |
| 379 | 389 | } | |
| 380 | - | return wait(&cfg, matchers); | |
| 390 | + | return wait(gpa, io, &cfg, matchers); | |
| 381 | 391 | } else if (std.mem.eql(u8, cmd, "tail") or std.mem.eql(u8, cmd, "t")) { | |
| 382 | 392 | var matchers: std.ArrayList(SessionMatch) = .empty; | |
| 383 | 393 | defer { | |
| 384 | 394 | for (matchers.items) |m| { | |
| 385 | - | alloc.free(m.name); | |
| 395 | + | gpa.free(m.name); | |
| 386 | 396 | } | |
| 387 | - | matchers.deinit(alloc); | |
| 397 | + | matchers.deinit(gpa); | |
| 388 | 398 | } | |
| 389 | 399 | while (args.next()) |session_name| { | |
| 390 | 400 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 391 | - | return help(); | |
| 401 | + | return help(io); | |
| 392 | 402 | } | |
| 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); | |
| 395 | 405 | } | |
| 396 | 406 | if (matchers.items.len == 0) { | |
| 397 | 407 | return error.SessionNameRequired; |
| ... | ... | @@ -401,9 +411,9 @@ pub fn main() !void { | |
| 401 | 411 | var resolved_names: std.ArrayList([]const u8) = .empty; | |
| 402 | 412 | defer { | |
| 403 | 413 | for (resolved_names.items) |name| { | |
| 404 | - | alloc.free(name); | |
| 414 | + | gpa.free(name); | |
| 405 | 415 | } | |
| 406 | - | resolved_names.deinit(alloc); | |
| 416 | + | resolved_names.deinit(gpa); | |
| 407 | 417 | } | |
| 408 | 418 | ||
| 409 | 419 | var any_prefix = false; |
| ... | ... | @@ -415,17 +425,17 @@ pub fn main() !void { | |
| 415 | 425 | } | |
| 416 | 426 | ||
| 417 | 427 | 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); | |
| 419 | 429 | defer { | |
| 420 | 430 | for (sessions.items) |session| { | |
| 421 | - | session.deinit(alloc); | |
| 431 | + | session.deinit(gpa); | |
| 422 | 432 | } | |
| 423 | - | sessions.deinit(alloc); | |
| 433 | + | sessions.deinit(gpa); | |
| 424 | 434 | } | |
| 425 | 435 | for (sessions.items) |session| { | |
| 426 | 436 | for (matchers.items) |m| { | |
| 427 | 437 | 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)); | |
| 429 | 439 | break; | |
| 430 | 440 | } | |
| 431 | 441 | } |
| ... | ... | @@ -434,66 +444,69 @@ pub fn main() !void { | |
| 434 | 444 | // Add exact-match names directly. | |
| 435 | 445 | for (matchers.items) |m| { | |
| 436 | 446 | 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)); | |
| 438 | 448 | } | |
| 439 | 449 | } | |
| 440 | 450 | ||
| 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); | |
| 442 | 452 | defer { | |
| 443 | 453 | for (client_socket_fds.items) |client_fd| { | |
| 444 | - | posix.close(client_fd); | |
| 454 | + | lib_posix.close(client_fd); | |
| 445 | 455 | } | |
| 446 | - | client_socket_fds.deinit(alloc); | |
| 456 | + | client_socket_fds.deinit(gpa); | |
| 447 | 457 | } | |
| 448 | 458 | ||
| 449 | 459 | 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), | |
| 452 | 462 | error.OutOfMemory => return err, | |
| 453 | 463 | }; | |
| 454 | 464 | 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); | |
| 456 | 466 | } | |
| 457 | - | _ = try tail(client_socket_fds, false, false); | |
| 467 | + | _ = try tail(gpa, client_socket_fds, false, false); | |
| 458 | 468 | } else if (std.mem.eql(u8, cmd, "write") or std.mem.eql(u8, cmd, "wr")) { | |
| 459 | 469 | const session_name = args.next() orelse ""; | |
| 460 | 470 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 461 | - | return help(); | |
| 471 | + | return help(io); | |
| 462 | 472 | } | |
| 463 | 473 | if (session_name.len == 0) return error.SessionNameRequired; | |
| 464 | 474 | const file_path = args.next() orelse ""; | |
| 465 | 475 | if (std.mem.eql(u8, file_path, "--help") or std.mem.eql(u8, file_path, "-h")) { | |
| 466 | - | return help(); | |
| 476 | + | return help(io); | |
| 467 | 477 | } | |
| 468 | 478 | if (file_path.len == 0) return error.FilePathRequired; | |
| 469 | 479 | ||
| 470 | 480 | 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); | |
| 475 | 486 | var daemon = Daemon{ | |
| 487 | + | .io = io, | |
| 476 | 488 | .running = true, | |
| 477 | 489 | .cfg = &cfg, | |
| 478 | - | .alloc = alloc, | |
| 490 | + | .alloc = std.heap.c_allocator, | |
| 479 | 491 | .clients = clients, | |
| 480 | 492 | .session_name = sesh, | |
| 481 | 493 | .socket_path = undefined, | |
| 482 | 494 | .pid = undefined, | |
| 483 | 495 | .command = null, | |
| 484 | 496 | .cwd = cwd, | |
| 485 | - | .created_at = @intCast(std.time.timestamp()), | |
| 497 | + | .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds), | |
| 486 | 498 | .is_task_mode = true, | |
| 487 | 499 | .leader_client_fd = null, | |
| 500 | + | .shell = shell_env, | |
| 488 | 501 | }; | |
| 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), | |
| 491 | 504 | error.OutOfMemory => return err, | |
| 492 | 505 | }; | |
| 493 | 506 | std.log.info("socket path={s}", .{daemon.socket_path}); | |
| 494 | 507 | try writeFile(&daemon, file_path); | |
| 495 | 508 | } else { | |
| 496 | - | return help(); | |
| 509 | + | return help(io); | |
| 497 | 510 | } | |
| 498 | 511 | } | |
| 499 | 512 |
| ... | ... | @@ -508,7 +521,7 @@ const Client = struct { | |
| 508 | 521 | write_buf: std.ArrayList(u8), | |
| 509 | 522 | ||
| 510 | 523 | pub fn deinit(self: *Client) void { | |
| 511 | - | posix.close(self.socket_fd); | |
| 524 | + | lib_posix.close(self.socket_fd); | |
| 512 | 525 | self.read_buf.deinit(); | |
| 513 | 526 | self.write_buf.deinit(self.alloc); | |
| 514 | 527 | } |
| ... | ... | @@ -524,18 +537,18 @@ const Cfg = struct { | |
| 524 | 537 | dir_mode: u32 = 0o750, | |
| 525 | 538 | log_mode: u32 = 0o640, | |
| 526 | 539 | ||
| 527 | - | pub fn init(alloc: std.mem.Allocator) !Cfg { | |
| 540 | + | pub fn init(alloc: std.mem.Allocator, io: std.Io) !Cfg { | |
| 528 | 541 | const socket_dir = try socketDir(alloc); | |
| 529 | 542 | errdefer alloc.free(socket_dir); | |
| 530 | 543 | const log_dir = try logDir(alloc); | |
| 531 | 544 | errdefer alloc.free(log_dir); | |
| 532 | 545 | ||
| 533 | - | const dir_mode = if (std.posix.getenv("ZMX_DIR_MODE")) |m| | |
| 546 | + | const dir_mode = if (lib_posix.getenv("ZMX_DIR_MODE")) |m| | |
| 534 | 547 | std.fmt.parseInt(u32, m, 8) catch 0o750 | |
| 535 | 548 | else | |
| 536 | 549 | 0o750; | |
| 537 | 550 | ||
| 538 | - | const log_mode = if (std.posix.getenv("ZMX_LOG_MODE")) |m| | |
| 551 | + | const log_mode = if (lib_posix.getenv("ZMX_LOG_MODE")) |m| | |
| 539 | 552 | std.fmt.parseInt(u32, m, 8) catch 0o640 | |
| 540 | 553 | else | |
| 541 | 554 | 0o640; |
| ... | ... | @@ -547,18 +560,18 @@ const Cfg = struct { | |
| 547 | 560 | .log_mode = log_mode, | |
| 548 | 561 | }; | |
| 549 | 562 | ||
| 550 | - | try cfg.mkdir(); | |
| 563 | + | try cfg.mkdir(io); | |
| 551 | 564 | ||
| 552 | 565 | return cfg; | |
| 553 | 566 | } | |
| 554 | 567 | ||
| 555 | 568 | 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(); | |
| 558 | 571 | ||
| 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| | |
| 560 | 573 | 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| | |
| 562 | 575 | try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime}) | |
| 563 | 576 | else | |
| 564 | 577 | try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid }); |
| ... | ... | @@ -567,16 +580,16 @@ const Cfg = struct { | |
| 567 | 580 | } | |
| 568 | 581 | ||
| 569 | 582 | 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| | |
| 571 | 584 | 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| | |
| 573 | 586 | 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| | |
| 575 | 588 | try std.fmt.allocPrint(alloc, "{s}/.local/state/zmx/logs", .{home_dir}) | |
| 576 | 589 | else fallback: { | |
| 577 | 590 | // 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(); | |
| 580 | 593 | break :fallback try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid }); | |
| 581 | 594 | }; | |
| 582 | 595 |
| ... | ... | @@ -588,16 +601,18 @@ const Cfg = struct { | |
| 588 | 601 | if (self.log_dir.len > 0) alloc.free(self.log_dir); | |
| 589 | 602 | } | |
| 590 | 603 | ||
| 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); | |
| 594 | 609 | } | |
| 595 | 610 | ||
| 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); | |
| 598 | 613 | var component = it.last() orelse return error.BadPathName; | |
| 599 | 614 | 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) { | |
| 601 | 616 | error.PathAlreadyExists => {}, | |
| 602 | 617 | error.FileNotFound => |e| { | |
| 603 | 618 | component = it.previous() orelse return e; |
| ... | ... | @@ -617,7 +632,7 @@ test "Cfg.init uses default modes when env vars are not set" { | |
| 617 | 632 | _ = cross.c.unsetenv("ZMX_DIR_MODE"); | |
| 618 | 633 | _ = cross.c.unsetenv("ZMX_LOG_MODE"); | |
| 619 | 634 | ||
| 620 | - | var cfg = try Cfg.init(alloc); | |
| 635 | + | var cfg = try Cfg.init(alloc, std.testing.io); | |
| 621 | 636 | defer cfg.deinit(alloc); | |
| 622 | 637 | ||
| 623 | 638 | try std.testing.expectEqual(@as(u32, 0o750), cfg.dir_mode); |
| ... | ... | @@ -635,7 +650,7 @@ test "Cfg.init uses custom modes from env vars" { | |
| 635 | 650 | _ = cross.c.unsetenv("ZMX_LOG_MODE"); | |
| 636 | 651 | } | |
| 637 | 652 | ||
| 638 | - | var cfg = try Cfg.init(alloc); | |
| 653 | + | var cfg = try Cfg.init(alloc, std.testing.io); | |
| 639 | 654 | defer cfg.deinit(alloc); | |
| 640 | 655 | ||
| 641 | 656 | try std.testing.expectEqual(@as(u32, 0o770), cfg.dir_mode); |
| ... | ... | @@ -651,6 +666,7 @@ test "Cfg.init uses custom modes from env vars" { | |
| 651 | 666 | /// | |
| 652 | 667 | /// Conceptually it's also much simpler to reason about. | |
| 653 | 668 | const Daemon = struct { | |
| 669 | + | io: std.Io, | |
| 654 | 670 | cfg: *Cfg, | |
| 655 | 671 | alloc: std.mem.Allocator, | |
| 656 | 672 | clients: std.ArrayList(*Client), |
| ... | ... | @@ -673,6 +689,7 @@ const Daemon = struct { | |
| 673 | 689 | task_ended_at: ?u64 = null, // timestamp when task exited | |
| 674 | 690 | pty_fd: i32 = -1, // set by daemonLoop so handleRun can probe the foreground process | |
| 675 | 691 | pty_write_buf: std.ArrayList(u8) = .empty, | |
| 692 | + | shell: []const u8 = "/bin/sh", | |
| 676 | 693 | ||
| 677 | 694 | const EnsureSessionResult = struct { | |
| 678 | 695 | created: bool, |
| ... | ... | @@ -808,12 +825,14 @@ const Daemon = struct { | |
| 808 | 825 | for (cmd_args, 0..) |arg, i| { | |
| 809 | 826 | argv[i] = try alloc.dupeZ(u8, arg); | |
| 810 | 827 | } | |
| 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); | |
| 812 | 829 | std.log.err("execvpe failed: cmd={s} err={s}", .{ cmd_args[0], @errorName(err) }); | |
| 813 | - | std.posix.exit(1); | |
| 830 | + | lib_posix.exit(1); | |
| 814 | 831 | } | |
| 815 | 832 | ||
| 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; | |
| 817 | 836 | // Use "-shellname" as argv[0] to signal login shell (traditional method) | |
| 818 | 837 | const login_shell = try std.fmt.allocPrintSentinel( | |
| 819 | 838 | alloc, |
| ... | ... | @@ -822,9 +841,9 @@ const Daemon = struct { | |
| 822 | 841 | 0, | |
| 823 | 842 | ); | |
| 824 | 843 | 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); | |
| 826 | 845 | std.log.err("execvpe failed: shell={s} err={s}", .{ shell, @errorName(err) }); | |
| 827 | - | std.posix.exit(1); | |
| 846 | + | lib_posix.exit(1); | |
| 828 | 847 | } | |
| 829 | 848 | ||
| 830 | 849 | /// spawnPty runs forkpty() and executes the shell or shell command the user provides. |
| ... | ... | @@ -850,7 +869,7 @@ const Daemon = struct { | |
| 850 | 869 | // errdefers that delete the parent's socket file). | |
| 851 | 870 | execChild(self) catch |err| { | |
| 852 | 871 | std.log.err("child setup failed: {s}", .{@errorName(err)}); | |
| 853 | - | std.posix.exit(1); | |
| 872 | + | lib_posix.exit(1); | |
| 854 | 873 | }; | |
| 855 | 874 | unreachable; // execChild either execs or exits, never returns ok | |
| 856 | 875 | } |
| ... | ... | @@ -859,8 +878,8 @@ const Daemon = struct { | |
| 859 | 878 | std.log.info("pty spawned session={s} pid={d}", .{ self.session_name, pid }); | |
| 860 | 879 | ||
| 861 | 880 | // 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); | |
| 864 | 883 | return master_fd; | |
| 865 | 884 | } | |
| 866 | 885 |
| ... | ... | @@ -868,15 +887,15 @@ const Daemon = struct { | |
| 868 | 887 | /// If not it creates one and spawns the daemon. | |
| 869 | 888 | fn ensureSession(self: *Daemon) !EnsureSessionResult { | |
| 870 | 889 | 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); | |
| 873 | 892 | ||
| 874 | - | const exists = try socket.sessionExists(dir, self.session_name); | |
| 893 | + | const exists = try socket.sessionExists(self.io, dir, self.session_name); | |
| 875 | 894 | var should_create = !exists; | |
| 876 | 895 | ||
| 877 | 896 | if (exists) { | |
| 878 | 897 | if (ipc.connectSession(self.socket_path)) |fd| { | |
| 879 | - | posix.close(fd); | |
| 898 | + | lib_posix.close(fd); | |
| 880 | 899 | if (self.command != null) { | |
| 881 | 900 | std.log.warn( | |
| 882 | 901 | "session already exists, ignoring command session={s}", |
| ... | ... | @@ -886,7 +905,7 @@ const Daemon = struct { | |
| 886 | 905 | } else |err| switch (err) { | |
| 887 | 906 | // Daemon is definitively gone: safe to replace. | |
| 888 | 907 | error.ConnectionRefused => { | |
| 889 | - | socket.cleanupStaleSocket(dir, self.session_name); | |
| 908 | + | socket.cleanupStaleSocket(self.io, dir, self.session_name); | |
| 890 | 909 | should_create = true; | |
| 891 | 910 | }, | |
| 892 | 911 | // Connect failed for an unusual reason. The check is only to |
| ... | ... | @@ -906,10 +925,10 @@ const Daemon = struct { | |
| 906 | 925 | const server_sock_fd = try socket.createSocket(self.socket_path); | |
| 907 | 926 | ||
| 908 | 927 | // creates the daemon | |
| 909 | - | const pid = try posix.fork(); | |
| 928 | + | const pid = try lib_posix.fork(); | |
| 910 | 929 | if (pid == 0) { // child (daemon) | |
| 911 | 930 | // becomes the session leader and detaches process from its controlling terminal | |
| 912 | - | _ = try posix.setsid(); | |
| 931 | + | _ = try lib_posix.setsid(); | |
| 913 | 932 | ||
| 914 | 933 | log_system.deinit(); | |
| 915 | 934 |
| ... | ... | @@ -919,7 +938,7 @@ const Daemon = struct { | |
| 919 | 938 | // keyword) stays open for the daemon's lifetime, causing | |
| 920 | 939 | // the caller to hang waiting for EOF. | |
| 921 | 940 | { | |
| 922 | - | const devnull = std.posix.open( | |
| 941 | + | const devnull = lib_posix.open( | |
| 923 | 942 | "/dev/null", | |
| 924 | 943 | .{ .ACCMODE = .RDWR }, | |
| 925 | 944 | 0, |
| ... | ... | @@ -928,12 +947,12 @@ const Daemon = struct { | |
| 928 | 947 | return err; | |
| 929 | 948 | }; | |
| 930 | 949 | 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| { | |
| 932 | 951 | std.log.warn("dup2 /dev/null -> {d}: {s}", .{ fd, @errorName(err) }); | |
| 933 | 952 | return err; | |
| 934 | 953 | }; | |
| 935 | 954 | } | |
| 936 | - | if (devnull > 2) posix.close(devnull); | |
| 955 | + | if (devnull > 2) lib_posix.close(devnull); | |
| 937 | 956 | } | |
| 938 | 957 | ||
| 939 | 958 | // Close file descriptors inherited from the parent that the |
| ... | ... | @@ -948,7 +967,7 @@ const Daemon = struct { | |
| 948 | 967 | // Skip server_sock_fd (needed for IPC) and dir.fd (needed to | |
| 949 | 968 | // delete the socket file on shutdown). | |
| 950 | 969 | { | |
| 951 | - | const dir_fd = @as(i32, @intCast(dir.fd)); | |
| 970 | + | const dir_fd = @as(i32, @intCast(dir.handle)); | |
| 952 | 971 | var fd: i32 = 3; | |
| 953 | 972 | while (fd < 64) : (fd += 1) { | |
| 954 | 973 | if (fd == server_sock_fd or fd == dir_fd) continue; |
| ... | ... | @@ -967,14 +986,15 @@ const Daemon = struct { | |
| 967 | 986 | &.{ self.cfg.log_dir, session_log_name }, | |
| 968 | 987 | ); | |
| 969 | 988 | 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); | |
| 971 | 991 | ||
| 972 | 992 | // If spawnPty fails, clean up here. Once it succeeds, | |
| 973 | 993 | // the inner block's defer takes ownership of cleanup to | |
| 974 | 994 | // avoid double-closing server_sock_fd on daemonLoop error. | |
| 975 | 995 | 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 {}; | |
| 978 | 998 | return err; | |
| 979 | 999 | }; | |
| 980 | 1000 |
| ... | ... | @@ -983,23 +1003,23 @@ const Daemon = struct { | |
| 983 | 1003 | // 500ms SIGHUP->SIGKILL grace sleep. Otherwise a `zmx run` | |
| 984 | 1004 | // for the same name issued in that window will hang waiting | |
| 985 | 1005 | // for a connect. | |
| 986 | - | posix.close(server_sock_fd); | |
| 1006 | + | lib_posix.close(server_sock_fd); | |
| 987 | 1007 | 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| { | |
| 989 | 1009 | std.log.warn("failed to delete socket file err={s}", .{@errorName(err)}); | |
| 990 | 1010 | }; | |
| 991 | 1011 | self.handleKill(); | |
| 992 | 1012 | 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); | |
| 995 | 1015 | } | |
| 996 | 1016 | ||
| 997 | 1017 | try daemonLoop(self, server_sock_fd, pty_fd); | |
| 998 | 1018 | std.log.info("daemon loop shutdown", .{}); | |
| 999 | 1019 | return .{ .created = true, .is_daemon = true }; | |
| 1000 | 1020 | } | |
| 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; | |
| 1003 | 1023 | return .{ .created = true, .is_daemon = false }; | |
| 1004 | 1024 | } | |
| 1005 | 1025 |
| ... | ... | @@ -1204,7 +1224,7 @@ const Daemon = struct { | |
| 1204 | 1224 | posix.kill(-self.pid, posix.SIG.HUP) catch |err| { | |
| 1205 | 1225 | std.log.warn("failed to send SIGHUP to pty child err={s}", .{@errorName(err)}); | |
| 1206 | 1226 | }; | |
| 1207 | - | std.Thread.sleep(500 * std.time.ns_per_ms); | |
| 1227 | + | std.Io.sleep(self.io, std.Io.Duration.fromMilliseconds(500), .real) catch unreachable; | |
| 1208 | 1228 | posix.kill(-self.pid, posix.SIG.KILL) catch |err| { | |
| 1209 | 1229 | std.log.warn("failed to send SIGKILL to pty child err={s}", .{@errorName(err)}); | |
| 1210 | 1230 | }; |
| ... | ... | @@ -1265,7 +1285,7 @@ const Daemon = struct { | |
| 1265 | 1285 | payload: []const u8, | |
| 1266 | 1286 | ) !void { | |
| 1267 | 1287 | const format: util.HistoryFormat = if (payload.len > 0) | |
| 1268 | - | std.meta.intToEnum(util.HistoryFormat, payload[0]) catch .plain | |
| 1288 | + | @enumFromInt(payload[0]) | |
| 1269 | 1289 | else | |
| 1270 | 1290 | .plain; | |
| 1271 | 1291 | if (util.serializeTerminal(self.alloc, term, format)) |output| { |
| ... | ... | @@ -1385,6 +1405,7 @@ test "send queues PTY input without changing leader" { | |
| 1385 | 1405 | .leader_client_fd = 42, | |
| 1386 | 1406 | .session_name = "test", | |
| 1387 | 1407 | .socket_path = "", | |
| 1408 | + | .io = std.testing.io, | |
| 1388 | 1409 | .running = true, | |
| 1389 | 1410 | .pid = 0, | |
| 1390 | 1411 | .created_at = 0, |
| ... | ... | @@ -1397,9 +1418,9 @@ test "send queues PTY input without changing leader" { | |
| 1397 | 1418 | try std.testing.expectEqualStrings("hello", daemon.pty_write_buf.items); | |
| 1398 | 1419 | } | |
| 1399 | 1420 | ||
| 1400 | - | fn printVersion(cfg: *Cfg) !void { | |
| 1421 | + | fn printVersion(io: std.Io, cfg: *Cfg) !void { | |
| 1401 | 1422 | var buf: [256]u8 = undefined; | |
| 1402 | - | var w = std.fs.File.stdout().writer(&buf); | |
| 1423 | + | var w = std.Io.File.stdout().writer(io, &buf); | |
| 1403 | 1424 | try w.interface.print( | |
| 1404 | 1425 | "zmx\t\t{s}\nghostty_vt\t{s}\nsocket_dir\t{s}\nlog_dir\t\t{s}\n", | |
| 1405 | 1426 | .{ version, ghostty_version, cfg.socket_dir, cfg.log_dir }, |
| ... | ... | @@ -1407,15 +1428,15 @@ fn printVersion(cfg: *Cfg) !void { | |
| 1407 | 1428 | try w.interface.flush(); | |
| 1408 | 1429 | } | |
| 1409 | 1430 | ||
| 1410 | - | fn printCompletions(shell: completions.Shell) !void { | |
| 1431 | + | fn printCompletions(io: std.Io, shell: completions.Shell) !void { | |
| 1411 | 1432 | const script = shell.getCompletionScript(); | |
| 1412 | 1433 | var buf: [8192]u8 = undefined; | |
| 1413 | - | var w = std.fs.File.stdout().writer(&buf); | |
| 1434 | + | var w = std.Io.File.stdout().writer(io, &buf); | |
| 1414 | 1435 | try w.interface.print("{s}\n", .{script}); | |
| 1415 | 1436 | try w.interface.flush(); | |
| 1416 | 1437 | } | |
| 1417 | 1438 | ||
| 1418 | - | fn help() !void { | |
| 1439 | + | fn help(io: std.Io) !void { | |
| 1419 | 1440 | const help_text = | |
| 1420 | 1441 | \\zmx - session persistence for terminal processes | |
| 1421 | 1442 | \\ |
| ... | ... | @@ -1553,16 +1574,12 @@ fn help() !void { | |
| 1553 | 1574 | \\ | |
| 1554 | 1575 | ; | |
| 1555 | 1576 | var buf: [8192]u8 = undefined; | |
| 1556 | - | var w = std.fs.File.stdout().writer(&buf); | |
| 1577 | + | var w = std.Io.File.stdout().writer(io, &buf); | |
| 1557 | 1578 | try w.interface.print(help_text, .{}); | |
| 1558 | 1579 | try w.interface.flush(); | |
| 1559 | 1580 | } | |
| 1560 | 1581 | ||
| 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 { | |
| 1566 | 1583 | var poll_fds = try std.ArrayList(posix.pollfd).initCapacity(alloc, 4); | |
| 1567 | 1584 | defer poll_fds.deinit(alloc); | |
| 1568 | 1585 |
| ... | ... | @@ -1625,7 +1642,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) | |
| 1625 | 1642 | switch (msg.header.tag) { | |
| 1626 | 1643 | .Ack => { | |
| 1627 | 1644 | 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: { | |
| 1629 | 1646 | if (err == error.WouldBlock) break :blk 0; | |
| 1630 | 1647 | return err; | |
| 1631 | 1648 | }; |
| ... | ... | @@ -1687,7 +1704,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) | |
| 1687 | 1704 | if (task_complete_code) |exit_code| { | |
| 1688 | 1705 | // Flush any remaining output before returning | |
| 1689 | 1706 | 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| { | |
| 1691 | 1708 | if (err == error.WouldBlock) break :flush_loop; | |
| 1692 | 1709 | return err; | |
| 1693 | 1710 | }; |
| ... | ... | @@ -1697,7 +1714,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) | |
| 1697 | 1714 | } | |
| 1698 | 1715 | ||
| 1699 | 1716 | 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: { | |
| 1701 | 1718 | if (err == error.WouldBlock) break :blk 0; | |
| 1702 | 1719 | return err; | |
| 1703 | 1720 | }; |
| ... | ... | @@ -1715,17 +1732,13 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) | |
| 1715 | 1732 | } | |
| 1716 | 1733 | } | |
| 1717 | 1734 | ||
| 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 { | |
| 1723 | 1736 | 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); | |
| 1725 | 1738 | const stdout = &stdout_writer.interface; | |
| 1726 | 1739 | ||
| 1727 | 1740 | 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); | |
| 1729 | 1742 | const stderr = &stderr_writer.interface; | |
| 1730 | 1743 | ||
| 1731 | 1744 | // 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 { | |
| 1735 | 1748 | var zero_match_iters: u32 = 0; | |
| 1736 | 1749 | ||
| 1737 | 1750 | var agg_exit_code: u8 = 0; | |
| 1738 | - | var last_print: i64 = 0; | |
| 1751 | + | var last_print: i96 = 0; | |
| 1739 | 1752 | var prev_done: i32 = 0; | |
| 1740 | 1753 | while (true) { | |
| 1741 | 1754 | 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); | |
| 1743 | 1756 | var total: i32 = 0; | |
| 1744 | 1757 | var done: i32 = 0; | |
| 1745 | 1758 |
| ... | ... | @@ -1763,7 +1776,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void { | |
| 1763 | 1776 | // waiting". Count it as done+failed so wait terminates. | |
| 1764 | 1777 | try stderr.print( | |
| 1765 | 1778 | "[{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" }, | |
| 1767 | 1780 | ); | |
| 1768 | 1781 | try stderr.flush(); | |
| 1769 | 1782 | agg_exit_code = 1; |
| ... | ... | @@ -1771,7 +1784,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void { | |
| 1771 | 1784 | continue; | |
| 1772 | 1785 | } | |
| 1773 | 1786 | if (session.task_ended_at == 0) { | |
| 1774 | - | const now = std.time.timestamp(); | |
| 1787 | + | const now = std.Io.Timestamp.now(io, .real).nanoseconds; | |
| 1775 | 1788 | if (now - last_print >= 5) { | |
| 1776 | 1789 | try stdout.print( | |
| 1777 | 1790 | "[{d}] waiting task={s}\n", |
| ... | ... | @@ -1834,7 +1847,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void { | |
| 1834 | 1847 | } | |
| 1835 | 1848 | ||
| 1836 | 1849 | 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; | |
| 1838 | 1851 | } | |
| 1839 | 1852 | ||
| 1840 | 1853 | if (agg_exit_code == 0) { |
| ... | ... | @@ -1844,7 +1857,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void { | |
| 1844 | 1857 | } | |
| 1845 | 1858 | try stdout.flush(); | |
| 1846 | 1859 | ||
| 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); | |
| 1848 | 1861 | for (sessions.items) |session| { | |
| 1849 | 1862 | var found = false; | |
| 1850 | 1863 | for (matchers.items) |m| { |
| ... | ... | @@ -1866,7 +1879,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void { | |
| 1866 | 1879 | ||
| 1867 | 1880 | // Fetch and print the last 20 lines of history for debugging | |
| 1868 | 1881 | 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; | |
| 1870 | 1883 | if (history_text) |text| { | |
| 1871 | 1884 | defer alloc.free(text); | |
| 1872 | 1885 | 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 { | |
| 1897 | 1910 | std.process.exit(agg_exit_code); | |
| 1898 | 1911 | } | |
| 1899 | 1912 | ||
| 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 { | |
| 1905 | 1914 | const current_session = socket.getSeshNameFromEnv(); | |
| 1906 | 1915 | 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); | |
| 1909 | 1918 | defer { | |
| 1910 | 1919 | for (sessions.items) |session| { | |
| 1911 | 1920 | session.deinit(alloc); |
| ... | ... | @@ -1916,7 +1925,7 @@ fn list(cfg: *Cfg, short: bool) !void { | |
| 1916 | 1925 | if (sessions.items.len == 0) { | |
| 1917 | 1926 | if (short) return; | |
| 1918 | 1927 | var errbuf: [4096]u8 = undefined; | |
| 1919 | - | var stderr = std.fs.File.stderr().writer(&errbuf); | |
| 1928 | + | var stderr = std.Io.File.stderr().writer(io, &errbuf); | |
| 1920 | 1929 | try stderr.interface.print("no sessions found in {s}\n", .{cfg.socket_dir}); | |
| 1921 | 1930 | try stderr.interface.flush(); | |
| 1922 | 1931 | return; |
| ... | ... | @@ -1936,10 +1945,7 @@ fn list(cfg: *Cfg, short: bool) !void { | |
| 1936 | 1945 | } | |
| 1937 | 1946 | } | |
| 1938 | 1947 | ||
| 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 { | |
| 1943 | 1949 | const session_name = socket.getSeshNameFromEnv(); | |
| 1944 | 1950 | if (session_name.len == 0) { | |
| 1945 | 1951 | std.log.err("ZMX_SESSION env var not found: are you inside a zmx session?", .{}); |
| ... | ... | @@ -1947,45 +1953,41 @@ fn detachAll(cfg: *Cfg) !void { | |
| 1947 | 1953 | } | |
| 1948 | 1954 | std.log.info("detach all session={s}", .{session_name}); | |
| 1949 | 1955 | ||
| 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); | |
| 1952 | 1958 | ||
| 1953 | 1959 | 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), | |
| 1955 | 1961 | error.OutOfMemory => return err, | |
| 1956 | 1962 | }; | |
| 1957 | 1963 | defer alloc.free(socket_path); | |
| 1958 | 1964 | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 1959 | 1965 | 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); | |
| 1961 | 1967 | return; | |
| 1962 | 1968 | }; | |
| 1963 | - | defer posix.close(fd); | |
| 1969 | + | defer lib_posix.close(fd); | |
| 1964 | 1970 | ipc.send(fd, .DetachAll, "") catch |err| switch (err) { | |
| 1965 | 1971 | error.BrokenPipe, error.ConnectionResetByPeer => return, | |
| 1966 | 1972 | else => return err, | |
| 1967 | 1973 | }; | |
| 1968 | 1974 | } | |
| 1969 | 1975 | ||
| 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 { | |
| 1971 | 1977 | std.log.info("kill session={s}", .{session_name}); | |
| 1972 | - | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 1973 | - | defer _ = gpa.deinit(); | |
| 1974 | - | const alloc = gpa.allocator(); | |
| 1975 | - | ||
| 1976 | 1978 | 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), | |
| 1978 | 1980 | error.OutOfMemory => return err, | |
| 1979 | 1981 | }; | |
| 1980 | 1982 | defer alloc.free(socket_path); | |
| 1981 | 1983 | ||
| 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); | |
| 1984 | 1986 | ||
| 1985 | - | const exists = try socket.sessionExists(dir, session_name); | |
| 1987 | + | const exists = try socket.sessionExists(io, dir, session_name); | |
| 1986 | 1988 | if (!exists) { | |
| 1987 | 1989 | var buf: [4096]u8 = undefined; | |
| 1988 | - | var w = std.fs.File.stderr().writer(&buf); | |
| 1990 | + | var w = std.Io.File.stderr().writer(io, &buf); | |
| 1989 | 1991 | w.interface.print("error: session \"{s}\" does not exist\n", .{session_name}) catch {}; | |
| 1990 | 1992 | w.interface.flush() catch {}; | |
| 1991 | 1993 | return error.SessionNotFound; |
| ... | ... | @@ -1993,9 +1995,9 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void { | |
| 1993 | 1995 | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 1994 | 1996 | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 1995 | 1997 | var buf: [4096]u8 = undefined; | |
| 1996 | - | var w = std.fs.File.stdout().writer(&buf); | |
| 1998 | + | var w = std.Io.File.stdout().writer(io, &buf); | |
| 1997 | 1999 | if (force or err == error.ConnectionRefused) { | |
| 1998 | - | socket.cleanupStaleSocket(dir, session_name); | |
| 2000 | + | socket.cleanupStaleSocket(io, dir, session_name); | |
| 1999 | 2001 | w.interface.print("cleaned up stale session {s}\n", .{session_name}) catch {}; | |
| 2000 | 2002 | } else { | |
| 2001 | 2003 | w.interface.print( |
| ... | ... | @@ -2007,7 +2009,7 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void { | |
| 2007 | 2009 | return; | |
| 2008 | 2010 | }; | |
| 2009 | 2011 | ||
| 2010 | - | defer posix.close(fd); | |
| 2012 | + | defer lib_posix.close(fd); | |
| 2011 | 2013 | ipc.send(fd, .Kill, "") catch |err| switch (err) { | |
| 2012 | 2014 | error.BrokenPipe, error.ConnectionResetByPeer => return, | |
| 2013 | 2015 | else => return err, |
| ... | ... | @@ -2025,14 +2027,14 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void { | |
| 2025 | 2027 | } | |
| 2026 | 2028 | ||
| 2027 | 2029 | var buf: [100]u8 = undefined; | |
| 2028 | - | var w = std.fs.File.stdout().writer(&buf); | |
| 2030 | + | var w = std.Io.File.stdout().writer(io, &buf); | |
| 2029 | 2031 | try w.interface.print("killed session {s}\n", .{session_name}); | |
| 2030 | 2032 | try w.interface.flush(); | |
| 2031 | 2033 | } | |
| 2032 | 2034 | ||
| 2033 | - | fn printLabelError(session_name: []const u8, err: anyerror) noreturn { | |
| 2035 | + | fn printLabelError(io: std.Io, session_name: []const u8, err: anyerror) noreturn { | |
| 2034 | 2036 | var buf: [4096]u8 = undefined; | |
| 2035 | - | var w = std.fs.File.stderr().writer(&buf); | |
| 2037 | + | var w = std.Io.File.stderr().writer(io, &buf); | |
| 2036 | 2038 | switch (err) { | |
| 2037 | 2039 | error.Timeout => w.interface.print( | |
| 2038 | 2040 | "error: session \"{s}\" does not support labels (daemon too old?)\n", |
| ... | ... | @@ -2051,25 +2053,22 @@ fn printLabelError(session_name: []const u8, err: anyerror) noreturn { | |
| 2051 | 2053 | std.process.exit(1); | |
| 2052 | 2054 | } | |
| 2053 | 2055 | ||
| 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 { | |
| 2055 | 2057 | std.log.info("label get session={s}", .{session_name}); | |
| 2056 | - | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 2057 | - | defer _ = gpa.deinit(); | |
| 2058 | - | const alloc = gpa.allocator(); | |
| 2059 | 2058 | ||
| 2060 | 2059 | 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), | |
| 2062 | 2061 | error.OutOfMemory => return err, | |
| 2063 | 2062 | }; | |
| 2064 | 2063 | defer alloc.free(socket_path); | |
| 2065 | 2064 | ||
| 2066 | 2065 | const payload = ipc.roundTripForTag(alloc, socket_path, .LabelGet, "", .LabelData) catch |err| { | |
| 2067 | - | printLabelError(session_name, err); | |
| 2066 | + | printLabelError(io, session_name, err); | |
| 2068 | 2067 | }; | |
| 2069 | 2068 | defer alloc.free(payload); | |
| 2070 | 2069 | ||
| 2071 | 2070 | var buf: [4096]u8 = undefined; | |
| 2072 | - | var stdout = std.fs.File.stdout().writer(&buf); | |
| 2071 | + | var stdout = std.Io.File.stdout().writer(io, &buf); | |
| 2073 | 2072 | if (single_kv.len == 0) { | |
| 2074 | 2073 | try stdout.interface.print("{s}", .{payload}); | |
| 2075 | 2074 | try stdout.interface.flush(); |
| ... | ... | @@ -2081,17 +2080,14 @@ fn labelGet(cfg: *Cfg, session_name: []const u8, single_kv: []const u8) !void { | |
| 2081 | 2080 | try stdout.interface.flush(); | |
| 2082 | 2081 | } | |
| 2083 | 2082 | ||
| 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 { | |
| 2085 | 2084 | std.log.info("label set session={s}", .{session_name}); | |
| 2086 | - | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 2087 | - | defer _ = gpa.deinit(); | |
| 2088 | - | const alloc = gpa.allocator(); | |
| 2089 | 2085 | ||
| 2090 | 2086 | var kvs = label.LabelIterator.init(labels); | |
| 2091 | 2087 | while (kvs.next()) |kv| { | |
| 2092 | 2088 | label.assertLabel(kv.key, kv.value) catch |err| { | |
| 2093 | 2089 | var buf: [4096]u8 = undefined; | |
| 2094 | - | var w = std.fs.File.stderr().writer(&buf); | |
| 2090 | + | var w = std.Io.File.stderr().writer(io, &buf); | |
| 2095 | 2091 | const msg = "error: key-value kvs can only contain [a-z, A-Z, 0-9, -_.] characters"; | |
| 2096 | 2092 | switch (err) { | |
| 2097 | 2093 | error.LabelKeyEmpty => { |
| ... | ... | @@ -2113,30 +2109,27 @@ fn labelSet(cfg: *Cfg, session_name: []const u8, labels: []const u8) !void { | |
| 2113 | 2109 | } | |
| 2114 | 2110 | ||
| 2115 | 2111 | 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), | |
| 2117 | 2113 | error.OutOfMemory => return err, | |
| 2118 | 2114 | }; | |
| 2119 | 2115 | defer alloc.free(socket_path); | |
| 2120 | 2116 | ||
| 2121 | 2117 | _ = ipc.roundTripForTag(alloc, socket_path, .LabelSet, labels, .Ack) catch |err| { | |
| 2122 | - | printLabelError(session_name, err); | |
| 2118 | + | printLabelError(io, session_name, err); | |
| 2123 | 2119 | }; | |
| 2124 | 2120 | } | |
| 2125 | 2121 | ||
| 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 { | |
| 2127 | 2123 | std.log.info("label clear session={s}", .{session_name}); | |
| 2128 | - | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 2129 | - | defer _ = gpa.deinit(); | |
| 2130 | - | const alloc = gpa.allocator(); | |
| 2131 | 2124 | ||
| 2132 | 2125 | 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), | |
| 2134 | 2127 | error.OutOfMemory => return err, | |
| 2135 | 2128 | }; | |
| 2136 | 2129 | defer alloc.free(socket_path); | |
| 2137 | 2130 | ||
| 2138 | 2131 | _ = ipc.roundTripForTag(alloc, socket_path, .LabelClear, "", .Ack) catch |err| { | |
| 2139 | - | printLabelError(session_name, err); | |
| 2132 | + | printLabelError(io, session_name, err); | |
| 2140 | 2133 | }; | |
| 2141 | 2134 | } | |
| 2142 | 2135 |
| ... | ... | @@ -2144,32 +2137,33 @@ fn labelClear(cfg: *Cfg, session_name: []const u8) !void { | |
| 2144 | 2137 | /// string. Caller owns the returned memory and must free it. | |
| 2145 | 2138 | fn fetchHistory( | |
| 2146 | 2139 | alloc: std.mem.Allocator, | |
| 2140 | + | io: std.Io, | |
| 2147 | 2141 | cfg: *Cfg, | |
| 2148 | 2142 | session_name: []const u8, | |
| 2149 | 2143 | ) ![]const u8 { | |
| 2150 | 2144 | std.log.info("fetch history session={s}", .{session_name}); | |
| 2151 | 2145 | const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) { | |
| 2152 | 2146 | error.NameTooLong => { | |
| 2153 | - | socket.printSessionNameTooLong(session_name, cfg.socket_dir); | |
| 2147 | + | socket.printSessionNameTooLong(io, session_name, cfg.socket_dir); | |
| 2154 | 2148 | return error.NameTooLong; | |
| 2155 | 2149 | }, | |
| 2156 | 2150 | error.OutOfMemory => return err, | |
| 2157 | 2151 | }; | |
| 2158 | 2152 | defer alloc.free(socket_path); | |
| 2159 | 2153 | ||
| 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); | |
| 2162 | 2156 | ||
| 2163 | - | const exists = try socket.sessionExists(dir, session_name); | |
| 2157 | + | const exists = try socket.sessionExists(io, dir, session_name); | |
| 2164 | 2158 | if (!exists) { | |
| 2165 | 2159 | return error.SessionNotFound; | |
| 2166 | 2160 | } | |
| 2167 | 2161 | ||
| 2168 | 2162 | 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); | |
| 2170 | 2164 | return err; | |
| 2171 | 2165 | }; | |
| 2172 | - | defer posix.close(fd); | |
| 2166 | + | defer lib_posix.close(fd); | |
| 2173 | 2167 | ||
| 2174 | 2168 | const format_byte: u8 = @intFromEnum(util.HistoryFormat.plain); | |
| 2175 | 2169 | const payload = [_]u8{format_byte}; |
| ... | ... | @@ -2205,35 +2199,32 @@ fn fetchHistory( | |
| 2205 | 2199 | return error.NoHistoryResponse; | |
| 2206 | 2200 | } | |
| 2207 | 2201 | ||
| 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 { | |
| 2209 | 2203 | std.log.info("history session={s}", .{session_name}); | |
| 2210 | - | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 2211 | - | defer _ = gpa.deinit(); | |
| 2212 | - | const alloc = gpa.allocator(); | |
| 2213 | 2204 | ||
| 2214 | 2205 | 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), | |
| 2216 | 2207 | error.OutOfMemory => return err, | |
| 2217 | 2208 | }; | |
| 2218 | 2209 | defer alloc.free(socket_path); | |
| 2219 | 2210 | ||
| 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); | |
| 2222 | 2213 | ||
| 2223 | - | const exists = try socket.sessionExists(dir, session_name); | |
| 2214 | + | const exists = try socket.sessionExists(io, dir, session_name); | |
| 2224 | 2215 | if (!exists) { | |
| 2225 | 2216 | var buf: [4096]u8 = undefined; | |
| 2226 | - | var w = std.fs.File.stderr().writer(&buf); | |
| 2217 | + | var w = std.Io.File.stderr().writer(io, &buf); | |
| 2227 | 2218 | w.interface.print("error: session \"{s}\" does not exist\n", .{session_name}) catch {}; | |
| 2228 | 2219 | w.interface.flush() catch {}; | |
| 2229 | 2220 | return error.SessionNotFound; | |
| 2230 | 2221 | } | |
| 2231 | 2222 | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 2232 | 2223 | 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); | |
| 2234 | 2225 | return; | |
| 2235 | 2226 | }; | |
| 2236 | - | defer posix.close(fd); | |
| 2227 | + | defer lib_posix.close(fd); | |
| 2237 | 2228 | ||
| 2238 | 2229 | const format_byte = [_]u8{@intFromEnum(format)}; | |
| 2239 | 2230 | 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 | |
| 2257 | 2248 | ||
| 2258 | 2249 | while (sb.next()) |msg| { | |
| 2259 | 2250 | 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; | |
| 2261 | 2252 | return; | |
| 2262 | 2253 | } | |
| 2263 | 2254 | } |
| ... | ... | @@ -2271,28 +2262,28 @@ fn switchSesh(daemon: *Daemon, current_sesh: []const u8) !void { | |
| 2271 | 2262 | std.log.info("switch session cur={s} next={s}", .{ current_sesh, next_session }); | |
| 2272 | 2263 | ||
| 2273 | 2264 | 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), | |
| 2275 | 2266 | error.OutOfMemory => return err, | |
| 2276 | 2267 | }; | |
| 2277 | 2268 | defer daemon.alloc.free(socket_path); | |
| 2278 | 2269 | ||
| 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); | |
| 2281 | 2272 | ||
| 2282 | - | const exists = try socket.sessionExists(dir, current_sesh); | |
| 2273 | + | const exists = try socket.sessionExists(daemon.io, dir, current_sesh); | |
| 2283 | 2274 | if (!exists) { | |
| 2284 | 2275 | 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); | |
| 2286 | 2277 | w.interface.print("error: session \"{s}\" does not exist\n", .{current_sesh}) catch {}; | |
| 2287 | 2278 | w.interface.flush() catch {}; | |
| 2288 | 2279 | return error.SessionNotFound; | |
| 2289 | 2280 | } | |
| 2290 | 2281 | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 2291 | 2282 | 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); | |
| 2293 | 2284 | return; | |
| 2294 | 2285 | }; | |
| 2295 | - | defer posix.close(fd); | |
| 2286 | + | defer lib_posix.close(fd); | |
| 2296 | 2287 | ||
| 2297 | 2288 | ipc.send(fd, .Switch, next_session) catch |err| switch (err) { | |
| 2298 | 2289 | error.BrokenPipe, error.ConnectionResetByPeer => return, |
| ... | ... | @@ -2331,7 +2322,7 @@ fn attach(daemon: *Daemon) !void { | |
| 2331 | 2322 | } | |
| 2332 | 2323 | // Reset terminal modes on detach | |
| 2333 | 2324 | const restore_seq = "\x1bc"; | |
| 2334 | - | _ = posix.write(posix.STDOUT_FILENO, restore_seq) catch {}; | |
| 2325 | + | _ = lib_posix.write(posix.STDOUT_FILENO, restore_seq) catch {}; | |
| 2335 | 2326 | } | |
| 2336 | 2327 | ||
| 2337 | 2328 | if (stdin_is_tty) { |
| ... | ... | @@ -2355,7 +2346,7 @@ fn attach(daemon: *Daemon) !void { | |
| 2355 | 2346 | // Clear screen before attaching. This provides a clean slate before | |
| 2356 | 2347 | // the session restore. | |
| 2357 | 2348 | 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); | |
| 2359 | 2350 | ||
| 2360 | 2351 | const looper = try clientLoop(client_sock); | |
| 2361 | 2352 | switch (looper.kind) { |
| ... | ... | @@ -2363,13 +2354,15 @@ fn attach(daemon: *Daemon) !void { | |
| 2363 | 2354 | .switch_session => { | |
| 2364 | 2355 | if (looper.session_name) |session_name| { | |
| 2365 | 2356 | 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]; | |
| 2367 | 2359 | const target_path = socket.getSocketPath( | |
| 2368 | 2360 | daemon.alloc, | |
| 2369 | 2361 | daemon.cfg.socket_dir, | |
| 2370 | 2362 | session_name, | |
| 2371 | 2363 | ) catch |err| switch (err) { | |
| 2372 | 2364 | error.NameTooLong => return socket.printSessionNameTooLong( | |
| 2365 | + | daemon.io, | |
| 2373 | 2366 | session_name, | |
| 2374 | 2367 | daemon.cfg.socket_dir, | |
| 2375 | 2368 | ), |
| ... | ... | @@ -2378,6 +2371,7 @@ fn attach(daemon: *Daemon) !void { | |
| 2378 | 2371 | ||
| 2379 | 2372 | const clients = try std.ArrayList(*Client).initCapacity(daemon.alloc, 10); | |
| 2380 | 2373 | var target_daemon = Daemon{ | |
| 2374 | + | .io = daemon.io, | |
| 2381 | 2375 | .running = true, | |
| 2382 | 2376 | .cfg = daemon.cfg, | |
| 2383 | 2377 | .alloc = daemon.alloc, |
| ... | ... | @@ -2386,7 +2380,7 @@ fn attach(daemon: *Daemon) !void { | |
| 2386 | 2380 | .socket_path = target_path, | |
| 2387 | 2381 | .pid = undefined, | |
| 2388 | 2382 | .cwd = cwd, | |
| 2389 | - | .created_at = @intCast(std.time.timestamp()), | |
| 2383 | + | .created_at = @intCast(std.Io.Timestamp.now(daemon.io, .real).nanoseconds), | |
| 2390 | 2384 | .leader_client_fd = null, | |
| 2391 | 2385 | }; | |
| 2392 | 2386 | return attach(&target_daemon); |
| ... | ... | @@ -2397,7 +2391,7 @@ fn attach(daemon: *Daemon) !void { | |
| 2397 | 2391 | ||
| 2398 | 2392 | fn writeFile(daemon: *Daemon, file_path: []const u8) !void { | |
| 2399 | 2393 | 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); | |
| 2401 | 2395 | const sesh_result = try daemon.ensureSession(); | |
| 2402 | 2396 | if (sesh_result.is_daemon) return; | |
| 2403 | 2397 |
| ... | ... | @@ -2425,18 +2419,19 @@ fn writeFile(daemon: *Daemon, file_path: []const u8) !void { | |
| 2425 | 2419 | daemon.session_name, | |
| 2426 | 2420 | ) catch |err| switch (err) { | |
| 2427 | 2421 | error.NameTooLong => return socket.printSessionNameTooLong( | |
| 2422 | + | daemon.io, | |
| 2428 | 2423 | daemon.session_name, | |
| 2429 | 2424 | daemon.cfg.socket_dir, | |
| 2430 | 2425 | ), | |
| 2431 | 2426 | error.OutOfMemory => return err, | |
| 2432 | 2427 | }; | |
| 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); | |
| 2435 | 2430 | ||
| 2436 | 2431 | const result = ipc.probeSession(daemon.alloc, socket_path) catch |err| { | |
| 2437 | 2432 | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 2438 | 2433 | if (err == error.ConnectionRefused) { | |
| 2439 | - | socket.cleanupStaleSocket(dir, daemon.session_name); | |
| 2434 | + | socket.cleanupStaleSocket(daemon.io, dir, daemon.session_name); | |
| 2440 | 2435 | w.interface.print("cleaned up stale session {s}\n", .{daemon.session_name}) catch {}; | |
| 2441 | 2436 | } else { | |
| 2442 | 2437 | w.interface.print( |
| ... | ... | @@ -2483,11 +2478,10 @@ fn writeFile(daemon: *Daemon, file_path: []const u8) !void { | |
| 2483 | 2478 | return error.NoAckReceived; | |
| 2484 | 2479 | } | |
| 2485 | 2480 | ||
| 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 { | |
| 2487 | 2482 | std.log.info("send session={s}", .{session_name}); | |
| 2488 | - | const alloc = std.heap.c_allocator; | |
| 2489 | 2483 | var buf: [4096]u8 = undefined; | |
| 2490 | - | var w = std.fs.File.stdout().writer(&buf); | |
| 2484 | + | var w = std.Io.File.stdout().writer(io, &buf); | |
| 2491 | 2485 | ||
| 2492 | 2486 | var payload = std.ArrayList(u8).empty; | |
| 2493 | 2487 | defer payload.deinit(alloc); |
| ... | ... | @@ -2499,16 +2493,16 @@ fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts | |
| 2499 | 2493 | } | |
| 2500 | 2494 | } else { | |
| 2501 | 2495 | // 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)) { | |
| 2504 | 2501 | 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]); | |
| 2512 | 2506 | } | |
| 2513 | 2507 | // Strip trailing newline from piped input; the caller is | |
| 2514 | 2508 | // 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 | |
| 2521 | 2515 | ||
| 2522 | 2516 | if (payload.items.len == 0) return error.TextRequired; | |
| 2523 | 2517 | ||
| 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); | |
| 2526 | 2520 | ||
| 2527 | 2521 | const probe_result = ipc.probeSession(alloc, socket_path) catch |err| { | |
| 2528 | 2522 | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 2529 | 2523 | if (err == error.ConnectionRefused) { | |
| 2530 | - | socket.cleanupStaleSocket(dir, session_name); | |
| 2524 | + | socket.cleanupStaleSocket(io, dir, session_name); | |
| 2531 | 2525 | try w.interface.print("cleaned up stale session {s}\n", .{session_name}); | |
| 2532 | 2526 | } else { | |
| 2533 | 2527 | try w.interface.print( |
| ... | ... | @@ -2549,7 +2543,7 @@ fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts | |
| 2549 | 2543 | fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void { | |
| 2550 | 2544 | const alloc = daemon.alloc; | |
| 2551 | 2545 | 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); | |
| 2553 | 2547 | ||
| 2554 | 2548 | var cmd_to_send: ?[]const u8 = null; | |
| 2555 | 2549 | var allocated_cmd: ?[]u8 = null; |
| ... | ... | @@ -2587,19 +2581,19 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void { | |
| 2587 | 2581 | cmd_to_send = try cmd_list.toOwnedSlice(alloc); | |
| 2588 | 2582 | allocated_cmd = @constCast(cmd_to_send.?); | |
| 2589 | 2583 | } 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)) { | |
| 2595 | 2592 | 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]); | |
| 2603 | 2597 | } | |
| 2604 | 2598 | ||
| 2605 | 2599 | if (stdin_buf.items.len > 0) { |
| ... | ... | @@ -2615,6 +2609,35 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void { | |
| 2615 | 2609 | allocated_cmd = @constCast(cmd_to_send.?); | |
| 2616 | 2610 | } | |
| 2617 | 2611 | } | |
| 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 | + | // } | |
| 2618 | 2641 | } | |
| 2619 | 2642 | ||
| 2620 | 2643 | if (cmd_to_send == null) { |
| ... | ... | @@ -2625,7 +2648,7 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void { | |
| 2625 | 2648 | std.log.err("session not ready: {s}", .{@errorName(err)}); | |
| 2626 | 2649 | return error.SessionNotReady; | |
| 2627 | 2650 | }; | |
| 2628 | - | defer posix.close(client_sock); | |
| 2651 | + | defer lib_posix.close(client_sock); | |
| 2629 | 2652 | ||
| 2630 | 2653 | var fds = try std.ArrayList(i32).initCapacity(alloc, 1); | |
| 2631 | 2654 | defer fds.deinit(alloc); |
| ... | ... | @@ -2636,8 +2659,8 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void { | |
| 2636 | 2659 | else => return err, | |
| 2637 | 2660 | }; | |
| 2638 | 2661 | ||
| 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); | |
| 2641 | 2664 | } | |
| 2642 | 2665 | ||
| 2643 | 2666 | const ClientResult = struct { |
| ... | ... | @@ -2654,15 +2677,15 @@ fn clientLoop(client_sock_fd: i32) !ClientResult { | |
| 2654 | 2677 | std.log.info("client loop fd={d}", .{client_sock_fd}); | |
| 2655 | 2678 | // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking | |
| 2656 | 2679 | const alloc = std.heap.c_allocator; | |
| 2657 | - | defer posix.close(client_sock_fd); | |
| 2680 | + | defer lib_posix.close(client_sock_fd); | |
| 2658 | 2681 | ||
| 2659 | 2682 | try openSignalPipe(); | |
| 2660 | - | installWakeHandler(posix.SIG.WINCH); | |
| 2683 | + | installWakeHandler(@intFromEnum(posix.SIG.WINCH)); | |
| 2661 | 2684 | ||
| 2662 | 2685 | // 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); | |
| 2664 | 2687 | 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); | |
| 2666 | 2689 | ||
| 2667 | 2690 | // Buffer for outgoing socket writes | |
| 2668 | 2691 | var sock_write_buf = try std.ArrayList(u8).initCapacity(alloc, 4096); |
| ... | ... | @@ -2686,9 +2709,9 @@ fn clientLoop(client_sock_fd: i32) !ClientResult { | |
| 2686 | 2709 | // Make stdin non-blocking. O_NONBLOCK is set on the open file description, | |
| 2687 | 2710 | // which is shared with the parent shell; restore on exit to avoid | |
| 2688 | 2711 | // 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 {}; | |
| 2692 | 2715 | ||
| 2693 | 2716 | while (true) { | |
| 2694 | 2717 | poll_fds.clearRetainingCapacity(); |
| ... | ... | @@ -2800,7 +2823,7 @@ fn clientLoop(client_sock_fd: i32) !ClientResult { | |
| 2800 | 2823 | // Handle socket write (flush buffered messages to daemon) | |
| 2801 | 2824 | if (poll_fds.items[1].revents & posix.POLL.OUT != 0) { | |
| 2802 | 2825 | 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: { | |
| 2804 | 2827 | if (err == error.WouldBlock) break :blk 0; | |
| 2805 | 2828 | if (err == error.ConnectionResetByPeer or err == error.BrokenPipe) { | |
| 2806 | 2829 | std.log.info("connection reset or broken pipe", .{}); |
| ... | ... | @@ -2815,7 +2838,7 @@ fn clientLoop(client_sock_fd: i32) !ClientResult { | |
| 2815 | 2838 | } | |
| 2816 | 2839 | ||
| 2817 | 2840 | 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: { | |
| 2819 | 2842 | if (err == error.WouldBlock) break :blk 0; | |
| 2820 | 2843 | return err; | |
| 2821 | 2844 | }; |
| ... | ... | @@ -2837,12 +2860,12 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void { | |
| 2837 | 2860 | std.log.info("daemon started session={s} pty_fd={d}", .{ daemon.session_name, pty_fd }); | |
| 2838 | 2861 | daemon.pty_fd = pty_fd; | |
| 2839 | 2862 | try openSignalPipe(); | |
| 2840 | - | installWakeHandler(posix.SIG.TERM); | |
| 2863 | + | installWakeHandler(@intFromEnum(lib_posix.SIG.TERM)); | |
| 2841 | 2864 | var poll_fds = try std.ArrayList(posix.pollfd).initCapacity(daemon.alloc, 8); | |
| 2842 | 2865 | defer poll_fds.deinit(daemon.alloc); | |
| 2843 | 2866 | ||
| 2844 | 2867 | 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, .{ | |
| 2846 | 2869 | .cols = init_size.cols, | |
| 2847 | 2870 | .rows = init_size.rows, | |
| 2848 | 2871 | .max_scrollback = daemon.cfg.max_scrollback, |
| ... | ... | @@ -2905,7 +2928,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void { | |
| 2905 | 2928 | std.log.err("server socket error revents={d}", .{poll_fds.items[0].revents}); | |
| 2906 | 2929 | break :daemon_loop; | |
| 2907 | 2930 | } 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( | |
| 2909 | 2932 | server_sock_fd, | |
| 2910 | 2933 | null, | |
| 2911 | 2934 | null, |
| ... | ... | @@ -2979,7 +3002,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void { | |
| 2979 | 3002 | ||
| 2980 | 3003 | if (util.findTaskExitMarker(scan_buf[0..scan_len])) |exit_code| { | |
| 2981 | 3004 | 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); | |
| 2983 | 3006 | ||
| 2984 | 3007 | std.log.info("task completed exit_code={d}", .{exit_code}); | |
| 2985 | 3008 |
| ... | ... | @@ -3018,7 +3041,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void { | |
| 3018 | 3041 | ||
| 3019 | 3042 | if (poll_fds.items[1].revents & posix.POLL.OUT != 0) { | |
| 3020 | 3043 | 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| { | |
| 3022 | 3045 | if (err != error.WouldBlock) { | |
| 3023 | 3046 | std.log.warn("pty write failed: {s}", .{@errorName(err)}); | |
| 3024 | 3047 | daemon.pty_write_buf.clearRetainingCapacity(); |
| ... | ... | @@ -3102,7 +3125,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void { | |
| 3102 | 3125 | ||
| 3103 | 3126 | if (revents & posix.POLL.OUT != 0) { | |
| 3104 | 3127 | // 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: { | |
| 3106 | 3129 | if (err == error.WouldBlock) break :blk 0; | |
| 3107 | 3130 | // Error on write, close client | |
| 3108 | 3131 | const last = daemon.closeClient(client, i, false); |
| ... | ... | @@ -3127,7 +3150,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void { | |
| 3127 | 3150 | } | |
| 3128 | 3151 | } | |
| 3129 | 3152 | ||
| 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 { | |
| 3131 | 3154 | const saved = std.c._errno().*; | |
| 3132 | 3155 | _ = std.c.write(sig_pipe[1], "x", 1); | |
| 3133 | 3156 | std.c._errno().* = saved; |
| ... | ... | @@ -3142,7 +3165,7 @@ fn installWakeHandler(sig: u6) void { | |
| 3142 | 3165 | .mask = posix.sigemptyset(), | |
| 3143 | 3166 | .flags = posix.SA.SIGINFO, | |
| 3144 | 3167 | }; | |
| 3145 | - | posix.sigaction(sig, &act, null); | |
| 3168 | + | posix.sigaction(@as(posix.SIG, @enumFromInt(sig)), &act, null); | |
| 3146 | 3169 | } | |
| 3147 | 3170 | ||
| 3148 | 3171 | 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 @@ | |
| 1 | 1 | const std = @import("std"); | |
| 2 | 2 | const posix = std.posix; | |
| 3 | + | const lib_posix = @import("posix.zig"); | |
| 3 | 4 | ||
| 4 | 5 | pub fn getSeshPrefix() []const u8 { | |
| 5 | - | return std.posix.getenv("ZMX_SESSION_PREFIX") orelse ""; | |
| 6 | + | return lib_posix.getenv("ZMX_SESSION_PREFIX") orelse ""; | |
| 6 | 7 | } | |
| 7 | 8 | ||
| 8 | 9 | pub fn getSeshNameFromEnv() []const u8 { | |
| 9 | - | return std.posix.getenv("ZMX_SESSION") orelse ""; | |
| 10 | + | return lib_posix.getenv("ZMX_SESSION") orelse ""; | |
| 10 | 11 | } | |
| 11 | 12 | ||
| 12 | 13 | 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 { | |
| 29 | 30 | } | |
| 30 | 31 | ||
| 31 | 32 | 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()); | |
| 36 | 37 | return socket_fd; | |
| 37 | 38 | } | |
| 38 | 39 | ||
| 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 { | |
| 40 | 41 | 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| { | |
| 42 | 43 | std.log.warn("failed to delete stale socket err={s}", .{@errorName(err)}); | |
| 43 | 44 | }; | |
| 44 | 45 | } | |
| 45 | 46 | ||
| 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 | + | } | |
| 53 | 53 | }; | |
| 54 | - | if (!posix.S.ISSOCK(stat.mode)) { | |
| 54 | + | if (stat.kind != .unix_domain_socket) { | |
| 55 | 55 | return error.FileNotUnixSocket; | |
| 56 | 56 | } | |
| 57 | 57 | return true; | |
| 58 | 58 | } | |
| 59 | 59 | ||
| 60 | - | pub fn createSocket(fname: []const u8) !i32 { | |
| 60 | + | pub fn createSocket(sesh: []const u8) !i32 { | |
| 61 | 61 | // AF.UNIX: Unix domain socket for local IPC with client processes | |
| 62 | 62 | // SOCK.STREAM: Reliable, bidirectional communication | |
| 63 | 63 | // SOCK.NONBLOCK: Set socket to non-blocking | |
| 64 | - | const fd = try posix.socket( | |
| 64 | + | const fd = try lib_posix.socket( | |
| 65 | 65 | posix.AF.UNIX, | |
| 66 | 66 | posix.SOCK.STREAM | posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC, | |
| 67 | 67 | 0, | |
| 68 | 68 | ); | |
| 69 | - | errdefer posix.close(fd); | |
| 69 | + | errdefer lib_posix.close(fd); | |
| 70 | 70 | ||
| 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); | |
| 74 | 74 | return fd; | |
| 75 | 75 | } | |
| 76 | 76 |
| ... | ... | @@ -96,9 +96,9 @@ pub fn getSocketPath( | |
| 96 | 96 | return fname; | |
| 97 | 97 | } | |
| 98 | 98 | ||
| 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 { | |
| 100 | 100 | var buf: [4096]u8 = undefined; | |
| 101 | - | var w = std.fs.File.stderr().writer(&buf); | |
| 101 | + | var w = std.Io.File.stderr().writer(io, &buf); | |
| 102 | 102 | if (maxSessionNameLen(socket_dir)) |max_len| { | |
| 103 | 103 | w.interface.print( | |
| 104 | 104 | "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 { | |
| 33 | 33 | ||
| 34 | 34 | pub fn get_session_entries( | |
| 35 | 35 | alloc: std.mem.Allocator, | |
| 36 | + | io: std.Io, | |
| 36 | 37 | socket_dir: []const u8, | |
| 37 | 38 | ) !std.ArrayList(SessionEntry) { | |
| 38 | 39 | 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); | |
| 41 | 42 | var iter = dir.iterate(); | |
| 42 | 43 | ||
| 43 | 44 | var sessions = try std.ArrayList(SessionEntry).initCapacity(alloc, 30); | |
| 44 | 45 | ||
| 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; | |
| 47 | 48 | if (exists) { | |
| 48 | 49 | const name = try alloc.dupe(u8, entry.name); | |
| 49 | 50 | errdefer alloc.free(name); |
| ... | ... | @@ -70,7 +71,7 @@ pub fn get_session_entries( | |
| 70 | 71 | // daemon can miss the probe timeout; deleting its socket | |
| 71 | 72 | // orphans it permanently. | |
| 72 | 73 | if (err == error.ConnectionRefused) { | |
| 73 | - | socket.cleanupStaleSocket(dir, entry.name); | |
| 74 | + | socket.cleanupStaleSocket(io, dir, entry.name); | |
| 74 | 75 | } | |
| 75 | 76 | continue; | |
| 76 | 77 | }; |
| ... | ... | @@ -404,7 +405,7 @@ pub fn stripAnsi(alloc: std.mem.Allocator, data: []const u8) ![]const u8 { | |
| 404 | 405 | return result.toOwnedSlice(alloc); | |
| 405 | 406 | } | |
| 406 | 407 | ||
| 407 | - | /// Detects Ctrl+\ across raw, Kitty CSI u, and xterm modifyOtherKeys encodings. | |
| 408 | + | /// Dcts Ctrl+\ across raw, Kitty CSI u, and xterm modifyOtherKeys encodings. | |
| 408 | 409 | pub fn isCtrlBackslash(buf: []const u8) bool { | |
| 409 | 410 | if (buf.len == 0) return false; | |
| 410 | 411 | return buf[0] == 0x1C or isKeyPressed(buf, 0x5c, 0b100) or isModifyOtherKey(buf, 0x5c, 0b100); |
| ... | ... | @@ -765,10 +766,6 @@ pub fn serializeTerminal( | |
| 765 | 766 | }; | |
| 766 | 767 | } | |
| 767 | 768 | ||
| 768 | - | pub fn detectShell() [:0]const u8 { | |
| 769 | - | return std.posix.getenv("SHELL") orelse "/bin/sh"; | |
| 770 | - | } | |
| 771 | - | ||
| 772 | 769 | /// Formats a session entry for list output (only the name when `short` is | |
| 773 | 770 | /// true), adding a prefix to indicate the current session, if there is one. | |
| 774 | 771 | pub fn writeSessionLine( |
| ... | ... | @@ -1168,8 +1165,9 @@ test "isCtrlBackslash xterm modifyOtherKeys" { | |
| 1168 | 1165 | ||
| 1169 | 1166 | test "serializeTerminalState excludes synchronized output replay" { | |
| 1170 | 1167 | const alloc = testing.allocator; | |
| 1168 | + | const io = testing.io; | |
| 1171 | 1169 | ||
| 1172 | - | var term = try ghostty_vt.Terminal.init(alloc, .{ | |
| 1170 | + | var term = try ghostty_vt.Terminal.init(io, alloc, .{ | |
| 1173 | 1171 | .cols = 80, | |
| 1174 | 1172 | .rows = 24, | |
| 1175 | 1173 | }); |
| ... | ... | @@ -1194,8 +1192,8 @@ test "serializeTerminalState excludes synchronized output replay" { | |
| 1194 | 1192 | try testing.expect(std.mem.indexOf(u8, output, "\x1b[?2026h") == null); | |
| 1195 | 1193 | } | |
| 1196 | 1194 | ||
| 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, .{ | |
| 1199 | 1197 | .cols = cols, | |
| 1200 | 1198 | .rows = rows, | |
| 1201 | 1199 | .max_scrollback = 10_000_000, |
| ... | ... | @@ -1222,12 +1220,12 @@ fn expectCursorAt(term: *ghostty_vt.Terminal, row: usize, col: usize) !void { | |
| 1222 | 1220 | try testing.expectEqual(row, cursor.y); | |
| 1223 | 1221 | } | |
| 1224 | 1222 | ||
| 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 { | |
| 1226 | 1224 | const serialized = serializeTerminalState(alloc, source) orelse | |
| 1227 | 1225 | return error.SerializationFailed; | |
| 1228 | 1226 | defer alloc.free(serialized); | |
| 1229 | 1227 | ||
| 1230 | - | var dest = try ghostty_vt.Terminal.init(alloc, .{ | |
| 1228 | + | var dest = try ghostty_vt.Terminal.init(io, alloc, .{ | |
| 1231 | 1229 | .cols = source.screens.active.pages.cols, | |
| 1232 | 1230 | .rows = source.screens.active.pages.rows, | |
| 1233 | 1231 | .max_scrollback = 10_000_000, |
| ... | ... | @@ -1256,15 +1254,16 @@ fn expectMarkerAtRow(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal, marke | |
| 1256 | 1254 | ||
| 1257 | 1255 | test "serializeTerminalState roundtrip preserves cursor position" { | |
| 1258 | 1256 | const alloc = testing.allocator; | |
| 1257 | + | const io = testing.io; | |
| 1259 | 1258 | ||
| 1260 | - | var term = try testCreateTerminal(alloc, 80, 24, "\x1b[2J" ++ // clear | |
| 1259 | + | var term = try testCreateTerminal(alloc, io, 80, 24, "\x1b[2J" ++ // clear | |
| 1261 | 1260 | "\x1b[10;20H" // cursor at row 10, col 20 (1-indexed) | |
| 1262 | 1261 | ); | |
| 1263 | 1262 | defer term.deinit(alloc); | |
| 1264 | 1263 | ||
| 1265 | 1264 | try expectCursorAt(&term, 9, 19); // 0-indexed | |
| 1266 | 1265 | ||
| 1267 | - | var client = try serializeRoundtrip(alloc, &term); | |
| 1266 | + | var client = try serializeRoundtrip(alloc, io, &term); | |
| 1268 | 1267 | defer client.deinit(alloc); | |
| 1269 | 1268 | ||
| 1270 | 1269 | try expectCursorAt(&client, 9, 19); |
| ... | ... | @@ -1272,8 +1271,9 @@ test "serializeTerminalState roundtrip preserves cursor position" { | |
| 1272 | 1271 | ||
| 1273 | 1272 | test "serializeTerminalState roundtrip preserves CUP-positioned markers" { | |
| 1274 | 1273 | const alloc = testing.allocator; | |
| 1274 | + | const io = testing.io; | |
| 1275 | 1275 | ||
| 1276 | - | var term = try testCreateTerminal(alloc, 80, 24, "\x1b[2J" ++ | |
| 1276 | + | var term = try testCreateTerminal(alloc, io, 80, 24, "\x1b[2J" ++ | |
| 1277 | 1277 | "\x1b[2;5HMARK_A" ++ | |
| 1278 | 1278 | "\x1b[6;15HMARK_B" ++ | |
| 1279 | 1279 | "\x1b[10;30HMARK_C" ++ |
| ... | ... | @@ -1281,7 +1281,7 @@ test "serializeTerminalState roundtrip preserves CUP-positioned markers" { | |
| 1281 | 1281 | "\x1b[16;20H"); | |
| 1282 | 1282 | defer term.deinit(alloc); | |
| 1283 | 1283 | ||
| 1284 | - | var client = try serializeRoundtrip(alloc, &term); | |
| 1284 | + | var client = try serializeRoundtrip(alloc, io, &term); | |
| 1285 | 1285 | defer client.deinit(alloc); | |
| 1286 | 1286 | ||
| 1287 | 1287 | try expectScreensMatch(alloc, &term, &client); |
| ... | ... | @@ -1294,8 +1294,9 @@ test "serializeTerminalState roundtrip preserves CUP-positioned markers" { | |
| 1294 | 1294 | ||
| 1295 | 1295 | test "serializeTerminalState with scrollback preserves visible content" { | |
| 1296 | 1296 | const alloc = testing.allocator; | |
| 1297 | + | const io = testing.io; | |
| 1297 | 1298 | ||
| 1298 | - | var term = try testCreateTerminal(alloc, 80, 24, ""); | |
| 1299 | + | var term = try testCreateTerminal(alloc, io, 80, 24, ""); | |
| 1299 | 1300 | defer term.deinit(alloc); | |
| 1300 | 1301 | ||
| 1301 | 1302 | var stream = term.vtStream(); |
| ... | ... | @@ -1321,7 +1322,7 @@ test "serializeTerminalState with scrollback preserves visible content" { | |
| 1321 | 1322 | try testing.expect(has_scrollback); | |
| 1322 | 1323 | ||
| 1323 | 1324 | // Roundtrip: serialize → feed into fresh terminal | |
| 1324 | - | var client = try serializeRoundtrip(alloc, &term); | |
| 1325 | + | var client = try serializeRoundtrip(alloc, io, &term); | |
| 1325 | 1326 | defer client.deinit(alloc); | |
| 1326 | 1327 | ||
| 1327 | 1328 | // Visible content must match (this is the core cursor corruption test) |
| ... | ... | @@ -1336,9 +1337,10 @@ test "serializeTerminalState nested roundtrip preserves content" { | |
| 1336 | 1337 | // Simulates: inner zmx → serialized state → outer ghostty-vt → serialized again → client | |
| 1337 | 1338 | // This is the exact nested session scenario (zmx → SSH → zmx). | |
| 1338 | 1339 | const alloc = testing.allocator; | |
| 1340 | + | const io = testing.io; | |
| 1339 | 1341 | ||
| 1340 | 1342 | // "Inner" terminal with scrollback + markers | |
| 1341 | - | var inner = try testCreateTerminal(alloc, 80, 24, ""); | |
| 1343 | + | var inner = try testCreateTerminal(alloc, io, 80, 24, ""); | |
| 1342 | 1344 | defer inner.deinit(alloc); | |
| 1343 | 1345 | ||
| 1344 | 1346 | { |
| ... | ... | @@ -1365,7 +1367,7 @@ test "serializeTerminalState nested roundtrip preserves content" { | |
| 1365 | 1367 | defer alloc.free(inner_serialized); | |
| 1366 | 1368 | ||
| 1367 | 1369 | // "Outer" terminal processes inner's serialized output | |
| 1368 | - | var outer = try testCreateTerminal(alloc, 80, 24, ""); | |
| 1370 | + | var outer = try testCreateTerminal(alloc, io, 80, 24, ""); | |
| 1369 | 1371 | defer outer.deinit(alloc); | |
| 1370 | 1372 | ||
| 1371 | 1373 | { |
| ... | ... | @@ -1375,7 +1377,7 @@ test "serializeTerminalState nested roundtrip preserves content" { | |
| 1375 | 1377 | } | |
| 1376 | 1378 | ||
| 1377 | 1379 | // Serialize outer (simulates outer daemon re-attach after detach) | |
| 1378 | - | var client = try serializeRoundtrip(alloc, &outer); | |
| 1380 | + | var client = try serializeRoundtrip(alloc, io, &outer); | |
| 1379 | 1381 | defer client.deinit(alloc); | |
| 1380 | 1382 | ||
| 1381 | 1383 | // Client must see the same content as inner's visible screen |
| ... | ... | @@ -1387,15 +1389,16 @@ test "serializeTerminalState nested roundtrip preserves content" { | |
| 1387 | 1389 | ||
| 1388 | 1390 | test "serializeTerminalState alternate screen not leaked" { | |
| 1389 | 1391 | const alloc = testing.allocator; | |
| 1392 | + | const io = testing.io; | |
| 1390 | 1393 | ||
| 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 | |
| 1392 | 1395 | "\x1b[2J\x1b[3;10HALT_MARK" ++ // write on alt screen | |
| 1393 | 1396 | "\x1b[?1049l" ++ // exit alt screen | |
| 1394 | 1397 | "\x1b[2J\x1b[2;5HMAIN_MARK\x1b[8;20H" // write on main screen | |
| 1395 | 1398 | ); | |
| 1396 | 1399 | defer term.deinit(alloc); | |
| 1397 | 1400 | ||
| 1398 | - | var client = try serializeRoundtrip(alloc, &term); | |
| 1401 | + | var client = try serializeRoundtrip(alloc, io, &term); | |
| 1399 | 1402 | defer client.deinit(alloc); | |
| 1400 | 1403 | ||
| 1401 | 1404 | try expectScreensMatch(alloc, &term, &client); |
| ... | ... | @@ -1408,8 +1411,9 @@ test "serializeTerminalState alternate screen not leaked" { | |
| 1408 | 1411 | ||
| 1409 | 1412 | test "serializeTerminalState size mismatch roundtrip" { | |
| 1410 | 1413 | const alloc = testing.allocator; | |
| 1414 | + | const io = testing.io; | |
| 1411 | 1415 | ||
| 1412 | - | var term = try testCreateTerminal(alloc, 80, 30, "\x1b[2J" ++ | |
| 1416 | + | var term = try testCreateTerminal(alloc, io, 80, 30, "\x1b[2J" ++ | |
| 1413 | 1417 | "\x1b[3;10HSIZE_A" ++ | |
| 1414 | 1418 | "\x1b[12;20HSIZE_B" ++ | |
| 1415 | 1419 | "\x1b[20;40HSIZE_C" ++ |
| ... | ... | @@ -1419,7 +1423,7 @@ test "serializeTerminalState size mismatch roundtrip" { | |
| 1419 | 1423 | // Resize to 24 rows (simulates outer terminal being smaller) | |
| 1420 | 1424 | try term.resize(alloc, ghostty_vt.Terminal.Resize{ .cols = 80, .rows = 24 }); | |
| 1421 | 1425 | ||
| 1422 | - | var client = try serializeRoundtrip(alloc, &term); | |
| 1426 | + | var client = try serializeRoundtrip(alloc, io, &term); | |
| 1423 | 1427 | defer client.deinit(alloc); | |
| 1424 | 1428 | ||
| 1425 | 1429 | try expectScreensMatch(alloc, &term, &client); |
| ... | ... | @@ -1428,8 +1432,9 @@ test "serializeTerminalState size mismatch roundtrip" { | |
| 1428 | 1432 | ||
| 1429 | 1433 | test "serializeTerminalState scrollback + size mismatch nested roundtrip" { | |
| 1430 | 1434 | const alloc = testing.allocator; | |
| 1435 | + | const io = testing.io; | |
| 1431 | 1436 | ||
| 1432 | - | var inner = try testCreateTerminal(alloc, 80, 30, ""); | |
| 1437 | + | var inner = try testCreateTerminal(alloc, io, 80, 30, ""); | |
| 1433 | 1438 | defer inner.deinit(alloc); | |
| 1434 | 1439 | ||
| 1435 | 1440 | { |
| ... | ... | @@ -1457,7 +1462,7 @@ test "serializeTerminalState scrollback + size mismatch nested roundtrip" { | |
| 1457 | 1462 | return error.SerializationFailed; | |
| 1458 | 1463 | defer alloc.free(inner_ser); | |
| 1459 | 1464 | ||
| 1460 | - | var outer = try testCreateTerminal(alloc, 80, 24, ""); | |
| 1465 | + | var outer = try testCreateTerminal(alloc, io, 80, 24, ""); | |
| 1461 | 1466 | defer outer.deinit(alloc); | |
| 1462 | 1467 | { | |
| 1463 | 1468 | var outer_stream = outer.vtStream(); |
| ... | ... | @@ -1465,7 +1470,7 @@ test "serializeTerminalState scrollback + size mismatch nested roundtrip" { | |
| 1465 | 1470 | outer_stream.nextSlice(inner_ser); | |
| 1466 | 1471 | } | |
| 1467 | 1472 | ||
| 1468 | - | var client = try serializeRoundtrip(alloc, &outer); | |
| 1473 | + | var client = try serializeRoundtrip(alloc, io, &outer); | |
| 1469 | 1474 | defer client.deinit(alloc); | |
| 1470 | 1475 | ||
| 1471 | 1476 | try expectScreensMatch(alloc, &inner, &client); |