summaryrefslogtreecommitdiff
path: root/9proc/README.md
blob: 15177d77961b7399e5314ecc640aaaf8ae87bc68 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# 9proc

A 9P2000 debug/introspection server as a Zig 0.16 library, built on
[cloud9](../): a debugger-shaped interface where the protocol is just files.
Anything that can read a filesystem (a shell, an agent, an editor, `9p`,
[9ns](../9ns)) can inspect a running program: build facts, comptime
type layouts, live values, threads and their stacks, memory, breakpoints,
panics.

The core (`core`, `vars`) is freestanding: no allocator, no OS, no threads,
caller-owned static `Storage`, fixed-capacity tables sized at comptime. It
compiles for `riscv32-freestanding-none`. The tree is a backend of
`cloud9.fs.Server`, the file-server engine every 9P server built on cloud9
shares: the engine owns fids, walks, permissions and directory cursors;
the core answers its requests from static nodes, exposed variables and
providers, and a provider may answer a read later (`error.Again` parks it
until a later step). `scratch` (an in-memory read/write
tree) takes an allocator; `linux` is the platform layer (listeners, a poll
loop on one background thread, threads/stacks/registers, memory, breakpoints
and panics via `std.debug`). [docs/LIBRARY.md](docs/LIBRARY.md) has the full
contract.

```
9proc/
  build.zig          fragment imported by cloud9's root build.zig (steps below)
  src/root.zig       pub const core, vars, scratch, linux; Config, Server(cfg), Provider
  src/core.zig       the tree (static nodes, vars, providers) as a backend of cloud9.fs.Server
  src/vars.zig       comptime value renderers (@typeInfo) for /vars
  src/scratch.zig    in-memory read/write tree provider (takes an Allocator)
  src/freestanding_check.zig   root for the riscv32-freestanding-none compile check
  src/linux/probe.zig     background thread + poll loop + unix/tcp/fd listeners
  src/linux/debug.zig     threads, stacks, registers, addr→source, memory, breakpoints, panic
  src/linux/provider.zig  the debug provider (/threads, /addr, /mem, /hex, /breakpoints, /panic)
  src/linux/runtime.zig   /runtime generators (pid, uptime, argv, cwd, env, clients)
  demo/main.zig      the `9proc-demo` binary (below)
  test/              debug.sh, adv_9proc_hostile, adv_core_hostile, adv_linux_probe, adversarial.sh
  docs/LIBRARY.md    design rules and the module contracts
```

## Using the library

cloud9's `build.zig` exports two modules: `cloud9` (the protocol) and
`9proc` (this library, which imports `cloud9` itself). A package that
depends on cloud9 takes both from the one dependency:

```zig
const cloud9_dep = b.dependency("cloud9", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("cloud9", cloud9_dep.module("cloud9"));
exe.root_module.addImport("9proc", cloud9_dep.module("9proc"));
```

Embedding the core is three static objects and a push/step/output loop, the
same shape as cloud9's `Server`:

```zig
const proc9 = @import("9proc"); // the module is `9proc`; identifiers cannot start with a digit

const State = struct { ticks: u32, phase: enum { idle, busy } };
const cfg: proc9.Config = .{ .name = "fw", .types = &.{State}, .msize = 2048, .max_fids = 16 };
const S = proc9.Server(cfg);

var state: State = .{ .ticks = 0, .phase = .idle };
var storage: S.Storage = undefined;   // per connection: in/out frames + snapshot slots
var shared: S.Shared = undefined;     // once: providers and exposed variables

pub fn main() void {
    shared = .init(&state);
    shared.expose("state", &state) catch unreachable;   // /vars/state/{value,type,size,addr,raw,f/...}
    var conn: S.Conn = .init(&shared, &storage, cfg.msize);
    // transport loop: conn.push(bytes) ... while (try conn.step()) {} ... send conn.output(), conn.wrote(n)
}
```

On Linux, `proc9.linux.Probe` runs that loop for you on one background
thread over a Unix, TCP or inherited listener, and adds the debug provider;
`pub const panic = std.debug.FullPanic(proc9.linux.debug.panicHook);`
in the root module publishes panics under `/panic`. `demo/main.zig` shows
every piece together.

## The demo (`zig build 9proc`, binary `9proc-demo`)

A single-binary 9P2000 server whose file tree is the binary itself: build-time
facts, `comptime` reflection, live runtime state, a worker thread whose state
is exposed under `/vars`, and the Linux debug layer.

```
/README
/build/{zig_version,target,optimize,time,change}   captured by 9proc/build.zig (jj change id, UTC time)
/comptime/types/<T>/{name,size,align,fields}        @sizeOf/@alignOf/@typeInfo, generated at comptime
/comptime/decls                                     pub declarations of the server module
/runtime/{pid,ppid,uptime,argv,cwd,env,clients}
/runtime/fn/<name>                                  reading calls a Zig function (hostname, now, random, uname, fib30)
/runtime/ctl                                        write "fib N" | "add A B" | "echo TEXT" | "sleep-ms N" | "trap" | "panic"
/scratch/                                           in-memory read/write tree
/vars/state/...                                     the worker's State (readable and writable leaves)
/threads/<tid>/{name,stat,stack,regs}  /addr/<hex>  /mem/{maps,<hex>}  /hex/<hex>
/breakpoints/<tid>/{stack,regs,ctl}    /panic/{message,stack,ctl}
```

`/runtime/env` exposes the server's whole environment, so serve it on a Unix
socket or loopback only.

```sh
zig-out/bin/9proc-demo --unix /tmp/intro.sock &          # or --tcp IP:PORT, --stdio, --no-hold
zig-out/bin/9ns --unix /tmp/intro.sock -- sh -c '
  cat $NINE_MOUNT/build/zig_version; echo
  cat $NINE_MOUNT/comptime/types/Qid/fields
  echo "fib 20" > $NINE_MOUNT/runtime/ctl; cat $NINE_MOUNT/runtime/ctl
  cat $NINE_MOUNT/threads/*/stack'
```

## Building and testing

9proc lives in the cloud9 repository as `cloud9/9proc/` and is
wired into cloud9's `build.zig` through the fragment `9proc/build.zig`.
Everything is run from the cloud9 root:

```sh
zig build                          # installs zig-out/bin/9proc-demo with the other binaries
zig build 9proc                    # build and install only the demo
zig build 9proc-test               # library unit tests (core, vars, scratch, linux) and the demo's
zig build 9proc-check-freestanding # compile the core for riscv32-freestanding-none
zig build 9proc-debug-test         # src/linux/debug.zig unit tests
zig build 9proc-debug-itest        # test/debug.sh: threads, stacks, breakpoints, panic through 9ns
zig build 9proc-adv                # hostile raw-9P clients against the demo and the core,
                                   # the Linux layer through a 9ns mount (several minutes)
zig build -D9proc=false            # leave 9proc out
zig build 9proc-check-freestanding -Dtarget=riscv32-freestanding-none -D9proc=true
```

`-D9proc` (default: on for Linux targets) enables the module and the
freestanding check on any target; the demo and the Linux suites are added
only when the target OS is Linux. The end-to-end suites also need 9ns
(`-D9ns=true`, the Linux default), unprivileged user namespaces,
`/dev/fuse` and Python 3, and skip themselves otherwise.