a73x

src/cli/mux.zig

Ref:   Size: 5.2 KiB   History

//! Top-level dispatcher for the `mux` binary. The first argument selects `d`
//! (daemon), `a` (JSON agent), or `web` (browser hub); every other argument is
//! handled by the client. There is no top-level flag grammar or command alias,
//! so `mux run` treats `run` as a hostname.
//!
//! The ssh askpass mode has no command word. It is selected through the
//! `SSH_ASKPASS` environment and receives the prompt as argv[1].
const std = @import("std");
const daemon = @import("main.zig");
const agent = @import("agent");
const hub = @import("webhub_main.zig");
const client = @import("mux_main.zig");
const askpass = @import("client").askpass;

/// The dispatch decision as a value, allowing it to be tested without exiting
/// a process.
const DispatchMode = enum { daemon, agent, hub, client, askpass };

fn modeOf(args: []const [:0]const u8, ask_sock: ?[]const u8) DispatchMode {
    if (args.len < 2) return .client;
    if (std.mem.eql(u8, args[1], "d")) return .daemon;
    if (std.mem.eql(u8, args[1], "a")) return .agent;
    if (std.mem.eql(u8, args[1], "web")) return .hub;
    // An explicit mode takes precedence over the askpass environment. This
    // keeps remote `mux d endpoint` invocations and commands typed inside an
    // SSH process tree from being mistaken for password prompts. Askpass is
    // valid only for the single prompt argument supplied by SSH.
    if (ask_sock != null and args.len == 2) return .askpass;
    return .client;
}

pub fn main() !u8 {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer if (gpa.deinit() == .leak)
        std.debug.print("mux: LEAK: allocations outlived deinit\n", .{});
    const alloc = gpa.allocator();

    const args = try std.process.argsAlloc(alloc);
    defer std.process.argsFree(alloc, args);
    if (try @import("client").resolver.helper(alloc, args[1..])) |code| return code;

    // Named modes receive a slice beginning with their mode word, matching the
    // argv shape expected by their parsers. The client has no mode word and
    // therefore receives the complete argv.
    const ask_sock = std.posix.getenv(askpass.sock_env);
    const ask_kind = askpass.Kind.of(std.posix.getenv(askpass.prompt_env));
    return switch (modeOf(args, ask_sock)) {
        .daemon => daemon.main(args[1..]),
        .agent => agent.main(args[1..]),
        .hub => hub.main(args[1..]),
        .client => client.main(args),
        // SSH reads the response from stdout, so that stream must contain only
        // the askpass result. The prompt-kind environment controls whether the
        // wall masks the displayed response.
        .askpass => askpass.helperMain(args[1], ask_sock.?, ask_kind, std.posix.STDOUT_FILENO),
    };
}

test "modeOf: the three mode words, and nothing else" {
    try std.testing.expectEqual(DispatchMode.daemon, modeOf(&.{ "mux", "d", "start" }, null));
    try std.testing.expectEqual(DispatchMode.agent, modeOf(&.{ "mux", "a", "status" }, null));
    try std.testing.expectEqual(DispatchMode.hub, modeOf(&.{ "mux", "web" }, null));
    try std.testing.expectEqual(DispatchMode.client, modeOf(&.{"mux"}, null));
    try std.testing.expectEqual(DispatchMode.client, modeOf(&.{ "mux", "box" }, null));
}

test "modeOf: the helper is named by ssh's variable, and a mode word still wins" {
    const sock = "/run/user/1000/mux-ask-7.sock";
    // The whole of what ssh hands a helper: one argument, the prompt.
    try std.testing.expectEqual(DispatchMode.askpass, modeOf(&.{ "mux", "box's password: " }, sock));
    // Without the askpass environment, the same argv is treated as a hostname.
    try std.testing.expectEqual(DispatchMode.client, modeOf(&.{ "mux", "box's password: " }, null));
    // Inside the tree, a mode word is still a mode: the remote command the
    // wall's ssh runs is `mux d endpoint`, and a user typing one at that
    // ssh's far end would otherwise get a prompt helper.
    try std.testing.expectEqual(DispatchMode.daemon, modeOf(&.{ "mux", "d", "endpoint" }, sock));
    try std.testing.expectEqual(DispatchMode.agent, modeOf(&.{ "mux", "a", "status" }, sock));
    try std.testing.expectEqual(DispatchMode.hub, modeOf(&.{ "mux", "web" }, sock));
    // SSH askpass supplies exactly one prompt argument. Longer invocations are
    // dispatched to the client even if they inherit the environment variable.
    try std.testing.expectEqual(DispatchMode.client, modeOf(&.{ "mux", "box", "-A" }, sock));
    try std.testing.expectEqual(DispatchMode.client, modeOf(&.{"mux"}, sock));
}

test "modeOf: `run` is a host, and no daemon verb has a top-level alias" {
    // There is no compatibility alias for the old bare `run` command. Older
    // daemons reject this binary during their version check before executing
    // it, and every unrecognized mode word remains a client target.
    try std.testing.expectEqual(DispatchMode.client, modeOf(&.{ "mux", "run" }, null));
    try std.testing.expectEqual(DispatchMode.client, modeOf(&.{ "mux", "run", "--resume-fd", "5" }, null));
}

test {
    std.testing.refAllDeclsRecursive(@This());
    // These imports make the child modules' tests reachable from this root test
    // artifact. The agent and webhub modules also have separate test artifacts.
    _ = @import("main.zig");
    _ = @import("mux_main.zig");
    _ = @import("webhub_main.zig");
}