- commit
- 15dfe33
- parent
- 1f5f0b8
- author
- Eric Bower
- date
- 2025-11-26 14:26:51 -0500 EST
feat: send logs to a file with rotation
3 files changed,
+156,
-14
+1,
-0
1@@ -9,3 +9,4 @@ commit_msg
2 zig_std_src/
3 ghostty_src/
4 prior_art/
5+.beads/
+88,
-0
1@@ -0,0 +1,88 @@
2+const std = @import("std");
3+
4+pub const LogSystem = struct {
5+ file: ?std.fs.File = null,
6+ mutex: std.Thread.Mutex = .{},
7+ current_size: u64 = 0,
8+ max_size: u64 = 5 * 1024 * 1024, // 5MB
9+ path: []const u8 = "",
10+ alloc: std.mem.Allocator = undefined,
11+
12+ pub fn init(self: *LogSystem, alloc: std.mem.Allocator, path: []const u8) !void {
13+ self.alloc = alloc;
14+ self.path = try alloc.dupe(u8, path);
15+
16+ const file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) {
17+ error.FileNotFound => try std.fs.createFileAbsolute(path, .{ .read = true }),
18+ else => return err,
19+ };
20+
21+ const end_pos = try file.getEndPos();
22+ try file.seekTo(end_pos);
23+ self.current_size = end_pos;
24+ self.file = file;
25+ }
26+
27+ pub fn deinit(self: *LogSystem) void {
28+ if (self.file) |f| f.close();
29+ if (self.path.len > 0) self.alloc.free(self.path);
30+ }
31+
32+ pub fn log(self: *LogSystem, comptime level: std.log.Level, comptime scope: @Type(.enum_literal), comptime format: []const u8, args: anytype) void {
33+ self.mutex.lock();
34+ defer self.mutex.unlock();
35+
36+ if (self.file == null) {
37+ std.log.defaultLog(level, scope, format, args);
38+ return;
39+ }
40+
41+ if (self.current_size >= self.max_size) {
42+ self.rotate() catch |err| {
43+ std.debug.print("Log rotation failed: {s}\n", .{@errorName(err)});
44+ };
45+ }
46+
47+ const now = std.time.milliTimestamp();
48+ const prefix = "[{d}] [{s}] ({s}): ";
49+ const scope_name = @tagName(scope);
50+ const level_name = level.asText();
51+
52+ const prefix_args = .{
53+ now,
54+ level_name,
55+ scope_name,
56+ };
57+
58+ if (self.file) |f| {
59+ const prefix_len = std.fmt.count(prefix, prefix_args);
60+ const msg_len = std.fmt.count(format, args);
61+ const newline_len = 1;
62+ const total_len = prefix_len + msg_len + newline_len;
63+ self.current_size += total_len;
64+
65+ var buf: [4096]u8 = undefined;
66+ var w = f.writer(&buf);
67+ w.interface.print(prefix ++ format ++ "\n", prefix_args ++ args) catch {};
68+ w.interface.flush() catch {};
69+ }
70+ }
71+
72+ fn rotate(self: *LogSystem) !void {
73+ if (self.file) |f| {
74+ f.close();
75+ self.file = null;
76+ }
77+
78+ const old_path = try std.fmt.allocPrint(self.alloc, "{s}.old", .{self.path});
79+ defer self.alloc.free(old_path);
80+
81+ std.fs.renameAbsolute(self.path, old_path) catch |err| switch (err) {
82+ error.FileNotFound => {},
83+ else => return err,
84+ };
85+
86+ self.file = try std.fs.createFileAbsolute(self.path, .{ .truncate = true, .read = true });
87+ self.current_size = 0;
88+ }
89+};
+67,
-14
1@@ -1,7 +1,24 @@
2 const std = @import("std");
3 const posix = std.posix;
4-const ipc = @import("ipc.zig");
5 const builtin = @import("builtin");
6+const ipc = @import("ipc.zig");
7+const log = @import("log.zig");
8+const RingBuffer = @import("ring_buffer.zig").RingBuffer;
9+
10+var log_system = log.LogSystem{};
11+
12+pub const std_options: std.Options = .{
13+ .logFn = zmxLogFn,
14+};
15+
16+fn zmxLogFn(
17+ comptime level: std.log.Level,
18+ comptime scope: @Type(.enum_literal),
19+ comptime format: []const u8,
20+ args: anytype,
21+) void {
22+ log_system.log(level, scope, format, args);
23+}
24
25 const c = switch (builtin.os.tag) {
26 .macos => @cImport({
27@@ -24,10 +41,6 @@ const c = switch (builtin.os.tag) {
28 }),
29 };
30
31-// pub const std_options: std.Options = .{
32-// .log_level = .err,
33-// };
34-
35 const Client = struct {
36 alloc: std.mem.Allocator,
37 socket_fd: i32,
38@@ -44,13 +57,16 @@ const Client = struct {
39
40 const Cfg = struct {
41 socket_dir: []const u8 = "/tmp/zmx",
42+ log_dir: []const u8 = "/tmp/zmx/logs",
43
44 pub fn mkdir(self: *Cfg) !void {
45- std.log.info("creating socket dir: socket_dir={s}", .{self.socket_dir});
46 std.fs.makeDirAbsolute(self.socket_dir) catch |err| switch (err) {
47- error.PathAlreadyExists => {
48- std.log.info("socket dir already exists", .{});
49- },
50+ error.PathAlreadyExists => {},
51+ else => return err,
52+ };
53+
54+ std.fs.makeDirAbsolute(self.log_dir) catch |err| switch (err) {
55+ error.PathAlreadyExists => {},
56 else => return err,
57 };
58 }
59@@ -96,7 +112,6 @@ const Daemon = struct {
60 };
61
62 pub fn main() !void {
63- std.log.info("running cli", .{});
64 // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
65 const alloc = std.heap.c_allocator;
66
67@@ -107,6 +122,13 @@ pub fn main() !void {
68 var cfg = Cfg{};
69 try cfg.mkdir();
70
71+ const log_path = try std.fs.path.join(alloc, &.{ cfg.log_dir, "zmx.log" });
72+ defer alloc.free(log_path);
73+ try log_system.init(alloc, log_path);
74+ defer log_system.deinit();
75+
76+ std.log.info("running cli", .{});
77+
78 const cmd = args.next() orelse {
79 return list(&cfg);
80 };
81@@ -159,13 +181,18 @@ fn list(cfg: *Cfg) !void {
82 var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{ .iterate = true });
83 defer dir.close();
84 var iter = dir.iterate();
85+ var hasSessions = false;
86 while (try iter.next()) |entry| {
87- if (try sessionExists(dir, entry.name)) {
88+ const exists = sessionExists(dir, entry.name) catch continue;
89+ if (exists) {
90+ hasSessions = true;
91 const socket_path = try getSocketPath(alloc, cfg.socket_dir, entry.name);
92 defer alloc.free(socket_path);
93
94 const fd = sessionConnect(socket_path) catch |err| {
95- std.log.warn("could not connect to session {s}: {s}", .{ entry.name, @errorName(err) });
96+ var msg_buf: [256]u8 = undefined;
97+ const msg = try std.fmt.bufPrint(&msg_buf, "could not connect to session {s}: {s}\n", .{ entry.name, @errorName(err) });
98+ try std.fs.File.stdout().writeAll(msg);
99 continue;
100 };
101 defer posix.close(fd);
102@@ -191,12 +218,22 @@ fn list(cfg: *Cfg) !void {
103 }
104
105 if (info) |i| {
106- std.log.info("session_name={s}\tpid={d}\tclients={d}", .{ entry.name, i.pid, i.clients_len });
107+ var msg_buf: [256]u8 = undefined;
108+ const msg = try std.fmt.bufPrint(&msg_buf, "session_name={s}\tpid={d}\tclients={d}\n", .{ entry.name, i.pid, i.clients_len });
109+ try std.fs.File.stdout().writeAll(msg);
110 } else {
111- std.log.info("session_name={s}\tpid=?\tclients=?", .{entry.name});
112+ var msg_buf: [256]u8 = undefined;
113+ const msg = try std.fmt.bufPrint(&msg_buf, "session_name={s}\tpid=?\tclients=?\n", .{entry.name});
114+ try std.fs.File.stdout().writeAll(msg);
115 }
116 }
117 }
118+
119+ if (!hasSessions) {
120+ var msg_buf: [256]u8 = undefined;
121+ const msg = try std.fmt.bufPrint(&msg_buf, "no sessions found in {s}\n", .{cfg.socket_dir});
122+ try std.fs.File.stdout().writeAll(msg);
123+ }
124 }
125
126 fn detachAll(cfg: *Cfg) !void {
127@@ -216,6 +253,7 @@ fn detachAll(cfg: *Cfg) !void {
128 const socket_path = try getSocketPath(alloc, cfg.socket_dir, session_name);
129 defer alloc.free(socket_path);
130 const client_sock_fd = try sessionConnect(socket_path);
131+ defer posix.close(client_sock_fd);
132 ipc.send(client_sock_fd, .DetachAll, "") catch |err| switch (err) {
133 error.BrokenPipe, error.ConnectionResetByPeer => return,
134 else => return err,
135@@ -239,10 +277,16 @@ fn kill(cfg: *Cfg, session_name: []const u8) !void {
136 const socket_path = try getSocketPath(alloc, cfg.socket_dir, session_name);
137 defer alloc.free(socket_path);
138 const client_sock_fd = try sessionConnect(socket_path);
139+ defer posix.close(client_sock_fd);
140 ipc.send(client_sock_fd, .Kill, "") catch |err| switch (err) {
141 error.BrokenPipe, error.ConnectionResetByPeer => return,
142 else => return err,
143 };
144+
145+ var buf: [4096]u8 = undefined;
146+ var w = std.fs.File.stdout().writer(&buf);
147+ try w.interface.print("killed session {s}\n", .{session_name});
148+ try w.interface.flush();
149 }
150
151 fn attach(daemon: *Daemon) !void {
152@@ -278,6 +322,14 @@ fn attach(daemon: *Daemon) !void {
153 const pid = try posix.fork();
154 if (pid == 0) { // child
155 _ = try posix.setsid();
156+
157+ log_system.deinit();
158+ const session_log_name = try std.fmt.allocPrint(daemon.alloc, "{s}.log", .{daemon.session_name});
159+ defer daemon.alloc.free(session_log_name);
160+ const session_log_path = try std.fs.path.join(daemon.alloc, &.{ daemon.cfg.log_dir, session_log_name });
161+ defer daemon.alloc.free(session_log_path);
162+ try log_system.init(daemon.alloc, session_log_path);
163+
164 const pty_fd = try spawnPty(daemon);
165 defer {
166 posix.close(server_sock_fd);
167@@ -707,6 +759,7 @@ fn spawnPty(daemon: *Daemon) !c_int {
168 fn sessionConnect(fname: []const u8) !i32 {
169 var unix_addr = try std.net.Address.initUnix(fname);
170 const socket_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0);
171+ errdefer posix.close(socket_fd);
172 posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen()) catch |err| {
173 if (err == error.ConnectionRefused) {
174 std.log.err("unable to connect to unix socket fname={s}", .{fname});