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

Every native session serves 9P2000 on a Unix socket. Pane shells receive
`PARDES_9P` (socket path) and `PARDES_PANE` (pane serial). The socket is
`$XDG_RUNTIME_DIR/pardes-9p-<pid>.sock`, or lives under
`~/.local/state/pardes` when XDG_RUNTIME_DIR is unset. Detached sessions use
their session name; `--9p=<name>` overrides it.

A `pardes <file>` launched from a pane forwards Look to that pane over 9P.
`--nested` opens a separate editor and disables forwarding from its pane shells;
its 9P service stays available.

Look resolves the OS filesystem first, then the editor's virtual filesystem.
Explicit paths bypass that search:

| Editor path | Meaning | 9P server path |
|---|---|---|
| `/n/os/proc/self` | OS filesystem | `/os/proc/self` |
| `/n/self/pane/2/body` | pane 2's text | `/pane/2/body` |
| `/virtual/pane/2/body` | the same, in the editor's own spelling | `/pane/2/body` |
| `/virtual/src/pardes.zig` | source embedded in this build | `/src/pardes.zig` |
| `/n/peer/pane/2/body` | another session's text | peer's `/pane/2/body` |

The mount name `self` is reserved and maps to the server root, so `/n/self/X`
and `/virtual/X` both name the served `/X`.

`--mount=peer=work` mounts the named session `work`; the dial can also be an
absolute socket path, `unix!/path`, `tcp!IP!port`, or `quic!IP!port`.
At runtime, use `Mount peer dial` and
`Unmount peer`. There are eight named mounts; `os` and `self` are reserved.
Unmount refuses mounts still used by a pane, its working directory, or a
pending Save. Mounts are saved in dumps. Save uses the file's original mount.

`pardes --9p-tcp='tcp!127.0.0.1!5640'` adds a TCP listener alongside the Unix
socket. Build with `-Dquic=true` and system OpenSSL 3.6+ to enable QUIC;
`--9p-quic='quic!127.0.0.1!5641'` adds its listener. Both accept numeric
IPv4/IPv6 addresses, not DNS names. Listener port zero chooses a free port;
`/listeners` reports all active dial addresses.

All connections have session access, including `os`. TCP is unencrypted.
QUIC uses an ephemeral TLS identity without peer verification or login.
It carries 9P2000 on one bidirectional stream with ALPN `pardes-9p`.
Unix and TCP connections share four slots served by cloud9's `std.Io`
runner; QUIC has four of its own on the editor's poll loop. OpenSSL's
internal buffers are separate, dynamically allocated memory.

[Plan9port's client](https://9fans.github.io/plan9port/man/man1/9p.html) can
drive Unix or TCP without a kernel mount, and `9ns` mounts the tree in a
private namespace:

```sh
9p -n -a "unix!$PARDES_9P" read index
9p -n -a 'tcp!127.0.0.1!5640' ls pane/1
9ns --unix "$PARDES_9P" -- sh -c 'cat "$NINE_MOUNT/index"'
```

For [Linux v9fs](https://www.kernel.org/doc/html/latest/filesystems/9p.html),
use `version=9p2000,cache=none,access=any` and `trans=unix`, or `trans=tcp`
with `port=5640`. Set `uname`, `dfltuid`, and `dfltgid` for the local user.
Leave `aname` empty. The opt-in
[Linux v9fs experiment](v9fs.md) tests a kernel mount in a separate subprocess
namespace (`zig build v9fs-test`, requiring explicit mount authorization).
Neither 9P2000.u nor 9P2000.L is implemented.
Existing Plan9port/v9fs clients need a userspace bridge for QUIC.

## The served tree

```
/README      this guide, also src/fs-help.txt
/index       one line per pane: serial, kind (text|term|pdf|image), dirty flag, name
/ctl         write: one command per line; read: the serials the last command made or touched
/new         reading it creates one empty pane and answers "<serial>\n" (the /net/tcp/clone idiom)
/log         one line per editor event: new|del|rename|save <serial> <name>; reads park
/screen      rendered screen JSON; frozen per open handle
/listeners   the session's dial addresses
/pane/<n>/   name body tag ctl addr data xdata sel errors event, plus pty/{ctl,status,data}
/os/         the host filesystem
/src/        the editor's embedded sources, only when built with -Dembed-sources=true
```

Nothing in the tree is created by list, stat, walk or read, except that
reading `/new` makes a pane; that is its whole purpose, so a recursive read
of the tree makes one pane per open of it.

`/ctl` and `/pane/<n>/ctl` take the editor's own command language, two verbs:

- `look TEXT` is a right click on `TEXT`: a path opens a file, `file:12` jumps
  to a line, a directory opens a shell there, a URL opens in the browser. From
  `/ctl` the look happens at the active pane; from a pane's `ctl` at that pane.
- `exec TEXT` is a middle click: a command word from `src/builtins.zig`
  (`Save`, `Del`, `New`, `Newcol`, `Mount NAME DIAL`, `Unmount NAME`, `Dump`,
  `Restore`, `Msg TEXT`, `Find`, `Grep`, `Tty`, ...), or anything else, which
  runs in the pane's terminal.

Both verbs are lowercase; the words after `exec` are the editor's capitalized
commands. A pane's `ctl` also keeps acme's addr verbs (`addr=dot`, `dot=addr`,
`limit=addr`, `clean`, `dirty`, `cleartag`, `get`, `mark`, `nomark`,
`noscroll`, `scroll`, `show`). Every line of a write is checked before any
line runs; a malformed line fails the write with EINVAL. A command that fails
inside the editor is reported on the message row, not as a write error.

After a write to either `ctl`, reading `/ctl` answers the serials of the panes
the command created, or, when it created none, the pane a look focused or the
pane an exec acted on (even one it closed), one per line. Before any command it answers `pid`, `version` and `panes` lines.

`/pane/<n>/name` reads the pane's file name (a terminal's directory) and
writing it renames the buffer; a relative name resolves against the pane's
directory. `sel` reads the editor selection and writing it replaces the
selection. `body` appends on write and replaces on truncating open. `addr`
selects a range and `data` or `xdata` read or replace it. `errors` appends to
the directory's `+Errors` pane. Holding `event` open redirects the pane's Look
and Exec clicks to that client; writing a record back performs the action.

Stats report real lengths for `index`, `ctl`, `name`, `body`, `tag` and `sel`,
modes 0644/0666 (0444 for read-only files, 0222 for write-only), the pane's
last edit time or the process start as mtime, and stable qids. Directory
entries carry no sizes; stat the entry.

`/log` records `new`, `del`, `rename` and `save` while at least one client
holds it open; a read parks until a record arrives. `/screen` returns JSON with
`cols`, `rows`, `cursor`, a `styles` table, and row-major `cells` of
`[grapheme, style_index]`. Each open freezes one frame until close. A
terminal `body` freezes its history on the first read of each open handle;
`pty/data` streams live output. Screen and terminal-body snapshots share 32
handle slots, released on close or disconnect.

`-Dembed-sources=true` embeds the editor's sources and serves them under
`/src` (and `/shaders` on GUI builds). `EffectCode <effect>` lists the current
backend's implementation files under `/virtual`, which Look opens; without the
option the command reports the sources as unavailable. The esp32p4 build
enables the option by default, so the device can serve its own source.

This is a control filesystem, not a complete POSIX export. Native filenames
may contain up to 255 bytes. Existing regular OS files support read, write,
and truncation to zero; protocol create, remove, rename, and other metadata
changes are refused. Ownership and permissions under `/os` are synthetic.
Zero-length truncation accepts the accompanying `mtime` hint sent by Linux
v9fs; the hint is not stored. Standalone timestamp changes remain refused.

The tree lives in `src/ninep/`: `tree.zig` (nodes, lookup, readdir, dispatch,
and the editor's reply payload over cloud9's backend contract), `pane.zig`
(pane files), `ctl.zig`, `addr.zig`, `pty.zig`, `events.zig` (event and log
streams), `screen.zig` and `sources.zig`. The protocol engine is cloud9's
`fs.Server`, configured in `src/9p.zig` (the editor's and the board's
capacities); the transports are `src/9p_io.zig` (cloud9's `serve.Runner`
for Unix and TCP, a poll loop for QUIC, and the 9P client for mounts);
`src/fs.zig` keeps host access, mounts, resolution, find and grep.

`zig build fs-test` drives real sessions using the independent Python client
in `test/ninep.py`; `zig build fs-discovery-test` checks that browsing creates
nothing and that `new`, `ctl`, `name`, `sel` and `log` behave. `zig build
9p-test` checks the two engine configurations' budgets (the engine's own
tests are cloud9's `zig build test`); `zig build fs-bench` measures
filesystem transactions in the core.
`zig build fs-test quic-test -Dquic=true` also exercises QUIC mounts and I/O.