Commit fbf0171
Adrian
·
2026-04-13 11:58:12 -0400 EDT
parent 5f30ba8
feat: allow configuring socket and log permissions via environment variables (#130)
3 files changed,
+67,
-7
+9,
-0
| ... | ... | @@ -360,6 +360,15 @@ Each session gets its own unix socket file. The default location depends on your | |
| 360 | 360 | 1. `TMPDIR` => uses `{TMPDIR}/zmx-{uid}` (appends uid for multi-user safety) | |
| 361 | 361 | 1. `/tmp` => uses `/tmp/zmx-{uid}` (default fallback, appends uid for multi-user safety) | |
| 362 | 362 | ||
| 363 | + | ## permissions | |
| 364 | + | ||
| 365 | + | You can configure the permissions for the socket directory and log files using the following environment variables: | |
| 366 | + | ||
| 367 | + | - `ZMX_DIR_MODE` => sets the mode for the socket and log directories (octal, defaults to `0750`) | |
| 368 | + | - `ZMX_LOG_MODE` => sets the mode for the log files (octal, defaults to `0640`) | |
| 369 | + | ||
| 370 | + | This is particularly useful when running `zmx` as a system service with a shared group. For example, setting `ZMX_DIR_MODE=0770` and `ZMX_LOG_MODE=0660` allows group members to attach to the session. | |
| 371 | + | ||
| 363 | 372 | ## debugging | |
| 364 | 373 | ||
| 365 | 374 | 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. |
+5,
-3
| ... | ... | @@ -7,15 +7,17 @@ pub const LogSystem = struct { | |
| 7 | 7 | max_size: u64 = 5 * 1024 * 1024, // 5MB | |
| 8 | 8 | path: []const u8 = "", | |
| 9 | 9 | alloc: std.mem.Allocator = undefined, | |
| 10 | + | mode: u32 = 0o640, | |
| 10 | 11 | ||
| 11 | - | pub fn init(self: *LogSystem, alloc: std.mem.Allocator, path: []const u8) !void { | |
| 12 | + | pub fn init(self: *LogSystem, alloc: std.mem.Allocator, path: []const u8, mode: u32) !void { | |
| 12 | 13 | self.alloc = alloc; | |
| 13 | 14 | self.path = try alloc.dupe(u8, path); | |
| 15 | + | self.mode = mode; | |
| 14 | 16 | ||
| 15 | 17 | const file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) { | |
| 16 | 18 | error.FileNotFound => try std.fs.createFileAbsolute( | |
| 17 | 19 | path, | |
| 18 | - | .{ .read = true, .mode = 0o640 }, | |
| 20 | + | .{ .read = true, .mode = @intCast(self.mode) }, | |
| 19 | 21 | ), | |
| 20 | 22 | else => return err, | |
| 21 | 23 | }; |
| ... | ... | @@ -93,7 +95,7 @@ pub const LogSystem = struct { | |
| 93 | 95 | ||
| 94 | 96 | self.file = try std.fs.createFileAbsolute( | |
| 95 | 97 | self.path, | |
| 96 | - | .{ .truncate = true, .read = true, .mode = 0o640 }, | |
| 98 | + | .{ .truncate = true, .read = true, .mode = @intCast(self.mode) }, | |
| 97 | 99 | ); | |
| 98 | 100 | self.current_size = 0; | |
| 99 | 101 | } |
+53,
-4
| ... | ... | @@ -55,7 +55,7 @@ pub fn main() !void { | |
| 55 | 55 | ||
| 56 | 56 | const log_path = try std.fs.path.join(alloc, &.{ cfg.log_dir, "zmx.log" }); | |
| 57 | 57 | defer alloc.free(log_path); | |
| 58 | - | try log_system.init(alloc, log_path); | |
| 58 | + | try log_system.init(alloc, log_path, cfg.log_mode); | |
| 59 | 59 | defer log_system.deinit(); | |
| 60 | 60 | ||
| 61 | 61 | const cmd = args.next() orelse { |
| ... | ... | @@ -270,15 +270,29 @@ const Cfg = struct { | |
| 270 | 270 | socket_dir: []const u8, | |
| 271 | 271 | log_dir: []const u8, | |
| 272 | 272 | max_scrollback: usize = 10_000_000, | |
| 273 | + | dir_mode: u32 = 0o750, | |
| 274 | + | log_mode: u32 = 0o640, | |
| 273 | 275 | ||
| 274 | 276 | pub fn init(alloc: std.mem.Allocator) !Cfg { | |
| 275 | 277 | const socket_dir = try socketDir(alloc); | |
| 276 | 278 | const log_dir = try std.fmt.allocPrint(alloc, "{s}/logs", .{socket_dir}); | |
| 277 | 279 | errdefer alloc.free(log_dir); | |
| 278 | 280 | ||
| 281 | + | const dir_mode = if (std.posix.getenv("ZMX_DIR_MODE")) |m| | |
| 282 | + | std.fmt.parseInt(u32, m, 8) catch 0o750 | |
| 283 | + | else | |
| 284 | + | 0o750; | |
| 285 | + | ||
| 286 | + | const log_mode = if (std.posix.getenv("ZMX_LOG_MODE")) |m| | |
| 287 | + | std.fmt.parseInt(u32, m, 8) catch 0o640 | |
| 288 | + | else | |
| 289 | + | 0o640; | |
| 290 | + | ||
| 279 | 291 | var cfg = Cfg{ | |
| 280 | 292 | .socket_dir = socket_dir, | |
| 281 | 293 | .log_dir = log_dir, | |
| 294 | + | .dir_mode = dir_mode, | |
| 295 | + | .log_mode = log_mode, | |
| 282 | 296 | }; | |
| 283 | 297 | ||
| 284 | 298 | try cfg.mkdir(); |
| ... | ... | @@ -307,18 +321,51 @@ const Cfg = struct { | |
| 307 | 321 | } | |
| 308 | 322 | ||
| 309 | 323 | pub fn mkdir(self: *Cfg) !void { | |
| 310 | - | posix.mkdirat(posix.AT.FDCWD, self.socket_dir, 0o750) catch |err| switch (err) { | |
| 324 | + | posix.mkdirat(posix.AT.FDCWD, self.socket_dir, @intCast(self.dir_mode)) catch |err| switch (err) { | |
| 311 | 325 | error.PathAlreadyExists => {}, | |
| 312 | 326 | else => return err, | |
| 313 | 327 | }; | |
| 314 | 328 | ||
| 315 | - | posix.mkdirat(posix.AT.FDCWD, self.log_dir, 0o750) catch |err| switch (err) { | |
| 329 | + | posix.mkdirat(posix.AT.FDCWD, self.log_dir, @intCast(self.dir_mode)) catch |err| switch (err) { | |
| 316 | 330 | error.PathAlreadyExists => {}, | |
| 317 | 331 | else => return err, | |
| 318 | 332 | }; | |
| 319 | 333 | } | |
| 320 | 334 | }; | |
| 321 | 335 | ||
| 336 | + | test "Cfg.init uses default modes when env vars are not set" { | |
| 337 | + | const alloc = std.testing.allocator; | |
| 338 | + | ||
| 339 | + | // Ensure they are not set | |
| 340 | + | _ = cross.c.unsetenv("ZMX_DIR_MODE"); | |
| 341 | + | _ = cross.c.unsetenv("ZMX_LOG_MODE"); | |
| 342 | + | ||
| 343 | + | var cfg = try Cfg.init(alloc); | |
| 344 | + | defer cfg.deinit(alloc); | |
| 345 | + | ||
| 346 | + | try std.testing.expectEqual(@as(u32, 0o750), cfg.dir_mode); | |
| 347 | + | try std.testing.expectEqual(@as(u32, 0o640), cfg.log_mode); | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | test "Cfg.init uses custom modes from env vars" { | |
| 351 | + | const alloc = std.testing.allocator; | |
| 352 | + | ||
| 353 | + | // Set custom octal values | |
| 354 | + | _ = cross.c.setenv("ZMX_DIR_MODE", "770", 1); | |
| 355 | + | _ = cross.c.setenv("ZMX_LOG_MODE", "660", 1); | |
| 356 | + | defer { | |
| 357 | + | _ = cross.c.unsetenv("ZMX_DIR_MODE"); | |
| 358 | + | _ = cross.c.unsetenv("ZMX_LOG_MODE"); | |
| 359 | + | } | |
| 360 | + | ||
| 361 | + | var cfg = try Cfg.init(alloc); | |
| 362 | + | defer cfg.deinit(alloc); | |
| 363 | + | ||
| 364 | + | try std.testing.expectEqual(@as(u32, 0o770), cfg.dir_mode); | |
| 365 | + | try std.testing.expectEqual(@as(u32, 0o660), cfg.log_mode); | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | ||
| 322 | 369 | /// Daemon is responsible for managing a zmx session. | |
| 323 | 370 | /// | |
| 324 | 371 | /// It holds all the state for a running session. Instead of a single daemon for all sessions, we |
| ... | ... | @@ -535,7 +582,7 @@ const Daemon = struct { | |
| 535 | 582 | &.{ self.cfg.log_dir, session_log_name }, | |
| 536 | 583 | ); | |
| 537 | 584 | defer self.alloc.free(session_log_path); | |
| 538 | - | try log_system.init(self.alloc, session_log_path); | |
| 585 | + | try log_system.init(self.alloc, session_log_path, self.cfg.log_mode); | |
| 539 | 586 | ||
| 540 | 587 | // If spawnPty fails, clean up here. Once it succeeds, | |
| 541 | 588 | // the inner block's defer takes ownership of cleanup to |
| ... | ... | @@ -891,6 +938,8 @@ fn help() !void { | |
| 891 | 938 | \\ - TMPDIR Controls which folder is used to store unix socket files (prio: 3) | |
| 892 | 939 | \\ - ZMX_SESSION The session name we inject into every zmx session automatically | |
| 893 | 940 | \\ - ZMX_SESSION_PREFIX Adds this value to the start of every session name for all commands | |
| 941 | + | \\ - ZMX_DIR_MODE Sets the mode for the socket and log directories (octal, defaults to 0750) | |
| 942 | + | \\ - ZMX_LOG_MODE Sets the mode for the log files (octal, defaults to 0640) | |
| 894 | 943 | \\ | |
| 895 | 944 | ; | |
| 896 | 945 | var buf: [4096]u8 = undefined; |