Commit 97a1423
JoeJoeflyn
·
2026-08-20 11:54:35 -0400 EDT
parent e586f62
fix: audit all commands and ensure non-zero exit codes on error (#178)
4 files changed,
+324,
-88
+132,
-86
| ... | ... | @@ -23,7 +23,18 @@ pub const std_options: std.Options = .{ | |
| 23 | 23 | .log_level = .debug, | |
| 24 | 24 | }; | |
| 25 | 25 | ||
| 26 | - | /// This is the entry point for the CLI. | |
| 26 | + | fn detectHelp(arg: []const u8) bool { | |
| 27 | + | return (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")); | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | /// Prints an error message to stderr and exits with status 1. | |
| 31 | + | fn printError(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn { | |
| 32 | + | var buf: [4096]u8 = undefined; | |
| 33 | + | var w = std.Io.File.stderr().writer(io, &buf); | |
| 34 | + | w.interface.print("error: " ++ fmt ++ "\n", args) catch {}; | |
| 35 | + | w.interface.flush() catch {}; | |
| 36 | + | std.process.exit(1); | |
| 37 | + | } | |
| 27 | 38 | pub fn main(init: std.process.Init) !void { | |
| 28 | 39 | const gpa = init.gpa; | |
| 29 | 40 | const io = init.io; |
| ... | ... | @@ -65,14 +76,18 @@ pub fn main(init: std.process.Init) !void { | |
| 65 | 76 | } | |
| 66 | 77 | return list(gpa, io, &cfg, short); | |
| 67 | 78 | } else if (std.mem.eql(u8, cmd, "get") or std.mem.eql(u8, cmd, "g")) { | |
| 68 | - | const sesh_name = args.next() orelse return error.SessionNameRequired; | |
| 79 | + | const sesh_name = args.next() orelse { | |
| 80 | + | return printError(io, "session name required (or run inside a zmx session)", .{}); | |
| 81 | + | }; | |
| 69 | 82 | if (detectHelp(sesh_name)) return help(io); | |
| 70 | 83 | const sesh = try socket.resolveSessionOrEnv(gpa, io, sesh_name); | |
| 71 | 84 | defer gpa.free(sesh); | |
| 72 | 85 | const single_kv = args.next() orelse ""; | |
| 73 | 86 | return labelGet(gpa, io, &cfg, sesh, single_kv); | |
| 74 | 87 | } else if (std.mem.eql(u8, cmd, "set")) { | |
| 75 | - | const sesh_name = args.next() orelse return error.SessionNameRequired; | |
| 88 | + | const sesh_name = args.next() orelse { | |
| 89 | + | return printError(io, "session name required (or run inside a zmx session)", .{}); | |
| 90 | + | }; | |
| 76 | 91 | if (detectHelp(sesh_name)) return help(io); | |
| 77 | 92 | const sesh = try socket.resolveSessionOrEnv(gpa, io, sesh_name); | |
| 78 | 93 | defer gpa.free(sesh); |
| ... | ... | @@ -85,19 +100,28 @@ pub fn main(init: std.process.Init) !void { | |
| 85 | 100 | try kvs.appendSlice(gpa, arg); | |
| 86 | 101 | first = false; | |
| 87 | 102 | } | |
| 103 | + | if (kvs.items.len == 0) { | |
| 104 | + | return printError(io, "at least one key=value pair required", .{}); | |
| 105 | + | } | |
| 88 | 106 | return labelSet(gpa, io, &cfg, sesh, kvs.items); | |
| 89 | - | } else if (std.mem.eql(u8, cmd, "clear")) { | |
| 90 | - | const sesh_name = args.next() orelse return error.SessionNameRequired; | |
| 107 | + | } else if (std.mem.eql(u8, cmd, "clear") or std.mem.eql(u8, cmd, "cl")) { | |
| 108 | + | const sesh_name = args.next() orelse { | |
| 109 | + | return printError(io, "session name required (or run inside a zmx session)", .{}); | |
| 110 | + | }; | |
| 91 | 111 | if (detectHelp(sesh_name)) return help(io); | |
| 92 | 112 | const sesh = try socket.resolveSessionOrEnv(gpa, io, sesh_name); | |
| 93 | 113 | defer gpa.free(sesh); | |
| 94 | 114 | return labelClear(gpa, io, &cfg, sesh); | |
| 95 | 115 | } else if (std.mem.eql(u8, cmd, "completions") or std.mem.eql(u8, cmd, "c")) { | |
| 96 | - | const arg = args.next() orelse return; | |
| 116 | + | const arg = args.next() orelse { | |
| 117 | + | return printError(io, "completions requires a shell argument (bash, zsh, fish, nu)", .{}); | |
| 118 | + | }; | |
| 97 | 119 | if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { | |
| 98 | 120 | return help(io); | |
| 99 | 121 | } | |
| 100 | - | const shell = completions.Shell.fromString(arg) orelse return; | |
| 122 | + | const shell = completions.Shell.fromString(arg) orelse { | |
| 123 | + | return printError(io, "unknown shell \"{s}\" (valid: bash, zsh, fish, nu)", .{arg}); | |
| 124 | + | }; | |
| 101 | 125 | return printCompletions(io, shell); | |
| 102 | 126 | } else if (std.mem.eql(u8, cmd, "detach") or std.mem.eql(u8, cmd, "d")) { | |
| 103 | 127 | return detachAll(gpa, io, &cfg); |
| ... | ... | @@ -116,7 +140,10 @@ pub fn main(init: std.process.Init) !void { | |
| 116 | 140 | } | |
| 117 | 141 | } | |
| 118 | 142 | const sesh_env = socket.getSeshNameFromEnv(); | |
| 119 | - | const sesh = try socket.getSeshName(gpa, session_name orelse sesh_env); | |
| 143 | + | const raw_name = session_name orelse (if (sesh_env.len > 0) sesh_env else { | |
| 144 | + | return printError(io, "session name required (or run inside a zmx session)", .{}); | |
| 145 | + | }); | |
| 146 | + | const sesh = try socket.getSeshName(gpa, raw_name); | |
| 120 | 147 | defer gpa.free(sesh); | |
| 121 | 148 | return history(gpa, io, &cfg, sesh, format); | |
| 122 | 149 | } else if (std.mem.eql(u8, cmd, "attach") or std.mem.eql(u8, cmd, "a")) { |
| ... | ... | @@ -131,11 +158,7 @@ pub fn main(init: std.process.Init) !void { | |
| 131 | 158 | return help(io); | |
| 132 | 159 | } | |
| 133 | 160 | if (parsed.missing_labels_value) { | |
| 134 | - | var buf: [4096]u8 = undefined; | |
| 135 | - | var w = std.Io.File.stderr().writer(io, &buf); | |
| 136 | - | w.interface.print("error: --labels requires \"key=value ...\"\n", .{}) catch {}; | |
| 137 | - | w.interface.flush() catch {}; | |
| 138 | - | std.process.exit(1); | |
| 161 | + | return printError(io, "--labels requires \"key=value ...\"", .{}); | |
| 139 | 162 | } | |
| 140 | 163 | // Before ensureSession, so a rejected label does not leave a session | |
| 141 | 164 | // behind that the caller never asked for. |
| ... | ... | @@ -206,7 +229,9 @@ pub fn main(init: std.process.Init) !void { | |
| 206 | 229 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 207 | 230 | return help(io); | |
| 208 | 231 | } | |
| 209 | - | if (session_name.len == 0) return error.SessionNameRequired; | |
| 232 | + | if (session_name.len == 0) { | |
| 233 | + | return printError(io, "session name required", .{}); | |
| 234 | + | } | |
| 210 | 235 | ||
| 211 | 236 | var text_parts: std.ArrayList([]const u8) = .empty; | |
| 212 | 237 | defer text_parts.deinit(gpa); |
| ... | ... | @@ -220,13 +245,18 @@ pub fn main(init: std.process.Init) !void { | |
| 220 | 245 | error.NameTooLong => return socket.printSessionNameTooLong(io, sesh, cfg.socket_dir), | |
| 221 | 246 | error.OutOfMemory => return err, | |
| 222 | 247 | }; | |
| 223 | - | return send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Send); | |
| 248 | + | send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Send) catch |err| { | |
| 249 | + | if (err == error.SessionUnresponsive) std.process.exit(1); | |
| 250 | + | return printError(io, "send failed: {s}", .{@errorName(err)}); | |
| 251 | + | }; | |
| 224 | 252 | } else if (std.mem.eql(u8, cmd, "print") or std.mem.eql(u8, cmd, "p")) { | |
| 225 | 253 | const session_name = args.next() orelse ""; | |
| 226 | 254 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 227 | 255 | return help(io); | |
| 228 | 256 | } | |
| 229 | - | if (session_name.len == 0) return error.SessionNameRequired; | |
| 257 | + | if (session_name.len == 0) { | |
| 258 | + | return printError(io, "session name required", .{}); | |
| 259 | + | } | |
| 230 | 260 | ||
| 231 | 261 | var text_parts: std.ArrayList([]const u8) = .empty; | |
| 232 | 262 | defer text_parts.deinit(gpa); |
| ... | ... | @@ -240,7 +270,10 @@ pub fn main(init: std.process.Init) !void { | |
| 240 | 270 | error.NameTooLong => return socket.printSessionNameTooLong(io, sesh, cfg.socket_dir), | |
| 241 | 271 | error.OutOfMemory => return err, | |
| 242 | 272 | }; | |
| 243 | - | return send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Output); | |
| 273 | + | send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Output) catch |err| { | |
| 274 | + | if (err == error.SessionUnresponsive) std.process.exit(1); | |
| 275 | + | return printError(io, "print failed: {s}", .{@errorName(err)}); | |
| 276 | + | }; | |
| 244 | 277 | } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) { | |
| 245 | 278 | var stderr_buffer: [1024]u8 = undefined; | |
| 246 | 279 | var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer); |
| ... | ... | @@ -266,7 +299,7 @@ pub fn main(init: std.process.Init) !void { | |
| 266 | 299 | try matchers.append(gpa, m); | |
| 267 | 300 | } | |
| 268 | 301 | if (matchers.items.len == 0) { | |
| 269 | - | return error.SessionNameRequired; | |
| 302 | + | return printError(io, "session name required", .{}); | |
| 270 | 303 | } | |
| 271 | 304 | var sessions = try util.get_session_entries(gpa, io, cfg.socket_dir); | |
| 272 | 305 | defer { |
| ... | ... | @@ -276,22 +309,36 @@ pub fn main(init: std.process.Init) !void { | |
| 276 | 309 | sessions.deinit(gpa); | |
| 277 | 310 | } | |
| 278 | 311 | ||
| 312 | + | var killed_any = false; | |
| 279 | 313 | for (sessions.items) |session| { | |
| 280 | 314 | for (matchers.items) |m| { | |
| 281 | - | if (!m.matches(session.name)) { | |
| 282 | - | continue; | |
| 283 | - | } | |
| 284 | - | ||
| 315 | + | if (!m.matches(session.name)) continue; | |
| 285 | 316 | kill(gpa, io, &cfg, session.name, force) catch |err| { | |
| 286 | - | try stderr.print( | |
| 287 | - | "failed to kill session={s}: {s}\n", | |
| 288 | - | .{ session.name, @errorName(err) }, | |
| 289 | - | ); | |
| 317 | + | if (err == error.SessionNotFound and force) { | |
| 318 | + | killed_any = true; | |
| 319 | + | continue; | |
| 320 | + | } | |
| 321 | + | try stderr.print("failed to kill session={s}: {s}\n", .{ session.name, @errorName(err) }); | |
| 290 | 322 | try stderr.flush(); | |
| 291 | 323 | }; | |
| 324 | + | killed_any = true; | |
| 292 | 325 | break; | |
| 293 | 326 | } | |
| 294 | 327 | } | |
| 328 | + | if (!killed_any) { | |
| 329 | + | for (matchers.items) |m| { | |
| 330 | + | if (m.is_prefix) continue; | |
| 331 | + | kill(gpa, io, &cfg, m.name, force) catch |err| { | |
| 332 | + | if (err == error.SessionNotFound and force) { | |
| 333 | + | killed_any = true; | |
| 334 | + | continue; | |
| 335 | + | } | |
| 336 | + | return printError(io, "failed to kill session={s}: {s}", .{ m.name, @errorName(err) }); | |
| 337 | + | }; | |
| 338 | + | killed_any = true; | |
| 339 | + | } | |
| 340 | + | if (!killed_any) return printError(io, "no matching sessions found", .{}); | |
| 341 | + | } | |
| 295 | 342 | } else if (std.mem.eql(u8, cmd, "wait") or std.mem.eql(u8, cmd, "w")) { | |
| 296 | 343 | var matchers: std.ArrayList(socket.SessionMatch) = .empty; | |
| 297 | 344 | defer { |
| ... | ... | @@ -308,7 +355,7 @@ pub fn main(init: std.process.Init) !void { | |
| 308 | 355 | try matchers.append(gpa, m); | |
| 309 | 356 | } | |
| 310 | 357 | if (matchers.items.len == 0) { | |
| 311 | - | return error.SessionNameRequired; | |
| 358 | + | return printError(io, "session name required", .{}); | |
| 312 | 359 | } | |
| 313 | 360 | return wait(gpa, io, &cfg, matchers); | |
| 314 | 361 | } else if (std.mem.eql(u8, cmd, "tail") or std.mem.eql(u8, cmd, "t")) { |
| ... | ... | @@ -327,7 +374,7 @@ pub fn main(init: std.process.Init) !void { | |
| 327 | 374 | try matchers.append(gpa, m); | |
| 328 | 375 | } | |
| 329 | 376 | if (matchers.items.len == 0) { | |
| 330 | - | return error.SessionNameRequired; | |
| 377 | + | return printError(io, "session name required", .{}); | |
| 331 | 378 | } | |
| 332 | 379 | ||
| 333 | 380 | // Resolve matchers against session list to get actual session names. |
| ... | ... | @@ -371,6 +418,10 @@ pub fn main(init: std.process.Init) !void { | |
| 371 | 418 | } | |
| 372 | 419 | } | |
| 373 | 420 | ||
| 421 | + | if (resolved_names.items.len == 0) { | |
| 422 | + | return printError(io, "no matching sessions found", .{}); | |
| 423 | + | } | |
| 424 | + | ||
| 374 | 425 | var client_socket_fds = try std.ArrayList(i32).initCapacity(gpa, resolved_names.items.len); | |
| 375 | 426 | defer { | |
| 376 | 427 | for (client_socket_fds.items) |client_fd| { |
| ... | ... | @@ -384,7 +435,9 @@ pub fn main(init: std.process.Init) !void { | |
| 384 | 435 | error.NameTooLong => return socket.printSessionNameTooLong(init.io, session_name, cfg.socket_dir), | |
| 385 | 436 | error.OutOfMemory => return err, | |
| 386 | 437 | }; | |
| 387 | - | const client_sock = try socket.sessionConnect(socket_path); | |
| 438 | + | const client_sock = socket.sessionConnect(socket_path) catch |err| { | |
| 439 | + | return printError(io, "cannot connect to session \"{s}\": {s}", .{ session_name, @errorName(err) }); | |
| 440 | + | }; | |
| 388 | 441 | try client_socket_fds.append(gpa, client_sock); | |
| 389 | 442 | } | |
| 390 | 443 | _ = try tail(gpa, client_socket_fds, false, false); |
| ... | ... | @@ -393,12 +446,16 @@ pub fn main(init: std.process.Init) !void { | |
| 393 | 446 | if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) { | |
| 394 | 447 | return help(io); | |
| 395 | 448 | } | |
| 396 | - | if (session_name.len == 0) return error.SessionNameRequired; | |
| 449 | + | if (session_name.len == 0) { | |
| 450 | + | return printError(io, "session name required", .{}); | |
| 451 | + | } | |
| 397 | 452 | const file_path = args.next() orelse ""; | |
| 398 | 453 | if (std.mem.eql(u8, file_path, "--help") or std.mem.eql(u8, file_path, "-h")) { | |
| 399 | 454 | return help(io); | |
| 400 | 455 | } | |
| 401 | - | if (file_path.len == 0) return error.FilePathRequired; | |
| 456 | + | if (file_path.len == 0) { | |
| 457 | + | return printError(io, "file path required", .{}); | |
| 458 | + | } | |
| 402 | 459 | ||
| 403 | 460 | var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; | |
| 404 | 461 | const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0; |
| ... | ... | @@ -414,7 +471,10 @@ pub fn main(init: std.process.Init) !void { | |
| 414 | 471 | daemon.setCwd(cwd); | |
| 415 | 472 | daemon.shell = shell_env; | |
| 416 | 473 | std.log.info("socket path={s}", .{daemon.socket_path}); | |
| 417 | - | try writeFile(gpa, io, &daemon, file_path); | |
| 474 | + | writeFile(gpa, io, &daemon, file_path) catch |err| { | |
| 475 | + | if (err == error.SessionUnresponsive) std.process.exit(1); | |
| 476 | + | return printError(io, "write failed: {s}", .{@errorName(err)}); | |
| 477 | + | }; | |
| 418 | 478 | } else if (std.mem.eql(u8, cmd, "print-env")) { | |
| 419 | 479 | var shell_mode = false; | |
| 420 | 480 | var session_name: ?[]const u8 = null; |
| ... | ... | @@ -431,12 +491,14 @@ pub fn main(init: std.process.Init) !void { | |
| 431 | 491 | } | |
| 432 | 492 | } | |
| 433 | 493 | ||
| 434 | - | const sesh_arg = session_name orelse return error.SessionNameRequired; | |
| 494 | + | const sesh_arg = session_name orelse { | |
| 495 | + | return printError(io, "session name required (or run inside a zmx session)", .{}); | |
| 496 | + | }; | |
| 435 | 497 | const sesh = try socket.resolveSessionOrEnv(gpa, io, sesh_arg); | |
| 436 | 498 | defer gpa.free(sesh); | |
| 437 | 499 | return envGet(gpa, io, &cfg, sesh, single_kv, shell_mode); | |
| 438 | 500 | } else { | |
| 439 | - | return help(io); | |
| 501 | + | return printError(io, "unknown command \"{s}\"", .{cmd}); | |
| 440 | 502 | } | |
| 441 | 503 | } | |
| 442 | 504 |
| ... | ... | @@ -624,10 +686,6 @@ fn printCompletions(io: std.Io, shell: completions.Shell) !void { | |
| 624 | 686 | try w.interface.flush(); | |
| 625 | 687 | } | |
| 626 | 688 | ||
| 627 | - | fn detectHelp(arg: []const u8) bool { | |
| 628 | - | return (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")); | |
| 629 | - | } | |
| 630 | - | ||
| 631 | 689 | fn tail(alloc: std.mem.Allocator, client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) !u8 { | |
| 632 | 690 | var poll_fds = try std.ArrayList(lib_posix.pollfd).initCapacity(alloc, 4); | |
| 633 | 691 | defer poll_fds.deinit(alloc); |
| ... | ... | @@ -1002,8 +1060,7 @@ fn list(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, short: bool) !void { | |
| 1002 | 1060 | fn detachAll(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg) !void { | |
| 1003 | 1061 | const session_name = socket.getSeshNameFromEnv(); | |
| 1004 | 1062 | if (session_name.len == 0) { | |
| 1005 | - | std.log.err("ZMX_SESSION env var not found: are you inside a zmx session?", .{}); | |
| 1006 | - | return; | |
| 1063 | + | return printError(io, "not inside a zmx session (ZMX_SESSION not set)", .{}); | |
| 1007 | 1064 | } | |
| 1008 | 1065 | std.log.info("detach all session={s}", .{session_name}); | |
| 1009 | 1066 |
| ... | ... | @@ -1040,27 +1097,20 @@ fn kill(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u | |
| 1040 | 1097 | ||
| 1041 | 1098 | const exists = try socket.sessionExists(io, dir, session_name); | |
| 1042 | 1099 | if (!exists) { | |
| 1043 | - | var buf: [4096]u8 = undefined; | |
| 1044 | - | var w = std.Io.File.stderr().writer(io, &buf); | |
| 1045 | - | w.interface.print("error: session \"{s}\" does not exist\n", .{session_name}) catch {}; | |
| 1046 | - | w.interface.flush() catch {}; | |
| 1047 | 1100 | return error.SessionNotFound; | |
| 1048 | 1101 | } | |
| 1049 | 1102 | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 1050 | 1103 | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 1051 | - | var buf: [4096]u8 = undefined; | |
| 1052 | - | var w = std.Io.File.stdout().writer(io, &buf); | |
| 1053 | 1104 | if (force or err == error.ConnectionRefused) { | |
| 1054 | 1105 | socket.cleanupStaleSocket(io, dir, session_name); | |
| 1055 | - | w.interface.print("cleaned up stale session {s}\n", .{session_name}) catch {}; | |
| 1106 | + | var ebuf: [4096]u8 = undefined; | |
| 1107 | + | var ew = std.Io.File.stderr().writer(io, &ebuf); | |
| 1108 | + | ew.interface.print("cleaned up stale session {s}\n", .{session_name}) catch {}; | |
| 1109 | + | ew.interface.flush() catch {}; | |
| 1110 | + | return; | |
| 1056 | 1111 | } else { | |
| 1057 | - | w.interface.print( | |
| 1058 | - | "session {s} is unresponsive ({s})\ndaemon may be busy: try again, add `--force` flag, or kill the process directly\n", | |
| 1059 | - | .{ session_name, @errorName(err) }, | |
| 1060 | - | ) catch {}; | |
| 1112 | + | return error.SessionUnresponsive; | |
| 1061 | 1113 | } | |
| 1062 | - | w.interface.flush() catch {}; | |
| 1063 | - | return; | |
| 1064 | 1114 | }; | |
| 1065 | 1115 | ||
| 1066 | 1116 | defer lib_posix.close(fd); |
| ... | ... | @@ -1129,7 +1179,9 @@ fn labelGet(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []con | |
| 1129 | 1179 | return; | |
| 1130 | 1180 | } | |
| 1131 | 1181 | ||
| 1132 | - | const val = try label.getLabelValueFromPairs(single_kv, payload); | |
| 1182 | + | const val = label.getLabelValueFromPairs(single_kv, payload) catch |err| switch (err) { | |
| 1183 | + | error.LabelKeyNotFound => return printError(io, "label key \"{s}\" not found in session \"{s}\"", .{ single_kv, session_name }), | |
| 1184 | + | }; | |
| 1133 | 1185 | try stdout.interface.print("{s}", .{val}); | |
| 1134 | 1186 | try stdout.interface.flush(); | |
| 1135 | 1187 | } |
| ... | ... | @@ -1332,10 +1384,7 @@ fn fetchHistory( | |
| 1332 | 1384 | ) ![]const u8 { | |
| 1333 | 1385 | std.log.info("fetch history session={s}", .{session_name}); | |
| 1334 | 1386 | const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) { | |
| 1335 | - | error.NameTooLong => { | |
| 1336 | - | socket.printSessionNameTooLong(io, session_name, cfg.socket_dir); | |
| 1337 | - | return error.NameTooLong; | |
| 1338 | - | }, | |
| 1387 | + | error.NameTooLong => return error.NameTooLong, | |
| 1339 | 1388 | error.OutOfMemory => return err, | |
| 1340 | 1389 | }; | |
| 1341 | 1390 | defer alloc.free(socket_path); |
| ... | ... | @@ -1402,16 +1451,12 @@ fn history(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []cons | |
| 1402 | 1451 | ||
| 1403 | 1452 | const exists = try socket.sessionExists(io, dir, session_name); | |
| 1404 | 1453 | if (!exists) { | |
| 1405 | - | var buf: [4096]u8 = undefined; | |
| 1406 | - | var w = std.Io.File.stderr().writer(io, &buf); | |
| 1407 | - | w.interface.print("error: session \"{s}\" does not exist\n", .{session_name}) catch {}; | |
| 1408 | - | w.interface.flush() catch {}; | |
| 1409 | - | return error.SessionNotFound; | |
| 1454 | + | return printError(io, "session \"{s}\" does not exist", .{session_name}); | |
| 1410 | 1455 | } | |
| 1411 | 1456 | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 1412 | 1457 | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 1413 | 1458 | if (err == error.ConnectionRefused) socket.cleanupStaleSocket(io, dir, session_name); | |
| 1414 | - | return; | |
| 1459 | + | return printError(io, "session \"{s}\" is unresponsive ({s})", .{ session_name, @errorName(err) }); | |
| 1415 | 1460 | }; | |
| 1416 | 1461 | defer lib_posix.close(fd); | |
| 1417 | 1462 |
| ... | ... | @@ -1461,16 +1506,12 @@ fn switchSesh(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon, current_sesh: | |
| 1461 | 1506 | ||
| 1462 | 1507 | const exists = try socket.sessionExists(io, dir, current_sesh); | |
| 1463 | 1508 | if (!exists) { | |
| 1464 | - | var buf: [4096]u8 = undefined; | |
| 1465 | - | var w = std.Io.File.stderr().writer(io, &buf); | |
| 1466 | - | w.interface.print("error: session \"{s}\" does not exist\n", .{current_sesh}) catch {}; | |
| 1467 | - | w.interface.flush() catch {}; | |
| 1468 | - | return error.SessionNotFound; | |
| 1509 | + | return printError(io, "session \"{s}\" does not exist", .{current_sesh}); | |
| 1469 | 1510 | } | |
| 1470 | 1511 | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 1471 | 1512 | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 1472 | 1513 | if (err == error.ConnectionRefused) socket.cleanupStaleSocket(io, dir, current_sesh); | |
| 1473 | - | return; | |
| 1514 | + | return printError(io, "session \"{s}\" is unresponsive ({s})", .{ current_sesh, @errorName(err) }); | |
| 1474 | 1515 | }; | |
| 1475 | 1516 | defer lib_posix.close(fd); | |
| 1476 | 1517 |
| ... | ... | @@ -1545,7 +1586,9 @@ fn attach(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon, env_str: []const | |
| 1545 | 1586 | try labelSet(gpa, io, daemon.cfg, daemon.session_name, kvs); | |
| 1546 | 1587 | } | |
| 1547 | 1588 | ||
| 1548 | - | const client_sock = try socket.sessionConnect(daemon.socket_path); | |
| 1589 | + | const client_sock = socket.sessionConnect(daemon.socket_path) catch |err| { | |
| 1590 | + | return printError(io, "cannot connect to session \"{s}\": {s}", .{ daemon.session_name, @errorName(err) }); | |
| 1591 | + | }; | |
| 1549 | 1592 | std.log.info("attached session={s}", .{daemon.session_name}); | |
| 1550 | 1593 | // This is typically used with tcsetattr() to modify terminal settings. | |
| 1551 | 1594 | // - you first get the current settings with tcgetattr() |
| ... | ... | @@ -1666,18 +1709,19 @@ fn writeFile(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon, file_path: []c | |
| 1666 | 1709 | defer dir.close(io); | |
| 1667 | 1710 | ||
| 1668 | 1711 | const result = ipc.probeSession(gpa, socket_path) catch |err| { | |
| 1669 | - | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 1712 | + | var errbuf: [4096]u8 = undefined; | |
| 1713 | + | var ew = std.Io.File.stderr().writer(io, &errbuf); | |
| 1670 | 1714 | if (err == error.ConnectionRefused) { | |
| 1671 | 1715 | socket.cleanupStaleSocket(io, dir, daemon.session_name); | |
| 1672 | - | w.interface.print("cleaned up stale session {s}\n", .{daemon.session_name}) catch {}; | |
| 1716 | + | ew.interface.print("cleaned up stale session {s}\n", .{daemon.session_name}) catch {}; | |
| 1673 | 1717 | } else { | |
| 1674 | - | w.interface.print( | |
| 1718 | + | ew.interface.print( | |
| 1675 | 1719 | "session {s} is unresponsive ({s})\ndaemon may be busy: try again\n", | |
| 1676 | 1720 | .{ daemon.session_name, @errorName(err) }, | |
| 1677 | 1721 | ) catch {}; | |
| 1678 | 1722 | } | |
| 1679 | - | w.interface.flush() catch {}; | |
| 1680 | - | return; | |
| 1723 | + | ew.interface.flush() catch {}; | |
| 1724 | + | return error.SessionUnresponsive; | |
| 1681 | 1725 | }; | |
| 1682 | 1726 | ||
| 1683 | 1727 | defer result.deinit(); |
| ... | ... | @@ -1717,8 +1761,6 @@ fn writeFile(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon, file_path: []c | |
| 1717 | 1761 | ||
| 1718 | 1762 | 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 { | |
| 1719 | 1763 | std.log.info("send session={s}", .{session_name}); | |
| 1720 | - | var buf: [4096]u8 = undefined; | |
| 1721 | - | var w = std.Io.File.stdout().writer(io, &buf); | |
| 1722 | 1764 | ||
| 1723 | 1765 | var payload = std.ArrayList(u8).empty; | |
| 1724 | 1766 | defer payload.deinit(alloc); |
| ... | ... | @@ -1750,24 +1792,28 @@ fn send(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u | |
| 1750 | 1792 | } | |
| 1751 | 1793 | } | |
| 1752 | 1794 | ||
| 1753 | - | if (payload.items.len == 0) return error.TextRequired; | |
| 1795 | + | if (payload.items.len == 0) { | |
| 1796 | + | return printError(io, "text argument required (or pipe input via stdin)", .{}); | |
| 1797 | + | } | |
| 1754 | 1798 | ||
| 1755 | 1799 | var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{}); | |
| 1756 | 1800 | defer dir.close(io); | |
| 1757 | 1801 | ||
| 1758 | 1802 | const probe_result = ipc.probeSession(alloc, socket_path) catch |err| { | |
| 1759 | 1803 | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 1804 | + | var errbuf: [4096]u8 = undefined; | |
| 1805 | + | var ew = std.Io.File.stderr().writer(io, &errbuf); | |
| 1760 | 1806 | if (err == error.ConnectionRefused) { | |
| 1761 | 1807 | socket.cleanupStaleSocket(io, dir, session_name); | |
| 1762 | - | try w.interface.print("cleaned up stale session {s}\n", .{session_name}); | |
| 1808 | + | ew.interface.print("cleaned up stale session {s}\n", .{session_name}) catch {}; | |
| 1763 | 1809 | } else { | |
| 1764 | - | try w.interface.print( | |
| 1810 | + | ew.interface.print( | |
| 1765 | 1811 | "session {s} is unresponsive ({s})\ndaemon may be busy: try again\n", | |
| 1766 | 1812 | .{ session_name, @errorName(err) }, | |
| 1767 | - | ); | |
| 1813 | + | ) catch {}; | |
| 1768 | 1814 | } | |
| 1769 | - | try w.interface.flush(); | |
| 1770 | - | return; | |
| 1815 | + | ew.interface.flush() catch {}; | |
| 1816 | + | return error.SessionUnresponsive; | |
| 1771 | 1817 | }; | |
| 1772 | 1818 | defer probe_result.deinit(); | |
| 1773 | 1819 |
| ... | ... | @@ -1840,12 +1886,12 @@ fn run(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon, detached: bool, comm | |
| 1840 | 1886 | } | |
| 1841 | 1887 | ||
| 1842 | 1888 | if (cmd_to_send == null) { | |
| 1843 | - | return error.CommandRequired; | |
| 1889 | + | return printError(io, "run requires a command (or pipe input via stdin)", .{}); | |
| 1844 | 1890 | } | |
| 1845 | 1891 | ||
| 1846 | 1892 | const client_sock = ipc.connectSession(daemon.socket_path) catch |err| { | |
| 1847 | 1893 | std.log.err("session not ready: {s}", .{@errorName(err)}); | |
| 1848 | - | return error.SessionNotReady; | |
| 1894 | + | return printError(io, "session not ready: {s}", .{@errorName(err)}); | |
| 1849 | 1895 | }; | |
| 1850 | 1896 | defer lib_posix.close(client_sock); | |
| 1851 | 1897 |
+2,
-1
| ... | ... | @@ -137,7 +137,7 @@ pub fn getSocketPath( | |
| 137 | 137 | return fname; | |
| 138 | 138 | } | |
| 139 | 139 | ||
| 140 | - | pub fn printSessionNameTooLong(io: std.Io, session_name: []const u8, socket_dir: []const u8) void { | |
| 140 | + | pub fn printSessionNameTooLong(io: std.Io, session_name: []const u8, socket_dir: []const u8) noreturn { | |
| 141 | 141 | var buf: [4096]u8 = undefined; | |
| 142 | 142 | var w = std.Io.File.stderr().writer(io, &buf); | |
| 143 | 143 | if (maxSessionNameLen(socket_dir)) |max_len| { |
| ... | ... | @@ -152,6 +152,7 @@ pub fn printSessionNameTooLong(io: std.Io, session_name: []const u8, socket_dir: | |
| 152 | 152 | ) catch {}; | |
| 153 | 153 | } | |
| 154 | 154 | w.interface.flush() catch {}; | |
| 155 | + | std.process.exit(1); | |
| 155 | 156 | } | |
| 156 | 157 | ||
| 157 | 158 | /// Returns the maximum session name length for a given socket directory, |
+155,
-0
| ... | ... | @@ -0,0 +1,155 @@ | |
| 1 | + | #!/usr/bin/env bats | |
| 2 | + | # CLI argument validation tests for zmx. | |
| 3 | + | # See https://github.com/neurosnap/zmx/issues/178 | |
| 4 | + | ||
| 5 | + | load test_helper | |
| 6 | + | ||
| 7 | + | # ============================================================================ | |
| 8 | + | # Invalid commands must exit non-zero | |
| 9 | + | # ============================================================================ | |
| 10 | + | ||
| 11 | + | @test "unknown command exits non-zero" { | |
| 12 | + | run "$ZMX" foo | |
| 13 | + | [ "$status" -ne 0 ] | |
| 14 | + | [[ "$output" == *"unknown command"* ]] | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | @test "unknown command alias exits non-zero" { | |
| 18 | + | run "$ZMX" not-a-real-command | |
| 19 | + | [ "$status" -ne 0 ] | |
| 20 | + | [[ "$output" == *"unknown command"* ]] | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | @test "completions with invalid shell exits non-zero" { | |
| 24 | + | run "$ZMX" completions foo | |
| 25 | + | [ "$status" -ne 0 ] | |
| 26 | + | [[ "$output" == *"unknown shell"* ]] | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | @test "completions alias with invalid shell exits non-zero" { | |
| 30 | + | run "$ZMX" c foo | |
| 31 | + | [ "$status" -ne 0 ] | |
| 32 | + | [[ "$output" == *"unknown shell"* ]] | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | @test "completions with no argument exits non-zero" { | |
| 36 | + | run "$ZMX" completions | |
| 37 | + | [ "$status" -ne 0 ] | |
| 38 | + | [[ "$output" == *"shell argument"* ]] | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | @test "get with no argument outside session exits non-zero" { | |
| 42 | + | run env -u ZMX_SESSION "$ZMX" get | |
| 43 | + | [ "$status" -ne 0 ] | |
| 44 | + | [[ "$output" == *"session name required"* ]] | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | @test "set with no argument outside session exits non-zero" { | |
| 48 | + | run env -u ZMX_SESSION "$ZMX" set | |
| 49 | + | [ "$status" -ne 0 ] | |
| 50 | + | [[ "$output" == *"session name required"* ]] | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | @test "clear with no argument outside session exits non-zero" { | |
| 54 | + | run env -u ZMX_SESSION "$ZMX" clear | |
| 55 | + | [ "$status" -ne 0 ] | |
| 56 | + | [[ "$output" == *"session name required"* ]] | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | @test "detach outside session exits non-zero" { | |
| 60 | + | run env -u ZMX_SESSION "$ZMX" detach | |
| 61 | + | [ "$status" -ne 0 ] | |
| 62 | + | [[ "$output" == *"not inside a zmx session"* ]] | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | @test "history with no argument outside session exits non-zero" { | |
| 66 | + | run env -u ZMX_SESSION "$ZMX" history | |
| 67 | + | [ "$status" -ne 0 ] | |
| 68 | + | [[ "$output" == *"session name required"* ]] | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | @test "kill with no argument exits non-zero" { | |
| 72 | + | run "$ZMX" kill | |
| 73 | + | [ "$status" -ne 0 ] | |
| 74 | + | [[ "$output" == *"session name required"* ]] | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | @test "wait with no argument exits non-zero" { | |
| 78 | + | run "$ZMX" wait | |
| 79 | + | [ "$status" -ne 0 ] | |
| 80 | + | [[ "$output" == *"session name required"* ]] | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | @test "tail with no argument exits non-zero" { | |
| 84 | + | run "$ZMX" tail | |
| 85 | + | [ "$status" -ne 0 ] | |
| 86 | + | [[ "$output" == *"session name required"* ]] | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | @test "send with no argument exits non-zero" { | |
| 90 | + | run "$ZMX" send | |
| 91 | + | [ "$status" -ne 0 ] | |
| 92 | + | [[ "$output" == *"session name required"* ]] | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | @test "print with no argument exits non-zero" { | |
| 96 | + | run "$ZMX" print | |
| 97 | + | [ "$status" -ne 0 ] | |
| 98 | + | [[ "$output" == *"session name required"* ]] | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | @test "attach with missing labels value exits non-zero" { | |
| 102 | + | run "$ZMX" attach --labels | |
| 103 | + | [ "$status" -ne 0 ] | |
| 104 | + | [[ "$output" == *"--labels requires"* ]] | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | @test "print-env with no argument outside session exits non-zero" { | |
| 108 | + | run env -u ZMX_SESSION "$ZMX" print-env | |
| 109 | + | [ "$status" -ne 0 ] | |
| 110 | + | [[ "$output" == *"session name required"* ]] | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | @test "write with no argument exits non-zero" { | |
| 114 | + | run "$ZMX" write | |
| 115 | + | [ "$status" -ne 0 ] | |
| 116 | + | [[ "$output" == *"session name required"* ]] | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | @test "write with missing file path exits non-zero" { | |
| 120 | + | run "$ZMX" write dummy_session | |
| 121 | + | [ "$status" -ne 0 ] | |
| 122 | + | [[ "$output" == *"file path required"* ]] | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | # ============================================================================ | |
| 126 | + | # Valid commands still exit zero | |
| 127 | + | # ============================================================================ | |
| 128 | + | ||
| 129 | + | @test "help exits zero" { | |
| 130 | + | run "$ZMX" help | |
| 131 | + | [ "$status" -eq 0 ] | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | @test "version exits zero" { | |
| 135 | + | run "$ZMX" version | |
| 136 | + | [ "$status" -eq 0 ] | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | @test "completions with valid shell exits zero" { | |
| 140 | + | run "$ZMX" completions bash | |
| 141 | + | [ "$status" -eq 0 ] | |
| 142 | + | [[ "$output" == *"_zmx_completions"* ]] | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | @test "completions --help exits zero" { | |
| 146 | + | run "$ZMX" completions --help | |
| 147 | + | [ "$status" -eq 0 ] | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | @test "print-env --help exits zero" { | |
| 151 | + | run "$ZMX" print-env --help | |
| 152 | + | [ "$status" -eq 0 ] | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + |
+35,
-1
| ... | ... | @@ -87,6 +87,38 @@ load test_helper | |
| 87 | 87 | [ -z "$output" ] | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | + | @test "clear: cl alias works" { | |
| 91 | + | "$ZMX" run test-clear-alias -d sleep 30 | |
| 92 | + | wait_for_session test-clear-alias | |
| 93 | + | ||
| 94 | + | run "$ZMX" set test-clear-alias x=1 y=2 | |
| 95 | + | run "$ZMX" cl test-clear-alias | |
| 96 | + | [ "$status" -eq 0 ] | |
| 97 | + | ||
| 98 | + | run "$ZMX" get test-clear-alias | |
| 99 | + | [ "$status" -eq 0 ] | |
| 100 | + | [ -z "$output" ] | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | @test "set: missing key-value pairs exits non-zero" { | |
| 104 | + | "$ZMX" run test-nokv -d sleep 30 | |
| 105 | + | wait_for_session test-nokv | |
| 106 | + | ||
| 107 | + | run "$ZMX" set test-nokv | |
| 108 | + | [ "$status" -ne 0 ] | |
| 109 | + | [[ "$output" == *"key=value"* ]] | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | @test "get: missing key in existing session exits non-zero" { | |
| 113 | + | "$ZMX" run test-getkey -d sleep 30 | |
| 114 | + | wait_for_session test-getkey | |
| 115 | + | ||
| 116 | + | run "$ZMX" set test-getkey a=1 | |
| 117 | + | run "$ZMX" get test-getkey b | |
| 118 | + | [ "$status" -ne 0 ] | |
| 119 | + | [[ "$output" == *"not found"* ]] | |
| 120 | + | } | |
| 121 | + | ||
| 90 | 122 | @test "get: no session prints error" { | |
| 91 | 123 | run "$ZMX" get nonexistent | |
| 92 | 124 | [ "$status" -ne 0 ] |