summaryrefslogtreecommitdiff
path: root/docs/http.md
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-20 03:53:17 -0300
committerGabriel Schneider <[email protected]>2026-09-20 03:53:17 -0300
commitf1b53c1533539aecbf16ad19fd9156deae091f92 (patch)
tree38ff00810ef5e0ad271c26e218cf6af4578c71ce /docs/http.md
parentba7ec40782ba7020d82a56896a5eb1b52578d6aa (diff)
downloadcloud9-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.md99
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: