diff options
Diffstat (limited to 'introspect/demo/main.zig')
| -rw-r--r-- | introspect/demo/main.zig | 423 |
1 files changed, 0 insertions, 423 deletions
diff --git a/introspect/demo/main.zig b/introspect/demo/main.zig deleted file mode 100644 index b1c2c57..0000000 --- a/introspect/demo/main.zig +++ /dev/null @@ -1,423 +0,0 @@ -//! introspect: the demo 9P2000 server, built on the introspect 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 introspect = @import("introspect"); -const linux = std.os.linux; -const Writer = std.Io.Writer; -const plinux = introspect.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: introspect.Config = .{ - .name = "introspect", - .build = Build, - .types = &.{ cloud9.Qid, cloud9.Stat, cloud9.Msg, introspect.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 = introspect.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(introspect.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 -/// introspect/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_introspect_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: introspect [--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("introspect: {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("introspect: 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 introspect.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("introspect: unix socket path too long\n", .{}), - error.BadAddress => std.debug.print("introspect: --tcp wants an IPv4 literal a.b.c.d:port\n", .{}), - error.Syscall => std.debug.print("introspect: {t} ({t})\n", .{ e, probe.last_errno }), - else => std.debug.print("introspect: {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("introspect: worker thread: {t}\n", .{e}); - return error.Syscall; - }; - worker.detach(); - - switch (mode) { - .unix => std.debug.print("introspect: listening on unix!{s}\n", .{address}), - .tcp => std.debug.print("introspect: listening on tcp!{s}\n", .{address}), - .stdio => {}, - } - probe.start() catch |e| { - std.debug.print("introspect: 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): 9player 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); -} |
