diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-19 23:28:22 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-19 23:28:22 -0300 |
| commit | ba996acfcad1698adbf4a1834fe50e73b1c6cab9 (patch) | |
| tree | 282ba00ce5b10d7416aecb9f2f0f0a439340a57d /9player/src/main.zig | |
| parent | b05abcba3ea09ea106ad28364c6e40a3ec31b890 (diff) | |
| download | cloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.tar.gz cloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.zip | |
Rename programs: 9player -> 9ns, introspect -> 9proc, app -> web (9web)
Directories, binaries, build options (-D9ns, -D9proc), step names, module
name (9proc), thread and fs names, env var NINEPLAYER_MOUNT -> NINE_MOUNT,
docs and test scripts. Browser assets move to web/static.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to '9player/src/main.zig')
| -rw-r--r-- | 9player/src/main.zig | 444 |
1 files changed, 0 insertions, 444 deletions
diff --git a/9player/src/main.zig b/9player/src/main.zig deleted file mode 100644 index 24990b9..0000000 --- a/9player/src/main.zig +++ /dev/null @@ -1,444 +0,0 @@ -//! 9player: mount a 9P2000 tree into a fresh user+mount namespace via FUSE -//! and run a program inside it. -//! -//! Exit codes: the child's status (128+sig if signalled); 125 for 9player's -//! own failures (usage, connect, attach, namespace/mount); 126/127 for exec -//! failures. - -const std = @import("std"); -const linux = std.os.linux; -const ns = @import("ns.zig"); -const nine = @import("nine.zig"); -const bridge = @import("bridge.zig"); - -const version_string = "9player 0.1.0"; - -const usage_text = - \\Usage: 9player [options] -- PROGRAM [ARGS...] - \\Transport (exactly one): - \\ --unix PATH Unix stream socket - \\ --tcp IP:PORT TCP (IPv4/IPv6 literal) - \\ --fd N already-connected inherited descriptor - \\ --spawn CMD run CMD (via /bin/sh -c) with a socketpair on its stdin/stdout - \\Options: - \\ --mount PATH mountpoint inside the new namespace (default /mnt/9p) - \\ --uname NAME 9P user name (default $USER, else "none") - \\ --aname NAME 9P tree to attach (default "") - \\ --msize BYTES maximum 9P message size to request (default 131072) - \\ --cache SECONDS attr/entry cache validity, may be fractional (default 1) - \\ --no-direct-io let the kernel cache file pages (trusts stat length) - \\ --debug trace FUSE and 9P operations on stderr - \\ --help, --version - \\PROGRAM defaults to $SHELL (else /bin/sh). The mountpoint is exported as $NINEPLAYER_MOUNT. - \\ -; - -const own_failure: u8 = 125; -/// Largest 9P message size we agree to request: the session allocates two -/// buffers of this size up front, before the server negotiates it down. -const max_msize: u32 = 16 * 1024 * 1024; - -/// Write `text` to stdout (informational output such as --help); errors are -/// ignored, there is nowhere better to report them. -fn printStdout(text: []const u8) void { - var off: usize = 0; - while (off < text.len) { - const rc = linux.write(1, text[off..].ptr, text.len - off); - switch (linux.errno(rc)) { - .SUCCESS => off += rc, - .INTR => continue, - else => return, - } - } -} - -const Config = struct { - address: ?nine.Address = null, - spawn_cmd: ?[]const u8 = null, - mount: []const u8 = "/mnt/9p", - uname: ?[]const u8 = null, - aname: []const u8 = "", - msize: u32 = 131072, - cache_ns: u64 = 1_000_000_000, - direct_io: bool = true, - debug: bool = false, - /// Empty means "default program". - program: []const []const u8 = &.{}, -}; - -const ParseResult = union(enum) { - run: Config, - /// Usage error, already reported on stderr; exit with this status. - exit: u8, - /// --help/--version: text for stdout, then exit 0. Printing is left to - /// `main` so that no test path writes to fd 1 (under `zig build test` - /// that is the test runner's protocol pipe). - info: []const u8, -}; - -fn usageError(comptime fmt: []const u8, args: anytype) ParseResult { - std.debug.print("9player: " ++ fmt ++ "\n(try 9player --help)\n", args); - return .{ .exit = own_failure }; -} - -fn parseArgs(arena: std.mem.Allocator, args: []const [:0]const u8) !ParseResult { - var cfg = Config{}; - var transports: usize = 0; - var i: usize = 1; - var program_start: ?usize = null; - while (i < args.len) : (i += 1) { - const arg: []const u8 = args[i]; - if (std.mem.eql(u8, arg, "--")) { - program_start = i + 1; - break; - } - if (!std.mem.startsWith(u8, arg, "--")) { - // A single-dash word is a typo for an option, not a program. - if (arg.len > 1 and arg[0] == '-') return usageError("unknown option {s} (options start with --)", .{arg}); - // A bare word starts PROGRAM, as if "--" were given. - program_start = i; - break; - } - // Split "--opt=value". - var name = arg; - var inline_value: ?[]const u8 = null; - if (std.mem.indexOfScalar(u8, arg, '=')) |eq| { - name = arg[0..eq]; - inline_value = arg[eq + 1 ..]; - } - const Opt = enum { unix, tcp, fd, spawn, mount, uname, aname, msize, cache, @"no-direct-io", debug, help, version, unknown }; - const opt = std.meta.stringToEnum(Opt, name[2..]) orelse .unknown; - switch (opt) { - .@"no-direct-io", .debug, .help, .version => if (inline_value != null) return usageError("{s} takes no value", .{name}), - .unknown => return usageError("unknown option {s}", .{name}), - else => {}, - } - const value: []const u8 = switch (opt) { - .@"no-direct-io", .debug, .help, .version, .unknown => "", - else => inline_value orelse blk: { - i += 1; - if (i >= args.len) return usageError("{s} needs a value", .{name}); - break :blk args[i]; - }, - }; - switch (opt) { - .unix => { - if (value.len == 0) return usageError("--unix wants a socket path", .{}); - cfg.address = .{ .unix = value }; - transports += 1; - }, - .tcp => { - cfg.address = parseTcp(value) orelse return usageError("--tcp wants IP:PORT (IPv6 as [ADDR]:PORT), got '{s}'", .{value}); - transports += 1; - }, - .fd => { - const n = std.fmt.parseInt(i32, value, 10) catch return usageError("--fd wants a number, got '{s}'", .{value}); - if (n < 0) return usageError("--fd wants a non-negative number", .{}); - cfg.address = .{ .fd = n }; - transports += 1; - }, - .spawn => { - if (value.len == 0) return usageError("--spawn wants a command", .{}); - cfg.spawn_cmd = value; - transports += 1; - }, - .mount => { - if (value.len == 0) return usageError("--mount wants a path", .{}); - cfg.mount = value; - }, - .uname => cfg.uname = value, - .aname => cfg.aname = value, - .msize => { - cfg.msize = std.fmt.parseInt(u32, value, 10) catch return usageError("--msize wants a number, got '{s}'", .{value}); - if (cfg.msize < 4096 or cfg.msize > max_msize) return usageError("--msize must be between 4096 and {d}", .{max_msize}); - }, - .cache => { - const secs = std.fmt.parseFloat(f64, value) catch return usageError("--cache wants seconds, got '{s}'", .{value}); - if (!(secs >= 0) or secs > 1e9) return usageError("--cache out of range", .{}); - cfg.cache_ns = @intFromFloat(secs * 1e9); - }, - .@"no-direct-io" => cfg.direct_io = false, - .debug => cfg.debug = true, - .help => return .{ .info = usage_text }, - .version => return .{ .info = version_string ++ "\n" }, - .unknown => unreachable, - } - } - if (transports == 0) return usageError("one transport is required (--unix, --tcp, --fd or --spawn)", .{}); - if (transports > 1) return usageError("exactly one transport is allowed", .{}); - if (program_start) |start| { - const prog = try arena.alloc([]const u8, args.len - start); - for (args[start..], 0..) |a, j| prog[j] = a; - cfg.program = prog; - } - return .{ .run = cfg }; -} - -fn parseTcp(spec: []const u8) ?nine.Address { - const colon = std.mem.lastIndexOfScalar(u8, spec, ':') orelse return null; - var host = spec[0..colon]; - if (host.len >= 2 and host[0] == '[' and host[host.len - 1] == ']') host = host[1 .. host.len - 1]; - if (host.len == 0) return null; - const port = std.fmt.parseInt(u16, spec[colon + 1 ..], 10) catch return null; - return .{ .tcp = .{ .host = host, .port = port } }; -} - -/// `--spawn`: run CMD under /bin/sh with one end of a socketpair as its -/// stdin/stdout; the other end is the 9P transport. -const Server = struct { pid: i32, fd: i32 }; - -fn spawnServer(cmd: [:0]const u8, envp: [*:null]const ?[*:0]const u8) !Server { - var sv: [2]i32 = undefined; - switch (linux.errno(linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0, &sv))) { - .SUCCESS => {}, - else => |e| { - std.debug.print("9player: socketpair: E{t}\n", .{e}); - return error.SystemResources; - }, - } - const rc = linux.fork(); - switch (linux.errno(rc)) { - .SUCCESS => {}, - else => |e| { - _ = linux.close(sv[0]); - _ = linux.close(sv[1]); - std.debug.print("9player: fork: E{t}\n", .{e}); - return error.SystemResources; - }, - } - if (rc == 0) { - // Child: dup2 clears CLOEXEC on 0 and 1; everything else is CLOEXEC. - if (linux.errno(linux.dup2(sv[1], 0)) != .SUCCESS or linux.errno(linux.dup2(sv[1], 1)) != .SUCCESS) linux.exit_group(125); - // The server shares our process group, so a Ctrl-C meant for the - // program would kill it and take the mount down with it: ignore the - // tty signals (inherited across exec). SIGPIPE goes back to its - // default, we only ignore it for ourselves. - ignoreSignal(.INT); - ignoreSignal(.QUIT); - defaultSignal(.PIPE); - const argv = [_:null]?[*:0]const u8{ "sh", "-c", cmd.ptr }; - const e = linux.errno(linux.execve("/bin/sh", &argv, envp)); - std.debug.print("9player: --spawn: exec /bin/sh: E{t}\n", .{e}); - linux.exit_group(127); - } - _ = linux.close(sv[1]); - return .{ .pid = @intCast(rc), .fd = sv[0] }; -} - -fn stopServer(server: ?Server) void { - const s = server orelse return; - _ = linux.kill(s.pid, .TERM); - ns.reapAny(s.pid); -} - -/// Fail early (before spawning servers or forking) if /dev/fuse is unusable. -fn probeFuseDevice() bool { - const rc = linux.open("/dev/fuse", .{ .ACCMODE = .RDWR, .CLOEXEC = true }, 0); - switch (linux.errno(rc)) { - .SUCCESS => { - _ = linux.close(@intCast(rc)); - return true; - }, - .NOENT => std.debug.print("9player: /dev/fuse: ENOENT (is the fuse module loaded? try: modprobe fuse)\n", .{}), - else => |e| std.debug.print("9player: open /dev/fuse: E{t}\n", .{e}), - } - return false; -} - -fn ignoreSignal(sig: linux.SIG) void { - const ign = linux.Sigaction{ .handler = .{ .handler = linux.SIG.IGN }, .mask = linux.sigemptyset(), .flags = 0 }; - std.posix.sigaction(sig, &ign, null); -} - -fn defaultSignal(sig: linux.SIG) void { - const dfl = linux.Sigaction{ .handler = .{ .handler = linux.SIG.DFL }, .mask = linux.sigemptyset(), .flags = 0 }; - std.posix.sigaction(sig, &dfl, null); -} - -/// `--fd N`: the descriptor is ours from now on; it must not leak into the -/// program (which could otherwise read 9P replies meant for us). Fails on a -/// bad descriptor, which is the earliest place to report it. -fn adoptFd(fd: i32) bool { - switch (linux.errno(linux.fcntl(fd, linux.F.SETFD, linux.FD_CLOEXEC))) { - .SUCCESS => return true, - else => |e| { - std.debug.print("9player: --fd {d}: E{t}\n", .{ fd, e }); - return false; - }, - } -} - -fn describeAddress(a: nine.Address, buf: []u8) []const u8 { - return switch (a) { - .unix => |p| std.fmt.bufPrint(buf, "unix socket {s}", .{p}) catch "unix socket", - .tcp => |t| std.fmt.bufPrint(buf, "tcp {s}:{d}", .{ t.host, t.port }) catch "tcp", - .fd => |fd| std.fmt.bufPrint(buf, "fd {d}", .{fd}) catch "fd", - }; -} - -pub fn main(init: std.process.Init) !u8 { - const gpa = init.gpa; - const arena = init.arena.allocator(); - const args = try init.minimal.args.toSlice(arena); - const envp: [*:null]const ?[*:0]const u8 = init.minimal.environ.block.slice.ptr; - - var cfg = switch (try parseArgs(arena, args)) { - .exit => |code| return code, - .info => |text| { - printStdout(text); - return 0; - }, - .run => |c| c, - }; - - // Defaults that come from the environment. - if (cfg.program.len == 0) { - const env_shell = ns.getenv(envp, "SHELL") orelse ""; - const shell = if (env_shell.len == 0) "/bin/sh" else env_shell; - cfg.program = try arena.dupe([]const u8, &.{shell}); - } - const uname = cfg.uname orelse ns.getenv(envp, "USER") orelse "none"; - const mountpoint = ns.resolveMountpoint(gpa, cfg.mount) catch |err| { - std.debug.print("9player: --mount {s}: {t}\n", .{ cfg.mount, err }); - return own_failure; - }; - defer gpa.free(mountpoint); - - if (!probeFuseDevice()) return own_failure; - - // Writes to a dead server socket must not kill us. - ignoreSignal(.PIPE); - - var server: ?Server = null; - var address: nine.Address = undefined; - if (cfg.spawn_cmd) |cmd| { - const cmd_z = try arena.dupeZ(u8, cmd); - server = spawnServer(cmd_z, envp) catch return own_failure; - ns.watchServer(server.?.pid); - address = .{ .fd = server.?.fd }; - } else { - address = cfg.address.?; - if (address == .fd and !adoptFd(address.fd)) return own_failure; - } - - var addr_buf: [256]u8 = undefined; - var session = nine.Session.connect(gpa, address, cfg.msize) catch |err| { - std.debug.print("9player: connect to {s}: {t}\n", .{ describeAddress(address, &addr_buf), err }); - stopServer(server); - return own_failure; - }; - defer session.deinit(); - defer stopServer(server); - - _ = session.attach(0, uname, cfg.aname) catch |err| { - switch (err) { - error.Nine => std.debug.print("9player: attach (uname={s}, aname='{s}'): {s}\n", .{ uname, cfg.aname, session.ename[0..session.ename_len] }), - else => std.debug.print("9player: attach: {t}\n", .{err}), - } - return own_failure; - }; - if (cfg.debug) std.debug.print("9player: attached to {s} (msize {d}), mounting on {s}\n", .{ describeAddress(address, &addr_buf), session.msize, mountpoint }); - - var child_pid: i32 = 0; - const stop_fd = ns.installSignals(&child_pid) catch return own_failure; - - const uid = linux.getuid(); - const gid = linux.getgid(); - const child = ns.spawn(gpa, .{ - .argv = cfg.program, - .envp = envp, - .mountpoint = mountpoint, - .uid = uid, - .gid = gid, - .max_read = bridge.max_write, - }) catch return own_failure; - - bridge.serve(gpa, child.fuse_fd, &session, 0, stop_fd, .{ - .uid = uid, - .gid = gid, - .attr_timeout_ns = cfg.cache_ns, - .direct_io = cfg.direct_io, - .debug = cfg.debug, - }) catch |err| switch (err) { - error.Closed => std.debug.print("9player: 9P server connection closed\n", .{}), - else => std.debug.print("9player: fuse: {t}\n", .{err}), - }; - - // Closing the device aborts the FUSE connection: anything still using - // the mount gets ENOTCONN instead of hanging on an unserved request. - _ = linux.close(child.fuse_fd); - - const status = ns.reapIfExited(child.pid) orelse ns.waitChild(child.pid) catch own_failure; - // An exec failure (126/127) is already in `status`; this prints its message. - _ = ns.reportExecFailure(child); - return status; -} - -test "parseTcp" { - const a = parseTcp("127.0.0.1:564").?; - try std.testing.expectEqualStrings("127.0.0.1", a.tcp.host); - try std.testing.expectEqual(@as(u16, 564), a.tcp.port); - const b = parseTcp("[::1]:9999").?; - try std.testing.expectEqualStrings("::1", b.tcp.host); - try std.testing.expectEqual(@as(u16, 9999), b.tcp.port); - try std.testing.expect(parseTcp("nohost") == null); - try std.testing.expect(parseTcp(":564") == null); - try std.testing.expect(parseTcp("1.2.3.4:") == null); - try std.testing.expect(parseTcp("1.2.3.4:70000") == null); -} - -test "parseArgs" { - const arena = std.testing.allocator; - { - const args = [_][:0]const u8{ "9player", "--unix", "/s", "--cache", "0.5", "--msize=8192", "--no-direct-io", "--", "sh", "-c", "x" }; - const r = try parseArgs(arena, &args); - defer arena.free(r.run.program); - try std.testing.expectEqualStrings("/s", r.run.address.?.unix); - try std.testing.expectEqual(@as(u64, 500_000_000), r.run.cache_ns); - try std.testing.expectEqual(@as(u32, 8192), r.run.msize); - try std.testing.expect(!r.run.direct_io); - try std.testing.expectEqual(@as(usize, 3), r.run.program.len); - try std.testing.expectEqualStrings("x", r.run.program[2]); - } - { - const args = [_][:0]const u8{ "9player", "--fd", "3" }; - const r = try parseArgs(arena, &args); - try std.testing.expectEqual(@as(i32, 3), r.run.address.?.fd); - try std.testing.expectEqual(@as(usize, 0), r.run.program.len); - try std.testing.expectEqualStrings("/mnt/9p", r.run.mount); - } - { - // Two transports, no transport, unknown option, missing value: all 125. - const two = [_][:0]const u8{ "9player", "--fd", "3", "--unix", "/s" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &two)).exit); - const none = [_][:0]const u8{ "9player", "--", "sh" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &none)).exit); - const unknown = [_][:0]const u8{ "9player", "--bogus" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &unknown)).exit); - const missing = [_][:0]const u8{ "9player", "--unix" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &missing)).exit); - const badcache = [_][:0]const u8{ "9player", "--fd", "3", "--cache", "abc" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &badcache)).exit); - // Empty values, a single-dash typo, and an msize that would allocate gigabytes. - const emptyunix = [_][:0]const u8{ "9player", "--unix=", "--", "sh" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &emptyunix)).exit); - const emptymount = [_][:0]const u8{ "9player", "--fd", "3", "--mount", "" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &emptymount)).exit); - const singledash = [_][:0]const u8{ "9player", "--fd", "3", "-mount", "/x" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &singledash)).exit); - const hugemsize = [_][:0]const u8{ "9player", "--fd", "3", "--msize", "4294967295" }; - try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &hugemsize)).exit); - const okmsize = [_][:0]const u8{ "9player", "--fd", "3", "--msize", "16777216" }; - try std.testing.expectEqual(@as(u32, 16777216), (try parseArgs(arena, &okmsize)).run.msize); - } - { - const ver = [_][:0]const u8{ "9player", "--version" }; - try std.testing.expectEqualStrings(version_string ++ "\n", (try parseArgs(arena, &ver)).info); - const help = [_][:0]const u8{ "9player", "--help" }; - try std.testing.expect(std.mem.startsWith(u8, (try parseArgs(arena, &help)).info, "Usage: 9player")); - } -} - -test { - _ = ns; -} |
