diff options
| -rw-r--r-- | 9ns/README.md (renamed from 9player/README.md) | 74 | ||||
| -rw-r--r-- | 9ns/build.zig (renamed from 9player/build.zig) | 50 | ||||
| -rw-r--r-- | 9ns/docs/DESIGN.md (renamed from 9player/docs/DESIGN.md) | 64 | ||||
| -rw-r--r-- | 9ns/src/bridge.zig (renamed from 9player/src/bridge.zig) | 4 | ||||
| -rw-r--r-- | 9ns/src/fuse.zig (renamed from 9player/src/fuse.zig) | 2 | ||||
| -rw-r--r-- | 9ns/src/main.zig (renamed from 9player/src/main.zig) | 68 | ||||
| -rw-r--r-- | 9ns/src/nine.zig (renamed from 9player/src/nine.zig) | 0 | ||||
| -rw-r--r-- | 9ns/src/ns.zig (renamed from 9player/src/ns.zig) | 84 | ||||
| -rwxr-xr-x | 9ns/test/adv_bridge_hostile.py (renamed from 9player/test/adv_bridge_hostile.py) | 2 | ||||
| -rwxr-xr-x | 9ns/test/adv_bridge_hostile.sh (renamed from 9player/test/adv_bridge_hostile.sh) | 34 | ||||
| -rwxr-xr-x | 9ns/test/adv_bridge_semantics.sh (renamed from 9player/test/adv_bridge_semantics.sh) | 16 | ||||
| -rwxr-xr-x | 9ns/test/adv_bridge_stress.sh (renamed from 9player/test/adv_bridge_stress.sh) | 14 | ||||
| -rwxr-xr-x | 9ns/test/adv_ns_process.sh (renamed from 9player/test/adv_ns_process.sh) | 74 | ||||
| -rwxr-xr-x | 9ns/test/adversarial.sh | 15 | ||||
| -rwxr-xr-x | 9ns/test/integration.sh (renamed from 9player/test/integration.sh) | 44 | ||||
| -rwxr-xr-x | 9player/test/adversarial.sh | 15 | ||||
| -rw-r--r-- | 9proc/README.md (renamed from introspect/README.md) | 70 | ||||
| -rw-r--r-- | 9proc/build.zig (renamed from introspect/build.zig) | 92 | ||||
| -rw-r--r-- | 9proc/demo/main.zig (renamed from introspect/demo/main.zig) | 46 | ||||
| -rw-r--r-- | 9proc/docs/LIBRARY.md (renamed from introspect/docs/LIBRARY.md) | 48 | ||||
| -rw-r--r-- | 9proc/src/core.zig (renamed from introspect/src/core.zig) | 2 | ||||
| -rw-r--r-- | 9proc/src/freestanding_check.zig (renamed from introspect/src/freestanding_check.zig) | 6 | ||||
| -rw-r--r-- | 9proc/src/linux/debug.zig (renamed from introspect/src/linux/debug.zig) | 2 | ||||
| -rw-r--r-- | 9proc/src/linux/probe.zig (renamed from introspect/src/linux/probe.zig) | 6 | ||||
| -rw-r--r-- | 9proc/src/linux/provider.zig (renamed from introspect/src/linux/provider.zig) | 0 | ||||
| -rw-r--r-- | 9proc/src/linux/runtime.zig (renamed from introspect/src/linux/runtime.zig) | 0 | ||||
| -rw-r--r-- | 9proc/src/root.zig (renamed from introspect/src/root.zig) | 2 | ||||
| -rw-r--r-- | 9proc/src/scratch.zig (renamed from introspect/src/scratch.zig) | 0 | ||||
| -rw-r--r-- | 9proc/src/vars.zig (renamed from introspect/src/vars.zig) | 0 | ||||
| -rwxr-xr-x | 9proc/test/adv_9proc_hostile.py (renamed from introspect/test/adv_introspect_hostile.py) | 6 | ||||
| -rwxr-xr-x | 9proc/test/adv_9proc_hostile.sh | 10 | ||||
| -rwxr-xr-x | 9proc/test/adv_core_hostile.py (renamed from introspect/test/adv_core_hostile.py) | 10 | ||||
| -rwxr-xr-x | 9proc/test/adv_core_hostile.sh | 10 | ||||
| -rwxr-xr-x | 9proc/test/adv_linux_probe.py (renamed from introspect/test/adv_linux_probe.py) | 34 | ||||
| -rwxr-xr-x | 9proc/test/adv_linux_probe.sh | 10 | ||||
| -rwxr-xr-x | 9proc/test/adversarial.sh | 19 | ||||
| -rwxr-xr-x | 9proc/test/debug.sh (renamed from introspect/test/debug.sh) | 12 | ||||
| -rw-r--r-- | README.md | 39 | ||||
| -rw-r--r-- | build.zig | 48 | ||||
| -rw-r--r-- | build.zig.zon | 2 | ||||
| -rw-r--r-- | docs/design.md | 9 | ||||
| -rw-r--r-- | docs/http.md | 8 | ||||
| -rw-r--r-- | docs/validation.md | 2 | ||||
| -rwxr-xr-x | introspect/test/adv_core_hostile.sh | 10 | ||||
| -rwxr-xr-x | introspect/test/adv_introspect_hostile.sh | 10 | ||||
| -rwxr-xr-x | introspect/test/adv_linux_probe.sh | 10 | ||||
| -rwxr-xr-x | introspect/test/adversarial.sh | 19 | ||||
| -rw-r--r-- | test/web/e2e.mjs | 4 | ||||
| -rw-r--r-- | web/client.zig (renamed from app/client.zig) | 0 | ||||
| -rw-r--r-- | web/main.zig (renamed from app/main.zig) | 16 | ||||
| -rw-r--r-- | web/static/app.mjs (renamed from app/web/app.mjs) | 0 | ||||
| -rw-r--r-- | web/static/client.mjs (renamed from app/web/client.mjs) | 0 | ||||
| -rw-r--r-- | web/static/index.html (renamed from app/web/index.html) | 0 | ||||
| -rw-r--r-- | web/static/style.css (renamed from app/web/style.css) | 0 |
54 files changed, 564 insertions, 558 deletions
diff --git a/9player/README.md b/9ns/README.md index 77081d6..04edffb 100644 --- a/9player/README.md +++ b/9ns/README.md @@ -1,28 +1,28 @@ -# 9player +# 9ns Mount a 9P2000 file tree into a fresh mount namespace and run a program in it, as a plain user, without touching the host's mount table. ```sh -9player --unix /run/user/1000/acme -- fish # a shell that sees the tree at /mnt/9p -9player --tcp 127.0.0.1:564 -- claude # an agent that sees it too -9player --spawn 'introspect --stdio' -- bash # start the server yourself, talk over a socketpair +9ns --unix /run/user/1000/acme -- fish # a shell that sees the tree at /mnt/9p +9ns --tcp 127.0.0.1:564 -- claude # an agent that sees it too +9ns --spawn '9proc-demo --stdio' -- bash # start the server yourself, talk over a socketpair ``` Inside, the tree is ordinary files: `ls`, `cat`, `echo x > ctl`, editors, -`find`, `rsync`, whatever. `$NINEPLAYER_MOUNT` tells programs where it is -(default `/mnt/9p`). When the program exits, 9player exits with its status +`find`, `rsync`, whatever. `$NINE_MOUNT` tells programs where it is +(default `/mnt/9p`). When the program exits, 9ns exits with its status and the namespace, mount and connection disappear. ## How it works The kernel's own `9p` filesystem cannot be mounted inside an unprivileged user -namespace, so 9player is a small FUSE server that speaks 9P2000 to the real +namespace, so 9ns is a small FUSE server that speaks 9P2000 to the real server. There is no libfuse and no libc: `src/fuse.zig` implements the subset of the kernel FUSE protocol needed, straight from `linux/fuse.h`. ``` - program (fish/bash/claude) 9player (parent) 9P server + program (fish/bash/claude) 9ns (parent) 9P server in a new user+mount namespace │ /mnt/9p ── FUSE ──▶ kernel ────▶│ fuse.zig ─▶ bridge.zig ─▶ nine.zig ──▶ unix / tcp / socketpair │ (framing) (translation) (cloud9 Client) @@ -43,33 +43,33 @@ Files are opened with `FOPEN_DIRECT_IO`, so synthetic files that report length travels inside the 9P open mode (`OTRUNC`) rather than as a separate truncate. Repeated lookups of the same qid map to the same inode. -If `/mnt/9p` does not exist and cannot be created (the normal case), 9player +If `/mnt/9p` does not exist and cannot be created (the normal case), 9ns mounts a tmpfs over `/mnt` *inside the namespace only* and bind-mounts every existing entry of `/mnt` back into it, so nothing is hidden. Pass `--mount DIR` to use any other directory. ## Building and testing -9player lives in the [cloud9](../) repository as `cloud9/9player/`, beside +9ns lives in the [cloud9](../) repository as `cloud9/9ns/`, beside the 9P2000 protocol library it is built on, and is wired into cloud9's -`build.zig` through the fragment `9player/build.zig`. Everything is run from +`build.zig` through the fragment `9ns/build.zig`. Everything is run from the cloud9 root with Zig 0.16: ```sh -zig build # zig-out/bin/{9player,introspect,cloud9-http,cloud9-probe} -zig build 9player # build and install only zig-out/bin/9player -zig build 9player-test # unit tests (protocol structs, session, bridge, namespace helpers) -zig build 9player-itest # integration tests: real namespaces, real FUSE, - # introspect over unix/tcp/socketpair, and plan9port's - # ramfs when /usr/lib/plan9/bin/ramfs is installed -zig build 9player-adv # adversarial suites: hostile 9P servers, FUSE semantics, - # namespace/signal edge cases, stress (several minutes) +zig build # zig-out/bin/{9ns,9proc-demo,9web,cloud9-probe} +zig build 9ns # build and install only zig-out/bin/9ns +zig build 9ns-test # unit tests (protocol structs, session, bridge, namespace helpers) +zig build 9ns-itest # integration tests: real namespaces, real FUSE, + # 9proc-demo over unix/tcp/socketpair, and plan9port's + # ramfs when /usr/lib/plan9/bin/ramfs is installed +zig build 9ns-adv # adversarial suites: hostile 9P servers, FUSE semantics, + # namespace/signal edge cases, stress (several minutes) zig build -Doptimize=ReleaseSafe -zig build -D9player=false # leave 9player out (the default on non-Linux targets) +zig build -D9ns=false # leave 9ns out (the default on non-Linux targets) ``` -The integration suites mount the `introspect` demo server (`../introspect`), -so they need `-Dintrospect=true` (the default on Linux), unprivileged user +The integration suites mount the `9proc-demo` server (`../9proc`), +so they need `-D9proc=true` (the default on Linux), unprivileged user namespaces (`kernel.unprivileged_userns_clone=1` on distributions that have the knob), `/dev/fuse` and Python 3; they skip themselves otherwise. `zig build programs-test` and `programs-itest` run the unit and integration @@ -78,7 +78,7 @@ steps of every program in the repository. ## Usage ``` -9player [options] -- PROGRAM [ARGS...] +9ns [options] -- PROGRAM [ARGS...] Transport (exactly one): --unix PATH Unix stream socket @@ -98,15 +98,15 @@ Options: ``` PROGRAM defaults to `$SHELL`. Exit status is the program's (`128+signal` if it -was killed); 125 means 9player itself failed (usage, connect, namespace, +was killed); 125 means 9ns itself failed (usage, connect, namespace, mount); 126/127 are exec failures as usual. -## introspect: a demo 9P server +## 9proc-demo: a demo 9P server -`introspect` is a single-binary 9P2000 server whose file tree is the binary +`9proc-demo` is a single-binary 9P2000 server whose file tree is the binary itself: build-time facts, `comptime` reflection and live runtime state. It is -the demo of the [introspect library](../introspect) (`../introspect/demo/main.zig`), -built and installed by `zig build introspect`. +the demo of the [9proc library](../9proc) (`../9proc/demo/main.zig`), +built and installed by `zig build 9proc`. ``` /README @@ -120,15 +120,15 @@ built and installed by `zig build introspect`. /scratch/ in-memory read/write tree ``` -`/runtime/env` exposes the server's whole environment, so serve introspect on +`/runtime/env` exposes the server's whole environment, so serve 9proc-demo on a Unix socket or loopback only. ```sh -zig-out/bin/introspect --unix /tmp/intro.sock & -zig-out/bin/9player --unix /tmp/intro.sock -- sh -c ' - cat $NINEPLAYER_MOUNT/build/zig_version; echo - cat $NINEPLAYER_MOUNT/comptime/types/Qid/fields - echo "fib 20" > $NINEPLAYER_MOUNT/runtime/ctl; cat $NINEPLAYER_MOUNT/runtime/ctl' +zig-out/bin/9proc-demo --unix /tmp/intro.sock & +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' ``` The same command with `claude -p "explore /mnt/9p ..."` as the program gives an @@ -147,10 +147,10 @@ use. ## Relation to cloud9 -9player consumes cloud9 as the module `cloud9` and keeps all mounting, +9ns consumes cloud9 as the module `cloud9` and keeps all mounting, namespace and process policy on its side, which is what cloud9's design asks of applications. It ships from the cloud9 repository as the sibling directory -`9player/` (sources in `src/`, suites in `test/`, this README and +`9ns/` (sources in `src/`, suites in `test/`, this README and `docs/DESIGN.md`) with a build fragment that the root `build.zig` enables with -`-D9player` on Linux targets; nothing in the code depends on that layout. +`-D9ns` on Linux targets; nothing in the code depends on that layout. `docs/DESIGN.md` has the full module contracts. diff --git a/9player/build.zig b/9ns/build.zig index f3d5c1b..7f2155f 100644 --- a/9player/build.zig +++ b/9ns/build.zig @@ -1,30 +1,30 @@ -//! Build fragment for 9player: mount a 9P2000 tree into a fresh mount +//! Build fragment for 9ns: mount a 9P2000 tree into a fresh mount //! namespace via FUSE and run a program in it (Linux only, no libc). It is //! `@import`ed by the root build.zig and called with the root builder, so //! every `b.path(...)` here is relative to the cloud9 root (hence the -//! `9player/` prefix), every option is defined by the root (no +//! `9ns/` prefix), every option is defined by the root (no //! `standardTargetOptions` here) and every step it registers lands in the -//! root's step list under the `9player` prefix. +//! root's step list under the `9ns` prefix. //! -//! Steps: 9player, 9player-test, 9player-itest, 9player-adv. +//! Steps: 9ns, 9ns-test, 9ns-itest, 9ns-adv. const std = @import("std"); /// What the root passes in. The root owns target/optimize resolution and the /// cloud9 module; the integration suites also need a 9P server to mount, -/// which is the introspect demo built by the sibling fragment. +/// which is the 9proc demo built by the sibling fragment. pub const Context = struct { target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, cloud9: *std.Build.Module, - /// The `introspect` demo server; null when introspect is disabled, in + /// The `9proc-demo` server; null when 9proc is disabled, in /// which case the end-to-end steps exist but fail with a notice. - introspect_demo: ?*std.Build.Step.Compile, + proc_demo: ?*std.Build.Step.Compile, }; pub const Artifacts = struct { - /// The 9player executable (also installed by the plain `zig build`). + /// The 9ns executable (also installed by the plain `zig build`). exe: *std.Build.Step.Compile, - /// `9player-test`, `9player-itest`, `9player-adv`. + /// `9ns-test`, `9ns-itest`, `9ns-adv`. test_step: *std.Build.Step, itest_step: *std.Build.Step, adv_step: *std.Build.Step, @@ -33,39 +33,39 @@ pub const Artifacts = struct { pub fn add(b: *std.Build, ctx: Context) Artifacts { const target = ctx.target; const optimize = ctx.optimize; - std.debug.assert(target.result.os.tag == .linux); // the root only enables 9player on Linux + std.debug.assert(target.result.os.tag == .linux); // the root only enables 9ns on Linux - const player_mod = b.createModule(.{ - .root_source_file = b.path("9player/src/main.zig"), + const ns_mod = b.createModule(.{ + .root_source_file = b.path("9ns/src/main.zig"), .target = target, .optimize = optimize, .imports = &.{.{ .name = "cloud9", .module = ctx.cloud9 }}, }); - const player = b.addExecutable(.{ .name = "9player", .root_module = player_mod }); - const install = b.addInstallArtifact(player, .{}); + const ns = b.addExecutable(.{ .name = "9ns", .root_module = ns_mod }); + const install = b.addInstallArtifact(ns, .{}); b.getInstallStep().dependOn(&install.step); - b.step("9player", "Build and install only the 9player binary").dependOn(&install.step); + b.step("9ns", "Build and install only the 9ns binary").dependOn(&install.step); // Unit tests: protocol structs, session, bridge, namespace helpers (main.zig // reaches every module). - const test_step = b.step("9player-test", "Run 9player's unit tests"); - test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = player_mod })).step); + const test_step = b.step("9ns-test", "Run 9ns's unit tests"); + test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = ns_mod })).step); // End-to-end suites: real namespaces, real FUSE, a real 9P server. - const itest = b.step("9player-itest", "Run 9player/test/integration.sh (needs unprivileged user namespaces and /dev/fuse)"); - const adv = b.step("9player-adv", "Run 9player/test/adversarial.sh (hostile servers, namespaces, stress; several minutes)"); - const demo = ctx.introspect_demo orelse { - const fail = b.addFail("9player-itest and 9player-adv need the introspect demo server (build with -Dintrospect=true)"); + const itest = b.step("9ns-itest", "Run 9ns/test/integration.sh (needs unprivileged user namespaces and /dev/fuse)"); + const adv = b.step("9ns-adv", "Run 9ns/test/adversarial.sh (hostile servers, namespaces, stress; several minutes)"); + const demo = ctx.proc_demo orelse { + const fail = b.addFail("9ns-itest and 9ns-adv need the 9proc-demo server (build with -D9proc=true)"); itest.dependOn(&fail.step); adv.dependOn(&fail.step); - return .{ .exe = player, .test_step = test_step, .itest_step = itest, .adv_step = adv }; + return .{ .exe = ns, .test_step = test_step, .itest_step = itest, .adv_step = adv }; }; inline for (.{ .{ itest, "integration" }, .{ adv, "adversarial" } }) |pair| { const run = b.addSystemCommand(&.{"bash"}); - run.addFileArg(b.path("9player/test/" ++ pair[1] ++ ".sh")); - run.addArtifactArg(player); + run.addFileArg(b.path("9ns/test/" ++ pair[1] ++ ".sh")); + run.addArtifactArg(ns); run.addArtifactArg(demo); pair[0].dependOn(&run.step); } - return .{ .exe = player, .test_step = test_step, .itest_step = itest, .adv_step = adv }; + return .{ .exe = ns, .test_step = test_step, .itest_step = itest, .adv_step = adv }; } diff --git a/9player/docs/DESIGN.md b/9ns/docs/DESIGN.md index b9b2fbe..7f943a8 100644 --- a/9player/docs/DESIGN.md +++ b/9ns/docs/DESIGN.md @@ -1,6 +1,6 @@ -# 9player design +# 9ns design -`9player` mounts a 9P2000 file tree served over a Unix or TCP stream socket +`9ns` mounts a 9P2000 file tree served over a Unix or TCP stream socket into a **fresh mount namespace** and runs a program inside it. The program (fish, bash, `claude`, anything) sees the 9P tree as ordinary files, without root and without touching the host's mount table. @@ -10,11 +10,11 @@ root and without touching the host's mount table. The kernel's own `9p` filesystem is not mountable inside an unprivileged user namespace (it lacks `FS_USERNS_MOUNT`) and loading it needs root. FUSE has been user-namespace mountable since Linux 4.18, and `/dev/fuse` is world read/write. -So 9player is a tiny FUSE server that speaks 9P2000 to the real server: +So 9ns is a tiny FUSE server that speaks 9P2000 to the real server: ``` - program (fish/bash/claude) 9player (parent) 9P server - in new user+mount namespace │ (introspect, + program (fish/bash/claude) 9ns (parent) 9P server + in new user+mount namespace │ (9proc-demo, /mnt/9p ─── FUSE ───▶ kernel ──▶│ fuse.zig ──▶ bridge.zig ──▶ nine.zig ──▶ ramfs, ...) │ (framing) (translation) (cloud9 Client) ``` @@ -39,23 +39,23 @@ protocol we need directly against `/usr/include/linux/fuse.h`. argv (`toSlice(allocator)`), `init.minimal.environ.block` the envp block. * No libc is linked. Do not use `std.c.*`. Hostname lookups are therefore out of scope: `--tcp` takes IP literals only. -* 9player lives in the cloud9 repository as `cloud9/9player/` and is built by - the root `build.zig` through the fragment `9player/build.zig` (steps - `9player`, `9player-test`, `9player-itest`, `9player-adv`; toggle - `-D9player`). cloud9 itself is imported as module `cloud9` +* 9ns lives in the cloud9 repository as `cloud9/9ns/` and is built by + the root `build.zig` through the fragment `9ns/build.zig` (steps + `9ns`, `9ns-test`, `9ns-itest`, `9ns-adv`; toggle + `-D9ns`). cloud9 itself is imported as module `cloud9` (`@import("cloud9")`). Read `../src/client.zig`, `Server.zig`, `wire.zig` and `../docs/design.md`. Its core is allocation-free and caller-driven: you push bytes in, take results out. The demo 9P server the tests mount is the - sibling program `../introspect` (`zig build introspect`). + sibling program `../9proc` (`zig build 9proc`). * Standalone module tests while other files are missing (from the cloud9 root): - `zig test --dep cloud9 -Mroot=9player/src/<file>.zig -Mcloud9=src/root.zig`. + `zig test --dep cloud9 -Mroot=9ns/src/<file>.zig -Mcloud9=src/root.zig`. * Format everything with `zig fmt`. ## Process model ``` -9player [options] -- PROGRAM [ARGS...] +9ns [options] -- PROGRAM [ARGS...] ``` 1. Parent parses args, probes that `/dev/fuse` exists, connects to the 9P @@ -73,13 +73,13 @@ protocol we need directly against `/usr/include/linux/fuse.h`. fuse descriptor opened from a different user namespace than the mount ("wrong user namespace for fuse device"), so this must happen here, not in the parent. - 6. `mount("9player", mountpoint, "fuse", MS_NOSUID|MS_NODEV, + 6. `mount("9ns", mountpoint, "fuse", MS_NOSUID|MS_NODEV, "fd=<fusefd>,rootmode=40000,user_id=<uid>,group_id=<gid>,max_read=<n>")`. 7. Sends the fuse fd to the parent over the status socket (`SCM_RIGHTS`). 8. `statx` of the mountpoint: this forces one GETATTR, which the parent serves. Without it the kernel keeps the root inode's initial uid 0 (unmapped in the namespace) and every create in the root gets `EACCES`. - 9. Sets `NINEPLAYER_MOUNT=<mountpoint>` in the environment. + 9. Sets `NINE_MOUNT=<mountpoint>` in the environment. 10. `execve` of PROGRAM with PATH search (implemented by hand; no libc). Exec failures are reported through the `CLOEXEC` status socket (errno + message); the parent prints them after the serve loop ends. @@ -89,7 +89,7 @@ protocol we need directly against `/usr/include/linux/fuse.h`. closes the fuse fd and exits with the child's status (`128+sig` if signalled). The self-pipe is also watched by the 9P session while a reply is outstanding (`Session.stop_fd` → `error.Stopped`), so a server that - never answers cannot keep 9player alive after the child is gone; a 3 s + never answers cannot keep 9ns alive after the child is gone; a 3 s watchdog armed from the SIGCHLD handler is the last resort. 4. Signals in the parent: `SIGINT`/`SIGQUIT` ignored (the child owns the tty and gets them itself); `SIGTERM`/`SIGHUP` forwarded to the child; @@ -161,7 +161,7 @@ pub fn body(comptime T: type, req: Request) !*const T; // aligned copy-free vie pub fn nameAfter(comptime T: type, req: Request) ![]const u8; // NUL-terminated name after a struct ``` -The request buffer must be ≥ `max_write + 4096`; 9player uses 1 MiB + 4 KiB. +The request buffer must be ≥ `max_write + 4096`; 9ns uses 1 MiB + 4 KiB. Requests with an unknown/unsupported opcode get `ENOSYS`. ### `src/nine.zig` — synchronous 9P2000 session on a blocking fd @@ -286,7 +286,7 @@ Attr mapping from `cloud9.Stat`: `mode = (S_IFDIR if DMDIR else S_IFREG) | Errors: `nine.Session.Error.Nine` → `nine.errno()`; `Closed`/`Protocol`/`Io` → `EIO` and, since the session is dead, `serve` returns `error.Closed` after -replying so 9player can report "9P server went away". +replying so 9ns can report "9P server went away". With `direct_io` the kernel never trusts `length` for reads: synthetic files that report length 0 (very common in 9P) still `cat` correctly, and reads run @@ -329,7 +329,7 @@ read end (used as `stop_fd` for `bridge.serve`), and ### `src/main.zig` — CLI ``` -Usage: 9player [options] -- PROGRAM [ARGS...] +Usage: 9ns [options] -- PROGRAM [ARGS...] Transport (exactly one): --unix PATH Unix stream socket --tcp IP:PORT TCP (IPv4/IPv6 literal) @@ -344,16 +344,16 @@ Options: --no-direct-io let the kernel cache file pages (trusts stat length) --debug trace FUSE and 9P operations on stderr --help, --version -PROGRAM defaults to $SHELL (else /bin/sh). The mountpoint is exported as $NINEPLAYER_MOUNT. +PROGRAM defaults to $SHELL (else /bin/sh). The mountpoint is exported as $NINE_MOUNT. ``` -Exit codes: child's status; 125 for 9player's own failures (usage, connect, +Exit codes: child's status; 125 for 9ns's own failures (usage, connect, mount); 126/127 as usual for exec failures. -### `../introspect/demo/main.zig` — demo 9P2000 server (binary `introspect`) +### `../9proc/demo/main.zig` — demo 9P2000 server (binary `9proc-demo`) The demo server is a separate program in this repository, built on the -introspect library; see `../introspect/docs/LIBRARY.md` for the library +9proc library; see `../9proc/docs/LIBRARY.md` for the library contract (freestanding core, value renderers, Linux debug probe). The tree it serves keeps the paths the integration tests read (`/build/*`, `/comptime/types/<T>/*`, `/comptime/decls`, `/runtime/fn/*`, `/runtime/ctl`, `/runtime/{pid,ppid,uptime,argv,cwd,env,clients}`, @@ -362,38 +362,38 @@ contract (freestanding core, value renderers, Linux debug probe). The tree it se ## Integration test plan (`test/integration.sh`) -Run by `zig build 9player-itest`; args: path to `9player`, path to `introspect`. +Run by `zig build 9ns-itest`; args: path to `9ns`, path to `9proc-demo`. Everything under a temp dir. Skips (exit 0 with a notice) when `unshare -Urm true` fails or `/dev/fuse` is missing. -1. introspect on a Unix socket; `9player --unix … -- sh -c` scripts: +1. 9proc-demo on a Unix socket; `9ns --unix … -- sh -c` scripts: `cat /mnt/9p/build/zig_version` == `zig version`; `ls` listings; `stat` sizes; `/runtime/fn/now` is numeric; `ctl` round trip; `/scratch`: create, append (`>>`), overwrite, truncate, `mkdir -p a/b/c`, rename within dir, `mv` across dirs fails with `EXDEV`-ish message, `rm`, `rmdir`, 1 MiB random file round trip compared with `sha256sum`, `dd` with odd block sizes, many small files, `find`, exit-status propagation (`exit 7` → 7), - `$NINEPLAYER_MOUNT` set, nested `9player` inside `9player`. -2. `--spawn "<introspect> --stdio"` variant. + `$NINE_MOUNT` set, nested `9ns` inside `9ns`. +2. `--spawn "<9proc-demo> --stdio"` variant. 3. `--tcp 127.0.0.1:<port>` variant. 4. If `/usr/lib/plan9/bin/ramfs` exists: `NAMESPACE=$tmp ramfs -s ramfs` creates `$tmp/ramfs`; run the scratch battery against it. 5. `--mount` with an existing dir, with a relative path, and the default `/mnt/9p` (exercises parent shadowing; verify `/mnt`'s other entries are still visible inside). -6. Kill tests: 9player exits when the child exits; server death during use +6. Kill tests: 9ns exits when the child exits; server death during use yields `EIO`, not a hang. ## Verification -`zig build 9player-test` (unit), `zig build 9player-itest` (74 end-to-end -checks against introspect over unix/tcp/socketpair and against plan9port's -`ramfs`) and `zig build 9player-adv` (adversarial suites: a scriptable +`zig build 9ns-test` (unit), `zig build 9ns-itest` (74 end-to-end +checks against 9proc-demo over unix/tcp/socketpair and against plan9port's +`ramfs`) and `zig build 9ns-adv` (adversarial suites: a scriptable hostile 9P server with ~30 misbehaviour modes, FUSE semantics through the bridge, process/namespace/signal edge cases with 51 checks, and stress). The -suites that attack the introspect server itself (a hostile raw-9P client with +suites that attack the 9proc server itself (a hostile raw-9P client with 181 checks, the core, the Linux layer) moved with it to -`../introspect/test` (`zig build introspect-adv`). All pass in Debug and +`../9proc/test` (`zig build 9proc-adv`). All pass in Debug and ReleaseSafe. ## Out of scope for v1 (documented, not hidden) diff --git a/9player/src/bridge.zig b/9ns/src/bridge.zig index 387e184..3a072ec 100644 --- a/9player/src/bridge.zig +++ b/9ns/src/bridge.zig @@ -169,7 +169,7 @@ const Bridge = struct { } fn trace(b: *const Bridge, comptime fmt: []const u8, args: anytype) void { - if (b.opts.debug) std.debug.print("9player: " ++ fmt ++ "\n", args); + if (b.opts.debug) std.debug.print("9ns: " ++ fmt ++ "\n", args); } // -- dispatch -------------------------------------------------------------------- @@ -519,7 +519,7 @@ const Bridge = struct { return b.wstat(src, st); } var park_buf: [48]u8 = undefined; - const park = std.fmt.bufPrint(&park_buf, ".9player-rename-{x}", .{randomU64()}) catch unreachable; + const park = std.fmt.bufPrint(&park_buf, ".9ns-rename-{x}", .{randomU64()}) catch unreachable; b.trace(" rename target exists; parking it as {s} and retrying", .{park}); var pst = nine.dontcare; pst.name = park; diff --git a/9player/src/fuse.zig b/9ns/src/fuse.zig index ef11873..682b3d5 100644 --- a/9player/src/fuse.zig +++ b/9ns/src/fuse.zig @@ -2,7 +2,7 @@ //! //! Extern structs mirror `/usr/include/linux/fuse.h` (kernel header 7.45); //! every layout is checked against the header's size at comptime. Only the -//! opcodes and structs 9player needs are here. The I/O helpers are blocking +//! opcodes and structs 9ns needs are here. The I/O helpers are blocking //! and allocation-free: the caller owns a single request buffer. //! //! Wire rules worth remembering: diff --git a/9player/src/main.zig b/9ns/src/main.zig index 24990b9..26bd699 100644 --- a/9player/src/main.zig +++ b/9ns/src/main.zig @@ -1,7 +1,7 @@ -//! 9player: mount a 9P2000 tree into a fresh user+mount namespace via FUSE +//! 9ns: mount a 9P2000 tree into a fresh user+mount namespace via FUSE //! and run a program inside it. //! -//! Exit codes: the child's status (128+sig if signalled); 125 for 9player's +//! Exit codes: the child's status (128+sig if signalled); 125 for 9ns's //! own failures (usage, connect, attach, namespace/mount); 126/127 for exec //! failures. @@ -11,10 +11,10 @@ const ns = @import("ns.zig"); const nine = @import("nine.zig"); const bridge = @import("bridge.zig"); -const version_string = "9player 0.1.0"; +const version_string = "9ns 0.1.0"; const usage_text = - \\Usage: 9player [options] -- PROGRAM [ARGS...] + \\Usage: 9ns [options] -- PROGRAM [ARGS...] \\Transport (exactly one): \\ --unix PATH Unix stream socket \\ --tcp IP:PORT TCP (IPv4/IPv6 literal) @@ -29,7 +29,7 @@ const usage_text = \\ --no-direct-io let the kernel cache file pages (trusts stat length) \\ --debug trace FUSE and 9P operations on stderr \\ --help, --version - \\PROGRAM defaults to $SHELL (else /bin/sh). The mountpoint is exported as $NINEPLAYER_MOUNT. + \\PROGRAM defaults to $SHELL (else /bin/sh). The mountpoint is exported as $NINE_MOUNT. \\ ; @@ -77,7 +77,7 @@ const ParseResult = union(enum) { }; fn usageError(comptime fmt: []const u8, args: anytype) ParseResult { - std.debug.print("9player: " ++ fmt ++ "\n(try 9player --help)\n", args); + std.debug.print("9ns: " ++ fmt ++ "\n(try 9ns --help)\n", args); return .{ .exit = own_failure }; } @@ -192,7 +192,7 @@ fn spawnServer(cmd: [:0]const u8, envp: [*:null]const ?[*:0]const u8) !Server { switch (linux.errno(linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0, &sv))) { .SUCCESS => {}, else => |e| { - std.debug.print("9player: socketpair: E{t}\n", .{e}); + std.debug.print("9ns: socketpair: E{t}\n", .{e}); return error.SystemResources; }, } @@ -202,7 +202,7 @@ fn spawnServer(cmd: [:0]const u8, envp: [*:null]const ?[*:0]const u8) !Server { else => |e| { _ = linux.close(sv[0]); _ = linux.close(sv[1]); - std.debug.print("9player: fork: E{t}\n", .{e}); + std.debug.print("9ns: fork: E{t}\n", .{e}); return error.SystemResources; }, } @@ -218,7 +218,7 @@ fn spawnServer(cmd: [:0]const u8, envp: [*:null]const ?[*:0]const u8) !Server { defaultSignal(.PIPE); const argv = [_:null]?[*:0]const u8{ "sh", "-c", cmd.ptr }; const e = linux.errno(linux.execve("/bin/sh", &argv, envp)); - std.debug.print("9player: --spawn: exec /bin/sh: E{t}\n", .{e}); + std.debug.print("9ns: --spawn: exec /bin/sh: E{t}\n", .{e}); linux.exit_group(127); } _ = linux.close(sv[1]); @@ -239,8 +239,8 @@ fn probeFuseDevice() bool { _ = linux.close(@intCast(rc)); return true; }, - .NOENT => std.debug.print("9player: /dev/fuse: ENOENT (is the fuse module loaded? try: modprobe fuse)\n", .{}), - else => |e| std.debug.print("9player: open /dev/fuse: E{t}\n", .{e}), + .NOENT => std.debug.print("9ns: /dev/fuse: ENOENT (is the fuse module loaded? try: modprobe fuse)\n", .{}), + else => |e| std.debug.print("9ns: open /dev/fuse: E{t}\n", .{e}), } return false; } @@ -262,7 +262,7 @@ fn adoptFd(fd: i32) bool { switch (linux.errno(linux.fcntl(fd, linux.F.SETFD, linux.FD_CLOEXEC))) { .SUCCESS => return true, else => |e| { - std.debug.print("9player: --fd {d}: E{t}\n", .{ fd, e }); + std.debug.print("9ns: --fd {d}: E{t}\n", .{ fd, e }); return false; }, } @@ -299,7 +299,7 @@ pub fn main(init: std.process.Init) !u8 { } const uname = cfg.uname orelse ns.getenv(envp, "USER") orelse "none"; const mountpoint = ns.resolveMountpoint(gpa, cfg.mount) catch |err| { - std.debug.print("9player: --mount {s}: {t}\n", .{ cfg.mount, err }); + std.debug.print("9ns: --mount {s}: {t}\n", .{ cfg.mount, err }); return own_failure; }; defer gpa.free(mountpoint); @@ -323,7 +323,7 @@ pub fn main(init: std.process.Init) !u8 { var addr_buf: [256]u8 = undefined; var session = nine.Session.connect(gpa, address, cfg.msize) catch |err| { - std.debug.print("9player: connect to {s}: {t}\n", .{ describeAddress(address, &addr_buf), err }); + std.debug.print("9ns: connect to {s}: {t}\n", .{ describeAddress(address, &addr_buf), err }); stopServer(server); return own_failure; }; @@ -332,12 +332,12 @@ pub fn main(init: std.process.Init) !u8 { _ = session.attach(0, uname, cfg.aname) catch |err| { switch (err) { - error.Nine => std.debug.print("9player: attach (uname={s}, aname='{s}'): {s}\n", .{ uname, cfg.aname, session.ename[0..session.ename_len] }), - else => std.debug.print("9player: attach: {t}\n", .{err}), + error.Nine => std.debug.print("9ns: attach (uname={s}, aname='{s}'): {s}\n", .{ uname, cfg.aname, session.ename[0..session.ename_len] }), + else => std.debug.print("9ns: attach: {t}\n", .{err}), } return own_failure; }; - if (cfg.debug) std.debug.print("9player: attached to {s} (msize {d}), mounting on {s}\n", .{ describeAddress(address, &addr_buf), session.msize, mountpoint }); + if (cfg.debug) std.debug.print("9ns: attached to {s} (msize {d}), mounting on {s}\n", .{ describeAddress(address, &addr_buf), session.msize, mountpoint }); var child_pid: i32 = 0; const stop_fd = ns.installSignals(&child_pid) catch return own_failure; @@ -360,8 +360,8 @@ pub fn main(init: std.process.Init) !u8 { .direct_io = cfg.direct_io, .debug = cfg.debug, }) catch |err| switch (err) { - error.Closed => std.debug.print("9player: 9P server connection closed\n", .{}), - else => std.debug.print("9player: fuse: {t}\n", .{err}), + error.Closed => std.debug.print("9ns: 9P server connection closed\n", .{}), + else => std.debug.print("9ns: fuse: {t}\n", .{err}), }; // Closing the device aborts the FUSE connection: anything still using @@ -390,7 +390,7 @@ test "parseTcp" { test "parseArgs" { const arena = std.testing.allocator; { - const args = [_][:0]const u8{ "9player", "--unix", "/s", "--cache", "0.5", "--msize=8192", "--no-direct-io", "--", "sh", "-c", "x" }; + const args = [_][:0]const u8{ "9ns", "--unix", "/s", "--cache", "0.5", "--msize=8192", "--no-direct-io", "--", "sh", "-c", "x" }; const r = try parseArgs(arena, &args); defer arena.free(r.run.program); try std.testing.expectEqualStrings("/s", r.run.address.?.unix); @@ -401,7 +401,7 @@ test "parseArgs" { try std.testing.expectEqualStrings("x", r.run.program[2]); } { - const args = [_][:0]const u8{ "9player", "--fd", "3" }; + const args = [_][:0]const u8{ "9ns", "--fd", "3" }; const r = try parseArgs(arena, &args); try std.testing.expectEqual(@as(i32, 3), r.run.address.?.fd); try std.testing.expectEqual(@as(usize, 0), r.run.program.len); @@ -409,33 +409,33 @@ test "parseArgs" { } { // Two transports, no transport, unknown option, missing value: all 125. - const two = [_][:0]const u8{ "9player", "--fd", "3", "--unix", "/s" }; + const two = [_][:0]const u8{ "9ns", "--fd", "3", "--unix", "/s" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &two)).exit); - const none = [_][:0]const u8{ "9player", "--", "sh" }; + const none = [_][:0]const u8{ "9ns", "--", "sh" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &none)).exit); - const unknown = [_][:0]const u8{ "9player", "--bogus" }; + const unknown = [_][:0]const u8{ "9ns", "--bogus" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &unknown)).exit); - const missing = [_][:0]const u8{ "9player", "--unix" }; + const missing = [_][:0]const u8{ "9ns", "--unix" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &missing)).exit); - const badcache = [_][:0]const u8{ "9player", "--fd", "3", "--cache", "abc" }; + const badcache = [_][:0]const u8{ "9ns", "--fd", "3", "--cache", "abc" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &badcache)).exit); // Empty values, a single-dash typo, and an msize that would allocate gigabytes. - const emptyunix = [_][:0]const u8{ "9player", "--unix=", "--", "sh" }; + const emptyunix = [_][:0]const u8{ "9ns", "--unix=", "--", "sh" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &emptyunix)).exit); - const emptymount = [_][:0]const u8{ "9player", "--fd", "3", "--mount", "" }; + const emptymount = [_][:0]const u8{ "9ns", "--fd", "3", "--mount", "" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &emptymount)).exit); - const singledash = [_][:0]const u8{ "9player", "--fd", "3", "-mount", "/x" }; + const singledash = [_][:0]const u8{ "9ns", "--fd", "3", "-mount", "/x" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &singledash)).exit); - const hugemsize = [_][:0]const u8{ "9player", "--fd", "3", "--msize", "4294967295" }; + const hugemsize = [_][:0]const u8{ "9ns", "--fd", "3", "--msize", "4294967295" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &hugemsize)).exit); - const okmsize = [_][:0]const u8{ "9player", "--fd", "3", "--msize", "16777216" }; + const okmsize = [_][:0]const u8{ "9ns", "--fd", "3", "--msize", "16777216" }; try std.testing.expectEqual(@as(u32, 16777216), (try parseArgs(arena, &okmsize)).run.msize); } { - const ver = [_][:0]const u8{ "9player", "--version" }; + const ver = [_][:0]const u8{ "9ns", "--version" }; try std.testing.expectEqualStrings(version_string ++ "\n", (try parseArgs(arena, &ver)).info); - const help = [_][:0]const u8{ "9player", "--help" }; - try std.testing.expect(std.mem.startsWith(u8, (try parseArgs(arena, &help)).info, "Usage: 9player")); + const help = [_][:0]const u8{ "9ns", "--help" }; + try std.testing.expect(std.mem.startsWith(u8, (try parseArgs(arena, &help)).info, "Usage: 9ns")); } } diff --git a/9player/src/nine.zig b/9ns/src/nine.zig index 70633e6..70633e6 100644 --- a/9player/src/nine.zig +++ b/9ns/src/nine.zig diff --git a/9player/src/ns.zig b/9ns/src/ns.zig index 2c6f202..6da2c7f 100644 --- a/9player/src/ns.zig +++ b/9ns/src/ns.zig @@ -1,4 +1,4 @@ -//! Namespace and process plumbing for 9player. +//! Namespace and process plumbing for 9ns. //! //! Everything here is raw `std.os.linux` syscalls (no libc). The child side //! of `spawn` runs between `fork` and `execve`; it does not allocate except @@ -18,7 +18,7 @@ const E = linux.E; pub const Spawn = struct { /// argv[0] is PATH-searched unless it contains '/'. argv: []const []const u8, - /// Inherited environment; `NINEPLAYER_MOUNT` is added or replaced. + /// Inherited environment; `NINE_MOUNT` is added or replaced. envp: [*:null]const ?[*:0]const u8, /// Absolute mountpoint (see `resolveMountpoint`). mountpoint: []const u8, @@ -43,7 +43,7 @@ pub const Child = struct { status_fd: i32, }; -/// Exit status used by the child for setup failures (matches 9player's own). +/// Exit status used by the child for setup failures (matches 9ns's own). pub const setup_failure_status: u8 = 125; /// Refuse to shadow a directory with more entries than this. pub const max_shadow_entries: usize = 4096; @@ -66,7 +66,7 @@ pub fn resolveMountpoint(gpa: Allocator, path: []const u8) ![:0]u8 { switch (linux.errno(rc)) { .SUCCESS => {}, else => |e| { - std.debug.print("9player: getcwd: E{t}\n", .{e}); + std.debug.print("9ns: getcwd: E{t}\n", .{e}); return error.Cwd; }, } @@ -162,10 +162,10 @@ pub fn findInPath(gpa: Allocator, envp: [*:null]const ?[*:0]const u8, name: []co return error.FileNotFound; } -/// New envp block: every entry of `envp` except `NINEPLAYER_MOUNT=...`, -/// followed by `NINEPLAYER_MOUNT=<mountpoint>`. +/// New envp block: every entry of `envp` except `NINE_MOUNT=...`, +/// followed by `NINE_MOUNT=<mountpoint>`. fn buildEnvp(gpa: Allocator, envp: [*:null]const ?[*:0]const u8, mountpoint: []const u8) ![:null]?[*:0]const u8 { - const key = "NINEPLAYER_MOUNT="; + const key = "NINE_MOUNT="; var keep: usize = 0; var i: usize = 0; while (envp[i]) |entry| : (i += 1) { @@ -203,17 +203,17 @@ fn buildArgv(gpa: Allocator, argv: []const []const u8) ![:null]?[*:0]const u8 { /// directories and files, recreated symlinks) and `mkdir` inside it; /// * anything else fails with the errno and a hint. /// -/// Every failure prints `9player: <step> <path>: E<errno>` to stderr before +/// Every failure prints `9ns: <step> <path>: E<errno>` to stderr before /// returning. Meant to be called in the child of `spawn` (or from a /// throwaway namespace: `unshare -Urm`). pub fn ensureMountpoint(gpa: Allocator, path: [:0]const u8) !void { if (fileType(linux.AT.FDCWD, path, false)) |ft| { if (ft == .dir) return; - std.debug.print("9player: mountpoint {s}: exists but is not a directory\n", .{path}); + std.debug.print("9ns: mountpoint {s}: exists but is not a directory\n", .{path}); return error.Mountpoint; } if (fileType(linux.AT.FDCWD, path, true) == .symlink) { - std.debug.print("9player: mountpoint {s}: dangling symlink\n", .{path}); + std.debug.print("9ns: mountpoint {s}: dangling symlink\n", .{path}); return error.Mountpoint; } const mk = linux.errno(linux.mkdirat(linux.AT.FDCWD, path, 0o755)); @@ -221,19 +221,19 @@ pub fn ensureMountpoint(gpa: Allocator, path: [:0]const u8) !void { .SUCCESS => return, .ACCES, .PERM, .ROFS => {}, else => |e| { - std.debug.print("9player: mkdir {s}: E{t} (pass --mount an existing directory)\n", .{ path, e }); + std.debug.print("9ns: mkdir {s}: E{t} (pass --mount an existing directory)\n", .{ path, e }); return error.Mountpoint; }, } const parent = std.fs.path.dirname(path) orelse "/"; if (std.mem.eql(u8, parent, "/") or isSameDirectory(parent, "/")) { - std.debug.print("9player: mkdir {s}: E{t}; refusing to shadow / (pass --mount an existing directory)\n", .{ path, mk }); + std.debug.print("9ns: mkdir {s}: E{t}; refusing to shadow / (pass --mount an existing directory)\n", .{ path, mk }); return error.Mountpoint; } // The shadow rebuilds entries from /proc/self/fd/<fd>/<name>; a tmpfs // over /proc (or a subtree of it) would take that away from itself. if (std.mem.eql(u8, parent, "/proc") or std.mem.startsWith(u8, parent, "/proc/")) { - std.debug.print("9player: mkdir {s}: E{t}; refusing to shadow {s} (pass --mount an existing directory)\n", .{ path, mk, parent }); + std.debug.print("9ns: mkdir {s}: E{t}; refusing to shadow {s} (pass --mount an existing directory)\n", .{ path, mk, parent }); return error.Mountpoint; } const parent_z = try gpa.dupeZ(u8, parent); @@ -242,7 +242,7 @@ pub fn ensureMountpoint(gpa: Allocator, path: [:0]const u8) !void { switch (linux.errno(linux.mkdirat(linux.AT.FDCWD, path, 0o755))) { .SUCCESS => {}, else => |e| { - std.debug.print("9player: mkdir {s} (in shadow tmpfs): E{t}\n", .{ path, e }); + std.debug.print("9ns: mkdir {s} (in shadow tmpfs): E{t}\n", .{ path, e }); return error.Mountpoint; }, } @@ -289,7 +289,7 @@ fn listDir(gpa: Allocator, fd: i32, dirpath: []const u8) ![]Entry { switch (linux.errno(rc)) { .SUCCESS => {}, else => |e| { - std.debug.print("9player: getdents64 {s}: E{t}\n", .{ dirpath, e }); + std.debug.print("9ns: getdents64 {s}: E{t}\n", .{ dirpath, e }); return error.Mountpoint; }, } @@ -303,7 +303,7 @@ fn listDir(gpa: Allocator, fd: i32, dirpath: []const u8) ![]Entry { off += d.reclen; if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) continue; if (list.items.len >= max_shadow_entries) { - std.debug.print("9player: refusing to shadow {s}: more than {d} entries\n", .{ dirpath, max_shadow_entries }); + std.debug.print("9ns: refusing to shadow {s}: more than {d} entries\n", .{ dirpath, max_shadow_entries }); return error.TooManyEntries; } const kind: FileType = switch (dtype) { @@ -323,7 +323,7 @@ fn shadowDirectory(gpa: Allocator, parent: [:0]const u8) !void { switch (linux.errno(open_rc)) { .SUCCESS => {}, else => |e| { - std.debug.print("9player: open {s}: E{t}\n", .{ parent, e }); + std.debug.print("9ns: open {s}: E{t}\n", .{ parent, e }); return error.Mountpoint; }, } @@ -340,7 +340,7 @@ fn shadowDirectory(gpa: Allocator, parent: [:0]const u8) !void { switch (linux.errno(linux.mount("tmpfs", parent, "tmpfs", linux.MS.NOSUID | linux.MS.NODEV, @intFromPtr(tmpfs_opts)))) { .SUCCESS => {}, else => |e| { - std.debug.print("9player: mount tmpfs on {s}: E{t}\n", .{ parent, e }); + std.debug.print("9ns: mount tmpfs on {s}: E{t}\n", .{ parent, e }); return error.Mountpoint; }, } @@ -352,11 +352,11 @@ fn shadowDirectory(gpa: Allocator, parent: [:0]const u8) !void { var link_buf: [path_max]u8 = undefined; for (entries) |e| { const src = std.fmt.bufPrintZ(&src_buf, "/proc/self/fd/{d}/{s}", .{ pfd, e.name }) catch { - std.debug.print("9player: shadow {s}/{s}: name too long (skipped)\n", .{ parent, e.name }); + std.debug.print("9ns: shadow {s}/{s}: name too long (skipped)\n", .{ parent, e.name }); continue; }; const dst = std.fmt.bufPrintZ(&dst_buf, "{s}/{s}", .{ parent, e.name }) catch { - std.debug.print("9player: shadow {s}/{s}: name too long (skipped)\n", .{ parent, e.name }); + std.debug.print("9ns: shadow {s}/{s}: name too long (skipped)\n", .{ parent, e.name }); continue; }; switch (e.kind) { @@ -387,7 +387,7 @@ fn check(step: []const u8, path: [*:0]const u8, rc: usize) bool { switch (linux.errno(rc)) { .SUCCESS => return true, else => |e| { - std.debug.print("9player: shadow: {s} {s}: E{t} (skipped)\n", .{ step, std.mem.span(path), e }); + std.debug.print("9ns: shadow: {s} {s}: E{t} (skipped)\n", .{ step, std.mem.span(path), e }); return false; }, } @@ -424,7 +424,7 @@ const ok_byte: u8 = 0; /// The child then stats the mountpoint, which makes the kernel fetch the /// root's attributes once the parent serves (the kernel seeds the fuse root /// with uid 0, unmapped in the new user namespace, so nothing could be -/// created in the root until then), sets `NINEPLAYER_MOUNT` and execs +/// created in the root until then), sets `NINE_MOUNT` and execs /// `argv`. An exec failure is reported on `Child.status_fd` and ends the /// child with 126/127; collect it with `reportExecFailure` after /// `bridge.serve` returns. @@ -433,7 +433,7 @@ const ok_byte: u8 = 0; /// variable as soon as fork returns so no SIGCHLD can be missed. pub fn spawn(gpa: Allocator, s: Spawn) !Child { if (s.argv.len == 0 or s.argv[0].len == 0) { - std.debug.print("9player: empty program name\n", .{}); + std.debug.print("9ns: empty program name\n", .{}); return error.EmptyProgramName; } @@ -452,7 +452,7 @@ pub fn spawn(gpa: Allocator, s: Spawn) !Child { } const envp = try buildEnvp(gpa, s.envp, s.mountpoint); defer { - gpa.free(std.mem.span(envp[envp.len - 1].?)); // the NINEPLAYER_MOUNT entry we created + gpa.free(std.mem.span(envp[envp.len - 1].?)); // the NINE_MOUNT entry we created gpa.free(envp); } const candidates = try pathCandidates(gpa, s.envp, s.argv[0]); @@ -465,7 +465,7 @@ pub fn spawn(gpa: Allocator, s: Spawn) !Child { switch (linux.errno(linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0, &sv))) { .SUCCESS => {}, else => |e| { - std.debug.print("9player: socketpair: E{t}\n", .{e}); + std.debug.print("9ns: socketpair: E{t}\n", .{e}); return error.SystemResources; }, } @@ -490,7 +490,7 @@ pub fn spawn(gpa: Allocator, s: Spawn) !Child { else => |e| { _ = linux.close(sv[0]); _ = linux.close(sv[1]); - std.debug.print("9player: fork: E{t}\n", .{e}); + std.debug.print("9ns: fork: E{t}\n", .{e}); return error.SystemResources; }, } @@ -541,11 +541,11 @@ pub fn spawn(gpa: Allocator, s: Spawn) !Child { } _ = linux.close(sv[0]); if (reported) { - std.debug.print("9player: {s}\n", .{msg[0..len]}); + std.debug.print("9ns: {s}\n", .{msg[0..len]}); } else if (n == 1) { - std.debug.print("9player: child handshake failed: no fuse fd received (out of file descriptors?)\n", .{}); + std.debug.print("9ns: child handshake failed: no fuse fd received (out of file descriptors?)\n", .{}); } else { - std.debug.print("9player: child exited before reporting\n", .{}); + std.debug.print("9ns: child exited before reporting\n", .{}); } _ = waitChild(pid) catch {}; if (child_pid_ptr) |p| @atomicStore(i32, p, 0, .seq_cst); @@ -586,7 +586,7 @@ pub fn reportExecFailure(child: Child) ?u8 { len += rc; } if (len == 0) return null; - std.debug.print("9player: {s}\n", .{msg[1..len]}); + std.debug.print("9ns: {s}\n", .{msg[1..len]}); return msg[0]; } @@ -674,7 +674,7 @@ fn childMain(c: *const ChildArgs) noreturn { const fd: i32 = @intCast(rc_open); var opts_buf: [256]u8 = undefined; const opts = std.fmt.bufPrintZ(&opts_buf, "fd={d},{s}", .{ fd, c.fuse_opts_prefix }) catch unreachable; - const rc = linux.mount("9player", c.mountpoint, "fuse", linux.MS.NOSUID | linux.MS.NODEV, @intFromPtr(opts.ptr)); + const rc = linux.mount("9ns", c.mountpoint, "fuse", linux.MS.NOSUID | linux.MS.NODEV, @intFromPtr(opts.ptr)); if (linux.errno(rc) != .SUCCESS) childFail(c, setup_failure_status, "mount fuse", linux.errno(rc), true); fuse_fd = fd; } @@ -774,7 +774,7 @@ pub const exit_grace_seconds: isize = 3; /// The watched child is already dead but the serve loop has not come back /// (it is stuck in a 9P request the server never answers): a terminal -/// signal, or the watchdog armed by `onChld`, then ends 9player with the +/// signal, or the watchdog armed by `onChld`, then ends 9ns with the /// child's status instead of hanging. Nothing is lost: the mount is torn /// down when the process exits. fn bailIfChildGone() void { @@ -849,7 +849,7 @@ pub fn installSignals(child_pid: *i32) !i32 { switch (linux.errno(linux.pipe2(&fds, .{ .CLOEXEC = true, .NONBLOCK = true }))) { .SUCCESS => {}, else => |e| { - std.debug.print("9player: pipe2: E{t}\n", .{e}); + std.debug.print("9ns: pipe2: E{t}\n", .{e}); return error.SystemResources; }, } @@ -909,7 +909,7 @@ pub fn waitChild(pid: i32) !u8 { return error.NoChild; }, else => |e| { - std.debug.print("9player: waitpid: E{t}\n", .{e}); + std.debug.print("9ns: waitpid: E{t}\n", .{e}); return error.Wait; }, } @@ -936,7 +936,7 @@ pub fn reapAny(pid: i32) void { // --------------------------------------------------------------------------- // Tests (no namespaces needed; `ensureMountpoint` is exercised by -// test/integration.sh through the 9player binary) +// test/integration.sh through the 9ns binary) // --------------------------------------------------------------------------- const testing = std.testing; @@ -994,11 +994,11 @@ test "resolveMountpoint: relative resolves against the real cwd" { } test "getenv" { - const env = [_:null]?[*:0]const u8{ "PATH=/a:/b", "X=", "PATHX=no", "NINEPLAYER_MOUNT=/m" }; + const env = [_:null]?[*:0]const u8{ "PATH=/a:/b", "X=", "PATHX=no", "NINE_MOUNT=/m" }; const envp: [*:null]const ?[*:0]const u8 = &env; try testing.expectEqualStrings("/a:/b", getenv(envp, "PATH").?); try testing.expectEqualStrings("", getenv(envp, "X").?); - try testing.expectEqualStrings("/m", getenv(envp, "NINEPLAYER_MOUNT").?); + try testing.expectEqualStrings("/m", getenv(envp, "NINE_MOUNT").?); try testing.expect(getenv(envp, "NOPE") == null); try testing.expect(getenv(envp, "PAT") == null); } @@ -1048,12 +1048,12 @@ test "findInPath finds sh" { const p = try findInPath(gpa, &env, "sh"); defer gpa.free(p); try testing.expect(std.mem.endsWith(u8, p, "/sh")); - try testing.expectError(error.FileNotFound, findInPath(gpa, &env, "definitely-not-a-program-9player")); + try testing.expectError(error.FileNotFound, findInPath(gpa, &env, "definitely-not-a-program-9ns")); } -test "buildEnvp replaces NINEPLAYER_MOUNT" { +test "buildEnvp replaces NINE_MOUNT" { const gpa = testing.allocator; - const env = [_:null]?[*:0]const u8{ "A=1", "NINEPLAYER_MOUNT=/old", "B=2" }; + const env = [_:null]?[*:0]const u8{ "A=1", "NINE_MOUNT=/old", "B=2" }; const out = try buildEnvp(gpa, &env, "/mnt/9p"); defer { gpa.free(std.mem.span(out[out.len - 1].?)); @@ -1062,9 +1062,9 @@ test "buildEnvp replaces NINEPLAYER_MOUNT" { try testing.expectEqual(@as(usize, 3), out.len); try testing.expectEqualStrings("A=1", std.mem.span(out[0].?)); try testing.expectEqualStrings("B=2", std.mem.span(out[1].?)); - try testing.expectEqualStrings("NINEPLAYER_MOUNT=/mnt/9p", std.mem.span(out[2].?)); + try testing.expectEqualStrings("NINE_MOUNT=/mnt/9p", std.mem.span(out[2].?)); try testing.expect(out[3] == null); - try testing.expectEqualStrings("/mnt/9p", getenv(out.ptr, "NINEPLAYER_MOUNT").?); + try testing.expectEqualStrings("/mnt/9p", getenv(out.ptr, "NINE_MOUNT").?); } test "decodeStatus" { diff --git a/9player/test/adv_bridge_hostile.py b/9ns/test/adv_bridge_hostile.py index b842a1a..d353541 100755 --- a/9player/test/adv_bridge_hostile.py +++ b/9ns/test/adv_bridge_hostile.py @@ -11,7 +11,7 @@ Serves a tiny in-memory tree: plus create/write/remove/wstat so the scratch battery can run in `ok` mode. MODE selects one misbehaviour (see MODES below). Everything not covered by -the mode behaves normally, so 9player gets through version/attach/stat(root). +the mode behaves normally, so 9ns gets through version/attach/stat(root). """ import os import random diff --git a/9player/test/adv_bridge_hostile.sh b/9ns/test/adv_bridge_hostile.sh index f1ee292..f5ba871 100755 --- a/9player/test/adv_bridge_hostile.sh +++ b/9ns/test/adv_bridge_hostile.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash -# Hostile-server tests: 9player against 9player/test/adv_bridge_hostile.py in every -# misbehaviour mode. 9player must never crash (panic/segfault) and must turn +# Hostile-server tests: 9ns against 9ns/test/adv_bridge_hostile.py in every +# misbehaviour mode. 9ns must never crash (panic/segfault) and must turn # each misbehaviour into an errno for the child. -# Usage: bash 9player/test/adv_bridge_hostile.sh <9player> <introspect> (introspect unused; part of zig build 9player-adv) +# Usage: bash 9ns/test/adv_bridge_hostile.sh <9ns> <9proc-demo> (9proc unused; part of zig build 9ns-adv) set -u -PLAYER=$(realpath "${1:?path to 9player}") +NS=$(realpath "${1:?path to 9ns}") HERE=$(cd "$(dirname "$0")" && pwd) SRV=$HERE/adv_bridge_hostile.py -TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-adv.XXXXXX") +TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-adv.XXXXXX") M=/mnt/9p FAILED=0 PASSED=0 @@ -33,18 +33,18 @@ start_server() { # mode echo "server for $1 did not start"; cat "$TMP/$1.srv.out"; exit 1 } -# run MODE SCRIPT [extra 9player args...]: starts the server, runs 9player; sets OUT, RC, STDERR. +# run MODE SCRIPT [extra 9ns args...]: starts the server, runs 9ns; sets OUT, RC, STDERR. run() { local mode=$1 script=$2; shift 2 start_server "$mode" - OUT=$(timeout 30 "$PLAYER" --unix "$SOCK" "$@" -- sh -c "$script" 2>"$TMP/stderr") + OUT=$(timeout 30 "$NS" --unix "$SOCK" "$@" -- sh -c "$script" 2>"$TMP/stderr") RC=$? STDERR=$(cat "$TMP/stderr") } -# 9player must not die of a signal or panic. RC 124 = timeout(1) fired. +# 9ns must not die of a signal or panic. RC 124 = timeout(1) fired. no_crash() { # name - if [ "$RC" -ge 128 ] || [ "$RC" -eq 124 ]; then fail "$1: 9player exit $RC" "$STDERR"; return; fi + if [ "$RC" -ge 128 ] || [ "$RC" -eq 124 ]; then fail "$1: 9ns exit $RC" "$STDERR"; return; fi case "$STDERR" in *panic*|*"Segmentation"*|*"integer overflow"*|*"reached unreachable"*|*"index out of bounds"*) fail "$1: crash text in stderr" "$STDERR";; *) pass "$1: no crash (exit $RC)";; esac } @@ -138,34 +138,34 @@ expect_contains "qid_zero: nested file readable" "in d" "$OUT" echo "# version negotiation" run version_unknown "echo ran" -expect_eq "version_unknown: 9player refuses (125)" "125" "$RC" +expect_eq "version_unknown: 9ns refuses (125)" "125" "$RC" run msize_tiny "cat $M/f 2>&1; echo status=\$?" no_crash "msize_tiny" echo "# a server that never replies" start_server never -# SIGTERM is forwarded to the child; once the child is gone 9player must leave the +# SIGTERM is forwarded to the child; once the child is gone 9ns must leave the # pending 9P reply behind and exit even though the server stays silent. -timeout -s TERM 3 "$PLAYER" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" & +timeout -s TERM 3 "$NS" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" & TPID=$! sleep 4 if kill -0 "$TPID" 2>/dev/null; then - fail "never: SIGTERM did not end 9player while a reply was outstanding"; kill -9 "$TPID" + fail "never: SIGTERM did not end 9ns while a reply was outstanding"; kill -9 "$TPID" else - pass "never: SIGTERM ends 9player even while the server is silent" + pass "never: SIGTERM ends 9ns even while the server is silent" fi wait "$TPID" 2>/dev/null # Without a signal the mount hangs (documented v1 limitation) until the server dies. -timeout 30 "$PLAYER" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" & +timeout 30 "$NS" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" & TPID=$! sleep 1.5 if kill -0 "$TPID" 2>/dev/null; then pass "never: mount hangs while the server is silent (documented v1 limitation)" kill "$SRVPID"; wait "$SRVPID" 2>/dev/null; SRVPID= for _ in $(seq 1 50); do kill -0 "$TPID" 2>/dev/null || break; sleep 0.1; done - if kill -0 "$TPID" 2>/dev/null; then fail "never: 9player still alive after its server died"; kill -9 "$TPID"; else pass "never: killing the server unblocks 9player"; fi + if kill -0 "$TPID" 2>/dev/null; then fail "never: 9ns still alive after its server died"; kill -9 "$TPID"; else pass "never: killing the server unblocks 9ns"; fi else - wait "$TPID"; fail "never: 9player exited early ($?)" "$(cat "$TMP/never.err")" + wait "$TPID"; fail "never: 9ns exited early ($?)" "$(cat "$TMP/never.err")" fi echo "# interrupting a slow read (INTERRUPT must not confuse reply matching)" diff --git a/9player/test/adv_bridge_semantics.sh b/9ns/test/adv_bridge_semantics.sh index f13fd57..b38ce9c 100755 --- a/9player/test/adv_bridge_semantics.sh +++ b/9ns/test/adv_bridge_semantics.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# FUSE semantics through the bridge against introspect's /scratch tree. -# Usage: bash 9player/test/adv_bridge_semantics.sh <9player> <introspect> (part of zig build 9player-adv) +# FUSE semantics through the bridge against 9proc-demo's /scratch tree. +# Usage: bash 9ns/test/adv_bridge_semantics.sh <9ns> <9proc-demo> (part of zig build 9ns-adv) set -u -PLAYER=$(realpath "${1:?path to 9player}") -INTROSPECT=$(realpath "${2:?path to introspect}") -TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-sem.XXXXXX") +NS=$(realpath "${1:?path to 9ns}") +PROC=$(realpath "${2:?path to 9proc-demo}") +TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-sem.XXXXXX") M=/mnt/9p S=$M/scratch FAILED=0 @@ -20,11 +20,11 @@ expect_eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(pr expect_contains() { case "$3" in *"$2"*) pass "$1" ;; *) fail "$1" "missing: $(printf %q "$2")" "in: $(printf %q "$3")" ;; esac; } SOCK=$TMP/i.sock -"$INTROSPECT" --unix "$SOCK" >"$TMP/srv.out" 2>&1 & +"$PROC" --unix "$SOCK" >"$TMP/srv.out" 2>&1 & SRVPID=$! for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.02; done # Each run is a fresh session; state persists in the server, so tests clean up after themselves. -run() { OUT=$(timeout 120 "$PLAYER" --unix "$SOCK" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); } +run() { OUT=$(timeout 120 "$NS" --unix "$SOCK" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); } EXTRA=() py() { run "python3 - <<'PYEOF' $1 @@ -146,7 +146,7 @@ expect_eq ".. of the root and of a subdir" $'ok\n1\n1\n1\n1' "$OUT" run "cd $M && find . -type d | wc -l && find . -type f | head -1 && find $S -type f | wc -l" expect_contains "find -type works" "./README" "$OUT" run "stat -f -c '%T %S %l' $M; df -P $M | tail -1 | awk '{print \$1}'; sync -f $M && echo synced; sync && echo synced2" -expect_eq "statfs, df, syncfs, sync" $'fuse 4096 255\n9player\nsynced\nsynced2' "$OUT" +expect_eq "statfs, df, syncfs, sync" $'fuse 4096 255\n9ns\nsynced\nsynced2' "$OUT" echo "# server refusals keep their errno through the error path" run "echo x > $M/build/zig_version; a=\$?; mkdir $M/build/x 2>/dev/null; b=\$?; rm $M/README 2>/dev/null; c=\$?; rmdir $M/build 2>/dev/null; d=\$?; echo \$a\$b\$c\$d" 2>/dev/null diff --git a/9player/test/adv_bridge_stress.sh b/9ns/test/adv_bridge_stress.sh index 3d1203a..b306b28 100755 --- a/9player/test/adv_bridge_stress.sh +++ b/9ns/test/adv_bridge_stress.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# Resource and concurrency stress through the bridge against introspect. -# Usage: bash 9player/test/adv_bridge_stress.sh <9player> <introspect> (~1-2 min; part of zig build 9player-adv) +# Resource and concurrency stress through the bridge against 9proc-demo. +# Usage: bash 9ns/test/adv_bridge_stress.sh <9ns> <9proc-demo> (~1-2 min; part of zig build 9ns-adv) set -u -PLAYER=$(realpath "${1:?path to 9player}") -INTROSPECT=$(realpath "${2:?path to introspect}") -TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-stress.XXXXXX") +NS=$(realpath "${1:?path to 9ns}") +PROC=$(realpath "${2:?path to 9proc-demo}") +TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-stress.XXXXXX") M=/mnt/9p S=$M/scratch FAILED=0 @@ -18,10 +18,10 @@ fail() { FAILED=$((FAILED + 1)); echo "FAIL - $1"; shift; [ $# -gt 0 ] && printf expect_eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(printf %q "$2")" "actual: $(printf %q "$3")"; fi; } SOCK=$TMP/i.sock -"$INTROSPECT" --unix "$SOCK" >"$TMP/srv.out" 2>&1 & +"$PROC" --unix "$SOCK" >"$TMP/srv.out" 2>&1 & SRVPID=$! for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.02; done -run() { OUT=$(timeout 600 "$PLAYER" --unix "$SOCK" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); } +run() { OUT=$(timeout 600 "$NS" --unix "$SOCK" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); } echo "# 100k+ 9P operations in one session; RSS must plateau" # Each iteration: create+write+close, open+read+close, unlink, plus a failing lookup: ~15 RPCs. diff --git a/9player/test/adv_ns_process.sh b/9ns/test/adv_ns_process.sh index 4ce0ae8..db759ca 100755 --- a/9player/test/adv_ns_process.sh +++ b/9ns/test/adv_ns_process.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Adversarial regression tests for 9player/src/ns.zig and 9player/src/main.zig: process, +# Adversarial regression tests for 9ns/src/ns.zig and 9ns/src/main.zig: process, # namespace, signal and CLI handling. Real namespaces, real FUSE. -# Usage: bash 9player/test/adv_ns_process.sh <9player> <introspect> (part of zig build 9player-adv) +# Usage: bash 9ns/test/adv_ns_process.sh <9ns> <9proc-demo> (part of zig build 9ns-adv) # Exit 0 on success (or when the machine cannot run the tests), 1 on failure. set -u -PLAYER=$(realpath "${1:?path to 9player}") -INTROSPECT=$(realpath "${2:?path to introspect}") +NS=$(realpath "${1:?path to 9ns}") +PROC=$(realpath "${2:?path to 9proc-demo}") # Unix socket paths are limited to ~107 bytes; keep the temp dir short. TMP=$(mktemp -d "${TMPDIR:-/tmp}/9padv.XXXXXX") PIDS=() @@ -28,34 +28,34 @@ expect_eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(pr expect_contains() { case "$3" in *"$2"*) pass "$1" ;; *) fail "$1" "missing: $(printf %q "$2")" "in: $(printf %q "$3")" ;; esac; } SOCK=$TMP/s -"$INTROSPECT" --unix "$SOCK" & +"$PROC" --unix "$SOCK" & PIDS+=($!) for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.05; done -[ -S "$SOCK" ] || { echo "introspect did not create $SOCK"; exit 1; } +[ -S "$SOCK" ] || { echo "9proc-demo did not create $SOCK"; exit 1; } MI_BEFORE=$(grep -v " $TMP" /proc/self/mountinfo | sort) TIMEOUT=$(command -v timeout) -run() { "$TIMEOUT" 60 "$PLAYER" --unix "$SOCK" "$@"; } +run() { "$TIMEOUT" 60 "$NS" --unix "$SOCK" "$@"; } echo "# CLI" -expect_eq "--help goes to stdout, exit 0" "Usage: 9player" "$(run --help 2>/dev/null | head -1 | cut -c1-14; )" -expect_eq "--help exit code" "0" "$("$PLAYER" --help >/dev/null 2>&1; echo $?)" -expect_eq "--version on stdout" "9player" "$("$PLAYER" --version 2>/dev/null | cut -d' ' -f1)" +expect_eq "--help goes to stdout, exit 0" "Usage: 9ns" "$(run --help 2>/dev/null | head -1 | cut -c1-10; )" +expect_eq "--help exit code" "0" "$("$NS" --help >/dev/null 2>&1; echo $?)" +expect_eq "--version on stdout" "9ns" "$("$NS" --version 2>/dev/null | cut -d' ' -f1)" expect_eq "single-dash typo is a usage error, not a program" "125" "$(run -mount /x -- true 2>/dev/null; echo $?)" expect_contains "single-dash typo message" "unknown option -mount" "$(run -mount /x -- true 2>&1)" -expect_eq "--unix= empty is a usage error" "125" "$("$PLAYER" --unix= -- true 2>/dev/null; echo $?)" -expect_contains "--unix= message" "socket path" "$("$PLAYER" --unix= -- true 2>&1)" +expect_eq "--unix= empty is a usage error" "125" "$("$NS" --unix= -- true 2>/dev/null; echo $?)" +expect_contains "--unix= message" "socket path" "$("$NS" --unix= -- true 2>&1)" expect_eq "--mount '' is a usage error" "125" "$(run --mount '' -- true 2>/dev/null; echo $?)" expect_eq "--msize huge rejected" "125" "$(run --msize 4294967295 -- true 2>/dev/null; echo $?)" expect_eq "--msize 16 MiB accepted" "ok" "$(run --msize 16777216 -- sh -c 'echo ok')" expect_contains "empty program name is reported" "empty program name" "$(run -- '' 2>&1)" expect_eq "empty program name exit" "125" "$(run -- '' 2>/dev/null; echo $?)" expect_eq "empty \$SHELL falls back to /bin/sh" "0" "$(SHELL= run -- </dev/null >/dev/null 2>&1; echo $?)" -expect_eq "--fd with a closed descriptor fails early" "125" "$("$PLAYER" --fd 987 -- true 2>/dev/null; echo $?)" -expect_contains "--fd bad descriptor message" "--fd 987: EBADF" "$("$PLAYER" --fd 987 -- true 2>&1)" +expect_eq "--fd with a closed descriptor fails early" "125" "$("$NS" --fd 987 -- true 2>/dev/null; echo $?)" +expect_contains "--fd bad descriptor message" "--fd 987: EBADF" "$("$NS" --fd 987 -- true 2>&1)" echo "# exec failures" -expect_eq "not found is 127" "127" "$(run -- no-such-program-9player 2>/dev/null; echo $?)" +expect_eq "not found is 127" "127" "$(run -- no-such-program-9ns 2>/dev/null; echo $?)" expect_eq "PATH element that is a file: still 127" "127" "$(PATH=/etc/passwd run -- true 2>/dev/null; echo $?)" expect_contains "PATH element that is a file: message" "exec true: E" "$(PATH=/etc/passwd run -- true 2>&1)" printf '#!/bin/sh\necho no\n' >"$TMP/nx"; chmod 644 "$TMP/nx" @@ -64,16 +64,16 @@ mkdir -p "$TMP/p1" "$TMP/p2"; cp "$TMP/nx" "$TMP/p1/prog"; printf '#!/bin/sh\nec expect_eq "non-executable first in PATH, executable later" "right" "$(PATH=$TMP/p1:$TMP/p2 run -- prog)" expect_eq "non-executable only in PATH is 126" "126" "$(PATH=$TMP/p1 run -- prog 2>/dev/null; echo $?)" expect_eq "argv[0] preserved" "sh" "$(run -- sh -c 'echo $0')" -expect_eq "PATH unset uses default" "ok" "$(env -u PATH "$PLAYER" --unix "$SOCK" -- sh -c 'echo ok')" +expect_eq "PATH unset uses default" "ok" "$(env -u PATH "$NS" --unix "$SOCK" -- sh -c 'echo ok')" echo "# fd hygiene" -# 9player passes inherited descriptors through untouched, so compare with what a +# 9ns passes inherited descriptors through untouched, so compare with what a # plain child of this script sees (the runner may itself hold extra fds). FD_LIST='ls /proc/self/fd | grep -v "^3$" | sort -n | tr "\n" " " | sed "s/ $//"' FD_BASE=$(sh -c "$FD_LIST") expect_eq "no extra fds in the program (unix)" "$FD_BASE" "$(run -- sh -c "$FD_LIST")" -expect_eq "no extra fds in the program (spawn)" "$FD_BASE" "$("$TIMEOUT" 60 "$PLAYER" --spawn "$INTROSPECT --stdio" -- sh -c "$FD_LIST")" -expect_eq "--fd transport does not leak into the program" "0 1 2" "$(python3 - "$PLAYER" "$SOCK" <<'EOF' +expect_eq "no extra fds in the program (spawn)" "$FD_BASE" "$("$TIMEOUT" 60 "$NS" --spawn "$PROC --stdio" -- sh -c "$FD_LIST")" +expect_eq "--fd transport does not leak into the program" "0 1 2" "$(python3 - "$NS" "$SOCK" <<'EOF' import socket, subprocess, sys, os s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(sys.argv[2]) r = subprocess.run([sys.argv[1], "--fd", str(s.fileno()), "--", "sh", "-c", @@ -86,15 +86,15 @@ EOF echo "# signals" expect_eq "SIGTERM forwarded" "143" "$(run -- sh -c 'kill -TERM $PPID; sleep 5; echo alive' >/dev/null 2>&1; echo $?)" expect_eq "SIGHUP forwarded" "129" "$(run -- sh -c 'kill -HUP $PPID; sleep 5; echo alive' >/dev/null 2>&1; echo $?)" -expect_eq "SIGINT to 9player is ignored while the child lives" "still-here" "$(run -- sh -c 'kill -INT $PPID; sleep 0.3; echo still-here')" +expect_eq "SIGINT to 9ns is ignored while the child lives" "still-here" "$(run -- sh -c 'kill -INT $PPID; sleep 0.3; echo still-here')" # Ctrl-C from the tty must not kill the --spawn server (same process group). -expect_eq "Ctrl-C on the tty leaves the --spawn server alive" "ok" "$(timeout 30 python3 - "$PLAYER" "$INTROSPECT" <<'EOF' +expect_eq "Ctrl-C on the tty leaves the --spawn server alive" "ok" "$(timeout 30 python3 - "$NS" "$PROC" <<'EOF' import os, pty, sys, time, select P, I = sys.argv[1], sys.argv[2] prog = ["python3", "-c", """ import os, signal, sys, time signal.signal(signal.SIGINT, lambda *a: None) -m = os.environ['NINEPLAYER_MOUNT'] +m = os.environ['NINE_MOUNT'] open(m + '/build/optimize').read() sys.stdin.readline() try: @@ -122,9 +122,9 @@ print(out.decode(errors="replace").replace("^C", "").strip().splitlines()[-1] if EOF )" # The --spawn server dying mid-session is reaped (no zombie) and does not end the session. -OUT=$("$TIMEOUT" 60 "$PLAYER" --spawn "$INTROSPECT --stdio" -- sh -c 'srv=$(cat $NINEPLAYER_MOUNT/runtime/pid); kill -TERM $srv; sleep 0.5; st=$(ps -o stat= -p $srv 2>/dev/null); echo "${st:-gone}"; exit 5' 2>/dev/null); RC=$? +OUT=$("$TIMEOUT" 60 "$NS" --spawn "$PROC --stdio" -- sh -c 'srv=$(cat $NINE_MOUNT/runtime/pid); kill -TERM $srv; sleep 0.5; st=$(ps -o stat= -p $srv 2>/dev/null); echo "${st:-gone}"; exit 5' 2>/dev/null); RC=$? expect_eq "server death mid-session: exit status still the child's, server reaped (no zombie)" "5 gone" "$RC $OUT" -# A server that never answers: once the child is dead, SIGTERM must end 9player. +# A server that never answers: once the child is dead, SIGTERM must end 9ns. cat >"$TMP/hang.py" <<'EOF' import struct, os, sys, time def rd(n): @@ -146,33 +146,33 @@ while True: time.sleep(3600) os.write(1, struct.pack("<I", 4 + len(r)) + r) EOF -"$PLAYER" --spawn "python3 $TMP/hang.py" -- true 2>/dev/null & +"$NS" --spawn "python3 $TMP/hang.py" -- true 2>/dev/null & HP=$! sleep 1; kill -TERM $HP START=$(date +%s) for _ in $(seq 1 100); do kill -0 $HP 2>/dev/null || break; sleep 0.1; done if kill -0 $HP 2>/dev/null; then kill -KILL $HP; RC=hung; else wait $HP; RC=$?; fi -expect_eq "hung server: one SIGTERM ends 9player once the child is dead (watchdog)" "143" "$RC" +expect_eq "hung server: one SIGTERM ends 9ns once the child is dead (watchdog)" "143" "$RC" expect_eq "hung server: exit was prompt" "yes" "$([ $(( $(date +%s) - START )) -lt 8 ] && echo yes)" pkill -f "$TMP/hang.py" 2>/dev/null echo "# child/parent protocol" if command -v strace >/dev/null 2>&1 && strace -qq -e trace=none true 2>/dev/null; then - expect_eq "child killed before handoff" "125" "$(strace -f -qq -e trace=unshare -e inject=unshare:signal=KILL -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" -- true 2>/dev/null; echo $?)" - expect_contains "child killed before handoff: message" "child exited before reporting" "$(strace -f -qq -e trace=unshare -e inject=unshare:signal=KILL -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" -- true 2>&1)" - expect_eq "status handoff fails" "125" "$(strace -f -qq -e trace=sendmsg -e inject=sendmsg:error=EPIPE -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" -- true 2>/dev/null; echo $?)" + expect_eq "child killed before handoff" "125" "$(strace -f -qq -e trace=unshare -e inject=unshare:signal=KILL -o /dev/null timeout 20 "$NS" --unix "$SOCK" -- true 2>/dev/null; echo $?)" + expect_contains "child killed before handoff: message" "child exited before reporting" "$(strace -f -qq -e trace=unshare -e inject=unshare:signal=KILL -o /dev/null timeout 20 "$NS" --unix "$SOCK" -- true 2>&1)" + expect_eq "status handoff fails" "125" "$(strace -f -qq -e trace=sendmsg -e inject=sendmsg:error=EPIPE -o /dev/null timeout 20 "$NS" --unix "$SOCK" -- true 2>/dev/null; echo $?)" # recvmsg skipped (returns 1 without the fd): the child must be killed, not exec'd onto a dead mount. - OUT=$(strace -f -qq -e trace=recvmsg -e inject=recvmsg:retval=1:when=1 -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" -- sh -c 'echo child-ran' 2>&1; echo "rc=$?") + OUT=$(strace -f -qq -e trace=recvmsg -e inject=recvmsg:retval=1:when=1 -o /dev/null timeout 20 "$NS" --unix "$SOCK" -- sh -c 'echo child-ran' 2>&1; echo "rc=$?") expect_contains "truncated fd handoff: child not exec'd" "rc=125" "$OUT" expect_eq "truncated fd handoff: program never ran" "no" "$(case "$OUT" in *child-ran*) echo yes;; *) echo no;; esac)" - expect_contains "fuse mount failure is reported" "mount fuse: EPERM" "$(strace -f -qq -e trace=mount -e inject=mount:error=EPERM:when=2 -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" --mount "$TMP/mp" -- true 2>&1)" + expect_contains "fuse mount failure is reported" "mount fuse: EPERM" "$(strace -f -qq -e trace=mount -e inject=mount:error=EPERM:when=2 -o /dev/null timeout 20 "$NS" --unix "$SOCK" --mount "$TMP/mp" -- true 2>&1)" else echo "skip - strace unavailable (child failure injection)" fi expect_contains "fork failure is reported" "fork: E" "$(python3 -c " import resource, os resource.setrlimit(resource.RLIMIT_NPROC, (1, 1)) -os.execv('$PLAYER', ['$PLAYER', '--unix', '$SOCK', '--', 'true'])" 2>&1)" +os.execv('$NS', ['$NS', '--unix', '$SOCK', '--', 'true'])" 2>&1)" echo "# mountpoint policy" ln -s /nonexistent "$TMP/dangling" @@ -186,15 +186,15 @@ else echo "skip - no root-owned directory with >4096 entries" fi expect_eq "shadowed /run keeps its entries" "$(ls -A /run | sort | tr '\n' ' ')" "$(run --mount /run/x9p -- sh -c 'ls -A /run | grep -v "^x9p$" | sort | tr "\n" " "')" -expect_eq "mountpoint with spaces" "ok" "$(mkdir -p "$TMP/with space" && run --mount "$TMP/with space" -- sh -c '[ -f "$NINEPLAYER_MOUNT/README" ] && echo ok')" +expect_eq "mountpoint with spaces" "ok" "$(mkdir -p "$TMP/with space" && run --mount "$TMP/with space" -- sh -c '[ -f "$NINE_MOUNT/README" ] && echo ok')" expect_eq "mountpoint is a file" "125" "$(run --mount "$TMP/nx" -- true 2>/dev/null; echo $?)" echo "# leaks" -for i in $(seq 1 30); do run -- sh -c 'cat $NINEPLAYER_MOUNT/build/optimize >/dev/null' 2>/dev/null; done -BG=(); for i in $(seq 1 10); do ( run -- sh -c 'cat $NINEPLAYER_MOUNT/build/optimize >/dev/null' 2>/dev/null ) & BG+=($!); done; wait "${BG[@]}" # not a bare wait: that would also wait for the server +for i in $(seq 1 30); do run -- sh -c 'cat $NINE_MOUNT/build/optimize >/dev/null' 2>/dev/null; done +BG=(); for i in $(seq 1 10); do ( run -- sh -c 'cat $NINE_MOUNT/build/optimize >/dev/null' 2>/dev/null ) & BG+=($!); done; wait "${BG[@]}" # not a bare wait: that would also wait for the server sleep 0.3 -expect_eq "no stray 9player processes" "" "$(pgrep -f "^$PLAYER " | tr '\n' ' ')" -expect_eq "no stray --stdio servers" "" "$(pgrep -f "$INTROSPECT --stdio" | tr '\n' ' ')" +expect_eq "no stray 9ns processes" "" "$(pgrep -f "^$NS " | tr '\n' ' ')" +expect_eq "no stray --stdio servers" "" "$(pgrep -f "$PROC --stdio" | tr '\n' ' ')" expect_eq "host mount table untouched" "same" "$([ "$MI_BEFORE" = "$(grep -v " $TMP" /proc/self/mountinfo | sort)" ] && echo same || echo changed)" echo diff --git a/9ns/test/adversarial.sh b/9ns/test/adversarial.sh new file mode 100755 index 0000000..71c6f44 --- /dev/null +++ b/9ns/test/adversarial.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Runs every 9ns adversarial suite in sequence (hostile servers, FUSE +# semantics, process/namespace edge cases, stress). The suites aimed at the +# 9proc server itself live in 9proc/test (zig build 9proc-adv). +# Usage: bash 9ns/test/adversarial.sh <9ns> <9proc-demo> (zig build 9ns-adv) +set -u +NS=${1:?path to 9ns} +PROC=${2:?path to 9proc-demo} +HERE=$(cd "$(dirname "$0")" && pwd) +status=0 +for suite in adv_ns_process adv_bridge_hostile adv_bridge_semantics adv_bridge_stress; do + echo "### $suite" + if bash "$HERE/$suite.sh" "$NS" "$PROC"; then echo "### $suite: ok"; else echo "### $suite: FAILED"; status=1; fi +done +exit $status diff --git a/9player/test/integration.sh b/9ns/test/integration.sh index e12cb5f..5e0ad13 100755 --- a/9player/test/integration.sh +++ b/9ns/test/integration.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Integration tests for 9player: real user+mount namespaces, real FUSE, real 9P servers. -# Usage: bash 9player/test/integration.sh <9player> <introspect> (zig build 9player-itest) +# Integration tests for 9ns: real user+mount namespaces, real FUSE, real 9P servers. +# Usage: bash 9ns/test/integration.sh <9ns> <9proc-demo> (zig build 9ns-itest) # Exit 0 on success (or when the machine cannot run the tests), 1 on failure. set -u -PLAYER=$(realpath "${1:?path to 9player}") -INTROSPECT=$(realpath "${2:?path to introspect}") -TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-itest.XXXXXX") +NS=$(realpath "${1:?path to 9ns}") +PROC=$(realpath "${2:?path to 9proc-demo}") +TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-itest.XXXXXX") PIDS=() FAILED=0 PASSED=0 @@ -40,7 +40,7 @@ wait_socket() { # path } # run_in "<shell script>" — run inside a namespace with the current transport ($TRANSPORT array). -run_in() { timeout 60 "$PLAYER" "${TRANSPORT[@]}" -- sh -c "$1" 2>"$TMP/stderr"; } +run_in() { timeout 60 "$NS" "${TRANSPORT[@]}" -- sh -c "$1" 2>"$TMP/stderr"; } # --- scratch battery: works against any writable 9P tree rooted at $1 (relative to mount) --- scratch_battery() { # label scratchdir @@ -77,15 +77,15 @@ except OSError as e: print(errno.errorcode[e.errno]) } # ============================================================================ -echo "# introspect over a Unix socket" -SOCK=$TMP/introspect.sock -"$INTROSPECT" --unix "$SOCK" & +echo "# 9proc-demo over a Unix socket" +SOCK=$TMP/9proc.sock +"$PROC" --unix "$SOCK" & PIDS+=($!) -wait_socket "$SOCK" || { echo "introspect did not create $SOCK"; exit 1; } +wait_socket "$SOCK" || { echo "9proc-demo did not create $SOCK"; exit 1; } TRANSPORT=(--unix "$SOCK") expect_eq "zig_version" "$(zig version)" "$(run_in "cat $M/build/zig_version")" -expect_eq "mount exported" "$M" "$(run_in 'echo $NINEPLAYER_MOUNT')" +expect_eq "mount exported" "$M" "$(run_in 'echo $NINE_MOUNT')" expect_contains "root listing" "build" "$(run_in "ls $M")" expect_contains "root listing has scratch" "scratch" "$(run_in "ls $M")" expect_eq "README size > 0" "yes" "$(run_in "[ \$(stat -c %s $M/README) -gt 0 ] && echo yes")" @@ -98,35 +98,35 @@ expect_contains "comptime types" "Qid" "$(run_in "ls $M/comptime/types")" expect_eq "comptime size of Qid" "16" "$(run_in "cat $M/comptime/types/Qid/size")" expect_eq "runtime pid is server pid" "${PIDS[-1]}" "$(run_in "cat $M/runtime/pid")" expect_eq "exit status propagates" "7" "$(run_in 'exit 7'; echo $?)" -expect_eq "mount is fuse" "yes" "$(run_in "grep -q \"^9player $M fuse\" /proc/mounts && echo yes")" +expect_eq "mount is fuse" "yes" "$(run_in "grep -q \"^9ns $M fuse\" /proc/mounts && echo yes")" expect_eq "host /mnt entries still visible" "$(ls -A /mnt | sort | tr '\n' ' ')" "$(run_in "ls -A /mnt | grep -v '^9p\$' | sort | tr '\n' ' '")" expect_eq "host mount table untouched" "no" "$(grep -q " $M " /proc/self/mountinfo && echo yes || echo no)" -scratch_battery "introspect" scratch +scratch_battery "9proc-demo" scratch -echo "# nested 9player" -expect_eq "nested mount" "$(zig version)" "$(run_in "$PLAYER --unix $SOCK --mount $TMP/inner -- sh -c 'cat \$NINEPLAYER_MOUNT/build/zig_version'")" +echo "# nested 9ns" +expect_eq "nested mount" "$(zig version)" "$(run_in "$NS --unix $SOCK --mount $TMP/inner -- sh -c 'cat \$NINE_MOUNT/build/zig_version'")" echo "# --mount variants" mkdir -p "$TMP/mnt" -expect_eq "--mount existing dir" "ok" "$(timeout 60 "$PLAYER" --unix "$SOCK" --mount "$TMP/mnt" -- sh -c "[ -f $TMP/mnt/README ] && echo ok")" -expect_eq "--mount relative" "ok" "$(cd "$TMP" && timeout 60 "$PLAYER" --unix "$SOCK" --mount rel -- sh -c "[ -f $TMP/rel/README ] && echo ok")" -expect_eq "--mount missing under /" "125" "$(timeout 60 "$PLAYER" --unix "$SOCK" --mount /nonexistent-9player-dir -- true 2>/dev/null; echo $?)" +expect_eq "--mount existing dir" "ok" "$(timeout 60 "$NS" --unix "$SOCK" --mount "$TMP/mnt" -- sh -c "[ -f $TMP/mnt/README ] && echo ok")" +expect_eq "--mount relative" "ok" "$(cd "$TMP" && timeout 60 "$NS" --unix "$SOCK" --mount rel -- sh -c "[ -f $TMP/rel/README ] && echo ok")" +expect_eq "--mount missing under /" "125" "$(timeout 60 "$NS" --unix "$SOCK" --mount /nonexistent-9ns-dir -- true 2>/dev/null; echo $?)" echo "# lifecycle" START=$(date +%s) expect_eq "background grandchild does not block exit" "3" "$(run_in 'sleep 30 >/dev/null 2>&1 & exit 3'; echo $?)" expect_eq "exit was prompt" "yes" "$([ $(( $(date +%s) - START )) -lt 10 ] && echo yes)" -expect_eq "SIGTERM forwarded" "143" "$(timeout 60 "$PLAYER" --unix "$SOCK" -- sh -c 'kill -TERM $PPID; sleep 5; echo alive' >/dev/null 2>&1; echo $?)" +expect_eq "SIGTERM forwarded" "143" "$(timeout 60 "$NS" --unix "$SOCK" -- sh -c 'kill -TERM $PPID; sleep 5; echo alive' >/dev/null 2>&1; echo $?)" echo "# --spawn transport" -TRANSPORT=(--spawn "$INTROSPECT --stdio") +TRANSPORT=(--spawn "$PROC --stdio") expect_eq "spawn: zig_version" "$(zig version)" "$(run_in "cat $M/build/zig_version")" expect_eq "spawn: ctl" "7" "$(run_in "echo 'add 3 4' > $M/runtime/ctl && cat $M/runtime/ctl")" expect_eq "spawn: stateful sequence in one session" 'hello world 12 moved 0' "$(run_in "cd $M/scratch && echo hello > a && echo world >> a && cat a && stat -c %s a && mkdir d && echo moved > d/f && mv d/f d/g && cat d/g && rm d/g && rmdir d && rm a && ls | wc -l" | tr '\n' ' ' | sed 's/ $//')" echo "# --tcp transport" PORT=$(( 20000 + RANDOM % 20000 )) -"$INTROSPECT" --tcp "127.0.0.1:$PORT" & +"$PROC" --tcp "127.0.0.1:$PORT" & PIDS+=($!) sleep 0.3 TRANSPORT=(--tcp "127.0.0.1:$PORT") @@ -134,7 +134,7 @@ expect_eq "tcp: zig_version" "$(zig version)" "$(run_in "cat $M/build/zig_versio expect_eq "tcp: scratch" "tcp" "$(run_in "echo tcp > $M/scratch/t && cat $M/scratch/t && rm $M/scratch/t")" echo "# server death" -"$INTROSPECT" --unix "$TMP/dying.sock" & +"$PROC" --unix "$TMP/dying.sock" & DYING=$! wait_socket "$TMP/dying.sock" TRANSPORT=(--unix "$TMP/dying.sock") diff --git a/9player/test/adversarial.sh b/9player/test/adversarial.sh deleted file mode 100755 index 857af14..0000000 --- a/9player/test/adversarial.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -# Runs every 9player adversarial suite in sequence (hostile servers, FUSE -# semantics, process/namespace edge cases, stress). The suites aimed at the -# introspect server itself live in introspect/test (zig build introspect-adv). -# Usage: bash 9player/test/adversarial.sh <9player> <introspect> (zig build 9player-adv) -set -u -PLAYER=${1:?path to 9player} -INTROSPECT=${2:?path to introspect} -HERE=$(cd "$(dirname "$0")" && pwd) -status=0 -for suite in adv_ns_process adv_bridge_hostile adv_bridge_semantics adv_bridge_stress; do - echo "### $suite" - if bash "$HERE/$suite.sh" "$PLAYER" "$INTROSPECT"; then echo "### $suite: ok"; else echo "### $suite: FAILED"; status=1; fi -done -exit $status diff --git a/introspect/README.md b/9proc/README.md index ba7120b..0fd3ddb 100644 --- a/introspect/README.md +++ b/9proc/README.md @@ -1,9 +1,9 @@ -# introspect +# 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`, -[9player](../9player)) can inspect a running program: build facts, comptime +[9ns](../9ns)) can inspect a running program: build facts, comptime type layouts, live values, threads and their stacks, memory, breakpoints, panics. @@ -16,7 +16,7 @@ and panics via `std.debug`). [docs/LIBRARY.md](docs/LIBRARY.md) has the full contract. ``` -introspect/ +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 Tree/Server engine on cloud9.Server: fids, walks, dir reads, providers @@ -27,32 +27,32 @@ introspect/ 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 `introspect` binary (below) - test/ debug.sh, adv_introspect_hostile, adv_core_hostile, adv_linux_probe, adversarial.sh + 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 -`introspect` (this library, which imports `cloud9` itself). A package that +`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("introspect", cloud9_dep.module("introspect")); +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 introspect = @import("introspect"); +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: introspect.Config = .{ .name = "fw", .types = &.{State}, .msize = 2048, .max_fids = 16 }; -const S = introspect.Server(cfg); +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 @@ -66,13 +66,13 @@ pub fn main() void { } ``` -On Linux, `introspect.linux.Probe` runs that loop for you on one background +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(introspect.linux.debug.panicHook);` +`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 introspect`, binary `introspect`) +## 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 @@ -80,7 +80,7 @@ is exposed under `/vars`, and the Linux debug layer. ``` /README -/build/{zig_version,target,optimize,time,change} captured by introspect/build.zig (jj change id, UTC time) +/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} @@ -96,35 +96,35 @@ is exposed under `/vars`, and the Linux debug layer. socket or loopback only. ```sh -zig-out/bin/introspect --unix /tmp/intro.sock & # or --tcp IP:PORT, --stdio, --no-hold -zig-out/bin/9player --unix /tmp/intro.sock -- sh -c ' - cat $NINEPLAYER_MOUNT/build/zig_version; echo - cat $NINEPLAYER_MOUNT/comptime/types/Qid/fields - echo "fib 20" > $NINEPLAYER_MOUNT/runtime/ctl; cat $NINEPLAYER_MOUNT/runtime/ctl - cat $NINEPLAYER_MOUNT/threads/*/stack' +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 -introspect lives in the cloud9 repository as `cloud9/introspect/` and is -wired into cloud9's `build.zig` through the fragment `introspect/build.zig`. +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/introspect with the other binaries -zig build introspect # build and install only the demo -zig build introspect-test # library unit tests (core, vars, scratch, linux) and the demo's -zig build introspect-check-freestanding # compile the core for riscv32-freestanding-none -zig build introspect-debug-test # src/linux/debug.zig unit tests -zig build introspect-debug-itest # test/debug.sh: threads, stacks, breakpoints, panic through 9player -zig build introspect-adv # hostile raw-9P clients against the demo and the core, - # the Linux layer through a 9player mount (several minutes) -zig build -Dintrospect=false # leave introspect out -zig build introspect-check-freestanding -Dtarget=riscv32-freestanding-none -Dintrospect=true +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 ``` -`-Dintrospect` (default: on for Linux targets) enables the module and the +`-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 9player -(`-D9player=true`, the Linux default), unprivileged user namespaces, +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. diff --git a/introspect/build.zig b/9proc/build.zig index 6662151..670ea27 100644 --- a/introspect/build.zig +++ b/9proc/build.zig @@ -1,13 +1,13 @@ -//! Build fragment for introspect: the 9P debug/introspection library (module -//! `introspect`), its freestanding check, the `introspect` demo server and the -//! test suites under introspect/test. It is `@import`ed by the root build.zig +//! Build fragment for 9proc: the 9P debug/introspection library (module +//! `9proc`), its freestanding check, the `9proc-demo` server and the +//! test suites under 9proc/test. It is `@import`ed by the root build.zig //! and called with the root builder, so every `b.path(...)` here is relative -//! to the cloud9 root (hence the `introspect/` prefix), every option is +//! to the cloud9 root (hence the `9proc/` prefix), every option is //! defined by the root (no `standardTargetOptions` here) and every step it -//! registers lands in the root's step list under the `introspect` prefix. +//! registers lands in the root's step list under the `9proc` prefix. //! -//! Steps: introspect, introspect-test, introspect-check-freestanding, -//! introspect-debug-test, introspect-debug-itest, introspect-adv. +//! Steps: 9proc, 9proc-test, 9proc-check-freestanding, +//! 9proc-debug-test, 9proc-debug-itest, 9proc-adv. const std = @import("std"); /// What the root passes in. The root owns target/optimize resolution and the @@ -21,21 +21,21 @@ pub const Context = struct { }; pub const Artifacts = struct { - /// The `introspect` module, exported with `b.addModule` so dependents of - /// cloud9 can `.module("introspect")`. Built for any target; the Linux + /// The `9proc` module, exported with `b.addModule` so dependents of + /// cloud9 can `.module("9proc")`. Built for any target; the Linux /// layer is compiled in only when the target OS is Linux. module: *std.Build.Module, - /// The demo 9P2000 server (binary `introspect`); null when the target is - /// not Linux. 9player's integration tests use it as their server. + /// The demo 9P2000 server (binary `9proc-demo`); null when the target is + /// not Linux. 9ns's integration tests use it as their server. demo: ?*std.Build.Step.Compile, - /// `introspect-test`: library (and demo) unit tests. + /// `9proc-test`: library (and demo) unit tests. test_step: *std.Build.Step, - /// `introspect-check-freestanding`: the core compiled for riscv32-freestanding-none. + /// `9proc-check-freestanding`: the core compiled for riscv32-freestanding-none. check_step: *std.Build.Step, - /// `introspect-debug-test`: linux/debug.zig unit tests; null when not Linux. + /// `9proc-debug-test`: linux/debug.zig unit tests; null when not Linux. debug_test_step: ?*std.Build.Step, - /// `introspect-adv`: the hostile-client suites are attached by `add`, the - /// Linux-layer suite (which needs a 9player mount) by `addPlayerTests`. + /// `9proc-adv`: the hostile-client suites are attached by `add`, the + /// Linux-layer suite (which needs a 9ns mount) by `addNsTests`. adv_step: *std.Build.Step, }; @@ -59,18 +59,18 @@ pub fn add(b: *std.Build, ctx: Context) Artifacts { build_options.addOption([]const u8, "target", target.result.zigTriple(b.allocator) catch "unknown"); // The library: freestanding core + vars, scratch (allocator), Linux layer. - // Exported under the name `introspect` for packages that depend on cloud9. - const lib_mod = b.addModule("introspect", .{ - .root_source_file = b.path("introspect/src/root.zig"), + // Exported under the name `9proc` for packages that depend on cloud9. + const lib_mod = b.addModule("9proc", .{ + .root_source_file = b.path("9proc/src/root.zig"), .target = target, .optimize = optimize, .imports = &.{.{ .name = "cloud9", .module = ctx.cloud9 }}, }); - const test_step = b.step("introspect-test", "Run the introspect library's unit tests (and the demo's)"); + const test_step = b.step("9proc-test", "Run the 9proc library's unit tests (and the demo's)"); test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = lib_mod })).step); - // The core must compile without an OS (rule 1 of introspect/docs/LIBRARY.md). + // The core must compile without an OS (rule 1 of 9proc/docs/LIBRARY.md). // cloud9 is re-instantiated for that target from the same root source. const fs_target = b.resolveTargetQuery(.{ .cpu_arch = .riscv32, .os_tag = .freestanding, .abi = .none }); const fs_cloud9 = b.createModule(.{ @@ -79,58 +79,58 @@ pub fn add(b: *std.Build, ctx: Context) Artifacts { .optimize = optimize, }); const fs_check = b.addObject(.{ - .name = "introspect-freestanding", + .name = "9proc-freestanding", .root_module = b.createModule(.{ - .root_source_file = b.path("introspect/src/freestanding_check.zig"), + .root_source_file = b.path("9proc/src/freestanding_check.zig"), .target = fs_target, .optimize = optimize, .imports = &.{.{ .name = "cloud9", .module = fs_cloud9 }}, }), }); - const check_step = b.step("introspect-check-freestanding", "Compile the introspect core for riscv32-freestanding-none"); + const check_step = b.step("9proc-check-freestanding", "Compile the 9proc core for riscv32-freestanding-none"); check_step.dependOn(&fs_check.step); - // `introspect-adv` exists on every target so the step list is stable; its - // suites are attached below (Linux only) and by addPlayerTests. - const adv_step = b.step("introspect-adv", "Run introspect's adversarial suites (hostile clients, Linux layer; several minutes)"); + // `9proc-adv` exists on every target so the step list is stable; its + // suites are attached below (Linux only) and by addNsTests. + const adv_step = b.step("9proc-adv", "Run 9proc's adversarial suites (hostile clients, Linux layer; several minutes)"); if (!is_linux) { - adv_step.dependOn(&b.addFail("introspect-adv needs a Linux target (the demo server is Linux-only)").step); + adv_step.dependOn(&b.addFail("9proc-adv needs a Linux target (the demo server is Linux-only)").step); return .{ .module = lib_mod, .demo = null, .test_step = test_step, .check_step = check_step, .debug_test_step = null, .adv_step = adv_step }; } // The demo: a 9P2000 server whose tree is the binary itself (build-time, // comptime and runtime facts, a worker thread, the Linux debug layer). const demo_mod = b.createModule(.{ - .root_source_file = b.path("introspect/demo/main.zig"), + .root_source_file = b.path("9proc/demo/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "cloud9", .module = ctx.cloud9 }, .{ .name = "build_options", .module = build_options.createModule() }, - .{ .name = "introspect", .module = lib_mod }, + .{ .name = "9proc", .module = lib_mod }, }, }); - const demo = b.addExecutable(.{ .name = "introspect", .root_module = demo_mod }); + const demo = b.addExecutable(.{ .name = "9proc-demo", .root_module = demo_mod }); const install_demo = b.addInstallArtifact(demo, .{}); b.getInstallStep().dependOn(&install_demo.step); - b.step("introspect", "Build and install only the introspect demo server").dependOn(&install_demo.step); + b.step("9proc", "Build and install only the 9proc-demo server").dependOn(&install_demo.step); test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = demo_mod })).step); // Linux debug facilities (threads, stacks, breakpoints, panic): self-contained unit tests. const debug_mod = b.createModule(.{ - .root_source_file = b.path("introspect/src/linux/debug.zig"), + .root_source_file = b.path("9proc/src/linux/debug.zig"), .target = target, .optimize = optimize, }); - const debug_test_step = b.step("introspect-debug-test", "Run the introspect/src/linux/debug.zig unit tests"); + const debug_test_step = b.step("9proc-debug-test", "Run the 9proc/src/linux/debug.zig unit tests"); debug_test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = debug_mod })).step); // Hostile raw-9P clients against the demo (framing, tags, floods; the core's // /vars tree, snapshots, fid table). `--fast` as in the umbrella script. - inline for (.{ "adv_introspect_hostile", "adv_core_hostile" }) |suite| { + inline for (.{ "adv_9proc_hostile", "adv_core_hostile" }) |suite| { const run = b.addSystemCommand(&.{"bash"}); - run.addFileArg(b.path("introspect/test/" ++ suite ++ ".sh")); + run.addFileArg(b.path("9proc/test/" ++ suite ++ ".sh")); run.addArtifactArg(demo); run.addArg("--fast"); adv_step.dependOn(&run.step); @@ -139,29 +139,29 @@ pub fn add(b: *std.Build, ctx: Context) Artifacts { return .{ .module = lib_mod, .demo = demo, .test_step = test_step, .check_step = check_step, .debug_test_step = debug_test_step, .adv_step = adv_step }; } -/// The suites that drive the demo through a 9player mount: test/debug.sh +/// The suites that drive the demo through a 9ns mount: test/debug.sh /// (threads, stacks, breakpoints, panic end to end) and /// test/adv_linux_probe.sh (memory endpoints, signal machinery, poll loop). -/// Called by the root after the 9player fragment; `player` is null when -/// 9player is disabled, in which case the steps exist but fail with a notice. -/// Returns the `introspect-debug-itest` step (null when the target is not Linux). -pub fn addPlayerTests(b: *std.Build, arts: Artifacts, player: ?*std.Build.Step.Compile) ?*std.Build.Step { +/// Called by the root after the 9ns fragment; `ns` is null when +/// 9ns is disabled, in which case the steps exist but fail with a notice. +/// Returns the `9proc-debug-itest` step (null when the target is not Linux). +pub fn addNsTests(b: *std.Build, arts: Artifacts, ns: ?*std.Build.Step.Compile) ?*std.Build.Step { const demo = arts.demo orelse return null; // not Linux: nothing to drive - const debug_itest = b.step("introspect-debug-itest", "Run introspect/test/debug.sh (threads, stacks, breakpoints, panic through 9player)"); - const exe = player orelse { - const fail = b.addFail("introspect-debug-itest and the Linux-layer adversarial suite need 9player (build with -D9player=true)"); + const debug_itest = b.step("9proc-debug-itest", "Run 9proc/test/debug.sh (threads, stacks, breakpoints, panic through 9ns)"); + const exe = ns orelse { + const fail = b.addFail("9proc-debug-itest and the Linux-layer adversarial suite need 9ns (build with -D9ns=true)"); debug_itest.dependOn(&fail.step); arts.adv_step.dependOn(&fail.step); return debug_itest; }; const dbg = b.addSystemCommand(&.{"bash"}); - dbg.addFileArg(b.path("introspect/test/debug.sh")); + dbg.addFileArg(b.path("9proc/test/debug.sh")); dbg.addArtifactArg(exe); dbg.addArtifactArg(demo); debug_itest.dependOn(&dbg.step); const adv_linux = b.addSystemCommand(&.{"bash"}); - adv_linux.addFileArg(b.path("introspect/test/adv_linux_probe.sh")); + adv_linux.addFileArg(b.path("9proc/test/adv_linux_probe.sh")); adv_linux.addArtifactArg(exe); adv_linux.addArtifactArg(demo); arts.adv_step.dependOn(&adv_linux.step); diff --git a/introspect/demo/main.zig b/9proc/demo/main.zig index b1c2c57..df06369 100644 --- a/introspect/demo/main.zig +++ b/9proc/demo/main.zig @@ -1,4 +1,4 @@ -//! introspect: the demo 9P2000 server, built on the introspect library. +//! 9proc: the demo 9P2000 server, built on the 9proc library. //! //! /README, /build/*, /comptime/{types,decls}, /runtime/{pid,ppid,uptime,argv,cwd,env,clients}, //! /runtime/fn/{fib30,hostname,now,random,uname}, /runtime/ctl (echo|fib|sleep-ms|add|trap|panic), @@ -22,10 +22,10 @@ const std = @import("std"); const builtin = @import("builtin"); const cloud9 = @import("cloud9"); const build_options = @import("build_options"); -const introspect = @import("introspect"); +const proc9 = @import("9proc"); const linux = std.os.linux; const Writer = std.Io.Writer; -const plinux = introspect.linux; +const plinux = proc9.linux; const runtime = plinux.runtime; pub const panic = std.debug.FullPanic(plinux.debug.panicHook); @@ -93,10 +93,10 @@ pub const Fns = struct { } }; -const cfg: introspect.Config = .{ - .name = "introspect", +const cfg: proc9.Config = .{ + .name = "9proc-demo", .build = Build, - .types = &.{ cloud9.Qid, cloud9.Stat, cloud9.Msg, introspect.core.Node, linux.Statx }, + .types = &.{ cloud9.Qid, cloud9.Stat, cloud9.Msg, proc9.core.Node, linux.Statx }, .decls_of = @This(), .fns = Fns, .runtime = runtime.Fns(App, "info"), @@ -109,7 +109,7 @@ const cfg: introspect.Config = .{ .snapshot_slots = 8, .snapshot_bytes = 64 * 1024, }; -const S = introspect.Server(cfg); +const S = proc9.Server(cfg); const ProbeT = plinux.Probe(S); const ProbeStorage = ProbeT.Storage(max_clients); @@ -134,7 +134,7 @@ var app_mem: Bss(App) = .{}; var shared_mem: Bss(S.Shared) = .{}; var probe_storage_mem: Bss(ProbeStorage) = .{}; var probe_mem: Bss(ProbeT) = .{}; -var scratch_mem: Bss(introspect.Scratch) = .{}; +var scratch_mem: Bss(proc9.Scratch) = .{}; /// Sizes of the static pieces, for the report and `--help`. pub const static_bytes = @sizeOf(ProbeStorage) + @sizeOf(S.Shared) + @sizeOf(ProbeT); @@ -168,7 +168,7 @@ pub noinline fn workerLoop() void { /// The worker's sleep, issued as a raw syscall from this file so that the /// thread's innermost frame (the first line of /threads/<tid>/stack, which -/// introspect/test/debug.sh resolves through /addr) is in demo/main.zig rather than in std. +/// 9proc/test/debug.sh resolves through /addr) is in demo/main.zig rather than in std. /// EINTR (a capture signal) just ends the nap early. inline fn napMs(ms: u64) void { var req: linux.timespec = .{ .sec = @intCast(ms / 1000), .nsec = @intCast((ms % 1000) * 1_000_000) }; @@ -193,7 +193,7 @@ inline fn napMs(ms: u64) void { /// The /runtime/ctl handler. The core stages the output and commits it only /// on success, so a failed command leaves the previous result in place -/// (test/adv_introspect_hostile.py checks that). +/// (test/adv_9proc_hostile.py checks that). fn ctl(ctx: *anyopaque, cmd: []const u8, out: *Writer) anyerror!void { const a: *App = @ptrCast(@alignCast(ctx)); const line = std.mem.trim(u8, cmd, " \t\r\n\x00"); @@ -240,7 +240,7 @@ fn fib(n: u32) u64 { // -- main --------------------------------------------------------------------- const usage_text = - \\usage: introspect [--unix PATH | --tcp IP:PORT | --stdio] [--no-hold] + \\usage: 9proc-demo [--unix PATH | --tcp IP:PORT | --stdio] [--no-hold] \\ \\A demo 9P2000 file server exposing this binary's build-time, comptime and \\runtime facts, plus a debugger-shaped view of the process (threads, stacks, @@ -276,7 +276,7 @@ fn run(init: std.process.Init) !void { } else if (std.mem.eql(u8, a, "--unix") or std.mem.eql(u8, a, "--tcp")) { i += 1; if (i >= args.len) { - std.debug.print("introspect: {s} needs an argument\n{s}", .{ a, usage_text }); + std.debug.print("9proc-demo: {s} needs an argument\n{s}", .{ a, usage_text }); return error.Usage; } mode = if (a[2] == 'u') .unix else .tcp; @@ -287,7 +287,7 @@ fn run(init: std.process.Init) !void { std.debug.print("{s}\nstatic memory: {d} bytes ({d} clients, msize {d})\n", .{ usage_text, static_bytes, max_clients, max_msize }); return; } else { - std.debug.print("introspect: unknown argument {s}\n{s}", .{ a, usage_text }); + std.debug.print("9proc-demo: unknown argument {s}\n{s}", .{ a, usage_text }); return error.Usage; } } @@ -321,7 +321,7 @@ fn run(init: std.process.Init) !void { }; shared.* = .init(app); try shared.expose("state", &state); - scratch.* = try introspect.Scratch.init(init.gpa, 512 << 20); + scratch.* = try proc9.Scratch.init(init.gpa, 512 << 20); // Freed on the way out so a Debug build's allocator does not report the // tree as leaked (with a stack trace on stderr) after a clean --stdio EOF. defer scratch.deinit(); @@ -336,10 +336,10 @@ fn run(init: std.process.Init) !void { }; probe.init(shared, probe_storage_mem.get(), .{ .io = init.io, .listen = listen, .msize = max_msize, .hold_on_panic = hold }) catch |e| { switch (e) { - error.PathTooLong => std.debug.print("introspect: unix socket path too long\n", .{}), - error.BadAddress => std.debug.print("introspect: --tcp wants an IPv4 literal a.b.c.d:port\n", .{}), - error.Syscall => std.debug.print("introspect: {t} ({t})\n", .{ e, probe.last_errno }), - else => std.debug.print("introspect: {t}\n", .{e}), + error.PathTooLong => std.debug.print("9proc-demo: unix socket path too long\n", .{}), + error.BadAddress => std.debug.print("9proc-demo: --tcp wants an IPv4 literal a.b.c.d:port\n", .{}), + error.Syscall => std.debug.print("9proc-demo: {t} ({t})\n", .{ e, probe.last_errno }), + else => std.debug.print("9proc-demo: {t}\n", .{e}), } return if (e == error.PathTooLong or e == error.BadAddress) error.Usage else error.Syscall; }; @@ -349,24 +349,24 @@ fn run(init: std.process.Init) !void { app.info.clients = probe.clientCounter(); const worker = std.Thread.spawn(.{}, workerLoop, .{}) catch |e| { - std.debug.print("introspect: worker thread: {t}\n", .{e}); + std.debug.print("9proc-demo: worker thread: {t}\n", .{e}); return error.Syscall; }; worker.detach(); switch (mode) { - .unix => std.debug.print("introspect: listening on unix!{s}\n", .{address}), - .tcp => std.debug.print("introspect: listening on tcp!{s}\n", .{address}), + .unix => std.debug.print("9proc-demo: listening on unix!{s}\n", .{address}), + .tcp => std.debug.print("9proc-demo: listening on tcp!{s}\n", .{address}), .stdio => {}, } probe.start() catch |e| { - std.debug.print("introspect: thread spawn failed: {t}\n", .{e}); + std.debug.print("9proc-demo: thread spawn failed: {t}\n", .{e}); return error.Syscall; }; // SIGTERM/SIGINT end the loop cleanly: the socket file is unlinked, the // signal dispositions restored and the scratch tree freed. // A disposition of SIG_IGN inherited from the parent is left alone (Unix - // convention): 9player runs a --spawn server with SIGINT ignored so that + // convention): 9ns runs a --spawn server with SIGINT ignored so that // Ctrl-C on the terminal reaches only the program, not its file server. const term: linux.Sigaction = .{ .handler = .{ .handler = onTerm }, .mask = linux.sigemptyset(), .flags = 0 }; for ([_]linux.SIG{ .TERM, .INT }) |sig| { diff --git a/introspect/docs/LIBRARY.md b/9proc/docs/LIBRARY.md index 22840dc..eef7dad 100644 --- a/introspect/docs/LIBRARY.md +++ b/9proc/docs/LIBRARY.md @@ -1,9 +1,9 @@ -# introspect: a 9P debug/introspection server as a library +# 9proc: a 9P debug/introspection server as a library -The demo server that 9player's tests use grows into a library any Zig program +The demo server that 9ns's tests use grows into a library any Zig program can embed: a debugger-shaped interface where the protocol is just files. Anything that can read a filesystem (a shell, an agent, an editor, `9p`, -9player) can inspect a running process: build facts, comptime type layouts, +9ns) can inspect a running process: build facts, comptime type layouts, live values, threads and their stacks, memory, breakpoints, panics. Design rules (non-negotiable, they mirror cloud9): @@ -11,7 +11,7 @@ Design rules (non-negotiable, they mirror cloud9): 1. **The core is freestanding.** No allocator, no OS, no threads, no `std.Io`. Caller-owned buffers, fixed-capacity tables sized at comptime. It must compile for `riscv32-freestanding-none` (the ESP32-P4 firmware target, - `../05-zig-p4`), and `zig build introspect-check-freestanding` proves it. + `../05-zig-p4`), and `zig build 9proc-check-freestanding` proves it. 2. **Every dependency on a runtime is an explicit argument.** Features that truly need an `Allocator` or an `std.Io` take them in their `init`; nothing reaches for `std.heap.page_allocator` or a global `Io`. Where memory is @@ -19,27 +19,27 @@ Design rules (non-negotiable, they mirror cloud9): `Storage` struct the caller places in static memory. 3. **All allocation happens up front**, at init, from what the caller passed. Steady-state operation does not allocate. -4. **Platform layers are separate modules** (`introspect.linux`) and are the +4. **Platform layers are separate modules** (`9proc.linux`) and are the only places that touch sockets, threads, signals, `/proc` or `std.debug`. ``` - introspect/src/root.zig pub const core, vars, scratch, linux (linux only), Server(cfg) - introspect/src/core.zig Tree/Server engine on cloud9.Server: fids, walks, dir reads, providers - introspect/src/vars.zig comptime value renderers (@typeInfo) for /vars - introspect/src/scratch.zig in-memory read/write tree provider (takes an Allocator) - introspect/src/linux/probe.zig background thread + poll loop + unix/tcp/fd listeners - introspect/src/linux/debug.zig threads, stacks, registers, addr→source, memory, breakpoints, panic - introspect/demo/main.zig the `introspect` binary: embeds everything, worker thread, exposed vars + 9proc/src/root.zig pub const core, vars, scratch, linux (linux only), Server(cfg) + 9proc/src/core.zig Tree/Server engine on cloud9.Server: fids, walks, dir reads, providers + 9proc/src/vars.zig comptime value renderers (@typeInfo) for /vars + 9proc/src/scratch.zig in-memory read/write tree provider (takes an Allocator) + 9proc/src/linux/probe.zig background thread + poll loop + unix/tcp/fd listeners + 9proc/src/linux/debug.zig threads, stacks, registers, addr→source, memory, breakpoints, panic + 9proc/demo/main.zig the `9proc-demo` binary: embeds everything, worker thread, exposed vars -(paths from the cloud9 root; the library is the module `introspect` that -cloud9's `build.zig` exports next to `cloud9`, wired by `introspect/build.zig`) +(paths from the cloud9 root; the library is the module `9proc` that +cloud9's `build.zig` exports next to `cloud9`, wired by `9proc/build.zig`) ``` ## Core (`core.zig`) ```zig pub const Config = struct { - name: []const u8 = "introspect", // /README and Stat uid/gid + name: []const u8 = "9proc", // /README and Stat uid/gid types: []const type = &.{}, // /comptime/types/<short name>/... decls_of: ?type = null, // /comptime/decls lists this type's pub decls fns: type = struct {}, // /runtime/fn/<name>: pub fn (ctx: *anyopaque, w: *std.Io.Writer) anyerror!void @@ -107,7 +107,7 @@ pub const Provider = struct { ``` Error → Rerror text mapping lives in one place in the core, using the Plan 9 -strings 9player's bridge already understands (`file does not exist`, +strings 9ns's bridge already understands (`file does not exist`, `permission denied`, `file already exists`, `directory not empty`, `not a directory`, `is a directory`, `bad offset`, `no space`, `i/o error`, `not supported`). @@ -223,7 +223,7 @@ steps over the breakpoint (`traps_skipped` counts these). The probe's own serving thread is never parked or held: a trap or panic on it goes straight to the default behaviour, since nobody could write its `ctl` files. -**Panics**: `pub const panic = introspect.linux.panic;` in the root module +**Panics**: `pub const panic = proc9.linux.panic;` in the root module (built with `std.debug.FullPanic`). The first panic records message and a stack capture (`captureCurrentStackTrace` with `first_address`), publishes them, and, if `hold_on_panic` and the probe is running, futex-waits until @@ -233,7 +233,7 @@ trace and aborts). A nested or second panic goes straight to the default. Signal handlers are installed by `Probe.init` (breakpoints optional) and restored by `stop`. -## Demo (`demo/main.zig`, binary `introspect`) +## Demo (`demo/main.zig`, binary `9proc-demo`) Keeps every path the existing tests read (`/build/*`, `/comptime/types/Qid/*`, `/comptime/decls`, `/runtime/fn/now|hostname|…`, `/runtime/ctl` with @@ -256,20 +256,20 @@ the demo's Storage is a global. snapshots), vars (render/set for every category), scratch, debug (capture own thread and a helper thread; breakpoint pause/continue on a helper thread; panic record path without holding). -* `zig build introspect-check-freestanding`: compiles `core.zig` + `vars.zig` +* `zig build 9proc-check-freestanding`: compiles `core.zig` + `vars.zig` for `riscv32-freestanding-none` with a tiny freestanding root that instantiates `Server(cfg)` with static Storage. -* `zig build introspect-test` (library and demo unit tests) and - `zig build introspect-debug-test` (linux/debug.zig). -* 9player's `test/integration.sh` unchanged and passing; `test/debug.sh` - (`zig build introspect-debug-itest`) through 9player: read the worker's +* `zig build 9proc-test` (library and demo unit tests) and + `zig build 9proc-debug-test` (linux/debug.zig). +* 9ns's `test/integration.sh` unchanged and passing; `test/debug.sh` + (`zig build 9proc-debug-itest`) through 9ns: read the worker's stack (contains `workerLoop` and `demo/main.zig:`), resolve a frame through `/addr`, dump `/hex` of the exposed state, read and write `/vars/state/f/ticks/value`, trap → `/breakpoints` lists the worker, its stack shows `workerLoop`, `continue` resumes (ticks keep increasing), panic → `/panic/message`, `/panic/stack`, `continue` → server exits non-zero. -* Adversarial pass afterwards (`zig build introspect-adv`: hostile client +* Adversarial pass afterwards (`zig build 9proc-adv`: hostile client against the server and the core, signal races, memory reads of unmapped addresses, panic while a capture is in flight; `test/adversarial.sh` runs the same suites by hand). diff --git a/introspect/src/core.zig b/9proc/src/core.zig index 2707af2..cead8f3 100644 --- a/introspect/src/core.zig +++ b/9proc/src/core.zig @@ -22,7 +22,7 @@ pub const Ctl = *const fn (ctx: *anyopaque, cmd: []const u8, out: *Writer) anyer pub const Config = struct { /// Appears in /README and as uid/gid/muid of every Stat. - name: []const u8 = "introspect", + name: []const u8 = "9proc", /// A type whose pub decls `zig_version`, `target`, `optimize`, `time` /// and `change` (all `[]const u8`) become the files of /build. `null` /// omits /build. diff --git a/introspect/src/freestanding_check.zig b/9proc/src/freestanding_check.zig index ad88d0e..2a0c12f 100644 --- a/introspect/src/freestanding_check.zig +++ b/9proc/src/freestanding_check.zig @@ -1,5 +1,5 @@ //! A tiny freestanding root proving that `core` and `vars` compile without an -//! OS: `zig build introspect-check-freestanding` builds this for riscv32-freestanding-none. +//! OS: `zig build 9proc-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. @@ -54,7 +54,7 @@ var conn: S.Conn = undefined; 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 { +pub export fn proc9_check() u32 { shared = .init(&state); shared.expose("state", &state) catch unreachable; conn = .init(&shared, &storage, cfg.msize); @@ -66,6 +66,6 @@ pub export fn introspect_check() u32 { } pub export fn _start() noreturn { - _ = introspect_check(); + _ = proc9_check(); while (true) {} } diff --git a/introspect/src/linux/debug.zig b/9proc/src/linux/debug.zig index 4d43d5b..a344380 100644 --- a/introspect/src/linux/debug.zig +++ b/9proc/src/linux/debug.zig @@ -1,4 +1,4 @@ -//! Linux debug facilities for the introspect server: threads, stacks, +//! Linux debug facilities for the 9proc 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`. diff --git a/introspect/src/linux/probe.zig b/9proc/src/linux/probe.zig index f38c491..559d981 100644 --- a/introspect/src/linux/probe.zig +++ b/9proc/src/linux/probe.zig @@ -284,7 +284,7 @@ pub fn Probe(comptime Srv: type) type { 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"); + setThreadName("9proc"); while (!p.stopping.load(.acquire)) { if (p.single and p.clientCount() == 0) break; p.pollOnce(-1); @@ -375,7 +375,7 @@ pub fn Probe(comptime Srv: type) type { 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 }); + std.debug.print("9proc: refused connection ({d} clients open, {d} refused so far)\n", .{ p.clients.len, p.refused }); _ = linux.close(cfd); } } @@ -690,7 +690,7 @@ const SockClient = struct { }; fn testSockPath(buf: []u8, tag: []const u8) ![]const u8 { - return std.fmt.bufPrint(buf, "/tmp/introspect-probe-{d}-{s}.sock", .{ linux.getpid(), tag }); + return std.fmt.bufPrint(buf, "/tmp/9proc-probe-{d}-{s}.sock", .{ linux.getpid(), tag }); } const TestCtx = struct { info: runtime.Info }; diff --git a/introspect/src/linux/provider.zig b/9proc/src/linux/provider.zig index 9952398..9952398 100644 --- a/introspect/src/linux/provider.zig +++ b/9proc/src/linux/provider.zig diff --git a/introspect/src/linux/runtime.zig b/9proc/src/linux/runtime.zig index 503d6c2..503d6c2 100644 --- a/introspect/src/linux/runtime.zig +++ b/9proc/src/linux/runtime.zig diff --git a/introspect/src/root.zig b/9proc/src/root.zig index 1dcc892..5a8e135 100644 --- a/introspect/src/root.zig +++ b/9proc/src/root.zig @@ -1,4 +1,4 @@ -//! introspect: a 9P2000 debug/introspection server as a library. See +//! 9proc: 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"); diff --git a/introspect/src/scratch.zig b/9proc/src/scratch.zig index 2163a28..2163a28 100644 --- a/introspect/src/scratch.zig +++ b/9proc/src/scratch.zig diff --git a/introspect/src/vars.zig b/9proc/src/vars.zig index 20cadfc..20cadfc 100644 --- a/introspect/src/vars.zig +++ b/9proc/src/vars.zig diff --git a/introspect/test/adv_introspect_hostile.py b/9proc/test/adv_9proc_hostile.py index 7dbb5c0..934e757 100755 --- a/introspect/test/adv_introspect_hostile.py +++ b/9proc/test/adv_9proc_hostile.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -"""Hostile raw-9P2000 client for the introspect server (stdlib only). +"""Hostile raw-9P2000 client for the 9proc-demo server (stdlib only). Usage: - adv_introspect_hostile.py --server zig-out/bin/introspect # spawns it on a temp unix socket - adv_introspect_hostile.py --socket PATH # attacks a running server + adv_9proc_hostile.py --server zig-out/bin/9proc-demo # spawns it on a temp unix socket + adv_9proc_hostile.py --socket PATH # attacks a running server Every attack is followed by a "server still healthy" probe on a fresh connection. Exit status is non-zero if any check fails, the server dies, or a probe hangs. diff --git a/9proc/test/adv_9proc_hostile.sh b/9proc/test/adv_9proc_hostile.sh new file mode 100755 index 0000000..80d3342 --- /dev/null +++ b/9proc/test/adv_9proc_hostile.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Adversarial raw-9P2000 client tests for the 9proc-demo server. +# Usage: bash 9proc/test/adv_9proc_hostile.sh <9proc-demo> [--fast] (part of zig build 9proc-adv) +# Spawns the server on a temporary unix socket and attacks it with +# 9proc/test/adv_9proc_hostile.py (Python 3 stdlib). Exit 1 on any failure. +set -u +PROC=$(realpath "${1:?path to 9proc-demo}") +shift +command -v python3 >/dev/null || { echo "SKIP: python3 missing"; exit 0; } +exec python3 "$(dirname "$0")/adv_9proc_hostile.py" --server "$PROC" "$@" diff --git a/introspect/test/adv_core_hostile.py b/9proc/test/adv_core_hostile.py index 56ef5a3..464876f 100755 --- a/introspect/test/adv_core_hostile.py +++ b/9proc/test/adv_core_hostile.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 -"""Hostile raw-9P2000 client aimed at the introspect *core* (stdlib only). +"""Hostile raw-9P2000 client aimed at the 9proc *core* (stdlib only). -Complements adv_introspect_hostile.py in this directory (framing, tags, scratch, floods) +Complements adv_9proc_hostile.py in this directory (framing, tags, scratch, floods) with attacks on the freestanding engine's own paths: the /vars tree and its comptime renderers, snapshot slots, the static tree, the fid table at its configured maximum, directory-read offsets, msize 24, the ctl staging rule, and the demo's debug providers driven as black boxes. Usage: - adv_core_hostile.py --server zig-out/bin/introspect # spawns it on a temp unix socket + adv_core_hostile.py --server zig-out/bin/9proc-demo # spawns it on a temp unix socket adv_core_hostile.py --socket PATH # attacks a running server Exit status is non-zero if any check fails or the server dies. @@ -24,8 +24,8 @@ import threading import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -import adv_introspect_hostile as base # noqa: E402 -from adv_introspect_hostile import ( # noqa: E402 +import adv_9proc_hostile as base # noqa: E402 +from adv_9proc_hostile import ( # noqa: E402 NOTAG, Tversion, Tflush, Rflush, Twalk, Rwalk, Topen, Ropen, Rcreate, Tread, Rread, Twrite, Rwrite, Tclunk, Rclunk, Tremove, Rremove, Tstat, Rstat, Twstat, Rwstat, Rerror, OREAD, OWRITE, ORDWR, OEXEC, OTRUNC, ORCLOSE, DMDIR, diff --git a/9proc/test/adv_core_hostile.sh b/9proc/test/adv_core_hostile.sh new file mode 100755 index 0000000..28a7fc2 --- /dev/null +++ b/9proc/test/adv_core_hostile.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Adversarial raw-9P2000 client tests aimed at the 9proc core. +# Usage: bash 9proc/test/adv_core_hostile.sh <9proc-demo> [--fast] (part of zig build 9proc-adv) +# Spawns the server on a temporary unix socket and attacks it with +# 9proc/test/adv_core_hostile.py (Python 3 stdlib). Exit 1 on any failure. +set -u +PROC=$(realpath "${1:?path to 9proc-demo}") +shift +command -v python3 >/dev/null || { echo "SKIP: python3 missing"; exit 0; } +exec python3 "$(dirname "$0")/adv_core_hostile.py" --server "$PROC" "$@" diff --git a/introspect/test/adv_linux_probe.py b/9proc/test/adv_linux_probe.py index 83de771..aa28cf9 100755 --- a/introspect/test/adv_linux_probe.py +++ b/9proc/test/adv_linux_probe.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -"""Adversarial tests of the introspect Linux layer: probe loop, debug provider, -signal machinery. Raw 9P2000 over a unix socket, plus one 9player mount. -Usage: adv_linux_probe.py --player <9player> --server <introspect> -Reuses the client of adv_introspect_hostile.py. Exit 1 on any failure. +"""Adversarial tests of the 9proc Linux layer: probe loop, debug provider, +signal machinery. Raw 9P2000 over a unix socket, plus one 9ns mount. +Usage: adv_linux_probe.py --ns <9ns> --server <9proc-demo> +Reuses the client of adv_9proc_hostile.py. Exit 1 on any failure. """ import argparse import ctypes @@ -17,10 +17,10 @@ import threading import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from adv_introspect_hostile import ( # noqa: E402 +from adv_9proc_hostile import ( # noqa: E402 NOFID, NOTAG, OREAD, OWRITE, Nine, Rerror, Ropen, Rread, Rversion, Rwalk, Rwrite, Tattach, Tread, Tversion, Twrite, frame, healthy, ok, parse_stat, s16) -import adv_introspect_hostile as hostile # noqa: E402 +import adv_9proc_hostile as hostile # noqa: E402 libc = ctypes.CDLL(None, use_errno=True) SYS_tgkill = 234 if os.uname().machine == "x86_64" else 131 # aarch64: 131 @@ -170,7 +170,7 @@ def attack_memory(server): ok("read across the end of a mapping is a short read of 16 bytes", rt == Rread and len(d) == 16, (rt, d and len(d))) rt, d = rd(c, [b"hex", b"%x" % (hi - 16)]) ok("hexdump across the end of a mapping stops at the boundary", rt == Rread and d.count(b"\n") == 1, (rt, d)) - code = [ln for ln in maps.splitlines() if "r-xp" in ln and "introspect" in ln][0] + code = [ln for ln in maps.splitlines() if "r-xp" in ln and "9proc-demo" in ln][0] clo = int(code.split("-")[0], 16) ok("write into read-only code is an error, not a fault", wr(c, [b"mem", b"%x" % (clo + 0x100)], b"\xcc") == ("err", "i/o error")) ok("server alive after memory attacks", s.alive() and healthy(s.path)) @@ -189,10 +189,10 @@ def attack_signals(server): s = Srv(server) c = client(s.path, timeout=8) w = thread_by_name(c, s.pid, b"worker") - probe = thread_by_name(c, s.pid, b"introspect") + probe = thread_by_name(c, s.pid, b"9proc") ok("worker and probe threads found by name", bool(w and probe), (w, probe)) names = sorted(open(f"/proc/{s.pid}/task/{t}/comm").read().strip() for t in os.listdir(f"/proc/{s.pid}/task")) - ok("thread names are introspect, introspect, worker", names == ["introspect", "introspect", "worker"], names) + ok("thread names are 9proc, 9proc-demo, worker", names == ["9proc", "9proc-demo", "worker"], names) # 4 clients hammer stacks of every thread while trap/continue interleave errs, count = [], [0] @@ -290,7 +290,7 @@ def attack_signals(server): s.stop() -def attack_probe(player, server): +def attack_probe(ns, server): print("# probe loop and admission") s = Srv(server) c0 = cpu_ticks(s.pid) @@ -309,10 +309,10 @@ def attack_probe(player, server): time.sleep(1.0) ok("no fd leak over 1000 connect/disconnect cycles", fds(s.pid) == f0, (f0, fds(s.pid))) held = [client(s.path, timeout=8) for _ in range(14)] - pl = subprocess.Popen([player, "--unix", s.path, "--", "sh", "-c", "cat /mnt/9p/build/zig_version; echo; sleep 1000"], + pl = subprocess.Popen([ns, "--unix", s.path, "--", "sh", "-c", "cat /mnt/9p/build/zig_version; echo; sleep 1000"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) seen = pl.stdout.readline().strip() - ok("9player mount alongside 14 attached clients", bool(seen), seen) + ok("9ns mount alongside 14 attached clients", bool(seen), seen) time.sleep(1.0) # past evict_idle_ms: everyone is idle now slow = [] for _ in range(40): @@ -330,9 +330,9 @@ def attack_probe(player, server): pass time.sleep(0.2) ok("attached clients are never evicted by a flood", all(h.path_read([b"build", b"zig_version"]) for h in held)) - ok("the 9player mount survives the flood", pl.poll() is None) - pl2 = subprocess.run([player, "--unix", s.path, "--", "cat", "/mnt/9p/build/zig_version"], capture_output=True, timeout=30) - ok("a new 9player mount is refused cleanly while the table is full", pl2.returncode != 0 or bool(pl2.stdout.strip()), pl2.stderr[-100:]) + ok("the 9ns mount survives the flood", pl.poll() is None) + pl2 = subprocess.run([ns, "--unix", s.path, "--", "cat", "/mnt/9p/build/zig_version"], capture_output=True, timeout=30) + ok("a new 9ns mount is refused cleanly while the table is full", pl2.returncode != 0 or bool(pl2.stdout.strip()), pl2.stderr[-100:]) for so in slow: so.close() time.sleep(0.5) @@ -404,13 +404,13 @@ def attack_probe(player, server): def main(): ap = argparse.ArgumentParser() - ap.add_argument("--player", required=True) + ap.add_argument("--ns", required=True) ap.add_argument("--server", required=True) args = ap.parse_args() attack_memory(args.server) attack_signals(args.server) if subprocess.run(["unshare", "-Urm", "true"], capture_output=True).returncode == 0 and os.path.exists("/dev/fuse"): - attack_probe(args.player, args.server) + attack_probe(args.ns, args.server) else: print("# probe/admission: SKIP (namespaces or /dev/fuse unavailable)") print(f"# {hostile.PASSES} passed, {len(hostile.FAILS)} failed") diff --git a/9proc/test/adv_linux_probe.sh b/9proc/test/adv_linux_probe.sh new file mode 100755 index 0000000..0508f08 --- /dev/null +++ b/9proc/test/adv_linux_probe.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Adversarial tests of the 9proc Linux layer (probe loop, debug provider, +# signal machinery) with raw 9P2000 over a unix socket and one 9ns mount. +# Usage: bash 9proc/test/adv_linux_probe.sh <9ns> <9proc-demo> (part of zig build 9proc-adv) +# Exit 1 on any failure; SKIP (exit 0) without python3. +set -u +NS=$(realpath "${1:?path to 9ns}") +PROC=$(realpath "${2:?path to 9proc-demo}") +command -v python3 >/dev/null || { echo "SKIP: python3 missing"; exit 0; } +exec python3 "$(dirname "$0")/adv_linux_probe.py" --ns "$NS" --server "$PROC" diff --git a/9proc/test/adversarial.sh b/9proc/test/adversarial.sh new file mode 100755 index 0000000..f41c276 --- /dev/null +++ b/9proc/test/adversarial.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Runs every 9proc adversarial suite in sequence: hostile raw-9P clients +# against the demo (framing, tags, floods; the core's /vars, snapshots, fids) +# and the Linux layer through one 9ns mount (memory, signals, poll loop). +# Usage: bash 9proc/test/adversarial.sh <9ns> <9proc-demo> [--fast] +# `zig build 9proc-adv` runs the same suites as separate steps. +set -u +NS=${1:?path to 9ns} +PROC=${2:?path to 9proc-demo} +shift 2 +HERE=$(cd "$(dirname "$0")" && pwd) +status=0 +for suite in adv_9proc_hostile adv_core_hostile; do + echo "### $suite" + if bash "$HERE/$suite.sh" "$PROC" "$@"; then echo "### $suite: ok"; else echo "### $suite: FAILED"; status=1; fi +done +echo "### adv_linux_probe" +if bash "$HERE/adv_linux_probe.sh" "$NS" "$PROC"; then echo "### adv_linux_probe: ok"; else echo "### adv_linux_probe: FAILED"; status=1; fi +exit $status diff --git a/introspect/test/debug.sh b/9proc/test/debug.sh index c3be406..1334dff 100755 --- a/introspect/test/debug.sh +++ b/9proc/test/debug.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# End-to-end test of the introspect debug facilities through 9player: +# End-to-end test of the 9proc debug facilities through 9ns: # threads and stacks, address resolution, memory, exposed values, breakpoints, panics. -# Usage: bash introspect/test/debug.sh <9player> <introspect> (zig build introspect-debug-itest) +# Usage: bash 9proc/test/debug.sh <9ns> <9proc-demo> (zig build 9proc-debug-itest) set -u -PLAYER=$(realpath "${1:?path to 9player}") -INTROSPECT=$(realpath "${2:?path to introspect}") +NS=$(realpath "${1:?path to 9ns}") +PROC=$(realpath "${2:?path to 9proc-demo}") TMP=$(mktemp -d /tmp/9pdbg.XXXXXX) M=/mnt/9p FAILED=0; PASSED=0 @@ -18,10 +18,10 @@ pass() { PASSED=$((PASSED + 1)); echo "ok - $1"; } fail() { FAILED=$((FAILED + 1)); echo "FAIL - $1"; shift; [ $# -gt 0 ] && printf ' %s\n' "$@"; } expect_eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(printf %q "$2")" "actual: $(printf %q "$3")"; fi; } expect_contains() { case "$3" in *"$2"*) pass "$1" ;; *) fail "$1" "missing: $(printf %q "$2")" "in: $(printf %q "$3")" ;; esac; } -run_in() { timeout 60 "$PLAYER" --unix "$SOCK" -- sh -c "$1" 2>"$TMP/stderr"; } +run_in() { timeout 60 "$NS" --unix "$SOCK" -- sh -c "$1" 2>"$TMP/stderr"; } SOCK=$TMP/dbg.sock -"$INTROSPECT" --unix "$SOCK" >"$TMP/server.log" 2>&1 & +"$PROC" --unix "$SOCK" >"$TMP/server.log" 2>&1 & SRV=$! for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.05; done [ -S "$SOCK" ] || { echo "server did not start"; cat "$TMP/server.log"; exit 1; } @@ -79,31 +79,36 @@ tags, counts, negotiated frame sizes, and flush completion. ## Programs -Programs built on cloud9 live in sibling directories of `src/`, each with its +Related programs live in `cloud9/<name>/`, one directory per program beside +`src/`, and carry `9`-prefixed names (`9web`, `9proc`, `9ns`). Each has its own sources, tests, README and a `build.zig` fragment that the root -`build.zig` imports and enables with a toggle (`zig build --help` lists the -steps). New related programs follow the same layout. +`build.zig` imports and enables with a `-D<name>` toggle (`zig build --help` +lists the steps). New related programs follow the same layout. -* [`introspect/`](introspect/README.md) — a 9P debug/introspection server as - a library (module `introspect`, exported next to `cloud9`; freestanding - core, Linux debug layer) and its demo binary `introspect`. - `-Dintrospect=[bool]`; steps `introspect`, `introspect-test`, - `introspect-check-freestanding`, `introspect-debug-test`, - `introspect-debug-itest`, `introspect-adv`. -* [`9player/`](9player/README.md) — mount a 9P tree into a fresh user+mount +* [`web/`](docs/http.md) — the HTTP/WebSocket gateway `9web` and its + WebAssembly browser client (`web/main.zig`, `web/client.zig`, assets under + `web/static/`). Always built; steps `serve`, `web`, `http-test`, `e2e`. +* [`9proc/`](9proc/README.md) — a 9P debug/introspection server as + a library (module `9proc`, exported next to `cloud9`; freestanding + core, Linux debug layer) and its demo binary `9proc-demo`. + `-D9proc=[bool]`; steps `9proc`, `9proc-test`, + `9proc-check-freestanding`, `9proc-debug-test`, + `9proc-debug-itest`, `9proc-adv`. +* [`9ns/`](9ns/README.md) — mount a 9P tree into a fresh user+mount namespace via FUSE and run a program in it, without root (Linux, no libc). - `-D9player=[bool]`; steps `9player`, `9player-test`, `9player-itest`, - `9player-adv`. + `-D9ns=[bool]`; steps `9ns`, `9ns-test`, `9ns-itest`, + `9ns-adv`. -Both toggles default to on for Linux targets and off elsewhere; enabled -programs are installed by the plain `zig build`, and `programs-test` / -`programs-itest` run every enabled program's unit and integration steps. A -dependent package gets both modules from the one dependency: +The `9proc` and `9ns` toggles default to on for Linux targets and off +elsewhere; enabled programs are installed by the plain `zig build` next to +`9web` and `cloud9-probe`, and `programs-test` / `programs-itest` run every +enabled program's unit and integration steps. A dependent package gets both +modules from the one dependency: ```zig const cloud9_dep = b.dependency("cloud9", .{ .target = target, .optimize = optimize }); app.root_module.addImport("cloud9", cloud9_dep.module("cloud9")); -app.root_module.addImport("introspect", cloud9_dep.module("introspect")); +app.root_module.addImport("9proc", cloud9_dep.module("9proc")); ``` See [design and ownership contracts](docs/design.md), @@ -16,7 +16,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); const wasm = b.addExecutable(.{ .name = "cloud9", .root_module = b.createModule(.{ - .root_source_file = b.path("app/client.zig"), + .root_source_file = b.path("web/client.zig"), .target = wasm_target, .optimize = optimize, .imports = &.{.{ .name = "cloud9", .module = wasm_core }}, @@ -30,10 +30,10 @@ pub fn build(b: *std.Build) void { const web = b.step("web", "Install the standalone browser client into zig-out/web"); web.dependOn(&b.addInstallFileWithDir(wasm.getEmittedBin(), .{ .custom = "web/_cloud9" }, "cloud9.wasm").step); for ([_][]const u8{ "index.html", "app.mjs", "client.mjs", "style.css" }) |file| { - web.dependOn(&b.addInstallFileWithDir(b.path(b.fmt("app/web/{s}", .{file})), .{ .custom = if (std.mem.eql(u8, file, "index.html")) "web" else "web/_cloud9" }, file).step); + web.dependOn(&b.addInstallFileWithDir(b.path(b.fmt("web/static/{s}", .{file})), .{ .custom = if (std.mem.eql(u8, file, "index.html")) "web" else "web/_cloud9" }, file).step); } - const bridge = b.addExecutable(.{ .name = "cloud9-http", .root_module = b.createModule(.{ - .root_source_file = b.path("app/main.zig"), + const bridge = b.addExecutable(.{ .name = "9web", .root_module = b.createModule(.{ + .root_source_file = b.path("web/main.zig"), .target = target, .optimize = optimize, .link_libc = true, @@ -44,7 +44,7 @@ pub fn build(b: *std.Build) void { const run_bridge = b.addRunArtifact(bridge); if (b.args) |args| run_bridge.addArgs(args); b.step("serve", "Run the HTTP/WebSocket bridge (--upstream tcp:127.0.0.1:564)").dependOn(&run_bridge.step); - const native_http = b.addExecutable(.{ .name = "cloud9-http-test", .root_module = b.createModule(.{ + const native_http = b.addExecutable(.{ .name = "9web-test", .root_module = b.createModule(.{ .root_source_file = b.path("test/web/native.zig"), .target = target, .optimize = optimize, @@ -127,58 +127,58 @@ pub fn build(b: *std.Build) void { // ------------------------------------------------------------------ // Programs related to cloud9. Each lives in its own directory beside - // src/ (introspect/, 9player/) with a build.zig *fragment* exposing + // src/ (9proc/, 9ns/) with a build.zig *fragment* exposing // `add(b, ctx)`; it is imported here and runs with this builder, so its // b.path() calls are relative to this root and its steps are namespaced // by the program's name. New related programs follow the same pattern: // add a directory, a fragment, a toggle here, and the path to // build.zig.zon. // - // -Dintrospect=[bool] library module (any target) + freestanding check; - // demo server and Linux suites when the target is Linux - // -D9player=[bool] the FUSE mount CLI (Linux only) + // -D9proc=[bool] library module (any target) + freestanding check; + // demo server and Linux suites when the target is Linux + // -D9ns=[bool] the FUSE mount CLI (Linux only) // // Both default to true on a Linux target and false elsewhere. Enabled - // programs are installed by the plain `zig build` next to cloud9-http + // programs are installed by the plain `zig build` next to 9web // and cloud9-probe. const is_linux = target.result.os.tag == .linux; - const want_introspect = b.option(bool, "introspect", "Build the introspect library module and demo (default: target is Linux)") orelse is_linux; - const want_9player = b.option(bool, "9player", "Build the 9player FUSE mount CLI (default: target is Linux; Linux only)") orelse is_linux; - if (want_9player and !is_linux) { - std.debug.print("error: -D9player=true needs a Linux target (got {s})\n", .{@tagName(target.result.os.tag)}); + const want_9proc = b.option(bool, "9proc", "Build the 9proc library module and demo (default: target is Linux)") orelse is_linux; + const want_9ns = b.option(bool, "9ns", "Build the 9ns FUSE mount CLI (default: target is Linux; Linux only)") orelse is_linux; + if (want_9ns and !is_linux) { + std.debug.print("error: -D9ns=true needs a Linux target (got {s})\n", .{@tagName(target.result.os.tag)}); std.process.exit(1); } const programs_test = b.step("programs-test", "Run every enabled program's unit tests and checks"); const programs_itest = b.step("programs-itest", "Run every enabled program's integration suites"); - const introspect_build = @import("introspect/build.zig"); - const nineplayer_build = @import("9player/build.zig"); + const proc_build = @import("9proc/build.zig"); + const ns_build = @import("9ns/build.zig"); - const introspect: ?introspect_build.Artifacts = if (want_introspect) introspect_build.add(b, .{ + const proc: ?proc_build.Artifacts = if (want_9proc) proc_build.add(b, .{ .target = target, .optimize = optimize, .cloud9 = module, }) else null; - if (introspect) |i| { + if (proc) |i| { programs_test.dependOn(i.test_step); programs_test.dependOn(i.check_step); if (i.debug_test_step) |s| programs_test.dependOn(s); } - const nineplayer: ?nineplayer_build.Artifacts = if (want_9player) nineplayer_build.add(b, .{ + const ns: ?ns_build.Artifacts = if (want_9ns) ns_build.add(b, .{ .target = target, .optimize = optimize, .cloud9 = module, - .introspect_demo = if (introspect) |i| i.demo else null, + .proc_demo = if (proc) |i| i.demo else null, }) else null; - if (nineplayer) |p| { + if (ns) |p| { programs_test.dependOn(p.test_step); programs_itest.dependOn(p.itest_step); } - // introspect's end-to-end suites drive the demo through a 9player mount, + // 9proc's end-to-end suites drive the demo through a 9ns mount, // so they are wired once both fragments have run. - if (introspect) |i| { - if (introspect_build.addPlayerTests(b, i, if (nineplayer) |p| p.exe else null)) |s| programs_itest.dependOn(s); + if (proc) |i| { + if (proc_build.addNsTests(b, i, if (ns) |p| p.exe else null)) |s| programs_itest.dependOn(s); } } diff --git a/build.zig.zon b/build.zig.zon index c170b9d..a7a4e3b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,5 +3,5 @@ .fingerprint = 0xc8b2d5eaa3adfca, .version = "0.1.0", .minimum_zig_version = "0.16.0", - .paths = .{ "build.zig", "build.zig.zon", "src", "app", "test", "docs", "README.md", "9player", "introspect" }, + .paths = .{ "build.zig", "build.zig.zon", "src", "web", "test", "docs", "README.md", "9ns", "9proc" }, } diff --git a/docs/design.md b/docs/design.md index d12b92c..2013b3e 100644 --- a/docs/design.md +++ b/docs/design.md @@ -57,7 +57,7 @@ or buffers. `http.Client` negotiates HTTP/1.1 upgrades using `std.http.Client`, including its certificate-verified HTTPS support. `http.Bridge` relays between an accepted WebSocket and arbitrary standard readers/writers, including UART adapters. The runnable application's web UI, device leases, deadlines, and -origin policy remain in `app/`. See [HTTP ownership and usage](http.md). +origin policy remain in `web/`. See [HTTP ownership and usage](http.md). `Quic(OpenSSL, alpn)` is an optional OpenSSL 3.6+ adapter with a single ordered bidirectional stream per connection. It preserves the previous Pardes transport: @@ -68,15 +68,16 @@ protocol core. Accepted connections must close before their shared listener. # Related programs -Programs built on the library ship from this repository in sibling -directories of `src/` (`introspect/`, `9player/`), never inside it: each has +Programs built on the library ship from this repository as `cloud9/<name>/`, +sibling directories of `src/` with `9`-prefixed names (`web/` builds `9web`; +`9proc/` and `9ns/` build `9proc-demo` and `9ns`), never inside it: each has its own `src/`, `test/`, `docs/`, README and a `build.zig` fragment (`pub fn add(b, ctx)`) that the root `build.zig` imports, passes the resolved target, optimize mode and the `cloud9` module to, and enables with a `-D<name>` toggle. Fragments register namespaced steps (`<name>`, `<name>-test`, ...), never call `standardTargetOptions`, and use root-relative `b.path("<name>/...")`. The library keeps its contract: mounting, namespaces, -threads, allocation and process policy stay in the program. `introspect` is +threads, allocation and process policy stay in the program. `9proc` is also exported as a module next to `cloud9` for dependents. New related programs follow this layout. diff --git a/docs/http.md b/docs/http.md index f486d52..4e92e5e 100644 --- a/docs/http.md +++ b/docs/http.md @@ -16,7 +16,7 @@ flowchart LR ```sh zig build -Doptimize=ReleaseSafe -./zig-out/bin/cloud9-http --upstream tcp:127.0.0.1:564 +./zig-out/bin/9web --upstream tcp:127.0.0.1:564 # Or run directly: zig build serve -Doptimize=ReleaseSafe -- --upstream unix:/path/to/9p.sock ``` @@ -91,7 +91,7 @@ For a configured serial device: ```sh # Set raw mode, baud rate, and flow control with your platform's serial tools. -./zig-out/bin/cloud9-http --upstream file:/dev/ttyUSB0 +./zig-out/bin/9web --upstream file:/dev/ttyUSB0 ``` The serial path transports 9P bytes; it does not interpret ESP32 boot logs or @@ -174,9 +174,9 @@ checks; it does not allocate fids, mount filesystems, or implement a filesystem. Each Bridge value runs once. Timeout and cancellation stop both pumps before returning. The caller owns and closes the underlying streams. -`app/client.zig` is a browser ABI around the same `cloud9.Client` and `Stat` +`web/client.zig` is a browser ABI around the same `cloud9.Client` and `Stat` implementations. Its WASM memory is fixed at 1 MiB and it imports no host -functions. `app/web/client.mjs` supplies asynchronous WebSocket I/O and file +functions. `web/static/client.mjs` supplies asynchronous WebSocket I/O and file operations. Each browser Client owns a separate WASM instance. The JavaScript contains no 9P encoder or decoder. diff --git a/docs/validation.md b/docs/validation.md index 98b73d6..5b34b82 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -86,7 +86,7 @@ HTTP/3 is not implemented or tested. The [HTTP E2E report](results/http-e2e.json) records the scenarios and byte counts. The run's screenshots and TLS fixtures remain in `.zig-cache/e2e-3HAs6G/`. -`zig build -Doptimize=ReleaseSafe` installs the standalone `cloud9-http` binary; +`zig build -Doptimize=ReleaseSafe` installs the standalone `9web` binary; `zig build web -Doptimize=ReleaseSafe` also installs the separate browser assets. See [HTTP usage and ownership](http.md) for the reusable API and runnable gateway. diff --git a/introspect/test/adv_core_hostile.sh b/introspect/test/adv_core_hostile.sh deleted file mode 100755 index bf4f812..0000000 --- a/introspect/test/adv_core_hostile.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -# Adversarial raw-9P2000 client tests aimed at the introspect core. -# Usage: bash introspect/test/adv_core_hostile.sh <introspect> [--fast] (part of zig build introspect-adv) -# Spawns the server on a temporary unix socket and attacks it with -# introspect/test/adv_core_hostile.py (Python 3 stdlib). Exit 1 on any failure. -set -u -INTROSPECT=$(realpath "${1:?path to introspect}") -shift -command -v python3 >/dev/null || { echo "SKIP: python3 missing"; exit 0; } -exec python3 "$(dirname "$0")/adv_core_hostile.py" --server "$INTROSPECT" "$@" diff --git a/introspect/test/adv_introspect_hostile.sh b/introspect/test/adv_introspect_hostile.sh deleted file mode 100755 index 3ee2ecb..0000000 --- a/introspect/test/adv_introspect_hostile.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -# Adversarial raw-9P2000 client tests for the introspect server. -# Usage: bash introspect/test/adv_introspect_hostile.sh <introspect> [--fast] (part of zig build introspect-adv) -# Spawns the server on a temporary unix socket and attacks it with -# introspect/test/adv_introspect_hostile.py (Python 3 stdlib). Exit 1 on any failure. -set -u -INTROSPECT=$(realpath "${1:?path to introspect}") -shift -command -v python3 >/dev/null || { echo "SKIP: python3 missing"; exit 0; } -exec python3 "$(dirname "$0")/adv_introspect_hostile.py" --server "$INTROSPECT" "$@" diff --git a/introspect/test/adv_linux_probe.sh b/introspect/test/adv_linux_probe.sh deleted file mode 100755 index 3535aa9..0000000 --- a/introspect/test/adv_linux_probe.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -# Adversarial tests of the introspect Linux layer (probe loop, debug provider, -# signal machinery) with raw 9P2000 over a unix socket and one 9player mount. -# Usage: bash introspect/test/adv_linux_probe.sh <9player> <introspect> (part of zig build introspect-adv) -# Exit 1 on any failure; SKIP (exit 0) without python3. -set -u -PLAYER=$(realpath "${1:?path to 9player}") -INTROSPECT=$(realpath "${2:?path to introspect}") -command -v python3 >/dev/null || { echo "SKIP: python3 missing"; exit 0; } -exec python3 "$(dirname "$0")/adv_linux_probe.py" --player "$PLAYER" --server "$INTROSPECT" diff --git a/introspect/test/adversarial.sh b/introspect/test/adversarial.sh deleted file mode 100755 index 918bb9a..0000000 --- a/introspect/test/adversarial.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# Runs every introspect adversarial suite in sequence: hostile raw-9P clients -# against the demo (framing, tags, floods; the core's /vars, snapshots, fids) -# and the Linux layer through one 9player mount (memory, signals, poll loop). -# Usage: bash introspect/test/adversarial.sh <9player> <introspect> [--fast] -# `zig build introspect-adv` runs the same suites as separate steps. -set -u -PLAYER=${1:?path to 9player} -INTROSPECT=${2:?path to introspect} -shift 2 -HERE=$(cd "$(dirname "$0")" && pwd) -status=0 -for suite in adv_introspect_hostile adv_core_hostile; do - echo "### $suite" - if bash "$HERE/$suite.sh" "$INTROSPECT" "$@"; then echo "### $suite: ok"; else echo "### $suite: FAILED"; status=1; fi -done -echo "### adv_linux_probe" -if bash "$HERE/adv_linux_probe.sh" "$PLAYER" "$INTROSPECT"; then echo "### adv_linux_probe: ok"; else echo "### adv_linux_probe: FAILED"; status=1; fi -exit $status diff --git a/test/web/e2e.mjs b/test/web/e2e.mjs index 2ff83ae..ad10295 100644 --- a/test/web/e2e.mjs +++ b/test/web/e2e.mjs @@ -41,7 +41,7 @@ async function launchBridge(upstream, origin, extra = []) { const child = start(bridgeBinary, ['--listen', '127.0.0.1:0', '--upstream', upstream, ...(origin ? ['--origin', origin] : []), ...extra]); // With a public origin the log does not include the bound address. Tests // requiring a proxy instead reserve a port and pass it explicitly below. - const match = await wait(() => child.errors.match(/cloud9-http (http:\/\/127\.0\.0\.1:\d+)/), 'bridge ready'); + const match = await wait(() => child.errors.match(/9web (http:\/\/127\.0\.0\.1:\d+)/), 'bridge ready'); return { child, url: match[1] }; } async function port() { const s = net.createServer(); s.listen(0, '127.0.0.1'); await once(s, 'listening'); const p = s.address().port; await new Promise(r => s.close(r)); return p; } @@ -213,7 +213,7 @@ try { const httpsPort = await port(), backendPort = await port(); const origin = `https://localhost:${httpsPort}`; const secureBridge = start(bridgeBinary, ['--listen', `127.0.0.1:${backendPort}`, '--upstream', `tcp:${upstream}`, '--origin', origin]); - await wait(() => secureBridge.errors.includes('cloud9-http'), 'TLS backend ready'); + await wait(() => secureBridge.errors.includes('9web'), 'TLS backend ready'); tlsServer = tls.createServer({ key: await fs.readFile(key), cert: await fs.readFile(cert), ALPNProtocols: ['http/1.1'] }, socket => { const upstreamSocket = net.connect(backendPort, '127.0.0.1'); sockets.add(socket); sockets.add(upstreamSocket); diff --git a/app/client.zig b/web/client.zig index 2ec9163..2ec9163 100644 --- a/app/client.zig +++ b/web/client.zig diff --git a/app/main.zig b/web/main.zig index 720503f..e6aa421 100644 --- a/app/main.zig +++ b/web/main.zig @@ -29,7 +29,7 @@ pub fn main(init: std.process.Init) !void { var i: usize = 1; while (i < args.len) : (i += 1) { if (std.mem.eql(u8, args[i], "--help")) { - std.debug.print("Usage: cloud9-http [--listen IP:PORT] [--upstream tcp:IP:PORT|unix:PATH|file:PATH] [--origin https://HOST:PORT] [--timeout-ms 300000] [--user NAME] [--tree NAME]\nDefault: http://127.0.0.1:8080 -> tcp:127.0.0.1:564\n", .{}); + std.debug.print("Usage: 9web [--listen IP:PORT] [--upstream tcp:IP:PORT|unix:PATH|file:PATH] [--origin https://HOST:PORT] [--timeout-ms 300000] [--user NAME] [--tree NAME]\nDefault: http://127.0.0.1:8080 -> tcp:127.0.0.1:564\n", .{}); return; } if (i + 1 == args.len) return error.MissingArgument; @@ -65,9 +65,9 @@ pub fn main(init: std.process.Init) !void { const io = init.io; var listener = c9.transport.listen(io, .{ .tcp = address }, max_connections) catch |err| { if (err == error.AddressInUse) { - std.debug.print("cloud9-http: cannot listen on {s}: address already in use.\nUse --listen 127.0.0.1:0 to select a free port; the URL is printed at startup.\n", .{listen_text}); + std.debug.print("9web: cannot listen on {s}: address already in use.\nUse --listen 127.0.0.1:0 to select a free port; the URL is printed at startup.\n", .{listen_text}); } else { - std.debug.print("cloud9-http: cannot listen on {s}: {s}\n", .{ listen_text, @errorName(err) }); + std.debug.print("9web: cannot listen on {s}: {s}\n", .{ listen_text, @errorName(err) }); } return err; }; @@ -81,7 +81,7 @@ pub fn main(init: std.process.Init) !void { if (user.len > 256 or tree.len > 256) return error.AttachNameTooLong; const browser_config = try std.json.Stringify.valueAlloc(allocator, .{ .user = user, .tree = tree }, .{}); const config: Config = .{ .upstream = upstream, .host = host, .origin = origin_text, .public_host = origin_text[origin_uri.scheme.len + 3 ..], .timeout_ms = timeout_ms, .browser_config = browser_config }; - std.debug.print("cloud9-http {s} -> {s}\n", .{ config.origin, upstream_text }); + std.debug.print("9web {s} -> {s}\n", .{ config.origin, upstream_text }); var group: Io.Group = .init; defer group.cancel(io); while (true) { @@ -151,16 +151,16 @@ fn serve(io: Io, stream: Io.net.Stream, config: Config) !void { if (request.head.method != .GET) return respond(&request, "Use GET\n", "text/plain", .method_not_allowed); const path = std.mem.sliceTo(request.head.target, '?'); if (std.mem.eql(u8, path, "/_cloud9/config.json")) return respond(&request, config.browser_config, "application/json", .ok); - if (std.mem.eql(u8, path, "/_cloud9/app.mjs")) return respond(&request, @embedFile("web/app.mjs"), "text/javascript; charset=utf-8", .ok); - if (std.mem.eql(u8, path, "/_cloud9/client.mjs")) return respond(&request, @embedFile("web/client.mjs"), "text/javascript; charset=utf-8", .ok); - if (std.mem.eql(u8, path, "/_cloud9/style.css")) return respond(&request, @embedFile("web/style.css"), "text/css; charset=utf-8", .ok); + if (std.mem.eql(u8, path, "/_cloud9/app.mjs")) return respond(&request, @embedFile("static/app.mjs"), "text/javascript; charset=utf-8", .ok); + if (std.mem.eql(u8, path, "/_cloud9/client.mjs")) return respond(&request, @embedFile("static/client.mjs"), "text/javascript; charset=utf-8", .ok); + if (std.mem.eql(u8, path, "/_cloud9/style.css")) return respond(&request, @embedFile("static/style.css"), "text/css; charset=utf-8", .ok); if (std.mem.eql(u8, path, "/_cloud9/cloud9.wasm")) return respond(&request, @embedFile("client.wasm"), "application/wasm", .ok); if (!std.mem.eql(u8, path, "/_cloud9/9p")) { if (std.mem.eql(u8, path, "/_cloud9") or std.mem.startsWith(u8, path, "/_cloud9/")) return respond(&request, "Not found\n", "text/plain", .not_found); // File URLs boot the same browser client. The client resolves the path // in the upstream 9P namespace, never in this machine's filesystem. - return respond(&request, @embedFile("web/index.html"), "text/html; charset=utf-8", .ok); + return respond(&request, @embedFile("static/index.html"), "text/html; charset=utf-8", .ok); } if (!std.mem.eql(u8, origin orelse "", config.origin)) return respond(&request, "Unexpected Origin\n", "text/plain", .forbidden); if (!std.mem.eql(u8, version orelse "", "13")) return respond(&request, "WebSocket version 13 required\n", "text/plain", .bad_request); diff --git a/app/web/app.mjs b/web/static/app.mjs index a6985f0..a6985f0 100644 --- a/app/web/app.mjs +++ b/web/static/app.mjs diff --git a/app/web/client.mjs b/web/static/client.mjs index 69f44d2..69f44d2 100644 --- a/app/web/client.mjs +++ b/web/static/client.mjs diff --git a/app/web/index.html b/web/static/index.html index aeca5d3..aeca5d3 100644 --- a/app/web/index.html +++ b/web/static/index.html diff --git a/app/web/style.css b/web/static/style.css index a8605f5..a8605f5 100644 --- a/app/web/style.css +++ b/web/static/style.css |
