summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/design.md47
-rw-r--r--docs/http.md99
2 files changed, 133 insertions, 13 deletions
diff --git a/docs/design.md b/docs/design.md
index 3ad9338..f34a3ea 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -173,6 +173,53 @@ that waits on another thread must stop waiting once `stop()` has begun.
Unix socket paths are the application's: the runner neither unlinks,
chmods nor removes them.
+# Multiplexer (9web)
+
+`9web` (`web/`) is a gateway and a 9P multiplexer. It keeps **one** upstream
+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 at
+`/_cloud9/9p`, plain 9P clients over an optional `--serve` TCP/Unix listener,
+and an HTTP view of the tree under `/fs/`. `web/mux.zig` owns the shared
+upstream; `web/httpfs.zig` is the HTTP view; `web/probe.zig` embeds 9proc under
+`--probe`.
+
+The mux is a **frame-level remux**, not an `fs.Server`/`serve.Runner` backend.
+Each downstream request is forwarded upstream on a remapped tag with its fids
+remapped into the shared upstream fid space, and each upstream reply is routed
+back by tag. `Tversion` is answered locally (the upstream is negotiated once at
+connect); `Tattach` is forwarded, so each downstream gets its own upstream tree
+root; `Tflush` is forwarded as `Tflush`. Downstream fid spaces are isolated by
+construction — two downstreams never share an upstream fid. The choice is
+deliberate: 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, iounit and qids
+and turn the readdir byte offset into an index mapping. Forwarding frames
+unchanged is exactly the "forward each request upstream" contract, the shape
+plan9port's 9pserve has, and it keeps the upstream's bytes intact. `serve.Runner`
+remains the right tool for a *server* whose backend is a real filesystem; a
+transparent *proxy* is not that.
+
+The shared upstream runs one reader task and any number of downstream forwarder
+tasks under one mutex; the tag, fid and pending tables are comptime-sized and
+nothing allocates per request. Sockets are only written under the mutex with the
+client's own output buffer (disjoint from the stream writer's), and replies are
+encoded into per-tag buffers and sent to downstreams outside the lock so a slow
+downstream cannot stall the client. The WebSocket and `--serve` downstreams
+drive the mux the same way — each reads whole frames (WebSocket binary messages
+or `transport.readFrame`) and calls `forward`; the runner's socket-only listener
+is not used for them because WebSocket framing and the HTTP `/fs` view do not fit
+it. When all sixteen upstream tags are in flight, a further downstream request
+waits for one to free (Tflush keeps its reserved seventeenth tag). On an upstream
+I/O failure the reader fails every outstanding request, reconnects, bumps a
+generation, and answers any downstream request that still names a pre-reconnect
+fid with EIO until it re-attaches.
+
+The HTTP `/fs` view uses the same shared upstream at the fid level through a
+blocking `rpc`: it allocates upstream fids from the mux pool and walks, stats,
+reads and writes directly, so `GET`/`PUT`/`DELETE`, directory JSON, `Range` and
+`?follow=1` server-sent events all ride the one upstream connection. A parked
+`follow` read is cancelled with an upstream `Tflush` when its client goes away.
+
# Related programs
Programs built on the library ship from this repository as `cloud9/<name>/`,
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: