//! 9proc: the demo 9P2000 server, built on the 9proc library. //! //! /README, /build/*, /comptime/{types,decls}, /runtime/{pid,ppid,uptime,argv,cwd,env,clients}, //! /runtime/fn/{fib30,hostname,now,random,uname}, /runtime/ctl (echo|fib|sleep-ms|add|trap|panic), //! /scratch (in-memory tree), /vars/state (the worker's exposed state), //! /threads, /addr, /mem, /hex, /breakpoints, /panic (the Linux debug layer). //! //! A worker thread ("worker") runs `workerLoop`, incrementing `state.ticks` //! every ~10 ms; `trap` makes it execute `@breakpoint()` on its next tick and //! `panic` makes it panic from inside `workerLoop`. Panics go through the //! library's hook, so the message and stack are published under /panic and //! the process is held there until /panic/ctl says "continue" (`--no-hold` //! disables the hold). //! //! Static memory: every buffer is a global; the only heap user is /scratch //! (init.gpa, 512 MiB budget). With `max_clients` = 16 and msize = 1 MiB the //! per-client core Storage is 3 MiB + 8 x 64 KiB snapshots and the Conn's fid //! table ~1.88 MiB (32768 fids plus their hash index, needed for the 20000-fid //! adversarial test), so `probe_storage` is ~86 MiB of BSS; untouched pages //! cost nothing (an idle server has an RSS of ~7 MiB). const std = @import("std"); const builtin = @import("builtin"); const cloud9 = @import("cloud9"); const build_options = @import("build_options"); const proc9 = @import("9proc"); const linux = std.os.linux; const Writer = std.Io.Writer; const plinux = proc9.linux; const runtime = plinux.runtime; pub const panic = std.debug.FullPanic(plinux.debug.panicHook); /// Simultaneous 9P clients; further connections are closed (see probe.zig for /// the idle-eviction rule). pub const max_clients = 16; pub const max_msize: u32 = 1 << 20; pub const State = struct { ticks: u64, phase: enum { idle, working, trapped }, last_job: struct { id: u32, cost: f32 }, }; const Build = struct { pub const zig_version: []const u8 = builtin.zig_version_string; pub const target: []const u8 = build_options.target; pub const optimize: []const u8 = build_options.optimize; pub const time: []const u8 = build_options.build_time; pub const change: []const u8 = build_options.change_id; }; /// What every generator and the ctl handler see (`Shared.ctx`). const App = struct { info: runtime.Info, probe: *ProbeT, shared: *S.Shared, }; /// /runtime/fn/: reading the file calls the function. pub const Fns = struct { pub fn hostname(_: *anyopaque, w: *Writer) anyerror!void { var u: linux.utsname = undefined; if (linux.errno(linux.uname(&u)) != .SUCCESS) return error.Uname; try w.writeAll(std.mem.sliceTo(&u.nodename, 0)); } pub fn now(_: *anyopaque, w: *Writer) anyerror!void { try w.print("{d}", .{runtime.realtimeSecs()}); } pub fn random(_: *anyopaque, w: *Writer) anyerror!void { var b: [8]u8 = undefined; var got: usize = 0; while (got < b.len) { const rc = linux.getrandom(b[got..].ptr, b.len - got, 0); switch (linux.errno(rc)) { .SUCCESS => got += rc, .INTR => continue, else => return error.Random, } } try w.print("{x:0>16}", .{std.mem.readInt(u64, &b, .little)}); } pub fn uname(_: *anyopaque, w: *Writer) anyerror!void { var u: linux.utsname = undefined; if (linux.errno(linux.uname(&u)) != .SUCCESS) return error.Uname; try w.writeAll(std.mem.sliceTo(&u.release, 0)); } pub fn fib30(_: *anyopaque, w: *Writer) anyerror!void { try w.print("{d}", .{fib(30)}); } }; const cfg: proc9.Config = .{ .name = "9proc-demo", .build = Build, .types = &.{ cloud9.Qid, cloud9.Stat, cloud9.Msg, proc9.core.Node, linux.Statx }, .decls_of = @This(), .fns = Fns, .runtime = runtime.Fns(App, "info"), .ctl = &ctl, .ctl_dir = .runtime, .ctl_bytes = 64 * 1024, .msize = max_msize, .max_fids = 32768, .max_providers = 8, .snapshot_slots = 8, .snapshot_bytes = 64 * 1024, }; const S = proc9.Server(cfg); const ProbeT = plinux.Probe(S); const ProbeStorage = ProbeT.Storage(max_clients); // -- static state ------------------------------------------------------------- pub var state: State = .{ .ticks = 0, .phase = .idle, .last_job = .{ .id = 0, .cost = 0 } }; var trap_requested: std.atomic.Value(bool) = .init(false); var panic_requested: std.atomic.Value(bool) = .init(false); /// Zero-filled static memory for `T`. (An `= undefined` global is emitted as /// 0xAA-filled .data in Debug builds, which would make the binary 120 MiB; /// zeros go to .bss and cost nothing until touched.) fn Bss(comptime T: type) type { return struct { bytes: [@sizeOf(T)]u8 align(@alignOf(T)) = @splat(0), fn get(b: *@This()) *T { return @ptrCast(&b.bytes); } }; } var app_mem: Bss(App) = .{}; var shared_mem: Bss(S.Shared) = .{}; var probe_storage_mem: Bss(ProbeStorage) = .{}; var probe_mem: Bss(ProbeT) = .{}; var scratch_mem: Bss(proc9.Scratch) = .{}; /// Sizes of the static pieces, for the report and `--help`. pub const static_bytes = @sizeOf(ProbeStorage) + @sizeOf(S.Shared) + @sizeOf(ProbeT); // -- the worker --------------------------------------------------------------- /// Ticks every ~10 ms; honours `trap` and `panic` requests from /runtime/ctl. pub noinline fn workerLoop() void { plinux.setThreadName("worker"); var job: u32 = 0; while (true) { napMs(10); state.ticks +%= 1; if (panic_requested.swap(false, .acq_rel)) { state.phase = .working; @panic("demo panic requested over 9p"); } if (trap_requested.swap(false, .acq_rel)) { state.phase = .trapped; @breakpoint(); state.phase = .idle; } if (state.ticks % 100 == 0) { job +%= 1; state.phase = .working; state.last_job = .{ .id = job, .cost = @as(f32, @floatFromInt(job % 7)) * 0.5 }; state.phase = .idle; } } } /// The worker's sleep, issued as a raw syscall from this file so that the /// thread's innermost frame (the first line of /threads//stack, which /// 9proc/test/debug.sh resolves through /addr) is in demo/main.zig rather than in std. /// EINTR (a capture signal) just ends the nap early. inline fn napMs(ms: u64) void { var req: linux.timespec = .{ .sec = @intCast(ms / 1000), .nsec = @intCast((ms % 1000) * 1_000_000) }; switch (builtin.cpu.arch) { .x86_64 => _ = asm volatile ("syscall" : [ret] "={rax}" (-> usize), : [number] "{rax}" (@intFromEnum(linux.SYS.nanosleep)), [arg1] "{rdi}" (@intFromPtr(&req)), [arg2] "{rsi}" (@as(usize, 0)), : .{ .rcx = true, .r11 = true, .memory = true }), .aarch64 => _ = asm volatile ("svc #0" : [ret] "={x0}" (-> usize), : [number] "{x8}" (@intFromEnum(linux.SYS.nanosleep)), [arg1] "{x0}" (@intFromPtr(&req)), [arg2] "{x1}" (@as(usize, 0)), : .{ .memory = true }), else => plinux.sleepMs(ms), } } // -- /runtime/ctl ------------------------------------------------------------- /// The /runtime/ctl handler. The core stages the output and commits it only /// on success, so a failed command leaves the previous result in place /// (test/adv_9proc_hostile.py checks that). fn ctl(ctx: *anyopaque, cmd: []const u8, out: *Writer) anyerror!void { const a: *App = @ptrCast(@alignCast(ctx)); const line = std.mem.trim(u8, cmd, " \t\r\n\x00"); var it = std.mem.tokenizeScalar(u8, line, ' '); const verb = it.next() orelse return error.BadCommand; if (std.mem.eql(u8, verb, "echo")) { try out.writeAll(std.mem.trimStart(u8, line[verb.len..], " \t")); } else if (std.mem.eql(u8, verb, "fib")) { const n = std.fmt.parseInt(u32, it.next() orelse return error.BadCommand, 10) catch return error.BadCommand; if (n > 93) return error.BadCommand; // fib(94) overflows u64 try out.print("{d}", .{fib(n)}); } else if (std.mem.eql(u8, verb, "sleep-ms")) { const n = std.fmt.parseInt(u64, it.next() orelse return error.BadCommand, 10) catch return error.BadCommand; const ms = @min(n, 10_000); a.probe.sleepServing(ms); try out.print("slept {d} ms", .{ms}); } else if (std.mem.eql(u8, verb, "add")) { const x = std.fmt.parseInt(i64, it.next() orelse return error.BadCommand, 10) catch return error.BadCommand; const y = std.fmt.parseInt(i64, it.next() orelse return error.BadCommand, 10) catch return error.BadCommand; try out.print("{d}", .{x +% y}); } else if (std.mem.eql(u8, verb, "trap")) { trap_requested.store(true, .release); try out.writeAll("trap armed: the worker stops in @breakpoint() on its next tick"); } else if (std.mem.eql(u8, verb, "panic")) { panic_requested.store(true, .release); try out.writeAll("panic armed: the worker panics on its next tick"); } else return error.BadCommand; } /// fib(n) for n <= 93 (fib(93) is the largest that fits u64). fn fib(n: u32) u64 { std.debug.assert(n <= 93); if (n == 0) return 0; var a: u64 = 0; var b: u64 = 1; for (1..n) |_| { const c = a + b; a = b; b = c; } return b; } // -- main --------------------------------------------------------------------- const usage_text = \\usage: 9proc-demo [--unix PATH | --tcp IP:PORT | --stdio] [--no-hold] \\ \\A demo 9P2000 file server exposing this binary's build-time, comptime and \\runtime facts, plus a debugger-shaped view of the process (threads, stacks, \\memory, breakpoints, panics). Default is --stdio (9P on fd 0/1). \\--no-hold lets a panic abort at once instead of waiting for /panic/ctl. \\ ; pub fn main(init: std.process.Init) !void { run(init) catch |e| switch (e) { // Already reported on stderr; no stack trace wanted. error.Usage, error.Syscall => std.process.exit(1), else => return e, }; } fn run(init: std.process.Init) !void { // Transparent huge pages would back the first touched page of every // client buffer with 2 MiB. Best effort: ignore failure. _ = linux.prctl(@intFromEnum(linux.PR.SET_THP_DISABLE), 1, 0, 0, 0); const arena = init.arena.allocator(); const args = try init.minimal.args.toSlice(arena); const Mode = enum { stdio, unix, tcp }; var mode: Mode = .stdio; var address: []const u8 = ""; var hold = true; var i: usize = 1; while (i < args.len) : (i += 1) { const a = args[i]; if (std.mem.eql(u8, a, "--stdio")) { mode = .stdio; } else if (std.mem.eql(u8, a, "--unix") or std.mem.eql(u8, a, "--tcp")) { i += 1; if (i >= args.len) { std.debug.print("9proc-demo: {s} needs an argument\n{s}", .{ a, usage_text }); return error.Usage; } mode = if (a[2] == 'u') .unix else .tcp; address = args[i]; } else if (std.mem.eql(u8, a, "--no-hold")) { hold = false; } else if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) { std.debug.print("{s}\nstatic memory: {d} bytes ({d} clients, msize {d})\n", .{ usage_text, static_bytes, max_clients, max_msize }); return; } else { std.debug.print("9proc-demo: unknown argument {s}\n{s}", .{ a, usage_text }); return error.Usage; } } // argv, env and cwd are gathered once, into the arena. var argv_text: std.ArrayList(u8) = .empty; for (args) |a| { try argv_text.appendSlice(arena, a); try argv_text.append(arena, '\n'); } var env_text: std.ArrayList(u8) = .empty; for (init.minimal.environ.block.view().slice) |entry| { try env_text.appendSlice(arena, std.mem.span(entry)); try env_text.append(arena, '\n'); } var cwd_buf: [4096]u8 = undefined; const cwd_rc = linux.getcwd(&cwd_buf, cwd_buf.len); const cwd_text: []const u8 = if (linux.errno(cwd_rc) == .SUCCESS) try arena.dupe(u8, std.mem.sliceTo(cwd_buf[0..cwd_rc], 0)) else ""; const app = app_mem.get(); const shared = shared_mem.get(); const probe = probe_mem.get(); const scratch = scratch_mem.get(); app.* = .{ .info = .{ .argv = argv_text.items, .env = env_text.items, .cwd = cwd_text, .start_mono = runtime.monotonicSecs() }, .probe = probe, .shared = shared, }; shared.* = .init(app); try shared.expose("state", &state); scratch.* = try proc9.Scratch.init(init.gpa, 512 << 20); // Freed on the way out so a Debug build's allocator does not report the // tree as leaked (with a stack trace on stderr) after a clean --stdio EOF. defer scratch.deinit(); scratch.max_file = 64 << 20; scratch.now = &runtime.realtimeSecs; try shared.addProvider(scratch.provider("scratch")); const listen: plinux.Listen = switch (mode) { .stdio => .{ .client = .{ .in = 0, .out = 1 } }, .unix => .{ .unix = address }, .tcp => .{ .tcp = address }, }; probe.init(shared, probe_storage_mem.get(), .{ .io = init.io, .listen = listen, .msize = max_msize, .hold_on_panic = hold }) catch |e| { switch (e) { error.PathTooLong => std.debug.print("9proc-demo: unix socket path too long\n", .{}), error.BadAddress => std.debug.print("9proc-demo: --tcp wants an IPv4 literal a.b.c.d:port\n", .{}), error.Syscall => std.debug.print("9proc-demo: {t} ({t})\n", .{ e, probe.last_errno }), else => std.debug.print("9proc-demo: {t}\n", .{e}), } return if (e == error.PathTooLong or e == error.BadAddress) error.Usage else error.Syscall; }; // Failure past this point (thread spawn) still closes the listener and // restores the signal dispositions. errdefer probe.stop(); app.info.clients = probe.clientCounter(); const worker = std.Thread.spawn(.{}, workerLoop, .{}) catch |e| { std.debug.print("9proc-demo: worker thread: {t}\n", .{e}); return error.Syscall; }; worker.detach(); switch (mode) { .unix => std.debug.print("9proc-demo: listening on unix!{s}\n", .{address}), .tcp => std.debug.print("9proc-demo: listening on tcp!{s}\n", .{address}), .stdio => {}, } probe.start() catch |e| { std.debug.print("9proc-demo: thread spawn failed: {t}\n", .{e}); return error.Syscall; }; // SIGTERM/SIGINT end the loop cleanly: the socket file is unlinked, the // signal dispositions restored and the scratch tree freed. // A disposition of SIG_IGN inherited from the parent is left alone (Unix // convention): 9ns runs a --spawn server with SIGINT ignored so that // Ctrl-C on the terminal reaches only the program, not its file server. const term: linux.Sigaction = .{ .handler = .{ .handler = onTerm }, .mask = linux.sigemptyset(), .flags = 0 }; for ([_]linux.SIG{ .TERM, .INT }) |sig| { var old: linux.Sigaction = undefined; _ = linux.sigaction(sig, null, &old); if (old.handler.handler != linux.SIG.IGN) _ = linux.sigaction(sig, &term, null); } probe.wait(); probe.stop(); } /// Async-signal-safe: an atomic store and one eventfd write. fn onTerm(_: linux.SIG) callconv(.c) void { probe_mem.get().requestStop(); } // -- tests -------------------------------------------------------------------- test "ctl commands" { const probe = probe_mem.get(); const shared = shared_mem.get(); var a: App = .{ .info = .{}, .probe = probe, .shared = shared }; probe.thread_tid = .init(0); probe.serving = .initEmpty(); probe.nested = 0; var buf: [128]u8 = undefined; var w: Writer = .fixed(&buf); try ctl(&a, "add 2 3\n", &w); try std.testing.expectEqualStrings("5", w.buffered()); try std.testing.expectError(error.BadCommand, ctl(&a, "nope", &w)); w = .fixed(&buf); try ctl(&a, "fib 93", &w); try std.testing.expectEqualStrings("12200160415121876738", w.buffered()); w = .fixed(&buf); try std.testing.expectError(error.BadCommand, ctl(&a, "fib 94", &w)); try std.testing.expectError(error.BadCommand, ctl(&a, "frobnicate", &w)); w = .fixed(&buf); try ctl(&a, "echo hi there ", &w); try std.testing.expectEqualStrings("hi there", w.buffered()); w = .fixed(&buf); try ctl(&a, "sleep-ms 1", &w); try std.testing.expectEqualStrings("slept 1 ms", w.buffered()); w = .fixed(&buf); try ctl(&a, "trap", &w); try std.testing.expect(trap_requested.swap(false, .acq_rel)); w = .fixed(&buf); try ctl(&a, "panic", &w); try std.testing.expect(panic_requested.swap(false, .acq_rel)); } test "static footprint is what the file comment says" { try std.testing.expect(@sizeOf(ProbeStorage) > 16 * (3 << 20)); try std.testing.expect(@sizeOf(ProbeStorage) < 100 << 20); }