summaryrefslogtreecommitdiff
path: root/9proc/demo/main.zig
diff options
context:
space:
mode:
Diffstat (limited to '9proc/demo/main.zig')
-rw-r--r--9proc/demo/main.zig423
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);
+}