From ba996acfcad1698adbf4a1834fe50e73b1c6cab9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sat, 19 Sep 2026 23:28:22 -0300 Subject: Rename programs: 9player -> 9ns, introspect -> 9proc, app -> web (9web) Directories, binaries, build options (-D9ns, -D9proc), step names, module name (9proc), thread and fs names, env var NINEPLAYER_MOUNT -> NINE_MOUNT, docs and test scripts. Browser assets move to web/static. Co-Authored-By: Claude Fable 5.1 --- introspect/src/core.zig | 2656 --------------------------------- introspect/src/freestanding_check.zig | 71 - introspect/src/linux/debug.zig | 1458 ------------------ introspect/src/linux/probe.zig | 829 ---------- introspect/src/linux/provider.zig | 604 -------- introspect/src/linux/runtime.zig | 90 -- introspect/src/root.zig | 24 - introspect/src/scratch.zig | 689 --------- introspect/src/vars.zig | 824 ---------- 9 files changed, 7245 deletions(-) delete mode 100644 introspect/src/core.zig delete mode 100644 introspect/src/freestanding_check.zig delete mode 100644 introspect/src/linux/debug.zig delete mode 100644 introspect/src/linux/probe.zig delete mode 100644 introspect/src/linux/provider.zig delete mode 100644 introspect/src/linux/runtime.zig delete mode 100644 introspect/src/root.zig delete mode 100644 introspect/src/scratch.zig delete mode 100644 introspect/src/vars.zig (limited to 'introspect/src') diff --git a/introspect/src/core.zig b/introspect/src/core.zig deleted file mode 100644 index 2707af2..0000000 --- a/introspect/src/core.zig +++ /dev/null @@ -1,2656 +0,0 @@ -//! The freestanding 9P2000 introspection engine: a static tree generated at -//! comptime from a `Config` (README, /build, /comptime, /runtime/fn, /ctl, -//! /vars) plus runtime `Provider`s mounted at the top level, served over a -//! `cloud9.Server` connection. No allocator, no OS, no threads: every buffer is -//! caller-owned (`Storage`, `Shared`, `Conn`), every table is sized at comptime. -//! See docs/LIBRARY.md. -const std = @import("std"); -const builtin = @import("builtin"); -const cloud9 = @import("cloud9"); -const vars = @import("vars.zig"); -const Writer = std.Io.Writer; - -/// Longest file name accepted in a create or rename. -pub const max_name: usize = 255; - -/// A dynamic-file generator (`Config.fns`, `Config.runtime`): writes the file's -/// content into `w` at open time (and again at each read from offset 0). -pub const Gen = *const fn (ctx: *anyopaque, w: *Writer) anyerror!void; -/// The /ctl command handler: `cmd` is the written text, `out` receives the -/// result that later reads of /ctl return. -pub const Ctl = *const fn (ctx: *anyopaque, cmd: []const u8, out: *Writer) anyerror!void; - -pub const Config = struct { - /// Appears in /README and as uid/gid/muid of every Stat. - name: []const u8 = "introspect", - /// A type whose pub decls `zig_version`, `target`, `optimize`, `time` - /// and `change` (all `[]const u8`) become the files of /build. `null` - /// omits /build. - build: ?type = null, - /// /comptime/types//{name,size,align,fields}. - types: []const type = &.{}, - /// /comptime/decls lists this type's pub decls (empty when null). - decls_of: ?type = null, - /// /runtime/fn/: every pub decl is a `fn (ctx: *anyopaque, w: *std.Io.Writer) anyerror!void`. - fns: type = struct {}, - /// /runtime/: same signature as `fns`, one level up (pid, uptime, ...). - runtime: type = struct {}, - /// The /ctl handler; `null` omits /ctl. - ctl: ?Ctl = null, - /// Where /ctl lives: the root or /runtime/ctl. - ctl_dir: enum { root, runtime } = .root, - /// Capacity of the ctl result (in `Shared`). - ctl_bytes: u32 = 4096, - /// Largest negotiable msize; sizes `Storage.in/out/data`. - msize: u32 = 8192, - max_fids: u16 = 64, - max_providers: u8 = 8, - max_vars: u8 = 32, - /// Dynamic file contents are generated at open time into per-fid snapshot - /// slots so that reads at arbitrary offsets are consistent. - snapshot_slots: u8 = 8, - snapshot_bytes: u32 = 16 * 1024, -}; - -/// Attributes of a provider node, filled by `VTable.stat` and `VTable.list`. -pub const NodeStat = struct { - /// Permission bits plus `cloud9.dmdir`/`dmappend`/`dmexcl`. - mode: u32, - length: u64 = 0, - atime: u32 = 0, - mtime: u32 = 0, - /// Becomes the qid version. - version: u32 = 0, - /// The entry name (`list`) or the node's own name (`stat`; ignored for the - /// provider root, whose name is the mount name). Must stay valid until the - /// provider's next call. - name: []const u8 = "", - /// Filled by `list`: the entry's handle. Not retained by the core. - handle: Provider.Handle = 0, - /// A stable identity for the qid path (low 56 bits), for providers whose - /// handles are not stable across the node's life (e.g. memory addresses - /// that an allocator may reuse). 0 means "the handle is the path". - path: u64 = 0, - - pub fn isDir(s: NodeStat) bool { - return s.mode & cloud9.dmdir != 0; - } -}; - -/// A runtime subtree mounted at a top-level name. -/// -/// Handle lifetime: every handle returned by `walk` or `create` is released by -/// the core with exactly one `clunk` (after `close` if the fid was open). The -/// root handle 0 is never obtained through `walk`, so providers must treat -/// `clunk(0)` as a no-op. `walk` must accept "." on any node, file or directory -/// (a fresh reference to the same node; the core clones fids with it), and ".." -/// on directories (except at the root, which the core resolves itself). -pub const Provider = struct { - name: []const u8, - ctx: *anyopaque, - vtable: *const VTable, - - /// Provider-defined node id; 0 = provider root. - pub const Handle = u64; - pub const root: Handle = 0; - - pub const Error = error{ NotFound, Exists, Perm, NotDir, IsDir, NotEmpty, BadOffset, NoSpace, Io, Unsupported, Excl }; - - pub const VTable = struct { - walk: *const fn (ctx: *anyopaque, parent: Handle, name: []const u8) Error!Handle, - stat: *const fn (ctx: *anyopaque, h: Handle, out: *NodeStat) Error!void, - /// The `index`-th entry of `dir`; false when done. - list: *const fn (ctx: *anyopaque, dir: Handle, index: usize, out: *NodeStat) Error!bool, - open: *const fn (ctx: *anyopaque, h: Handle, mode: u8) Error!void, - read: *const fn (ctx: *anyopaque, h: Handle, offset: u64, buf: []u8) Error!usize, - write: *const fn (ctx: *anyopaque, h: Handle, offset: u64, data: []const u8) Error!usize, - /// Returns the new node, already open with `mode`. - create: ?*const fn (ctx: *anyopaque, dir: Handle, name: []const u8, perm: u32, mode: u8) Error!Handle = null, - remove: ?*const fn (ctx: *anyopaque, h: Handle) Error!void = null, - /// Only name, length, mode and mtime can differ from the current stat - /// (the core has already checked the immutable fields and the name). - wstat: ?*const fn (ctx: *anyopaque, h: Handle, st: *const cloud9.Stat) Error!void = null, - /// An open fid on `h` was released (before `clunk`). - close: ?*const fn (ctx: *anyopaque, h: Handle) void = null, - /// A fid holding `h` was released (also on hangup and Tversion). - clunk: *const fn (ctx: *anyopaque, h: Handle) void, - }; -}; - -/// The Plan 9 error string for any error the engine or a provider can raise. -pub fn ename(err: anyerror) []const u8 { - return switch (err) { - error.NotFound, error.NoFile => "file does not exist", - error.Perm => "permission denied", - error.Exists => "file already exists", - error.NotEmpty => "directory not empty", - error.NotDir => "not a directory", - error.IsDir => "is a directory", - error.BadOffset => "bad offset", - error.NoSpace => "no space left on device", - error.Io => "i/o error", - error.Unsupported => "not supported", - error.Excl => "exclusive use file already open", - error.FidInUse => "fid in use", - error.UnknownFid => "unknown fid", - error.NotOpen => "file not open", - error.AlreadyOpen => "file already open", - error.AuthNotRequired => "authentication not required", - error.BadCommand => "bad command", - error.BadName => "bad file name", - error.Invalid, error.BadValue => "bad value", - error.TooManyFids => "too many fids", - error.NoSnapshot => "too many open dynamic files", - error.WriteFailed => "no space in buffer", - error.ReplyTooLarge => "reply too large for msize", - error.OutOfMemory => "out of memory", - else => "i/o error", - }; -} - -/// A "don't care" Twstat: every field left as it is. -pub const stat_dontcare: cloud9.Stat = .{ - .type = 0xFFFF, - .dev = 0xFFFF_FFFF, - .qid = .{ .type = 0xFF, .version = 0xFFFF_FFFF, .path = 0xFFFF_FFFF_FFFF_FFFF }, - .mode = 0xFFFF_FFFF, - .atime = 0xFFFF_FFFF, - .mtime = 0xFFFF_FFFF, - .length = 0xFFFF_FFFF_FFFF_FFFF, - .name = "", - .uid = "", - .gid = "", - .muid = "", -}; - -pub fn validName(name: []const u8) error{BadName}!void { - if (name.len == 0 or name.len > max_name) return error.BadName; - if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return error.BadName; - if (std.mem.indexOfAny(u8, name, "/\x00") != null) return error.BadName; -} - -/// "YYYY-MM-DDTHH:MM:SSZ" as unix seconds, or null. -pub fn parseIso8601(s: []const u8) ?u32 { - if (s.len != 20 or s[4] != '-' or s[7] != '-' or s[10] != 'T' or s[13] != ':' or s[16] != ':' or s[19] != 'Z') return null; - const y = std.fmt.parseInt(i64, s[0..4], 10) catch return null; - const mo = std.fmt.parseInt(i64, s[5..7], 10) catch return null; - const d = std.fmt.parseInt(i64, s[8..10], 10) catch return null; - const h = std.fmt.parseInt(i64, s[11..13], 10) catch return null; - const mi = std.fmt.parseInt(i64, s[14..16], 10) catch return null; - const sec = std.fmt.parseInt(i64, s[17..19], 10) catch return null; - if (mo < 1 or mo > 12 or d < 1 or d > 31 or h > 23 or mi > 59 or sec > 60) return null; - // Howard Hinnant's days_from_civil. - const yy = if (mo <= 2) y - 1 else y; - const era = @divFloor(yy, 400); - const yoe = yy - era * 400; - const mp = if (mo > 2) mo - 3 else mo + 9; - const doy = @divFloor(153 * mp + 2, 5) + d - 1; - const doe = yoe * 365 + @divFloor(yoe, 4) - @divFloor(yoe, 100) + doy; - const days = era * 146097 + doe - 719468; - const total = days * 86400 + h * 3600 + mi * 60 + sec; - if (total < 0 or total > std.math.maxInt(u32)) return null; - return @intCast(total); -} - -/// The last component of @typeName(T): "wire.Qid" -> "Qid". -pub fn shortTypeName(comptime T: type) []const u8 { - const full = @typeName(T); - const dot = std.mem.lastIndexOfScalar(u8, full, '.') orelse return full; - return full[dot + 1 ..]; -} - -/// The /comptime/types//fields text: "name: type @offset" per line. -pub fn fieldsText(comptime T: type) []const u8 { - comptime { - @setEvalBranchQuota(200_000); - var s: []const u8 = ""; - switch (@typeInfo(T)) { - .@"struct" => |info| for (info.fields) |f| { - if (info.layout == .@"packed") { - s = s ++ std.fmt.comptimePrint("{s}: {s} @{d}b\n", .{ f.name, @typeName(f.type), @bitOffsetOf(T, f.name) }); - } else if (f.is_comptime) { - s = s ++ std.fmt.comptimePrint("{s}: {s} (comptime)\n", .{ f.name, @typeName(f.type) }); - } else { - s = s ++ std.fmt.comptimePrint("{s}: {s} @{d}\n", .{ f.name, @typeName(f.type), @offsetOf(T, f.name) }); - } - }, - .@"union" => |info| for (info.fields) |f| { - s = s ++ f.name ++ ": " ++ @typeName(f.type) ++ "\n"; - }, - .@"enum" => |info| for (info.fields) |f| { - s = s ++ std.fmt.comptimePrint("{s} = {d}\n", .{ f.name, f.value }); - }, - else => s = @typeName(T) ++ "\n", - } - return s; - } -} - -fn declsText(comptime T: type) []const u8 { - comptime { - @setEvalBranchQuota(20_000); - const decls = switch (@typeInfo(T)) { - inline .@"struct", .@"union", .@"enum", .@"opaque" => |info| info.decls, - else => &[_]std.builtin.Type.Declaration{}, - }; - var s: []const u8 = ""; - for (decls) |d| s = s ++ d.name ++ "\n"; - return s; - } -} - -/// Wraps `Fns.` in a function of exactly the `Gen` signature. -fn genFor(comptime Fns: type, comptime name: []const u8) Gen { - return &struct { - fn g(ctx: *anyopaque, w: *Writer) anyerror!void { - return @field(Fns, name)(ctx, w); - } - }.g; -} - -/// A node of the static tree, described at comptime. -pub const Node = struct { - name: []const u8, - kind: Kind, - children: []const Node = &.{}, - content: []const u8 = "", - gen: ?Gen = null, - - pub const Kind = enum(u8) { dir, static, dynamic, ctl, vars }; - - fn isDir(n: Node) bool { - return n.kind == .dir or n.kind == .vars; - } -}; - -fn genNodes(comptime Fns: type) [@typeInfo(Fns).@"struct".decls.len]Node { - const decls = @typeInfo(Fns).@"struct".decls; - var arr: [decls.len]Node = undefined; - for (decls, 0..) |d, i| arr[i] = .{ .name = d.name, .kind = .dynamic, .gen = genFor(Fns, d.name) }; - return arr; -} - -pub fn Server(comptime cfg: Config) type { - return struct { - const Self = @This(); - - // -- the static tree ------------------------------------------------ - - pub const readme_text = std.fmt.comptimePrint( - \\{s}: a 9P2000 introspection server (built on cloud9). - \\ - \\/build facts baked in at build time (zig version, target, optimize, time, change id) - \\/comptime facts computed by the Zig compiler: type layouts under types//, pub decls - \\/runtime live facts; fn/ calls a Zig function on every read - \\/ctl write a command, read the result - \\/vars exposed variables: /{{value,type,size,addr,raw,f//...}} - \\ - \\Other top-level directories are providers mounted at runtime. - \\ - , .{cfg.name}); - - fn typeDir(comptime T: type) Node { - return .{ .name = shortTypeName(T), .kind = .dir, .children = &.{ - .{ .name = "name", .kind = .static, .content = @typeName(T) }, - .{ .name = "size", .kind = .static, .content = std.fmt.comptimePrint("{d}", .{@sizeOf(T)}) }, - .{ .name = "align", .kind = .static, .content = std.fmt.comptimePrint("{d}", .{@alignOf(T)}) }, - .{ .name = "fields", .kind = .static, .content = fieldsText(T) }, - } }; - } - - const type_dirs: [cfg.types.len]Node = blk: { - @setEvalBranchQuota(200_000); - var arr: [cfg.types.len]Node = undefined; - for (cfg.types, 0..) |T, i| arr[i] = typeDir(T); - for (arr, 0..) |a, i| for (arr[i + 1 ..]) |b| { - if (std.mem.eql(u8, a.name, b.name)) @compileError("duplicate short type name " ++ a.name); - }; - break :blk arr; - }; - - const decls_text: []const u8 = if (cfg.decls_of) |T| declsText(T) else ""; - const fn_nodes = genNodes(cfg.fns); - const runtime_nodes = genNodes(cfg.runtime); - const ctl_node: Node = .{ .name = "ctl", .kind = .ctl }; - - const build_nodes: []const Node = if (cfg.build) |B| &[_]Node{ - .{ .name = "zig_version", .kind = .static, .content = B.zig_version }, - .{ .name = "target", .kind = .static, .content = B.target }, - .{ .name = "optimize", .kind = .static, .content = B.optimize }, - .{ .name = "time", .kind = .static, .content = B.time }, - .{ .name = "change", .kind = .static, .content = B.change }, - } else &.{}; - - /// Build time as unix seconds (for static atime/mtime), or 0. - pub const build_secs: u32 = if (cfg.build) |B| (parseIso8601(B.time) orelse 0) else 0; - - const runtime_children: []const Node = blk: { - var list: []const Node = &runtime_nodes; - list = list ++ &[_]Node{.{ .name = "fn", .kind = .dir, .children = &fn_nodes }}; - if (cfg.ctl != null and cfg.ctl_dir == .runtime) list = list ++ &[_]Node{ctl_node}; - break :blk list; - }; - - const root_children: []const Node = blk: { - var list: []const Node = &[_]Node{.{ .name = "README", .kind = .static, .content = readme_text }}; - if (cfg.build != null) list = list ++ &[_]Node{.{ .name = "build", .kind = .dir, .children = build_nodes }}; - list = list ++ &[_]Node{ - .{ .name = "comptime", .kind = .dir, .children = &.{ - .{ .name = "types", .kind = .dir, .children = &type_dirs }, - .{ .name = "decls", .kind = .static, .content = decls_text }, - } }, - .{ .name = "runtime", .kind = .dir, .children = runtime_children }, - }; - if (cfg.ctl != null and cfg.ctl_dir == .root) list = list ++ &[_]Node{ctl_node}; - list = list ++ &[_]Node{.{ .name = "vars", .kind = .vars }}; - break :blk list; - }; - - pub const root_node: Node = .{ .name = "/", .kind = .dir, .children = root_children }; - - /// The static tree flattened so nodes can be referenced by index; the - /// children of a node occupy consecutive slots `first..first+count`. - const Flat = struct { node: Node, parent: u32, first: u32, count: u32 }; - - fn countNodes(n: Node) usize { - var c: usize = 1; - for (n.children) |ch| c += countNodes(ch); - return c; - } - - fn fillFlat(arr: []Flat, next: *usize, idx: usize, n: Node, parent: u32) void { - const first = next.*; - next.* += n.children.len; - arr[idx] = .{ .node = n, .parent = parent, .first = @intCast(first), .count = @intCast(n.children.len) }; - for (n.children, 0..) |ch, i| fillFlat(arr, next, first + i, ch, @intCast(idx)); - } - - pub const flat_len = countNodes(root_node); - pub const flat: [flat_len]Flat = blk: { - @setEvalBranchQuota(100_000); - var arr: [flat_len]Flat = undefined; - var next: usize = 1; - fillFlat(&arr, &next, 0, root_node, 0); - break :blk arr; - }; - const vars_idx: u32 = blk: { - for (flat, 0..) |f, i| if (f.node.kind == .vars) break :blk @intCast(i); - @compileError("no vars node"); - }; - - comptime { - for (flat) |f| if (f.node.kind == .dynamic and f.node.gen == null) @compileError("dynamic node without generator"); - std.debug.assert(cfg.msize >= cloud9.Server.msize_min); - std.debug.assert(cfg.snapshot_slots > 0 and cfg.max_fids > 0); - // Provider index 0xFE/0xFF would collide with the var/static qid tags. - std.debug.assert(cfg.max_providers < 0xFE); - } - - // -- qid paths ------------------------------------------------------ - - const static_tag: u64 = 0xFF << 56; - const var_tag: u64 = 0xFE << 56; - const handle_mask: u64 = (1 << 56) - 1; - - // -- storage -------------------------------------------------------- - - /// Per-connection buffers; the caller places one in static memory. - pub const Storage = struct { - in: [cfg.msize]u8, - out: [cfg.msize]u8, - /// Staging area for read replies (directory records, provider and raw reads). - data: [cfg.msize]u8, - snapshots: [cfg.snapshot_slots][cfg.snapshot_bytes]u8, - }; - - const Var = struct { - name: []const u8, - ptr: *anyopaque, - vt: *const vars.VTable, - }; - - /// State common to all connections: providers, exposed variables, the - /// ctl result. Not internally synchronized: one thread serves all - /// connections (or the caller serializes). - pub const Shared = struct { - ctx: *anyopaque, - providers: [cfg.max_providers]Provider = undefined, - nprov: u8 = 0, - vars: [cfg.max_vars]Var = undefined, - nvars: u8 = 0, - /// The ctl result is double-buffered: a command writes into the - /// buffer that is not current and commits it only on success, so - /// a failed command leaves the previous result intact. - ctl_bufs: [2][cfg.ctl_bytes]u8 = undefined, - ctl_cur: u1 = 0, - /// Length of the current ctl result (in `ctl_bufs[ctl_cur]`). - ctl_len: u32 = 0, - ctl_version: u32 = 0, - /// Entropy for the per-connection fid hash. The core mixes in a - /// connection counter and buffer addresses; a platform layer with a - /// random source may set this once after `init` to make the seed - /// unpredictable even where addresses are static. - hash_seed: u32 = 0, - conn_seq: u32 = 0, - - /// `ctx` is passed to every `fns`/`runtime` generator and to `ctl`. - pub fn init(ctx: *anyopaque) Shared { - return .{ .ctx = ctx }; - } - - /// Mounts `p` at `/`. The name must not collide with a - /// static entry or another provider. - pub fn addProvider(s: *Shared, p: Provider) error{Full}!void { - if (s.nprov == cfg.max_providers) return error.Full; - std.debug.assert(validName(p.name) != error.BadName); - std.debug.assert(s.findProvider(p.name) == null); - std.debug.assert(staticChild(0, p.name) == null); - s.providers[s.nprov] = p; - s.nprov += 1; - } - - /// Publishes `ptr.*` as /vars/. `name` and the pointee must - /// outlive the server. - pub fn expose(s: *Shared, name: []const u8, ptr: anytype) error{Full}!void { - const P = @TypeOf(ptr); - const info = @typeInfo(P); - if (info != .pointer or info.pointer.size != .one or info.pointer.is_const) @compileError("expose wants a *T, got " ++ @typeName(P)); - if (s.nvars == cfg.max_vars) return error.Full; - std.debug.assert(validName(name) != error.BadName); - std.debug.assert(s.findVar(name) == null); - s.vars[s.nvars] = .{ .name = name, .ptr = @ptrCast(ptr), .vt = vars.vtableFor(info.pointer.child) }; - s.nvars += 1; - } - - /// The result of the last successful ctl command. - pub fn ctlResult(s: *const Shared) []const u8 { - return s.ctl_bufs[s.ctl_cur][0..s.ctl_len]; - } - - fn findProvider(s: *const Shared, name: []const u8) ?u8 { - for (s.providers[0..s.nprov], 0..) |p, i| if (std.mem.eql(u8, p.name, name)) return @intCast(i); - return null; - } - - fn findVar(s: *const Shared, name: []const u8) ?u8 { - for (s.vars[0..s.nvars], 0..) |v, i| if (std.mem.eql(u8, v.name, name)) return @intCast(i); - return null; - } - }; - - fn staticChild(idx: u32, name: []const u8) ?u32 { - const f = flat[idx]; - for (f.first..f.first + f.count) |ci| { - if (std.mem.eql(u8, flat[ci].node.name, name)) return @intCast(ci); - } - return null; - } - - // -- connection ----------------------------------------------------- - - const NodeRef = union(enum) { - static: u32, - prov: struct { idx: u8, h: Provider.Handle }, - @"var": struct { idx: u8, node: u32 }, - }; - - const Fid = struct { - id: u32 = 0, - used: bool = false, - node: NodeRef = .{ .static = 0 }, - is_dir: bool = true, - open: bool = false, - mode: u8 = 0, - rclose: bool = false, - dir_offset: u64 = 0, - dir_index: usize = 0, - /// Snapshot slot of an open dynamic file. - snap: ?u8 = null, - /// Free-list link, meaningful while `!used`. - next_free: u16 = no_slot, - }; - - const no_slot: u16 = std.math.maxInt(u16); - /// The fid index is an open-addressing (linear probing) table from fid - /// number to a slot of `Conn.fids`, sized to stay at most half full so - /// that lookups are O(1) with any number of fids. - const index_len: usize = std.math.ceilPowerOfTwoAssert(usize, @as(usize, cfg.max_fids) * 2); - const index_mask: usize = index_len - 1; - const index_shift: u5 = @intCast(32 - @as(usize, std.math.log2_int(usize, index_len))); - - /// MurmurHash3's 32-bit finalizer: every input bit affects every output bit. - fn fmix32(x: u32) u32 { - var h = x; - h ^= h >> 16; - h *%= 0x85EB_CA6B; - h ^= h >> 13; - h *%= 0xC2B2_AE35; - h ^= h >> 16; - return h; - } - - /// Everything the engine needs to know about a node for qid/stat. - const Info = struct { - is_dir: bool, - mode: u32, - length: u64, - atime: u32, - mtime: u32, - version: u32, - path: u64, - name: []const u8, - - fn qid(i: Info) cloud9.Qid { - var t: u8 = if (i.is_dir) cloud9.qtdir else cloud9.qtfile; - if (i.mode & cloud9.dmappend != 0) t |= cloud9.qtappend; - if (i.mode & cloud9.dmexcl != 0) t |= cloud9.qtexcl; - return .{ .type = t, .version = i.version, .path = i.path }; - } - - fn stat(i: Info) cloud9.Stat { - return .{ - .type = 0, - .dev = 0, - .qid = i.qid(), - .mode = i.mode, - .atime = i.atime, - .mtime = i.mtime, - .length = i.length, - .name = i.name, - .uid = cfg.name, - .gid = cfg.name, - .muid = cfg.name, - }; - } - }; - - /// One 9P connection: a cloud9.Server plus a fid table and snapshot slots. - pub const Conn = struct { - shared: *Shared, - storage: *Storage, - server: cloud9.Server, - /// Largest msize this connection negotiates. - msize_cap: u32, - fids: [cfg.max_fids]Fid = @splat(.{}), - /// XORed into every fid number before hashing so that a client - /// cannot precompute fid numbers that collide (which would turn the - /// index back into a linear scan). - hash_seed: u32, - /// fid number -> slot of `fids` (`no_slot` = empty bucket). - index: [index_len]u16 = @splat(no_slot), - /// Head of the free list threaded through `Fid.next_free`. - free_head: u16 = no_slot, - /// Slots `high_water..` have never been used (bump allocation). - high_water: u16 = 0, - nfids: u16 = 0, - slot_used: [cfg.snapshot_slots]bool = @splat(false), - slot_len: [cfg.snapshot_slots]u32 = @splat(0), - name_buf: [max_name]u8 = undefined, - - pub fn init(shared: *Shared, storage: *Storage, msize: u32) Conn { - shared.conn_seq +%= 1; - const addr = @intFromPtr(storage) ^ (@intFromPtr(shared) << 7); - const seed = fmix32(shared.hash_seed ^ (shared.conn_seq *% 0x9E37_79B1) ^ @as(u32, @truncate(addr)) ^ @as(u32, @truncate(addr >> 16))); - return .{ - .shared = shared, - .storage = storage, - .server = .init(.{ .in = &storage.in, .out = &storage.out }), - .msize_cap = @max(@min(msize, cfg.msize), cloud9.Server.msize_min), - .hash_seed = seed, - }; - } - - /// Fibonacci hashing of the (seeded) fid number into `index_len` buckets. - fn fidHome(c: *const Conn, id: u32) usize { - return @intCast(((id ^ c.hash_seed) *% 0x9E37_79B1) >> index_shift); - } - - /// Feeds transport bytes; returns how many were taken. - pub fn push(c: *Conn, bytes: []const u8) usize { - return c.server.push(bytes); - } - - /// Bytes to send to the client. - pub fn output(c: *const Conn) []const u8 { - return c.server.output(); - } - - pub fn wrote(c: *Conn, n: usize) void { - c.server.wrote(n); - } - - /// Drops every fid (telling providers) and kills the session. - pub fn hangup(c: *Conn) void { - c.resetFids(); - c.server.hangup(); - } - - /// Handles at most one request. Returns false when more input (or - /// output drainage) is needed. `error.Protocol` is terminal. - pub fn step(c: *Conn) error{Protocol}!bool { - const req = (c.server.receive() catch return error.Protocol) orelse return false; - defer c.server.release(); - switch (req.msg) { - .tversion => |m| { - c.resetFids(); - c.server.negotiate(@min(m.msize, c.msize_cap), m.version) catch return error.Protocol; - }, - else => { - const reply = c.dispatch(req.msg) catch |e| cloud9.Msg{ .rerror = .{ .ename = ename(e) } }; - c.server.reply(req.tag, reply) catch |e| switch (e) { - // The reply does not fit the negotiated msize (Rstat or a long - // Rwalk at a tiny msize). receive() guarantees room for one - // msize-sized message, so this is never backpressure: answer with - // an Rerror (truncated to fit by cloud9). Rwalk, the only - // variable-size reply that follows a state change, is size-checked - // in walk() before anything is mutated. - error.TooLarge => c.server.reply(req.tag, .{ .rerror = .{ .ename = ename(error.ReplyTooLarge) } }) catch return error.Protocol, - else => return error.Protocol, - }; - }, - } - return true; - } - - /// Number of fids currently held. - pub fn fidCount(c: *const Conn) usize { - return c.nfids; - } - - fn dispatch(c: *Conn, msg: cloud9.Msg) anyerror!cloud9.Msg { - return switch (msg) { - .tauth => error.AuthNotRequired, - .tattach => |m| c.attach(m), - .tflush => .rflush, - .twalk => |m| c.walk(m), - .topen => |m| c.open(m), - .tcreate => |m| c.create(m), - .tread => |m| c.read(m), - .twrite => |m| c.write(m), - .tclunk => |m| c.clunk(m), - .tremove => |m| c.remove(m), - .tstat => |m| c.stat(m), - .twstat => |m| c.wstat(m), - else => error.Protocol, - }; - } - - // -- fid table -- - - /// The index bucket holding `id`, if any. - fn findBucket(c: *const Conn, id: u32) ?usize { - var pos = c.fidHome(id); - while (true) : (pos = (pos + 1) & index_mask) { - const slot = c.index[pos]; - if (slot == no_slot) return null; - if (c.fids[slot].id == id) return pos; - } - } - - fn findFid(c: *Conn, id: u32) ?*Fid { - const pos = c.findBucket(id) orelse return null; - return &c.fids[c.index[pos]]; - } - - fn allocFid(c: *Conn, id: u32) !*Fid { - if (c.findBucket(id) != null) return error.FidInUse; - if (c.nfids >= cfg.max_fids) return error.TooManyFids; - const slot: u16 = if (c.free_head != no_slot) blk: { - const slot = c.free_head; - c.free_head = c.fids[slot].next_free; - break :blk slot; - } else blk: { - const slot = c.high_water; - c.high_water += 1; - break :blk slot; - }; - c.fids[slot] = .{ .id = id, .used = true }; - var pos = c.fidHome(id); - while (c.index[pos] != no_slot) pos = (pos + 1) & index_mask; - c.index[pos] = slot; - c.nfids += 1; - return &c.fids[slot]; - } - - /// Removes `id` from the index (backward-shift deletion: no tombstones). - fn unlinkFid(c: *Conn, id: u32) void { - var i = c.findBucket(id).?; - var j = i; - while (true) { - j = (j + 1) & index_mask; - const slot = c.index[j]; - if (slot == no_slot) break; - const k = c.fidHome(c.fids[slot].id); - // The entry at j may move into the hole at i unless its home - // lies in the cyclic interval (i, j]. - const stays = if (i <= j) (k > i and k <= j) else (k > i or k <= j); - if (!stays) { - c.index[i] = slot; - i = j; - } - } - c.index[i] = no_slot; - } - - /// Releases everything a fid holds; the slot stays allocated. - fn dropContents(c: *Conn, f: *Fid) void { - if (f.snap) |s| c.slot_used[s] = false; - f.snap = null; - if (f.node == .prov) { - const p = c.shared.providers[f.node.prov.idx]; - if (f.open) if (p.vtable.close) |close| close(p.ctx, f.node.prov.h); - if (f.rclose and f.open) if (p.vtable.remove) |rm| rm(p.ctx, f.node.prov.h) catch {}; - p.vtable.clunk(p.ctx, f.node.prov.h); - } - f.open = false; - f.rclose = false; - } - - fn freeFid(c: *Conn, f: *Fid) void { - c.dropContents(f); - c.unlinkFid(f.id); - const slot: u16 = @intCast((@intFromPtr(f) - @intFromPtr(&c.fids)) / @sizeOf(Fid)); - f.* = .{ .next_free = c.free_head }; - c.free_head = slot; - c.nfids -= 1; - } - - fn resetFids(c: *Conn) void { - for (c.fids[0..c.high_water]) |*f| { - if (f.used) c.dropContents(f); - f.* = .{}; - } - @memset(&c.index, no_slot); - c.free_head = no_slot; - c.high_water = 0; - c.nfids = 0; - } - - /// Releases a provider handle that is not held by any fid. - fn releaseRef(c: *Conn, ref: NodeRef) void { - if (ref == .prov) { - const p = c.shared.providers[ref.prov.idx]; - p.vtable.clunk(p.ctx, ref.prov.h); - } - } - - // -- node helpers -- - - fn provider(c: *Conn, idx: u8) Provider { - return c.shared.providers[idx]; - } - - fn varBase(c: *Conn, idx: u8, node: u32) [*]u8 { - const v = c.shared.vars[idx]; - return @as([*]u8, @ptrCast(v.ptr)) + v.vt.nodes[node].offset; - } - - fn info(c: *Conn, ref: NodeRef) !Info { - switch (ref) { - .static => |idx| { - const n = flat[idx].node; - return .{ - .is_dir = n.isDir(), - .mode = switch (n.kind) { - .dir, .vars => cloud9.dmdir | 0o555, - .ctl => 0o666, - else => 0o444, - }, - .length = switch (n.kind) { - .static => n.content.len, - .ctl => c.shared.ctl_len, - else => 0, - }, - .atime = build_secs, - .mtime = build_secs, - .version = if (n.kind == .ctl) c.shared.ctl_version else 0, - .path = static_tag | idx, - .name = n.name, - }; - }, - .@"var" => |v| { - const sv = c.shared.vars[v.idx]; - const n = sv.vt.nodes[v.node]; - return .{ - .is_dir = n.isDir(), - .mode = if (n.isDir()) cloud9.dmdir | 0o555 else if (n.writable()) 0o644 else 0o444, - .length = switch (n.kind) { - .type_name, .size => n.content.len, - .raw => n.size, - else => 0, - }, - .atime = 0, - .mtime = 0, - .version = 0, - .path = var_tag | (@as(u64, v.idx) << 32) | v.node, - .name = if (v.node == 0) sv.name else n.name, - }; - }, - .prov => |p| { - const pr = c.provider(p.idx); - var st: NodeStat = .{ .mode = 0 }; - try pr.vtable.stat(pr.ctx, p.h, &st); - return provInfo(pr, p.idx, p.h, st); - }, - } - } - - fn provInfo(pr: Provider, idx: u8, h: Provider.Handle, st: NodeStat) Info { - return .{ - .is_dir = st.isDir(), - .mode = st.mode, - .length = if (st.isDir()) 0 else st.length, - .atime = st.atime, - .mtime = st.mtime, - .version = st.version, - .path = (@as(u64, idx) << 56) | ((if (st.path != 0) st.path else h) & handle_mask), - .name = if (h == Provider.root) pr.name else st.name, - }; - } - - /// Copies `st.name` into the connection so the reply cannot dangle. - fn pinName(c: *Conn, st: cloud9.Stat) cloud9.Stat { - var out = st; - const n = @min(st.name.len, c.name_buf.len); - @memcpy(c.name_buf[0..n], st.name[0..n]); - out.name = c.name_buf[0..n]; - return out; - } - - const Looked = struct { ref: NodeRef, info: Info }; - - /// Resolves `name` in the directory `ref`. A returned provider ref - /// is a fresh handle the caller must release or retain. - fn lookup(c: *Conn, ref: NodeRef, name: []const u8) !Looked { - switch (ref) { - .static => |idx| { - const dot = std.mem.eql(u8, name, "."); - const dotdot = std.mem.eql(u8, name, ".."); - var next: NodeRef = undefined; - if (dot) { - next = ref; - } else if (dotdot) { - next = .{ .static = flat[idx].parent }; - } else if (flat[idx].node.kind == .vars) { - const vi = c.shared.findVar(name) orelse return error.NotFound; - next = .{ .@"var" = .{ .idx = vi, .node = 0 } }; - } else if (staticChild(idx, name)) |ci| { - next = .{ .static = ci }; - } else if (idx == 0) { - const pi = c.shared.findProvider(name) orelse return error.NotFound; - next = .{ .prov = .{ .idx = pi, .h = Provider.root } }; - } else return error.NotFound; - return .{ .ref = next, .info = try c.info(next) }; - }, - .@"var" => |v| { - const vt = c.shared.vars[v.idx].vt; - var next = ref; - if (std.mem.eql(u8, name, ".")) { - // unchanged - } else if (std.mem.eql(u8, name, "..")) { - next = if (v.node == 0) .{ .static = vars_idx } else .{ .@"var" = .{ .idx = v.idx, .node = vt.nodes[v.node].parent } }; - } else { - const ci = vt.child(v.node, name) orelse return error.NotFound; - next = .{ .@"var" = .{ .idx = v.idx, .node = ci } }; - } - return .{ .ref = next, .info = try c.info(next) }; - }, - .prov => |p| { - if (p.h == Provider.root and std.mem.eql(u8, name, "..")) { - const next: NodeRef = .{ .static = 0 }; - return .{ .ref = next, .info = try c.info(next) }; - } - const pr = c.provider(p.idx); - const h = try pr.vtable.walk(pr.ctx, p.h, name); - const next: NodeRef = .{ .prov = .{ .idx = p.idx, .h = h } }; - errdefer c.releaseRef(next); - return .{ .ref = next, .info = try c.info(next) }; - }, - } - } - - /// The i-th entry of directory `ref` as a Stat, or null past the end. - /// The name borrows either static memory or the provider's NodeStat. - fn entryStat(c: *Conn, ref: NodeRef, i: usize) !?cloud9.Stat { - switch (ref) { - .static => |idx| { - const f = flat[idx]; - if (f.node.kind == .vars) { - if (i >= c.shared.nvars) return null; - return (try c.info(.{ .@"var" = .{ .idx = @intCast(i), .node = 0 } })).stat(); - } - if (i < f.count) return (try c.info(.{ .static = f.first + @as(u32, @intCast(i)) })).stat(); - if (idx == 0) { - const pi = i - f.count; - if (pi >= c.shared.nprov) return null; - return (try c.info(.{ .prov = .{ .idx = @intCast(pi), .h = Provider.root } })).stat(); - } - return null; - }, - .@"var" => |v| { - const n = c.shared.vars[v.idx].vt.nodes[v.node]; - if (i >= n.count) return null; - return (try c.info(.{ .@"var" = .{ .idx = v.idx, .node = n.first + @as(u32, @intCast(i)) } })).stat(); - }, - .prov => |p| { - const pr = c.provider(p.idx); - var st: NodeStat = .{ .mode = 0 }; - if (!try pr.vtable.list(pr.ctx, p.h, i, &st)) return null; - return provInfo(pr, p.idx, st.handle, st).stat(); - }, - } - } - - // -- snapshots -- - - fn takeSlot(c: *Conn) !u8 { - for (&c.slot_used, 0..) |*u, i| if (!u.*) { - u.* = true; - return @intCast(i); - }; - return error.NoSnapshot; - } - - /// (Re)generates the content of a dynamic file into its slot. - fn generate(c: *Conn, f: *Fid) !void { - const s = f.snap.?; - var w: Writer = .fixed(&c.storage.snapshots[s]); - c.slot_len[s] = 0; - switch (f.node) { - .static => |idx| try flat[idx].node.gen.?(c.shared.ctx, &w), - .@"var" => |v| { - const n = c.shared.vars[v.idx].vt.nodes[v.node]; - const base = c.varBase(v.idx, v.node); - switch (n.kind) { - .value => try n.render.?(base, &w), - .addr => try w.print("0x{x}", .{@intFromPtr(base)}), - else => unreachable, - } - }, - .prov => unreachable, - } - c.slot_len[s] = @intCast(w.buffered().len); - } - - fn isDynamic(c: *Conn, ref: NodeRef) bool { - return switch (ref) { - .static => |idx| flat[idx].node.kind == .dynamic, - .@"var" => |v| switch (c.shared.vars[v.idx].vt.nodes[v.node].kind) { - .value, .addr => true, - else => false, - }, - .prov => false, - }; - } - - // -- request handlers -- - - fn attach(c: *Conn, m: anytype) !cloud9.Msg { - const f = try c.allocFid(m.fid); - f.node = .{ .static = 0 }; - f.is_dir = true; - return .{ .rattach = .{ .qid = (try c.info(f.node)).qid() } }; - } - - fn walk(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - if (m.newfid != m.fid and c.findFid(m.newfid) != null) return error.FidInUse; - if (m.newfid != m.fid and c.nfids >= cfg.max_fids) return error.TooManyFids; - if (m.nwname > 0 and f.open) return error.AlreadyOpen; - // Cloning a fid onto itself changes nothing; in particular it must not - // close an open fid or discard generated content. - if (m.nwname == 0 and m.newfid == m.fid) return .{ .rwalk = .{ .nwqid = 0 } }; - // A full Rwalk must fit the negotiated msize; check before binding anything. - if (cloud9.header_len + 2 + cloud9.qid_len * @as(usize, m.nwname) > c.server.msize) return error.ReplyTooLarge; - var cur = f.node; - var cur_is_dir = f.is_dir; - var held = false; // cur is a provider handle obtained here, not the fid's - var reply: cloud9.Msg = .{ .rwalk = .{ .nwqid = 0 } }; - const names = m.wname[0..m.nwname]; - for (names, 0..) |name, i| { - if (!cur_is_dir) { - if (i == 0) return error.NotDir; - break; - } - const next = c.lookup(cur, name) catch |e| { - if (i == 0) return e; - break; - }; - if (held) c.releaseRef(cur); - cur = next.ref; - cur_is_dir = next.info.is_dir; - held = cur == .prov; - reply.rwalk.wqid[i] = next.info.qid(); - reply.rwalk.nwqid += 1; - } - if (reply.rwalk.nwqid != names.len) { - if (held) c.releaseRef(cur); - return reply; - } - if (names.len == 0 and cur == .prov) { - // A clone of a provider handle needs its own reference. - const dup = try c.lookup(cur, "."); - cur = dup.ref; - cur_is_dir = dup.info.is_dir; - held = true; - } - const target = if (m.newfid == m.fid) f else c.allocFid(m.newfid) catch |e| { - if (held) c.releaseRef(cur); - return e; - }; - if (target == f) c.dropContents(f); - target.node = cur; - target.is_dir = cur_is_dir; - return reply; - } - - fn open(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - if (f.open) return error.AlreadyOpen; - const acc = m.mode & 3; - const want_write = acc == cloud9.owrite or acc == cloud9.ordwr; - const trunc = m.mode & cloud9.otrunc != 0; - if (f.is_dir and (want_write or trunc)) return error.IsDir; - switch (f.node) { - .static => |idx| switch (flat[idx].node.kind) { - .dir, .vars, .ctl => {}, - .static, .dynamic => if (want_write or trunc) return error.Perm, - }, - .@"var" => |v| { - const n = c.shared.vars[v.idx].vt.nodes[v.node]; - if ((want_write or trunc) and !n.writable()) return error.Perm; - }, - .prov => |p| { - const pr = c.provider(p.idx); - try pr.vtable.open(pr.ctx, p.h, m.mode); - }, - } - const qid = (c.info(f.node) catch |e| { - // The provider's open succeeded but its stat did not: undo the open. - if (f.node == .prov) { - const pr = c.provider(f.node.prov.idx); - if (pr.vtable.close) |close| close(pr.ctx, f.node.prov.h); - } - return e; - }).qid(); - if (c.isDynamic(f.node)) { - f.snap = try c.takeSlot(); - c.generate(f) catch |e| { - c.slot_used[f.snap.?] = false; - f.snap = null; - if (f.node == .prov) unreachable; - return e; - }; - } - f.open = true; - f.mode = m.mode; - f.rclose = m.mode & cloud9.orclose != 0; - f.dir_offset = 0; - f.dir_index = 0; - return .{ .ropen = .{ .qid = qid, .iounit = 0 } }; - } - - fn create(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - if (f.open) return error.AlreadyOpen; - const p = switch (f.node) { - .prov => |p| p, - else => return error.Perm, - }; - if (!f.is_dir) return error.NotDir; - const pr = c.provider(p.idx); - const create_fn = pr.vtable.create orelse return error.Perm; - try validName(m.name); - const is_dir = m.perm & cloud9.dmdir != 0; - const acc = m.mode & 3; - if (is_dir and (acc != cloud9.oread or m.mode & cloud9.otrunc != 0)) return error.IsDir; - const h = try create_fn(pr.ctx, p.h, m.name, m.perm, m.mode); - const node: NodeRef = .{ .prov = .{ .idx = p.idx, .h = h } }; - const qid = (c.info(node) catch |e| { - if (pr.vtable.close) |close| close(pr.ctx, h); - pr.vtable.clunk(pr.ctx, h); - return e; - }).qid(); - c.dropContents(f); - f.node = node; - f.is_dir = is_dir; - f.open = true; - f.mode = m.mode; - f.rclose = m.mode & cloud9.orclose != 0; - f.dir_offset = 0; - f.dir_index = 0; - return .{ .rcreate = .{ .qid = qid, .iounit = 0 } }; - } - - fn read(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - if (!f.open or (f.mode & 3) == cloud9.owrite) return error.NotOpen; - const count: usize = @min(m.count, c.server.msize -| cloud9.iohdrsz, c.storage.data.len); - if (f.is_dir) return c.readDir(f, m.offset, count); - const data = &c.storage.data; - const src: []const u8 = switch (f.node) { - .static => |idx| blk: { - const n = flat[idx].node; - switch (n.kind) { - .static => break :blk n.content, - .ctl => break :blk c.shared.ctlResult(), - .dynamic => { - if (m.offset == 0) try c.generate(f); - break :blk c.storage.snapshots[f.snap.?][0..c.slot_len[f.snap.?]]; - }, - .dir, .vars => unreachable, - } - }, - .@"var" => |v| blk: { - const n = c.shared.vars[v.idx].vt.nodes[v.node]; - switch (n.kind) { - .type_name, .size => break :blk n.content, - .value, .addr => { - if (m.offset == 0) try c.generate(f); - break :blk c.storage.snapshots[f.snap.?][0..c.slot_len[f.snap.?]]; - }, - .raw => break :blk c.varBase(v.idx, v.node)[0..n.size], - .dir, .fields => unreachable, - } - }, - .prov => |p| { - const pr = c.provider(p.idx); - const n = try pr.vtable.read(pr.ctx, p.h, m.offset, data[0..count]); - return .{ .rread = .{ .data = data[0..@min(n, count)] } }; - }, - }; - if (m.offset >= src.len) return .{ .rread = .{ .data = "" } }; - const off: usize = @intCast(m.offset); - const n = @min(count, src.len - off); - if (f.node == .@"var" and c.shared.vars[f.node.@"var".idx].vt.nodes[f.node.@"var".node].kind == .raw) { - // Copy out of the variable so the reply does not read live memory twice. - @memcpy(data[0..n], src[off..][0..n]); - return .{ .rread = .{ .data = data[0..n] } }; - } - return .{ .rread = .{ .data = src[off..][0..n] } }; - } - - fn readDir(c: *Conn, f: *Fid, offset: u64, count: usize) !cloud9.Msg { - if (offset == 0) { - f.dir_offset = 0; - f.dir_index = 0; - } else if (offset != f.dir_offset) return error.BadOffset; - const data = &c.storage.data; - var used: usize = 0; - var i = f.dir_index; - while (try c.entryStat(f.node, i)) |st| : (i += 1) { - const rec = st.encode(data[used..count]) catch |e| switch (e) { - error.NoSpace => break, - else => return error.Io, - }; - used += rec.len; - } - f.dir_offset += used; - f.dir_index = i; - return .{ .rread = .{ .data = data[0..used] } }; - } - - fn write(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - const acc = f.mode & 3; - if (!f.open or (acc != cloud9.owrite and acc != cloud9.ordwr)) return error.NotOpen; - if (f.is_dir) return error.IsDir; - switch (f.node) { - .static => |idx| switch (flat[idx].node.kind) { - .ctl => try c.ctlCommand(m.data), - else => return error.Perm, - }, - .@"var" => |v| { - const n = c.shared.vars[v.idx].vt.nodes[v.node]; - const set = n.set orelse return error.Perm; - try set(c.varBase(v.idx, v.node), m.data); - }, - .prov => |p| { - const pr = c.provider(p.idx); - const n = try pr.vtable.write(pr.ctx, p.h, m.offset, m.data); - return .{ .rwrite = .{ .count = @intCast(@min(n, m.data.len)) } }; - }, - } - return .{ .rwrite = .{ .count = @intCast(m.data.len) } }; - } - - /// Runs `cfg.ctl`; on success its output becomes the ctl result. - /// On failure the previous result (and its qid version) survive: - /// the handler writes into the staging half of `ctl_bufs`. - fn ctlCommand(c: *Conn, line: []const u8) !void { - const s = c.shared; - const next = s.ctl_cur ^ 1; - var w: Writer = .fixed(&s.ctl_bufs[next]); - try cfg.ctl.?(s.ctx, line, &w); - s.ctl_cur = next; - s.ctl_len = @intCast(w.buffered().len); - s.ctl_version +%= 1; - } - - fn clunk(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - c.freeFid(f); - return .rclunk; - } - - fn remove(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - defer c.freeFid(f); // Tremove always clunks - f.rclose = false; - switch (f.node) { - .prov => |p| { - const pr = c.provider(p.idx); - const rm = pr.vtable.remove orelse return error.Perm; - try rm(pr.ctx, p.h); - }, - else => return error.Perm, - } - return .rremove; - } - - fn stat(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - return .{ .rstat = .{ .stat = c.pinName((try c.info(f.node)).stat()) } }; - } - - fn wstat(c: *Conn, m: anytype) !cloud9.Msg { - const f = c.findFid(m.fid) orelse return error.UnknownFid; - const p = switch (f.node) { - .prov => |p| p, - else => return error.Perm, - }; - const pr = c.provider(p.idx); - const ws = pr.vtable.wstat orelse return error.Perm; - const cur = try c.info(f.node); - const st = m.stat; - const q = cur.qid(); - // Fields we cannot change must be "don't care" or unchanged. - if (st.type != 0xFFFF and st.type != 0) return error.Perm; - if (st.dev != 0xFFFF_FFFF and st.dev != 0) return error.Perm; - if (st.qid.type != 0xFF and st.qid.type != q.type) return error.Perm; - if (st.qid.version != 0xFFFF_FFFF and st.qid.version != q.version) return error.Perm; - if (st.qid.path != 0xFFFF_FFFF_FFFF_FFFF and st.qid.path != q.path) return error.Perm; - if (st.uid.len != 0 and !std.mem.eql(u8, st.uid, cfg.name)) return error.Perm; - if (st.gid.len != 0 and !std.mem.eql(u8, st.gid, cfg.name)) return error.Perm; - if (st.muid.len != 0 and !std.mem.eql(u8, st.muid, cfg.name)) return error.Perm; - if (st.name.len != 0 and !std.mem.eql(u8, st.name, cur.name)) { - if (p.h == Provider.root) return error.Perm; - try validName(st.name); - } - if (st.length != 0xFFFF_FFFF_FFFF_FFFF and st.length != cur.length and cur.is_dir) return error.IsDir; - if (st.mode != 0xFFFF_FFFF and (st.mode & cloud9.dmdir) != (cur.mode & cloud9.dmdir)) return error.Perm; - try ws(pr.ctx, p.h, &st); - return .rwstat; - } - }; - - // -- in-memory test harness ------------------------------------------- - - /// Drives a `Conn` with a `cloud9.Client` in memory. Test-only (uses - /// std.testing.allocator); never referenced by non-test code. - pub const Harness = struct { - shared: *Shared, - storage: *Storage, - conn: Conn, - client: cloud9.Client, - cin: []u8, - cout: []u8, - - pub fn init(h: *Harness, shared: *Shared, storage: *Storage) !void { - h.shared = shared; - h.storage = storage; - h.conn = .init(shared, storage, cfg.msize); - h.cin = try testing.allocator.alloc(u8, cfg.msize); - errdefer testing.allocator.free(h.cin); - h.cout = try testing.allocator.alloc(u8, cfg.msize); - errdefer testing.allocator.free(h.cout); - h.client = .init(.{ .in = h.cin, .out = h.cout }); - try h.version(cfg.msize); - _ = try h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } }); - } - - pub fn deinit(h: *Harness) void { - h.conn.hangup(); - testing.allocator.free(h.cin); - testing.allocator.free(h.cout); - } - - pub fn version(h: *Harness, msize: u32) !void { - const v = try h.rpc(.{ .version = .{ .msize = msize } }); - try testing.expectEqual(msize, v.version.msize); - try testing.expectEqualStrings("9P2000", v.version.version); - } - - /// One round trip; the result borrows the client input buffer until the next call. - pub fn rpc(h: *Harness, req: cloud9.Client.Request) !cloud9.Client.Result { - _ = try h.client.submit(req); - while (true) { - var moved = false; - while (h.client.output().len > 0) { - const k = h.conn.push(h.client.output()); - h.client.wrote(k); - moved = moved or k > 0; - while (try h.conn.step()) {} - while (h.conn.output().len > 0) { - const n = h.client.push(h.conn.output()); - h.conn.wrote(n); - moved = moved or n > 0; - } - if (k == 0) break; - } - while (try h.conn.step()) {} - while (h.conn.output().len > 0) { - const n = h.client.push(h.conn.output()); - h.conn.wrote(n); - moved = moved or n > 0; - } - if (h.client.take()) |done| return done.result; - if (!moved) return error.Stuck; - } - } - - pub fn ok(h: *Harness, req: cloud9.Client.Request) !cloud9.Client.Result { - const r = try h.rpc(req); - if (r == .fail) { - std.debug.print("unexpected Rerror: {s}\n", .{r.fail}); - return error.Rerror; - } - return r; - } - - pub fn expectFail(h: *Harness, req: cloud9.Client.Request, msg: []const u8) !void { - const r = try h.rpc(req); - if (r != .fail) return error.ExpectedRerror; - try testing.expectEqualStrings(msg, r.fail); - } - - pub fn walkTo(h: *Harness, newfid: u32, names: []const []const u8) !void { - const r = try h.ok(.{ .walk = .{ .fid = 0, .newfid = newfid, .names = names } }); - try testing.expectEqual(@as(u16, @intCast(names.len)), r.walk.nwqid); - } - - /// Opens `fid` for reading and reads it whole (across consecutive offsets); caller frees. - pub fn readAll(h: *Harness, fid: u32) ![]u8 { - _ = try h.ok(.{ .open = .{ .fid = fid, .mode = cloud9.oread } }); - return h.readOpen(fid); - } - - pub fn readOpen(h: *Harness, fid: u32) ![]u8 { - var acc: std.ArrayList(u8) = .empty; - errdefer acc.deinit(testing.allocator); - while (true) { - const r = try h.ok(.{ .read = .{ .fid = fid, .offset = acc.items.len, .count = 1024 } }); - if (r.read.len == 0) break; - try acc.appendSlice(testing.allocator, r.read); - } - return acc.toOwnedSlice(testing.allocator); - } - - pub fn readPath(h: *Harness, names: []const []const u8) ![]u8 { - try h.walkTo(99, names); - defer _ = h.rpc(.{ .clunk = .{ .fid = 99 } }) catch {}; - return h.readAll(99); - } - - pub fn writePath(h: *Harness, names: []const []const u8, data: []const u8) !void { - try h.walkTo(98, names); - defer _ = h.rpc(.{ .clunk = .{ .fid = 98 } }) catch {}; - _ = try h.ok(.{ .open = .{ .fid = 98, .mode = cloud9.owrite } }); - const w = try h.ok(.{ .write = .{ .fid = 98, .offset = 0, .data = data } }); - try testing.expectEqual(@as(u32, @intCast(data.len)), w.write); - } - - /// Reads a whole directory in `count`-byte reads at consecutive offsets; returns owned names. - pub fn listDir(h: *Harness, fid: u32, count: u32) ![][]u8 { - var names: std.ArrayList([]u8) = .empty; - errdefer { - for (names.items) |n| testing.allocator.free(n); - names.deinit(testing.allocator); - } - var offset: u64 = 0; - while (true) { - const r = try h.ok(.{ .read = .{ .fid = fid, .offset = offset, .count = count } }); - if (r.read.len == 0) break; - offset += r.read.len; - var rest = r.read; - while (rest.len > 0) { - const n = std.mem.readInt(u16, rest[0..2], .little) + 2; - const st = try cloud9.Stat.decode(rest[0..n]); - try names.append(testing.allocator, try testing.allocator.dupe(u8, st.name)); - rest = rest[n..]; - } - } - return names.toOwnedSlice(testing.allocator); - } - - pub fn listPath(h: *Harness, names: []const []const u8) ![][]u8 { - try h.walkTo(97, names); - defer _ = h.rpc(.{ .clunk = .{ .fid = 97 } }) catch {}; - _ = try h.ok(.{ .open = .{ .fid = 97, .mode = cloud9.oread } }); - return h.listDir(97, 1024); - } - - pub fn freeNames(names: [][]u8) void { - for (names) |n| testing.allocator.free(n); - testing.allocator.free(names); - } - - pub fn hasName(names: []const []const u8, want: []const u8) bool { - for (names) |n| if (std.mem.eql(u8, n, want)) return true; - return false; - } - }; - }; -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -const testing = std.testing; - -const TestBuild = struct { - pub const zig_version: []const u8 = builtin.zig_version_string; - pub const target: []const u8 = "test-target"; - pub const optimize: []const u8 = "Debug"; - pub const time: []const u8 = "2023-11-14T22:13:20Z"; - pub const change: []const u8 = "abc123"; -}; - -const Layout = struct { a: u8, b: u32, c: u64 }; -const Decls = struct { - pub const one = 1; - pub const two = 2; - pub fn three() void {} -}; - -/// The context every generator and the ctl handler receive in tests. -const TestCtx = struct { - calls: u32 = 0, - ctl_state: i64 = 0, -}; - -const TestFns = struct { - pub fn counter(ctx: *anyopaque, w: *Writer) anyerror!void { - const t: *TestCtx = @ptrCast(@alignCast(ctx)); - t.calls += 1; - try w.print("{d}", .{t.calls}); - } - pub fn fib30(_: *anyopaque, w: *Writer) anyerror!void { - try w.print("{d}", .{fib(30)}); - } - pub fn failing(_: *anyopaque, _: *Writer) anyerror!void { - return error.BadCommand; - } - pub fn huge(_: *anyopaque, w: *Writer) anyerror!void { - try w.splatByteAll('x', 1 << 20); - } -}; - -const TestRuntime = struct { - pub fn pid(_: *anyopaque, w: *Writer) anyerror!void { - try w.writeAll("4242"); - } -}; - -fn fib(n: u32) u64 { - 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; -} - -fn testCtl(ctx: *anyopaque, cmd: []const u8, out: *Writer) anyerror!void { - const t: *TestCtx = @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, "add")) { - const a = std.fmt.parseInt(i64, it.next() orelse return error.BadCommand, 10) catch return error.BadCommand; - const b = std.fmt.parseInt(i64, it.next() orelse return error.BadCommand, 10) catch return error.BadCommand; - t.ctl_state = a +% b; - try out.print("{d}", .{t.ctl_state}); - } else if (std.mem.eql(u8, verb, "partial")) { - try out.writeAll("half-written"); - return error.BadCommand; - } else return error.BadCommand; -} - -const test_cfg: Config = .{ - .name = "tester", - .build = TestBuild, - .types = &.{ Layout, cloud9.Qid }, - .decls_of = Decls, - .fns = TestFns, - .runtime = TestRuntime, - .ctl = &testCtl, - .msize = 8192, - .max_fids = 8, - .max_providers = 2, - .max_vars = 4, - .snapshot_slots = 2, - .snapshot_bytes = 512, -}; - -const TS = Server(test_cfg); - -/// A small in-memory provider: /prov/{hello,dir/{inner}} with create/remove/wstat, -/// counting every handle reference so tests can check clunk discipline. -const TestProv = struct { - const max_nodes = 16; - const Entry = struct { - used: bool = false, - name: [max_name]u8 = undefined, - name_len: u8 = 0, - parent: u32 = 0, - is_dir: bool = false, - mode: u32 = 0o644, - data: [64]u8 = undefined, - len: usize = 0, - refs: u32 = 0, - opens: u32 = 0, - mtime: u32 = 0, - - fn nameSlice(e: *const Entry) []const u8 { - return e.name[0..e.name_len]; - } - }; - nodes: [max_nodes]Entry = @splat(.{}), - total_refs: u32 = 0, - clunks: u32 = 0, - fail_io: bool = false, - fail_stat: bool = false, - - fn init() TestProv { - var p: TestProv = .{}; - p.nodes[0] = .{ .used = true, .is_dir = true, .mode = cloud9.dmdir | 0o755 }; - _ = p.add(0, "hello", false, 0o644); - p.nodes[1].len = 5; - @memcpy(p.nodes[1].data[0..5], "hello"); - const d = p.add(0, "dir", true, cloud9.dmdir | 0o755); - _ = p.add(d, "inner", false, 0o600); - _ = p.add(0, "locked", false, 0o000); - return p; - } - - fn add(p: *TestProv, parent: u32, name: []const u8, is_dir: bool, mode: u32) u32 { - for (&p.nodes, 0..) |*e, i| if (!e.used) { - e.* = .{ .used = true, .parent = parent, .is_dir = is_dir, .mode = mode }; - @memcpy(e.name[0..name.len], name); - e.name_len = @intCast(name.len); - return @intCast(i); - }; - unreachable; - } - - fn self(ctx: *anyopaque) *TestProv { - return @ptrCast(@alignCast(ctx)); - } - - fn node(p: *TestProv, h: Provider.Handle) Provider.Error!*Entry { - if (h >= max_nodes or !p.nodes[h].used) return error.NotFound; - return &p.nodes[h]; - } - - fn retain(p: *TestProv, h: Provider.Handle) Provider.Handle { - if (h != 0) { - p.nodes[h].refs += 1; - p.total_refs += 1; - } - return h; - } - - fn walk(ctx: *anyopaque, parent: Provider.Handle, name: []const u8) Provider.Error!Provider.Handle { - const p = self(ctx); - if (p.fail_io) return error.Io; - const d = try p.node(parent); - if (std.mem.eql(u8, name, ".")) return p.retain(parent); - if (!d.is_dir) return error.NotDir; - if (std.mem.eql(u8, name, "..")) return p.retain(d.parent); - for (p.nodes[0..], 0..) |*e, i| { - if (e.used and e.parent == parent and i != 0 and std.mem.eql(u8, e.nameSlice(), name)) return p.retain(@intCast(i)); - } - return error.NotFound; - } - - fn fillStat(e: *const Entry, h: Provider.Handle, out: *NodeStat) void { - out.* = .{ .mode = e.mode, .length = e.len, .mtime = e.mtime, .name = e.nameSlice(), .handle = h }; - } - - fn stat(ctx: *anyopaque, h: Provider.Handle, out: *NodeStat) Provider.Error!void { - const p = self(ctx); - if (p.fail_stat) return error.Io; - fillStat(try p.node(h), h, out); - } - - fn list(ctx: *anyopaque, dir: Provider.Handle, index: usize, out: *NodeStat) Provider.Error!bool { - const p = self(ctx); - const d = try p.node(dir); - if (!d.is_dir) return error.NotDir; - var k: usize = 0; - for (p.nodes[0..], 0..) |*e, i| { - if (!e.used or e.parent != dir or i == 0) continue; - if (k == index) { - fillStat(e, @intCast(i), out); - return true; - } - k += 1; - } - return false; - } - - fn open(ctx: *anyopaque, h: Provider.Handle, mode: u8) Provider.Error!void { - const p = self(ctx); - const e = try p.node(h); - const acc = mode & 3; - if (acc != cloud9.owrite and e.mode & 0o400 == 0) return error.Perm; - if (acc != cloud9.oread and e.mode & 0o200 == 0) return error.Perm; - if (mode & cloud9.otrunc != 0) e.len = 0; - e.opens += 1; - } - - fn close(ctx: *anyopaque, h: Provider.Handle) void { - const p = self(ctx); - p.nodes[h].opens -= 1; - } - - fn read(ctx: *anyopaque, h: Provider.Handle, offset: u64, buf: []u8) Provider.Error!usize { - const p = self(ctx); - const e = try p.node(h); - if (offset >= e.len) return 0; - const n = @min(buf.len, e.len - @as(usize, @intCast(offset))); - @memcpy(buf[0..n], e.data[@intCast(offset)..][0..n]); - return n; - } - - fn write(ctx: *anyopaque, h: Provider.Handle, offset: u64, data: []const u8) Provider.Error!usize { - const p = self(ctx); - const e = try p.node(h); - if (offset + data.len > e.data.len) return error.NoSpace; - const off: usize = @intCast(offset); - @memcpy(e.data[off..][0..data.len], data); - e.len = @max(e.len, off + data.len); - e.mtime += 1; - return data.len; - } - - fn create(ctx: *anyopaque, dir: Provider.Handle, name: []const u8, perm: u32, mode: u8) Provider.Error!Provider.Handle { - const p = self(ctx); - const d = try p.node(dir); - if (!d.is_dir) return error.NotDir; - for (p.nodes[0..]) |*e| if (e.used and e.parent == dir and std.mem.eql(u8, e.nameSlice(), name)) return error.Exists; - var free: ?u32 = null; - for (p.nodes[0..], 0..) |*e, i| if (!e.used) { - free = @intCast(i); - break; - }; - const idx = free orelse return error.NoSpace; - const h = p.add(@intCast(dir), name, perm & cloud9.dmdir != 0, perm); - std.debug.assert(h == idx); - p.nodes[h].opens = 1; - _ = mode; - return p.retain(h); - } - - fn remove(ctx: *anyopaque, h: Provider.Handle) Provider.Error!void { - const p = self(ctx); - const e = try p.node(h); - if (h == 0) return error.Perm; - for (p.nodes[0..]) |*c| if (c.used and c.parent == h) return error.NotEmpty; - e.used = false; // refs still keep the slot "alive" for clunk accounting - e.used = true; - e.parent = std.math.maxInt(u32); // unlinked - } - - fn wstat(ctx: *anyopaque, h: Provider.Handle, st: *const cloud9.Stat) Provider.Error!void { - const p = self(ctx); - const e = try p.node(h); - if (st.name.len != 0) { - @memcpy(e.name[0..st.name.len], st.name); - e.name_len = @intCast(st.name.len); - } - if (st.length != 0xFFFF_FFFF_FFFF_FFFF) { - if (st.length > e.data.len) return error.NoSpace; - e.len = @intCast(st.length); - } - if (st.mode != 0xFFFF_FFFF) e.mode = st.mode; - if (st.mtime != 0xFFFF_FFFF) e.mtime = st.mtime; - } - - fn clunk(ctx: *anyopaque, h: Provider.Handle) void { - const p = self(ctx); - p.clunks += 1; - if (h != 0) { - p.nodes[h].refs -= 1; - p.total_refs -= 1; - } - } - - const vtable: Provider.VTable = .{ - .walk = &walk, - .stat = &stat, - .list = &list, - .open = &open, - .read = &read, - .write = &write, - .create = &create, - .remove = &remove, - .wstat = &wstat, - .close = &close, - .clunk = &clunk, - }; - - fn provider(p: *TestProv) Provider { - return .{ .name = "prov", .ctx = p, .vtable = &vtable }; - } -}; - -const Inner = struct { x: f32 }; -const Exposed = struct { a: u32, b: bool, name: []const u8, inner: Inner }; - -/// Everything a core test needs, in one place; `harness.init` runs version+attach. -const Fixture = struct { - ctx: TestCtx = .{}, - shared: TS.Shared = undefined, - storage: TS.Storage = undefined, - prov: TestProv = undefined, - exposed: Exposed = .{ .a = 1, .b = true, .name = "hello", .inner = .{ .x = 0.5 } }, - counter: u64 = 7, - h: TS.Harness = undefined, - - fn init(x: *Fixture) !void { - x.shared = .init(&x.ctx); - x.prov = TestProv.init(); - try x.shared.addProvider(x.prov.provider()); - try x.shared.expose("state", &x.exposed); - try x.shared.expose("counter", &x.counter); - try x.h.init(&x.shared, &x.storage); - } - - fn deinit(x: *Fixture) void { - x.h.deinit(); - } -}; - -test "README, /build and the static tree read as expected" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - const readme = try x.h.readPath(&.{"README"}); - defer testing.allocator.free(readme); - try testing.expect(std.mem.startsWith(u8, readme, "tester: a 9P2000 introspection server")); - const zv = try x.h.readPath(&.{ "build", "zig_version" }); - defer testing.allocator.free(zv); - try testing.expectEqualStrings(builtin.zig_version_string, zv); - const ch = try x.h.readPath(&.{ "build", "change" }); - defer testing.allocator.free(ch); - try testing.expectEqualStrings("abc123", ch); - try testing.expectEqual(@as(u32, 1_700_000_000), TS.build_secs); - const names = try x.h.listPath(&.{}); - defer TS.Harness.freeNames(names); - for ([_][]const u8{ "README", "build", "comptime", "runtime", "ctl", "vars", "prov" }) |n| try testing.expect(TS.Harness.hasName(names, n)); - try testing.expectEqual(@as(usize, 7), names.len); - // static files are read-only; the static tree admits no creates or removes - try x.h.walkTo(1, &.{ "build", "target" }); - try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.owrite } }, "permission denied"); - try x.h.expectFail(.{ .remove = .{ .fid = 1 } }, "permission denied"); - try x.h.walkTo(2, &.{"build"}); - try x.h.expectFail(.{ .create = .{ .fid = 2, .name = "nope", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied"); - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = stat_dontcare } }, "permission denied"); - const st = try x.h.ok(.{ .stat = .{ .fid = 2 } }); - try testing.expectEqualStrings("build", st.stat.name); - try testing.expectEqualStrings("tester", st.stat.uid); - try testing.expect(st.stat.qid.type & cloud9.qtdir != 0); - try testing.expectEqual(TS.build_secs, st.stat.mtime); - try testing.expectEqual(@as(u32, 0), parseIso8601("1970-01-01T00:00:00Z").?); - try testing.expectEqual(@as(?u32, null), parseIso8601("unknown")); -} - -test "comptime/types fields carry @offsetOf and comptime/decls lists pub decls" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - const names = try x.h.listPath(&.{ "comptime", "types" }); - defer TS.Harness.freeNames(names); - try testing.expectEqual(@as(usize, 2), names.len); - try testing.expect(TS.Harness.hasName(names, "Layout")); - try testing.expect(TS.Harness.hasName(names, "Qid")); - const fields = try x.h.readPath(&.{ "comptime", "types", "Layout", "fields" }); - defer testing.allocator.free(fields); - var expect_buf: [128]u8 = undefined; - const expect = try std.fmt.bufPrint(&expect_buf, "a: u8 @{d}\nb: u32 @{d}\nc: u64 @{d}\n", .{ @offsetOf(Layout, "a"), @offsetOf(Layout, "b"), @offsetOf(Layout, "c") }); - try testing.expectEqualStrings(expect, fields); - const size = try x.h.readPath(&.{ "comptime", "types", "Layout", "size" }); - defer testing.allocator.free(size); - try testing.expectEqualStrings(std.fmt.comptimePrint("{d}", .{@sizeOf(Layout)}), size); - const name = try x.h.readPath(&.{ "comptime", "types", "Qid", "name" }); - defer testing.allocator.free(name); - try testing.expectEqualStrings(@typeName(cloud9.Qid), name); - const decls = try x.h.readPath(&.{ "comptime", "decls" }); - defer testing.allocator.free(decls); - try testing.expectEqualStrings("one\ntwo\nthree\n", decls); -} - -test "runtime/fn calls the function at open and at each read from offset 0" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - const names = try x.h.listPath(&.{ "runtime", "fn" }); - defer TS.Harness.freeNames(names); - try testing.expectEqual(@typeInfo(TestFns).@"struct".decls.len, names.len); - const fib_text = try x.h.readPath(&.{ "runtime", "fn", "fib30" }); - defer testing.allocator.free(fib_text); - try testing.expectEqualStrings("832040", fib_text); - const pid = try x.h.readPath(&.{ "runtime", "pid" }); - defer testing.allocator.free(pid); - try testing.expectEqualStrings("4242", pid); - // the generator runs at open, then again at each read from offset 0, not at offset > 0 - try x.h.walkTo(1, &.{ "runtime", "fn", "counter" }); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); - try testing.expectEqual(@as(u32, 1), x.ctx.calls); - const r1 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("2", r1.read); - const r2 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 1, .count = 100 } }); - try testing.expectEqualStrings("", r2.read); - try testing.expectEqual(@as(u32, 2), x.ctx.calls); - // stat of a dynamic file reports length 0 - const st = try x.h.ok(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqual(@as(u64, 0), st.stat.length); - try testing.expectEqual(@as(u32, 0o444), st.stat.mode); - // a generator error is the file's Rerror; a generator that overflows the slot too - try x.h.walkTo(2, &.{ "runtime", "fn", "failing" }); - try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }, "bad command"); - try x.h.walkTo(3, &.{ "runtime", "fn", "huge" }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "no space in buffer"); - // a failed open frees its slot: two more dynamic opens still succeed - try x.h.walkTo(4, &.{ "runtime", "fn", "fib30" }); - _ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - _ = try x.h.ok(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{} } }); - _ = try x.h.ok(.{ .open = .{ .fid = 5, .mode = cloud9.oread } }); -} - -test "snapshot slot exhaustion is an Rerror and clunk frees the slot" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{ "runtime", "fn", "fib30" }); - try x.h.walkTo(2, &.{ "runtime", "fn", "fib30" }); - try x.h.walkTo(3, &.{ "vars", "counter", "value" }); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "too many open dynamic files"); - // static and provider files need no slot - const t = try x.h.readPath(&.{ "vars", "counter", "type" }); - defer testing.allocator.free(t); - try testing.expectEqualStrings("u64", t); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }); - const r = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("7", r.read); - // cloning an open fid onto itself keeps it open and its content - const w = try x.h.ok(.{ .walk = .{ .fid = 3, .newfid = 3, .names = &.{} } }); - try testing.expectEqual(@as(u16, 0), w.walk.nwqid); - const r2 = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("7", r2.read); - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{".."} } }, "file already open"); - _ = try x.h.ok(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{} } }); - try x.h.expectFail(.{ .read = .{ .fid = 4, .offset = 0, .count = 100 } }, "file not open"); -} - -test "ctl round trip" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"ctl"}); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); - const w = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "add 2 3\n" } }); - try testing.expectEqual(@as(u32, 8), w.write); - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("5", r.read); - try testing.expectEqual(@as(i64, 5), x.ctx.ctl_state); - const st = try x.h.ok(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqual(@as(u64, 1), st.stat.length); - try testing.expectEqualStrings("ctl", st.stat.name); - try testing.expectEqual(@as(u32, 0o666), st.stat.mode); - const v1 = st.stat.qid.version; - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "echo hello world" } }); - const r2 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("hello world", r2.read); - const r3 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 6, .count = 100 } }); - try testing.expectEqualStrings("world", r3.read); - try testing.expect((try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.qid.version != v1); - const v2 = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.qid.version; - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "frobnicate" } }, "bad command"); - // a failed command leaves the previous result, length and version in place - const r4 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("hello world", r4.read); - const st4 = try x.h.ok(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqual(@as(u64, 11), st4.stat.length); - try testing.expectEqual(v2, st4.stat.qid.version); - // even when the handler wrote part of a result before failing - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "partial" } }, "bad command"); - const r5 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("hello world", r5.read); - try testing.expectEqualStrings("hello world", x.shared.ctlResult()); - // the empty result is a legitimate result too - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "echo" } }); - try testing.expectEqualStrings("", (try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } })).read); - try testing.expectEqual(@as(u64, 0), (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.length); - // Tversion resets the ctl fid like any other - try x.h.version(4096); - try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid"); -} - -test "auth is not required and flush is answered" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.expectFail(.{ .auth = .{ .afid = 5, .uname = "tester" } }, "authentication not required"); - const f = try x.h.ok(.{ .flush = .{ .oldtag = 1 } }); - try testing.expect(f == .flush); - try x.h.expectFail(.{ .attach = .{ .fid = 0, .uname = "tester" } }, "fid in use"); -} - -test "vars: value/type/size/addr/raw, fields and writes" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - const names = try x.h.listPath(&.{"vars"}); - defer TS.Harness.freeNames(names); - try testing.expectEqual(@as(usize, 2), names.len); - try testing.expectEqualStrings("state", names[0]); - const entries = try x.h.listPath(&.{ "vars", "state" }); - defer TS.Harness.freeNames(entries); - for ([_][]const u8{ "value", "type", "size", "addr", "raw", "f" }) |n| try testing.expect(TS.Harness.hasName(entries, n)); - try testing.expectEqual(@as(usize, 6), entries.len); - const value = try x.h.readPath(&.{ "vars", "state", "value" }); - defer testing.allocator.free(value); - try testing.expectEqualStrings("a: 1\nb: true\nname: \"hello\"\ninner:\n x: 0.5\n", value); - const tn = try x.h.readPath(&.{ "vars", "state", "type" }); - defer testing.allocator.free(tn); - try testing.expectEqualStrings(@typeName(Exposed), tn); - const size = try x.h.readPath(&.{ "vars", "state", "size" }); - defer testing.allocator.free(size); - try testing.expectEqualStrings(std.fmt.comptimePrint("{d}", .{@sizeOf(Exposed)}), size); - const addr = try x.h.readPath(&.{ "vars", "state", "addr" }); - defer testing.allocator.free(addr); - var addr_buf: [32]u8 = undefined; - try testing.expectEqualStrings(try std.fmt.bufPrint(&addr_buf, "0x{x}", .{@intFromPtr(&x.exposed)}), addr); - const raw = try x.h.readPath(&.{ "vars", "state", "raw" }); - defer testing.allocator.free(raw); - try testing.expectEqualSlices(u8, std.mem.asBytes(&x.exposed), raw); - try x.h.walkTo(1, &.{ "vars", "state", "raw" }); - const raw_st = try x.h.ok(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqual(@as(u64, @sizeOf(Exposed)), raw_st.stat.length); - try testing.expectEqual(@as(u32, 0o444), raw_st.stat.mode); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - // fields - const fnames = try x.h.listPath(&.{ "vars", "state", "f" }); - defer TS.Harness.freeNames(fnames); - try testing.expectEqual(@as(usize, 4), fnames.len); - const a_value = try x.h.readPath(&.{ "vars", "state", "f", "a", "value" }); - defer testing.allocator.free(a_value); - try testing.expectEqualStrings("1", a_value); - const a_type = try x.h.readPath(&.{ "vars", "state", "f", "a", "type" }); - defer testing.allocator.free(a_type); - try testing.expectEqualStrings("u32", a_type); - const xv = try x.h.readPath(&.{ "vars", "state", "f", "inner", "f", "x", "value" }); - defer testing.allocator.free(xv); - try testing.expectEqualStrings("0.5", xv); - const b_raw = try x.h.readPath(&.{ "vars", "state", "f", "b", "raw" }); - defer testing.allocator.free(b_raw); - try testing.expectEqualSlices(u8, &.{1}, b_raw); - // writes - try x.h.writePath(&.{ "vars", "state", "f", "a", "value" }, "42"); - try testing.expectEqual(@as(u32, 42), x.exposed.a); - try x.h.writePath(&.{ "vars", "state", "f", "b", "value" }, "false\n"); - try testing.expect(!x.exposed.b); - try x.h.writePath(&.{ "vars", "state", "f", "inner", "f", "x", "value" }, "2.25"); - try testing.expectEqual(@as(f32, 2.25), x.exposed.inner.x); - try x.h.writePath(&.{ "vars", "counter", "value" }, "0x10"); - try testing.expectEqual(@as(u64, 16), x.counter); - try x.h.walkTo(2, &.{ "vars", "state", "f", "a", "value" }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.ordwr } }); - try x.h.expectFail(.{ .write = .{ .fid = 2, .offset = 0, .data = "abc" } }, "bad value"); - const rd = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("42", rd.read); - const a_st = try x.h.ok(.{ .stat = .{ .fid = 2 } }); - try testing.expectEqual(@as(u32, 0o644), a_st.stat.mode); - try testing.expectEqualStrings("value", a_st.stat.name); - // non-scalar values, type/size/addr/raw and directories are read-only - try x.h.walkTo(3, &.{ "vars", "state", "value" }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.owrite } }, "permission denied"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); - try x.h.walkTo(4, &.{ "vars", "state", "f", "name", "value" }); - try x.h.expectFail(.{ .open = .{ .fid = 4, .mode = cloud9.owrite } }, "permission denied"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 4 } }); - try x.h.walkTo(5, &.{ "vars", "state" }); - try x.h.expectFail(.{ .open = .{ .fid = 5, .mode = cloud9.owrite } }, "is a directory"); - try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "z", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied"); - // .. climbs back out of the var tree; unknown names fail - const up = try x.h.ok(.{ .walk = .{ .fid = 5, .newfid = 6, .names = &.{ "f", "inner", "..", "..", "..", "..", "README" } } }); - try testing.expectEqual(@as(u16, 7), up.walk.nwqid); - _ = try x.h.ok(.{ .clunk = .{ .fid = 6 } }); - try x.h.walkTo(7, &.{"vars"}); - try x.h.expectFail(.{ .walk = .{ .fid = 7, .newfid = 8, .names = &.{"nope"} } }, "file does not exist"); - try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, "file already open"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - try x.h.walkTo(2, &.{ "vars", "state", "f", "a", "value" }); - try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 8, .names = &.{"x"} } }, "not a directory"); -} - -test "provider: walk/list/stat/open/read/write/create/remove/wstat/clunk and error mapping" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - const names = try x.h.listPath(&.{"prov"}); - defer TS.Harness.freeNames(names); - try testing.expectEqual(@as(usize, 3), names.len); - try testing.expect(TS.Harness.hasName(names, "hello") and TS.Harness.hasName(names, "dir") and TS.Harness.hasName(names, "locked")); - const hello = try x.h.readPath(&.{ "prov", "hello" }); - defer testing.allocator.free(hello); - try testing.expectEqualStrings("hello", hello); - try testing.expectEqual(@as(u32, 0), x.prov.total_refs); // every temp handle was clunked - // stat and qid scheme - try x.h.walkTo(1, &.{ "prov", "dir", "inner" }); - const st = try x.h.ok(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqualStrings("inner", st.stat.name); - try testing.expectEqual(@as(u32, 0o600), st.stat.mode); - try testing.expectEqual(@as(u64, 3), st.stat.qid.path); // provider 0, handle 3 - try testing.expectEqualStrings("tester", st.stat.gid); - try x.h.walkTo(2, &.{"prov"}); - const root_st = try x.h.ok(.{ .stat = .{ .fid = 2 } }); - try testing.expectEqualStrings("prov", root_st.stat.name); - try testing.expect(root_st.stat.qid.type & cloud9.qtdir != 0); - try testing.expectEqual(@as(u64, 0), root_st.stat.qid.path); - try testing.expectEqual(@as(u32, 1), x.prov.total_refs); // fid 1 holds inner; fid 2 holds root (unref'd) - // write then read back; opens are tracked through close - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); - try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].opens); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "abc" } }); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 3, .data = "def" } }); - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 1, .count = 100 } }); - try testing.expectEqualStrings("bcdef", r.read); - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 100, .data = "z" } }, "no space left on device"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens); - try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].refs); - // permission and kind errors come from the provider - try x.h.walkTo(3, &.{ "prov", "locked" }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "permission denied"); - try x.h.walkTo(4, &.{ "prov", "hello" }); - try x.h.expectFail(.{ .walk = .{ .fid = 4, .newfid = 5, .names = &.{"x"} } }, "not a directory"); - try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = 5, .names = &.{"missing"} } }, "file does not exist"); - try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.owrite } }, "is a directory"); - // create in a provider directory: the fid becomes the new open file - const cr = try x.h.ok(.{ .create = .{ .fid = 2, .name = "new", .perm = 0o644, .mode = cloud9.ordwr } }); - try testing.expectEqual(cloud9.qtfile, cr.create.qid.type); - _ = try x.h.ok(.{ .write = .{ .fid = 2, .offset = 0, .data = "fresh" } }); - const rr = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("fresh", rr.read); - try x.h.walkTo(6, &.{"prov"}); - try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "new", .perm = 0o644, .mode = cloud9.oread } }, "file already exists"); - const long_name = [_]u8{'n'} ** (max_name + 1); - try x.h.expectFail(.{ .create = .{ .fid = 6, .name = &long_name, .perm = 0o644, .mode = cloud9.oread } }, "bad file name"); - try x.h.expectFail(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.owrite } }, "is a directory"); - const dr = try x.h.ok(.{ .create = .{ .fid = 6, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.oread } }); - try testing.expectEqual(cloud9.qtdir, dr.create.qid.type); - // wstat: rename, truncate, mode, mtime; immutable fields are refused - var ws = stat_dontcare; - ws.name = "renamed"; - ws.length = 2; - ws.mode = 0o600; - ws.mtime = 99; - _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = ws } }); - const st2 = try x.h.ok(.{ .stat = .{ .fid = 2 } }); - try testing.expectEqualStrings("renamed", st2.stat.name); - try testing.expectEqual(@as(u64, 2), st2.stat.length); - try testing.expectEqual(@as(u32, 0o600), st2.stat.mode); - try testing.expectEqual(@as(u32, 99), st2.stat.mtime); - ws = stat_dontcare; - ws.uid = "someone-else"; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied"); - ws = stat_dontcare; - ws.mode = cloud9.dmdir | 0o755; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "permission denied"); - ws = stat_dontcare; - ws.name = "bad/name"; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "bad file name"); - ws.name = ".."; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = ws } }, "bad file name"); - ws = stat_dontcare; - ws.length = 5; - try x.h.walkTo(7, &.{ "prov", "d" }); - try x.h.expectFail(.{ .wstat = .{ .fid = 7, .stat = ws } }, "is a directory"); - ws = stat_dontcare; - ws.name = "root2"; // the provider root cannot be renamed - try x.h.walkTo(14, &.{"prov"}); - try x.h.expectFail(.{ .wstat = .{ .fid = 14, .stat = ws } }, "permission denied"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 14 } }); - // remove always clunks; a non-empty directory refuses - _ = try x.h.ok(.{ .clunk = .{ .fid = 6 } }); - try x.h.walkTo(8, &.{ "prov", "dir" }); - try x.h.expectFail(.{ .remove = .{ .fid = 8 } }, "directory not empty"); - try x.h.expectFail(.{ .clunk = .{ .fid = 8 } }, "unknown fid"); - _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); - try x.h.walkTo(9, &.{"prov"}); - try x.h.expectFail(.{ .walk = .{ .fid = 9, .newfid = 15, .names = &.{"renamed"} } }, "file does not exist"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 9 } }); - // an i/o error from the provider maps to "i/o error" - try x.h.walkTo(9, &.{"prov"}); - x.prov.fail_io = true; - try x.h.expectFail(.{ .walk = .{ .fid = 9, .newfid = 15, .names = &.{"hello"} } }, "i/o error"); - x.prov.fail_io = false; - _ = try x.h.ok(.{ .clunk = .{ .fid = 9 } }); - // ORCLOSE removes on clunk - try x.h.walkTo(9, &.{"prov"}); - _ = try x.h.ok(.{ .create = .{ .fid = 9, .name = "tmp", .perm = 0o644, .mode = cloud9.owrite | cloud9.orclose } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 9 } }); - try x.h.walkTo(9, &.{"prov"}); - try x.h.expectFail(.{ .walk = .{ .fid = 9, .newfid = 15, .names = &.{"tmp"} } }, "file does not exist"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 9 } }); - // walking .. out of the provider root and cloning provider fids keeps refs balanced - const up = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 10, .names = &.{ "prov", "dir", "..", "..", "build" } } }); - try testing.expectEqual(@as(u16, 5), up.walk.nwqid); - try x.h.walkTo(11, &.{ "prov", "dir", "inner" }); - _ = try x.h.ok(.{ .walk = .{ .fid = 11, .newfid = 12, .names = &.{} } }); - try testing.expectEqual(@as(u32, 2), x.prov.nodes[3].refs); - _ = try x.h.ok(.{ .clunk = .{ .fid = 11 } }); - try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].refs); - // a partial walk releases the handles it obtained - const part = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 13, .names = &.{ "prov", "dir", "nope" } } }); - try testing.expectEqual(@as(u16, 2), part.walk.nwqid); - try x.h.expectFail(.{ .clunk = .{ .fid = 13 } }, "unknown fid"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 12 } }); - for (x.h.conn.fids) |f| { - if (f.used) _ = try x.h.ok(.{ .clunk = .{ .fid = f.id } }); - } - try testing.expectEqual(@as(u32, 0), x.prov.total_refs); -} - -test "directory reads across offsets, bad offset, and records never split" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{}); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); - const first = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); - try testing.expect(first.read.len > 0); - try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 5, .count = 4096 } }, "bad offset"); - // offset 0 restarts; the same bytes come back - const again = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); - try testing.expectEqual(first.read.len, again.read.len); - // small reads at consecutive offsets return every record exactly once - const names = try x.h.listDir(1, 80); - defer TS.Harness.freeNames(names); - try testing.expectEqual(@as(usize, 7), names.len); - // a count too small for even one record returns nothing rather than splitting it - const tiny = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }); - try testing.expectEqual(@as(usize, 0), tiny.read.len); - // the same for provider and var directories - const pn = try x.h.listPath(&.{ "prov", "dir" }); - defer TS.Harness.freeNames(pn); - try testing.expectEqual(@as(usize, 1), pn.len); - try x.h.walkTo(2, &.{ "vars", "state", "f" }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - const vn = try x.h.listDir(2, 100); - defer TS.Harness.freeNames(vn); - try testing.expectEqual(@as(usize, 4), vn.len); - try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 1, .count = 100 } }, "bad offset"); -} - -test "Tversion mid-session resets fids and clunks every provider handle" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{ "prov", "hello" }); - try x.h.walkTo(2, &.{ "prov", "dir", "inner" }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - try x.h.walkTo(3, &.{ "runtime", "fn", "fib30" }); - _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }); - try testing.expectEqual(@as(u32, 2), x.prov.total_refs); - try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].opens); - try testing.expectEqual(@as(usize, 4), x.h.conn.fidCount()); - const before = x.prov.clunks; - try x.h.version(4096); - try testing.expectEqual(@as(usize, 0), x.h.conn.fidCount()); - try testing.expectEqual(@as(u32, 0), x.prov.total_refs); - try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens); - try testing.expectEqual(before + 2, x.prov.clunks); - try testing.expect(!x.h.conn.slot_used[0] and !x.h.conn.slot_used[1]); - try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid"); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } }); - try x.h.walkTo(1, &.{ "prov", "hello" }); - // hangup does the same - x.h.conn.hangup(); - try testing.expectEqual(@as(u32, 0), x.prov.total_refs); - try testing.expectEqual(@as(usize, 0), x.h.conn.fidCount()); -} - -test "a reply that does not fit msize is an Rerror, not a dead connection" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.version(64); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "t" } }); - // Rstat of the root is ~70 bytes. - try x.h.expectFail(.{ .stat = .{ .fid = 0 } }, "reply too large for msize"); - // Rwalk with 5 qids is 74 bytes; the walk must not bind newfid. - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ ".", ".", ".", ".", "." } } }, "reply too large for msize"); - try x.h.expectFail(.{ .clunk = .{ .fid = 1 } }, "unknown fid"); - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 0, .names = &.{ ".", ".", ".", ".", "." } } }, "reply too large for msize"); - const r = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"README"} } }); - try testing.expectEqual(@as(u16, 1), r.walk.nwqid); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); - const rd = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 40 } }); - try testing.expect(rd.read.len > 0 and rd.read.len <= 64 - cloud9.iohdrsz); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); -} - -test "fid table is bounded per connection" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - var i: u32 = 1; - while (x.h.conn.fidCount() < test_cfg.max_fids) : (i += 1) { - _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }); - } - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }, "too many fids"); - try x.h.expectFail(.{ .attach = .{ .fid = i, .uname = "tester" } }, "too many fids"); - // self-walks and clunks still work at the limit - _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 0, .names = &.{"build"} } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = i, .names = &.{} } }); - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{"README"} } }, "fid in use"); - try x.h.expectFail(.{ .walk = .{ .fid = 1234, .newfid = 2, .names = &.{} } }, "unknown fid"); - // a walk into a provider at the limit must not leak the handle - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - _ = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{ "..", "prov", "hello" } } }); - try x.h.expectFail(.{ .walk = .{ .fid = 2, .newfid = i + 1, .names = &.{} } }, "too many fids"); - try testing.expectEqual(@as(u32, 1), x.prov.total_refs); -} - -test "Shared refuses more providers or vars than configured" { - var ctx: TestCtx = .{}; - var shared: TS.Shared = .init(&ctx); - var p1 = TestProv.init(); - var p2 = TestProv.init(); - var p3 = TestProv.init(); - try shared.addProvider(.{ .name = "a", .ctx = &p1, .vtable = &TestProv.vtable }); - try shared.addProvider(.{ .name = "b", .ctx = &p2, .vtable = &TestProv.vtable }); - try testing.expectError(error.Full, shared.addProvider(.{ .name = "c", .ctx = &p3, .vtable = &TestProv.vtable })); - var v: [5]u32 = @splat(0); - try shared.expose("v0", &v[0]); - try shared.expose("v1", &v[1]); - try shared.expose("v2", &v[2]); - try shared.expose("v3", &v[3]); - try testing.expectError(error.Full, shared.expose("v4", &v[4])); -} - -/// A server with a large fid table for the index tests. -const big_cfg: Config = .{ - .name = "big", - .msize = 8192, - .max_fids = 4096, - .max_providers = 1, - .max_vars = 1, - .snapshot_slots = 1, - .snapshot_bytes = 256, -}; -const BigS = Server(big_cfg); - -/// Fid numbers chosen to stress the index: dense low ids, ids with only high -/// bits set, and ids counting down from 2^32-1 (all distinct for i < 2^20). -fn adversarialId(i: u32) u32 { - return switch (i % 3) { - 0 => i * 8192 + 1, - 1 => 0x8000_0000 | i, - else => 0xFFFF_FFFF - i, - }; -} - -/// Every index bucket points at a used fid that finds itself, and every used -/// fid is found: the invariant the hostile fid tests check after each phase. -fn checkFidIndex(c: *BigS.Conn) !void { - var indexed: usize = 0; - for (c.index) |slot| { - if (slot == BigS.no_slot) continue; - indexed += 1; - try testing.expect(c.fids[slot].used); - try testing.expectEqual(&c.fids[slot], c.findFid(c.fids[slot].id).?); - } - var used: usize = 0; - for (c.fids[0..c.high_water]) |*f| if (f.used) { - used += 1; - try testing.expectEqual(f, c.findFid(f.id).?); - }; - for (c.fids[c.high_water..]) |*f| try testing.expect(!f.used); - try testing.expectEqual(indexed, used); - try testing.expectEqual(used, c.nfids); -} - -test "fid index: thousands of fids, clunk in hostile orders, reuse, Tversion" { - var ctx: TestCtx = .{}; - var shared: BigS.Shared = .init(&ctx); - var prov = TestProv.init(); - try shared.addProvider(prov.provider()); - const storage = try testing.allocator.create(BigS.Storage); - defer testing.allocator.destroy(storage); - var h: BigS.Harness = undefined; - try h.init(&shared, storage); - defer h.deinit(); - const n: u32 = big_cfg.max_fids - 1; // fid 0 is the attach - var i: u32 = 0; - while (i < n) : (i += 1) { - _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = adversarialId(i), .names = &.{ "prov", "hello" } } }); - } - try testing.expectEqual(@as(usize, n + 1), h.conn.fidCount()); - try testing.expectEqual(n, prov.total_refs); - try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 0x7FFF_FFFF, .names = &.{} } }, "too many fids"); - try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = adversarialId(5), .names = &.{} } }, "fid in use"); - try h.expectFail(.{ .attach = .{ .fid = adversarialId(7), .uname = "t" } }, "fid in use"); - try testing.expect(h.conn.findFid(0x7FFF_FFFF) == null); - try testing.expect(h.conn.findFid(adversarialId(n)) == null); - try checkFidIndex(&h.conn); - // clunk every third fid, then the rest from the top: backward-shift deletion under churn - i = 0; - while (i < n) : (i += 3) _ = try h.ok(.{ .clunk = .{ .fid = adversarialId(i) } }); - try checkFidIndex(&h.conn); - i = n; - while (i > 0) { - i -= 1; - if (i % 3 == 0) { - try h.expectFail(.{ .clunk = .{ .fid = adversarialId(i) } }, "unknown fid"); - } else { - _ = try h.ok(.{ .clunk = .{ .fid = adversarialId(i) } }); - } - } - try testing.expectEqual(@as(usize, 1), h.conn.fidCount()); - try testing.expectEqual(@as(u32, 0), prov.total_refs); - try checkFidIndex(&h.conn); - // the whole table is reusable after the churn, through the free list - i = 0; - while (i < n) : (i += 1) _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = n - i, .names = &.{} } }); - try h.expectFail(.{ .walk = .{ .fid = 0, .newfid = n + 1, .names = &.{} } }, "too many fids"); - try checkFidIndex(&h.conn); - // pseudo-random alloc/free storm with verification - var prng = std.Random.DefaultPrng.init(0x9a11); - const rnd = prng.random(); - var live: [n + 1]bool = @splat(true); - live[0] = false; // never touch the attach fid - var round: usize = 0; - while (round < 20_000) : (round += 1) { - const id = 1 + rnd.uintLessThan(u32, n); - if (live[id]) { - _ = try h.ok(.{ .clunk = .{ .fid = id } }); - } else { - _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = id, .names = &.{"prov"} } }); - } - live[id] = !live[id]; - if (round % 997 == 0) try checkFidIndex(&h.conn); - } - try checkFidIndex(&h.conn); - // Tversion drops everything and the table starts over, provider refs balanced - try h.version(big_cfg.msize); - try testing.expectEqual(@as(usize, 0), h.conn.fidCount()); - try testing.expectEqual(@as(u32, 0), prov.total_refs); - try testing.expectEqual(@as(u16, 0), h.conn.high_water); - try checkFidIndex(&h.conn); - _ = try h.ok(.{ .attach = .{ .fid = 0xFFFF_FFFE, .uname = "t" } }); - _ = try h.ok(.{ .walk = .{ .fid = 0xFFFF_FFFE, .newfid = 0, .names = &.{} } }); - try checkFidIndex(&h.conn); -} - -test "open: a provider stat failure after a successful open closes the file again" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{ "prov", "hello" }); - x.prov.fail_stat = true; - try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "i/o error"); - x.prov.fail_stat = false; - try testing.expectEqual(@as(u32, 0), x.prov.nodes[1].opens); - try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, "file not open"); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); - try testing.expectEqual(@as(u32, 1), x.prov.nodes[1].opens); - // the same for create: a stat failure after the provider created the node releases it - try x.h.walkTo(2, &.{"prov"}); - x.prov.fail_stat = true; - try x.h.expectFail(.{ .create = .{ .fid = 2, .name = "born", .perm = 0o644, .mode = cloud9.owrite } }, "i/o error"); - x.prov.fail_stat = false; - for (x.prov.nodes) |e| if (e.used and std.mem.eql(u8, e.nameSlice(), "born")) { - try testing.expectEqual(@as(u32, 0), e.opens); - try testing.expectEqual(@as(u32, 0), e.refs); - }; - try testing.expect(!x.h.conn.findFid(2).?.open); - try testing.expectEqual(@as(u32, 1), x.prov.total_refs); // fid 1 only -} - -test "fid state machine: open twice, walk from open, remove/clunk of open provider fids" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{ "prov", "dir", "inner" }); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); - try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "file already open"); - try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{"."} } }, "file already open"); - try x.h.expectFail(.{ .create = .{ .fid = 1, .name = "z", .perm = 0o644, .mode = cloud9.oread } }, "file already open"); - // a clone of an open fid is a fresh, unopened reference - _ = try x.h.ok(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{} } }); - try testing.expectEqual(@as(u32, 2), x.prov.nodes[3].refs); - try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].opens); - // walking newfid == fid with names on an unopened provider fid swaps the handle, refs balanced - try x.h.walkTo(7, &.{ "prov", "dir" }); - try testing.expectEqual(@as(u32, 1), x.prov.nodes[2].refs); - _ = try x.h.ok(.{ .walk = .{ .fid = 7, .newfid = 7, .names = &.{ "..", "dir", "inner", "..", "..", "dir" } } }); - try testing.expectEqual(@as(u32, 1), x.prov.nodes[2].refs); - try testing.expectEqual(@as(u32, 2), x.prov.nodes[3].refs); - _ = try x.h.ok(.{ .clunk = .{ .fid = 7 } }); - try testing.expectEqual(@as(u32, 0), x.prov.nodes[2].refs); - // remove of an open fid: close, then remove, then clunk; refs and opens return to zero - _ = try x.h.ok(.{ .remove = .{ .fid = 1 } }); - try testing.expectEqual(@as(u32, 0), x.prov.nodes[3].opens); - try testing.expectEqual(@as(u32, 1), x.prov.nodes[3].refs); - try x.h.expectFail(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }, "unknown fid"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - try testing.expectEqual(@as(u32, 0), x.prov.total_refs); - // walking "." on a file fid is "not a directory" at the protocol level, without a provider walk - try x.h.walkTo(3, &.{ "prov", "hello" }); - const before = x.prov.clunks; - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"."} } }, "not a directory"); - try testing.expectEqual(before, x.prov.clunks); - try testing.expectEqual(@as(u32, 1), x.prov.total_refs); - // a partial walk through a file releases the handles it took - const part = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 5, .names = &.{ "prov", "hello", "x", "y" } } }); - try testing.expectEqual(@as(u16, 2), part.walk.nwqid); - try testing.expectEqual(@as(u32, 1), x.prov.total_refs); - try x.h.expectFail(.{ .clunk = .{ .fid = 5 } }, "unknown fid"); - // Tremove is always a clunk, even of a static node or when the provider refuses - try x.h.walkTo(6, &.{"README"}); - try x.h.expectFail(.{ .remove = .{ .fid = 6 } }, "permission denied"); - try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, "unknown fid"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); - try testing.expectEqual(@as(u32, 0), x.prov.total_refs); -} - -test "snapshot slots: exhaust, hold, Tversion frees; reads past the end and at huge offsets" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{ "runtime", "fn", "fib30" }); - try x.h.walkTo(2, &.{ "vars", "state", "value" }); - try x.h.walkTo(3, &.{ "vars", "state", "addr" }); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "too many open dynamic files"); - try testing.expect(!x.h.conn.findFid(3).?.open); - // reads at offsets near 2^64 never trap (counts above msize are a raw-9P - // case: the cloud9 client refuses to send them; test/adv_core_hostile.py covers it) - const max_count = test_cfg.msize - cloud9.iohdrsz; - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = std.math.maxInt(u64), .count = max_count } }); - try testing.expectEqualStrings("", r.read); - const r2 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 1 << 63, .count = 0 } }); - try testing.expectEqualStrings("", r2.read); - const r3 = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = max_count } }); - try testing.expectEqualStrings("832040", r3.read); - // raw beyond @sizeOf is empty; a partial raw read at the tail is bounded - try x.h.walkTo(4, &.{ "vars", "state", "raw" }); - _ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } }); - const raw_end = try x.h.ok(.{ .read = .{ .fid = 4, .offset = @sizeOf(Exposed), .count = 100 } }); - try testing.expectEqualStrings("", raw_end.read); - const raw_tail = try x.h.ok(.{ .read = .{ .fid = 4, .offset = @sizeOf(Exposed) - 1, .count = 100 } }); - try testing.expectEqual(@as(usize, 1), raw_tail.read.len); - const raw_huge = try x.h.ok(.{ .read = .{ .fid = 4, .offset = std.math.maxInt(u64) - 1, .count = 100 } }); - try testing.expectEqualStrings("", raw_huge.read); - // Tversion releases the held slots - try x.h.version(test_cfg.msize); - try testing.expect(!x.h.conn.slot_used[0] and !x.h.conn.slot_used[1]); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } }); - try x.h.walkTo(3, &.{ "vars", "state", "addr" }); - _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }); -} - -test "static and var nodes refuse create, remove and wstat; directories refuse writes" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - const dirs = [_][]const []const u8{ &.{}, &.{"build"}, &.{"comptime"}, &.{ "comptime", "types" }, &.{ "comptime", "types", "Layout" }, &.{"runtime"}, &.{ "runtime", "fn" }, &.{"vars"}, &.{ "vars", "state" }, &.{ "vars", "state", "f" }, &.{ "vars", "state", "f", "inner" } }; - for (dirs, 0..) |d, k| { - const fid: u32 = @intCast(10 + k); - try x.h.walkTo(fid, d); - try x.h.expectFail(.{ .create = .{ .fid = fid, .name = "x", .perm = 0o644, .mode = cloud9.owrite } }, "permission denied"); - try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } }, "permission denied"); - try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.owrite } }, "is a directory"); - try x.h.expectFail(.{ .open = .{ .fid = fid, .mode = cloud9.oread | cloud9.otrunc } }, "is a directory"); - try x.h.expectFail(.{ .remove = .{ .fid = fid } }, "permission denied"); - try x.h.expectFail(.{ .clunk = .{ .fid = fid } }, "unknown fid"); - } - const files = [_][]const []const u8{ &.{"README"}, &.{ "build", "time" }, &.{ "comptime", "decls" }, &.{ "runtime", "pid" }, &.{ "runtime", "fn", "fib30" }, &.{"ctl"}, &.{ "vars", "state", "value" }, &.{ "vars", "state", "raw" }, &.{ "vars", "state", "f", "a", "value" }, &.{ "vars", "counter", "type" } }; - for (files, 0..) |f, k| { - const fid: u32 = @intCast(30 + k); - try x.h.walkTo(fid, f); - try x.h.expectFail(.{ .wstat = .{ .fid = fid, .stat = stat_dontcare } }, "permission denied"); - try x.h.expectFail(.{ .walk = .{ .fid = fid, .newfid = 99, .names = &.{".."} } }, "not a directory"); - try x.h.expectFail(.{ .remove = .{ .fid = fid } }, "permission denied"); - } - // writes to a var value at a non-zero offset and with an empty payload - try x.h.walkTo(1, &.{ "vars", "state", "f", "a", "value" }); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.owrite | cloud9.otrunc } }); - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "" } }, "bad value"); - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "-1" } }, "bad value"); - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "1e3" } }, "bad value"); - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "99999999999999999999" } }, "bad value"); - try testing.expectEqual(@as(u32, 1), x.exposed.a); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = std.math.maxInt(u64), .data = "77\n" } }); - try testing.expectEqual(@as(u32, 77), x.exposed.a); - // reads of a write-only fid are refused; OEXEC reads like OREAD - try x.h.expectFail(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }, "file not open"); - try x.h.walkTo(2, &.{"README"}); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oexec } }); - try testing.expect((try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 10 } })).read.len == 10); -} - -test "msize 24: every request that fits is answered, every reply that cannot fit is an Rerror" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.version(24); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "u" } }); // Tattach 20, Rattach 20 - try x.h.expectFail(.{ .stat = .{ .fid = 0 } }, "reply too large"); // Rerror truncated to fit 24 bytes - const w = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"ctl"} } }); // Rwalk 22 - try testing.expectEqual(@as(u16, 1), w.walk.nwqid); - try x.h.expectFail(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{ ".", "." } } }, "reply too large"); - try x.h.expectFail(.{ .clunk = .{ .fid = 2 } }, "unknown fid"); - _ = try x.h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); // Ropen 24 - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 0, .data = "e" } }, "bad command"); // Twrite 24 - // the largest read the client may ask for is msize - iohdrsz = 0 bytes - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 0 } }); - try testing.expectEqual(@as(usize, 0), r.read.len); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try x.h.walkTo(3, &.{"build"}); - _ = try x.h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }); - const d = try x.h.ok(.{ .read = .{ .fid = 3, .offset = 0, .count = 0 } }); - try testing.expectEqual(@as(usize, 0), d.read.len); // no record fits in 0 bytes, nothing is split - try x.h.expectFail(.{ .read = .{ .fid = 3, .offset = 1, .count = 0 } }, "bad offset"); -} - -test "Conn.init clamps the msize cap to [msize_min, cfg.msize]" { - var ctx: TestCtx = .{}; - var shared: TS.Shared = .init(&ctx); - var storage: TS.Storage = undefined; - const lo: TS.Conn = .init(&shared, &storage, 0); - try testing.expectEqual(cloud9.Server.msize_min, lo.msize_cap); - const hi: TS.Conn = .init(&shared, &storage, std.math.maxInt(u32)); - try testing.expectEqual(test_cfg.msize, hi.msize_cap); - const mid: TS.Conn = .init(&shared, &storage, 4096); - try testing.expectEqual(@as(u32, 4096), mid.msize_cap); -} - -test "parseIso8601 rejects malformed stamps and never traps" { - try testing.expectEqual(@as(?u32, null), parseIso8601("")); - try testing.expectEqual(@as(?u32, null), parseIso8601("2023-11-14T22:13:20")); - try testing.expectEqual(@as(?u32, null), parseIso8601("2023-13-14T22:13:20Z")); - try testing.expectEqual(@as(?u32, null), parseIso8601("2023-11-32T22:13:20Z")); - try testing.expectEqual(@as(?u32, null), parseIso8601("2023-11-14T24:13:20Z")); - try testing.expectEqual(@as(?u32, null), parseIso8601("2023-11-14T22:60:20Z")); - try testing.expectEqual(@as(?u32, null), parseIso8601("1969-12-31T23:59:59Z")); - try testing.expectEqual(@as(?u32, null), parseIso8601("9999-12-31T23:59:59Z")); - try testing.expectEqual(@as(?u32, null), parseIso8601("20x3-11-14T22:13:20Z")); - try testing.expectEqual(@as(?u32, null), parseIso8601("0000-01-01T00:00:00Z")); - try testing.expectEqual(@as(u32, 1_700_000_000), parseIso8601("2023-11-14T22:13:20Z").?); - try testing.expectEqual(@as(u32, 951_782_400), parseIso8601("2000-02-29T00:00:00Z").?); - try testing.expectEqual(@as(u32, 4_102_444_799), parseIso8601("2099-12-31T23:59:59Z").?); - try testing.expectEqual(@as(u32, std.math.maxInt(u32)), parseIso8601("2106-02-07T06:28:15Z").?); - try testing.expectEqual(@as(?u32, null), parseIso8601("2106-02-07T06:28:16Z")); -} - -/// Multiplicative inverse of an odd 32-bit constant (Newton iteration). -fn inverseMod32(a: u32) u32 { - var x: u32 = a; - for (0..5) |_| x *%= 2 -% a *% x; - return x; -} - -test "fid index: fid numbers crafted to collide under the public hash do not cluster a seeded connection" { - var ctx: TestCtx = .{}; - var shared: BigS.Shared = .init(&ctx); - const storage = try testing.allocator.create(BigS.Storage); - defer testing.allocator.destroy(storage); - var h: BigS.Harness = undefined; - try h.init(&shared, storage); - defer h.deinit(); - // two connections on the same Shared never share a seed - const other: BigS.Conn = .init(&shared, storage, big_cfg.msize); - try testing.expect(other.hash_seed != h.conn.hash_seed); - // ids whose products with the golden ratio share their top bits: all one bucket when unseeded - const inv = inverseMod32(0x9E37_79B1); - try testing.expectEqual(@as(u32, 1), inv *% 0x9E37_79B1); - const n: u32 = big_cfg.max_fids - 1; - const base: u32 = 0x4242_0000; - var i: u32 = 0; - while (i < n) : (i += 1) { - const id = (base + i) *% inv; - try testing.expectEqual(@as(usize, base >> BigS.index_shift), @as(usize, @intCast((id *% 0x9E37_79B1) >> BigS.index_shift))); - _ = try h.ok(.{ .walk = .{ .fid = 0, .newfid = id, .names = &.{} } }); - } - try checkFidIndex(&h.conn); - // the longest probe sequence in the seeded table is short; unseeded it would be ~n - var worst: usize = 0; - i = 0; - while (i < n) : (i += 1) { - const id = (base + i) *% inv; - var pos = h.conn.fidHome(id); - var steps: usize = 0; - while (h.conn.fids[h.conn.index[pos]].id != id) : (pos = (pos + 1) & BigS.index_mask) steps += 1; - worst = @max(worst, steps); - } - try testing.expect(worst < 64); -} diff --git a/introspect/src/freestanding_check.zig b/introspect/src/freestanding_check.zig deleted file mode 100644 index ad88d0e..0000000 --- a/introspect/src/freestanding_check.zig +++ /dev/null @@ -1,71 +0,0 @@ -//! A tiny freestanding root proving that `core` and `vars` compile without an -//! OS: `zig build introspect-check-freestanding` builds this for riscv32-freestanding-none. -//! It instantiates `Server(cfg)` with static Storage/Shared, exposes one -//! variable, and runs one push/step over a canned Tversion frame. It must not -//! import scratch.zig (allocator) or anything OS-specific. -const std = @import("std"); -const core = @import("core.zig"); -const Writer = std.Io.Writer; - -const Build = struct { - pub const zig_version: []const u8 = @import("builtin").zig_version_string; - pub const target: []const u8 = "riscv32-freestanding-none"; - pub const optimize: []const u8 = "check"; - pub const time: []const u8 = "1970-01-01T00:00:00Z"; - pub const change: []const u8 = "none"; -}; - -const State = struct { ticks: u32, phase: enum { idle, busy }, inner: struct { x: f32 } }; - -const Fns = struct { - pub fn ticks(ctx: *anyopaque, w: *Writer) anyerror!void { - const s: *State = @ptrCast(@alignCast(ctx)); - try w.print("{d}", .{s.ticks}); - } -}; - -fn ctl(ctx: *anyopaque, cmd: []const u8, out: *Writer) anyerror!void { - const s: *State = @ptrCast(@alignCast(ctx)); - if (std.mem.startsWith(u8, cmd, "reset")) s.ticks = 0; - try out.writeAll("ok"); -} - -const cfg: core.Config = .{ - .name = "fw", - .build = Build, - .types = &.{ State, core.NodeStat }, - .decls_of = Fns, - .fns = Fns, - .ctl = &ctl, - .msize = 2048, - .max_fids = 16, - .snapshot_slots = 2, - .snapshot_bytes = 1024, -}; - -const S = core.Server(cfg); - -var state: State = .{ .ticks = 0, .phase = .idle, .inner = .{ .x = 0 } }; -var storage: S.Storage = undefined; -var shared: S.Shared = undefined; -var conn: S.Conn = undefined; - -/// Tversion msize=2048 version="9P2000". -const tversion = [_]u8{ 19, 0, 0, 0, 100, 0xFF, 0xFF, 0, 8, 0, 0, 6, 0, '9', 'P', '2', '0', '0', '0' }; - -/// Runs one Tversion through the engine; returns the number of reply bytes. -pub export fn introspect_check() u32 { - shared = .init(&state); - shared.expose("state", &state) catch unreachable; - conn = .init(&shared, &storage, cfg.msize); - _ = conn.push(&tversion); - _ = conn.step() catch return 0; - const out = conn.output(); - conn.wrote(out.len); - return @intCast(out.len); -} - -pub export fn _start() noreturn { - _ = introspect_check(); - while (true) {} -} diff --git a/introspect/src/linux/debug.zig b/introspect/src/linux/debug.zig deleted file mode 100644 index 4d43d5b..0000000 --- a/introspect/src/linux/debug.zig +++ /dev/null @@ -1,1458 +0,0 @@ -//! Linux debug facilities for the introspect server: threads, stacks, -//! registers, address → source, memory, breakpoints and panics. -//! -//! This file is a pure API; a later adapter turns it into a core `Provider`. -//! All text is written to a `*std.Io.Writer`. Nothing here allocates after -//! `init` except from the caller-provided `text_buf`, which is used as a fixed -//! arena for symbol text and reset before every query. -//! -//! Only one `Debug` may exist per process: the signal handlers and the panic -//! hook find their state through the global `current` pointer set by `init`. -//! -//! Mechanics -//! -//! * Capturing another thread's stack or registers: the calling (server) -//! thread sends `capture_signal` with `tgkill`. The SA_SIGINFO handler copies -//! the interrupted register state (`cpu_context.fromPosixSignalContext`) into -//! the single capture slot and parks on a futex. The server unwinds the -//! parked thread's stack from that context, releases the target, then -//! symbolizes. The handler is async-signal-safe: no allocation, no -//! `std.debug`, no locks other than the futex. A target that does not run -//! the handler within `capture_timeout_ns` (signal masked, thread in D -//! state, ...) yields `error.Timeout`; a late-arriving handler run cannot -//! corrupt a reused slot because it must match the requested tid and win a -//! compare-and-swap from `armed` on the slot state (that pair plays the role -//! of a generation counter: a stale run finds the slot idle, armed for -//! another tid, or armed for itself, in which case its capture is simply the -//! valid answer to the new request). -//! * Breakpoints: `@breakpoint()` raises SIGTRAP on the executing thread only. -//! The handler claims a pause slot, saves the context and parks on a futex -//! until `resumeThread`. On x86_64 the saved PC is already past `int3`; on -//! aarch64 the handler advances PC by 4 in the ucontext before returning -//! (only for a real `brk`, i.e. a kernel-generated si_code; a SIGTRAP sent -//! with kill/tgkill parks the thread where it was). With no free slot the -//! thread steps over the breakpoint and keeps running (`traps_skipped` -//! counts them): the debug layer never kills the process. The server thread -//! itself (`server_tid`) is never parked, a breakpoint there is stepped -//! over, because nobody could resume it. Only a stale handler run after -//! `deinit` (no `current`) falls back to the default disposition. -//! * Panics: `panicHook` records the message and a stack capture, then, if -//! `hold_on_panic` and a `Debug` exists, parks until `panicContinue`; then -//! `std.debug.defaultPanic` runs. A nested or second panic, or a panic on -//! the server thread itself (which could never be continued), goes -//! straight to the default handler. -//! * std.debug's `SelfInfo` guards its state with an `Io.RwLock`. A target -//! parked while holding it (a thread inside a stack-trace dump, say) would -//! deadlock the unwind, so after parking a thread the lock is probed with -//! `tryLock`; a held lock yields `error.Busy` and the target is released. -//! * Known-module guard: `std.debug.SelfInfo` (Zig 0.16) rebuilds its module -//! list whenever it is asked about an address outside every known module, -//! freeing the CIE lists its unwind cache still points into; later unwinds -//! then read freed memory. `init` records the PT_LOAD ranges of the -//! executable (the same source std uses) and every lookup or unwind is -//! first checked against them; addresses outside (unmapped, vDSO, ...) -//! render as "?" and are never handed to std. - -const std = @import("std"); -const builtin = @import("builtin"); -const linux = std.os.linux; -const cpu_context = std.debug.cpu_context; -const Writer = std.Io.Writer; -const Native = cpu_context.Native; -const arch = builtin.cpu.arch; - -pub const Options = struct { - /// Used for `std.debug` symbolization (reading debug info from disk). - io: std.Io, - /// Fixed arena for symbol text. A `FixedBufferAllocator` is placed over it - /// and reset before every query. 16 KiB is plenty; 4 KiB is a sane floor. - text_buf: []u8, - /// Real-time signal used to snapshot other threads. SIGRTMIN is 32 on - /// Linux without libc; the default is SIGRTMIN+3. - capture_signal: u8 = default_capture_signal, - /// How long to wait for a target thread to run the capture handler. - capture_timeout_ns: u64 = 250 * std.time.ns_per_ms, - /// How many threads may be parked in `@breakpoint()` at once (≤ 32). - max_paused: u8 = 16, -}; - -pub const default_capture_signal: u8 = 32 + 3; - -/// Hard upper bound of `Options.max_paused` (slot storage is static). -pub const max_paused_cap = 32; -/// Maximum number of frames written by any stack function. -pub const max_frames = 64; -/// Maximum number of tids enumerated from /proc/self/task. -pub const max_threads = 512; -/// Upper bound of the recorded panic message. -pub const panic_msg_cap = 1024; -/// Maximum number of PT_LOAD ranges recorded by the known-module guard. -pub const max_ranges = 64; - -/// Consulted by `panicHook`: when true and a `Debug` is initialized, the -/// panicking thread is held until `panicContinue`. -pub var hold_on_panic: bool = true; - -/// The one live instance, set by `init`, cleared by `deinit`. -pub var current: ?*Debug = null; - -/// The tid of the thread serving requests (0 = none). That thread is never -/// parked by a breakpoint or held by a panic, since nobody could release it. -pub var server_tid: std.atomic.Value(u32) = .init(0); - -/// Breakpoints stepped over because no pause slot was free, or because they -/// were hit on the server thread. -pub var traps_skipped: std.atomic.Value(u32) = .init(0); - -pub const Error = error{ - /// The target thread did not run the capture handler in time. - Timeout, - /// No thread with that tid exists in this process. - NoThread, - /// The address is not mapped (EFAULT from process_vm_readv/writev). - Unmapped, - /// The thread is not parked in a breakpoint. - NotPaused, - /// No panic has been recorded / is being held. - NoPanic, - /// Another `Debug` already exists in this process. - AlreadyInitialized, - /// The operation is not available on this architecture / kernel. - Unsupported, - /// The target thread is parked inside std.debug (holding its lock); its - /// stack cannot be unwound without deadlocking. Retry later. - Busy, - /// Invalid option value. - InvalidOptions, - /// A syscall or /proc read failed unexpectedly. - Unexpected, - /// The writer failed. - WriteFailed, -}; - -// Capture slot states. -const cap_idle: u32 = 0; -const cap_armed: u32 = 1; -const cap_capturing: u32 = 2; -const cap_captured: u32 = 3; -const cap_failed: u32 = 4; - -// Pause slot states. -const pause_free: u32 = 0; -const pause_claimed: u32 = 1; -const pause_paused: u32 = 2; -const pause_resuming: u32 = 3; - -const CaptureSlot = struct { - state: std.atomic.Value(u32) = .init(cap_idle), - target_tid: std.atomic.Value(u32) = .init(0), - ctx: Native = undefined, -}; - -const PauseSlot = struct { - state: std.atomic.Value(u32) = .init(pause_free), - tid: std.atomic.Value(u32) = .init(0), - ctx: Native = undefined, -}; - -pub const Debug = struct { - io: std.Io, - text_buf: []u8, - capture_signal: linux.SIG, - capture_timeout_ns: u64, - max_paused: u8, - - capture: CaptureSlot = .{}, - paused: [max_paused_cap]PauseSlot = [_]PauseSlot{.{}} ** max_paused_cap, - - old_capture_action: linux.Sigaction = undefined, - old_trap_action: linux.Sigaction = undefined, - breakpoints_enabled: bool = false, - - tids: [max_threads]u32 = undefined, - tid_count: usize = 0, - - ranges: [max_ranges]Range = undefined, - range_count: usize = 0, - - const Range = struct { start: usize, len: usize }; - - /// Installs the capture handler (not the SIGTRAP handler) and publishes - /// `d` as `current`. - pub fn init(d: *Debug, opts: Options) Error!void { - if (current != null) return error.AlreadyInitialized; - if (opts.capture_signal < 32 or opts.capture_signal >= linux.NSIG) return error.InvalidOptions; - if (opts.max_paused == 0 or opts.max_paused > max_paused_cap) return error.InvalidOptions; - if (Native == noreturn) return error.Unsupported; - d.* = .{ - .io = opts.io, - .text_buf = opts.text_buf, - .capture_signal = @enumFromInt(opts.capture_signal), - .capture_timeout_ns = opts.capture_timeout_ns, - .max_paused = opts.max_paused, - }; - d.scanModules(); - const act: linux.Sigaction = .{ - .handler = .{ .sigaction = captureHandler }, - .mask = linux.sigemptyset(), - .flags = linux.SA.SIGINFO | linux.SA.RESTART, - }; - current = d; - if (linux.errno(linux.sigaction(d.capture_signal, &act, &d.old_capture_action)) != .SUCCESS) { - current = null; - return error.Unexpected; - } - } - - /// Restores the signal dispositions and clears `current`. Threads parked - /// in a breakpoint are resumed first. - pub fn deinit(d: *Debug) void { - d.disableBreakpoints(); - _ = linux.sigaction(d.capture_signal, &d.old_capture_action, null); - if (current == d) current = null; - } - - /// Installs the SIGTRAP handler so that `@breakpoint()` parks the thread. - pub fn enableBreakpoints(d: *Debug) Error!void { - if (d.breakpoints_enabled) return; - if (arch != .x86_64 and !arch.isAARCH64()) return error.Unsupported; - const act: linux.Sigaction = .{ - .handler = .{ .sigaction = trapHandler }, - .mask = linux.sigemptyset(), - .flags = linux.SA.SIGINFO | linux.SA.RESTART, - }; - if (linux.errno(linux.sigaction(.TRAP, &act, &d.old_trap_action)) != .SUCCESS) return error.Unexpected; - d.breakpoints_enabled = true; - } - - /// Restores the previous SIGTRAP disposition and resumes every parked thread. - pub fn disableBreakpoints(d: *Debug) void { - if (!d.breakpoints_enabled) return; - _ = linux.sigaction(.TRAP, &d.old_trap_action, null); - d.breakpoints_enabled = false; - for (&d.paused) |*slot| { - if (slot.state.cmpxchgStrong(pause_paused, pause_resuming, .acq_rel, .acquire) == null) - futexWake(&slot.state); - } - } - - // ---------------------------------------------------------------- threads - - /// The nth tid of this process, numerically sorted; null past the end. - /// Index 0 rescans /proc/self/task; higher indices reuse that scan. - pub fn threadAt(d: *Debug, index: usize) ?u32 { - if (index == 0 or d.tid_count == 0) d.scanThreads(); - if (index >= d.tid_count) return null; - return d.tids[index]; - } - - pub fn threadExists(d: *Debug, tid: u32) bool { - _ = d; - var path_buf: [64]u8 = undefined; - const path = std.fmt.bufPrintZ(&path_buf, "/proc/self/task/{d}/comm", .{tid}) catch return false; - var buf: [32]u8 = undefined; - _ = readFile(path, &buf) catch return false; - return true; - } - - /// The thread's comm (without the trailing newline). - pub fn threadName(d: *Debug, tid: u32, w: *Writer) Error!void { - _ = d; - var path_buf: [64]u8 = undefined; - const path = std.fmt.bufPrintZ(&path_buf, "/proc/self/task/{d}/comm", .{tid}) catch return error.Unexpected; - var buf: [64]u8 = undefined; - const text = readFile(path, &buf) catch |err| switch (err) { - error.NotFound => return error.NoThread, - else => return error.Unexpected, - }; - w.writeAll(std.mem.trimEnd(u8, text, "\n")) catch return error.WriteFailed; - } - - /// A few fields of /proc/self/task//stat, one "name value" per line: - /// state, utime, stime, minflt, majflt, priority, nice, processor. - pub fn threadStat(d: *Debug, tid: u32, w: *Writer) Error!void { - _ = d; - var path_buf: [64]u8 = undefined; - const path = std.fmt.bufPrintZ(&path_buf, "/proc/self/task/{d}/stat", .{tid}) catch return error.Unexpected; - var buf: [1024]u8 = undefined; - const text = readFile(path, &buf) catch |err| switch (err) { - error.NotFound => return error.NoThread, - else => return error.Unexpected, - }; - // " () S "; comm may contain spaces and parens. - const close = std.mem.lastIndexOfScalar(u8, text, ')') orelse return error.Unexpected; - var it = std.mem.tokenizeScalar(u8, text[close + 1 ..], ' '); - // Field numbers below are 0-based from `state`. - const wanted = [_]struct { idx: usize, name: []const u8 }{ - .{ .idx = 0, .name = "state" }, - .{ .idx = 11, .name = "utime" }, - .{ .idx = 12, .name = "stime" }, - .{ .idx = 7, .name = "minflt" }, - .{ .idx = 9, .name = "majflt" }, - .{ .idx = 15, .name = "priority" }, - .{ .idx = 16, .name = "nice" }, - .{ .idx = 36, .name = "processor" }, - }; - var fields: [40][]const u8 = undefined; - var n: usize = 0; - while (it.next()) |f| : (n += 1) { - if (n == fields.len) break; - fields[n] = f; - } - for (wanted) |want| { - const value = if (want.idx < n) fields[want.idx] else "?"; - w.print("{s} {s}\n", .{ want.name, value }) catch return error.WriteFailed; - } - } - - /// "#n 0x in (::)" per frame. The calling - /// thread unwinds itself directly; any other thread is captured with the - /// capture signal. - pub fn threadStack(d: *Debug, tid: u32, w: *Writer) Error!void { - var addrs: [max_frames]usize = undefined; - var trace: std.debug.StackTrace = undefined; - if (tid == selfTid()) { - trace = std.debug.captureCurrentStackTrace(.{}, &addrs); - } else { - try d.captureThread(tid); - if (!d.selfInfoFree()) { - d.releaseCapture(); - return error.Busy; - } - trace = d.unwindContext(&d.capture.ctx, &addrs); - d.releaseCapture(); - } - try d.writeFrames(trace.return_addresses, w); - } - - /// " 0x" per general register, plus pc/sp/fp aliases. - pub fn threadRegs(d: *Debug, tid: u32, w: *Writer) Error!void { - if (tid == selfTid()) { - const ctx = Native.current(); - return writeRegs(&ctx, w); - } - try d.captureThread(tid); - const ctx = d.capture.ctx; - d.releaseCapture(); - return writeRegs(&ctx, w); - } - - // ------------------------------------------------------ addresses & memory - - /// "fn\nfile:line:col\nmodule\n", unknown parts as "?". - pub fn resolveAddr(d: *Debug, addr: usize, w: *Writer) Error!void { - if (!d.knownCode(addr)) return w.writeAll("?\n?\n?\n") catch error.WriteFailed; - var fba = std.heap.FixedBufferAllocator.init(d.text_buf); - const alloc = fba.allocator(); - const di = std.debug.getSelfDebugInfo() catch return error.Unsupported; - var sym = std.debug.Symbol.unknown; - var symbols: std.ArrayList(std.debug.Symbol) = .empty; - if (di.getSymbols(d.io, alloc, alloc, addr, true, &symbols)) { - if (symbols.items.len > 0) sym = symbols.items[0]; - } else |_| {} - w.print("{s}\n", .{sym.name orelse "?"}) catch return error.WriteFailed; - if (sym.source_location) |sl| { - w.print("{s}:{d}:{d}\n", .{ sl.file_name, sl.line, sl.column }) catch return error.WriteFailed; - } else { - w.writeAll("?\n") catch return error.WriteFailed; - } - const module = di.getModuleName(d.io, addr) catch "?"; - w.print("{s}\n", .{module}) catch return error.WriteFailed; - } - - /// Reads `buf.len` bytes at `addr` via process_vm_readv on the own - /// process. Never faults. Returns the number of bytes read (short when the - /// range crosses into an unmapped page); `error.Unmapped` when nothing - /// could be read. - pub fn readMem(d: *Debug, addr: usize, buf: []u8) Error!usize { - _ = d; - if (buf.len == 0) return 0; - // Page 0 is never mapped (mmap_min_addr) and a null `iovec.base` is a - // safety-checked cast; the same answer without the trap. - if (addr == 0) return error.Unmapped; - const local = [_]std.posix.iovec{.{ .base = buf.ptr, .len = buf.len }}; - const remote = [_]std.posix.iovec_const{.{ .base = @ptrFromInt(addr), .len = buf.len }}; - const rc = linux.process_vm_readv(linux.getpid(), &local, &remote, 0); - switch (linux.errno(rc)) { - .SUCCESS => return rc, - .FAULT => return error.Unmapped, - .NOSYS, .PERM => return error.Unsupported, - else => return error.Unexpected, - } - } - - /// Writes `data` at `addr` via process_vm_writev. Read-only mappings also - /// report `error.Unmapped` (the kernel says EFAULT for both). - pub fn writeMem(d: *Debug, addr: usize, data: []const u8) Error!usize { - _ = d; - if (data.len == 0) return 0; - if (addr == 0) return error.Unmapped; - const local = [_]std.posix.iovec_const{.{ .base = data.ptr, .len = data.len }}; - const remote = [_]std.posix.iovec_const{.{ .base = @ptrFromInt(addr), .len = data.len }}; - const rc = linux.process_vm_writev(linux.getpid(), &local, &remote, 0); - switch (linux.errno(rc)) { - .SUCCESS => return rc, - .FAULT => return error.Unmapped, - .NOSYS, .PERM => return error.Unsupported, - else => return error.Unexpected, - } - } - - /// Hexdump of `len` bytes at `addr` in the shape of `std.debug.dumpHex` - /// (16 bytes per line, address column, bytes in two groups, ASCII column). - /// Stops early at the first unmapped byte; `error.Unmapped` only when the - /// very first chunk is unreadable. - pub fn hexdump(d: *Debug, addr: usize, len: usize, w: *Writer) Error!void { - var chunk: [256]u8 = undefined; - var done: usize = 0; - while (done < len) { - const want = @min(chunk.len, len - done); - const got = d.readMem(addr +% done, chunk[0..want]) catch |err| switch (err) { - error.Unmapped => if (done == 0) return error.Unmapped else break, - else => return err, - }; - if (got == 0) break; - try writeHexLines(addr +% done, chunk[0..got], w); - done += got; - if (got < want) break; - } - } - - /// Copies /proc/self/maps to `w`. - pub fn maps(d: *Debug, w: *Writer) Error!void { - _ = d; - return streamFile("/proc/self/maps", w); - } - - /// Reads `buf.len` bytes of /proc/self/maps at `offset` (0 at the end). - /// Not a consistent snapshot across reads; a map appearing between two - /// reads shifts the text, like `cat` on /proc itself. - pub fn readMaps(d: *Debug, offset: u64, buf: []u8) Error!usize { - _ = d; - if (offset > std.math.maxInt(i64)) return 0; - return preadFile("/proc/self/maps", offset, buf); - } - - // ------------------------------------------------------------ breakpoints - - /// The nth tid currently parked in `@breakpoint()`. - pub fn pausedAt(d: *Debug, index: usize) ?u32 { - var n: usize = 0; - for (d.paused[0..d.max_paused]) |*slot| { - if (slot.state.load(.acquire) != pause_paused) continue; - if (n == index) return slot.tid.load(.acquire); - n += 1; - } - return null; - } - - pub fn isPaused(d: *Debug, tid: u32) bool { - return d.pausedSlot(tid) != null; - } - - pub fn pausedStack(d: *Debug, tid: u32, w: *Writer) Error!void { - const slot = d.pausedSlot(tid) orelse return error.NotPaused; - if (!d.selfInfoFree()) return error.Busy; - var addrs: [max_frames]usize = undefined; - const trace = d.unwindContext(&slot.ctx, &addrs); - try d.writeFrames(trace.return_addresses, w); - } - - pub fn pausedRegs(d: *Debug, tid: u32, w: *Writer) Error!void { - const slot = d.pausedSlot(tid) orelse return error.NotPaused; - return writeRegs(&slot.ctx, w); - } - - /// Lets a parked thread continue past its breakpoint. - pub fn resumeThread(d: *Debug, tid: u32) Error!void { - const slot = d.pausedSlot(tid) orelse return error.NotPaused; - if (slot.state.cmpxchgStrong(pause_paused, pause_resuming, .acq_rel, .acquire) != null) return error.NotPaused; - futexWake(&slot.state); - } - - fn pausedSlot(d: *Debug, tid: u32) ?*PauseSlot { - for (d.paused[0..d.max_paused]) |*slot| { - if (slot.state.load(.acquire) == pause_paused and slot.tid.load(.acquire) == tid) return slot; - } - return null; - } - - // ------------------------------------------------------------------ panic - - /// The recorded panic message; nothing before any panic. - pub fn panicMessage(d: *Debug, w: *Writer) Error!void { - _ = d; - if (panic_state.load(.acquire) == panic_none) return; - w.writeAll(panic_msg[0..panic_msg_len]) catch return error.WriteFailed; - } - - /// Frames of the panicking thread, symbolized lazily. - pub fn panicStack(d: *Debug, w: *Writer) Error!void { - if (panic_state.load(.acquire) == panic_none) return; - try d.writeFrames(panic_addrs[0..panic_addr_count], w); - } - - /// True while a panicking thread is parked waiting for `panicContinue`. - pub fn panicHeld(d: *Debug) bool { - _ = d; - return panic_state.load(.acquire) == panic_held; - } - - /// Releases the held panicking thread into `std.debug.defaultPanic`. - pub fn panicContinue(d: *Debug) Error!void { - _ = d; - if (panic_state.cmpxchgStrong(panic_held, panic_continued, .acq_rel, .acquire) != null) return error.NoPanic; - futexWake(&panic_state); - } - - // -------------------------------------------------------------- internals - - fn scanThreads(d: *Debug) void { - d.tid_count = 0; - const fd_rc = linux.open("/proc/self/task", .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true }, 0); - if (linux.errno(fd_rc) != .SUCCESS) return; - const fd: i32 = @intCast(fd_rc); - defer _ = linux.close(fd); - var buf: [4096]u8 align(@alignOf(linux.dirent64)) = undefined; - while (true) { - const rc = linux.getdents64(fd, &buf, buf.len); - if (linux.errno(rc) != .SUCCESS or rc == 0) break; - var off: usize = 0; - while (off < rc) { - const ent: *align(1) const linux.dirent64 = @ptrCast(&buf[off]); - const name_ptr: [*:0]const u8 = @ptrCast(&buf[off + @offsetOf(linux.dirent64, "name")]); - const name = std.mem.span(name_ptr); - if (std.fmt.parseInt(u32, name, 10)) |tid| { - if (d.tid_count < max_threads) { - d.tids[d.tid_count] = tid; - d.tid_count += 1; - } - } else |_| {} - off += ent.reclen; - } - } - std.mem.sort(u32, d.tids[0..d.tid_count], {}, std.sort.asc(u32)); - } - - /// Arms the capture slot for `tid`, signals it and waits until the handler - /// has parked with its context copied. On success the caller owns the - /// slot until `releaseCapture`. - fn captureThread(d: *Debug, tid: u32) Error!void { - const slot = &d.capture; - slot.target_tid.store(tid, .release); - slot.state.store(cap_armed, .release); - const rc = linux.tgkill(linux.getpid(), @intCast(tid), d.capture_signal); - switch (linux.errno(rc)) { - .SUCCESS => {}, - .SRCH => { - slot.state.store(cap_idle, .release); - return error.NoThread; - }, - else => { - slot.state.store(cap_idle, .release); - return error.Unexpected; - }, - } - const deadline = monotonicNs() + d.capture_timeout_ns; - while (true) { - const s = slot.state.load(.acquire); - switch (s) { - cap_captured => return, - cap_failed => { - slot.state.store(cap_idle, .release); - return error.Unsupported; - }, - cap_armed => { - const now = monotonicNs(); - if (now >= deadline) { - // Disarm; if the handler raced us it has moved on to - // `capturing` and we simply keep waiting for it. - if (slot.state.cmpxchgStrong(cap_armed, cap_idle, .acq_rel, .acquire) == null) return error.Timeout; - continue; - } - futexWaitNs(&slot.state, cap_armed, deadline - now); - }, - // The handler is copying registers; it finishes promptly. - cap_capturing => futexWaitNs(&slot.state, cap_capturing, 1 * std.time.ns_per_ms), - else => unreachable, - } - } - } - - /// Records the PT_LOAD ranges of every module `dl_iterate_phdr` reports - /// (for a static executable: the executable itself, not the vDSO). - fn scanModules(d: *Debug) void { - d.range_count = 0; - std.posix.dl_iterate_phdr(d, error{}, struct { - fn cb(info: *std.posix.dl_phdr_info, _: usize, ctx: *Debug) error{}!void { - for (info.phdr[0..info.phnum]) |phdr| { - if (phdr.type != .LOAD) continue; - if (ctx.range_count == max_ranges) return; - ctx.ranges[ctx.range_count] = .{ .start = info.addr +% phdr.vaddr, .len = phdr.memsz }; - ctx.range_count += 1; - } - } - }.cb) catch {}; - } - - /// True when `addr` lies in a module `std.debug` already knows about, so - /// that asking it about `addr` cannot trigger a module rescan. - fn knownCode(d: *const Debug, addr: usize) bool { - for (d.ranges[0..d.range_count]) |r| { - if (addr >= r.start and addr - r.start < r.len) return true; - } - return false; - } - - /// Unwinds from a saved context. A pc outside every known module (e.g. a - /// thread inside the vDSO) is reported as a single frame and not unwound, - /// because std would otherwise rescan its module list (see the header). - fn unwindContext(d: *const Debug, ctx: *const Native, addrs: *[max_frames]usize) std.debug.StackTrace { - if (!d.knownCode(ctx.getPc())) { - addrs[0] = ctx.getPc() +| 1; - return .{ .return_addresses = addrs[0..1], .skipped = .unknown }; - } - return std.debug.captureCurrentStackTrace(.{ .context = ctx }, addrs); - } - - /// True when nobody holds std.debug's `SelfInfo` lock right now. Called - /// with the target parked, so a held lock means the *target* (or another - /// live thread, which will let go) holds it; only the former deadlocks, - /// and the caller cannot tell them apart, so both yield `error.Busy`. - fn selfInfoFree(d: *const Debug) bool { - if (comptime !@hasField(std.debug.SelfInfo, "rwlock")) return true; - const di = std.debug.getSelfDebugInfo() catch return true; - if (!di.rwlock.tryLock(d.io)) return false; - di.rwlock.unlock(d.io); - return true; - } - - fn releaseCapture(d: *Debug) void { - d.capture.state.store(cap_idle, .release); - futexWake(&d.capture.state); - } - - fn writeFrames(d: *Debug, addrs: []const usize, w: *Writer) Error!void { - var fba = std.heap.FixedBufferAllocator.init(d.text_buf); - const alloc = fba.allocator(); - const di = std.debug.getSelfDebugInfo() catch return error.Unsupported; - for (addrs, 0..) |ret_addr, i| { - // Return addresses point after the call; the first frame of a - // context capture is stored as pc+1 by std for the same reason. - const addr = ret_addr -| 1; - fba.reset(); - var symbols: std.ArrayList(std.debug.Symbol) = .empty; - var sym = std.debug.Symbol.unknown; - if (d.knownCode(addr)) { - if (di.getSymbols(d.io, alloc, alloc, addr, true, &symbols)) { - if (symbols.items.len > 0) sym = symbols.items[0]; - } else |_| {} - } - w.print("#{d} 0x{x} in {s} (", .{ i, addr, sym.name orelse "?" }) catch return error.WriteFailed; - if (sym.source_location) |sl| { - w.print("{s}:{d}:{d})\n", .{ sl.file_name, sl.line, sl.column }) catch return error.WriteFailed; - } else { - w.writeAll("?)\n") catch return error.WriteFailed; - } - } - } -}; - -// ------------------------------------------------------------------ handlers - -fn selfTid() u32 { - return @intCast(linux.gettid()); -} - -fn captureHandler(_: linux.SIG, _: *const linux.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { - const d = current orelse return; - const slot = &d.capture; - const me = selfTid(); - if (slot.target_tid.load(.acquire) != me) return; - if (slot.state.cmpxchgStrong(cap_armed, cap_capturing, .acq_rel, .acquire) != null) return; - // The tid check and the swap are not one atomic step: a stale run (a - // signal that stayed pending while its request timed out) may have read - // the old tid and then won the swap of a request re-armed for another - // thread. `target_tid` is fixed while the slot is armed, so re-checking - // after the swap closes the window; hand the slot back untouched. - if (slot.target_tid.load(.acquire) != me) { - slot.state.store(cap_armed, .release); - futexWake(&slot.state); - return; - } - if (cpu_context.fromPosixSignalContext(ctx_ptr)) |ctx| { - slot.ctx = ctx; - slot.state.store(cap_captured, .release); - futexWake(&slot.state); - while (slot.state.load(.acquire) == cap_captured) futexWaitNs(&slot.state, cap_captured, null); - } else { - slot.state.store(cap_failed, .release); - futexWake(&slot.state); - } -} - -/// aarch64 Linux ucontext_t, only as far as `mcontext.pc` (see -/// std.debug.cpu_context's signal_ucontext_t). -const UcontextAarch64 = extern struct { - flags: usize, - link: ?*UcontextAarch64, - stack: linux.stack_t, - sigmask: linux.sigset_t, - unused: [120]u8, - mcontext: extern struct { - fault_address: u64 align(16), - x: [30]u64, - lr: u64, - sp: u64, - pc: u64, - }, -}; - -fn trapHandler(_: linux.SIG, info: *const linux.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { - const d = current orelse return trapFallback(); - const ctx = cpu_context.fromPosixSignalContext(ctx_ptr) orelse return trapFallback(); - // si_code > 0 is kernel-generated (TRAP_BRKPT for int3/brk); <= 0 is - // kill/tgkill/sigqueue from user space, where PC points at the - // interrupted instruction and must not be touched. - const from_instruction = info.code > 0; - if (comptime arch.isAARCH64()) { - // `brk #imm` does not advance PC; step over it so returning from the - // handler does not re-trap. - if (from_instruction) { - const uc: *UcontextAarch64 = @ptrCast(@alignCast(ctx_ptr.?)); - uc.mcontext.pc += 4; - } - } else if (comptime arch != .x86_64) { - return trapFallback(); - } - const tid = selfTid(); - if (tid == server_tid.load(.acquire)) { - // Nobody could resume the thread that serves /breakpoints: step over. - _ = traps_skipped.fetchAdd(1, .acq_rel); - return; - } - const slot: *PauseSlot = for (d.paused[0..d.max_paused]) |*slot| { - if (slot.state.cmpxchgStrong(pause_free, pause_claimed, .acq_rel, .acquire) == null) break slot; - } else { - _ = traps_skipped.fetchAdd(1, .acq_rel); - return; - }; - slot.ctx = ctx; - slot.tid.store(tid, .release); - slot.state.store(pause_paused, .release); - while (slot.state.load(.acquire) == pause_paused) futexWaitNs(&slot.state, pause_paused, null); - slot.state.store(pause_free, .release); -} - -/// Restores the default SIGTRAP disposition and re-raises it: the signal is -/// blocked while the handler runs, so it is delivered (fatally) on return. -/// Only for a handler run with no `Debug` (a trap in flight during `deinit`) -/// or on an architecture whose context cannot be read. -fn trapFallback() void { - const act: linux.Sigaction = .{ - .handler = .{ .handler = linux.SIG.DFL }, - .mask = linux.sigemptyset(), - .flags = 0, - }; - _ = linux.sigaction(.TRAP, &act, null); - _ = linux.tkill(linux.gettid(), .TRAP); -} - -// --------------------------------------------------------------------- panic - -const panic_none: u32 = 0; -const panic_recording: u32 = 1; -const panic_recorded: u32 = 2; -const panic_held: u32 = 3; -const panic_continued: u32 = 4; - -var panic_state: std.atomic.Value(u32) = .init(panic_none); -var panic_msg: [panic_msg_cap]u8 = undefined; -var panic_msg_len: usize = 0; -var panic_addrs: [max_frames]usize = undefined; -var panic_addr_count: usize = 0; -/// The tid of the panicking thread (0 before any panic). -pub var panic_tid: u32 = 0; - -/// Records the first panic: message (bounded copy) and stack addresses. -/// Returns false if a panic was already recorded (nested or second panic). -pub fn recordPanic(msg: []const u8, first_trace_addr: ?usize) bool { - if (panic_state.cmpxchgStrong(panic_none, panic_recording, .acq_rel, .acquire) != null) return false; - panic_tid = selfTid(); - panic_msg_len = @min(msg.len, panic_msg.len); - @memcpy(panic_msg[0..panic_msg_len], msg[0..panic_msg_len]); - const trace = std.debug.captureCurrentStackTrace(.{ .first_address = first_trace_addr }, &panic_addrs); - panic_addr_count = trace.return_addresses.len; - panic_state.store(panic_recorded, .release); - return true; -} - -/// Parks the panicking thread until `Debug.panicContinue` when holding is -/// enabled and a `Debug` exists; then hands over to `std.debug.defaultPanic`. -pub fn panicHook(msg: []const u8, first_trace_addr: ?usize) noreturn { - @branchHint(.cold); - if (recordPanic(msg, first_trace_addr)) { - // The server thread cannot be held: it is the one that would have to - // serve /panic/ctl. - if (hold_on_panic and current != null and panic_tid != server_tid.load(.acquire)) { - if (panic_state.cmpxchgStrong(panic_recorded, panic_held, .acq_rel, .acquire) == null) { - while (panic_state.load(.acquire) == panic_held) futexWaitNs(&panic_state, panic_held, null); - } - } - } - std.debug.defaultPanic(msg, first_trace_addr); -} - -/// Clears the recorded panic. Only meaningful in tests of the record path. -pub fn resetPanicRecord() void { - panic_msg_len = 0; - panic_addr_count = 0; - panic_tid = 0; - panic_state.store(panic_none, .release); -} - -// ------------------------------------------------------------------- helpers - -fn futexWake(word: *std.atomic.Value(u32)) void { - _ = linux.futex_3arg(&word.raw, .{ .cmd = .WAKE, .private = true }, std.math.maxInt(u32)); -} - -/// Waits while `*word == expect`, at most `timeout_ns` (forever when null). -/// Returns on wake, timeout, value change or EINTR; callers loop. -fn futexWaitNs(word: *std.atomic.Value(u32), expect: u32, timeout_ns: ?u64) void { - var ts: linux.timespec = undefined; - const ts_ptr: ?*const linux.timespec = if (timeout_ns) |ns| blk: { - ts = .{ .sec = @intCast(ns / std.time.ns_per_s), .nsec = @intCast(ns % std.time.ns_per_s) }; - break :blk &ts; - } else null; - _ = linux.futex_4arg(&word.raw, .{ .cmd = .WAIT, .private = true }, expect, ts_ptr); -} - -fn monotonicNs() u64 { - var ts: linux.timespec = undefined; - _ = linux.clock_gettime(.MONOTONIC, &ts); - return @as(u64, @intCast(ts.sec)) * std.time.ns_per_s + @as(u64, @intCast(ts.nsec)); -} - -const FileError = error{ NotFound, Unexpected, TooBig }; - -/// Reads a whole (small) file with raw syscalls. -fn readFile(path: [*:0]const u8, buf: []u8) FileError![]u8 { - const fd_rc = linux.open(path, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); - switch (linux.errno(fd_rc)) { - .SUCCESS => {}, - .NOENT, .SRCH => return error.NotFound, - else => return error.Unexpected, - } - const fd: i32 = @intCast(fd_rc); - defer _ = linux.close(fd); - var len: usize = 0; - while (len < buf.len) { - const rc = linux.read(fd, buf[len..].ptr, buf.len - len); - switch (linux.errno(rc)) { - .SUCCESS => {}, - .INTR => continue, - .SRCH, .NOENT => return error.NotFound, - else => return error.Unexpected, - } - if (rc == 0) return buf[0..len]; - len += rc; - } - return error.TooBig; -} - -/// One pread of `buf.len` bytes at `offset`; 0 at the end of the file. -fn preadFile(path: [*:0]const u8, offset: u64, buf: []u8) Error!usize { - const fd_rc = linux.open(path, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); - if (linux.errno(fd_rc) != .SUCCESS) return error.Unexpected; - const fd: i32 = @intCast(fd_rc); - defer _ = linux.close(fd); - var len: usize = 0; - while (len < buf.len) { - const rc = linux.pread(fd, buf[len..].ptr, buf.len - len, @intCast(offset + len)); - switch (linux.errno(rc)) { - .SUCCESS => {}, - .INTR => continue, - else => return error.Unexpected, - } - if (rc == 0) break; - len += rc; - } - return len; -} - -/// Streams a file of any size to `w`. -fn streamFile(path: [*:0]const u8, w: *Writer) Error!void { - const fd_rc = linux.open(path, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); - if (linux.errno(fd_rc) != .SUCCESS) return error.Unexpected; - const fd: i32 = @intCast(fd_rc); - defer _ = linux.close(fd); - var buf: [4096]u8 = undefined; - while (true) { - const rc = linux.read(fd, &buf, buf.len); - switch (linux.errno(rc)) { - .SUCCESS => {}, - .INTR => continue, - else => return error.Unexpected, - } - if (rc == 0) return; - w.writeAll(buf[0..rc]) catch return error.WriteFailed; - } -} - -fn writeHexLines(base: usize, bytes: []const u8, w: *Writer) Error!void { - var offset: usize = 0; - while (offset < bytes.len) : (offset += 16) { - const line = bytes[offset..@min(offset + 16, bytes.len)]; - w.print("{x:0>[1]} ", .{ base +% offset, @sizeOf(usize) * 2 }) catch return error.WriteFailed; - for (line, 0..) |byte, i| { - w.print("{X:0>2} ", .{byte}) catch return error.WriteFailed; - if (i == 7) w.writeByte(' ') catch return error.WriteFailed; - } - w.writeByte(' ') catch return error.WriteFailed; - if (line.len < 16) { - var missing = (16 - line.len) * 3; - if (line.len < 8) missing += 1; - w.splatByteAll(' ', missing) catch return error.WriteFailed; - } - for (line) |byte| { - w.writeByte(if (std.ascii.isPrint(byte)) byte else '.') catch return error.WriteFailed; - } - w.writeByte('\n') catch return error.WriteFailed; - } -} - -fn writeRegs(ctx: *const Native, w: *Writer) Error!void { - if (comptime arch == .x86_64) { - inline for (@typeInfo(Native.Gpr).@"enum".fields) |f| { - w.print("{s} 0x{x}\n", .{ f.name, ctx.gprs.get(@field(Native.Gpr, f.name)) }) catch return error.WriteFailed; - } - w.print("pc 0x{x}\nsp 0x{x}\nfp 0x{x}\n", .{ - ctx.gprs.get(.rip), ctx.gprs.get(.rsp), ctx.gprs.get(.rbp), - }) catch return error.WriteFailed; - } else if (comptime arch.isAARCH64()) { - for (ctx.x, 0..) |x, i| w.print("x{d} 0x{x}\n", .{ i, x }) catch return error.WriteFailed; - w.print("sp 0x{x}\npc 0x{x}\nfp 0x{x}\nlr 0x{x}\n", .{ - ctx.sp, ctx.pc, ctx.x[29], ctx.x[30], - }) catch return error.WriteFailed; - } else { - w.print("pc 0x{x}\nfp 0x{x}\n", .{ ctx.getPc(), ctx.getFp() }) catch return error.WriteFailed; - } -} - -// --------------------------------------------------------------------- tests - -const testing = std.testing; - -fn testOptions(text_buf: []u8) Options { - return .{ .io = testing.io, .text_buf = text_buf }; -} - -noinline fn sleepMs(ms: u64) void { - var ts: linux.timespec = .{ .sec = @intCast(ms / 1000), .nsec = @intCast((ms % 1000) * std.time.ns_per_ms) }; - _ = linux.nanosleep(&ts, null); -} - -// The test threads use atomic builtins rather than `std.atomic.Value` methods -// so that, in release modes, their pc is never inside an inlined callee: the -// DWARF symbolizer names the innermost inlined function at an address (see -// the notes on `writeFrames`). -const SpinState = struct { - tid: std.atomic.Value(u32) = .init(0), - stop: bool = false, - counter: u32 = 0, - done: bool = false, -}; - -noinline fn spinHere(st: *SpinState) void { - while (!@atomicLoad(bool, &st.stop, .acquire)) { - _ = @atomicRmw(u32, &st.counter, .Add, 1, .monotonic); - } -} - -fn spinThreadMain(st: *SpinState) void { - st.tid.store(selfTid(), .release); - spinHere(st); - @atomicStore(bool, &st.done, true, .release); // keeps the call above from becoming a tail call -} - -fn waitForTid(st: *SpinState) u32 { - var tries: usize = 0; - while (st.tid.load(.acquire) == 0) : (tries += 1) { - if (tries > 2000) return 0; - sleepMs(1); - } - return st.tid.load(.acquire); -} - -test "capture own stack" { - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - try d.init(testOptions(&text_buf)); - defer d.deinit(); - try testing.expect(current == &d); - - var out: Writer.Allocating = .init(testing.allocator); - defer out.deinit(); - try d.threadStack(selfTid(), &out.writer); - const text = out.written(); - try testing.expect(std.mem.indexOf(u8, text, "#0 0x") != null); - try testing.expect(std.mem.indexOf(u8, text, "debug.zig:") != null); - try testing.expect(std.mem.indexOf(u8, text, "test.capture own stack") != null); - - out.clearRetainingCapacity(); - try d.threadRegs(selfTid(), &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "pc 0x") != null); - try testing.expect(std.mem.indexOf(u8, out.written(), "pc 0x0\n") == null); -} - -test "capture another thread: stack, regs, name, stat" { - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - try d.init(testOptions(&text_buf)); - defer d.deinit(); - - var st: SpinState = .{}; - const th = try std.Thread.spawn(.{}, spinThreadMain, .{&st}); - const tid = waitForTid(&st); - try testing.expect(tid != 0); - - var out: Writer.Allocating = .init(testing.allocator); - defer out.deinit(); - try d.threadStack(tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "spinHere") != null); - try testing.expect(std.mem.indexOf(u8, out.written(), "spinThreadMain") != null); - - out.clearRetainingCapacity(); - try d.threadRegs(tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "pc 0x") != null); - try testing.expect(std.mem.indexOf(u8, out.written(), "pc 0x0\n") == null); - - out.clearRetainingCapacity(); - try d.threadName(tid, &out.writer); - try testing.expect(out.written().len > 0); - try testing.expect(std.mem.indexOfScalar(u8, out.written(), '\n') == null); - - out.clearRetainingCapacity(); - try d.threadStat(tid, &out.writer); - try testing.expect(std.mem.startsWith(u8, out.written(), "state ")); - try testing.expect(std.mem.indexOf(u8, out.written(), "\nutime ") != null); - - // Enumeration lists both threads and nothing bogus. - try testing.expect(d.threadExists(tid)); - try testing.expect(d.threadExists(selfTid())); - var found_self = false; - var found_other = false; - var i: usize = 0; - var prev: u32 = 0; - while (d.threadAt(i)) |t| : (i += 1) { - try testing.expect(t > prev); - prev = t; - if (t == tid) found_other = true; - if (t == selfTid()) found_self = true; - } - try testing.expect(found_self and found_other); - - // Repeated captures of the same thread keep working. - var k: usize = 0; - while (k < 5) : (k += 1) { - out.clearRetainingCapacity(); - try d.threadStack(tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "spinHere") != null); - } - const before = @atomicLoad(u32, &st.counter, .acquire); - sleepMs(2); - try testing.expect(@atomicLoad(u32, &st.counter, .acquire) != before); // the thread is running again - - @atomicStore(bool, &st.stop, true, .release); - th.join(); - try testing.expect(!d.threadExists(tid)); - try testing.expectError(error.NoThread, d.threadStack(tid, &out.writer)); - try testing.expectError(error.NoThread, d.threadName(tid, &out.writer)); -} - -/// The address of the call site in the caller, i.e. inside this file's test. -noinline fn callerAddress() usize { - return @returnAddress() - 1; -} - -test "resolveAddr names this file" { - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - try d.init(testOptions(&text_buf)); - defer d.deinit(); - var out: Writer.Allocating = .init(testing.allocator); - defer out.deinit(); - try d.resolveAddr(callerAddress(), &out.writer); - const text = out.written(); - var lines = std.mem.splitScalar(u8, text, '\n'); - const fn_name = lines.next().?; - const loc = lines.next().?; - const module = lines.next().?; - try testing.expect(fn_name.len > 0 and !std.mem.eql(u8, fn_name, "?")); - try testing.expect(std.mem.indexOf(u8, loc, "debug.zig:") != null); - try testing.expect(module.len > 0); - - out.clearRetainingCapacity(); - try d.resolveAddr(8, &out.writer); - try testing.expectEqualStrings("?\n?\n?\n", out.written()); - - // Regression: an unmapped lookup must not poison std's unwind cache (see - // the header); unwinding afterwards still works. - out.clearRetainingCapacity(); - try d.threadStack(selfTid(), &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "test.resolveAddr names this file") != null); -} - -test "readMem, writeMem, hexdump" { - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - try d.init(testOptions(&text_buf)); - defer d.deinit(); - - var value: [8]u8 = .{ 1, 2, 3, 4, 5, 6, 7, 8 }; - var got: [8]u8 = undefined; - try testing.expectEqual(@as(usize, 8), try d.readMem(@intFromPtr(&value), &got)); - try testing.expectEqualSlices(u8, &value, &got); - try testing.expectError(error.Unmapped, d.readMem(8, &got)); - - const new = [_]u8{ 0xaa, 0xbb, 0xcc }; - try testing.expectEqual(@as(usize, 3), try d.writeMem(@intFromPtr(&value) + 2, &new)); - try testing.expectEqualSlices(u8, &.{ 1, 2, 0xaa, 0xbb, 0xcc, 6, 7, 8 }, &value); - try testing.expectError(error.Unmapped, d.writeMem(8, &new)); - - var bytes: [19]u8 = .{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x01, 0x12, 0x13 }; - var out: Writer.Allocating = .init(testing.allocator); - defer out.deinit(); - try d.hexdump(@intFromPtr(&bytes), bytes.len, &out.writer); - const expected = try std.fmt.allocPrint(testing.allocator, - \\{x:0>[2]} 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF .."3DUfw........ - \\{x:0>[2]} 01 12 13 ... - \\ - , .{ @intFromPtr(&bytes), @intFromPtr(&bytes) + 16, @sizeOf(usize) * 2 }); - defer testing.allocator.free(expected); - try testing.expectEqualStrings(expected, out.written()); - try testing.expectError(error.Unmapped, d.hexdump(8, 16, &out.writer)); - - // Address 0 (also reached by an offset that wraps) must be an error, not a - // safety-checked null pointer cast on the server thread. - try testing.expectError(error.Unmapped, d.readMem(0, &got)); - try testing.expectError(error.Unmapped, d.writeMem(0, &new)); - try testing.expectError(error.Unmapped, d.hexdump(0, 16, &out.writer)); - try testing.expectError(error.Unmapped, d.readMem(std.math.maxInt(usize) - 3, &got)); - try testing.expectError(error.Unmapped, d.hexdump(std.math.maxInt(usize) - 3, 16, &out.writer)); - - out.clearRetainingCapacity(); - try d.maps(&out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "[stack]") != null); - - // readMaps serves the file piecewise at any offset and ends with 0. - var piece: [4096]u8 = undefined; - var total: usize = 0; - while (true) { - const n = try d.readMaps(total, &piece); - if (n == 0) break; - total += n; - } - try testing.expect(total >= out.written().len / 2); - try testing.expectEqual(@as(usize, 0), try d.readMaps(std.math.maxInt(u64), &piece)); -} - -test "breakpoint on the server thread and past the slot table steps over; tgkill SIGTRAP parks" { - if (arch != .x86_64 and !arch.isAARCH64()) return error.SkipZigTest; - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - var opts = testOptions(&text_buf); - opts.max_paused = 1; - try d.init(opts); - defer d.deinit(); - try d.enableBreakpoints(); - defer d.disableBreakpoints(); - var out: Writer.Allocating = .init(testing.allocator); - defer out.deinit(); - - // The "server" thread (this one, for the test) hits a breakpoint: it keeps running. - const skipped0 = traps_skipped.load(.acquire); - server_tid.store(selfTid(), .release); - defer server_tid.store(0, .release); - @breakpoint(); - try testing.expectEqual(skipped0 + 1, traps_skipped.load(.acquire)); - try testing.expect(!d.isPaused(selfTid())); - - // One slot: the first trapping thread parks, the second steps over. - var a: TrapState = .{}; - const ta = try std.Thread.spawn(.{}, trapThreadMain, .{&a}); - var tries: usize = 0; - while (a.tid.load(.acquire) == 0 or !d.isPaused(a.tid.load(.acquire))) : (tries += 1) { - try testing.expect(tries < 5000); - sleepMs(1); - } - var b: TrapState = .{}; - const tb = try std.Thread.spawn(.{}, trapThreadMain, .{&b}); - tb.join(); - try testing.expectEqual(@as(u32, 1), @atomicLoad(u32, &b.counter, .acquire)); - try testing.expectEqual(skipped0 + 2, traps_skipped.load(.acquire)); - try testing.expectEqual(@as(u32, 0), @atomicLoad(u32, &a.counter, .acquire)); - try d.resumeThread(a.tid.load(.acquire)); - ta.join(); - try testing.expectEqual(@as(u32, 1), @atomicLoad(u32, &a.counter, .acquire)); - - // A SIGTRAP sent with tgkill (not an int3/brk) parks the thread where it - // was; resuming it must not skip an instruction: the spinner keeps counting. - var st: SpinState = .{}; - const th = try std.Thread.spawn(.{}, spinThreadMain, .{&st}); - const tid = waitForTid(&st); - try testing.expect(tid != 0); - try testing.expectEqual(linux.E.SUCCESS, linux.errno(linux.tgkill(linux.getpid(), @intCast(tid), .TRAP))); - tries = 0; - while (!d.isPaused(tid)) : (tries += 1) { - try testing.expect(tries < 5000); - sleepMs(1); - } - const frozen = @atomicLoad(u32, &st.counter, .acquire); - sleepMs(5); - try testing.expectEqual(frozen, @atomicLoad(u32, &st.counter, .acquire)); - out.clearRetainingCapacity(); - try d.pausedStack(tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "spinHere") != null); - try d.resumeThread(tid); - sleepMs(5); - try testing.expect(@atomicLoad(u32, &st.counter, .acquire) != frozen); - @atomicStore(bool, &st.stop, true, .release); - th.join(); -} - -const LockState = struct { - tid: std.atomic.Value(u32) = .init(0), - release: std.atomic.Value(bool) = .init(false), - unlocked: std.atomic.Value(bool) = .init(false), - stop: std.atomic.Value(bool) = .init(false), - io: std.Io, -}; - -fn lockHolderMain(st: *LockState) void { - const di = std.debug.getSelfDebugInfo() catch return; - di.rwlock.lockUncancelable(st.io); - st.tid.store(selfTid(), .release); - while (!st.release.load(.acquire)) sleepMs(1); - di.rwlock.unlock(st.io); - st.unlocked.store(true, .release); - while (!st.stop.load(.acquire)) sleepMs(1); -} - -test "a target parked while holding std.debug's lock is Busy, not a deadlock" { - if (comptime !@hasField(std.debug.SelfInfo, "rwlock")) return error.SkipZigTest; - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - try d.init(testOptions(&text_buf)); - defer d.deinit(); - var st: LockState = .{ .io = testing.io }; - const th = try std.Thread.spawn(.{}, lockHolderMain, .{&st}); - var tries: usize = 0; - while (st.tid.load(.acquire) == 0) : (tries += 1) { - try testing.expect(tries < 2000); - sleepMs(1); - } - const tid = st.tid.load(.acquire); - // No allocation while the holder has the lock: `testing.allocator` - // records a stack trace per allocation, which needs that same lock. - var buf: [16 * 1024]u8 = undefined; - var w: Writer = .fixed(&buf); - try testing.expectError(error.Busy, d.threadStack(tid, &w)); - try testing.expectEqual(cap_idle, d.capture.state.load(.acquire)); - // Registers need no unwind and are still available. - try d.threadRegs(tid, &w); - try testing.expect(std.mem.indexOf(u8, w.buffered(), "pc 0x") != null); - // Handshake, not a sleep: a slow holder would otherwise still hold the - // lock and the next capture would legitimately be Busy again. - st.release.store(true, .release); - tries = 0; - while (!st.unlocked.load(.acquire)) : (tries += 1) { - try testing.expect(tries < 5000); - sleepMs(1); - } - w = .fixed(&buf); - try d.threadStack(tid, &w); - try testing.expect(std.mem.indexOf(u8, w.buffered(), "lockHolderMain") != null); - st.stop.store(true, .release); - th.join(); -} - -const TrapState = struct { - tid: std.atomic.Value(u32) = .init(0), - counter: u32 = 0, -}; - -noinline fn trapThreadMain(st: *TrapState) void { - st.tid.store(selfTid(), .release); - @breakpoint(); - _ = @atomicRmw(u32, &st.counter, .Add, 1, .acq_rel); -} - -test "breakpoint: pause, inspect, resume" { - if (arch != .x86_64 and !arch.isAARCH64()) return error.SkipZigTest; - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - try d.init(testOptions(&text_buf)); - defer d.deinit(); - try d.enableBreakpoints(); - - var st: TrapState = .{}; - const th = try std.Thread.spawn(.{}, trapThreadMain, .{&st}); - var tries: usize = 0; - while (st.tid.load(.acquire) == 0 or !d.isPaused(st.tid.load(.acquire))) : (tries += 1) { - try testing.expect(tries < 5000); - sleepMs(1); - } - const tid = st.tid.load(.acquire); - try testing.expectEqual(@as(?u32, tid), d.pausedAt(0)); - try testing.expectEqual(@as(?u32, null), d.pausedAt(1)); - try testing.expectEqual(@as(u32, 0), @atomicLoad(u32, &st.counter, .acquire)); - - var out: Writer.Allocating = .init(testing.allocator); - defer out.deinit(); - try d.pausedStack(tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "trapThreadMain") != null); - out.clearRetainingCapacity(); - try d.pausedRegs(tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "pc 0x") != null); - - // A paused thread can also be captured through the signal path. - out.clearRetainingCapacity(); - try d.threadStack(tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "#0 0x") != null); - - sleepMs(5); - try testing.expectEqual(@as(u32, 0), @atomicLoad(u32, &st.counter, .acquire)); - try d.resumeThread(tid); - th.join(); - try testing.expectEqual(@as(u32, 1), @atomicLoad(u32, &st.counter, .acquire)); - try testing.expect(!d.isPaused(tid)); - try testing.expectEqual(@as(?u32, null), d.pausedAt(0)); - try testing.expectError(error.NotPaused, d.resumeThread(tid)); - try testing.expectError(error.NotPaused, d.pausedStack(tid, &out.writer)); - d.disableBreakpoints(); -} - -/// Stands in for `FullPanic`'s call: the first trace address is the return -/// address into the panicking function. -noinline fn panicLike(msg: []const u8) bool { - return recordPanic(msg, @returnAddress()); -} - -test "panic record path" { - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - try d.init(testOptions(&text_buf)); - defer d.deinit(); - defer resetPanicRecord(); - - var out: Writer.Allocating = .init(testing.allocator); - defer out.deinit(); - try d.panicMessage(&out.writer); - try testing.expectEqualStrings("", out.written()); - try testing.expect(!d.panicHeld()); - try testing.expectError(error.NoPanic, d.panicContinue()); - - try testing.expect(panicLike("something broke")); - try testing.expect(!recordPanic("nested", null)); - try testing.expectEqual(selfTid(), panic_tid); - - try d.panicMessage(&out.writer); - try testing.expectEqualStrings("something broke", out.written()); - out.clearRetainingCapacity(); - try d.panicStack(&out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "#0 0x") != null); - try testing.expect(std.mem.indexOf(u8, out.written(), "test.panic record path") != null); - try testing.expect(!d.panicHeld()); - try testing.expectError(error.NoPanic, d.panicContinue()); - - // A long message is truncated, not overflowed. - resetPanicRecord(); - const long = [_]u8{'x'} ** (panic_msg_cap + 100); - try testing.expect(recordPanic(&long, null)); - out.clearRetainingCapacity(); - try d.panicMessage(&out.writer); - try testing.expectEqual(@as(usize, panic_msg_cap), out.written().len); -} - -const MaskState = struct { - tid: std.atomic.Value(u32) = .init(0), - unblock: std.atomic.Value(bool) = .init(false), - stop: std.atomic.Value(bool) = .init(false), - signal: linux.SIG, -}; - -fn maskedThreadMain(st: *MaskState) void { - var set = linux.sigemptyset(); - linux.sigaddset(&set, st.signal); - _ = linux.sigprocmask(linux.SIG.BLOCK, &set, null); - st.tid.store(selfTid(), .release); - while (!st.unblock.load(.acquire)) sleepMs(1); - _ = linux.sigprocmask(linux.SIG.UNBLOCK, &set, null); - while (!st.stop.load(.acquire)) sleepMs(1); -} - -test "capture timeout on a thread with the signal masked" { - var text_buf: [16 * 1024]u8 = undefined; - var d: Debug = undefined; - var opts = testOptions(&text_buf); - opts.capture_timeout_ns = 50 * std.time.ns_per_ms; - try d.init(opts); - defer d.deinit(); - - var st: MaskState = .{ .signal = d.capture_signal }; - const th = try std.Thread.spawn(.{}, maskedThreadMain, .{&st}); - var tries: usize = 0; - while (st.tid.load(.acquire) == 0) : (tries += 1) { - try testing.expect(tries < 2000); - sleepMs(1); - } - const masked_tid = st.tid.load(.acquire); - - var out: Writer.Allocating = .init(testing.allocator); - defer out.deinit(); - const t0 = monotonicNs(); - try testing.expectError(error.Timeout, d.threadStack(masked_tid, &out.writer)); - try testing.expect(monotonicNs() - t0 >= 50 * std.time.ns_per_ms); - try testing.expectEqual(cap_idle, d.capture.state.load(.acquire)); - - // The process is healthy: another thread can still be captured... - var spin: SpinState = .{}; - const spinner = try std.Thread.spawn(.{}, spinThreadMain, .{&spin}); - const spin_tid = waitForTid(&spin); - try testing.expect(spin_tid != 0); - out.clearRetainingCapacity(); - try d.threadStack(spin_tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "spinHere") != null); - - // ...and the late delivery of the pending signal is harmless. - st.unblock.store(true, .release); - sleepMs(20); - out.clearRetainingCapacity(); - try d.threadStack(spin_tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "spinHere") != null); - out.clearRetainingCapacity(); - try d.threadStack(masked_tid, &out.writer); - try testing.expect(std.mem.indexOf(u8, out.written(), "maskedThreadMain") != null); - - @atomicStore(bool, &spin.stop, true, .release); - spinner.join(); - st.stop.store(true, .release); - th.join(); -} - -test "options validation and single instance" { - var text_buf: [4096]u8 = undefined; - var d: Debug = undefined; - var opts = testOptions(&text_buf); - opts.capture_signal = 5; - try testing.expectError(error.InvalidOptions, d.init(opts)); - opts = testOptions(&text_buf); - opts.max_paused = max_paused_cap + 1; - try testing.expectError(error.InvalidOptions, d.init(opts)); - try d.init(testOptions(&text_buf)); - defer d.deinit(); - var d2: Debug = undefined; - try testing.expectError(error.AlreadyInitialized, d2.init(testOptions(&text_buf))); -} diff --git a/introspect/src/linux/probe.zig b/introspect/src/linux/probe.zig deleted file mode 100644 index f38c491..0000000 --- a/introspect/src/linux/probe.zig +++ /dev/null @@ -1,829 +0,0 @@ -//! The Linux platform layer: one background thread runs a `poll()` loop over -//! a listener and every client connection, feeding each connection's core -//! `Conn` with `push`/`step`/`output`/`wrote`. No per-connection threads, no -//! allocation after `init`; every buffer lives in a caller-placed `Storage`. -//! -//! Also home of the debug facilities (`debug`, `provider`) and the /runtime -//! generators (`runtime`). See docs/LIBRARY.md. -//! -//! Client admission: a new connection takes a free slot. When every slot is -//! taken, the connection that has held a slot without any fid (never -//! attached, or fully clunked) for longer than `evict_idle_ms` is dropped in -//! its favour; if there is none, the new connection is closed ("refused"). -//! Nothing that holds a fid is ever evicted. -//! -//! `sleepServing(ms)` lets a request handler (a ctl command, say) wait -//! without stalling the other clients: called on the probe thread from inside -//! a request it keeps running the poll loop for every client whose request -//! is not in progress until the time is up. Requests served from inside such -//! a wait may wait themselves, up to `max_nested_sleeps` deep (each level is a -//! different client, so the depth is bounded by the client table anyway); the -//! level past that, and any call off the probe thread, is a plain sleep. -const std = @import("std"); -const builtin = @import("builtin"); -const linux = std.os.linux; -const cloud9 = @import("cloud9"); -const core = @import("../core.zig"); - -pub const debug = @import("debug.zig"); -pub const provider = @import("provider.zig"); -pub const runtime = @import("runtime.zig"); -pub const DebugProvider = provider.DebugProvider; - -pub const Listen = union(enum) { - /// A unix socket path (< 108 bytes); a stale socket file is unlinked first. - unix: []const u8, - /// An IPv4 literal "a.b.c.d:port". - tcp: []const u8, - /// An already listening socket, owned by the caller. - fd: i32, - /// One pre-connected client on these descriptors (stdio: 0 and 1). Nothing - /// is accepted and the loop ends when the client hangs up. - client: struct { in: i32, out: i32 }, -}; - -pub const Options = struct { - /// For `std.debug` symbolization. - io: std.Io, - listen: Listen, - /// Largest msize offered to clients (clamped to the server's `cfg.msize`). - msize: u32 = 64 * 1024, - /// Hold a panicking thread until /panic/ctl says "continue". - hold_on_panic: bool = true, - /// Real-time signal used to snapshot other threads. - capture_signal: u8 = debug.default_capture_signal, - /// Install the SIGTRAP handler so `@breakpoint()` parks the thread. - breakpoints: bool = true, - /// Mount /threads, /addr, /mem, /hex, /breakpoints, /panic (six provider slots). - mount_debug: bool = true, -}; - -pub const Error = error{ - /// Another `Debug` (another probe) exists in this process. - AlreadyInitialized, - /// No register capture on this architecture. - Unsupported, - /// `Shared` has fewer than six free provider slots. - TooManyProviders, - PathTooLong, - BadAddress, - /// A syscall failed; `last_errno` says which error. - Syscall, -}; - -/// Idle time without fids after which a slot holder may be evicted. -pub const evict_idle_ms: i64 = 500; -/// Largest number of connections accepted per poll wakeup. -const accept_burst = 64; -/// How deep `sleepServing` may nest (each level keeps a poll round on the stack). -pub const max_nested_sleeps = 8; - -/// Static per-client storage: `max_clients` core `Storage`s and `Conn`s, the -/// poll table, the debug text arena and the debug provider's snapshot pool. -pub fn Storage(comptime max_clients: u8, comptime Srv: type) type { - return Probe(Srv).Storage(max_clients); -} - -pub fn Probe(comptime Srv: type) type { - return struct { - const Self = @This(); - - pub fn Storage(comptime max_clients: u8) type { - comptime std.debug.assert(max_clients > 0); - return struct { - pub const capacity = max_clients; - conns: [max_clients]Srv.Storage, - clients: [max_clients]Client, - /// [0] wake eventfd, [1] listener, [2..] one per client slot. - pollfds: [max_clients + 2]linux.pollfd, - text_buf: [16 * 1024]u8, - dp: DebugProvider, - }; - } - - pub const Client = struct { - conn: Srv.Conn, - in: i32 = -1, - out: i32 = -1, - used: bool = false, - /// Descriptors we opened (accepted) are closed on drop; borrowed ones are not. - owned: bool = false, - /// Send with MSG_NOSIGNAL; falls back to write(2) on ENOTSOCK. - is_socket: bool = true, - /// Monotonic ms of the last byte received. - last_active: i64 = 0, - }; - - shared: *Srv.Shared, - clients: []Client, - conns: []Srv.Storage, - pollfds: []linux.pollfd, - dbg: debug.Debug, - dp: *DebugProvider, - msize: u32, - listen_fd: i32 = -1, - own_listener: bool = false, - is_tcp: bool = false, - single: bool = false, - wake_fd: i32 = -1, - unix_path: [108]u8 = undefined, - unix_len: usize = 0, - thread: ?std.Thread = null, - thread_tid: std.atomic.Value(u32) = .init(0), - nclients: std.atomic.Value(u32) = .init(0), - /// Connections closed because no slot was free. - refused: u64 = 0, - stopping: std.atomic.Value(bool) = .init(false), - /// Slots whose request is being handled (excluded from nested servicing and eviction). - serving: std.StaticBitSet(256) = .initEmpty(), - /// Current `sleepServing` nesting depth. - nested: u8 = 0, - debug_ready: bool = false, - last_errno: linux.E = .SUCCESS, - - // -- lifecycle ------------------------------------------------------- - - /// Installs the debug facilities, mounts the debug providers into - /// `shared` and opens the listener. `storage` is a `*Storage(n)`. On - /// failure `shared` may already hold the debug providers and must be - /// discarded. - pub fn init(p: *Self, shared: *Srv.Shared, storage: anytype, opts: Options) Error!void { - p.* = .{ - .shared = shared, - .clients = &storage.clients, - .conns = &storage.conns, - .pollfds = &storage.pollfds, - .dbg = undefined, - .dp = &storage.dp, - .msize = opts.msize, - }; - for (p.clients) |*c| c.used = false; - shared.hash_seed = randomSeed(); - debug.hold_on_panic = opts.hold_on_panic; - p.dbg.init(.{ .io = opts.io, .text_buf = &storage.text_buf, .capture_signal = opts.capture_signal }) catch |e| return switch (e) { - error.AlreadyInitialized => error.AlreadyInitialized, - error.Unsupported => error.Unsupported, - else => error.Syscall, - }; - p.debug_ready = true; - errdefer { - p.dbg.deinit(); - p.debug_ready = false; - } - if (opts.breakpoints) p.dbg.enableBreakpoints() catch |e| switch (e) { - // No breakpoint support on this architecture: everything else still works. - error.Unsupported => {}, - else => return error.Syscall, - }; - if (opts.mount_debug) { - p.dp.init(&p.dbg); - p.dp.mountAll(shared) catch return error.TooManyProviders; - } - const efd = linux.eventfd(0, linux.EFD.CLOEXEC | linux.EFD.NONBLOCK); - try p.check(efd); - p.wake_fd = @intCast(efd); - errdefer { - _ = linux.close(p.wake_fd); - p.wake_fd = -1; - } - switch (opts.listen) { - .unix => |path| try p.listenUnix(path), - .tcp => |text| try p.listenTcp(text), - .fd => |fd| { - try p.setNonblock(fd); - p.listen_fd = fd; - }, - .client => |c| { - p.single = true; - try p.setNonblock(c.in); - if (c.out != c.in) try p.setNonblock(c.out); - _ = p.addClient(c.in, c.out, false); - }, - } - } - - /// Spawns the poll thread. - pub fn start(p: *Self) std.Thread.SpawnError!void { - std.debug.assert(p.thread == null); - p.stopping.store(false, .release); - p.thread = try std.Thread.spawn(.{}, run, .{p}); - } - - /// Waits for the poll thread to end (only happens by itself in - /// `.client` mode, when the client hangs up). - pub fn wait(p: *Self) void { - if (p.thread) |t| { - t.join(); - p.thread = null; - } - } - - /// Stops the poll thread, drops every client, closes what `init` - /// opened and restores the signal dispositions. - pub fn stop(p: *Self) void { - // Joining the poll thread from itself would hang forever; a - // request handler that wants the server gone uses `requestStop`. - std.debug.assert(p.thread_tid.load(.acquire) != @as(u32, @intCast(linux.gettid()))); - p.stopping.store(true, .release); - p.wakeLoop(); - p.wait(); - for (p.clients, 0..) |*c, i| if (c.used) p.dropClient(i); - if (p.listen_fd >= 0) { - if (p.own_listener) _ = linux.close(p.listen_fd); - p.listen_fd = -1; - } - if (p.unix_len > 0) { - _ = linux.unlink(@ptrCast(&p.unix_path)); - p.unix_len = 0; - } - if (p.wake_fd >= 0) { - _ = linux.close(p.wake_fd); - p.wake_fd = -1; - } - if (p.debug_ready) { - p.dbg.deinit(); - p.debug_ready = false; - } - } - - /// Live client count (for /runtime/clients). - pub fn clientCount(p: *const Self) u32 { - return p.nclients.load(.acquire); - } - - pub fn clientCounter(p: *const Self) *const std.atomic.Value(u32) { - return &p.nclients; - } - - /// Waits `ms` while keeping the other clients served (see the file comment). - pub fn sleepServing(p: *Self, ms: u64) void { - const on_thread = p.thread_tid.load(.acquire) == @as(u32, @intCast(linux.gettid())); - if (!on_thread or p.serving.count() == 0 or p.nested >= max_nested_sleeps) return sleepMs(ms); - p.nested += 1; - defer p.nested -= 1; - const deadline = monotonicMs() + @as(i64, @intCast(@min(ms, std.math.maxInt(i32)))); - while (!p.stopping.load(.acquire)) { - const now = monotonicMs(); - if (now >= deadline) break; - p.pollOnce(@intCast(deadline - now)); - } - } - - /// Asks the poll thread to stop; safe to call from a signal handler - /// (an atomic store and one write to the wake eventfd). `stop` (or - /// `wait`) still has to run afterwards to release everything. - pub fn requestStop(p: *Self) void { - p.stopping.store(true, .release); - p.wakeLoop(); - } - - // -- the loop -------------------------------------------------------- - - fn run(p: *Self) void { - const tid: u32 = @intCast(linux.gettid()); - p.thread_tid.store(tid, .release); - // A breakpoint or panic on this thread must never park it (see debug.zig). - debug.server_tid.store(tid, .release); - setThreadName("introspect"); - while (!p.stopping.load(.acquire)) { - if (p.single and p.clientCount() == 0) break; - p.pollOnce(-1); - } - debug.server_tid.store(0, .release); - p.thread_tid.store(0, .release); - } - - fn wakeLoop(p: *Self) void { - if (p.wake_fd < 0) return; - const one: u64 = 1; - _ = linux.write(p.wake_fd, @ptrCast(&one), 8); - } - - /// One `poll()` round: accept, read, step, write. Slots whose request - /// is in progress (`serving`, only inside `sleepServing`) are left untouched. - fn pollOnce(p: *Self, timeout_ms: i32) void { - p.pollfds[0] = .{ .fd = p.wake_fd, .events = linux.POLL.IN, .revents = 0 }; - p.pollfds[1] = .{ .fd = p.listen_fd, .events = linux.POLL.IN, .revents = 0 }; - for (p.clients, 0..) |*c, i| { - var fd: i32 = -1; - var events: i16 = 0; - if (c.used and !p.serving.isSet(i)) { - fd = c.in; - if (c.conn.output().len > 0) { - fd = c.out; - events = linux.POLL.OUT; - } else if (inputRoom(&c.conn) > 0) { - events = linux.POLL.IN; - } - } - p.pollfds[2 + i] = .{ .fd = fd, .events = events, .revents = 0 }; - } - const rc = linux.poll(p.pollfds.ptr, p.pollfds.len, timeout_ms); - switch (linux.errno(rc)) { - .SUCCESS => {}, - .INTR => return, - else => { - sleepMs(10); - return; - }, - } - if (p.pollfds[0].revents != 0) { - var v: u64 = 0; - _ = linux.read(p.wake_fd, @ptrCast(&v), 8); - } - if (p.stopping.load(.acquire)) return; - if (p.pollfds[1].revents != 0) p.acceptSome(); - for (p.clients, 0..) |*c, i| { - const re = p.pollfds[2 + i].revents; - if (re == 0 or !c.used or p.serving.isSet(i)) continue; - if (re & (linux.POLL.IN | linux.POLL.HUP | linux.POLL.ERR | linux.POLL.NVAL) != 0) { - p.readClient(i, re & linux.POLL.HUP != 0); - } else if (re & linux.POLL.OUT != 0) { - p.service(i); - } - if (p.stopping.load(.acquire)) return; - } - } - - fn acceptSome(p: *Self) void { - var n: usize = 0; - while (n < accept_burst) : (n += 1) { - const rc = linux.accept4(p.listen_fd, null, null, linux.SOCK.NONBLOCK | linux.SOCK.CLOEXEC); - switch (linux.errno(rc)) { - .SUCCESS => {}, - .AGAIN => return, - .INTR, .CONNABORTED => continue, - // Descriptor/memory exhaustion is transient (clients hang up); - // back off instead of spinning on a readable listener. - .MFILE, .NFILE, .NOBUFS, .NOMEM, .PERM => { - sleepMs(100); - return; - }, - else => return, - } - const cfd: i32 = @intCast(rc); - if (p.is_tcp) { - const one: u32 = 1; - _ = linux.setsockopt(cfd, linux.IPPROTO.TCP, linux.TCP.NODELAY, @ptrCast(&one), @sizeOf(u32)); - } - if (p.addClient(cfd, cfd, true) != null) continue; - if (p.evictable()) |victim| { - p.dropClient(victim); - _ = p.addClient(cfd, cfd, true); - continue; - } - p.refused += 1; - // A flood must not flood stderr. - if (p.refused == 1 or p.refused % 1000 == 0) - std.debug.print("introspect: refused connection ({d} clients open, {d} refused so far)\n", .{ p.clients.len, p.refused }); - _ = linux.close(cfd); - } - } - - /// The longest-idle slot holder without fids, if idle long enough. - fn evictable(p: *Self) ?usize { - const now = monotonicMs(); - var best: ?usize = null; - for (p.clients, 0..) |*c, i| { - if (!c.used or !c.owned) continue; - if (p.serving.isSet(i)) continue; - if (c.conn.fidCount() != 0) continue; - if (now - c.last_active < evict_idle_ms) continue; - if (best == null or c.last_active < p.clients[best.?].last_active) best = i; - } - return best; - } - - fn addClient(p: *Self, in: i32, out: i32, owned: bool) ?usize { - for (p.clients, 0..) |*c, i| { - if (c.used) continue; - c.conn = Srv.Conn.init(p.shared, &p.conns[i], p.msize); - c.in = in; - c.out = out; - c.used = true; - c.owned = owned; - c.is_socket = true; - c.last_active = monotonicMs(); - _ = p.nclients.fetchAdd(1, .acq_rel); - return i; - } - return null; - } - - fn dropClient(p: *Self, i: usize) void { - const c = &p.clients[i]; - if (!c.used) return; - c.conn.hangup(); - if (c.owned) { - _ = linux.close(c.in); - if (c.out != c.in) _ = linux.close(c.out); - } - c.used = false; - c.in = -1; - c.out = -1; - _ = p.nclients.fetchSub(1, .acq_rel); - } - - /// Free space in the connection's input buffer (cloud9 keeps one - /// msize-sized frame; `push` copies at most this much). - fn inputRoom(conn: *const Srv.Conn) usize { - return conn.server.in.len - conn.server.in_len; - } - - fn readClient(p: *Self, i: usize, hup: bool) void { - const c = &p.clients[i]; - var buf: [64 * 1024]u8 = undefined; - const room = inputRoom(&c.conn); - if (room == 0) return p.service(i); - const want = @min(room, buf.len); - while (true) { - const rc = linux.read(c.in, &buf, want); - switch (linux.errno(rc)) { - .SUCCESS => { - if (rc == 0) return p.dropClient(i); - const taken = c.conn.push(buf[0..rc]); - std.debug.assert(taken == rc); - c.last_active = monotonicMs(); - return p.service(i); - }, - .INTR => continue, - .AGAIN => { - if (hup) p.dropClient(i); - return; - }, - else => return p.dropClient(i), - } - } - } - - /// Runs requests and drains output until nothing moves. - fn service(p: *Self, i: usize) void { - const c = &p.clients[i]; - std.debug.assert(!p.serving.isSet(i)); - p.serving.set(i); - defer p.serving.unset(i); - while (c.used) { - var moved = false; - while (true) { - const more = c.conn.step() catch return p.dropClient(i); - if (!more) break; - moved = true; - } - const before = c.conn.output().len; - p.flush(c) catch return p.dropClient(i); - if (c.conn.output().len != before) moved = true; - if (!moved) return; - } - } - - fn flush(p: *Self, c: *Client) error{Closed}!void { - _ = p; - while (c.conn.output().len > 0) { - const chunk = c.conn.output(); - const rc = if (c.is_socket) - linux.sendto(c.out, chunk.ptr, chunk.len, linux.MSG.NOSIGNAL, null, 0) - else - linux.write(c.out, chunk.ptr, chunk.len); - switch (linux.errno(rc)) { - .SUCCESS => { - if (rc == 0) return; - c.conn.wrote(rc); - }, - .INTR => continue, - .AGAIN => return, - .NOTSOCK => c.is_socket = false, - else => return error.Closed, - } - } - } - - // -- listeners ------------------------------------------------------- - - fn check(p: *Self, rc: usize) Error!void { - const e = linux.errno(rc); - if (e != .SUCCESS) { - p.last_errno = e; - return error.Syscall; - } - } - - fn setNonblock(p: *Self, fd: i32) Error!void { - const rc = linux.fcntl(fd, linux.F.GETFL, 0); - try p.check(rc); - const nonblock: u32 = @bitCast(linux.O{ .NONBLOCK = true }); - try p.check(linux.fcntl(fd, linux.F.SETFL, rc | nonblock)); - } - - fn listenUnix(p: *Self, path: []const u8) Error!void { - var sa: linux.sockaddr.un = .{ .path = @splat(0) }; - if (path.len == 0 or path.len >= sa.path.len) return error.PathTooLong; - @memcpy(sa.path[0..path.len], path); - const rc = linux.socket(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC | linux.SOCK.NONBLOCK, 0); - try p.check(rc); - const lfd: i32 = @intCast(rc); - errdefer _ = linux.close(lfd); - // No libc, so no "is it still listening" probe: unlink a stale socket and bind. - _ = linux.unlink(@ptrCast(&sa.path)); - try p.check(linux.bind(lfd, @ptrCast(&sa), @sizeOf(linux.sockaddr.un))); - try p.check(linux.listen(lfd, 128)); - p.listen_fd = lfd; - p.own_listener = true; - p.unix_path = sa.path; - p.unix_len = path.len; - } - - fn listenTcp(p: *Self, text: []const u8) Error!void { - const sa = parseIpv4(text) orelse return error.BadAddress; - const rc = linux.socket(linux.AF.INET, linux.SOCK.STREAM | linux.SOCK.CLOEXEC | linux.SOCK.NONBLOCK, 0); - try p.check(rc); - const lfd: i32 = @intCast(rc); - errdefer _ = linux.close(lfd); - const one: u32 = 1; - _ = linux.setsockopt(lfd, linux.SOL.SOCKET, linux.SO.REUSEADDR, @ptrCast(&one), @sizeOf(u32)); - try p.check(linux.bind(lfd, @ptrCast(&sa), @sizeOf(linux.sockaddr.in))); - try p.check(linux.listen(lfd, 128)); - p.listen_fd = lfd; - p.own_listener = true; - p.is_tcp = true; - } - }; -} - -/// Entropy for the core's fid hash (so fid numbers cannot be chosen to -/// collide); falls back to the clock if getrandom fails. -fn randomSeed() u32 { - var b: [4]u8 = undefined; - if (linux.errno(linux.getrandom(&b, b.len, 0)) == .SUCCESS) return std.mem.readInt(u32, &b, .little); - var ts: linux.timespec = undefined; - _ = linux.clock_gettime(.MONOTONIC, &ts); - return @truncate(@as(u64, @bitCast(ts.nsec)) ^ (@as(u64, @bitCast(ts.sec)) << 20)); -} - -/// "a.b.c.d:port" as a socket address, or null. -pub fn parseIpv4(text: []const u8) ?linux.sockaddr.in { - const colon = std.mem.lastIndexOfScalar(u8, text, ':') orelse return null; - const port = std.fmt.parseInt(u16, text[colon + 1 ..], 10) catch return null; - var octets: [4]u8 = undefined; - var it = std.mem.splitScalar(u8, text[0..colon], '.'); - for (&octets) |*o| o.* = std.fmt.parseInt(u8, it.next() orelse return null, 10) catch return null; - if (it.next() != null) return null; - return .{ .port = std.mem.nativeToBig(u16, port), .addr = @bitCast(octets) }; -} - -/// Names the calling thread (comm, at most 15 bytes) via prctl. -pub fn setThreadName(name: []const u8) void { - var buf: [16]u8 = @splat(0); - const n = @min(name.len, 15); - @memcpy(buf[0..n], name[0..n]); - _ = linux.prctl(@intFromEnum(linux.PR.SET_NAME), @intFromPtr(&buf), 0, 0, 0); -} - -pub fn sleepMs(ms: u64) void { - var req: linux.timespec = .{ .sec = @intCast(ms / 1000), .nsec = @intCast((ms % 1000) * 1_000_000) }; - var rem: linux.timespec = undefined; - while (linux.errno(linux.nanosleep(&req, &rem)) == .INTR) req = rem; -} - -pub fn monotonicMs() i64 { - var ts: linux.timespec = undefined; - _ = linux.clock_gettime(.MONOTONIC, &ts); - return ts.sec * 1000 + @divTrunc(ts.nsec, 1_000_000); -} - -// --------------------------------------------------------------------------- -// Tests: a real unix socket, a cloud9.Client on the other end. -// --------------------------------------------------------------------------- - -const testing = std.testing; - -test { - _ = debug; - _ = provider; - _ = runtime; -} - -const TestBuild = struct { - pub const zig_version: []const u8 = builtin.zig_version_string; - pub const target: []const u8 = "test"; - pub const optimize: []const u8 = "Debug"; - pub const time: []const u8 = "2024-01-01T00:00:00Z"; - pub const change: []const u8 = "none"; -}; - -const test_cfg: core.Config = .{ - .name = "probetest", - .build = TestBuild, - .msize = 8192, - .max_fids = 16, - .max_providers = 6, - .snapshot_slots = 2, - .snapshot_bytes = 1024, -}; -const TS = core.Server(test_cfg); -const TP = Probe(TS); - -/// A blocking client over a connected socket. -const SockClient = struct { - fd: i32, - client: cloud9.Client, - cin: [8192]u8 = undefined, - cout: [8192]u8 = undefined, - - fn connect(sc: *SockClient, path: []const u8) !void { - var sa: linux.sockaddr.un = .{ .path = @splat(0) }; - @memcpy(sa.path[0..path.len], path); - const rc = linux.socket(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0); - if (linux.errno(rc) != .SUCCESS) return error.Socket; - sc.fd = @intCast(rc); - if (linux.errno(linux.connect(sc.fd, &sa, @sizeOf(linux.sockaddr.un))) != .SUCCESS) return error.Connect; - sc.client = .init(.{ .in = &sc.cin, .out = &sc.cout }); - } - - fn close(sc: *SockClient) void { - _ = linux.close(sc.fd); - } - - /// One round trip; null when the server closed the connection. - fn rpc(sc: *SockClient, req: cloud9.Client.Request) !?cloud9.Client.Result { - _ = try sc.client.submit(req); - while (sc.client.output().len > 0) { - const out = sc.client.output(); - const rc = linux.write(sc.fd, out.ptr, out.len); - switch (linux.errno(rc)) { - .SUCCESS => sc.client.wrote(rc), - .PIPE, .CONNRESET => return null, - else => return error.Write, - } - } - var buf: [8192]u8 = undefined; - while (true) { - if (sc.client.take()) |done| return done.result; - const rc = linux.read(sc.fd, &buf, buf.len); - switch (linux.errno(rc)) { - .SUCCESS => {}, - .CONNRESET => return null, - else => return error.Read, - } - if (rc == 0) return null; - var rest: []const u8 = buf[0..rc]; - while (rest.len > 0) rest = rest[sc.client.push(rest)..]; - } - } - - fn session(sc: *SockClient) !void { - const v = (try sc.rpc(.{ .version = .{ .msize = 8192 } })) orelse return error.Closed; - try testing.expectEqual(@as(u32, 8192), v.version.msize); - const a = (try sc.rpc(.{ .attach = .{ .fid = 0, .uname = "t" } })) orelse return error.Closed; - try testing.expect(a == .attach); - } - - fn readFile(sc: *SockClient, names: []const []const u8, out: []u8) ![]u8 { - const w = (try sc.rpc(.{ .walk = .{ .fid = 0, .newfid = 1, .names = names } })) orelse return error.Closed; - try testing.expectEqual(@as(u16, @intCast(names.len)), w.walk.nwqid); - _ = (try sc.rpc(.{ .open = .{ .fid = 1, .mode = cloud9.oread } })) orelse return error.Closed; - const r = (try sc.rpc(.{ .read = .{ .fid = 1, .offset = 0, .count = @intCast(out.len) } })) orelse return error.Closed; - const n = r.read.len; - @memcpy(out[0..n], r.read); - _ = (try sc.rpc(.{ .clunk = .{ .fid = 1 } })) orelse return error.Closed; - return out[0..n]; - } -}; - -fn testSockPath(buf: []u8, tag: []const u8) ![]const u8 { - return std.fmt.bufPrint(buf, "/tmp/introspect-probe-{d}-{s}.sock", .{ linux.getpid(), tag }); -} - -const TestCtx = struct { info: runtime.Info }; - -test "probe: start, serve a client over a unix socket, stop" { - var ctx: TestCtx = .{ .info = .now() }; - var shared: TS.Shared = .init(&ctx); - const storage = try testing.allocator.create(TP.Storage(2)); - defer testing.allocator.destroy(storage); - var probe: TP = undefined; - var path_buf: [64]u8 = undefined; - const path = try testSockPath(&path_buf, "basic"); - try probe.init(&shared, storage, .{ .io = testing.io, .listen = .{ .unix = path } }); - defer probe.stop(); - try probe.start(); - ctx.info.clients = probe.clientCounter(); - - var sc: SockClient = undefined; - try sc.connect(path); - defer sc.close(); - try sc.session(); - var buf: [1024]u8 = undefined; - const zv = try sc.readFile(&.{ "build", "zig_version" }, &buf); - try testing.expectEqualStrings(builtin.zig_version_string, zv); - try testing.expectEqual(@as(u32, 1), probe.clientCount()); - - // The debug providers are mounted: /threads lists the probe thread by name. - const names = (try sc.rpc(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &.{"threads"} } })) orelse return error.Closed; - try testing.expectEqual(@as(u16, 1), names.walk.nwqid); - _ = (try sc.rpc(.{ .open = .{ .fid = 2, .mode = cloud9.oread } })) orelse return error.Closed; - const dir = (try sc.rpc(.{ .read = .{ .fid = 2, .offset = 0, .count = 4096 } })) orelse return error.Closed; - try testing.expect(dir.read.len > 0); - _ = (try sc.rpc(.{ .clunk = .{ .fid = 2 } })) orelse return error.Closed; - - // A missing file is the Plan 9 error string. - const bad = (try sc.rpc(.{ .walk = .{ .fid = 0, .newfid = 3, .names = &.{"nope"} } })) orelse return error.Closed; - try testing.expect(bad == .fail); - try testing.expectEqualStrings("file does not exist", bad.fail); - - probe.stop(); - // stop() is idempotent and the socket file is gone. - probe.stop(); - var gone: SockClient = undefined; - try testing.expectError(error.Connect, gone.connect(path)); - // The debug facilities can be set up again after stop. - var probe2: TP = undefined; - var shared2: TS.Shared = .init(&ctx); - try probe2.init(&shared2, storage, .{ .io = testing.io, .listen = .{ .unix = path } }); - probe2.stop(); -} - -test "probe: max_clients refusal and idle eviction" { - var ctx: TestCtx = .{ .info = .now() }; - var shared: TS.Shared = .init(&ctx); - const storage = try testing.allocator.create(TP.Storage(2)); - defer testing.allocator.destroy(storage); - var probe: TP = undefined; - var path_buf: [64]u8 = undefined; - const path = try testSockPath(&path_buf, "limit"); - try probe.init(&shared, storage, .{ .io = testing.io, .listen = .{ .unix = path } }); - defer probe.stop(); - try probe.start(); - - // Two attached clients fill the table; a third is accepted then closed. - var a: SockClient = undefined; - try a.connect(path); - defer a.close(); - try a.session(); - var b: SockClient = undefined; - try b.connect(path); - defer b.close(); - try b.session(); - var c: SockClient = undefined; - try c.connect(path); - defer c.close(); - try testing.expectEqual(@as(?cloud9.Client.Result, null), try c.rpc(.{ .version = .{ .msize = 8192 } })); - try testing.expectEqual(@as(u64, 1), probe.refused); - // Attached clients are never evicted, even when idle for long. - sleepMs(evict_idle_ms + 100); - var d: SockClient = undefined; - try d.connect(path); - defer d.close(); - try testing.expectEqual(@as(?cloud9.Client.Result, null), try d.rpc(.{ .version = .{ .msize = 8192 } })); - var buf: [256]u8 = undefined; - _ = try a.readFile(&.{"README"}, &buf); - - // A client without fids that has been idle long enough gives way. - _ = (try b.rpc(.{ .clunk = .{ .fid = 0 } })) orelse return error.Closed; - sleepMs(evict_idle_ms + 100); - var e: SockClient = undefined; - try e.connect(path); - defer e.close(); - try e.session(); - try testing.expectEqual(@as(?cloud9.Client.Result, null), try b.rpc(.{ .version = .{ .msize = 8192 } })); - try testing.expectEqual(@as(u32, 2), probe.clientCount()); -} - -test "probe: single pre-connected client mode ends when the client hangs up" { - var ctx: TestCtx = .{ .info = .now() }; - var shared: TS.Shared = .init(&ctx); - const storage = try testing.allocator.create(TP.Storage(1)); - defer testing.allocator.destroy(storage); - var sv: [2]i32 = undefined; - try testing.expectEqual(linux.E.SUCCESS, linux.errno(linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0, &sv))); - var probe: TP = undefined; - try probe.init(&shared, storage, .{ .io = testing.io, .listen = .{ .client = .{ .in = sv[1], .out = sv[1] } } }); - defer probe.stop(); - try probe.start(); - var sc: SockClient = .{ .fd = sv[0], .client = undefined }; - sc.client = .init(.{ .in = &sc.cin, .out = &sc.cout }); - try sc.session(); - var buf: [256]u8 = undefined; - try testing.expect((try sc.readFile(&.{"README"}, &buf)).len > 0); - try testing.expectEqual(@as(u32, 1), probe.clientCount()); - _ = linux.close(sv[0]); - probe.wait(); - try testing.expectEqual(@as(u32, 0), probe.clientCount()); - _ = linux.close(sv[1]); -} - -test "parseIpv4 and sleepServing off the probe thread" { - const sa = parseIpv4("127.0.0.1:564").?; - try testing.expectEqual(std.mem.nativeToBig(u16, 564), sa.port); - try testing.expectEqual(@as(u32, @bitCast([4]u8{ 127, 0, 0, 1 })), sa.addr); - try testing.expect(parseIpv4("localhost:1") == null); - try testing.expect(parseIpv4("1.2.3:1") == null); - try testing.expect(parseIpv4("1.2.3.4") == null); - try testing.expect(parseIpv4("1.2.3.4:70000") == null); - const t0 = monotonicMs(); - var probe: TP = undefined; - probe.thread_tid = .init(0); - probe.serving = .initEmpty(); - probe.nested = 0; - probe.sleepServing(20); - try testing.expect(monotonicMs() - t0 >= 20); -} diff --git a/introspect/src/linux/provider.zig b/introspect/src/linux/provider.zig deleted file mode 100644 index 9952398..0000000 --- a/introspect/src/linux/provider.zig +++ /dev/null @@ -1,604 +0,0 @@ -//! Adapts `debug.Debug` into core `Provider`s. The core mounts providers at -//! top level only, so one `DebugProvider` registers six of them, all sharing -//! the same `Debug` and the same snapshot pool: -//! -//! /threads//{name,stat,stack,regs} (lists only live tids) -//! /addr/ "fn\nfile:line:col\nmodule\n" -//! /mem/maps, /mem/ /proc/self/maps; raw bytes at address+offset (writable) -//! /hex/ hexdump of 256 bytes at the address -//! /breakpoints//{stack,regs,ctl} ctl accepts "continue" (lists only paused tids) -//! /panic/{message,stack,ctl} ctl accepts "continue" -//! -//! /addr, /mem, /hex and /panic carry a README; /threads and /breakpoints -//! list nothing but tids so that a shell glob over them sees only threads. -//! -//! Handles encode `(kind, tid-or-address)` in 56 bits (the core keeps the -//! low 56 bits of a handle for the qid path): kind in bits 48..55, value in -//! bits 0..47. Handles carry no reference count, so `clunk` is a no-op. -//! -//! The core hands providers a buffer-based `read`, not a writer, so every -//! text file is generated at `open` into one of `snapshot_slots` fixed slots -//! (keyed by handle, reference counted across fids) and served from there; a -//! read at offset 0 regenerates, like the core's own dynamic files. `/mem/` -//! is read and written directly at address+offset and never snapshotted, and -//! `/mem/maps` is read straight from /proc/self/maps at the requested offset -//! (a big process has more mappings than a snapshot slot holds). -const std = @import("std"); -const cloud9 = @import("cloud9"); -const core = @import("../core.zig"); -const debug = @import("debug.zig"); -const Writer = std.Io.Writer; -const Provider = core.Provider; -const Handle = Provider.Handle; -const Error = Provider.Error; -const NodeStat = core.NodeStat; - -pub const snapshot_slots = 8; -pub const snapshot_bytes = 32 * 1024; -/// Bytes shown by /hex/. -pub const hex_bytes = 256; - -pub const Tree = enum(u8) { threads, addr, mem, hex, breakpoints, panic }; -pub const tree_names = [_][]const u8{ "threads", "addr", "mem", "hex", "breakpoints", "panic" }; - -const Kind = enum(u8) { - root = 0, - readme, - thread_dir, - thread_name, - thread_stat, - thread_stack, - thread_regs, - addr_file, - maps, - mem_file, - hex_file, - bp_dir, - bp_stack, - bp_regs, - bp_ctl, - panic_message, - panic_stack, - panic_ctl, - - fn isDir(k: Kind) bool { - return k == .root or k == .thread_dir or k == .bp_dir; - } - - /// Text files generated into a snapshot slot at open. - fn isText(k: Kind) bool { - return switch (k) { - .readme, .thread_name, .thread_stat, .thread_stack, .thread_regs, .addr_file, .hex_file, .bp_stack, .bp_regs, .panic_message, .panic_stack => true, - else => false, - }; - } - - fn isCtl(k: Kind) bool { - return k == .bp_ctl or k == .panic_ctl; - } - - fn mode(k: Kind) u32 { - if (k.isDir()) return cloud9.dmdir | 0o555; - if (k.isCtl()) return 0o222; - if (k == .mem_file) return 0o666; - return 0o444; - } - - fn fixedName(k: Kind) ?[]const u8 { - return switch (k) { - .readme => "README", - .thread_name => "name", - .thread_stat => "stat", - .thread_stack, .bp_stack, .panic_stack => "stack", - .thread_regs, .bp_regs => "regs", - .maps => "maps", - .bp_ctl, .panic_ctl => "ctl", - .panic_message => "message", - else => null, - }; - } -}; - -const value_bits = 48; -const value_mask: u64 = (1 << value_bits) - 1; - -fn mk(kind: Kind, value: u64) Handle { - return (@as(u64, @intFromEnum(kind)) << value_bits) | (value & value_mask); -} - -fn kindOf(h: Handle) Kind { - return @enumFromInt(@as(u8, @truncate(h >> value_bits))); -} - -fn valueOf(h: Handle) u64 { - return h & value_mask; -} - -const readme_threads = - \\One directory per thread of this process, named by tid: - \\ name the thread's comm - \\ stat state and a few fields of /proc/self/task//stat - \\ stack "#n 0x in (::)" per frame - \\ regs general registers captured while the thread was stopped - \\ -; -const readme_addr = - \\Walk any hex address: /addr/ reads as "fn\nfile:line:col\nmodule\n". - \\ -; -const readme_mem = - \\maps /proc/self/maps - \\ raw process memory at that address (+ file offset); writable - \\ -; -const readme_hex = - \\Walk any hex address: /hex/ is a hexdump of the 256 bytes there. - \\ -; -const readme_breakpoints = - \\One directory per thread stopped in @breakpoint(), named by tid: - \\ stack, regs as under /threads - \\ ctl write "continue" to resume the thread - \\ -; -const readme_panic = - \\message the first panic's message (empty before any panic) - \\stack frames of the panicking thread - \\ctl write "continue" to let the default panic handler run - \\ -; - -fn readmeFor(tree: Tree) []const u8 { - return switch (tree) { - .threads => readme_threads, - .addr => readme_addr, - .mem => readme_mem, - .hex => readme_hex, - .breakpoints => readme_breakpoints, - .panic => readme_panic, - }; -} - -const Slot = struct { - handle: Handle = 0, - refs: u32 = 0, - len: u32 = 0, - buf: [snapshot_bytes]u8 = undefined, -}; - -pub const DebugProvider = struct { - d: *debug.Debug, - slots: [snapshot_slots]Slot = @splat(.{}), - /// Backs `NodeStat.name` until the next call. - name_buf: [32]u8 = undefined, - - pub fn init(dp: *DebugProvider, d: *debug.Debug) void { - dp.* = .{ .d = d }; - } - - /// The provider for one tree, to pass to `Shared.addProvider`. - pub fn provider(dp: *DebugProvider, comptime tree: Tree) Provider { - return .{ .name = tree_names[@intFromEnum(tree)], .ctx = dp, .vtable = vtableFor(tree) }; - } - - /// Mounts all six trees; `shared` is a `Server(cfg).Shared`. - pub fn mountAll(dp: *DebugProvider, shared: anytype) error{Full}!void { - inline for (comptime std.meta.tags(Tree)) |tree| try shared.addProvider(dp.provider(tree)); - } - - fn self(ctx: *anyopaque) *DebugProvider { - return @ptrCast(@alignCast(ctx)); - } - - fn vtableFor(comptime tree: Tree) *const Provider.VTable { - return &struct { - const vt: Provider.VTable = .{ - .walk = walkFn, - .stat = statFn, - .list = listFn, - .open = openFn, - .read = readFn, - .write = writeFn, - .close = closeFn, - .clunk = clunkFn, - }; - fn walkFn(ctx: *anyopaque, parent: Handle, name: []const u8) Error!Handle { - return self(ctx).walk(tree, parent, name); - } - fn statFn(ctx: *anyopaque, h: Handle, out: *NodeStat) Error!void { - return self(ctx).stat(tree, h, out); - } - fn listFn(ctx: *anyopaque, dir: Handle, index: usize, out: *NodeStat) Error!bool { - return self(ctx).list(tree, dir, index, out); - } - fn openFn(ctx: *anyopaque, h: Handle, mode: u8) Error!void { - return self(ctx).open(tree, h, mode); - } - fn readFn(ctx: *anyopaque, h: Handle, offset: u64, buf: []u8) Error!usize { - return self(ctx).read(tree, h, offset, buf); - } - fn writeFn(ctx: *anyopaque, h: Handle, offset: u64, data: []const u8) Error!usize { - return self(ctx).write(tree, h, offset, data); - } - fn closeFn(ctx: *anyopaque, h: Handle) void { - self(ctx).close(h); - } - fn clunkFn(_: *anyopaque, _: Handle) void {} - }.vt; - } - - // -- naming ------------------------------------------------------------ - - fn parseTid(name: []const u8) ?u32 { - if (name.len == 0 or name.len > 10) return null; - for (name) |ch| if (!std.ascii.isDigit(ch)) return null; - return std.fmt.parseInt(u32, name, 10) catch null; - } - - fn parseHex(name: []const u8) ?u64 { - const digits = if (std.mem.startsWith(u8, name, "0x")) name[2..] else name; - if (digits.len == 0 or digits.len > 12) return null; - for (digits) |ch| if (!std.ascii.isHex(ch)) return null; - const v = std.fmt.parseInt(u64, digits, 16) catch return null; - if (v > value_mask) return null; - return v; - } - - fn nodeName(dp: *DebugProvider, h: Handle) []const u8 { - const k = kindOf(h); - if (k.fixedName()) |n| return n; - return switch (k) { - .root => "", - .thread_dir, .bp_dir => std.fmt.bufPrint(&dp.name_buf, "{d}", .{valueOf(h)}) catch unreachable, - .addr_file, .mem_file, .hex_file => std.fmt.bufPrint(&dp.name_buf, "{x}", .{valueOf(h)}) catch unreachable, - else => unreachable, - }; - } - - // -- vtable ------------------------------------------------------------ - - fn walk(dp: *DebugProvider, tree: Tree, parent: Handle, name: []const u8) Error!Handle { - const k = kindOf(parent); - if (std.mem.eql(u8, name, ".")) return parent; - if (!k.isDir()) return error.NotDir; - if (std.mem.eql(u8, name, "..")) return Provider.root; - switch (k) { - .root => { - if (tree != .threads and tree != .breakpoints and std.mem.eql(u8, name, "README")) return mk(.readme, 0); - switch (tree) { - .threads => { - const tid = parseTid(name) orelse return error.NotFound; - if (!dp.d.threadExists(tid)) return error.NotFound; - return mk(.thread_dir, tid); - }, - .addr => return mk(.addr_file, parseHex(name) orelse return error.NotFound), - .mem => { - if (std.mem.eql(u8, name, "maps")) return mk(.maps, 0); - return mk(.mem_file, parseHex(name) orelse return error.NotFound); - }, - .hex => return mk(.hex_file, parseHex(name) orelse return error.NotFound), - .breakpoints => { - const tid = parseTid(name) orelse return error.NotFound; - if (!dp.d.isPaused(tid)) return error.NotFound; - return mk(.bp_dir, tid); - }, - .panic => { - if (std.mem.eql(u8, name, "message")) return mk(.panic_message, 0); - if (std.mem.eql(u8, name, "stack")) return mk(.panic_stack, 0); - if (std.mem.eql(u8, name, "ctl")) return mk(.panic_ctl, 0); - return error.NotFound; - }, - } - }, - .thread_dir => { - const tid = valueOf(parent); - if (std.mem.eql(u8, name, "name")) return mk(.thread_name, tid); - if (std.mem.eql(u8, name, "stat")) return mk(.thread_stat, tid); - if (std.mem.eql(u8, name, "stack")) return mk(.thread_stack, tid); - if (std.mem.eql(u8, name, "regs")) return mk(.thread_regs, tid); - return error.NotFound; - }, - .bp_dir => { - const tid = valueOf(parent); - if (std.mem.eql(u8, name, "stack")) return mk(.bp_stack, tid); - if (std.mem.eql(u8, name, "regs")) return mk(.bp_regs, tid); - if (std.mem.eql(u8, name, "ctl")) return mk(.bp_ctl, tid); - return error.NotFound; - }, - else => unreachable, - } - } - - fn fill(dp: *DebugProvider, tree: Tree, h: Handle, out: *NodeStat) void { - const k = kindOf(h); - out.* = .{ - .mode = k.mode(), - .length = if (k == .readme) readmeFor(tree).len else 0, - .name = dp.nodeName(h), - .handle = h, - }; - } - - fn stat(dp: *DebugProvider, tree: Tree, h: Handle, out: *NodeStat) Error!void { - dp.fill(tree, h, out); - } - - fn list(dp: *DebugProvider, tree: Tree, dir: Handle, index: usize, out: *NodeStat) Error!bool { - const k = kindOf(dir); - if (!k.isDir()) return error.NotDir; - const h: Handle = switch (k) { - .root => switch (tree) { - .threads => mk(.thread_dir, dp.d.threadAt(index) orelse return false), - .addr, .hex => if (index == 0) mk(.readme, 0) else return false, - .mem => switch (index) { - 0 => mk(.readme, 0), - 1 => mk(.maps, 0), - else => return false, - }, - .breakpoints => mk(.bp_dir, dp.d.pausedAt(index) orelse return false), - .panic => switch (index) { - 0 => mk(.readme, 0), - 1 => mk(.panic_message, 0), - 2 => mk(.panic_stack, 0), - 3 => mk(.panic_ctl, 0), - else => return false, - }, - }, - .thread_dir => switch (index) { - 0 => mk(.thread_name, valueOf(dir)), - 1 => mk(.thread_stat, valueOf(dir)), - 2 => mk(.thread_stack, valueOf(dir)), - 3 => mk(.thread_regs, valueOf(dir)), - else => return false, - }, - .bp_dir => switch (index) { - 0 => mk(.bp_stack, valueOf(dir)), - 1 => mk(.bp_regs, valueOf(dir)), - 2 => mk(.bp_ctl, valueOf(dir)), - else => return false, - }, - else => unreachable, - }; - dp.fill(tree, h, out); - return true; - } - - fn open(dp: *DebugProvider, tree: Tree, h: Handle, mode: u8) Error!void { - const k = kindOf(h); - const acc = mode & 3; - const wants_write = acc == cloud9.owrite or acc == cloud9.ordwr; - const wants_read = acc != cloud9.owrite; - if (k.isDir()) { - if (wants_write or mode & cloud9.otrunc != 0) return error.IsDir; - return; - } - if (k.isCtl()) { - if (wants_read) return error.Perm; - return; - } - if (k == .mem_file) return; - if (wants_write or mode & cloud9.otrunc != 0) return error.Perm; - if (k == .maps) return; - std.debug.assert(k.isText()); - const slot = dp.takeSlot(h) orelse return error.NoSpace; - errdefer dp.releaseSlot(slot); - try dp.generate(tree, h, slot); - } - - fn close(dp: *DebugProvider, h: Handle) void { - if (!kindOf(h).isText()) return; - if (dp.findSlot(h)) |s| dp.releaseSlot(s); - } - - fn read(dp: *DebugProvider, tree: Tree, h: Handle, offset: u64, buf: []u8) Error!usize { - const k = kindOf(h); - if (k.isDir()) return error.IsDir; - if (k.isCtl()) return error.Perm; - if (k == .mem_file) { - const addr = valueOf(h) +% offset; - return dp.d.readMem(addr, buf) catch |e| mapErr(e); - } - if (k == .maps) return dp.d.readMaps(offset, buf) catch |e| mapErr(e); - const slot = dp.findSlot(h) orelse return error.Io; - if (offset == 0) try dp.generate(tree, h, slot); - if (offset >= slot.len) return 0; - const off: usize = @intCast(offset); - const n = @min(buf.len, slot.len - off); - @memcpy(buf[0..n], slot.buf[off..][0..n]); - return n; - } - - fn write(dp: *DebugProvider, tree: Tree, h: Handle, offset: u64, data: []const u8) Error!usize { - _ = tree; - const k = kindOf(h); - if (k.isDir()) return error.IsDir; - switch (k) { - .mem_file => { - const addr = valueOf(h) +% offset; - return dp.d.writeMem(addr, data) catch |e| mapErr(e); - }, - .bp_ctl, .panic_ctl => { - const cmd = std.mem.trim(u8, data, " \t\r\n\x00"); - if (!std.mem.eql(u8, cmd, "continue")) return error.Unsupported; - if (k == .bp_ctl) { - dp.d.resumeThread(@intCast(valueOf(h))) catch |e| return mapErr(e); - } else { - dp.d.panicContinue() catch |e| return mapErr(e); - } - return data.len; - }, - else => return error.Perm, - } - } - - // -- snapshots --------------------------------------------------------- - - fn findSlot(dp: *DebugProvider, h: Handle) ?*Slot { - for (&dp.slots) |*s| if (s.refs > 0 and s.handle == h) return s; - return null; - } - - fn takeSlot(dp: *DebugProvider, h: Handle) ?*Slot { - if (dp.findSlot(h)) |s| { - s.refs += 1; - return s; - } - for (&dp.slots) |*s| if (s.refs == 0) { - s.* = .{ .handle = h, .refs = 1 }; - return s; - }; - return null; - } - - fn releaseSlot(_: *DebugProvider, s: *Slot) void { - s.refs -= 1; - } - - /// (Re)generates the text of `h` into `slot`. A text that does not fit is - /// truncated, not an error. - fn generate(dp: *DebugProvider, tree: Tree, h: Handle, slot: *Slot) Error!void { - var w: Writer = .fixed(&slot.buf); - slot.len = 0; - dp.render(tree, h, &w) catch |e| switch (e) { - error.WriteFailed => {}, - else => return mapErr(e), - }; - slot.len = @intCast(w.buffered().len); - } - - fn render(dp: *DebugProvider, tree: Tree, h: Handle, w: *Writer) debug.Error!void { - const d = dp.d; - const v = valueOf(h); - switch (kindOf(h)) { - .readme => w.writeAll(readmeFor(tree)) catch return error.WriteFailed, - .thread_name => try d.threadName(@intCast(v), w), - .thread_stat => try d.threadStat(@intCast(v), w), - .thread_stack => try d.threadStack(@intCast(v), w), - .thread_regs => try d.threadRegs(@intCast(v), w), - .addr_file => try d.resolveAddr(@intCast(v), w), - .hex_file => try d.hexdump(@intCast(v), hex_bytes, w), - .bp_stack => try d.pausedStack(@intCast(v), w), - .bp_regs => try d.pausedRegs(@intCast(v), w), - .panic_message => try d.panicMessage(w), - .panic_stack => try d.panicStack(w), - else => unreachable, - } - } - - fn mapErr(e: debug.Error) Error { - return switch (e) { - error.NoThread, error.NotPaused, error.NoPanic => error.NotFound, - error.Unsupported => error.Unsupported, - error.WriteFailed => error.NoSpace, - error.Timeout, error.Busy, error.Unmapped, error.Unexpected, error.AlreadyInitialized, error.InvalidOptions => error.Io, - }; - } -}; - -// --------------------------------------------------------------------------- -// Tests (through the core's in-memory harness) -// --------------------------------------------------------------------------- - -const testing = std.testing; - -const TestCfg: core.Config = .{ .name = "dbgtest", .msize = 8192, .max_fids = 16, .max_providers = 6, .snapshot_slots = 2, .snapshot_bytes = 512 }; -const TS = core.Server(TestCfg); - -test "debug provider: threads, addr, mem, hex, breakpoints, panic through the core" { - var text_buf: [16 * 1024]u8 = undefined; - var d: debug.Debug = undefined; - try d.init(.{ .io = testing.io, .text_buf = &text_buf }); - defer d.deinit(); - var dp: DebugProvider = undefined; - dp.init(&d); - - var dummy: u8 = 0; - var shared: TS.Shared = .init(&dummy); - try dp.mountAll(&shared); - var storage: TS.Storage = undefined; - var h: TS.Harness = undefined; - try h.init(&shared, &storage); - defer h.deinit(); - - // /threads lists tids only, among them this thread. - const names = try h.listPath(&.{"threads"}); - defer TS.Harness.freeNames(names); - try testing.expect(names.len >= 1); - try testing.expect(!TS.Harness.hasName(names, "README")); - const no_readme = try h.ok(.{ .walk = .{ .fid = 0, .newfid = 7, .names = &.{ "threads", "README" } } }); - try testing.expectEqual(@as(u16, 1), no_readme.walk.nwqid); - var tid_buf: [16]u8 = undefined; - const tid = try std.fmt.bufPrint(&tid_buf, "{d}", .{std.os.linux.gettid()}); - try testing.expect(TS.Harness.hasName(names, tid)); - - // Own stack names this test function's file. - const stack = try h.readPath(&.{ "threads", tid, "stack" }); - defer testing.allocator.free(stack); - try testing.expect(std.mem.indexOf(u8, stack, "#0 0x") != null); - - // /addr/ of a function here resolves to this file. - var addr_buf: [32]u8 = undefined; - const addr_name = try std.fmt.bufPrint(&addr_buf, "{x}", .{@intFromPtr(&DebugProvider.parseTid)}); - const resolved = try h.readPath(&.{ "addr", addr_name }); - defer testing.allocator.free(resolved); - try testing.expect(std.mem.indexOf(u8, resolved, "provider.zig") != null); - const partial = try h.ok(.{ .walk = .{ .fid = 0, .newfid = 5, .names = &.{ "addr", "zzz" } } }); - try testing.expectEqual(@as(u16, 1), partial.walk.nwqid); - try h.walkTo(5, &.{"addr"}); - try h.expectFail(.{ .walk = .{ .fid = 5, .newfid = 6, .names = &.{"zzz"} } }, "file does not exist"); - _ = try h.ok(.{ .clunk = .{ .fid = 5 } }); - - // /mem/ reads and writes live memory; unmapped is an error. - var cell: [8]u8 = "abcdefgh".*; - var mem_buf: [32]u8 = undefined; - const mem_name = try std.fmt.bufPrint(&mem_buf, "{x}", .{@intFromPtr(&cell)}); - try h.walkTo(1, &.{ "mem", mem_name }); - _ = try h.ok(.{ .open = .{ .fid = 1, .mode = cloud9.ordwr } }); - const r = try h.ok(.{ .read = .{ .fid = 1, .offset = 2, .count = 4 } }); - try testing.expectEqualStrings("cdef", r.read); - _ = try h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "XY" } }); - try testing.expectEqualStrings("XYcdefgh", &cell); - _ = try h.ok(.{ .clunk = .{ .fid = 1 } }); - try h.walkTo(2, &.{ "mem", "8" }); - _ = try h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - try h.expectFail(.{ .read = .{ .fid = 2, .offset = 0, .count = 4 } }, "i/o error"); - _ = try h.ok(.{ .clunk = .{ .fid = 2 } }); - const maps = try h.readPath(&.{ "mem", "maps" }); - defer testing.allocator.free(maps); - try testing.expect(std.mem.indexOf(u8, maps, "r-xp") != null or std.mem.indexOf(u8, maps, "r--p") != null); - - // /hex/ is a hexdump. - const hex = try h.readPath(&.{ "hex", mem_name }); - defer testing.allocator.free(hex); - try testing.expect(std.mem.indexOf(u8, hex, "XYcdefgh") != null); - - // Nothing paused, no panic. - const bps = try h.listPath(&.{"breakpoints"}); - defer TS.Harness.freeNames(bps); - try testing.expectEqual(@as(usize, 0), bps.len); - const msg = try h.readPath(&.{ "panic", "message" }); - defer testing.allocator.free(msg); - try testing.expectEqualStrings("", msg); - try h.walkTo(3, &.{ "panic", "ctl" }); - _ = try h.ok(.{ .open = .{ .fid = 3, .mode = cloud9.owrite } }); - try h.expectFail(.{ .write = .{ .fid = 3, .offset = 0, .data = "continue" } }, "file does not exist"); - try h.expectFail(.{ .write = .{ .fid = 3, .offset = 0, .data = "bogus" } }, "not supported"); - _ = try h.ok(.{ .clunk = .{ .fid = 3 } }); - - // Snapshot slots are released on clunk: open more files than slots, sequentially. - for (0..4) |_| { - const t = try h.readPath(&.{ "threads", tid, "name" }); - testing.allocator.free(t); - } - for (&dp.slots) |s| try testing.expectEqual(@as(u32, 0), s.refs); -} - -test "handle encoding round-trips" { - const h = mk(.mem_file, 0x7fff_dead_beef); - try testing.expectEqual(Kind.mem_file, kindOf(h)); - try testing.expectEqual(@as(u64, 0x7fff_dead_beef), valueOf(h)); - try testing.expect(h < (1 << 56)); - try testing.expectEqual(@as(?u64, null), DebugProvider.parseHex("1_0")); - try testing.expectEqual(@as(?u64, 0x10), DebugProvider.parseHex("0x10")); - try testing.expectEqual(@as(?u32, null), DebugProvider.parseTid("+5")); -} diff --git a/introspect/src/linux/runtime.zig b/introspect/src/linux/runtime.zig deleted file mode 100644 index 503d6c2..0000000 --- a/introspect/src/linux/runtime.zig +++ /dev/null @@ -1,90 +0,0 @@ -//! Generators for `Config.runtime`: /runtime/{pid,ppid,uptime,argv,cwd,env,clients}. -//! The core passes every generator `Shared.ctx`; `Fns(Ctx, field)` casts it -//! to `*Ctx` and reads the `Info` stored in `@field(ctx, field)`. -const std = @import("std"); -const linux = std.os.linux; -const Writer = std.Io.Writer; - -/// What the generators report. Texts are borrowed for the server's lifetime. -pub const Info = struct { - /// argv, one argument per line. - argv: []const u8 = "", - /// Environment, one KEY=VALUE per line. - env: []const u8 = "", - cwd: []const u8 = "", - /// Monotonic seconds at startup; /runtime/uptime is the difference. - start_mono: i64 = 0, - /// Live client count, published by the probe. - clients: ?*const std.atomic.Value(u32) = null, - - pub fn now() Info { - return .{ .start_mono = monotonicSecs() }; - } -}; - -pub fn monotonicSecs() i64 { - var ts: linux.timespec = undefined; - _ = linux.clock_gettime(.MONOTONIC, &ts); - return ts.sec; -} - -/// Seconds since the epoch, clamped to u32 (for atime/mtime and `fn/now`). -pub fn realtimeSecs() u32 { - var ts: linux.timespec = undefined; - _ = linux.clock_gettime(.REALTIME, &ts); - return @intCast(std.math.clamp(ts.sec, 0, std.math.maxInt(u32))); -} - -/// The `Config.runtime` type: `Ctx` is the type behind `Shared.ctx`, `field` -/// the name of its `Info` field. -pub fn Fns(comptime Ctx: type, comptime field: []const u8) type { - return struct { - fn info(ctx: *anyopaque) *const Info { - const c: *Ctx = @ptrCast(@alignCast(ctx)); - return &@field(c, field); - } - pub fn pid(_: *anyopaque, w: *Writer) anyerror!void { - try w.print("{d}", .{linux.getpid()}); - } - pub fn ppid(_: *anyopaque, w: *Writer) anyerror!void { - try w.print("{d}", .{linux.getppid()}); - } - pub fn uptime(ctx: *anyopaque, w: *Writer) anyerror!void { - try w.print("{d}", .{monotonicSecs() - info(ctx).start_mono}); - } - pub fn argv(ctx: *anyopaque, w: *Writer) anyerror!void { - try w.writeAll(info(ctx).argv); - } - pub fn cwd(ctx: *anyopaque, w: *Writer) anyerror!void { - try w.writeAll(info(ctx).cwd); - } - pub fn env(ctx: *anyopaque, w: *Writer) anyerror!void { - try w.writeAll(info(ctx).env); - } - pub fn clients(ctx: *anyopaque, w: *Writer) anyerror!void { - const n: u32 = if (info(ctx).clients) |c| c.load(.acquire) else 0; - try w.print("{d}", .{n}); - } - }; -} - -test "runtime generators read Info through the context" { - const Ctx = struct { x: u32, info: Info }; - var count: std.atomic.Value(u32) = .init(3); - var ctx: Ctx = .{ .x = 0, .info = .{ .argv = "a\nb\n", .cwd = "/tmp", .env = "K=V\n", .start_mono = monotonicSecs(), .clients = &count } }; - const F = Fns(Ctx, "info"); - var buf: [64]u8 = undefined; - var w: Writer = .fixed(&buf); - try F.clients(&ctx, &w); - try std.testing.expectEqualStrings("3", w.buffered()); - w = .fixed(&buf); - try F.argv(&ctx, &w); - try std.testing.expectEqualStrings("a\nb\n", w.buffered()); - w = .fixed(&buf); - try F.uptime(&ctx, &w); - try std.testing.expect(w.buffered().len >= 1); - w = .fixed(&buf); - try F.pid(&ctx, &w); - try std.testing.expectEqual(linux.getpid(), try std.fmt.parseInt(i32, w.buffered(), 10)); - try std.testing.expectEqual(@as(usize, 7), @typeInfo(F).@"struct".decls.len); -} diff --git a/introspect/src/root.zig b/introspect/src/root.zig deleted file mode 100644 index 1dcc892..0000000 --- a/introspect/src/root.zig +++ /dev/null @@ -1,24 +0,0 @@ -//! introspect: a 9P2000 debug/introspection server as a library. See -//! docs/LIBRARY.md. `core` and `vars` are freestanding; `scratch` takes an -//! allocator; `linux` is the platform layer (only on Linux). -const std = @import("std"); -const builtin = @import("builtin"); - -pub const core = @import("core.zig"); -pub const vars = @import("vars.zig"); -pub const scratch = @import("scratch.zig"); -pub const linux = if (builtin.os.tag == .linux) @import("linux/probe.zig") else struct {}; - -pub const Config = core.Config; -pub const Server = core.Server; -pub const Provider = core.Provider; -pub const NodeStat = core.NodeStat; -pub const Scratch = scratch.Scratch; - -test { - std.testing.refAllDecls(@This()); - _ = core; - _ = vars; - _ = scratch; - if (builtin.os.tag == .linux) _ = linux; -} diff --git a/introspect/src/scratch.zig b/introspect/src/scratch.zig deleted file mode 100644 index 2163a28..0000000 --- a/introspect/src/scratch.zig +++ /dev/null @@ -1,689 +0,0 @@ -//! An in-memory read/write tree as a `Provider`: create, write, truncate, -//! rename, remove, mkdir, DMAPPEND, DMEXCL. The one core-level component that -//! takes an `Allocator` (nodes and file contents live on it); it is optional. -//! -//! Nodes are kept alive by `refs` (fids holding a handle) after removal, so a -//! handle stays valid until the core clunks it. Handles are node addresses; -//! the root is handle 0. Not internally synchronized (like `Shared`). -const std = @import("std"); -const cloud9 = @import("cloud9"); -const core = @import("core.zig"); -const Allocator = std.mem.Allocator; -const Provider = core.Provider; -const Handle = Provider.Handle; -const Error = Provider.Error; -const NodeStat = core.NodeStat; - -/// A node of the tree. -pub const Node = struct { - name: []u8, - path: u64, - version: u32 = 0, - mode: u32, - atime: u32, - mtime: u32, - data: std.ArrayList(u8) = .empty, - children: std.ArrayList(*Node) = .empty, - parent: ?*Node, - /// Handles held by the core. - refs: u32 = 0, - /// Fids currently open on this node (DMEXCL admits at most one). - opens: u32 = 0, - removed: bool = false, - - pub fn isDir(n: *const Node) bool { - return n.mode & cloud9.dmdir != 0; - } - - fn find(n: *const Node, name: []const u8) ?*Node { - for (n.children.items) |ch| if (std.mem.eql(u8, ch.name, name)) return ch; - return null; - } -}; - -/// Seconds since the epoch, for atime/mtime; the default clock reports 0. -pub const Clock = *const fn () u32; - -fn zeroClock() u32 { - return 0; -} - -pub const Scratch = struct { - gpa: Allocator, - root: *Node, - /// Qid paths are a counter, never reused: the root is 0 (the provider - /// root handle), so a removed-and-recreated file gets a fresh identity - /// even when the allocator hands back the same address. - next_path: u64 = 0, - /// Sum of all file lengths, bounded by `budget`. - bytes: usize = 0, - /// Largest total of file contents across all files. - budget: usize, - /// Largest single file; defaults to the budget. - max_file: usize, - /// The time source for atime/mtime (a platform layer sets it). - now: Clock = &zeroClock, - - /// The tree's only allocation policy: every node and every file's - /// contents come from `gpa`, and no file content ever exceeds `budget_bytes` - /// in total. - pub fn init(gpa: Allocator, budget_bytes: usize) Allocator.Error!Scratch { - var s: Scratch = .{ .gpa = gpa, .root = undefined, .budget = budget_bytes, .max_file = budget_bytes }; - s.root = try s.newNode("", cloud9.dmdir | 0o777, null); - return s; - } - - pub fn deinit(s: *Scratch) void { - s.destroyTree(s.root); - s.* = undefined; - } - - /// The provider to mount, at `/`. - pub fn provider(s: *Scratch, name: []const u8) Provider { - return .{ .name = name, .ctx = s, .vtable = &vtable }; - } - - pub const vtable: Provider.VTable = .{ - .walk = &walk, - .stat = &stat, - .list = &list, - .open = &open, - .read = &read, - .write = &write, - .create = &create, - .remove = &remove, - .wstat = &wstat, - .close = &close, - .clunk = &clunk, - }; - - // -- node management -- - - fn destroyTree(s: *Scratch, n: *Node) void { - for (n.children.items) |ch| s.destroyTree(ch); - n.children.clearRetainingCapacity(); - n.removed = true; - if (n.refs == 0 or n == s.root) s.destroyNode(n); - } - - fn destroyNode(s: *Scratch, n: *Node) void { - s.bytes -= n.data.items.len; - s.gpa.free(n.name); - n.data.deinit(s.gpa); - n.children.deinit(s.gpa); - s.gpa.destroy(n); - } - - fn newNode(s: *Scratch, name: []const u8, mode: u32, parent: ?*Node) Allocator.Error!*Node { - const n = try s.gpa.create(Node); - errdefer s.gpa.destroy(n); - const t = s.now(); - n.* = .{ - .name = try s.gpa.dupe(u8, name), - .path = s.next_path, - .mode = mode, - .atime = t, - .mtime = t, - .parent = parent, - }; - errdefer s.gpa.free(n.name); - if (parent) |p| try p.children.append(s.gpa, n); - s.next_path += 1; - return n; - } - - /// Sets a file's length, zero-filling growth and charging the budget. - /// Shrinking releases the memory so a truncated file costs nothing. - fn resizeData(s: *Scratch, n: *Node, new_len: usize) Error!void { - const old = n.data.items.len; - if (new_len > old) { - if (new_len > s.max_file) return error.NoSpace; - if (s.bytes + (new_len - old) > s.budget) return error.NoSpace; - n.data.resize(s.gpa, new_len) catch return error.NoSpace; - @memset(n.data.items[old..new_len], 0); - s.bytes += new_len - old; - } else if (new_len < old) { - n.data.shrinkAndFree(s.gpa, new_len); - s.bytes -= old - new_len; - } - } - - fn touch(s: *Scratch, n: *Node) void { - n.version +%= 1; - n.mtime = s.now(); - } - - fn self(ctx: *anyopaque) *Scratch { - return @ptrCast(@alignCast(ctx)); - } - - fn handle(s: *Scratch, n: *Node) Handle { - return if (n == s.root) Provider.root else @intFromPtr(n); - } - - fn node(s: *Scratch, h: Handle) *Node { - return if (h == Provider.root) s.root else @ptrFromInt(@as(usize, @intCast(h))); - } - - /// A handle the core will clunk exactly once. - fn retain(s: *Scratch, n: *Node) Handle { - if (n != s.root) n.refs += 1; - return s.handle(n); - } - - fn release(s: *Scratch, n: *Node) void { - if (n == s.root) return; - n.refs -= 1; - if (n.refs == 0 and n.removed) s.destroyNode(n); - } - - fn fillStat(n: *const Node, h: Handle, out: *NodeStat) void { - out.* = .{ - .mode = n.mode, - .length = if (n.isDir()) 0 else n.data.items.len, - .atime = n.atime, - .mtime = n.mtime, - .version = n.version, - .name = n.name, - .handle = h, - .path = n.path, - }; - } - - // -- the vtable -- - - fn walk(ctx: *anyopaque, parent: Handle, name: []const u8) Error!Handle { - const s = self(ctx); - const p = s.node(parent); - if (std.mem.eql(u8, name, ".")) return s.retain(p); - if (p.removed) return error.NotFound; - if (!p.isDir()) return error.NotDir; - if (std.mem.eql(u8, name, "..")) return s.retain(p.parent orelse s.root); - return s.retain(p.find(name) orelse return error.NotFound); - } - - fn stat(ctx: *anyopaque, h: Handle, out: *NodeStat) Error!void { - const s = self(ctx); - fillStat(s.node(h), h, out); - } - - fn list(ctx: *anyopaque, dir: Handle, index: usize, out: *NodeStat) Error!bool { - const s = self(ctx); - const d = s.node(dir); - if (!d.isDir()) return error.NotDir; - if (index >= d.children.items.len) return false; - const ch = d.children.items[index]; - fillStat(ch, s.handle(ch), out); - return true; - } - - fn open(ctx: *anyopaque, h: Handle, mode: u8) Error!void { - const s = self(ctx); - const n = s.node(h); - if (n.removed) return error.NotFound; - const acc = mode & 3; - const want_write = acc == cloud9.owrite or acc == cloud9.ordwr; - const want_read = !want_write or acc == cloud9.ordwr; - const trunc = mode & cloud9.otrunc != 0; - if (n.isDir()) { - if (want_write or trunc) return error.IsDir; - if (n.mode & 0o400 == 0) return error.Perm; - } else { - if (want_read and n.mode & 0o400 == 0) return error.Perm; - if ((want_write or trunc) and n.mode & 0o200 == 0) return error.Perm; - if (n.mode & cloud9.dmexcl != 0 and n.opens != 0) return error.Excl; - if (trunc and n.mode & cloud9.dmappend == 0) { - s.resizeData(n, 0) catch unreachable; // shrinking cannot fail - s.touch(n); - } - } - n.opens += 1; - } - - fn close(ctx: *anyopaque, h: Handle) void { - const s = self(ctx); - s.node(h).opens -= 1; - } - - fn read(ctx: *anyopaque, h: Handle, offset: u64, buf: []u8) Error!usize { - const s = self(ctx); - const n = s.node(h); - if (n.isDir()) return error.IsDir; - const src = n.data.items; - if (offset >= src.len) return 0; - const off: usize = @intCast(offset); - const len = @min(buf.len, src.len - off); - @memcpy(buf[0..len], src[off..][0..len]); - return len; - } - - fn write(ctx: *anyopaque, h: Handle, offset: u64, data: []const u8) Error!usize { - const s = self(ctx); - const n = s.node(h); - if (n.isDir()) return error.IsDir; - // A zero-length write changes nothing (and must not extend the file). - if (data.len == 0) return 0; - const off: usize = if (n.mode & cloud9.dmappend != 0) n.data.items.len else @intCast(@min(offset, s.max_file)); - const end = off + data.len; - if (end > s.max_file) return error.NoSpace; - if (end > n.data.items.len) try s.resizeData(n, end); - @memcpy(n.data.items[off..end], data); - s.touch(n); - return data.len; - } - - fn create(ctx: *anyopaque, dir: Handle, name: []const u8, perm: u32, mode: u8) Error!Handle { - const s = self(ctx); - const d = s.node(dir); - if (d.removed) return error.NotFound; - if (!d.isDir()) return error.NotDir; - if (d.mode & 0o200 == 0) return error.Perm; - if (d.find(name) != null) return error.Exists; - const is_dir = perm & cloud9.dmdir != 0; - const inherit: u32 = if (is_dir) d.mode & 0o777 else d.mode & 0o666; - const n = s.newNode(name, perm & (~@as(u32, 0o777) | inherit), d) catch return error.NoSpace; - s.touch(d); - n.opens += 1; - _ = mode; - return s.retain(n); - } - - fn remove(ctx: *anyopaque, h: Handle) Error!void { - const s = self(ctx); - const n = s.node(h); - if (n.removed) return error.NotFound; - const parent = n.parent orelse return error.Perm; - if (parent.mode & 0o200 == 0) return error.Perm; - if (n.isDir() and n.children.items.len != 0) return error.NotEmpty; - const i = std.mem.indexOfScalar(*Node, parent.children.items, n) orelse return error.NotFound; - _ = parent.children.orderedRemove(i); - s.touch(parent); - n.removed = true; - if (n.refs == 0) s.destroyNode(n); - } - - fn wstat(ctx: *anyopaque, h: Handle, st: *const cloud9.Stat) Error!void { - const s = self(ctx); - const n = s.node(h); - if (n.removed) return error.NotFound; - // Validate everything before changing anything. - const rename = st.name.len != 0 and !std.mem.eql(u8, st.name, n.name); - if (rename) { - const parent = n.parent orelse return error.Perm; - if (parent.find(st.name) != null) return error.Exists; - } - const cur_len: u64 = if (n.isDir()) 0 else n.data.items.len; - const set_len = st.length != 0xFFFF_FFFF_FFFF_FFFF and st.length != cur_len; - if (set_len) { - if (n.isDir()) return error.IsDir; - if (st.length > s.max_file) return error.NoSpace; - } - const set_mode = st.mode != 0xFFFF_FFFF and st.mode != n.mode; - if (set_mode and (st.mode & cloud9.dmdir) != (n.mode & cloud9.dmdir)) return error.Perm; - const set_mtime = st.mtime != 0xFFFF_FFFF and st.mtime != n.mtime; - if (!(rename or set_len or set_mode or set_mtime)) return; - const new_name: ?[]u8 = if (rename) s.gpa.dupe(u8, st.name) catch return error.NoSpace else null; - errdefer if (new_name) |nn| s.gpa.free(nn); - if (set_len) try s.resizeData(n, @intCast(st.length)); - // Nothing below can fail. - if (new_name) |nn| { - s.gpa.free(n.name); - n.name = nn; - s.touch(n.parent.?); - } - if (set_mode) n.mode = (n.mode & cloud9.dmdir) | (st.mode & ~cloud9.dmdir); - s.touch(n); - if (set_mtime) n.mtime = st.mtime; - } - - fn clunk(ctx: *anyopaque, h: Handle) void { - const s = self(ctx); - s.release(s.node(h)); - } -}; - -// --------------------------------------------------------------------------- -// Tests: the scratch tree mounted at /scratch of a core server. -// --------------------------------------------------------------------------- - -const testing = std.testing; - -const test_cfg: core.Config = .{ .name = "tester", .msize = 8192, .max_fids = 32 }; -const TS = core.Server(test_cfg); - -const budget: usize = 1 << 20; - -const Fixture = struct { - ctx: u8 = 0, - shared: TS.Shared = undefined, - storage: TS.Storage = undefined, - scratch: Scratch = undefined, - h: TS.Harness = undefined, - - fn init(x: *Fixture) !void { - x.shared = .init(&x.ctx); - x.scratch = try Scratch.init(testing.allocator, budget); - errdefer x.scratch.deinit(); - try x.shared.addProvider(x.scratch.provider("scratch")); - try x.h.init(&x.shared, &x.storage); - } - - fn deinit(x: *Fixture) void { - x.h.deinit(); - x.scratch.deinit(); - } - - fn nodeOf(x: *Fixture, fid: u32) *Node { - for (x.h.conn.fids) |f| if (f.used and f.id == fid) return x.scratch.node(f.node.prov.h); - unreachable; - } -}; - -const dontcare = core.stat_dontcare; - -test "scratch create/write/read/rename/truncate/remove" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - // create + write - const cr = try x.h.ok(.{ .create = .{ .fid = 1, .name = "x", .perm = 0o644, .mode = cloud9.ordwr } }); - try testing.expectEqual(cloud9.qtfile, cr.create.qid.type); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "hello" } }); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 5, .data = " world" } }); - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 100 } }); - try testing.expectEqualStrings("hello world", r.read); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - // rename x -> y - try x.h.walkTo(2, &.{ "scratch", "x" }); - var st = dontcare; - st.name = "y"; - _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = st } }); - try x.h.walkTo(3, &.{"scratch"}); - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 30, .names = &.{"x"} } }, "file does not exist"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); - try x.h.walkTo(3, &.{ "scratch", "y" }); - // truncate then extend with zero fill - st = dontcare; - st.length = 2; - _ = try x.h.ok(.{ .wstat = .{ .fid = 3, .stat = st } }); - st.length = 4; - _ = try x.h.ok(.{ .wstat = .{ .fid = 3, .stat = st } }); - const text = try x.h.readAll(3); - defer testing.allocator.free(text); - try testing.expectEqualStrings("he\x00\x00", text); - const s3 = try x.h.ok(.{ .stat = .{ .fid = 3 } }); - try testing.expectEqualStrings("y", s3.stat.name); - try testing.expectEqual(@as(u64, 4), s3.stat.length); - try testing.expectEqualStrings("tester", s3.stat.uid); - _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - // mkdir, nested create, remove rules - try x.h.walkTo(4, &.{"scratch"}); - const dr = try x.h.ok(.{ .create = .{ .fid = 4, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.oread } }); - try testing.expectEqual(cloud9.qtdir, dr.create.qid.type); - _ = try x.h.ok(.{ .clunk = .{ .fid = 4 } }); - try x.h.walkTo(5, &.{ "scratch", "d" }); - _ = try x.h.ok(.{ .create = .{ .fid = 5, .name = "inner", .perm = 0o600, .mode = cloud9.owrite } }); - _ = try x.h.ok(.{ .write = .{ .fid = 5, .offset = 0, .data = "z" } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 5 } }); - try x.h.walkTo(6, &.{ "scratch", "d" }); - try x.h.expectFail(.{ .remove = .{ .fid = 6 } }, "directory not empty"); - try x.h.expectFail(.{ .clunk = .{ .fid = 6 } }, "unknown fid"); // remove always clunks - try x.h.walkTo(7, &.{ "scratch", "d", "inner" }); - _ = try x.h.ok(.{ .remove = .{ .fid = 7 } }); - try x.h.walkTo(8, &.{ "scratch", "d" }); - _ = try x.h.ok(.{ .remove = .{ .fid = 8 } }); - try x.h.walkTo(9, &.{ "scratch", "y" }); - _ = try x.h.ok(.{ .remove = .{ .fid = 9 } }); - try x.h.walkTo(10, &.{"scratch"}); - _ = try x.h.ok(.{ .open = .{ .fid = 10, .mode = cloud9.oread } }); - const names = try x.h.listDir(10, 1024); - defer testing.allocator.free(names); - try testing.expectEqual(@as(usize, 0), names.len); - // append-only files ignore the offset - try x.h.walkTo(11, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 11, .name = "log", .perm = cloud9.dmappend | 0o644, .mode = cloud9.ordwr } }); - _ = try x.h.ok(.{ .write = .{ .fid = 11, .offset = 100, .data = "a" } }); - _ = try x.h.ok(.{ .write = .{ .fid = 11, .offset = 0, .data = "b" } }); - const lr = try x.h.ok(.{ .read = .{ .fid = 11, .offset = 0, .count = 10 } }); - try testing.expectEqualStrings("ab", lr.read); - try testing.expect(lr.read.len == 2); - const ls = try x.h.ok(.{ .stat = .{ .fid = 11 } }); - try testing.expect(ls.stat.qid.type & cloud9.qtappend != 0); - // the scratch root cannot be removed - try x.h.walkTo(12, &.{"scratch"}); - try x.h.expectFail(.{ .remove = .{ .fid = 12 } }, "permission denied"); -} - -test "walk of a missing name and walking a file" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - try x.h.expectFail(.{ .walk = .{ .fid = 1, .newfid = 2, .names = &.{"nope"} } }, "file does not exist"); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "f", .perm = 0o644, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try x.h.walkTo(3, &.{ "scratch", "f" }); - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"x"} } }, "not a directory"); - // a walk that fails past the first element is a partial Rwalk that leaves newfid unused - const r = try x.h.ok(.{ .walk = .{ .fid = 0, .newfid = 4, .names = &.{ "scratch", "nope", "x" } } }); - try testing.expectEqual(@as(u16, 1), r.walk.nwqid); - try x.h.expectFail(.{ .clunk = .{ .fid = 4 } }, "unknown fid"); - // .. from a file is not a directory; .. from the scratch root reaches the server root - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 5, .names = &.{".."} } }, "not a directory"); - try x.h.walkTo(5, &.{"scratch"}); - const up = try x.h.ok(.{ .walk = .{ .fid = 5, .newfid = 6, .names = &.{ "..", "scratch", "..", "README" } } }); - try testing.expectEqual(@as(u16, 4), up.walk.nwqid); - try testing.expectEqual(@as(u64, 0), up.walk.wqid[1].path); // provider 0, root - try testing.expect(up.walk.wqid[0].type & cloud9.qtdir != 0); -} - -test "directory read across consecutive offsets returns every record exactly once" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - const n = 40; - for (0..n) |i| { - try x.h.walkTo(1, &.{"scratch"}); - var name_buf: [64]u8 = undefined; - const name = try std.fmt.bufPrint(&name_buf, "file-with-a-long-name-{d:0>3}", .{i}); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = name, .perm = 0o644, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - } - try x.h.walkTo(2, &.{"scratch"}); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - // 200 bytes fits two records, so this takes many reads. - const names = try x.h.listDir(2, 200); - defer TS.Harness.freeNames(names); - try testing.expectEqual(@as(usize, n), names.len); - var seen: [n]bool = @splat(false); - for (names) |nm| { - const idx = try std.fmt.parseInt(usize, nm[nm.len - 3 ..], 10); - try testing.expect(!seen[idx]); - seen[idx] = true; - } - for (seen) |s| try testing.expect(s); - try x.h.expectFail(.{ .read = .{ .fid = 2, .offset = 7, .count = 200 } }, "bad offset"); - // a read that cannot fit even one record returns nothing rather than splitting it - const tiny = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 30 } }); - try testing.expectEqual(@as(usize, 0), tiny.read.len); - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - // Tversion resets every fid and every reference - try x.h.version(8192); - try testing.expectEqual(@as(usize, 0), x.h.conn.fidCount()); - for (x.scratch.root.children.items) |ch| try testing.expectEqual(@as(u32, 0), ch.refs); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } }); -} - -test "DMEXCL admits one open fid at a time" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - const cr = try x.h.ok(.{ .create = .{ .fid = 1, .name = "lock", .perm = cloud9.dmexcl | 0o644, .mode = cloud9.owrite } }); - try testing.expect(cr.create.qid.type & cloud9.qtexcl != 0); - try x.h.walkTo(2, &.{ "scratch", "lock" }); - try x.h.expectFail(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }, "exclusive use file already open"); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - try x.h.walkTo(3, &.{ "scratch", "lock" }); - try x.h.expectFail(.{ .open = .{ .fid = 3, .mode = cloud9.oread } }, "exclusive use file already open"); - // a Tversion reset drops the open and frees the file for the next session - try x.h.version(8192); - _ = try x.h.ok(.{ .attach = .{ .fid = 0, .uname = "tester" } }); - try x.h.walkTo(4, &.{ "scratch", "lock" }); - _ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } }); - try testing.expectEqual(@as(u32, 1), x.nodeOf(4).opens); - _ = try x.h.ok(.{ .remove = .{ .fid = 4 } }); -} - -test "scratch memory: zero-length writes, truncation frees, global budget" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - x.scratch.max_file = 4096; - try x.h.walkTo(1, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "f", .perm = 0o644, .mode = cloud9.ordwr } }); - // a zero-length write at a huge offset must not extend the file - const w0 = try x.h.ok(.{ .write = .{ .fid = 1, .offset = std.math.maxInt(u64), .data = "" } }); - try testing.expectEqual(@as(u32, 0), w0.write); - var st = try x.h.ok(.{ .stat = .{ .fid = 1 } }); - try testing.expectEqual(@as(u64, 0), st.stat.length); - // growth is charged to the budget; truncation releases it (memory too) - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 1000, .data = "x" } }); - try testing.expectEqual(@as(usize, 1001), x.scratch.bytes); - var ws = dontcare; - ws.length = 10; - _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = ws } }); - try testing.expectEqual(@as(usize, 10), x.scratch.bytes); - try testing.expectEqual(@as(usize, 10), x.nodeOf(1).data.capacity); - // per-file cap and the global budget both answer "no space" - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 4096, .data = "x" } }, "no space left on device"); - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = std.math.maxInt(u64), .data = "x" } }, "no space left on device"); - x.scratch.bytes = budget - 10; // pretend other files hold the rest - try x.h.expectFail(.{ .write = .{ .fid = 1, .offset = 10, .data = "0123456789A" } }, "no space left on device"); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 10, .data = "0123456789" } }); - try testing.expectEqual(budget, x.scratch.bytes); - ws.length = 4096; - try x.h.expectFail(.{ .wstat = .{ .fid = 1, .stat = ws } }, "no space left on device"); - x.scratch.bytes -= budget - 20; - // OTRUNC releases too - try x.h.walkTo(2, &.{ "scratch", "f" }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.owrite | cloud9.otrunc } }); - try testing.expectEqual(@as(usize, 0), x.scratch.bytes); - st = try x.h.ok(.{ .stat = .{ .fid = 2 } }); - try testing.expectEqual(@as(u64, 0), st.stat.length); - // removing a file with content returns its bytes once the last fid lets go - _ = try x.h.ok(.{ .write = .{ .fid = 2, .offset = 0, .data = "abc" } }); - try testing.expectEqual(@as(usize, 3), x.scratch.bytes); - _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); - try testing.expectEqual(@as(usize, 3), x.scratch.bytes); // fid 1 still holds it - const r = try x.h.ok(.{ .read = .{ .fid = 1, .offset = 0, .count = 10 } }); - try testing.expectEqualStrings("abc", r.read); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try testing.expectEqual(@as(usize, 0), x.scratch.bytes); -} - -test "wstat with every field equal to the current stat changes nothing" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "same", .perm = 0o640, .mode = cloud9.oread } }); - const before = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat; - var copy = before; - var name_buf: [core.max_name]u8 = undefined; - @memcpy(name_buf[0..before.name.len], before.name); - copy.name = name_buf[0..before.name.len]; - copy.uid = "tester"; - copy.gid = "tester"; - copy.muid = "tester"; - _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = copy } }); - const after = (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat; - try testing.expectEqual(before.qid, after.qid); - try testing.expectEqual(before.mtime, after.mtime); - try testing.expectEqual(before.mode, after.mode); - try testing.expectEqualStrings("same", after.name); - // and a rename to the very same name is also a no-op - var st = dontcare; - st.name = "same"; - _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = st } }); - try testing.expectEqual(before.qid, (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.qid); - // renaming onto an existing sibling is refused - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try x.h.walkTo(2, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 2, .name = "other", .perm = 0o640, .mode = cloud9.oread } }); - st.name = "same"; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = st } }, "file already exists"); - // the mode's directory bit is immutable, mtime is settable - st = dontcare; - st.mode = cloud9.dmdir | 0o640; - try x.h.expectFail(.{ .wstat = .{ .fid = 2, .stat = st } }, "permission denied"); - st = dontcare; - st.mtime = 12345; - _ = try x.h.ok(.{ .wstat = .{ .fid = 2, .stat = st } }); - try testing.expectEqual(@as(u32, 12345), (try x.h.ok(.{ .stat = .{ .fid = 2 } })).stat.mtime); - _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); -} - -test "ORCLOSE removes on clunk and removed files stay readable through open fids" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - _ = try x.h.ok(.{ .create = .{ .fid = 1, .name = "tmp", .perm = 0o644, .mode = cloud9.ordwr | cloud9.orclose } }); - _ = try x.h.ok(.{ .write = .{ .fid = 1, .offset = 0, .data = "gone" } }); - try x.h.walkTo(2, &.{ "scratch", "tmp" }); - _ = try x.h.ok(.{ .open = .{ .fid = 2, .mode = cloud9.oread } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 1 } }); - try x.h.walkTo(3, &.{"scratch"}); - try x.h.expectFail(.{ .walk = .{ .fid = 3, .newfid = 4, .names = &.{"tmp"} } }, "file does not exist"); - const r = try x.h.ok(.{ .read = .{ .fid = 2, .offset = 0, .count = 10 } }); - try testing.expectEqualStrings("gone", r.read); - try testing.expectEqual(@as(usize, 4), x.scratch.bytes); - _ = try x.h.ok(.{ .clunk = .{ .fid = 2 } }); - try testing.expectEqual(@as(usize, 0), x.scratch.bytes); - // create inside a removed directory fails - _ = try x.h.ok(.{ .create = .{ .fid = 3, .name = "d", .perm = cloud9.dmdir | 0o755, .mode = cloud9.oread } }); - try x.h.walkTo(5, &.{ "scratch", "d" }); - _ = try x.h.ok(.{ .remove = .{ .fid = 5 } }); - _ = try x.h.ok(.{ .clunk = .{ .fid = 3 } }); - try x.h.walkTo(6, &.{"scratch"}); - try x.h.expectFail(.{ .walk = .{ .fid = 6, .newfid = 7, .names = &.{"d"} } }, "file does not exist"); - try x.h.expectFail(.{ .create = .{ .fid = 5, .name = "x", .perm = 0o644, .mode = cloud9.oread } }, "unknown fid"); // remove clunked it -} - -test "qid paths are stable identities, not addresses: remove + recreate differ" { - var x: Fixture = .{}; - try x.init(); - defer x.deinit(); - try x.h.walkTo(1, &.{"scratch"}); - const a = try x.h.ok(.{ .create = .{ .fid = 1, .name = "f", .perm = 0o644, .mode = cloud9.oread } }); - const path_a = a.create.qid.path; - try testing.expectEqual(x.nodeOf(1).path, path_a & ((1 << 56) - 1)); - try testing.expect(path_a != 0); - // a rename keeps the identity - var st = dontcare; - st.name = "g"; - _ = try x.h.ok(.{ .wstat = .{ .fid = 1, .stat = st } }); - try testing.expectEqual(path_a, (try x.h.ok(.{ .stat = .{ .fid = 1 } })).stat.qid.path); - _ = try x.h.ok(.{ .remove = .{ .fid = 1 } }); - // the allocator very likely reuses the freed node's address here - try x.h.walkTo(2, &.{"scratch"}); - const b = try x.h.ok(.{ .create = .{ .fid = 2, .name = "f", .perm = 0o644, .mode = cloud9.oread } }); - try testing.expect(b.create.qid.path != path_a); - _ = try x.h.ok(.{ .remove = .{ .fid = 2 } }); - // the scratch root keeps path 0 (its handle), like every provider root - try x.h.walkTo(3, &.{"scratch"}); - try testing.expectEqual(@as(u64, 0), (try x.h.ok(.{ .stat = .{ .fid = 3 } })).stat.qid.path); - // directory listing reports the same identities as walking - _ = try x.h.ok(.{ .create = .{ .fid = 3, .name = "listed", .perm = 0o644, .mode = cloud9.oread } }); - const via_create = (try x.h.ok(.{ .stat = .{ .fid = 3 } })).stat.qid; - try x.h.walkTo(4, &.{"scratch"}); - _ = try x.h.ok(.{ .open = .{ .fid = 4, .mode = cloud9.oread } }); - const r = try x.h.ok(.{ .read = .{ .fid = 4, .offset = 0, .count = 1024 } }); - const listed = try cloud9.Stat.decode(r.read[0 .. std.mem.readInt(u16, r.read[0..2], .little) + 2]); - try testing.expectEqual(via_create, listed.qid); - _ = try x.h.ok(.{ .remove = .{ .fid = 3 } }); -} diff --git a/introspect/src/vars.zig b/introspect/src/vars.zig deleted file mode 100644 index 20cadfc..0000000 --- a/introspect/src/vars.zig +++ /dev/null @@ -1,824 +0,0 @@ -//! Comptime value renderers for /vars. For a type T, `vtableFor(T)` builds (at -//! comptime) a flat table of the files and directories that describe a value of -//! that type: -//! -//! /value rendered text /type @typeName -//! /size @sizeOf /addr 0x… -//! /raw the bytes /f//... recursively (depth <= max_depth) -//! -//! The core serves a variable by walking this table; a node index is the whole -//! state it needs. Rendering writes into a `*std.Io.Writer` and never allocates. -//! Writes to scalar `value` files are plain stores (not atomic). -const std = @import("std"); -const Writer = std.Io.Writer; - -/// Deepest `f/` nesting: /vars/x/f/a/f/b/f/c/f/d/value is depth 4. -pub const max_depth: u8 = 4; -/// Longest string rendered from a `[]const u8` / `[*:0]const u8` before "…". -pub const max_string: usize = 256; -/// Most array/slice elements rendered before "…". -pub const max_elems: usize = 64; - -pub const Kind = enum(u8) { - /// The directory of a value: value, type, size, addr, raw, [f]. - dir, - /// Rendered text; writable when `set` is non-null. - value, - /// @typeName, static content. - type_name, - /// @sizeOf, static content. - size, - /// "0x…" of the value's address. - addr, - /// The bytes of the value, length = size. - raw, - /// The `f` directory: one `dir` per struct field. - fields, -}; - -pub const RenderFn = *const fn (base: [*]const u8, w: *Writer) Writer.Error!void; -pub const SetFn = *const fn (base: [*]u8, text: []const u8) SetError!void; -pub const SetError = error{ Invalid, Unsupported }; - -/// One node of a type's tree. Children occupy `first..first+count`. -pub const Node = struct { - name: []const u8, - kind: Kind, - parent: u32, - first: u32 = 0, - count: u32 = 0, - /// Byte offset of the described value from the variable's base address. - offset: usize, - /// @sizeOf the described value. - size: usize, - /// Static text for `type_name` and `size` leaves. - content: []const u8 = "", - render: ?RenderFn = null, - set: ?SetFn = null, - - pub fn isDir(n: Node) bool { - return n.kind == .dir or n.kind == .fields; - } - - pub fn writable(n: Node) bool { - return n.kind == .value and n.set != null; - } -}; - -pub const VTable = struct { - nodes: []const Node, - type_name: []const u8, - size: usize, - - /// The child of `dir` named `name`, if any. - pub fn child(vt: *const VTable, dir: u32, name: []const u8) ?u32 { - const d = vt.nodes[dir]; - for (d.first..d.first + d.count) |i| { - if (std.mem.eql(u8, vt.nodes[i].name, name)) return @intCast(i); - } - return null; - } -}; - -/// The comptime-generated table for `T`; the same pointer for the same `T`. -pub fn vtableFor(comptime T: type) *const VTable { - const S = struct { - const nodes = buildTable(T); - const vt: VTable = .{ .nodes = &nodes, .type_name = @typeName(T), .size = @sizeOf(T) }; - }; - return &S.vt; -} - -/// Whether a struct's fields get an `f/` directory at this depth. -fn hasFields(comptime T: type, depth: u8) bool { - if (depth >= max_depth) return false; - return switch (@typeInfo(T)) { - .@"struct" => |s| s.layout != .@"packed" and fieldCount(T) > 0, - else => false, - }; -} - -fn fieldCount(comptime T: type) usize { - var n: usize = 0; - for (@typeInfo(T).@"struct".fields) |f| { - if (!f.is_comptime and @sizeOf(f.type) != 0) n += 1; - } - return n; -} - -fn countNodes(comptime T: type, depth: u8) usize { - var n: usize = 6; // dir + value, type, size, addr, raw - if (hasFields(T, depth)) { - n += 1; // f - for (@typeInfo(T).@"struct".fields) |f| { - if (f.is_comptime or @sizeOf(f.type) == 0) continue; - n += countNodes(f.type, depth + 1); - } - } - return n; -} - -fn fill(nodes: []Node, next: *usize, idx: usize, comptime T: type, name: []const u8, offset: usize, depth: u8, parent: u32) void { - const with_fields = hasFields(T, depth); - const count: u32 = if (with_fields) 6 else 5; - const first = next.*; - next.* += count; - nodes[idx] = .{ .name = name, .kind = .dir, .parent = parent, .first = @intCast(first), .count = count, .offset = offset, .size = @sizeOf(T) }; - const me: u32 = @intCast(idx); - nodes[first + 0] = .{ .name = "value", .kind = .value, .parent = me, .offset = offset, .size = @sizeOf(T), .render = renderFor(T), .set = setFor(T) }; - nodes[first + 1] = .{ .name = "type", .kind = .type_name, .parent = me, .offset = offset, .size = @sizeOf(T), .content = @typeName(T) }; - nodes[first + 2] = .{ .name = "size", .kind = .size, .parent = me, .offset = offset, .size = @sizeOf(T), .content = std.fmt.comptimePrint("{d}", .{@sizeOf(T)}) }; - nodes[first + 3] = .{ .name = "addr", .kind = .addr, .parent = me, .offset = offset, .size = @sizeOf(T) }; - nodes[first + 4] = .{ .name = "raw", .kind = .raw, .parent = me, .offset = offset, .size = @sizeOf(T) }; - if (with_fields) { - const fdir: u32 = @intCast(first + 5); - const nf = fieldCount(T); - const ffirst = next.*; - next.* += nf; - nodes[fdir] = .{ .name = "f", .kind = .fields, .parent = me, .first = @intCast(ffirst), .count = @intCast(nf), .offset = offset, .size = @sizeOf(T) }; - var i: usize = 0; - for (@typeInfo(T).@"struct".fields) |f| { - if (f.is_comptime or @sizeOf(f.type) == 0) continue; - fill(nodes, next, ffirst + i, f.type, f.name, offset + @offsetOf(T, f.name), depth + 1, fdir); - i += 1; - } - } -} - -fn buildTable(comptime T: type) [countNodes(T, 0)]Node { - @setEvalBranchQuota(1_000_000); - var nodes: [countNodes(T, 0)]Node = undefined; - var next: usize = 1; - fill(&nodes, &next, 0, T, "", 0, 0, 0); - std.debug.assert(next == nodes.len); - return nodes; -} - -fn renderFor(comptime T: type) RenderFn { - return &struct { - fn f(base: [*]const u8, w: *Writer) Writer.Error!void { - const p: *const T = @ptrCast(@alignCast(base)); - try render(p, w, max_depth); - } - }.f; -} - -fn setFor(comptime T: type) ?SetFn { - if (!settable(T)) return null; - return &struct { - fn f(base: [*]u8, text: []const u8) SetError!void { - const p: *T = @ptrCast(@alignCast(base)); - try set(p, text); - } - }.f; -} - -fn settable(comptime T: type) bool { - return switch (@typeInfo(T)) { - .int, .float, .bool, .@"enum" => true, - else => false, - }; -} - -// --------------------------------------------------------------------------- -// Rendering -// --------------------------------------------------------------------------- - -/// Renders `ptr.*`. Structs become "field: value" lines (nested structs -/// indented); everything else is a single line without a trailing newline. -/// `depth` bounds struct/union/optional nesting; deeper values print as "…". -pub fn render(ptr: anytype, w: *Writer, depth: usize) Writer.Error!void { - const T = @TypeOf(ptr.*); - if (comptime isPlainStruct(T)) { - try renderStruct(T, ptr, w, depth, 0); - } else { - try renderValue(T, ptr, w, depth); - } -} - -fn isPlainStruct(comptime T: type) bool { - return switch (@typeInfo(T)) { - .@"struct" => |s| !s.is_tuple and s.fields.len > 0, - else => false, - }; -} - -/// The multi-line form: each field on its own line, nested structs indented. -fn renderStruct(comptime T: type, ptr: *const T, w: *Writer, depth: usize, indent: usize) Writer.Error!void { - if (depth == 0) { - try w.splatByteAll(' ', indent); - try w.writeAll("…\n"); - return; - } - const packed_layout = @typeInfo(T).@"struct".layout == .@"packed"; - inline for (@typeInfo(T).@"struct".fields) |f| { - try w.splatByteAll(' ', indent); - try w.writeAll(f.name); - try w.writeByte(':'); - if (comptime f.is_comptime) { - try w.writeAll(" (comptime)\n"); - } else if (comptime packed_layout) { - // Fields of a packed struct have no byte address: render a copy. - const v = @field(ptr.*, f.name); - try w.writeByte(' '); - try renderValue(f.type, &v, w, depth - 1); - try w.writeByte('\n'); - } else if (comptime isPlainStruct(f.type)) { - try w.writeByte('\n'); - try renderStruct(f.type, &@field(ptr.*, f.name), w, depth - 1, indent + 2); - } else { - try w.writeByte(' '); - try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1); - try w.writeByte('\n'); - } - } -} - -/// The single-line form of any value. -fn renderValue(comptime T: type, ptr: *const T, w: *Writer, depth: usize) Writer.Error!void { - switch (@typeInfo(T)) { - .int, .comptime_int => try w.print("{d}", .{ptr.*}), - .float, .comptime_float => try w.print("{d}", .{ptr.*}), - .bool => { - // The variable is live memory that anything (a debugger's /mem write, - // a torn update) may have corrupted: judge the byte, not the bool. - const b = @as(*const u8, @ptrCast(ptr)).*; - switch (b) { - 0 => try w.writeAll("false"), - 1 => try w.writeAll("true"), - else => try w.print("{d}", .{b}), - } - }, - .void => try w.writeAll("{}"), - .@"enum" => |e| { - // Read the storage bytes as one integer: @tagName/switch on a corrupt - // value is a safety panic, and the value is caller memory we do not - // control. A load through the tag type would truncate to its bit - // width (a u2 tag in a byte), so the full storage width is read. - if (@sizeOf(T) == 0) return w.writeAll(e.fields[0].name); - const Raw = std.meta.Int(.unsigned, @sizeOf(T) * 8); - const raw = @as(*align(@alignOf(T)) const Raw, @ptrCast(ptr)).*; - const TagU = std.meta.Int(.unsigned, @bitSizeOf(e.tag_type)); - const padding: Raw = if (@bitSizeOf(TagU) == @bitSizeOf(Raw)) 0 else ~@as(Raw, std.math.maxInt(TagU)); - if (raw & padding == 0) { - const low: TagU = @truncate(raw); - inline for (e.fields) |f| { - if (low == @as(TagU, @bitCast(@as(e.tag_type, f.value)))) return w.writeAll(f.name); - } - } - try w.print("{d}", .{raw}); - }, - .error_set => try w.print("error.{s}", .{@errorName(ptr.*)}), - .error_union => |eu| if (ptr.*) |v| { - try renderValue(eu.payload, &v, w, depth); - } else |e| { - try w.print("error.{s}", .{@errorName(e)}); - }, - .optional => |o| if (ptr.*) |v| { - try renderValue(o.child, &v, w, depth); - } else { - try w.writeAll("null"); - }, - .pointer => |p| switch (p.size) { - .slice => if (p.child == u8) { - try renderString(ptr.*, w); - } else { - try renderElems(p.child, ptr.*, w, depth); - }, - .many => if (p.child == u8 and p.sentinel() == 0) { - try renderCString(ptr.*, w); - } else { - try w.print("0x{x}", .{@intFromPtr(ptr.*)}); - }, - .one, .c => try w.print("0x{x}", .{@intFromPtr(ptr.*)}), - }, - .array => |a| if (a.child == u8) { - try renderString(ptr.*[0..], w); - } else { - try renderElems(a.child, ptr.*[0..], w, depth); - }, - .vector => |v| { - const arr: [v.len]v.child = ptr.*; - try renderElems(v.child, &arr, w, depth); - }, - .@"struct" => |s| { - if (depth == 0) { - try w.writeAll("…"); - return; - } - if (s.fields.len == 0) { - try w.writeAll("{}"); - return; - } - try w.writeAll("{ "); - inline for (s.fields, 0..) |f, i| { - if (i != 0) try w.writeAll(", "); - if (!s.is_tuple) { - try w.writeAll(f.name); - try w.writeAll(": "); - } - if (comptime f.is_comptime) { - try w.writeAll("(comptime)"); - } else if (comptime s.layout == .@"packed") { - const v = @field(ptr.*, f.name); - try renderValue(f.type, &v, w, depth - 1); - } else { - try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1); - } - } - try w.writeAll(" }"); - }, - .@"union" => |u| { - const Tag = u.tag_type orelse { - try w.print("(untagged union, {d} bytes)", .{@sizeOf(T)}); - return; - }; - if (depth == 0) { - try w.writeAll("…"); - return; - } - // A switch on a corrupt tag is a safety panic: match the integer first. - const raw = @intFromEnum(@as(Tag, ptr.*)); - inline for (u.fields) |f| { - if (raw == @intFromEnum(@field(Tag, f.name))) { - try w.writeAll(f.name); - if (f.type != void) { - try w.writeAll(": "); - try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1); - } - return; - } - } - try w.print("(invalid tag {d})", .{raw}); - }, - .@"fn" => try w.print("0x{x}", .{@intFromPtr(ptr)}), - else => try w.print("<{s}>", .{@typeName(T)}), - } -} - -fn renderElems(comptime E: type, items: []const E, w: *Writer, depth: usize) Writer.Error!void { - try w.writeByte('['); - for (items, 0..) |*item, i| { - if (i == max_elems) { - try w.writeAll(", …"); - break; - } - if (i != 0) try w.writeAll(", "); - try renderValue(E, item, w, depth); - } - try w.writeByte(']'); -} - -/// A NUL-terminated string, scanning at most `max_string` + 1 bytes for the -/// terminator so that a missing one cannot walk off the end of the mapping. -fn renderCString(s: [*:0]const u8, w: *Writer) Writer.Error!void { - var n: usize = 0; - while (n <= max_string and s[n] != 0) n += 1; - try renderString(s[0..n], w); -} - -/// A double-quoted string with C-style escapes, truncated to `max_string` bytes. -fn renderString(s: []const u8, w: *Writer) Writer.Error!void { - try w.writeByte('"'); - for (s[0..@min(s.len, max_string)]) |b| switch (b) { - '\n' => try w.writeAll("\\n"), - '\r' => try w.writeAll("\\r"), - '\t' => try w.writeAll("\\t"), - '\\' => try w.writeAll("\\\\"), - '"' => try w.writeAll("\\\""), - ' '...'!', '#'...'[', ']'...'~' => try w.writeByte(b), - else => { - const hex = "0123456789abcdef"; - try w.writeAll("\\x"); - try w.writeByte(hex[b >> 4]); - try w.writeByte(hex[b & 15]); - }, - }; - try w.writeByte('"'); - if (s.len > max_string) try w.writeAll("…"); -} - -// --------------------------------------------------------------------------- -// Setting -// --------------------------------------------------------------------------- - -/// Parses `text` and stores it into `ptr.*`: ints in decimal or 0x/0o/0b, -/// floats, bools (true/false/1/0), enums by tag name (or by integer value for -/// non-exhaustive enums). Other types are `error.Unsupported`. -pub fn set(ptr: anytype, text: []const u8) SetError!void { - const T = @TypeOf(ptr.*); - const s = std.mem.trim(u8, text, " \t\r\n\x00"); - switch (@typeInfo(T)) { - .int => ptr.* = std.fmt.parseInt(T, s, 0) catch return error.Invalid, - .float => ptr.* = std.fmt.parseFloat(T, s) catch return error.Invalid, - .bool => { - if (std.mem.eql(u8, s, "true") or std.mem.eql(u8, s, "1")) { - ptr.* = true; - } else if (std.mem.eql(u8, s, "false") or std.mem.eql(u8, s, "0")) { - ptr.* = false; - } else return error.Invalid; - }, - .@"enum" => |e| { - if (std.meta.stringToEnum(T, s)) |v| { - ptr.* = v; - } else if (!e.is_exhaustive) { - const raw = std.fmt.parseInt(e.tag_type, s, 0) catch return error.Invalid; - ptr.* = @enumFromInt(raw); - } else return error.Invalid; - }, - else => return error.Unsupported, - } -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -const testing = std.testing; - -fn renderToBuf(buf: []u8, ptr: anytype) ![]const u8 { - var w: Writer = .fixed(buf); - try render(ptr, &w, max_depth); - return w.buffered(); -} - -test "render scalars, strings, pointers, optionals, enums, arrays" { - var buf: [512]u8 = undefined; - const i: i32 = -42; - try testing.expectEqualStrings("-42", try renderToBuf(&buf, &i)); - const f: f32 = 1.5; - try testing.expectEqualStrings("1.5", try renderToBuf(&buf, &f)); - const b: bool = true; - try testing.expectEqualStrings("true", try renderToBuf(&buf, &b)); - const s: []const u8 = "hi \"there\"\n"; - try testing.expectEqualStrings("\"hi \\\"there\\\"\\n\"", try renderToBuf(&buf, &s)); - const z: [*:0]const u8 = "zed"; - try testing.expectEqualStrings("\"zed\"", try renderToBuf(&buf, &z)); - const p: *const i32 = &i; - var expect_buf: [32]u8 = undefined; - const expect = try std.fmt.bufPrint(&expect_buf, "0x{x}", .{@intFromPtr(&i)}); - try testing.expectEqualStrings(expect, try renderToBuf(&buf, &p)); - const o: ?u8 = null; - try testing.expectEqualStrings("null", try renderToBuf(&buf, &o)); - const o2: ?u8 = 7; - try testing.expectEqualStrings("7", try renderToBuf(&buf, &o2)); - const E = enum { red, green }; - const e: E = .green; - try testing.expectEqualStrings("green", try renderToBuf(&buf, &e)); - const NE = enum(u8) { a, _ }; - const ne: NE = @enumFromInt(9); - try testing.expectEqualStrings("9", try renderToBuf(&buf, &ne)); - const arr = [_]u16{ 1, 2, 3 }; - try testing.expectEqualStrings("[1, 2, 3]", try renderToBuf(&buf, &arr)); - const bytes = [_]u8{ 0, 'a', 0xff }; - try testing.expectEqualStrings("\"\\x00a\\xff\"", try renderToBuf(&buf, &bytes)); - const U = union(enum) { none, some: u32 }; - const u: U = .{ .some = 5 }; - try testing.expectEqualStrings("some: 5", try renderToBuf(&buf, &u)); - const un: U = .none; - try testing.expectEqualStrings("none", try renderToBuf(&buf, &un)); -} - -test "render structs multi-line with nested indentation and depth limit" { - const Inner = struct { x: f32, flags: [2]bool }; - const Outer = struct { a: u32, b: bool, name: []const u8, inner: Inner, items: []const Inner }; - const v: Outer = .{ .a = 1, .b = false, .name = "n", .inner = .{ .x = 2.5, .flags = .{ true, false } }, .items = &.{.{ .x = 0, .flags = .{ false, false } }} }; - var buf: [512]u8 = undefined; - try testing.expectEqualStrings( - \\a: 1 - \\b: false - \\name: "n" - \\inner: - \\ x: 2.5 - \\ flags: [true, false] - \\items: [{ x: 0, flags: [false, false] }] - \\ - , try renderToBuf(&buf, &v)); - var w: Writer = .fixed(&buf); - try render(&v, &w, 1); - try testing.expectEqualStrings( - \\a: 1 - \\b: false - \\name: "n" - \\inner: - \\ … - \\items: […] - \\ - , w.buffered()); -} - -test "long strings and arrays are truncated" { - const long = [_]u8{'x'} ** 300; - var buf: [1024]u8 = undefined; - const s: []const u8 = &long; - const out = try renderToBuf(&buf, &s); - try testing.expectEqual(@as(usize, 1 + max_string + 1 + "…".len), out.len); - try testing.expect(std.mem.endsWith(u8, out, "\"…")); - const nums: [100]u32 = @splat(1); - const out2 = try renderToBuf(&buf, &nums); - try testing.expect(std.mem.endsWith(u8, out2, ", …]")); - try testing.expectEqual(@as(usize, max_elems), std.mem.count(u8, out2, "1")); -} - -test "set parses ints, floats, bools and enums" { - var i: u32 = 0; - try set(&i, "42\n"); - try testing.expectEqual(@as(u32, 42), i); - try set(&i, "0x10"); - try testing.expectEqual(@as(u32, 16), i); - try testing.expectError(error.Invalid, set(&i, "-1")); - try testing.expectError(error.Invalid, set(&i, "abc")); - var si: i8 = 0; - try set(&si, " -7 "); - try testing.expectEqual(@as(i8, -7), si); - try testing.expectError(error.Invalid, set(&si, "200")); - var f: f64 = 0; - try set(&f, "2.25"); - try testing.expectEqual(@as(f64, 2.25), f); - var b: bool = false; - try set(&b, "true"); - try testing.expect(b); - try set(&b, "0"); - try testing.expect(!b); - try testing.expectError(error.Invalid, set(&b, "maybe")); - const E = enum { off, on }; - var e: E = .off; - try set(&e, "on"); - try testing.expectEqual(E.on, e); - try testing.expectError(error.Invalid, set(&e, "blue")); - var s: []const u8 = "x"; - try testing.expectError(error.Unsupported, set(&s, "y")); -} - -test "vtable table layout for a nested struct" { - const Inner = struct { x: f32 }; - const T = struct { a: u32, b: bool, name: []const u8, inner: Inner }; - const vt = vtableFor(T); - try testing.expectEqual(vt, vtableFor(T)); - try testing.expectEqualStrings(@typeName(T), vt.type_name); - const root = vt.nodes[0]; - try testing.expect(root.isDir()); - try testing.expectEqual(@as(u32, 6), root.count); - const value = vt.child(0, "value").?; - try testing.expect(!vt.nodes[value].writable()); // a struct is not settable - try testing.expectEqualStrings(std.fmt.comptimePrint("{d}", .{@sizeOf(T)}), vt.nodes[vt.child(0, "size").?].content); - const f = vt.child(0, "f").?; - try testing.expectEqual(Kind.fields, vt.nodes[f].kind); - try testing.expectEqual(@as(u32, 4), vt.nodes[f].count); - const a = vt.child(f, "a").?; - try testing.expectEqual(@offsetOf(T, "a"), vt.nodes[a].offset); - const a_value = vt.child(a, "value").?; - try testing.expect(vt.nodes[a_value].writable()); - try testing.expectEqual(@as(usize, 4), vt.nodes[a_value].size); - try testing.expectEqual(a, vt.nodes[a_value].parent); - const inner = vt.child(f, "inner").?; - const inner_f = vt.child(inner, "f").?; - const x = vt.child(inner_f, "x").?; - try testing.expectEqual(@offsetOf(T, "inner") + @offsetOf(Inner, "x"), vt.nodes[x].offset); - try testing.expectEqualStrings("f32", vt.nodes[vt.child(x, "type").?].content); - try testing.expect(vt.child(f, "nope") == null); - // rendering and setting through the table - var v: T = .{ .a = 1, .b = true, .name = "n", .inner = .{ .x = 0.5 } }; - const base: [*]u8 = @ptrCast(&v); - var buf: [256]u8 = undefined; - var w: Writer = .fixed(&buf); - const x_value = vt.child(x, "value").?; - try vt.nodes[x_value].render.?(base + vt.nodes[x_value].offset, &w); - try testing.expectEqualStrings("0.5", w.buffered()); - try vt.nodes[a_value].set.?(base + vt.nodes[a_value].offset, "42"); - try testing.expectEqual(@as(u32, 42), v.a); - w = .fixed(&buf); - try vt.nodes[value].render.?(base, &w); - try testing.expect(std.mem.startsWith(u8, w.buffered(), "a: 42\nb: true\n")); -} - -test "depth limit stops the f/ tree at max_depth" { - const L4 = struct { v: u8 }; - const L3 = struct { l4: L4 }; - const L2 = struct { l3: L3 }; - const L1 = struct { l2: L2 }; - const L0 = struct { l1: L1 }; - const vt = vtableFor(L0); - var node: u32 = 0; - var depth: usize = 0; - while (vt.child(node, "f")) |f| : (depth += 1) { - node = vt.nodes[f].first; // the single field - } - try testing.expectEqual(@as(usize, max_depth), depth); - try testing.expect(vt.child(node, "value") != null); -} - -test "every @typeInfo category renders without dereferencing anything unbounded" { - var buf: [2048]u8 = undefined; - // packed and extern structs (packed fields have no address: rendered by copy) - const Packed = packed struct { a: u3, b: bool, c: u12, e: enum(u2) { p, q, r } }; - const pk: Packed = .{ .a = 5, .b = true, .c = 300, .e = .r }; - try testing.expectEqualStrings("a: 5\nb: true\nc: 300\ne: r\n", try renderToBuf(&buf, &pk)); - const Ext = extern struct { x: u16, y: f32, inner: extern struct { z: u8 } }; - const ex: Ext = .{ .x = 1, .y = 0.5, .inner = .{ .z = 9 } }; - try testing.expectEqualStrings("x: 1\ny: 0.5\ninner:\n z: 9\n", try renderToBuf(&buf, &ex)); - const Holder = struct { p: Packed, list: [2]Packed }; - const ho: Holder = .{ .p = pk, .list = .{ pk, pk } }; - try testing.expect(std.mem.startsWith(u8, try renderToBuf(&buf, &ho), "p:\n a: 5\n")); - // the f/ tree has no entries for a packed struct and works through a table - const vt = vtableFor(Packed); - try testing.expect(vt.child(0, "f") == null); - var w: Writer = .fixed(&buf); - try vt.nodes[vt.child(0, "value").?].render.?(@ptrCast(&pk), &w); - try testing.expect(std.mem.startsWith(u8, w.buffered(), "a: 5\n")); - // optionals of pointers are printed, never followed - var target: u32 = 7; - const op: ?*u32 = ⌖ - var expect_buf: [32]u8 = undefined; - try testing.expectEqualStrings(try std.fmt.bufPrint(&expect_buf, "0x{x}", .{@intFromPtr(&target)}), try renderToBuf(&buf, &op)); - const np: ?*u32 = null; - try testing.expectEqualStrings("null", try renderToBuf(&buf, &np)); - const dangling: *const u32 = @ptrFromInt(0x1000); - try testing.expectEqualStrings("0x1000", try renderToBuf(&buf, &dangling)); - const cptr: [*c]const u8 = @ptrFromInt(0x2000); - try testing.expectEqualStrings("0x2000", try renderToBuf(&buf, &cptr)); - const manyp: [*]const u32 = @ptrFromInt(0x3000); - try testing.expectEqualStrings("0x3000", try renderToBuf(&buf, &manyp)); - // untagged and tagged unions, error unions, error sets - const Untagged = union { a: u32, b: f32 }; - const un: Untagged = .{ .a = 1 }; - try testing.expectEqualStrings(std.fmt.comptimePrint("(untagged union, {d} bytes)", .{@sizeOf(Untagged)}), try renderToBuf(&buf, &un)); - const Tagged = union(enum(u8)) { none, some: u32, pair: struct { l: u8, r: u8 } }; - const tg: Tagged = .{ .pair = .{ .l = 1, .r = 2 } }; - try testing.expectEqualStrings("pair: { l: 1, r: 2 }", try renderToBuf(&buf, &tg)); - const eu: anyerror!u8 = error.Boom; - try testing.expectEqualStrings("error.Boom", try renderToBuf(&buf, &eu)); - const eu2: error{X}!u8 = 4; - try testing.expectEqualStrings("4", try renderToBuf(&buf, &eu2)); - const es: anyerror = error.Zap; - try testing.expectEqualStrings("error.Zap", try renderToBuf(&buf, &es)); - // wide ints and floats, vectors, sentinel arrays, slices of slices, void, comptime fields - const big: u128 = std.math.maxInt(u128); - try testing.expectEqualStrings("340282366920938463463374607431768211455", try renderToBuf(&buf, &big)); - const neg: i128 = std.math.minInt(i128); - try testing.expectEqualStrings("-170141183460469231731687303715884105728", try renderToBuf(&buf, &neg)); - const h: f16 = 1.5; - try testing.expectEqualStrings("1.5", try renderToBuf(&buf, &h)); - const ld: f80 = 2.25; - try testing.expectEqualStrings("2.25", try renderToBuf(&buf, &ld)); - const quad: f128 = 3.125; - try testing.expectEqualStrings("3.125", try renderToBuf(&buf, &quad)); - const vec: @Vector(4, i16) = .{ 1, -2, 3, -4 }; - try testing.expectEqualStrings("[1, -2, 3, -4]", try renderToBuf(&buf, &vec)); - const sarr: [3:0]u8 = .{ 'a', 'b', 'c' }; - try testing.expectEqualStrings("\"abc\"", try renderToBuf(&buf, &sarr)); - const rows: []const []const u8 = &.{ "ab", "cd" }; - try testing.expectEqualStrings("[\"ab\", \"cd\"]", try renderToBuf(&buf, &rows)); - const Odd = struct { v: void, comptime k: u8 = 3, n: u8 }; - const odd: Odd = .{ .v = {}, .n = 1 }; - try testing.expectEqualStrings("v: {}\nk: (comptime)\nn: 1\n", try renderToBuf(&buf, &odd)); - try testing.expectEqual(@as(u32, 1), vtableFor(Odd).nodes[vtableFor(Odd).child(0, "f").?].count); - // self-referential through a pointer: rendered as an address, table stays finite - const Link = struct { next: ?*const @This(), v: u8 }; - var a: Link = .{ .next = null, .v = 1 }; - const b: Link = .{ .next = &a, .v = 2 }; - a.next = &b; - try testing.expectEqualStrings(try std.fmt.bufPrint(&expect_buf, "next: 0x{x}\nv: 2\n", .{@intFromPtr(&a)}), try renderToBuf(&buf, &b)); - try testing.expect(vtableFor(Link).nodes.len < 32); - // tuples - const tup: struct { u8, []const u8 } = .{ 1, "x" }; - try testing.expectEqualStrings("{ 1, \"x\" }", try renderToBuf(&buf, &tup)); -} - -test "corrupt live memory renders instead of trapping: enums, unions, bools" { - var buf: [128]u8 = undefined; - const E = enum(u8) { a, b }; - var raw_e: u8 = 7; - try testing.expectEqualStrings("7", try renderToBuf(&buf, @as(*const E, @ptrCast(&raw_e)))); - raw_e = 1; - try testing.expectEqualStrings("b", try renderToBuf(&buf, @as(*const E, @ptrCast(&raw_e)))); - // a u2 tag in a byte: the whole byte is judged, not the truncated tag (ReleaseSafe would say "c") - const E3 = enum { a, b, c }; - var raw3: u8 = 0xEE; - try testing.expectEqualStrings("238", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3)))); - raw3 = 3; - try testing.expectEqualStrings("3", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3)))); - raw3 = 2; - try testing.expectEqualStrings("c", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3)))); - const E12 = enum(u12) { p = 5, q = 4095 }; - var raw12: u16 = 0xF005; - try testing.expectEqualStrings("61445", try renderToBuf(&buf, @as(*const E12, @ptrCast(&raw12)))); - raw12 = 4095; - try testing.expectEqualStrings("q", try renderToBuf(&buf, @as(*const E12, @ptrCast(&raw12)))); - const ES = enum(i8) { neg = -3, pos = 7 }; - var raws: u8 = 0xFD; - try testing.expectEqualStrings("neg", try renderToBuf(&buf, @as(*const ES, @ptrCast(&raws)))); - raws = 0x80; - try testing.expectEqualStrings("128", try renderToBuf(&buf, @as(*const ES, @ptrCast(&raws)))); - const E1 = enum { only }; - const e1: E1 = .only; - try testing.expectEqualStrings("only", try renderToBuf(&buf, &e1)); - const NE = enum(u16) { x = 5, _ }; - var raw_ne: u16 = 5; - try testing.expectEqualStrings("x", try renderToBuf(&buf, @as(*const NE, @ptrCast(&raw_ne)))); - raw_ne = 6; - try testing.expectEqualStrings("6", try renderToBuf(&buf, @as(*const NE, @ptrCast(&raw_ne)))); - var raw_b: u8 = 2; - try testing.expectEqualStrings("2", try renderToBuf(&buf, @as(*const bool, @ptrCast(&raw_b)))); - const U = union(enum(u8)) { x: u32, y: bool }; - var raw_u: [@sizeOf(U)]u8 align(@alignOf(U)) = @splat(0x55); - const out = try renderToBuf(&buf, @as(*const U, @ptrCast(&raw_u))); - try testing.expectEqualStrings("(invalid tag 85)", out); - const S = struct { e: E, u: U, b: bool }; - var raw_s: [@sizeOf(S)]u8 align(@alignOf(S)) = @splat(0xEE); - const ps: *const S = @ptrCast(&raw_s); - _ = try renderToBuf(&buf, ps); // no trap - try testing.expect(std.mem.indexOf(u8, try renderToBuf(&buf, ps), "238") != null); -} - -test "a [*:0]const u8 without a terminator is read at most max_string + 1 bytes" { - // Only the first max_string + 1 bytes exist; anything beyond is the - // testing allocator's guard, which a wider scan would touch. - const mem = try testing.allocator.alloc(u8, max_string + 1); - defer testing.allocator.free(mem); - @memset(mem, 'x'); - const z: [*:0]const u8 = @ptrCast(mem.ptr); - var buf: [1024]u8 = undefined; - const out = try renderToBuf(&buf, &z); - try testing.expectEqual(@as(usize, 1 + max_string + 1 + "…".len), out.len); - try testing.expect(std.mem.endsWith(u8, out, "\"…")); - // exactly max_string bytes then NUL: no ellipsis - const mem2 = try testing.allocator.alloc(u8, max_string + 1); - defer testing.allocator.free(mem2); - @memset(mem2, 'y'); - mem2[max_string] = 0; - const z2: [*:0]const u8 = @ptrCast(mem2.ptr); - const out2 = try renderToBuf(&buf, &z2); - try testing.expectEqual(@as(usize, 1 + max_string + 1), out2.len); - // a garbage-length []const u8 still reads at most max_string bytes - const garbage: []const u8 = mem[0..max_string]; - _ = try renderToBuf(&buf, &garbage); -} - -test "set rejects hostile input without partial writes" { - var u: u8 = 200; - for ([_][]const u8{ "-1", "256", "1e3", "0x", "", " ", "1.5", "+", "0b2", "١", "12abc", "0x100", "\x00", "1 2" }) |bad| { - try testing.expectError(error.Invalid, set(&u, bad)); - try testing.expectEqual(@as(u8, 200), u); - } - try set(&u, "0b1111_1111"); - try testing.expectEqual(@as(u8, 255), u); - try set(&u, "+0o17"); - try testing.expectEqual(@as(u8, 15), u); - var i: i64 = 1; - try set(&i, "-9223372036854775808"); - try testing.expectEqual(std.math.minInt(i64), i); - try testing.expectError(error.Invalid, set(&i, "9223372036854775808")); - var w: u128 = 0; - try set(&w, "340282366920938463463374607431768211455"); - try testing.expectEqual(std.math.maxInt(u128), w); - try testing.expectError(error.Invalid, set(&w, "340282366920938463463374607431768211456")); - // floats: exponents, hex floats, inf/nan spellings, and junk - var f: f32 = 1; - try set(&f, "1.5e3"); - try testing.expectEqual(@as(f32, 1500), f); - try set(&f, "-0x1p-2"); - try testing.expectEqual(@as(f32, -0.25), f); - try set(&f, "1e999"); - try testing.expect(std.math.isInf(f)); - try testing.expectError(error.Invalid, set(&f, "1.5.5")); - try testing.expectError(error.Invalid, set(&f, "e5")); - try testing.expectError(error.Invalid, set(&f, "")); - var h: f16 = 0; - try set(&h, "65504"); - try testing.expectEqual(@as(f16, 65504), h); - var q: f128 = 0; - try set(&q, "2.5"); - try testing.expectEqual(@as(f128, 2.5), q); - // enums: NULs inside the tag, case, trailing junk; non-exhaustive by integer only when out of names - const E = enum(u8) { off, on }; - var e: E = .off; - for ([_][]const u8{ "on\x00x", "On", "on x", "1", "0x1", "" }) |bad| { - try testing.expectError(error.Invalid, set(&e, bad)); - try testing.expectEqual(E.off, e); - } - try set(&e, "\x00on\n"); - try testing.expectEqual(E.on, e); - const NE = enum(u8) { a, _ }; - var ne: NE = .a; - try set(&ne, "200"); - try testing.expectEqual(@as(u8, 200), @intFromEnum(ne)); - try testing.expectError(error.Invalid, set(&ne, "256")); - try testing.expectError(error.Invalid, set(&ne, "-1")); - try set(&ne, "a"); - try testing.expectEqual(NE.a, ne); - // bools - var b: bool = true; - for ([_][]const u8{ "yes", "TRUE", "2", "", "01" }) |bad| { - try testing.expectError(error.Invalid, set(&b, bad)); - try testing.expect(b); - } - // unsupported types are refused without touching memory - var opt: ?u8 = 3; - try testing.expectError(error.Unsupported, set(&opt, "4")); - try testing.expectEqual(@as(?u8, 3), opt); - var arr: [2]u8 = .{ 1, 2 }; - try testing.expectError(error.Unsupported, set(&arr, "x")); - var un: union(enum) { a: u8 } = .{ .a = 1 }; - try testing.expectError(error.Unsupported, set(&un, "a")); -} -- cgit v1.3