//! 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")); }