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
......@@ -295,7 +295,8 @@ Instead, this tool specifically focuses on session persistence and defers window
295295 ## ssh workflow
296296
297297 ### Try it out quickly
298-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:
298+
299+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:
299300
300301 ```bash
301302 ssh -t dev-box zmx attach default
......@@ -378,7 +379,14 @@ This is particularly useful when running `zmx` as a system service with a shared
378379
379380 ## debugging
380381
381-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.
382+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.
383+
384+The log directory is resolved in this order:
385+
386+1. `ZMX_DIR/logs` if `ZMX_DIR` is set
387+1. `XDG_STATE_HOME/zmx/logs` if `XDG_STATE_HOME` is set
388+1. `HOME/.local/state/zmx/logs`
389+1. `TMPDIR/zmx-$UID` (or `/tmp/zmx-$UID`) as a last resort
382390
383391 ## a smol contract
384392
+36, -10
......@@ -476,7 +476,8 @@ const Cfg = struct {
476476
477477 pub fn init(alloc: std.mem.Allocator) !Cfg {
478478 const socket_dir = try socketDir(alloc);
479- const log_dir = try std.fmt.allocPrint(alloc, "{s}/logs", .{socket_dir});
479+ errdefer alloc.free(socket_dir);
480+ const log_dir = try logDir(alloc);
480481 errdefer alloc.free(log_dir);
481482
482483 const dir_mode = if (std.posix.getenv("ZMX_DIR_MODE")) |m|
......@@ -511,26 +512,51 @@ const Cfg = struct {
511512 try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime})
512513 else
513514 try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
514- errdefer alloc.free(socket_dir);
515515
516516 return socket_dir;
517517 }
518518
519+ fn logDir(alloc: std.mem.Allocator) ![]const u8 {
520+ const log_dir = if (posix.getenv("ZMX_DIR")) |zmxdir|
521+ try std.fmt.allocPrint(alloc, "{s}/logs", .{zmxdir})
522+ else if (posix.getenv("XDG_STATE_HOME")) |xdg_state_home|
523+ try std.fmt.allocPrint(alloc, "{s}/zmx/logs", .{xdg_state_home})
524+ else if (posix.getenv("HOME")) |home_dir|
525+ try std.fmt.allocPrint(alloc, "{s}/.local/state/zmx/logs", .{home_dir})
526+ else fallback: {
527+ // This is the last resort: falling back to /tmp/$UID if HOME is unset.
528+ const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/");
529+ const uid = posix.getuid();
530+ break :fallback try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
531+ };
532+
533+ return log_dir;
534+ }
535+
519536 pub fn deinit(self: *Cfg, alloc: std.mem.Allocator) void {
520537 if (self.socket_dir.len > 0) alloc.free(self.socket_dir);
521538 if (self.log_dir.len > 0) alloc.free(self.log_dir);
522539 }
523540
524541 pub fn mkdir(self: *Cfg) !void {
525- posix.mkdirat(posix.AT.FDCWD, self.socket_dir, @intCast(self.dir_mode)) catch |err| switch (err) {
526- error.PathAlreadyExists => {},
527- else => return err,
528- };
542+ try mkdirAll(self.socket_dir, @intCast(self.dir_mode));
543+ try mkdirAll(self.log_dir, @intCast(self.dir_mode));
544+ }
529545
530- posix.mkdirat(posix.AT.FDCWD, self.log_dir, @intCast(self.dir_mode)) catch |err| switch (err) {
531- error.PathAlreadyExists => {},
532- else => return err,
533- };
546+ fn mkdirAll(sub_dir_path: []const u8, mode: posix.mode_t) !void {
547+ var it = try std.fs.path.componentIterator(sub_dir_path);
548+ var component = it.last() orelse return error.BadPathName;
549+ while (true) {
550+ posix.mkdirat(posix.AT.FDCWD, component.path, mode) catch |err| switch (err) {
551+ error.PathAlreadyExists => {},
552+ error.FileNotFound => |e| {
553+ component = it.previous() orelse return e;
554+ continue;
555+ },
556+ else => |e| return e,
557+ };
558+ component = it.next() orelse return;
559+ }
534560 }
535561 };
536562