diff options
Diffstat (limited to '9proc/demo/main.zig')
| -rw-r--r-- | 9proc/demo/main.zig | 423 |
1 files changed, 423 insertions, 0 deletions
diff --git a/9proc/demo/main.zig b/9proc/demo/main.zig new file mode 100644 index 0000000..df06369 --- /dev/null +++ b/9proc/demo/main.zig @@ -0,0 +1,423 @@ +//! 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/<name>: 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/<tid>/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); +} |
