summaryrefslogtreecommitdiff
path: root/src/detached/wire.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/detached/wire.zig')
-rw-r--r--src/detached/wire.zig243
1 files changed, 10 insertions, 233 deletions
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index 8281fc63..ab19bcfb 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -1,227 +1,28 @@
-//! THE DETACHED-SESSION WIRE FORMAT: one `Event` and one `Host.VTable` call per
-//! message, byte for byte, with nothing native about the bytes.
-//!
-//! WHO OWNS THE CORE. The `Pardes` instance lives in the DETACHED process
-//! (server.zig). A frontend (client.zig) owns a terminal and a socket and
-//! nothing else: it sends the input it collects and draws the frames it is
-//! sent. One core per session, N frontends attached to it, all looking at the
-//! same screen — `screen -x`, not N sessions.
-//!
-//! WHY A CODEC AT ALL, when nested.zig's socket carries a builtin command line
-//! and has nothing to version: a command line cannot carry a frame, and frames
-//! and input are this transport's entire content.
-//!
-//! ARCHITECTURE-NEUTRAL, and not as decoration: the frontend on the other end
-//! may be riscv32-freestanding (the ESP32-P4 board) while the core is x86_64
-//! linux. So:
-//! * every integer is an explicit width, little-endian. No `usize` reaches
-//! the wire — a pointer-sized field is 4 bytes on the board and 8 here, and
-//! every field after it would then be read at the wrong offset.
-//! * no native struct is ever blitted. `@bitCast`/`std.mem.asBytes` of a Zig
-//! struct puts this compiler's field order and padding on a socket; every
-//! field below is written and read by hand.
-//! * every union and every enum gets a tag chosen HERE (`ClientTag`,
-//! `ServerTag`, `ColorTag`, ...) and never `@intFromEnum` of a core type,
-//! so reordering `Event` or `CellStyle.ul` cannot silently redefine the
-//! protocol. The mapping switches are exhaustive: adding a variant to the
-//! core is a compile error in this file, which is the point of them.
-//! * every variable-length payload carries an explicit length prefix, and
-//! `max_payload` bounds the lot. This is a parser on a socket: a malformed
-//! frame must be REFUSED, never indexed past.
-//! * a bool is one byte, 0 or 1. Any other value is a decode error rather
-//! than "nonzero is true": a byte this protocol cannot mean is evidence
-//! the stream is not the stream it claims to be.
-//! * floats travel as their IEEE-754 binary32 bit pattern inside an explicit
-//! u32. Both ends agree about binary32; neither agrees about struct layout.
-//!
-//! BUILD-NEUTRAL for the same reason. `Event.resize.cell_pixels` exists only
-//! when native PDF placement is compiled in (pardes.zig `CellPixels`), and a
-//! frontend must not have to have been built with the core's options — so it is
-//! ALWAYS on the wire and dropped on arrival by a build with nowhere to put it.
-//!
-//! WHAT IS NOT HERE. The seam has twenty-two methods; this carries FIVE of
-//! them — `push_present` as `frame`, `push_set_clipboard`,
-//! `pull_read_clipboard`, `push_open_link` and `push_detach` — and the
-//! seventeen it does not are named here with their reasons. The five are
-//! spelled out because this arithmetic has now gone stale twice in one day,
-//! once when the machine-local eight moved into the daemon and once when
-//! `detach` arrived, and a count nobody can check against a list is a comment
-//! that rots quietly.
-//! * The nine machine-local ones — `push_spawn`, `push_pty_write`,
-//! `push_pty_resize`, `push_pty_signal`, `push_write_file`,
-//! `push_write_dump`, `push_watch_file`, `push_watch_theme`,
-//! `push_dump_themes` — are
-//! performed by the detached core ITSELF, through `host_io.zig`. A unix
-//! socket means it is on the same machine, so there is no question of
-//! whose disk or whose process table is meant, and a pane's shell has to
-//! outlive the frontend that asked for it or a detached session is a
-//! promise it cannot keep. The `ServerTag` doc below carries the whole of
-//! that argument; this line exists so the count at the top of the file
-//! agrees with it.
-//! * `pull_wait_input` IS the server's poll loop, not a message.
-//! * `push_poll_frame` and `push_post_present` carry no information. They are
-//! per-frame bookkeeping ticks, and `frame` already arrives exactly once
-//! per pump at the same place in the order — a frontend does its per-frame
-//! work when a frame lands. Two more messages per frame per client would
-//! say nothing the frame does not already say.
-//! * `pull_tty_taken` and `pull_gpio_toggle` are answers the CALLER waits
-//! for, and `pull_lsp`/`pull_pipe` are work dispatched off the loop. A
-//! round trip inside `update` is the one thing this transport must never
-//! do: the core would block on a socket, and `pull_wait_input`'s own
-//! comment is that it is the only place this process may sleep. The
-//! process that owns the core answers all four.
-//! * `push_fs_reply` cannot be a broadcast. host.zig's rule is that the
-//! transport which asked is the one holding the request; with N frontends,
-//! N-1 would receive the answer to a request they never made. So the acme
-//! mount stays in the detached process, where the `Event.fs_req` that
-//! starts it is raised, and neither half of that pair is on the wire —
-//! which is also why `Event.fs_req` has no `ClientTag`.
+//! Detached input and frames use fixed-width little-endian fields and explicit tags.
+//! Native struct layout never reaches the wire.
const std = @import("std");
const pardes = @import("../pardes.zig");
-/// Bumped whenever any layout below changes. Checked on connect and refused
-/// loudly (see `Refusal.version`): two builds of pardes are routinely on one
-/// machine — `zig build` replaces the binary under a running session — and a
-/// frontend decoding another version's frame layout would paint garbage and
-/// blame the terminal.
-///
-/// 2: the layout did not move, but what a frontend is ALLOWED TO ASK FOR did.
-/// A frontend now clamps its window to `max_cols` x `max_rows` instead of
-/// sending it raw (client.zig), and a 512x128 grid is a full frame a v1 daemon
-/// PANICS encoding — its run length overflowed a u16 by exactly one cell, see
-/// `run_max`. A v1 session refused that geometry outright, so nothing was ever
-/// lost by refusing the connection instead; a v1 daemon meeting a v2 frontend
-/// answers `Refusal.version`, which says so, rather than dying with every pane
-/// shell it owns. This is the case the paragraph above is about: `zig build`
-/// replaces the binary under a running session.
pub const version: u16 = 2;
-pub const Error = error{
- /// The message ended inside a field.
- Truncated,
- /// A length prefix, a run, or a grid dimension larger than this protocol
- /// admits. Refused before anything is allocated or indexed.
- Overlong,
- /// A tag byte no version of this protocol has ever defined.
- BadTag,
- /// A tag this protocol does define, carrying a value it cannot mean: a
- /// 3-in-a-bool, a zero-column resize, a pane past MAX_PANES.
- BadValue,
- /// The payload was decoded and bytes were left over. A message that says
- /// more than its layout has room for is not this message.
- Trailing,
- /// The encoder ran out of caller-supplied buffer.
- NoSpace,
-};
-
-// ---------------------------------------------------------------------------
-// bounds
-// ---------------------------------------------------------------------------
+pub const Error = error{ Truncated, Overlong, BadTag, BadValue, Trailing, NoSpace };
-/// The largest grid this protocol carries. `Surface.cols`/`rows` are u16, so
-/// these are protocol bounds rather than type bounds, and they exist because
-/// `max_payload` below is derived from them: a decoder that accepts 65535
-/// columns accepts a 25 GiB frame prefix. The board's own grid is 56x14 and a
-/// terminal's is usually near 200x50.
-///
-/// NOT a ceiling above every real display, which is what this comment used to
-/// claim: a 4K window at the SDL shell's minimum 8-pixel font is around 768
-/// columns by 216 rows, and a tty on the same screen passes 128 rows at any
-/// ordinary line height. Those windows attach at 512x128 and letterbox the
-/// rest (client.zig clamps), rather than being refused as they were. Raising
-/// the pair instead would have been a bigger change than it looks: `frameBound`
-/// stays well inside `max_payload`, but a themed full frame at 512x128 is
-/// already ~0.9 MiB against server.zig's 1 MiB `out_backlog`.
pub const max_cols: u16 = 512;
pub const max_rows: u16 = 128;
+pub const max_payload: u32 = 16 << 20;
+pub const header_len = 5; // tag:u8, payload length:u32le
-/// One cell at its largest: `default` false, a 7-byte grapheme, two rgb colors,
-/// the attribute byte, the underline style and the font role. Written as the
-/// sum of the fields rather than a number so that adding a field to `Cell`
-/// moves it.
const cell_max = 1 + 1 + 7 + 4 + 4 + 1 + 1 + 1;
-
-/// `start:u32 + count:u16`. A run's cost, and therefore the break-even the
-/// encoder coalesces against (see `encodeFrame`).
-const run_header = 4 + 2;
-
-/// ...and the longest run that `count:u16` can describe, which is EXACTLY ONE
-/// SHORT of the largest grid this protocol carries: `max_cols * max_rows` is
-/// 512*128 = 65536, and `maxInt(u16)` is 65535.
-///
-/// A full frame of that grid is ONE run over all of it whenever the theme has
-/// a background: `render` fills the surface and every pane then repaints its
-/// text area, and `Surface.set` clears `default`, so `sendCell`'s `!default`
-/// holds for every cell. (Under a theme with `bg = null` — `dark` — untouched
-/// body cells stay default and a stretch of six of them breaks the run, so the
-/// overflow was theme-dependent as well as geometry-dependent, which is the
-/// worst kind of latent.) `encodeFrame`'s `@intCast(run_end - start)` then
-/// panicked in a safe build and was illegal behaviour in a fast one — LLVM
-/// happens to truncate to zero, which the far side refuses as `BadValue`, but
-/// nothing promises that. That grid is what a frontend with a big window now
-/// asks for (client.zig clamps to it), so the meeting point went from
-/// unreachable to routine, and the encoder splits the run instead.
+const run_header = 4 + 2; // start:u32, count:u16
const run_max = std.math.maxInt(u16);
-
-/// `kind:u8 + cols:u16 + rows:u16 + cursor(6) + nruns:u32`.
const frame_head = 1 + 2 + 2 + 6 + 4;
-/// The longest legal payload, and therefore the length prefix a decoder will
-/// accept before it refuses the stream. Two messages set it:
-/// * a full frame of the largest grid, worst case one run per cell:
-/// 512*128 * (6 + 20) = 1.6 MiB.
-/// * one paste, which the tty frontend already caps at 4 MiB (tty.zig
-/// `max_paste_bytes`) on the grounds that anything larger is a mis-click.
-/// 16 MiB is past every source file anyone edits in this editor and is still a
-/// buffer the receiving side can simply hold. A larger message is not sent and
-/// a larger prefix is not read.
-pub const max_payload: u32 = 16 << 20;
-
-/// Every message is `tag:u8, len:u32le, payload[len]`. A u32 because a full
-/// frame and a paste both pass 64 KiB; a u16 would have needed the frame split
-/// across messages, which is a second framing layer for no gain.
-pub const header_len = 5;
-
-/// Bytes `encodeFrame` may need for this grid, worst case: every cell changed,
-/// every cell in a run of its own, every cell at `cell_max`. The server sizes
-/// one buffer from this per geometry rather than guessing.
+// Worst case: every cell changed, each in its own run.
pub fn frameBound(cols: u16, rows: u16) usize {
return header_len + frame_head + @as(usize, cols) * @as(usize, rows) * (run_header + cell_max);
}
-// ---------------------------------------------------------------------------
-// tags
-// ---------------------------------------------------------------------------
-
-/// Frontend -> core. Exhaustive on purpose, which is the opposite of
-/// fuse.zig's `Opcode`: there, a newer KERNEL adds opcodes and a non-exhaustive
-/// enum is the only way to receive one without undefined behaviour. Here both
-/// ends are pardes and an unknown tag is not a newer peer — `version` already
-/// refused that — so it is a corrupt or hostile stream and must be rejected.
-/// `std.enums.fromInt` is how, at the one place a byte becomes a tag.
-///
-/// The numbers are the PROTOCOL's, grouped session/input rather than derived
-/// from `Event`'s declaration order, so reordering the union changes nothing.
-///
-/// EVERY TAG HERE IS SOMETHING A HUMAN DID, and that is the whole set: a
-/// handshake, a goodbye, and what a keyboard, a mouse, a trackpad or a window
-/// manager produces. Six numbers are missing from the input run — 0x13..0x17
-/// and 0x1e — and the gaps are left rather than tidied away, because
-/// renumbering is a change every deployed frontend feels. They were `output`,
-/// `eof`, `lsp_resp`, `pipe_resp`, `file_changed` and `tick`: the
-/// MACHINE-LOCAL host's own reports, which stopped being a frontend's business
-/// when the daemon took the disk and the process table (host_io.zig,
-/// file_watch.zig). No frontend ever produced one — tty.zig's attached loop
-/// swallowed them by name and gui.zig never handed `Input.post` one — and
-/// leaving them DECODABLE was not merely dead weight: server.zig's `apply`
-/// routes any decoded non-resize event straight into `core.update`, so an
-/// attached peer could forge a pane's output, forge an `eof` for a shell that
-/// was still running (and unlike the daemon's own `paneEof` the wire path never
-/// called `closePty`, so the master stayed open and the shell was orphaned for
-/// the life of the session), or replace a pane's text with bytes the next
-/// `Save` would write to disk. client.zig's header says a machine-local effect
-/// cannot return to the wire; deleting these is what makes that true in BOTH
-/// directions instead of only core -> frontend.
+// Stable wire numbers; gaps are retired tags. Unknown tags are rejected by the decoder.
pub const ClientTag = enum(u8) {
hello = 0x01,
bye = 0x02,
@@ -237,23 +38,7 @@ pub const ClientTag = enum(u8) {
pointer_leave = 0x1d,
};
-/// Core -> frontend. 0x01..0x0f is the session; 0x10.. is one `push_` method
-/// each, in `Host.VTable`'s own order so the two lists can be read side by
-/// side.
-///
-/// There are only THREE of those left, and which three is the whole design.
-/// The daemon performs every effect that needs a disk or a process table
-/// itself (see `host_io.zig`): a unix socket means it is on the same machine,
-/// so there is no question of whose disk is meant, and a pane's shell has to
-/// outlive the frontend that asked for it or a detached session is a promise
-/// it cannot keep. What is left on the wire is what a process nobody is
-/// looking at genuinely cannot do — put something on THIS human's clipboard,
-/// read it back, and open a link in front of the person who clicked it.
-///
-/// `detach` is in the SESSION range and not among those three on purpose: it is
-/// not an effect the core wants performed, it is the session telling one
-/// frontend that it is done. `quit` is its sibling — same shape, opposite
-/// meaning about whether anything survives.
+// Session control precedes display-local effects.
pub const ServerTag = enum(u8) {
welcome = 0x01,
refuse = 0x02,
@@ -880,15 +665,7 @@ fn sendCell(cells: []const pardes.Cell, prev: []const pardes.Cell, full: bool, i
fn clientTag(msg: ClientMsg) ClientTag {
return switch (msg) {
.event => |ev| switch (ev) {
- // SEVEN `Event`s a frontend cannot produce, so no `ClientTag`
- // exists for them and this arm is where the compiler says so.
- // `fs_req` is the acme mount, raised in the same process that
- // answers it. The other six are the machine-local host's own
- // reports — a pty's output and its EOF, a language or pipe worker's
- // answer, a watched file's new bytes, an animation tick — and after
- // the daemon took the disk and the process table every one of them
- // is raised by the process that already holds the core. See
- // `ClientTag` for what putting them back would let a peer forge.
+ // Machine-local reports and 9P requests belong to the session owner.
.output, .eof, .lsp_resp, .pipe_resp, .file_changed, .tick, .fs_req => unreachable,
inline else => |_, t| @field(ClientTag, @tagName(t)),
},