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