diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-20 03:53:17 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-20 03:53:17 -0300 |
| commit | f1b53c1533539aecbf16ad19fd9156deae091f92 (patch) | |
| tree | 38ff00810ef5e0ad271c26e218cf6af4578c71ce /docs/http.md | |
| parent | ba7ec40782ba7020d82a56896a5eb1b52578d6aa (diff) | |
| download | cloud9-f1b53c1533539aecbf16ad19fd9156deae091f92.tar.gz cloud9-f1b53c1533539aecbf16ad19fd9156deae091f92.zip | |
9web: multiplexer, HTTP view of the tree, live streams, richer page
One upstream 9P connection now serves any number of browser WebSocket
sessions and, with --serve, plain 9P clients over TCP or Unix (a 9pserve-
style frame remux: tags and fids remapped, Tflush forwarded, reconnect on
upstream loss). /fs/<path> maps HTTP onto the tree: GET file or directory
(JSON or HTML), Range, HEAD with 9P headers, PUT (create, truncate, append,
trailing slash makes a directory), DELETE, and ?follow=1 or text/event-stream
turning a blocking read into server-sent events with Tflush on disconnect.
The page gains a lazy tree, stat panel, create, rename, delete, upload and
follow mode. --probe embeds 9proc for self-introspection. New mux-test and
http-fs-test steps; e2e still passes.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'docs/http.md')
| -rw-r--r-- | docs/http.md | 99 |
1 files changed, 86 insertions, 13 deletions
diff --git a/docs/http.md b/docs/http.md index 4e92e5e..5b51e2f 100644 --- a/docs/http.md +++ b/docs/http.md @@ -4,14 +4,84 @@ cloud9 provides a reusable HTTP transport and a standalone browser gateway. The transport carries unchanged base 9P2000 messages. Filesystem permissions, mounting, UART configuration, and board drivers remain with their applications. +`9web` is a **multiplexer**: it holds one upstream 9P connection (a single +`cloud9.Client`, all sixteen tags) and fans it out to any number of downstream +sessions — the browser WASM client over a WebSocket, plain 9P clients over an +optional TCP/Unix listener (a mux like plan9port's 9pserve), and an HTTP view +of the tree under `/fs/`. Each downstream keeps its own fid space; two +downstreams never share an upstream fid. On an upstream failure the gateway +reconnects and invalidates downstream fids (their next request answers EIO +until they re-attach). + ```mermaid flowchart LR - Browser[Browser: cloud9 WASM client] -->|WebSocket / HTTPS| Gateway[HTTP gateway] + Browser[Browser: cloud9 WASM client] -->|WebSocket| Gateway[9web mux] Native[Native cloud9 client] -->|WebSocket / HTTPS| Gateway - Gateway -->|TCP or Unix stream| Server[9P server] - Gateway -->|Serial byte stream| Board[9P firmware / ESP32] + Curl[curl / fetch / SSE] -->|HTTP /fs| Gateway + P9[9ns / plan9port 9p] -->|TCP / Unix 9P| Gateway + Gateway -->|one shared TCP / Unix / serial connection| Server[9P server] +``` + +## The multiplexer + +Downstream requests are forwarded upstream on remapped tags with their fids +remapped into the shared upstream fid space; upstream replies are routed back to +the originating downstream by tag. `Tversion` is answered locally (the upstream +session is negotiated once at connect); `Tattach` is forwarded so every +downstream gets its own upstream tree root; `Tflush` is forwarded as `Tflush`. +Frames are forwarded unchanged, so directory reads, qids, iounits and stat +fields keep the upstream's exact values — the mux does not re-derive them. + +This is deliberately a frame-level remux rather than an `fs.Server`/`serve.Runner` +backend: the file-server engine re-decomposes each request into filesystem +operations and re-encodes directories with synthetic stats and an entry-index +cursor, which would lose the upstream's real directory stats and complicate the +readdir byte offset. Forwarding requests unchanged is exactly the "one upstream, +many downstreams" contract. See [design.md](design.md#multiplexer-9web). + +The tables are comptime-sized and nothing allocates per request: the shared +upstream has one reader task and any number of downstream forwarder tasks +serialized by one mutex, sixteen ordinary tags plus one flush tag, a fixed +upstream fid pool, and a bounded downstream-connection table. When all sixteen +upstream tags are in flight, further downstream requests wait for one to free. + +## The HTTP view of the tree + +`/fs/<path>` maps HTTP onto the 9P tree — file operations, not verbs: + +| Request | 9P | Result | +| --- | --- | --- | +| `GET /fs/<file>` | walk, open, read | bytes; `Content-Type` sniffed (`text/plain` or `application/octet-stream`); `Range` honored (206) | +| `GET /fs/<file>?follow=1` | walk, open, blocking reads | `text/event-stream`; each read that returns data is one SSE `data:` event; also triggered by `Accept: text/event-stream` | +| `GET /fs/<dir>` | walk, open, read | JSON array of `{name, dir, length, mode, mtime, qid}`; an HTML listing with `Accept: text/html` | +| `HEAD /fs/<path>` | walk, stat | headers: `content-length`, `x-9p-qid`, `x-9p-mode`, `x-9p-mtime` | +| `PUT /fs/<file>` | walk or create, open, write | write; creates if missing, truncates unless `?append=1` | +| `PUT /fs/<dir>/` | walk, create | create a directory (trailing slash) | +| `DELETE /fs/<path>` | walk, remove | remove | + +A 9P `Rerror` maps to a status with the ename in the body: *no such +file* → 404, *permission* → 403, *exists* / *not empty* / *is a directory* → +409, otherwise 500. Path components are percent-decoded once; encoded slashes, +NULs and malformed escapes are rejected. `..` is refused. + +Verify against a running `9proc-demo` upstream: + +```sh +9proc-demo --unix /tmp/up.sock & +9web --listen 127.0.0.1:8080 --upstream unix:/tmp/up.sock +curl -s localhost:8080/fs/ # directory JSON +curl -s localhost:8080/fs/runtime/fn/now # a live file +curl -s -X PUT --data-binary hi localhost:8080/fs/scratch/x # write +curl -N localhost:8080/fs/self/log # follow a blocking file (Pardes) ``` +## Self-introspection + +`--probe unix:PATH` (off by default) embeds [9proc](../9proc/README.md) and +serves the gateway's own live counters as files — `/runtime/fn/{connections, +downstreams,upstream,reconnects,negotiated}` — plus `/threads`, on a private 9P +listener. Read-only; serve it on a Unix socket or loopback. + ## Run ```sh @@ -71,21 +141,24 @@ Options: | Option | Default | Meaning | | --- | --- | --- | | `--listen IP:PORT` | `127.0.0.1:8080` | HTTP listener; IPv4 or bracketed IPv6 | -| `--upstream tcp:IP:PORT` | `tcp:127.0.0.1:564` | A new 9P connection per WebSocket | -| `--upstream unix:PATH` | — | A new Unix connection per WebSocket | -| `--upstream file:PATH` | — | An already configured duplex device, one session at a time | +| `--upstream tcp:IP:PORT` | `tcp:127.0.0.1:564` | The one shared upstream 9P connection (TCP) | +| `--upstream unix:PATH` | — | The one shared upstream 9P connection (Unix) | +| `--upstream file:PATH` | — | An already configured duplex device (serial), shared by all downstreams | +| `--serve tcp:IP:PORT\|unix:PATH` | — | Also serve plain 9P downstreams (for 9ns / plan9port clients) | +| `--probe unix:PATH\|tcp:IP:PORT` | — | Embed 9proc for self-introspection (off by default) | | `--origin SCHEME://HOST[:PORT]` | Listener origin | Public origin when a reverse proxy serves the gateway | | `--user NAME` | `user` | Browser's 9P attach user (`uname`), at most 256 bytes | | `--tree NAME` | empty | Browser's named export (`aname`), at most 256 bytes | -| `--timeout-ms N` | `300000` | Maximum connection lifetime, including HTTP headers; 0 disables it | +| `--timeout-ms N` | `300000` | Maximum HTTP/WebSocket connection lifetime; 0 disables it | -The gateway bounds concurrent HTTP/WebSocket connections at 32, HTTP headers at +The gateway bounds concurrent downstream connections at 64, HTTP headers at 8 KiB, and 9P frames at 64 KiB. Excess connections close. A connection ending or -expiring cancels both relay directions before releasing buffers and descriptors. -Each network connection has a separate upstream fid/tag space. A device lease -prevents multiple clients from mixing transactions on one physical UART stream. -A new serial session starts with Tversion; the device owner is responsible for -link reset/recovery if an interrupted physical link leaves stale bytes in transit. +expiring drops the session and reclaims its upstream fids. Downstream fid spaces +are isolated by the mux; the one shared upstream connection carries them all, +which also means one serial link is multiplexed rather than leased — the mux +serializes transactions over it. The upstream session starts with a single +Tversion at connect; the device owner is responsible for link reset/recovery if +an interrupted physical link leaves stale bytes in transit. For a configured serial device: |
