Commit fbbc6be

Vladimir Varankin  ·  2026-05-20 17:45:00 -0400 EDT
parent 30a153e
chore: store logs in XDG_STATE_HOME

Previously we were storing logs inside of our socket_dir technically worked
but according to the XDG Base Directory Spec it should be stored inside of STATE.

> The $XDG_STATE_HOME contains state data that should persist between (application)
> restarts, but that is not important or portable enough to the user that it
> should be stored in $XDG_DATA_HOME. It may contain:
>  - actions history (logs, history, recently used files, …)
>  - current state of the application that can be reused on a restart
>    (view, layout, open files, undo history, …)

Reference: https://specifications.freedesktop.org/basedir/latest/#variables
2 files changed,  +46, -12
+10, -2
 1@@ -295,7 +295,8 @@ Instead, this tool specifically focuses on session persistence and defers window
 2 ## ssh workflow
 3 
 4 ### Try it out quickly
 5-If you'd like to try out `zmx` and `ssh` without fiddling your `ssh` config, make sure to pass the `-t` option to `ssh`.  Here's an example:
 6+
 7+If you'd like to try out `zmx` and `ssh` without fiddling your `ssh` config, make sure to pass the `-t` option to `ssh`. Here's an example:
 8 
 9 ```bash
10 ssh -t dev-box zmx attach default
11@@ -378,7 +379,14 @@ This is particularly useful when running `zmx` as a system service with a shared
12 
13 ## debugging
14 
15-We store global logs for cli commands in `{socket_dir}/logs/zmx.log`. We store session-specific logs in `{socket_dir}/logs/{session_name}.log`. Right now they are enabled by default and cannot be disabled. The idea here is to help with initial development until we reach a stable state.
16+We store global logs for cli commands in `{log_dir}/zmx.log`. We store session-specific logs in `{log_dir}/{session_name}.log`. Right now they are enabled by default and cannot be disabled. The idea here is to help with initial development until we reach a stable state.
17+
18+The log directory is resolved in this order:
19+
20+1. `ZMX_DIR/logs` if `ZMX_DIR` is set
21+1. `XDG_STATE_HOME/zmx/logs` if `XDG_STATE_HOME` is set
22+1. `HOME/.local/state/zmx/logs`
23+1. `TMPDIR/zmx-$UID` (or `/tmp/zmx-$UID`) as a last resort
24 
25 ## a smol contract
26 
+36, -10
 1@@ -476,7 +476,8 @@ const Cfg = struct {
 2 
 3     pub fn init(alloc: std.mem.Allocator) !Cfg {
 4         const socket_dir = try socketDir(alloc);
 5-        const log_dir = try std.fmt.allocPrint(alloc, "{s}/logs", .{socket_dir});
 6+        errdefer alloc.free(socket_dir);
 7+        const log_dir = try logDir(alloc);
 8         errdefer alloc.free(log_dir);
 9 
10         const dir_mode = if (std.posix.getenv("ZMX_DIR_MODE")) |m|
11@@ -511,26 +512,51 @@ const Cfg = struct {
12             try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime})
13         else
14             try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
15-        errdefer alloc.free(socket_dir);
16 
17         return socket_dir;
18     }
19 
20+    fn logDir(alloc: std.mem.Allocator) ![]const u8 {
21+        const log_dir = if (posix.getenv("ZMX_DIR")) |zmxdir|
22+            try std.fmt.allocPrint(alloc, "{s}/logs", .{zmxdir})
23+        else if (posix.getenv("XDG_STATE_HOME")) |xdg_state_home|
24+            try std.fmt.allocPrint(alloc, "{s}/zmx/logs", .{xdg_state_home})
25+        else if (posix.getenv("HOME")) |home_dir|
26+            try std.fmt.allocPrint(alloc, "{s}/.local/state/zmx/logs", .{home_dir})
27+        else fallback: {
28+            // This is the last resort: falling back to /tmp/$UID if HOME is unset.
29+            const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/");
30+            const uid = posix.getuid();
31+            break :fallback try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
32+        };
33+
34+        return log_dir;
35+    }
36+
37     pub fn deinit(self: *Cfg, alloc: std.mem.Allocator) void {
38         if (self.socket_dir.len > 0) alloc.free(self.socket_dir);
39         if (self.log_dir.len > 0) alloc.free(self.log_dir);
40     }
41 
42     pub fn mkdir(self: *Cfg) !void {
43-        posix.mkdirat(posix.AT.FDCWD, self.socket_dir, @intCast(self.dir_mode)) catch |err| switch (err) {
44-            error.PathAlreadyExists => {},
45-            else => return err,
46-        };
47+        try mkdirAll(self.socket_dir, @intCast(self.dir_mode));
48+        try mkdirAll(self.log_dir, @intCast(self.dir_mode));
49+    }
50 
51-        posix.mkdirat(posix.AT.FDCWD, self.log_dir, @intCast(self.dir_mode)) catch |err| switch (err) {
52-            error.PathAlreadyExists => {},
53-            else => return err,
54-        };
55+    fn mkdirAll(sub_dir_path: []const u8, mode: posix.mode_t) !void {
56+        var it = try std.fs.path.componentIterator(sub_dir_path);
57+        var component = it.last() orelse return error.BadPathName;
58+        while (true) {
59+            posix.mkdirat(posix.AT.FDCWD, component.path, mode) catch |err| switch (err) {
60+                error.PathAlreadyExists => {},
61+                error.FileNotFound => |e| {
62+                    component = it.previous() orelse return e;
63+                    continue;
64+                },
65+                else => |e| return e,
66+            };
67+            component = it.next() orelse return;
68+        }
69     }
70 };
71