fcf6547b
refactor: the daemon's comments state the rule, not its provenance
a73x 2026-08-30 19:24
Commit message
docscheck.blocks
| Old | New | ||
|---|---|---|---|
| @@ -37,7 +37,7 @@ server_test_modes.zig 11 | |||
| 37 | server_test_quic.zig 6 | 37 | server_test_quic.zig 6 |
| 38 | server_test_session.zig 18 | 38 | server_test_session.zig 18 |
| 39 | server_test_upgrade.zig 1 | 39 | server_test_upgrade.zig 1 |
| 40 | server.zig 108 | 40 | server.zig 17 |
| 41 | shellint.zig 7 | 41 | shellint.zig 7 |
| 42 | sockpath.zig 5 | 42 | sockpath.zig 5 |
| 43 | spawn.zig 0 | 43 | spawn.zig 0 |
src/server/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,13 +1,9 @@ | |||
| 1 | //! The daemon core (`mux d start`): up to max_sessions sessions (engine + pty + command | 1 | //! The daemon core (`mux d start`): up to `max_sessions` sessions, each an |
| 2 | //! tracker each); a connection is a session, named at attach. One listener | 2 | //! engine, a pty and a command tracker, named at attach. One listener on the |
| 3 | //! on the unix socket, and a second on UDP when QUIC is configured. | 3 | //! unix socket and a second on UDP when QUIC is configured. Every state |
| 4 | //! Up to max_clients attached interactive clients plus a few one-shot | 4 | //! update broadcasts to that session's clients, and each grid follows its |
| 5 | //! observer connections (dump, stats, status, endpoint, stop). Every state | 5 | //! most recently active one (latest wins). Single-threaded: `pumpOnce` is one |
| 6 | //! update is broadcast to that session's attached clients; each grid follows | 6 | //! poll iteration, so tests can drive the loop. |
| 7 | //! its most recently active client — typing, attaching or resizing claims | ||
| 8 | //! it (latest wins). | ||
| 9 | //! Single-threaded; pumpOnce is one poll iteration so tests can drive the | ||
| 10 | //! loop. | ||
| 11 | const std = @import("std"); | 7 | const std = @import("std"); |
| 12 | const Engine = @import("term").engine.Engine; | 8 | const Engine = @import("term").engine.Engine; |
| 13 | const Pty = @import("pty").Pty; | 9 | const Pty = @import("pty").Pty; |
| @@ -46,12 +42,9 @@ pub const Observer = struct { | |||
| 46 | since_ms: i64, | 42 | since_ms: i64, |
| 47 | }; | 43 | }; |
| 48 | 44 | ||
| 49 | /// How long an observer may hold a slot without completing a frame. A | 45 | /// How long an observer may hold a slot without completing a frame. Without |
| 50 | /// one-read-then-silence peer no longer parks the daemon, but it would | 46 | /// it, four silent peers close every later attach at accept with no |
| 51 | /// hold one of `max_observers` slots for good — four of them and every | 47 | /// diagnostic. 10 s is for a `--via` relay's first frame over a slow link. |
| 52 | /// later attach is closed at accept with no diagnostic. Local tools | ||
| 53 | /// speak within milliseconds; 10 s is for a `--via` relay's first frame | ||
| 54 | /// crossing a slow link. | ||
| 55 | pub const observer_idle_ms_default: i64 = 10_000; | 48 | pub const observer_idle_ms_default: i64 = 10_000; |
| 56 | 49 | ||
| 57 | /// The clock the daemon's bounded deadlines measure against: a calendar | 50 | /// The clock the daemon's bounded deadlines measure against: a calendar |
| @@ -61,12 +54,9 @@ pub fn monoMs() i64 { | |||
| 61 | return @as(i64, t.sec) * 1000 + @divFloor(t.nsec, 1_000_000); | 54 | return @as(i64, t.sec) * 1000 + @divFloor(t.nsec, 1_000_000); |
| 62 | } | 55 | } |
| 63 | 56 | ||
| 64 | /// Observer frames one pump may answer. A single 64 KB read can hold | 57 | /// Observer frames one pump may answer. A 64 KB read can hold thousands of |
| 65 | /// thousands of five-byte `sessions_req`s, and each is answered with up to | 58 | /// five-byte `sessions_req`s, so without a cap a batching peer decides how |
| 66 | /// `sessions_text_len` bytes from the daemon's ONE loop — so without a cap | 59 | /// long every session on the box goes dark. The rest drain next pump. |
| 67 | /// a batching peer decides how long every session on the box goes dark. | ||
| 68 | /// The rest wait in the buffer and are drained by the next pump, which | ||
| 69 | /// does not wait for the peer to write again. | ||
| 70 | pub const max_observer_frames_per_pump: usize = 16; | 60 | pub const max_observer_frames_per_pump: usize = 16; |
| 71 | 61 | ||
| 72 | /// What one observer may hold part-received. Sized to the traffic this | 62 | /// What one observer may hold part-received. Sized to the traffic this |
| @@ -75,17 +65,10 @@ pub const max_observer_frames_per_pump: usize = 16; | |||
| 75 | /// `max_payload`, the 16 MB paste ceiling of the CLIENT path. | 65 | /// `max_payload`, the 16 MB paste ceiling of the CLIENT path. |
| 76 | pub const observer_inbound_max: usize = 8 * 1024; | 66 | pub const observer_inbound_max: usize = 8 * 1024; |
| 77 | 67 | ||
| 78 | /// Consecutive drain wakeups that may retire nothing before drainPending | 68 | /// Consecutive drain wakeups that may retire nothing before `drainPending` |
| 79 | /// concludes the peer is stuck. | 69 | /// concludes the peer is stuck. One proves nothing — a coalesced or |
| 80 | /// | 70 | /// duplicate ack, or a bare flow-control update, wakes the loop without |
| 81 | /// One unproductive wakeup proves nothing, which is what the single-shot | 71 | /// retiring a byte. Bounded, so a stuck-writable socket cannot spin here. |
| 82 | /// version of this got wrong: an acknowledgement can arrive coalesced with | ||
| 83 | /// others, can re-acknowledge packets already acknowledged, or can carry | ||
| 84 | /// nothing but a flow-control update. Any of those wakes the loop without | ||
| 85 | /// retiring a byte, and treating that as "the peer is finished" abandoned | ||
| 86 | /// data the peer was still going to take. Bounded rather than removed, | ||
| 87 | /// because a socket stuck writable must not be able to spin here until the | ||
| 88 | /// deadline. | ||
| 89 | const max_drain_stalls = 64; | 72 | const max_drain_stalls = 64; |
| 90 | 73 | ||
| 91 | /// The bound is on a RUN of unproductive wakeups, not their total: a slow peer | 74 | /// The bound is on a RUN of unproductive wakeups, not their total: a slow peer |
| @@ -168,14 +151,9 @@ pub fn installSignalHandlers() void { | |||
| 168 | proxy.ignoreSigpipe(); | 151 | proxy.ignoreSigpipe(); |
| 169 | } | 152 | } |
| 170 | 153 | ||
| 171 | /// Where one client's bytes go, and where they come from. | 154 | /// Where one client's bytes go, and where they come from. A QUIC client |
| 172 | /// | 155 | /// shares ONE UDP socket with every other peer and is named by connection |
| 173 | /// It used to be a bare fd, which was the assumption the whole daemon was | 156 | /// ID, so "the client's fd" is not something that can be polled or sent to. |
| 174 | /// built on: `clients[i].fd` went straight into the poll array, and | ||
| 175 | /// send/close reached it directly. A QUIC client shares ONE UDP socket with | ||
| 176 | /// every other peer and is identified by connection ID, so "the client's | ||
| 177 | /// fd" stops being something that can be polled or sent to. Breaking that | ||
| 178 | /// assumption is the entire point of this union. | ||
| 179 | const Sink = union(enum) { | 157 | const Sink = union(enum) { |
| 180 | socket: std.posix.fd_t, | 158 | socket: std.posix.fd_t, |
| 181 | /// A QUIC peer: the listener that owns the shared UDP socket, plus the | 159 | /// A QUIC peer: the listener that owns the shared UDP socket, plus the |
| @@ -233,11 +211,9 @@ const ClientSlot = struct { | |||
| 233 | /// socket client reads whole frames straight off its fd and never | 211 | /// socket client reads whole frames straight off its fd and never |
| 234 | /// buffers here; a QUIC client's stream chunks land and wait here. | 212 | /// buffers here; a QUIC client's stream chunks land and wait here. |
| 235 | inbound: std.ArrayList(u8) = .empty, | 213 | inbound: std.ArrayList(u8) = .empty, |
| 236 | /// The grid size this client last asked for *and got*: written only | 214 | /// The grid size this client last asked for *and got*: written only after |
| 237 | /// after an applySize that succeeded, so it is never some other | 215 | /// an `applySize` that succeeded, so it is never another client's size. |
| 238 | /// client's size. 0x0 until the first accepted attach/resize, which | 216 | /// 0x0 until the first accepted attach, which `claimGrid` reads as no claim. |
| 239 | /// claimGrid reads as "makes no claim". Latest-wins re-reads this on | ||
| 240 | /// every keystroke — see the `.input` arm. | ||
| 241 | cols: u16 = 0, | 217 | cols: u16 = 0, |
| 242 | rows: u16 = 0, | 218 | rows: u16 = 0, |
| 243 | /// Frames queued but not yet accepted by the kernel. Bounded by | 219 | /// Frames queued but not yet accepted by the kernel. Bounded by |
| @@ -249,16 +225,12 @@ const ClientSlot = struct { | |||
| 249 | /// be inventing a use case). | 225 | /// be inventing a use case). |
| 250 | await_state: ?AwaitState = null, | 226 | await_state: ?AwaitState = null, |
| 251 | /// Which session this client is attached to; null between a QUIC | 227 | /// Which session this client is attached to; null between a QUIC |
| 252 | /// handshake-promotion and its first attach. A session-less slot | 228 | /// handshake-promotion and its first attach. Only ever the INDEX — the |
| 253 | /// receives nothing and its session-scoped verbs go unanswered — an | 229 | /// wire name dies with the frame it rode in. |
| 254 | /// attach (each one re-resolves its name) is what fills this, and only | ||
| 255 | /// ever with the INDEX: the wire name dies with the frame it rode in. | ||
| 256 | session: ?usize = null, | 230 | session: ?usize = null, |
| 257 | /// Where this client sits in the daemon's activity order — see | 231 | /// Where this client sits in the daemon's activity order — see |
| 258 | /// Server.activity_clock and bumpActivity. Still 0 means never attached, | 232 | /// `Server.activity_clock`. Still 0 means never attached, which is also |
| 259 | /// which a QUIC slot really is between its handshake promotion and its | 233 | /// what a promoted-but-unattached QUIC slot's null `session` says. |
| 260 | /// first attach; that slot has no `session` either, so anything ranking | ||
| 261 | /// clients gets the same "not here yet" answer from both fields. | ||
| 262 | activity: u64 = 0, | 234 | activity: u64 = 0, |
| 263 | /// This client volunteered an SSH agent (`.agent_offer`). Opt-in and | 235 | /// This client volunteered an SSH agent (`.agent_offer`). Opt-in and |
| 264 | /// per-connection: a redial is a new slot and must offer again. | 236 | /// per-connection: a redial is a new slot and must offer again. |
| @@ -274,65 +246,40 @@ const AwaitState = struct { | |||
| 274 | /// milliTimestamp at acceptance; timeout measures from here. | 246 | /// milliTimestamp at acceptance; timeout measures from here. |
| 275 | started_ms: i64, | 247 | started_ms: i64, |
| 276 | /// pgid fallback edge detector: set once the fg pgid has been seen off | 248 | /// pgid fallback edge detector: set once the fg pgid has been seen off |
| 277 | /// the shell, so "back on the shell" means returned, not never-left. | 249 | /// the shell, so "back on the shell" means returned, not never-left. It |
| 278 | /// | 250 | /// cannot say WHICH command returned — that needs marks. |
| 279 | /// Note what this does NOT distinguish: an await issued while some | ||
| 280 | /// earlier command was already running answers on that command's tail, | ||
| 281 | /// not on a command the client started. That is still honest against | ||
| 282 | /// what the pgid can actually claim — "a foreground job has returned | ||
| 283 | /// since you asked" — and the null exit code keeps it from being read | ||
| 284 | /// as more. A client that needs "the command I started" needs marks. | ||
| 285 | saw_busy: bool = false, | 251 | saw_busy: bool = false, |
| 286 | }; | 252 | }; |
| 287 | 253 | ||
| 288 | // Matches `wallview.max_tiles`: the 4 was surface-bounding (decisions.md, | 254 | // Matches `wallview.max_tiles`. 32 NAMEABLE sessions to cycle through, not |
| 289 | // 2026-08-25), never a measured limit. What it buys is 32 NAMEABLE | 255 | // 32 watched at once: every tile is its own client slot, and `max_clients` |
| 290 | // sessions to cycle through, not 32 watched at once — every tile is its | 256 | // still refuses the ninth. |
| 291 | // own attach and so its own client slot, and `max_clients` still refuses | ||
| 292 | // the ninth. | ||
| 293 | pub const max_sessions = 32; | 257 | pub const max_sessions = 32; |
| 294 | 258 | ||
| 295 | /// The smallest grid a session may exist at. Two owners used to decide | 259 | /// The smallest grid a session may exist at, read by `resolveSession` and |
| 296 | /// this and disagreed: resolveSession created at anything nonzero while | 260 | /// `applySize` alike so a 1x1 attach cannot spawn a session no resize will |
| 297 | /// applySize refused anything under 2, so a 1x1 attach could spawn a | 261 | /// move. It lives in protocol.zig because the client's stripe floor is the |
| 298 | /// session no resize would ever move. One constant, read by both, is what | 262 | /// same contract on the other end of the wire. |
| 299 | /// makes "a size a session can live at" a single fact rather than a | ||
| 300 | /// coincidence between two comparisons. The value itself lives in | ||
| 301 | /// protocol.zig: the client's stripe floor is the same contract on the | ||
| 302 | /// other end of the wire, and two spellings of one floor is two chances | ||
| 303 | /// for a thin stripe to freeze. | ||
| 304 | pub const min_session_cols = proto.min_session_cols; | 263 | pub const min_session_cols = proto.min_session_cols; |
| 305 | pub const min_session_rows = proto.min_session_rows; | 264 | pub const min_session_rows = proto.min_session_rows; |
| 306 | 265 | ||
| 307 | /// One shell and everything the daemon knows about it. What used to be | 266 | /// One shell and everything the daemon knows about it. The command tracker |
| 308 | /// daemon-global state, one level down — because "the daemon's session" | 267 | /// lives here and not on `Server` for the same reason the engine does: marks |
| 309 | /// stopped being a definite article when the wall wanted the same host | 268 | /// are one shell's lifecycle, and two shells would interleave into nonsense. |
| 310 | /// twice. The command tracker lives here and not on Server for the same | ||
| 311 | /// reason the engine does: marks are one shell's lifecycle, and a tracker | ||
| 312 | /// fed by two shells would interleave them into nonsense. | ||
| 313 | pub const Session = struct { | 269 | pub const Session = struct { |
| 314 | eng: *Engine, | 270 | eng: *Engine, |
| 315 | pty: Pty, | 271 | pty: Pty, |
| 316 | /// Row-level change tracking behind the delta stream. | 272 | /// Row-level change tracking behind the delta stream. |
| 317 | tracker: DeltaTracker = .{}, | 273 | tracker: DeltaTracker = .{}, |
| 318 | /// Identifies this session instance in every snapshot it sends. Seqs are | 274 | /// Identifies this session instance in every snapshot it sends. Seqs mean |
| 319 | /// only meaningful within one instance: a restarted daemon counts from | 275 | /// nothing across instances: a client quoting a dead epoch must be |
| 320 | /// zero over different content, so a reattach quoting a pre-restart | 276 | /// snapshotted, never delta-served content it has never seen. Never 0 — |
| 321 | /// have_seq must be snapshotted, not deltaed. Per-session for the same | 277 | /// that is a client saying "I hold nothing". |
| 322 | /// reason: a recreated same-name session is a new instance, so a client | ||
| 323 | /// quoting the dead one's epoch resyncs by snapshot rather than being | ||
| 324 | /// delta-served content it has never seen. Never 0 — that value is | ||
| 325 | /// reserved for a client saying "I hold nothing". | ||
| 326 | epoch: u64, | 278 | epoch: u64, |
| 327 | /// This session's agent socket, listening, or -1 and null when the | 279 | /// This session's agent socket, or -1 when the daemon has no agent |
| 328 | /// daemon has no agent directory (or could not bind under it). One | 280 | /// directory. One listener per SESSION: the socket's path IS that |
| 329 | /// listener per session rather than one per daemon: the socket's path | 281 | /// session's `SSH_AUTH_SOCK`, and an agent connection says nothing else |
| 330 | /// IS the session's `SSH_AUTH_SOCK`, so a shared one could not tell | 282 | /// about who is calling. -1 rather than optional, like every other fd here. |
| 331 | /// which session a connecting client belonged to, and there is nothing | ||
| 332 | /// else in an agent connection to say. | ||
| 333 | /// | ||
| 334 | /// -1 rather than optional so the accept loop's guard is the same | ||
| 335 | /// comparison every other fd in this file uses. | ||
| 336 | agent_listener: std.posix.fd_t = -1, | 283 | agent_listener: std.posix.fd_t = -1, |
| 337 | /// The path of that socket — the value the shell was handed. Owned; | 284 | /// The path of that socket — the value the shell was handed. Owned; |
| 338 | /// freed with the listener in both teardown paths (Session.closeAgent). | 285 | /// freed with the listener in both teardown paths (Session.closeAgent). |
| @@ -345,52 +292,30 @@ pub const Session = struct { | |||
| 345 | /// The terminal modes as last put on the wire, or null before the first | 292 | /// The terminal modes as last put on the wire, or null before the first |
| 346 | /// sample. Same discipline as mode_sent: what clients have been TOLD. | 293 | /// sample. Same discipline as mode_sent: what clients have been TOLD. |
| 347 | term_modes_sent: ?proto.TermModes = null, | 294 | term_modes_sent: ?proto.TermModes = null, |
| 348 | /// The window title as last put on the wire, or null before the first | 295 | /// The window title as last put on the wire. OWNED: the engine rewrites |
| 349 | /// one. Same "what clients have been TOLD" discipline as the two fields | 296 | /// its one title buffer in place, so a borrowed slice would compare the |
| 350 | /// above, but OWNED: the engine keeps one title buffer and rewrites it | 297 | /// new title against itself. |
| 351 | /// in place on the next OSC 0, so a borrowed slice would compare the | ||
| 352 | /// new title against itself. Freed in both teardown paths. | ||
| 353 | title_sent: ?[]const u8 = null, | 298 | title_sent: ?[]const u8 = null, |
| 354 | /// The last side-channel event of each kind that a reconnecting client | 299 | /// The last side-channel event of each kind a reconnecting client might |
| 355 | /// might still be owed, with the seq it happened at. One slot per kind | 300 | /// still be owed, with the seq it happened at. One slot per EVENT kind, |
| 356 | /// rather than a log: two copies during a gap means the last one wins, | 301 | /// not per clipboard target: two copies during a gap means the last wins, |
| 357 | /// which is what the user meant, and forty bells means one ding. | 302 | /// and forty bells means one ding. The clipboard slot holds the USER'S |
| 358 | /// | 303 | /// TEXT, so it is dropped as soon as its seq stops being servable. |
| 359 | /// "Per kind" is per EVENT kind, not per clipboard target: a later `p` | ||
| 360 | /// set replaces an earlier `c` set, because the slot holds the last | ||
| 361 | /// clipboard event with its target inside it. | ||
| 362 | /// | ||
| 363 | /// The clipboard slot holds the USER'S COPIED TEXT in daemon memory, so | ||
| 364 | /// it is dropped as soon as its seq stops being servable — at which | ||
| 365 | /// point it could never have been delivered to anyone anyway. Freed in | ||
| 366 | /// both teardown paths, like title_sent. | ||
| 367 | pending_clipboard: ?PendingEvent = null, | 304 | pending_clipboard: ?PendingEvent = null, |
| 368 | pending_bell: ?PendingEvent = null, | 305 | pending_bell: ?PendingEvent = null, |
| 369 | /// The session's command state machine (OSC 133). Seq-stamped copies of | 306 | /// The session's command state machine (OSC 133). Seq-stamped copies of |
| 370 | /// its transitions are what cmd_state/await_reply/status_reply carry. | 307 | /// its transitions are what cmd_state/await_reply/status_reply carry. |
| 371 | cmd: cmdmod.Tracker = .{}, | 308 | cmd: cmdmod.Tracker = .{}, |
| 372 | /// The last completed command, frozen as it stood the moment it | 309 | /// The last completed command, frozen as it returned, and the one owner |
| 373 | /// returned, and the one owner of the return watermark: `.seq` is | 310 | /// of the return watermark: "a return happened at or before this seq" and |
| 374 | /// tracker.seq as of that return, so "a return happened at or before | 311 | /// "here is what it was" are one fact, not two fields to keep in step. |
| 375 | /// this seq" and "here is what it was" are one fact rather than two | 312 | /// An await asks about a PAST event, and the live tracker cannot describe |
| 376 | /// fields that have to be kept in step. Null until a command returns, | 313 | /// one — a shell's `D;code` and the next prompt's `A` arrive in a single |
| 377 | /// which is what every reader spells as a watermark of 0. | 314 | /// write, so live state loses the verdict a fraction of a pump after it. |
| 378 | /// | ||
| 379 | /// It exists because an await answers a question about a PAST event, and | ||
| 380 | /// the live tracker stops being able to describe that event almost | ||
| 381 | /// immediately. A real integrated shell emits `D;code` and the next | ||
| 382 | /// prompt's `A` in a single precmd write, so both fold into the tracker | ||
| 383 | /// before any await is looked at: phase is already back to `at_prompt`, | ||
| 384 | /// and the following command's `C` then clears the exit code outright. | ||
| 385 | /// Answering from live state therefore lost the verdict a fraction of a | ||
| 386 | /// pump after earning it — fast commands rode to their timeout, slow | ||
| 387 | /// ones degraded to pgid and threw the exit code away. | ||
| 388 | last_return: ?proto.CmdState = null, | 315 | last_return: ?proto.CmdState = null, |
| 389 | /// milliTimestamp of the last byte the pty produced; the settle floor. | 316 | /// `milliTimestamp` of the last byte the pty produced; the settle floor. |
| 390 | /// 0 means the session has never said anything, which no amount of | 317 | /// 0 is a session that has never spoken, which no elapsed silence should |
| 391 | /// elapsed silence should be read as a command having finished. | 318 | /// be read as a finished command. Per-session: this shell's silence. |
| 392 | /// Per-session because the floor measures THIS shell's silence: another | ||
| 393 | /// session's output must not reset it. | ||
| 394 | last_pty_ms: i64 = 0, | 319 | last_pty_ms: i64 = 0, |
| 395 | /// The `monoMs` at which an accepted `end_req` stops being polite. Null | 320 | /// The `monoMs` at which an accepted `end_req` stops being polite. Null |
| 396 | /// except between that accept and the SIGKILL `reap` sends once it passes. | 321 | /// except between that accept and the SIGKILL `reap` sends once it passes. |
| @@ -484,13 +409,9 @@ pub const Server = struct { | |||
| 484 | alloc: std.mem.Allocator, | 409 | alloc: std.mem.Allocator, |
| 485 | sessions: SessionTable = .{}, | 410 | sessions: SessionTable = .{}, |
| 486 | /// The one plan every session's shell is spawned from — this, not | 411 | /// The one plan every session's shell is spawned from — this, not |
| 487 | /// opts.shell, is what future sessions spawn with. Computed once in | 412 | /// `opts.shell`. Computed once and shared: `shellint.install` mints a |
| 488 | /// init and shared deliberately: every session gets identical | 413 | /// fresh directory per call, so a plan per session would orphan all but |
| 489 | /// integration and one shim directory serves them all. | 414 | /// the last from teardown. Every slice points into `shellint_arena`. |
| 490 | /// (shellint.install mints a fresh exclusively-created directory per | ||
| 491 | /// call, so deriving a plan per session would litter one directory per | ||
| 492 | /// shell and orphan all but the last from teardown.) Every slice points | ||
| 493 | /// into shellint_arena. | ||
| 494 | spawn_plan: SpawnPlan, | 415 | spawn_plan: SpawnPlan, |
| 495 | // Spawn inputs retained for the manifest: SpawnPlan is the computed | 416 | // Spawn inputs retained for the manifest: SpawnPlan is the computed |
| 496 | // result (argv/env after injection), not the inputs that produced it. | 417 | // result (argv/env after injection), not the inputs that produced it. |
| @@ -515,15 +436,10 @@ pub const Server = struct { | |||
| 515 | /// gives up on it. Overridden small in tests; 8 MiB is far more than a | 436 | /// gives up on it. Overridden small in tests; 8 MiB is far more than a |
| 516 | /// live session ever queues, so tripping it means the peer is gone. | 437 | /// live session ever queues, so tripping it means the peer is gone. |
| 517 | pending_cap: usize = 8 * 1024 * 1024, | 438 | pending_cap: usize = 8 * 1024 * 1024, |
| 518 | /// Connections that haven't attached (mux d dump, or a client waiting | 439 | /// Connections that have not attached (`mux d dump`, or a client waiting |
| 519 | /// to attach). May send debug_dump; attach promotes into a client slot. | 440 | /// to attach); an attach promotes one into a client slot. An fd and a |
| 520 | /// | 441 | /// buffer, never a Sink: QUIC serves clients, and an observer is a local |
| 521 | /// An fd and a buffer, never a Sink: QUIC serves clients, not | 442 | /// one-shot tool that reads one answer over the unix socket and exits. |
| 522 | /// observers. An observer is a local one-shot tool (`mux d dump`, | ||
| 523 | /// `mux d stats`) that connects over the unix socket, reads one answer | ||
| 524 | /// and exits — there is no remote story for it, so giving it a Sink | ||
| 525 | /// would be generality with no second case. If that ever changes, | ||
| 526 | /// this is the comment that was wrong. | ||
| 527 | observers: [max_observers]?Observer = @splat(null), | 443 | observers: [max_observers]?Observer = @splat(null), |
| 528 | observer_idle_ms: i64 = observer_idle_ms_default, | 444 | observer_idle_ms: i64 = observer_idle_ms_default, |
| 529 | /// `.none` is a first-class answer, not a failure: QUIC is opt-in per | 445 | /// `.none` is a first-class answer, not a failure: QUIC is opt-in per |
| @@ -547,11 +463,9 @@ pub const Server = struct { | |||
| 547 | /// for why it is handed a `*Server` rather than holding one. | 463 | /// for why it is handed a `*Server` rather than holding one. |
| 548 | agents: AgentRelay = .{}, | 464 | agents: AgentRelay = .{}, |
| 549 | stats: upgrade.Counters = .{}, | 465 | stats: upgrade.Counters = .{}, |
| 550 | // Set by the upgrade_req handler, checked by the run loop after each | 466 | // Set by the `upgrade_req` handler, checked by the run loop: the reply |
| 551 | // pump: the reply must drain before the exec, and the exec must happen | 467 | // must drain before the exec, and a mid-handler exec would strand the |
| 552 | // outside the frame handler (mid-handler exec would strand the observer | 468 | // observer fd and skip the close-all. |
| 553 | // fd and skip the close-all). Shaped like stop_req's shutdown_flag but | ||
| 554 | // per-instance: the upgrade path carries the candidate's path and memfd. | ||
| 555 | pending_upgrade: ?PendingUpgrade = null, | 469 | pending_upgrade: ?PendingUpgrade = null, |
| 556 | // The daemon's own version string, set at init from build_options. | 470 | // The daemon's own version string, set at init from build_options. |
| 557 | // Stored on the struct so the upgrade handler can reach it without | 471 | // Stored on the struct so the upgrade handler can reach it without |
| @@ -571,19 +485,12 @@ pub const Server = struct { | |||
| 571 | cols: u16 = 80, | 485 | cols: u16 = 80, |
| 572 | rows: u16 = 24, | 486 | rows: u16 = 24, |
| 573 | /// Inject the OSC 133 mark scripts into the session shell. OFF by | 487 | /// Inject the OSC 133 mark scripts into the session shell. OFF by |
| 574 | /// default: the shim is not free — under zsh it costs the user their | 488 | /// default: the shim costs the user their `~/.zshenv` under zsh and |
| 575 | /// `~/.zshenv`, under bash it displaces their DEBUG trap — and what | 489 | /// their DEBUG trap under bash, and only `mux a` reads what it buys. |
| 576 | /// it buys, a knowable exit code, is read only by `mux a`. A caller | ||
| 577 | /// that wants marks says so; `mux d start` says so for | ||
| 578 | /// `MUX_SHELL_INTEGRATION=1`. A shell shellint has no scripts for is | ||
| 579 | /// unaffected either way. | ||
| 580 | shell_integration: bool = false, | 490 | shell_integration: bool = false, |
| 581 | /// Extra variables for the session shell, set after the injection's | 491 | /// Extra variables for the session shell, set after the injection's |
| 582 | /// own so a caller can override one. The shell-integration tests | 492 | /// own so a caller can override one. The shell-integration tests point |
| 583 | /// point HOME at a temp directory with it: a shim that sources the | 493 | /// HOME at a temp dir with it, so no verdict depends on whose box ran. |
| 584 | /// box's real rc files is a test whose verdict depends on whose box | ||
| 585 | /// it ran on, and this feature's whole job is to be right about | ||
| 586 | /// shells it did not configure. | ||
| 587 | extra_env: []const Pty.EnvPair = &.{}, | 494 | extra_env: []const Pty.EnvPair = &.{}, |
| 588 | // The daemon's own version, for the upgrade skew check. | 495 | // The daemon's own version, for the upgrade skew check. |
| 589 | version: []const u8 = "", | 496 | version: []const u8 = "", |
| @@ -651,14 +558,10 @@ pub const Server = struct { | |||
| 651 | parsed: *const upgrade.Parsed, | 558 | parsed: *const upgrade.Parsed, |
| 652 | version: []const u8, | 559 | version: []const u8, |
| 653 | ) !Server { | 560 | ) !Server { |
| 654 | // Same pid, same children, same descriptors. No `sockpath.claim` | 561 | // Same pid, same children, same descriptors. No `sockpath.claim`: the |
| 655 | // here, deliberately: the inherited listener fd | 562 | // inherited listener fd IS the claim, and claim's probe would find our |
| 656 | // IS the claim, and claim's probe would find our own socket | 563 | // own socket answering. `version` is THIS binary's, never the |
| 657 | // answering and refuse the daemon that is already serving it. | 564 | // manifest's — the skew check measures against what is running now. |
| 658 | // | ||
| 659 | // `version` is THIS binary's, never the manifest's — the writer's | ||
| 660 | // version is the rollback story, and the next upgrade_req's skew | ||
| 661 | // check has to measure against what is running now. | ||
| 662 | const d = parsed.daemon; | 565 | const d = parsed.daemon; |
| 663 | 566 | ||
| 664 | // Everything the Server keeps a slice of is copied out of the | 567 | // Everything the Server keeps a slice of is copied out of the |
| @@ -675,13 +578,10 @@ pub const Server = struct { | |||
| 675 | .value = if (src.value) |v| try a.dupeZ(u8, v) else null, | 578 | .value = if (src.value) |v| try a.dupeZ(u8, v) else null, |
| 676 | }; | 579 | }; |
| 677 | 580 | ||
| 678 | // The shim directory crossed as a path because live shells hold it | 581 | // The shim directory crossed as a path: live shells hold it in |
| 679 | // in ZDOTDIR (or --init-file) and it is pid-named: this process | 582 | // ZDOTDIR and it is pid-named, so a fresh one would orphan the old. |
| 680 | // could never mint that name again, and a fresh one would leave the | 583 | // Rewritten with THIS binary's scripts under the name shells hold — |
| 681 | // old directory with no owner to delete it. `shellint.prepare` | 584 | // `shellint.prepare` creates exclusively, since adopting is an attack. |
| 682 | // creates its directory exclusively — adopting one is a symlink | ||
| 683 | // attack — so the path is taken back out and rewritten with THIS | ||
| 684 | // binary's scripts under the name the shells already name. | ||
| 685 | const injection: shellint.Injection = if (d.shellint_dir) |src| blk: { | 585 | const injection: shellint.Injection = if (d.shellint_dir) |src| blk: { |
| 686 | const dir = try a.dupe(u8, src); | 586 | const dir = try a.dupe(u8, src); |
| 687 | std.fs.cwd().deleteTree(dir) catch {}; | 587 | std.fs.cwd().deleteTree(dir) catch {}; |
| @@ -784,12 +684,9 @@ pub const Server = struct { | |||
| 784 | .end_row = rec.cmd.end_row, | 684 | .end_row = rec.cmd.end_row, |
| 785 | .exit_code = rec.cmd.exit_code, | 685 | .exit_code = rec.cmd.exit_code, |
| 786 | }; | 686 | }; |
| 787 | // The verdict crosses; its watermark cannot. `CmdState.seq` in | 687 | // The verdict crosses; its watermark cannot. The delta tracker is |
| 788 | // a status reply is the RETURN watermark an await compares | 688 | // rebuilt from zero here, so an old-space seq is a watermark from |
| 789 | // `since_seq` against, and the delta tracker is rebuilt from | 689 | // the future that no later return can exceed. |
| 790 | // zero here — a seq from the old space is a watermark from the | ||
| 791 | // future that no later return can exceed, and it swallowed the | ||
| 792 | // first await after every upgrade. | ||
| 793 | s.last_return = if (rec.last_return) |lr| blk: { | 690 | s.last_return = if (rec.last_return) |lr| blk: { |
| 794 | var restamped = lr; | 691 | var restamped = lr; |
| 795 | restamped.seq = s.tracker.seq; | 692 | restamped.seq = s.tracker.seq; |
| @@ -804,12 +701,9 @@ pub const Server = struct { | |||
| 804 | s.name_len = @intCast(n); | 701 | s.name_len = @intCast(n); |
| 805 | } | 702 | } |
| 806 | 703 | ||
| 807 | // The UDP socket crossed bound and the key crossed as bytes, so TLS | 704 | // The UDP socket crossed bound and the key as bytes, so TLS stands |
| 808 | // stands back up on the same port with the same PSK. Both arms | 705 | // back up on the same port with the same PSK. Both arms become |
| 809 | // become `.owned`: whatever held the reference in the previous image | 706 | // `.owned`: whatever held the previous reference went with the image. |
| 810 | // went with it, and deinit is the only thing left that could free | ||
| 811 | // this one. The handler's ctx is bound in `run`, which is the first | ||
| 812 | // place the Server has its final address. | ||
| 813 | switch (d.quic.arm) { | 707 | switch (d.quic.arm) { |
| 814 | .none => {}, | 708 | .none => {}, |
| 815 | .borrowed, .owned => { | 709 | .borrowed, .owned => { |
| @@ -843,13 +737,9 @@ pub const Server = struct { | |||
| 843 | /// it is the one part of starting a daemon that is neither the engine, | 737 | /// it is the one part of starting a daemon that is neither the engine, |
| 844 | /// the pty nor the listener, and inlining it buried those three. | 738 | /// the pty nor the listener, and inlining it buried those three. |
| 845 | fn prepareSpawn(a: std.mem.Allocator, opts: Options) !SpawnPlan { | 739 | fn prepareSpawn(a: std.mem.Allocator, opts: Options) !SpawnPlan { |
| 846 | // Beside the socket: that directory is already private, already | 740 | // Beside the socket: that directory is already private, runtime- |
| 847 | // runtime-appropriate and already per-user, which is three | 741 | // appropriate and per-user, which is what the shims need. What it is |
| 848 | // properties the shims need and none of them are ours to re-derive. | 742 | // called and what to say on failure are `shellint.install`'s to report. |
| 849 | // What the directory under it is CALLED, whether one was created, | ||
| 850 | // and what to say when the attempt fails are all shellint's — | ||
| 851 | // `install` reports the directory it made, so nothing here has to | ||
| 852 | // re-derive from the shell what that call already knew. | ||
| 853 | const injection: shellint.Injection = if (opts.shell_integration) | 743 | const injection: shellint.Injection = if (opts.shell_integration) |
| 854 | shellint.install(a, std.fs.path.dirname(opts.sock_path) orelse ".", opts.shell) | 744 | shellint.install(a, std.fs.path.dirname(opts.sock_path) orelse ".", opts.shell) |
| 855 | else | 745 | else |
| @@ -863,13 +753,9 @@ pub const Server = struct { | |||
| 863 | opts: Options, | 753 | opts: Options, |
| 864 | injection: shellint.Injection, | 754 | injection: shellint.Injection, |
| 865 | ) !SpawnPlan { | 755 | ) !SpawnPlan { |
| 866 | // Split from `prepareSpawn` for adoption: a resumed daemon must NOT | 756 | // Split from `prepareSpawn` for adoption: a resumed daemon must not |
| 867 | // mint a second shim directory — `initFromManifest` re-prepares the | 757 | // mint a second shim directory. shellint and pty each speak their own |
| 868 | // one the live shells already name — and everything below this line | 758 | // `EnvPair` to stay leaves, so the mapping lives here. |
| 869 | // is identical either way. | ||
| 870 | // shellint speaks its own EnvPair so it can stay a leaf, and so can | ||
| 871 | // pty; the daemon is the one place that knows about both, so the | ||
| 872 | // mapping lives here. | ||
| 873 | const env = try a.alloc(Pty.EnvPair, injection.env.len + opts.extra_env.len + 1); | 759 | const env = try a.alloc(Pty.EnvPair, injection.env.len + opts.extra_env.len + 1); |
| 874 | for (injection.env, env[0..injection.env.len]) |src, *dst| { | 760 | for (injection.env, env[0..injection.env.len]) |src, *dst| { |
| 875 | dst.* = .{ .key = src.key, .value = src.value }; | 761 | dst.* = .{ .key = src.key, .value = src.value }; |
| @@ -878,15 +764,10 @@ pub const Server = struct { | |||
| 878 | // the child sees (see the loop in Pty.spawnArgv). | 764 | // the child sees (see the loop in Pty.spawnArgv). |
| 879 | @memcpy(env[injection.env.len..][0..opts.extra_env.len], opts.extra_env); | 765 | @memcpy(env[injection.env.len..][0..opts.extra_env.len], opts.extra_env); |
| 880 | 766 | ||
| 881 | // After extra_env, not before: this is the daemon's identity, not an | 767 | // After `extra_env`, not before: this is the daemon's identity, and a |
| 882 | // option, and a client reads it back to refuse attaching to the very | 768 | // caller that could overwrite it would hand a shell a lie about where |
| 883 | // session it is running inside (`wallview.showsSelf`). A caller | 769 | // it is (`wallview.showsSelf` reads it back). One plan serves every |
| 884 | // that could overwrite it could hand a shell a lie about where it is. | 770 | // session, so only the socket belongs here; `createSession` adds the name. |
| 885 | // One plan serves every session, so only the socket belongs here — | ||
| 886 | // the session name differs per session and createSession appends it. | ||
| 887 | // Both names come from `protocol`, which is where the readers get | ||
| 888 | // them; a shell only ever sees what the planter wrote, so a rename | ||
| 889 | // on one side alone would fail silently. | ||
| 890 | env[env.len - 1] = .{ .key = proto.sock_env, .value = try a.dupeZ(u8, opts.sock_path) }; | 771 | env[env.len - 1] = .{ .key = proto.sock_env, .value = try a.dupeZ(u8, opts.sock_path) }; |
| 891 | 772 | ||
| 892 | // argv is the shell plus whatever the injection adds, null-terminated | 773 | // argv is the shell plus whatever the injection adds, null-terminated |
| @@ -923,32 +804,18 @@ pub const Server = struct { | |||
| 923 | .borrowed => {}, | 804 | .borrowed => {}, |
| 924 | .none => {}, | 805 | .none => {}, |
| 925 | } | 806 | } |
| 926 | // Only unlink the socket path if it still refers to *our* socket — a | 807 | // Unlink only if the path still names *our* socket: a newer daemon |
| 927 | // newer daemon may have replaced the file since we bound it, and | 808 | // may have replaced the file, and deleting that one would steal its |
| 928 | // deleting that one would hand its clients the same field incident | 809 | // clients. The stat comes AFTER the close, because a successor only |
| 929 | // the socket-steal fix exists to prevent. | 810 | // claims once nothing is listening — that narrows the race to the |
| 930 | // The stat comes AFTER the close. Statting first was vestigial: | 811 | // stat→unlink gap, which is the floor Linux gives for deleting by name. |
| 931 | // 8a82225 fstat'd the live descriptor and so needed the fd open, | ||
| 932 | // 6090604 switched to statting the path and killed that constraint. | ||
| 933 | // A successor only claims the path after concluding nothing is | ||
| 934 | // listening here — claim()'s probe cannot refuse while our fd is | ||
| 935 | // open — so asking on this side of our close is the correct side of | ||
| 936 | // that event, and it shrinks the window in which one can claim the | ||
| 937 | // path between our answer and our unlink down to the stat→unlink | ||
| 938 | // gap. That gap is the floor for deleting by name: Linux has no | ||
| 939 | // inode-predicated unlink. The close itself stays unconditional: | ||
| 940 | // the fd is ours whoever owns the path, and only the unlink touches | ||
| 941 | // the shared namespace. | ||
| 942 | self.listener.deinit(); | 812 | self.listener.deinit(); |
| 943 | if (self.path_id.stillAt(self.sock_path)) { | 813 | if (self.path_id.stillAt(self.sock_path)) { |
| 944 | std.fs.cwd().deleteFile(self.sock_path) catch {}; | 814 | std.fs.cwd().deleteFile(self.sock_path) catch {}; |
| 945 | } | 815 | } |
| 946 | // Two passes over one deadline. Every child is asked to go before | 816 | // Two passes over one deadline: every child is asked to go before any |
| 947 | // any is waited on, so a table of shells that ignore TERM costs one | 817 | // is waited on, so a table of shells that ignore TERM costs one |
| 948 | // `term_grace_ms`, not one each — a supervisor's stop timeout does | 818 | // `term_grace_ms` and not one each. |
| 949 | // not grow with `max_sessions`, and a daemon SIGKILLed halfway | ||
| 950 | // through leaves its socket and every per-session agent socket for | ||
| 951 | // the next `mux d start` to reason about. | ||
| 952 | for (&self.sessions.table) |*slot| { | 819 | for (&self.sessions.table) |*slot| { |
| 953 | if (slot.* != null) slot.*.?.pty.requestExit(); | 820 | if (slot.* != null) slot.*.?.pty.requestExit(); |
| 954 | } | 821 | } |
| @@ -983,11 +850,9 @@ pub const Server = struct { | |||
| 983 | return &self.sessions.table[si].?; | 850 | return &self.sessions.table[si].?; |
| 984 | } | 851 | } |
| 985 | 852 | ||
| 986 | /// One poll iteration. A session whose shell has exited is torn down | 853 | /// One poll iteration. A session whose shell exited is torn down first, |
| 987 | /// first — its clients told and dropped — and the pump carries on. It | 854 | /// its clients told and dropped, and the pump carries on: no session's |
| 988 | /// answers nothing: no session's death ends the daemon, not even the | 855 | /// death ends the daemon, so an emptied one keeps serving its socket. |
| 989 | /// last one's, so an emptied daemon keeps serving its socket and a | ||
| 990 | /// fresh attach is born into it. | ||
| 991 | pub fn pumpOnce(self: *Server, timeout_ms: i32) !void { | 856 | pub fn pumpOnce(self: *Server, timeout_ms: i32) !void { |
| 992 | self.sessions.reap(self); | 857 | self.sessions.reap(self); |
| 993 | 858 | ||
| @@ -1048,11 +913,9 @@ pub const Server = struct { | |||
| 1048 | // Unconditional: expiry work is due whether or not a packet | 913 | // Unconditional: expiry work is due whether or not a packet |
| 1049 | // arrived, and this is the only place it can happen. | 914 | // arrived, and this is the only place it can happen. |
| 1050 | q.tick(); | 915 | q.tick(); |
| 1051 | // Acks that just arrived are what frees room in a connection's | 916 | // Acks are what free room in a connection's egress ring, and a |
| 1052 | // egress ring, and a QUIC client has no descriptor of its own to | 917 | // QUIC client has no descriptor of its own to go writable: without |
| 1053 | // go writable. Without this, a client whose ring filled would | 918 | // this a filled ring waits for the next frame by coincidence. |
| 1054 | // sit on its backlog until the next frame happened to be queued | ||
| 1055 | // for it — progress by coincidence rather than by design. | ||
| 1056 | self.flushQuicClients(); | 919 | self.flushQuicClients(); |
| 1057 | } | 920 | } |
| 1058 | 921 | ||
| @@ -1077,26 +940,19 @@ pub const Server = struct { | |||
| 1077 | } | 940 | } |
| 1078 | } | 941 | } |
| 1079 | 942 | ||
| 1080 | // Deliberately outside the arm above rather than folded into it: a | 943 | // Outside the arm above: a program can call tcsetattr and print |
| 1081 | // program can call tcsetattr and print nothing whatsoever, which is | 944 | // nothing, which is what every password prompt looks like from here. |
| 1082 | // exactly what `stty -echo` and every password prompt look like from | 945 | // Checking only when the pty spoke would miss the quietest changes. |
| 1083 | // here. Checking only when the pty spoke would miss the quietest | ||
| 1084 | // mode changes — the ones a predicting client most needs to hear | ||
| 1085 | // about — until the next unrelated byte of output happened along. | ||
| 1086 | for (&self.sessions.table, 0..) |*slot, si| { | 946 | for (&self.sessions.table, 0..) |*slot, si| { |
| 1087 | if (slot.* != null) self.pollPtyMode(si); | 947 | if (slot.* != null) self.pollPtyMode(si); |
| 1088 | } | 948 | } |
| 1089 | 949 | ||
| 1090 | if (fds[listener_idx].revents & std.posix.POLL.IN != 0) self.acceptConn(); | 950 | if (fds[listener_idx].revents & std.posix.POLL.IN != 0) self.acceptConn(); |
| 1091 | 951 | ||
| 1092 | // Re-check each slot: an earlier arm (or a failed broadcast) may | 952 | // Re-check each slot: an earlier arm may have dropped a client whose |
| 1093 | // have dropped a client whose fd is still sitting in `fds`. | 953 | // fd is still in `fds`. The null check suffices only because no slot |
| 1094 | // The null check alone is enough only because no slot can be | 954 | // can be REFILLED between poll and here — move the observer loop above |
| 1095 | // *refilled* between poll and here: acceptConn only fills observer | 955 | // this one and it stops being true. |
| 1096 | // slots, and promotion into a client slot happens in | ||
| 1097 | // serviceObserver, which runs after this loop. So a slot that is | ||
| 1098 | // still non-null still holds the very fd whose revents we polled. | ||
| 1099 | // Move the observer loop above this one and that stops being true. | ||
| 1100 | for (0..max_clients) |i| { | 956 | for (0..max_clients) |i| { |
| 1101 | if (self.clients[i] == null) continue; | 957 | if (self.clients[i] == null) continue; |
| 1102 | const revents = fds[client_base + i].revents; | 958 | const revents = fds[client_base + i].revents; |
| @@ -1122,17 +978,10 @@ pub const Server = struct { | |||
| 1122 | self.reapIdleObservers(monoMs()); | 978 | self.reapIdleObservers(monoMs()); |
| 1123 | 979 | ||
| 1124 | // After the client arms, so a channel opened this pump is routed by | 980 | // After the client arms, so a channel opened this pump is routed by |
| 1125 | // the activity order as the frames just handled left it, and so a | 981 | // the activity order those frames left. Servicing BEFORE accepting |
| 1126 | // client dropped up there is not handed a channel on its way out. | 982 | // keeps `revents` and the table in step: accepting first could free |
| 1127 | // Each loop re-tests its slot, because the arms above drop clients | 983 | // and refill a slot in one pass, and the loop below would then read a |
| 1128 | // and dropClient closes that client's channels. | 984 | // descriptor on the strength of the previous occupant's poll. |
| 1129 | // | ||
| 1130 | // Servicing BEFORE accepting is what keeps `revents` and the table | ||
| 1131 | // in step. Accepting first could free a slot (a client's agent_close | ||
| 1132 | // this pump) and refill it in the same pass, and the loop below | ||
| 1133 | // would then read a descriptor on the strength of the previous | ||
| 1134 | // occupant's poll — a blocking read on a socket nobody has written | ||
| 1135 | // to yet. | ||
| 1136 | for (0..max_agent_chans) |s| { | 985 | for (0..max_agent_chans) |s| { |
| 1137 | if (self.agents.chans[s] == null) continue; | 986 | if (self.agents.chans[s] == null) continue; |
| 1138 | if (fds[agent_chan_base + s].revents != 0) self.agents.service(self, s); | 987 | if (fds[agent_chan_base + s].revents != 0) self.agents.service(self, s); |
| @@ -1145,28 +994,20 @@ pub const Server = struct { | |||
| 1145 | } | 994 | } |
| 1146 | } | 995 | } |
| 1147 | 996 | ||
| 1148 | // After every arm that can move the session on, so an await sees the | 997 | // After every arm that can move the session on, so an await sees THIS |
| 1149 | // marks, the pgid and the output silence that this pump produced | 998 | // pump's marks and silence — and before the QUIC drain, which is what |
| 1150 | // rather than last pump's — and before the QUIC drain below, because | 999 | // puts the frame it queues on the wire. |
| 1151 | // an await resolving here queues a frame and drainAll is what puts a | ||
| 1152 | // QUIC client's queued bytes on the wire. The other order would cost | ||
| 1153 | // every remote await a whole extra poll cycle. | ||
| 1154 | self.checkAwaits(); | 1000 | self.checkAwaits(); |
| 1155 | 1001 | ||
| 1156 | // Last, and deliberately at the very end of the pump: the listener's | 1002 | // Last in the pump: the listener's send only QUEUES — draining inside |
| 1157 | // send only QUEUES now (draining from inside an ngtcp2 callback is | 1003 | // an ngtcp2 callback is the defect that avoids — so this is where a |
| 1158 | // the defect that change exists to remove), so this is where a QUIC | 1004 | // QUIC client's bytes actually leave, after all the frame handling. |
| 1159 | // client's bytes actually leave. It has to run after all the frame | ||
| 1160 | // handling above, or everything queued during this pump would wait | ||
| 1161 | // for the next one and every reply would cost a poll cycle. | ||
| 1162 | if (self.quicListener()) |q| q.drainAll(); | 1005 | if (self.quicListener()) |q| q.drainAll(); |
| 1163 | } | 1006 | } |
| 1164 | 1007 | ||
| 1165 | /// Pumps until something asks the daemon to stop. The only nonzero exit | 1008 | /// Pumps until something asks the daemon to stop. Always 0: getting here |
| 1166 | /// `mux d start` has is a boot failure in main; getting here means the | 1009 | /// means the daemon served, and a supervisor reads a nonzero exit on a |
| 1167 | /// daemon served, so the answer is 0 whether the ask was `mux d stop` or | 1010 | /// clean shutdown as a crash. Only a boot failure in main exits nonzero. |
| 1168 | /// a SIGTERM from a supervisor — which reads a nonzero exit on a clean | ||
| 1169 | /// shutdown as a crash. | ||
| 1170 | pub fn run(self: *Server) !u8 { | 1011 | pub fn run(self: *Server) !u8 { |
| 1171 | // The pump cannot start on a listener whose handler points anywhere | 1012 | // The pump cannot start on a listener whose handler points anywhere |
| 1172 | // but here. An adopted listener (initFromManifest) is built before | 1013 | // but here. An adopted listener (initFromManifest) is built before |
| @@ -1190,11 +1031,9 @@ pub const Server = struct { | |||
| 1190 | 1031 | ||
| 1191 | fn acceptConn(self: *Server) void { | 1032 | fn acceptConn(self: *Server) void { |
| 1192 | const conn = self.listener.accept() catch return; | 1033 | const conn = self.listener.accept() catch return; |
| 1193 | // Nonblocking from the accept, and it stays that way through the | 1034 | // Nonblocking from the accept, and through the promotion into a |
| 1194 | // promotion into a client slot: every write on this fd is either a | 1035 | // client slot: a blocking fd lets one peer that stops reading stop the |
| 1195 | // bounded observer reply or flushClient's queue, and both are | 1036 | // daemon's only loop. Every write here tolerates a short write. |
| 1196 | // written for a short write. A blocking fd here lets one peer that | ||
| 1197 | // stops reading stop the daemon's only loop. | ||
| 1198 | setNonblocking(conn.stream.handle) catch { | 1037 | setNonblocking(conn.stream.handle) catch { |
| 1199 | conn.stream.close(); | 1038 | conn.stream.close(); |
| 1200 | return; | 1039 | return; |
| @@ -1219,11 +1058,9 @@ pub const Server = struct { | |||
| 1219 | if (self.clients[i]) |*c| c.agent_offer = false; | 1058 | if (self.clients[i]) |*c| c.agent_offer = false; |
| 1220 | } | 1059 | } |
| 1221 | 1060 | ||
| 1222 | /// Latest wins, the doctrine the grid follows: the person typing is the | 1061 | /// Latest wins, as the grid does: the person typing is the person whose |
| 1223 | /// person whose agent signs. Decided once per connection — an in-flight | 1062 | /// agent signs. Decided once per connection — swapping identities under a |
| 1224 | /// channel stays pinned to the client it opened on, because swapping | 1063 | /// mid-exchange ssh fails the signature rather than moving it. |
| 1225 | /// identities under a mid-exchange ssh fails the signature rather than | ||
| 1226 | /// moving it. | ||
| 1227 | pub fn agentAnswerer(self: *const Server, si: usize) ?usize { | 1064 | pub fn agentAnswerer(self: *const Server, si: usize) ?usize { |
| 1228 | var best: ?usize = null; | 1065 | var best: ?usize = null; |
| 1229 | for (self.clients, 0..) |c, i| { | 1066 | for (self.clients, 0..) |c, i| { |
| @@ -1251,14 +1088,10 @@ pub const Server = struct { | |||
| 1251 | self.teardownClient(i, true); | 1088 | self.teardownClient(i, true); |
| 1252 | } | 1089 | } |
| 1253 | 1090 | ||
| 1254 | /// Queue one frame for the client in slot `i` and offer it to the kernel | 1091 | /// Queue one frame for the client in slot `i` and offer it to the kernel. |
| 1255 | /// straight away. Returns false if the client is gone — either it was | 1092 | /// False if the client is gone, so callers can skip the accounting. The |
| 1256 | /// already dropped, or this call dropped it — so callers can skip the | 1093 | /// only way anything reaches a client fd, and it never blocks: the daemon |
| 1257 | /// accounting for a send that will never happen. | 1094 | /// is single-threaded and one stalled peer would freeze everyone. |
| 1258 | /// | ||
| 1259 | /// This is the only way anything reaches a client fd. Nothing here ever | ||
| 1260 | /// blocks: that is the whole point, since the daemon is single-threaded | ||
| 1261 | /// and one stalled peer would otherwise freeze the session for everyone. | ||
| 1262 | pub fn queueFrame(self: *Server, i: usize, t: proto.MsgType, payload: []const u8) bool { | 1095 | pub fn queueFrame(self: *Server, i: usize, t: proto.MsgType, payload: []const u8) bool { |
| 1263 | if (self.clients[i] == null) return false; | 1096 | if (self.clients[i] == null) return false; |
| 1264 | const slot = &self.clients[i].?; | 1097 | const slot = &self.clients[i].?; |
| @@ -1301,14 +1134,9 @@ pub const Server = struct { | |||
| 1301 | _ = self.queueFrame(i, .selection_reply, payload.items); | 1134 | _ = self.queueFrame(i, .selection_reply, payload.items); |
| 1302 | } | 1135 | } |
| 1303 | 1136 | ||
| 1304 | /// Wait, up to `budget_ms` in total, for clients to accept what they are | 1137 | /// Wait up to `budget_ms` for clients to accept what they are owed. Only |
| 1305 | /// still owed. Only the one path after which a client gets no next pump | 1138 | /// the path where a client gets no next pump uses it — a session's death — |
| 1306 | /// uses this — a session's death, for the clients about to be dropped: | 1139 | /// because there the alternative to a bounded wait is losing the frame. |
| 1307 | /// mid-session the pump's own POLLOUT handling drains queues without | ||
| 1308 | /// anyone waiting, and that is the property the rest of this change | ||
| 1309 | /// exists to protect. Here there is no next pump, so the alternative to | ||
| 1310 | /// a bounded wait is losing the frame. A peer that never reads costs | ||
| 1311 | /// the daemon the budget and no more. | ||
| 1312 | pub fn drainPending(self: *Server, budget_ms: i64) void { | 1140 | pub fn drainPending(self: *Server, budget_ms: i64) void { |
| 1313 | const deadline = std.time.milliTimestamp() + budget_ms; | 1141 | const deadline = std.time.milliTimestamp() + budget_ms; |
| 1314 | var stalls: usize = 0; | 1142 | var stalls: usize = 0; |
| @@ -1324,13 +1152,10 @@ pub const Server = struct { | |||
| 1324 | for (&self.clients, 0..) |*slot, i| { | 1152 | for (&self.clients, 0..) |*slot, i| { |
| 1325 | if (slot.* == null) continue; | 1153 | if (slot.* == null) continue; |
| 1326 | if (slot.*.?.sink == .quic) { | 1154 | if (slot.*.?.sink == .quic) { |
| 1327 | // A QUIC client has no descriptor of its own to wait on, | 1155 | // A QUIC client has no descriptor to wait on, so its |
| 1328 | // so its egress is driven directly. It owes in two | 1156 | // egress is driven directly. It owes in TWO places: bytes |
| 1329 | // places, and only counting the first is how this arm | 1157 | // queued in the slot, and bytes the ring took but the peer |
| 1330 | // used to look like it worked: bytes still queued in the | 1158 | // has not acked. A send finishes when it is acked. |
| 1331 | // slot, AND bytes the ring took but the peer has not | ||
| 1332 | // acknowledged. A send is not finished when it is | ||
| 1333 | // accepted, it is finished when it is acked. | ||
| 1334 | self.flushClient(i); | 1159 | self.flushClient(i); |
| 1335 | if (self.clients[i]) |*c| { | 1160 | if (self.clients[i]) |*c| { |
| 1336 | const still = c.pending.items.len + c.sink.inFlight(); | 1161 | const still = c.pending.items.len + c.sink.inFlight(); |
| @@ -1346,11 +1171,9 @@ pub const Server = struct { | |||
| 1346 | fds[n_sock] = .{ .fd = c.sink.pollFd(), .events = std.posix.POLL.OUT, .revents = 0 }; | 1171 | fds[n_sock] = .{ .fd = c.sink.pollFd(), .events = std.posix.POLL.OUT, .revents = 0 }; |
| 1347 | n_sock += 1; | 1172 | n_sock += 1; |
| 1348 | } | 1173 | } |
| 1349 | // Give QUIC egress a chance to leave the box before deciding | 1174 | // Give QUIC egress a chance to leave before deciding there is |
| 1350 | // there is nothing left to wait for. drainAll as well as tick: | 1175 | // nothing to wait for. `drainAll` as well as `tick`: tick only |
| 1351 | // tick only services connections whose timer is due, and the | 1176 | // services connections whose timer is due. |
| 1352 | // exit status this path exists to deliver was merely queued by | ||
| 1353 | // the flushClient above. | ||
| 1354 | if (self.quicListener()) |q| { | 1177 | if (self.quicListener()) |q| { |
| 1355 | q.tick(); | 1178 | q.tick(); |
| 1356 | q.drainAll(); | 1179 | q.drainAll(); |
| @@ -1376,11 +1199,9 @@ pub const Server = struct { | |||
| 1376 | else | 1199 | else |
| 1377 | null; | 1200 | null; |
| 1378 | const ready = std.posix.poll(fds[0..n], drainWaitMs(remaining, hint)) catch return; | 1201 | const ready = std.posix.poll(fds[0..n], drainWaitMs(remaining, hint)) catch return; |
| 1379 | // A timeout is a verdict on this slice, not on the peer. The | 1202 | // A timeout is a verdict on this slice, not on the peer: the |
| 1380 | // deadline check at the top of the loop is what ends this; | 1203 | // deadline at the top of the loop ends this. Returning here gives |
| 1381 | // returning here instead gave up the moment ngtcp2's next timer | 1204 | // up exactly when a lossy path was about to make progress. |
| 1382 | // came due, which on a lossy path is exactly when the work was | ||
| 1383 | // about to happen. | ||
| 1384 | if (ready == 0) continue; | 1205 | if (ready == 0) continue; |
| 1385 | 1206 | ||
| 1386 | if (quic_idx) |qi| { | 1207 | if (quic_idx) |qi| { |
| @@ -1427,11 +1248,9 @@ pub const Server = struct { | |||
| 1427 | off += n; | 1248 | off += n; |
| 1428 | } | 1249 | } |
| 1429 | if (off == slot.pending.items.len) { | 1250 | if (off == slot.pending.items.len) { |
| 1430 | // Retaining the allocation is the right default — steady-state | 1251 | // Retaining the allocation is the right default, but a one-off |
| 1431 | // frames reuse it — but a one-off burst (a big snapshot to a | 1252 | // burst would keep its buffer for the session's life, up to |
| 1432 | // briefly slow client) would otherwise keep its buffer attached | 1253 | // `pending_cap` x `max_clients`. Oversized buffers go back. |
| 1433 | // to the slot for the rest of the session, up to pending_cap | ||
| 1434 | // times max_clients. Hand back anything oversized once it empties. | ||
| 1435 | if (slot.pending.capacity > 64 * 1024) { | 1254 | if (slot.pending.capacity > 64 * 1024) { |
| 1436 | slot.pending.clearAndFree(self.alloc); | 1255 | slot.pending.clearAndFree(self.alloc); |
| 1437 | } else { | 1256 | } else { |
| @@ -1456,22 +1275,16 @@ pub const Server = struct { | |||
| 1456 | } | 1275 | } |
| 1457 | 1276 | ||
| 1458 | // ---- QUIC glue ------------------------------------------------------- | 1277 | // ---- QUIC glue ------------------------------------------------------- |
| 1459 | // | 1278 | // Three callbacks hold the entire difference between a QUIC client and a |
| 1460 | // Three callbacks, and between them they contain the entire difference | 1279 | // socket client. Everything past `pushInbound` has heard of neither. |
| 1461 | // between a QUIC client and a socket client. Everything past pushInbound | ||
| 1462 | // is code that has never heard of either. | ||
| 1463 | 1280 | ||
| 1464 | fn quicOnOpen(ctx: *anyopaque, id: u64) void { | 1281 | fn quicOnOpen(ctx: *anyopaque, id: u64) void { |
| 1465 | const self: *Server = @ptrCast(@alignCast(ctx)); | 1282 | const self: *Server = @ptrCast(@alignCast(ctx)); |
| 1466 | const listener = self.quicListener() orelse return; | 1283 | const listener = self.quicListener() orelse return; |
| 1467 | const slot = self.freeClientSlot() orelse { | 1284 | const slot = self.freeClientSlot() orelse { |
| 1468 | // Session full: the same answer the socket path gives, sent the | 1285 | // Session full: the same answer the socket path gives. A short |
| 1469 | // same way, then the connection goes. | 1286 | // accept would truncate the refusal into a corrupt frame — and on |
| 1470 | // A short accept would be a truncated refusal, which reads as a | 1287 | // a just-opened connection that means something is very wrong. |
| 1471 | // corrupt frame rather than a "no". The ring is empty on a | ||
| 1472 | // connection that has just opened and this frame is a handful of | ||
| 1473 | // bytes, so a partial take here means something is very wrong; | ||
| 1474 | // either way, closing is the answer. | ||
| 1475 | _ = listener.send(id, &refusalFrame()) catch {}; | 1288 | _ = listener.send(id, &refusalFrame()) catch {}; |
| 1476 | listener.closeConn(id); | 1289 | listener.closeConn(id); |
| 1477 | return; | 1290 | return; |
| @@ -1587,17 +1400,10 @@ pub const Server = struct { | |||
| 1587 | return 0; | 1400 | return 0; |
| 1588 | }, | 1401 | }, |
| 1589 | }; | 1402 | }; |
| 1590 | // The same refusals main.zig gives the --quic path, in the same | 1403 | // The same refusals main.zig gives `--quic`, in the same words: |
| 1591 | // words — literally the same, since `quic.keyRefusalBody` owns them | 1404 | // `quic.keyRefusalBody` owns them and this site owns only the prefix. |
| 1592 | // and this site owns only the prefix. An operator who has seen one | 1405 | // Every error routes there, catch-all included — nothing else in this |
| 1593 | // message should not have to learn a second phrasing for it, and | 1406 | // expression can fail, so an unclassified one is still an unread key. |
| 1594 | // this site's catch-all used to be exactly that second phrasing. | ||
| 1595 | // | ||
| 1596 | // Every error is routed, catch-all included, because every error | ||
| 1597 | // here can only have come from the load: there is nothing else in | ||
| 1598 | // this expression to have failed. So an unclassified one is still a | ||
| 1599 | // key that would not read, which is exactly what the body's fourth | ||
| 1600 | // sentence says. | ||
| 1601 | const key = quic.Key.load(key_path) catch |err| { | 1407 | const key = quic.Key.load(key_path) catch |err| { |
| 1602 | var buf: [quic.key_refusal_len]u8 = undefined; | 1408 | var buf: [quic.key_refusal_len]u8 = undefined; |
| 1603 | std.debug.print( | 1409 | std.debug.print( |
| @@ -1635,11 +1441,9 @@ pub const Server = struct { | |||
| 1635 | } | 1441 | } |
| 1636 | 1442 | ||
| 1637 | // ---- pty mode ----------------------------------------------------- | 1443 | // ---- pty mode ----------------------------------------------------- |
| 1638 | // | ||
| 1639 | // The daemon holds the only fd that knows whether a keystroke will be | 1444 | // The daemon holds the only fd that knows whether a keystroke will be |
| 1640 | // echoed, and by whom. Shipping that is what turns a client's local echo | 1445 | // echoed. Shipping that turns a client's local echo from a guess into a |
| 1641 | // from a guess into a deduction; the client is told what the terminal | 1446 | // deduction: it is told what the terminal IS and decides for itself. |
| 1642 | // IS, and decides for itself what to do about it. | ||
| 1643 | 1447 | ||
| 1644 | fn readPtyMode(self: *Server, si: usize) ?proto.PtyModeFlags { | 1448 | fn readPtyMode(self: *Server, si: usize) ?proto.PtyModeFlags { |
| 1645 | // Not merely defensive: tcgetattr on a closed master is `unreachable` | 1449 | // Not merely defensive: tcgetattr on a closed master is `unreachable` |
| @@ -1649,11 +1453,9 @@ pub const Server = struct { | |||
| 1649 | return .{ .icanon = m.icanon, .echo = m.echo }; | 1453 | return .{ .icanon = m.icanon, .echo = m.echo }; |
| 1650 | } | 1454 | } |
| 1651 | 1455 | ||
| 1652 | /// Read the pty's line discipline and, if it has moved since clients | 1456 | /// Read the pty's line discipline and, if it moved since clients were |
| 1653 | /// were last told, tell all of them. One tcgetattr per pump on an fd we | 1457 | /// last told, tell them. One tcgetattr per pump: polling is the only |
| 1654 | /// already own — no caching cleverness, because polling is the only | 1458 | /// mechanism there is, since nothing notifies us of another process. |
| 1655 | /// mechanism there is: nothing notifies a process that some other | ||
| 1656 | /// process changed the terminal. | ||
| 1657 | fn pollPtyMode(self: *Server, si: usize) void { | 1459 | fn pollPtyMode(self: *Server, si: usize) void { |
| 1658 | const flags = self.readPtyMode(si) orelse return; | 1460 | const flags = self.readPtyMode(si) orelse return; |
| 1659 | if (self.ses(si).mode_sent) |prev| { | 1461 | if (self.ses(si).mode_sent) |prev| { |
| @@ -1669,21 +1471,15 @@ pub const Server = struct { | |||
| 1669 | } | 1471 | } |
| 1670 | } | 1472 | } |
| 1671 | 1473 | ||
| 1672 | /// Tell one client what the pty is doing right now. A client that has | 1474 | /// Tell one client what the pty is doing right now. The broadcast above |
| 1673 | /// just attached has been told nothing, and the broadcast above only | 1475 | /// only fires on a CHANGE, so without this a client joining a session |
| 1674 | /// fires on a change — so without this, a client joining a session that | 1476 | /// sitting quietly at a prompt would wait forever to learn the mode. |
| 1675 | /// is sitting quietly at a prompt would wait for the mode to move before | ||
| 1676 | /// it learned anything about it, which is to say forever. | ||
| 1677 | fn sendPtyModeTo(self: *Server, si: usize, i: usize) void { | 1477 | fn sendPtyModeTo(self: *Server, si: usize, i: usize) void { |
| 1678 | const flags = self.ses(si).mode_sent orelse blk: { | 1478 | const flags = self.ses(si).mode_sent orelse blk: { |
| 1679 | // Reached whenever an attach lands before the mode has ever | 1479 | // Reached whenever an attach lands before the mode was ever |
| 1680 | // been polled — which is not only the pre-first-pump case a | 1480 | // polled: a QUIC handshake and attach completing in one pump get |
| 1681 | // socket client can hit. A QUIC client's bytes arrive from | 1481 | // here with `mode_sent` still null. Recorded as sent, so no |
| 1682 | // `q.readable()` at the top of the pump, and pollPtyMode runs | 1482 | // broadcast repeats the value to the client just handed it. |
| 1683 | // further down it, so a handshake and attach that complete in | ||
| 1684 | // the same pump reach here with mode_sent still null. Recorded | ||
| 1685 | // as sent, so this cannot be followed by a broadcast of the very | ||
| 1686 | // same value to the client that was just handed it. | ||
| 1687 | const f = self.readPtyMode(si) orelse return; | 1483 | const f = self.readPtyMode(si) orelse return; |
| 1688 | self.ses(si).mode_sent = f; | 1484 | self.ses(si).mode_sent = f; |
| 1689 | break :blk f; | 1485 | break :blk f; |
| @@ -1752,11 +1548,9 @@ pub const Server = struct { | |||
| 1752 | // deadline it was ever given. | 1548 | // deadline it was ever given. |
| 1753 | if (self.ses(si).end_by_ms == null) | 1549 | if (self.ses(si).end_by_ms == null) |
| 1754 | self.ses(si).end_by_ms = monoMs() + Pty.term_grace_ms; | 1550 | self.ses(si).end_by_ms = monoMs() + Pty.term_grace_ms; |
| 1755 | // An accepted end cancels a pending upgrade. `validateUpgrade` | 1551 | // An accepted end cancels a pending upgrade: `validateUpgrade` |
| 1756 | // refuses while any master is -1, but it decides a pump before | 1552 | // decides a pump before `run` execs, and an end accepted in between |
| 1757 | // `run` execs, and an end accepted in between would leave the exec | 1553 | // leaves the exec calling `clearCloexec(-1)` — a panic, not a refusal. |
| 1758 | // calling clearCloexec(-1) — `unreachable` in the pinned std, so a | ||
| 1759 | // daemon panic taking every session with it, not a refusal. | ||
| 1760 | self.cancelUpgrade(); | 1554 | self.cancelUpgrade(); |
| 1761 | return .{ .accepted = true, .others = others, .reason = proto.end_reason.accepted }; | 1555 | return .{ .accepted = true, .others = others, .reason = proto.end_reason.accepted }; |
| 1762 | } | 1556 | } |
| @@ -1783,11 +1577,9 @@ pub const Server = struct { | |||
| 1783 | self.observers[i] = null; | 1577 | self.observers[i] = null; |
| 1784 | } | 1578 | } |
| 1785 | 1579 | ||
| 1786 | /// One read per readiness, for both connection tables: `.bytes` is what | 1580 | /// One read per readiness, for both connection tables: `.bytes` arrived, |
| 1787 | /// arrived, `.again` a readiness with nothing behind it (the fds are | 1581 | /// `.again` is a readiness with nothing behind it, `.gone` is EOF or a |
| 1788 | /// nonblocking since acceptConn), `.gone` EOF or a dead socket. Stated | 1582 | /// dead socket. Stated once so client and observer cannot drift apart. |
| 1789 | /// once because the client and observer paths must not drift apart on | ||
| 1790 | /// the read discipline — they differ only in which table they drop from. | ||
| 1791 | const ReadOutcome = union(enum) { bytes: []u8, again, gone }; | 1583 | const ReadOutcome = union(enum) { bytes: []u8, again, gone }; |
| 1792 | 1584 | ||
| 1793 | fn readConn(fd: std.posix.fd_t, buf: []u8) ReadOutcome { | 1585 | fn readConn(fd: std.posix.fd_t, buf: []u8) ReadOutcome { |
| @@ -1819,14 +1611,9 @@ pub const Server = struct { | |||
| 1819 | } | 1611 | } |
| 1820 | 1612 | ||
| 1821 | /// Feed bytes that arrived for client `i`, extracting whole frames as | 1613 | /// Feed bytes that arrived for client `i`, extracting whole frames as |
| 1822 | /// they complete. Every transport lands here — a socket read and a QUIC | 1614 | /// they complete. Every transport lands here, so from the frame onward |
| 1823 | /// stream chunk from the shared UDP socket alike — so that from the | 1615 | /// they take byte-identical paths. Only the byte-split test below asks |
| 1824 | /// frame onward they take byte-identical paths. | 1616 | /// for the adversarial chunk boundaries a live link rarely produces. |
| 1825 | /// | ||
| 1826 | /// The byte-split test below still earns its keep with a real caller in | ||
| 1827 | /// place: the chunk boundaries a live QUIC link happens to produce are | ||
| 1828 | /// not the adversarial ones — a header split down the middle, two | ||
| 1829 | /// frames in one write — and only the test asks for those. | ||
| 1830 | pub fn pushInbound(self: *Server, i: usize, bytes: []const u8) void { | 1617 | pub fn pushInbound(self: *Server, i: usize, bytes: []const u8) void { |
| 1831 | if (self.clients[i] == null) return; | 1618 | if (self.clients[i] == null) return; |
| 1832 | self.clients[i].?.inbound.appendSlice(self.alloc, bytes) catch { | 1619 | self.clients[i].?.inbound.appendSlice(self.alloc, bytes) catch { |
| @@ -1847,15 +1634,10 @@ pub const Server = struct { | |||
| 1847 | } | 1634 | } |
| 1848 | } | 1635 | } |
| 1849 | 1636 | ||
| 1850 | /// Everything a client can ask for, once its bytes are a frame. Reached | 1637 | /// Everything a client can ask for, once its bytes are a frame. Slot `i` |
| 1851 | /// from a socket read or from an injection; deliberately knows about | 1638 | /// is live on entry — `pushInbound` re-reads it before every dispatch, |
| 1852 | /// neither. | 1639 | /// because a handler can drop the client mid-loop — so the arms below |
| 1853 | /// | 1640 | /// index with `.?` rather than each re-testing it. |
| 1854 | /// Slot `i` is live on entry — pushInbound re-reads it before every | ||
| 1855 | /// dispatch, precisely because a handler can drop the client mid-loop — | ||
| 1856 | /// so the arms below index it with `.?` rather than each re-testing it. | ||
| 1857 | /// One arm used to carry its own null check; it read like the others | ||
| 1858 | /// were missing one. | ||
| 1859 | fn handleFrame(self: *Server, i: usize, frame: proto.Frame) void { | 1641 | fn handleFrame(self: *Server, i: usize, frame: proto.Frame) void { |
| 1860 | if (self.handleDaemonVerb(.{ .client = i }, frame)) return; | 1642 | if (self.handleDaemonVerb(.{ .client = i }, frame)) return; |
| 1861 | switch (frame.type) { | 1643 | switch (frame.type) { |
| @@ -1920,19 +1702,10 @@ pub const Server = struct { | |||
| 1920 | } | 1702 | } |
| 1921 | 1703 | ||
| 1922 | /// The verbs whose answer is the DAEMON rather than the connection: | 1704 | /// The verbs whose answer is the DAEMON rather than the connection: |
| 1923 | /// identical bytes for an attached client and for a one-shot `mux d` | 1705 | /// identical bytes for an attached client and a one-shot `mux d` tool. |
| 1924 | /// tool. Returns false for a verb this owner does not have — an attach, | 1706 | /// False for a verb that does depend on who asked. The two owners differ |
| 1925 | /// an upgrade, a status_req — which really does depend on who asked. | 1707 | /// only in how an answer LEAVES, which is all `replyTo` is: a client has a |
| 1926 | /// Written twice before, and the two copies were not the same program. | 1708 | /// send queue, an observer gets a bounded write and a truncation drops it. |
| 1927 | /// | ||
| 1928 | /// The tables differ only in how an answer LEAVES, which is all | ||
| 1929 | /// `replyTo` is: a client has a send queue and never blocks, while an | ||
| 1930 | /// observer has none — `mux d dump`, `mux d stats` and `mux d endpoint` | ||
| 1931 | /// read their one answer and exit. So the observer's write is bounded: | ||
| 1932 | /// a dump can be megabytes and must tolerate a short write, and a peer | ||
| 1933 | /// that stops reading costs this reply's budget, not the daemon's loop. | ||
| 1934 | /// Past the budget the frame is truncated, which is why a failed write | ||
| 1935 | /// drops the connection rather than leave half a frame on it. | ||
| 1936 | fn handleDaemonVerb(self: *Server, p: Peer, frame: proto.Frame) bool { | 1709 | fn handleDaemonVerb(self: *Server, p: Peer, frame: proto.Frame) bool { |
| 1937 | switch (frame.type) { | 1710 | switch (frame.type) { |
| 1938 | .stats_req => { | 1711 | .stats_req => { |
| @@ -1948,11 +1721,9 @@ pub const Server = struct { | |||
| 1948 | self.replyTo(p, .stats_reply, text); | 1721 | self.replyTo(p, .stats_reply, text); |
| 1949 | }, | 1722 | }, |
| 1950 | .sessions_req => { | 1723 | .sessions_req => { |
| 1951 | // Daemon-global for the same reason .stats_req is: the answer | 1724 | // Daemon-global like `.stats_req`: the answer is the session |
| 1952 | // is the session TABLE, so which session the asker sits in | 1725 | // TABLE, so who asked cannot change it. An observation, not |
| 1953 | // (or whether it sits in one at all) cannot change it. | 1726 | // activity, so it claims no grid. |
| 1954 | // Answered on the asking connection only — it is an | ||
| 1955 | // observation, not activity, so it claims no grid. | ||
| 1956 | var buf: SessionsBuf = undefined; | 1727 | var buf: SessionsBuf = undefined; |
| 1957 | self.replyTo(p, .sessions_reply, self.sessions.text(&buf)); | 1728 | self.replyTo(p, .sessions_reply, self.sessions.text(&buf)); |
| 1958 | }, | 1729 | }, |
| @@ -1961,11 +1732,9 @@ pub const Server = struct { | |||
| 1961 | self.replyTo(p, .endpoint_reply, &payload); | 1732 | self.replyTo(p, .endpoint_reply, &payload); |
| 1962 | }, | 1733 | }, |
| 1963 | .debug_dump => { | 1734 | .debug_dump => { |
| 1964 | // Answers whatever the payload's tail names — it is a read | 1735 | // A read against a NAME, not a question about this |
| 1965 | // against a name, not a question about this connection's own | 1736 | // connection's session: an attached client may peek at any |
| 1966 | // session, and an attached client is free to peek at another | 1737 | // live one. `buildDump` owns the resolution and the refusal. |
| 1967 | // live session. See buildDump for the resolution and the | ||
| 1968 | // in-words unknown-name reply. | ||
| 1969 | const dump = self.buildDump(frame.payload) catch { | 1738 | const dump = self.buildDump(frame.payload) catch { |
| 1970 | self.dropPeer(p); | 1739 | self.dropPeer(p); |
| 1971 | return true; | 1740 | return true; |
| @@ -1983,14 +1752,9 @@ pub const Server = struct { | |||
| 1983 | var buf: [proto.end_reply_max_len]u8 = undefined; | 1752 | var buf: [proto.end_reply_max_len]u8 = undefined; |
| 1984 | self.replyTo(p, .end_reply, proto.encodeEndReply(&buf, v.accepted, v.others, v.reason)); | 1753 | self.replyTo(p, .end_reply, proto.encodeEndReply(&buf, v.accepted, v.others, v.reason)); |
| 1985 | }, | 1754 | }, |
| 1986 | // Shut down via the signal path, not a second one: this is the | 1755 | // The flag SIGTERM sets, not a second shutdown path, so the poll |
| 1987 | // flag SIGTERM sets, so the poll loop, deinit's unlink and the | 1756 | // loop and both teardowns stay already-tested code. The ack is the |
| 1988 | // pty teardown are all already-tested code. No reply is sent — | 1757 | // socket dying, which is what `mux d stop` polls for. |
| 1989 | // the ack is the socket dying, which is what `mux d stop` polls | ||
| 1990 | // for. `mux d stop` never attaches, so in the product this is | ||
| 1991 | // always the observer arm; a client reaching it grants no | ||
| 1992 | // authority an attached client lacks — see the .input arm, which | ||
| 1993 | // already writes to the pty master. | ||
| 1994 | .stop_req => shutdown_flag.store(true, .release), | 1758 | .stop_req => shutdown_flag.store(true, .release), |
| 1995 | else => return false, | 1759 | else => return false, |
| 1996 | } | 1760 | } |
| @@ -1998,18 +1762,9 @@ pub const Server = struct { | |||
| 1998 | } | 1762 | } |
| 1999 | 1763 | ||
| 2000 | fn onAttach(self: *Server, i: usize, frame: proto.Frame) void { | 1764 | fn onAttach(self: *Server, i: usize, frame: proto.Frame) void { |
| 2001 | // This used to snapshot unconditionally, on the reasoning | 1765 | // Not an unconditional snapshot: a QUIC connection is promoted to a |
| 2002 | // that a client attaching twice over one connection is rare | 1766 | // client slot at handshake, so its FIRST attach arrives here. Snapshot |
| 2003 | // enough not to deserve a second resync path. That was true | 1767 | // -serving them all would deny every QUIC client a delta resume. |
| 2004 | // while every client began life as an observer and did its | ||
| 2005 | // FIRST attach from there — and it stopped being true the | ||
| 2006 | // moment QUIC clients existed, because a QUIC connection is | ||
| 2007 | // promoted to a client slot when its handshake completes, so | ||
| 2008 | // its first attach arrives HERE. Every QUIC attach was | ||
| 2009 | // therefore snapshot-served, and a reconnecting QUIC client | ||
| 2010 | // could never resume from a delta no matter what it held. | ||
| 2011 | // Found by the reconnect scenario in e2e, which asserts the | ||
| 2012 | // counter rather than the rendering. | ||
| 2013 | const req = proto.decodeAttach(frame.payload) catch return; | 1768 | const req = proto.decodeAttach(frame.payload) catch return; |
| 2014 | // Re-resolved on every attach, because an attach on an | 1769 | // Re-resolved on every attach, because an attach on an |
| 2015 | // established connection IS the reconnect path — the name | 1770 | // established connection IS the reconnect path — the name |
| @@ -2022,15 +1777,9 @@ pub const Server = struct { | |||
| 2022 | return; | 1777 | return; |
| 2023 | }; | 1778 | }; |
| 2024 | if (self.clients[i]) |*c| { | 1779 | if (self.clients[i]) |*c| { |
| 2025 | // An await's since_seq is a watermark in the OLD | 1780 | // Seq series are per-session, so an await's `since_seq` from the |
| 2026 | // session's tracker, and seq series are per-session — | 1781 | // old one would answer instantly or never, arbitrarily. Only on a |
| 2027 | // carrying it across would compare one session's | 1782 | // real change: a reconnect re-resolving the same name keeps its await. |
| 2028 | // watermark against another's last_return and answer | ||
| 2029 | // instantly or never, arbitrarily. The re-attach is the | ||
| 2030 | // event that invalidates it, so it is dropped here. | ||
| 2031 | // Only on a real change: a reconnect re-resolving the | ||
| 2032 | // same name is the common path and must not lose an | ||
| 2033 | // await it is still entitled to. | ||
| 2034 | if (c.session != si) c.await_state = null; | 1783 | if (c.session != si) c.await_state = null; |
| 2035 | c.session = si; | 1784 | c.session = si; |
| 2036 | } | 1785 | } |
| @@ -2039,9 +1788,7 @@ pub const Server = struct { | |||
| 2039 | 1788 | ||
| 2040 | /// The seating both attach arms end on, past every refusal so `attaches` | 1789 | /// The seating both attach arms end on, past every refusal so `attaches` |
| 2041 | /// never grows for an attach that seated nobody. Size is recorded only | 1790 | /// never grows for an attach that seated nobody. Size is recorded only |
| 2042 | /// when the grid really went there (a refused one leaves the slot 0x0), | 1791 | /// when the grid really went there, and modes precede the state they describe. |
| 2043 | /// modes go ahead of the state they describe, and only a size change | ||
| 2044 | /// repaints anyone but the joiner. | ||
| 2045 | fn seatClient(self: *Server, i: usize, si: usize, req: proto.AttachReq) void { | 1792 | fn seatClient(self: *Server, i: usize, si: usize, req: proto.AttachReq) void { |
| 2046 | self.stats.attaches += 1; | 1793 | self.stats.attaches += 1; |
| 2047 | self.bumpActivity(i); | 1794 | self.bumpActivity(i); |
| @@ -2064,30 +1811,11 @@ pub const Server = struct { | |||
| 2064 | } | 1811 | } |
| 2065 | 1812 | ||
| 2066 | fn onInput(self: *Server, i: usize, frame: proto.Frame) void { | 1813 | fn onInput(self: *Server, i: usize, frame: proto.Frame) void { |
| 2067 | // Latest wins follows *activity*, which includes typing: | 1814 | // Latest wins follows ACTIVITY, and typing is activity: the console |
| 2068 | // the console you are typing at claims the grid, exactly as | 1815 | // you type at claims the grid. Scrollback, stats and detach are |
| 2069 | // attaching or resizing from it would. Costs one broadcast | 1816 | // deliberately not — paging history from a small terminal would yank |
| 2070 | // per switch between differently-sized clients — the first | 1817 | // the grid from whoever is working. Claimed before the bytes go out, |
| 2071 | // keystroke moves the grid, and every keystroke after that | 1818 | // so the shell reacts at the size the typist is watching. |
| 2072 | // finds the sizes already equal and does nothing. | ||
| 2073 | // | ||
| 2074 | // The other client-initiated frames are deliberately not | ||
| 2075 | // activity: .fetch_scrollback would let paging history from | ||
| 2076 | // a small terminal yank the grid away from the client that | ||
| 2077 | // is actually working, and .stats_req/.debug_dump/.detach | ||
| 2078 | // are not somebody using the terminal at all. This is the | ||
| 2079 | // same three-verb line bumpActivity records, so the grid and | ||
| 2080 | // the activity order can never disagree about who is here. | ||
| 2081 | // | ||
| 2082 | // Claimed before the bytes go out, so the shell reacts to | ||
| 2083 | // this keystroke at the size the typist is watching. The | ||
| 2084 | // input is forwarded even if the broadcast dropped this | ||
| 2085 | // client — the bytes are already ours, and dropClient on an | ||
| 2086 | // empty slot is a no-op. | ||
| 2087 | // | ||
| 2088 | // A session-less slot (promoted, never attached) has no | ||
| 2089 | // shell these bytes could honestly reach: dropped, like | ||
| 2090 | // every other session-scoped verb below. | ||
| 2091 | const si = self.clients[i].?.session orelse return; | 1819 | const si = self.clients[i].?.session orelse return; |
| 2092 | self.bumpActivity(i); | 1820 | self.bumpActivity(i); |
| 2093 | self.claimGrid(si, i); | 1821 | self.claimGrid(si, i); |
| @@ -2111,19 +1839,11 @@ pub const Server = struct { | |||
| 2111 | } | 1839 | } |
| 2112 | 1840 | ||
| 2113 | fn onSelectionReq(self: *Server, i: usize, frame: proto.Frame) void { | 1841 | fn onSelectionReq(self: *Server, i: usize, frame: proto.Frame) void { |
| 2114 | // Like scrollback, selection is a client-local observation | 1842 | // Selection is a client-local observation, not input: it claims no |
| 2115 | // of the authoritative grid. In particular it is not input: | 1843 | // size and repaints nobody. Decoded BEFORE the session lookup, because |
| 2116 | // it must not claim this client's advertised size or repaint | 1844 | // a malformed payload carries no id to correlate and only silence is |
| 2117 | // anybody else. | 1845 | // honest — while a well-formed one from a session-less slot deserves |
| 2118 | // | 1846 | // its answer rather than the client's whole timeout. |
| 2119 | // Decoded BEFORE the session is looked up, and that order is | ||
| 2120 | // the whole of this arm's totality. A malformed payload | ||
| 2121 | // carries no id to correlate, so silence is the only honest | ||
| 2122 | // answer and the ordered-replies test above documents it. A | ||
| 2123 | // WELL-FORMED request from a session-less slot (promoted by | ||
| 2124 | // a QUIC handshake, never attached) does carry one, and | ||
| 2125 | // dropping that on the floor costs the client its whole | ||
| 2126 | // timeout for an answer that was one line away. | ||
| 2127 | const req = proto.decodeSelectionReq(frame.payload) catch return; | 1847 | const req = proto.decodeSelectionReq(frame.payload) catch return; |
| 2128 | const si = self.clients[i].?.session orelse { | 1848 | const si = self.clients[i].?.session orelse { |
| 2129 | self.queueSelectionReply(i, req.id, selectionReplyStatus(null), 0, ""); | 1849 | self.queueSelectionReply(i, req.id, selectionReplyStatus(null), 0, ""); |
| @@ -2161,36 +1881,21 @@ pub const Server = struct { | |||
| 2161 | 1881 | ||
| 2162 | fn onStatusReq(self: *Server, i: usize, frame: proto.Frame) void { | 1882 | fn onStatusReq(self: *Server, i: usize, frame: proto.Frame) void { |
| 2163 | const si = self.clients[i].?.session orelse { | 1883 | const si = self.clients[i].?.session orelse { |
| 2164 | // A session-less slot (promoted, never attached — the | 1884 | // A session-less slot has nothing to compare a tail against, so |
| 2165 | // shape a QUIC connection has between handshake and its | 1885 | // the tail IS the question — an observer's `status_req`, reached |
| 2166 | // first attach) has no session of its own to compare a | 1886 | // over QUIC. A one-shot ask against a name, so the resolved index |
| 2167 | // tail against, so the tail IS the question — same as | 1887 | // is used and never stored on the slot. |
| 2168 | // an observer's status_req (serviceObserver's arm), | ||
| 2169 | // which only a unix connection can ever reach. Mirrors | ||
| 2170 | // debug_dump above for the same reason: this is a | ||
| 2171 | // one-shot ask against a name, not an attach, so the | ||
| 2172 | // resolved index is used and never stored on the slot. | ||
| 2173 | const found = self.sessions.find(frame.payload) orelse { | 1888 | const found = self.sessions.find(frame.payload) orelse { |
| 2174 | const name = SessionTable.safeName(frame.payload); | 1889 | const name = SessionTable.safeName(frame.payload); |
| 2175 | // Same wording as the observer arm: one stderr line | 1890 | // Same wording as the observer arm: one stderr line |
| 2176 | // an operator can grep for, since status_reply is a | 1891 | // an operator can grep for, since status_reply is a |
| 2177 | // fixed binary layout with no room for words. | 1892 | // fixed binary layout with no room for words. |
| 2178 | std.debug.print("mux d: status_req for unknown session: {s}\n", .{name}); | 1893 | std.debug.print("mux d: status_req for unknown session: {s}\n", .{name}); |
| 2179 | // The one word the wire has for no, the same one a | 1894 | // The one word the wire has for no. Before the drop, not |
| 2180 | // refused attach gets. Before the drop, not instead | 1895 | // instead of it: silence made `mux a status` report a dead |
| 2181 | // of it: silence made `mux a status --session nosuch` | 1896 | // daemon. It outlives the drop only inside an ngtcp2 callback, |
| 2182 | // report a dead daemon. | 1897 | // where the close defers to `.closing_quiet` and `reapClosing` |
| 2183 | // | 1898 | // puts the byte on the wire. |
| 2184 | // It outlives the drop only because this arm is | ||
| 2185 | // reached from inside an ngtcp2 callback: a QUIC | ||
| 2186 | // sink's send merely QUEUES (quic_server.send), and | ||
| 2187 | // the close that follows defers to `.closing_quiet` | ||
| 2188 | // rather than freeing, so `reapClosing`'s last drain | ||
| 2189 | // is what puts the byte on the wire. The same rope | ||
| 2190 | // `quicOnOpen`'s full-table refusal already hangs | ||
| 2191 | // from. Reach this arm from outside that callback | ||
| 2192 | // and the refusal is lost silently — the socket sink | ||
| 2193 | // is the one that flushes as it queues. | ||
| 2194 | _ = self.queueFrame(i, .exit_status, &.{1}); | 1899 | _ = self.queueFrame(i, .exit_status, &.{1}); |
| 2195 | self.dropClient(i); | 1900 | self.dropClient(i); |
| 2196 | return; | 1901 | return; |
| @@ -2199,13 +1904,9 @@ pub const Server = struct { | |||
| 2199 | _ = self.queueFrame(i, .status_reply, &payload); | 1904 | _ = self.queueFrame(i, .status_reply, &payload); |
| 2200 | return; | 1905 | return; |
| 2201 | }; | 1906 | }; |
| 2202 | // One rule, no aliasing: a non-empty tail must name the | 1907 | // One rule, no aliasing: a non-empty tail must name the slot's OWN |
| 2203 | // slot's OWN session or the frame is ignored outright — | 1908 | // session or the frame is ignored — never answered against the tail. |
| 2204 | // never answered against the tail instead. The client asked | 1909 | // The client asked two questions at once, and neither reading wins. |
| 2205 | // two different questions at once ("what is my slot | ||
| 2206 | // attached to" via the attach it already made, and "what is | ||
| 2207 | // session X" via this tail), and there is no reading where | ||
| 2208 | // both win; a mismatch gets silence, not a second opinion. | ||
| 2209 | if (frame.payload.len != 0 and !std.mem.eql(u8, frame.payload, self.ses(si).name())) return; | 1910 | if (frame.payload.len != 0 and !std.mem.eql(u8, frame.payload, self.ses(si).name())) return; |
| 2210 | const payload = proto.encodeStatusReply(self.buildStatusReply(si)); | 1911 | const payload = proto.encodeStatusReply(self.buildStatusReply(si)); |
| 2211 | _ = self.queueFrame(i, .status_reply, &payload); | 1912 | _ = self.queueFrame(i, .status_reply, &payload); |
| @@ -2223,30 +1924,23 @@ pub const Server = struct { | |||
| 2223 | .timeout_ms = req.timeout_ms, | 1924 | .timeout_ms = req.timeout_ms, |
| 2224 | .started_ms = std.time.milliTimestamp(), | 1925 | .started_ms = std.time.milliTimestamp(), |
| 2225 | }; | 1926 | }; |
| 2226 | // A return that already happened answers immediately — this | 1927 | // A return that already happened answers immediately, which is what |
| 2227 | // is what makes a reconnect re-issue safe. The request is | 1928 | // makes a reconnect re-issue safe — and keeps that true independent |
| 2228 | // answered inside this pump either way, since the pump-end | 1929 | // of the pump-end pass's ordering. |
| 2229 | // pass runs after frame handling; this call keeps that true | ||
| 2230 | // independent of pump-end ordering. | ||
| 2231 | self.checkAwaits(); | 1930 | self.checkAwaits(); |
| 2232 | } | 1931 | } |
| 2233 | 1932 | ||
| 2234 | fn onAgentOffer(self: *Server, i: usize) void { | 1933 | fn onAgentOffer(self: *Server, i: usize) void { |
| 2235 | // Re-sent after every attach, so this is idempotent by | 1934 | // Idempotent by design: a redial re-offers on a slot that may already |
| 2236 | // design: a redial re-offers on a slot that may already be | 1935 | // be flagged, including one `sweepMuteAgentChans` took away — a |
| 2237 | // flagged, and nothing here is allowed to care. That | 1936 | // reattach may bring a working agent. |
| 2238 | // includes an offer sweepMuteAgentChans took away — a | ||
| 2239 | // reattach may bring a working agent, and a mute one costs | ||
| 2240 | // one more agent_answer_ms per attach, not per dial. | ||
| 2241 | self.clients[i].?.agent_offer = true; | 1937 | self.clients[i].?.agent_offer = true; |
| 2242 | } | 1938 | } |
| 2243 | 1939 | ||
| 2244 | fn onAgentData(self: *Server, i: usize, frame: proto.Frame) void { | 1940 | fn onAgentData(self: *Server, i: usize, frame: proto.Frame) void { |
| 2245 | const id = proto.decodeAgentId(frame.payload) catch return; | 1941 | const id = proto.decodeAgentId(frame.payload) catch return; |
| 2246 | // An id naming no channel of THIS client's is dropped in | 1942 | // An id naming no channel of THIS client's is dropped in silence: a |
| 2247 | // silence — the unknown-frame stance, applied to channels. | 1943 | // refusal that told "not yours" from "no such channel" apart would |
| 2248 | // Silence is also the only safe answer: a refusal that | ||
| 2249 | // distinguished "not yours" from "no such channel" would | ||
| 2250 | // tell a client what other clients hold. | 1944 | // tell a client what other clients hold. |
| 2251 | const s = self.agents.find(id, i) orelse return; | 1945 | const s = self.agents.find(id, i) orelse return; |
| 2252 | // The cap, enforced on the side that did not choose it. The | 1946 | // The cap, enforced on the side that did not choose it. The |
| @@ -2260,18 +1954,11 @@ pub const Server = struct { | |||
| 2260 | // A reply proves; a volley before any question does not, or | 1954 | // A reply proves; a volley before any question does not, or |
| 2261 | // a mute peer clears the clock by talking first. | 1955 | // a mute peer clears the clock by talking first. |
| 2262 | if (self.agents.chans[s].?.answer == .asked) self.agents.chans[s].?.answer = .proven; | 1956 | if (self.agents.chans[s].?.answer == .asked) self.agents.chans[s].?.answer = .proven; |
| 2263 | // The daemon's one deliberate blocking write, and it is not | 1957 | // The daemon's one deliberate blocking write. "Never block on a |
| 2264 | // the doctrine's exception it looks like. "Never block on a | 1958 | // client" holds because a client is a stranger across a link; this |
| 2265 | // client" holds because a client is a stranger across a | 1959 | // peer dialled a socket in `makeAgentDir`'s 0700 directory, so it is |
| 2266 | // link. This fd is not a client: it is the local agent | 1960 | // same-uid and could already signal this daemon. Agent traffic is |
| 2267 | // connection, whose peer dialled a socket in the 0700 | 1961 | // small request-response, so the wedge to guard against does not exist. |
| 2268 | // directory `makeAgentDir` made, so it is same-uid — a peer | ||
| 2269 | // that could already stop this daemon with a signal, no | ||
| 2270 | // boundary crossed here that one does not cross. And agent | ||
| 2271 | // traffic is small request-response that cannot fill a | ||
| 2272 | // socket buffer, so the wedge this would have to guard | ||
| 2273 | // against does not exist. Turning this into a drop would | ||
| 2274 | // buy nothing and truncate a request mid-signature. | ||
| 2275 | proto.writeAllFd(self.agents.chans[s].?.fd, frame.payload[proto.agent_id_len..]) catch | 1962 | proto.writeAllFd(self.agents.chans[s].?.fd, frame.payload[proto.agent_id_len..]) catch |
| 2276 | self.agents.closeChan(self, s, .notify); | 1963 | self.agents.closeChan(self, s, .notify); |
| 2277 | } | 1964 | } |
| @@ -2283,24 +1970,15 @@ pub const Server = struct { | |||
| 2283 | if (self.agents.find(id, i)) |s| self.agents.closeChan(self, s, .silent); | 1970 | if (self.agents.find(id, i)) |s| self.agents.closeChan(self, s, .silent); |
| 2284 | } | 1971 | } |
| 2285 | 1972 | ||
| 2286 | /// Resolve any awaits that can be answered this pump. Granularity is | 1973 | /// Resolve any awaits that can be answered this pump, at the run loop's |
| 2287 | /// the run loop's 100ms tick — nothing here blocks, and no deadline | 1974 | /// 100ms granularity. Each client resolves against its OWN slot's |
| 2288 | /// folding into poll is needed at that resolution. One walk over the | 1975 | /// session: another session's return must never answer its await. |
| 2289 | /// clients, each resolved against its OWN slot's session: an await is | ||
| 2290 | /// a question about the shell its asker is attached to, and another | ||
| 2291 | /// session's return must never answer it. | ||
| 2292 | fn checkAwaits(self: *Server) void { | 1976 | fn checkAwaits(self: *Server) void { |
| 2293 | const now = std.time.milliTimestamp(); | 1977 | const now = std.time.milliTimestamp(); |
| 2294 | // One ioctl per session somebody is awaiting on, not one per | 1978 | // One ioctl per SESSION being awaited on, not per waiting client: the |
| 2295 | // waiting client: the foreground process group is a property of a | 1979 | // foreground process group belongs to a session's pty, so every client |
| 2296 | // session's pty, so every awaiting client of the SAME session | 1980 | // of it reads the same number. Null covers both "nobody asked" and |
| 2297 | // reads the same number back — that is the boundary at which | 1981 | // "the ioctl failed". `saw_busy` stays per-client: the transition is its own. |
| 2298 | // "everyone gets the same answer" survived multi-session. Null | ||
| 2299 | // covers both "marks hold the floor, so nobody asked" — the probe | ||
| 2300 | // is skipped entirely then, exactly as before — and "the ioctl | ||
| 2301 | // failed", which has always been silently ignored. What stays | ||
| 2302 | // per-client is `saw_busy`: the transition each await is watching | ||
| 2303 | // for is its own. | ||
| 2304 | var wanted: [max_sessions]bool = @splat(false); | 1982 | var wanted: [max_sessions]bool = @splat(false); |
| 2305 | for (&self.clients) |*cslot| { | 1983 | for (&self.clients) |*cslot| { |
| 2306 | if (cslot.*) |*c| { | 1984 | if (cslot.*) |*c| { |
| @@ -2321,28 +1999,18 @@ pub const Server = struct { | |||
| 2321 | if (self.clients[i] == null) continue; | 1999 | if (self.clients[i] == null) continue; |
| 2322 | const slot = &self.clients[i].?; | 2000 | const slot = &self.clients[i].?; |
| 2323 | if (slot.await_state == null) continue; | 2001 | if (slot.await_state == null) continue; |
| 2324 | // The .await_req arm only accepts an await from a slot with a | 2002 | // Unreachable: `.await_req` only accepts from a slot with a |
| 2325 | // session, and the exit arm atop the pump drops a dying | 2003 | // session, and no client survives its session's exit. Kept because |
| 2326 | // session's clients before this runs — no client survives its | 2004 | // a slot that lost one has nothing an answer could be about. |
| 2327 | // session, so this stays unreachable. Kept because a slot that | ||
| 2328 | // lost one anyway has nothing an answer could honestly be about. | ||
| 2329 | const si = slot.session orelse continue; | 2005 | const si = slot.session orelse continue; |
| 2330 | const fg_pgid = fg_pgids[si]; | 2006 | const fg_pgid = fg_pgids[si]; |
| 2331 | const a = &slot.await_state.?; | 2007 | const a = &slot.await_state.?; |
| 2332 | 2008 | ||
| 2333 | // 1. Marks: a return newer than since_seq answers with the full | 2009 | // 1. Marks: a return newer than `since_seq` answers with the full |
| 2334 | // story. Strictly greater: since_seq is "what I have". | 2010 | // story. Strictly greater — `since_seq` is "what I have". |
| 2335 | // | 2011 | // The WATERMARK alone decides, never live phase: a shell's |
| 2336 | // The watermark alone decides, and it is the snapshot's own | 2012 | // `D;code`+`A` burst lands in one pty read, so phase is back to |
| 2337 | // seq — the answer and the reason it qualifies are the same | 2013 | // `at_prompt` before this line runs. |
| 2338 | // record, so no reading of live tracker state can drift out | ||
| 2339 | // from under this test. Gating on | ||
| 2340 | // `cmd.phase == .returned` here was a defect: the shell's own | ||
| 2341 | // `D;code`+`A` burst lands in one pty read, so phase is back | ||
| 2342 | // to at_prompt before this line ever runs. Keying on the | ||
| 2343 | // watermark also answers correctly once the NEXT command is | ||
| 2344 | // already running — the client asked what had returned since | ||
| 2345 | // its seq, not what is happening now. | ||
| 2346 | if (self.ses(si).last_return) |st| { | 2014 | if (self.ses(si).last_return) |st| { |
| 2347 | if (st.seq > a.since_seq) { | 2015 | if (st.seq > a.since_seq) { |
| 2348 | self.answerAwait(i, st, .returned); | 2016 | self.answerAwait(i, st, .returned); |
| @@ -2384,14 +2052,10 @@ pub const Server = struct { | |||
| 2384 | // not a zero-length deadline but the absence of one — such an | 2052 | // not a zero-length deadline but the absence of one — such an |
| 2385 | // await ends only when marks, the pgid or settle end it. | 2053 | // await ends only when marks, the pgid or settle end it. |
| 2386 | if (a.timeout_ms > 0 and now - a.started_ms >= a.timeout_ms) { | 2054 | if (a.timeout_ms > 0 and now - a.started_ms >= a.timeout_ms) { |
| 2387 | // The phase is left as the session's own — a timeout reports | 2055 | // The phase stays the session's own — a timeout reports where |
| 2388 | // where things stand, mid-command and all — but the code goes, | 2056 | // things stand — but the code goes: nothing returned during |
| 2389 | // for the same reason it goes on the other two arms: nothing | 2057 | // THIS wait, so any code still standing is the previous |
| 2390 | // returned during THIS wait, so any code still standing is the | 2058 | // command's verdict, and `mechanism: "marks"` would sell it. |
| 2391 | // previous command's verdict. On a marks session it would have | ||
| 2392 | // ridden out under `mechanism: "marks"`, the one label the | ||
| 2393 | // published rule tells agents to trust, and been read as the | ||
| 2394 | // answer to a wait that has no answer. | ||
| 2395 | const st = self.fallbackState( | 2059 | const st = self.fallbackState( |
| 2396 | si, | 2060 | si, |
| 2397 | if (self.ses(si).cmd.marks_seen) .marks else .pgid, | 2061 | if (self.ses(si).cmd.marks_seen) .marks else .pgid, |
| @@ -2483,11 +2147,9 @@ pub const Server = struct { | |||
| 2483 | // Attach joins the session; it no longer displaces whoever | 2147 | // Attach joins the session; it no longer displaces whoever |
| 2484 | // was already there (takeover is retired; attach joins). | 2148 | // was already there (takeover is retired; attach joins). |
| 2485 | const slot = self.freeClientSlot() orelse { | 2149 | const slot = self.freeClientSlot() orelse { |
| 2486 | // Client table full. Refusing is the honest answer now | 2150 | // Client table full: the client exits nonzero rather than |
| 2487 | // that attaching can't evict anyone; the client exits | 2151 | // hanging on a silent socket. Checked BEFORE the name |
| 2488 | // nonzero rather than hanging on a silent socket. | 2152 | // resolves, so an unseatable client spawns no shell. |
| 2489 | // Checked BEFORE the name resolves so a client that | ||
| 2490 | // cannot be seated never spawns a shell either. | ||
| 2491 | return self.refuseObserver(i); | 2153 | return self.refuseObserver(i); |
| 2492 | }; | 2154 | }; |
| 2493 | // Attach-or-create; null is the refusal (bad name, session | 2155 | // Attach-or-create; null is the refusal (bad name, session |
| @@ -2508,10 +2170,8 @@ pub const Server = struct { | |||
| 2508 | }, | 2170 | }, |
| 2509 | .detach => self.dropObserver(i), | 2171 | .detach => self.dropObserver(i), |
| 2510 | // Where `mux d upgrade` lands: validate, reply, and let the run | 2172 | // Where `mux d upgrade` lands: validate, reply, and let the run |
| 2511 | // loop exec. The reply is blocking (observer has no send queue); | 2173 | // loop exec. Deferred via `pending_upgrade` so the close-all and |
| 2512 | // the exec is deferred to the run loop via pending_upgrade so | 2174 | // execve happen outside the observer's read cycle. |
| 2513 | // the frame handler returns cleanly and the close-all + execve | ||
| 2514 | // happen outside the observer's read cycle. | ||
| 2515 | .upgrade_req => { | 2175 | .upgrade_req => { |
| 2516 | const req = proto.parseUpgradeReq(frame.payload) catch return self.refuseUpgrade(i, "bad frame"); | 2176 | const req = proto.parseUpgradeReq(frame.payload) catch return self.refuseUpgrade(i, "bad frame"); |
| 2517 | if (self.validateUpgrade(req, self.version)) |reason| { | 2177 | if (self.validateUpgrade(req, self.version)) |reason| { |
| @@ -2546,14 +2206,10 @@ pub const Server = struct { | |||
| 2546 | // tail against — the tail IS the question. | 2206 | // tail against — the tail IS the question. |
| 2547 | const si = self.sessions.find(frame.payload) orelse { | 2207 | const si = self.sessions.find(frame.payload) orelse { |
| 2548 | const name = SessionTable.safeName(frame.payload); | 2208 | const name = SessionTable.safeName(frame.payload); |
| 2549 | // status_reply is StatusReply, a fixed binary layout | 2209 | // `status_reply` is a fixed binary layout with no room for |
| 2550 | // with no room for words — unlike dump_reply, which is | 2210 | // words, so the answer is the `exit_status` 1 a refused |
| 2551 | // free-form bytes and can just say so. So the answer is | 2211 | // attach gets and the name goes on stderr. Bounded write: |
| 2552 | // the same exit_status 1 a refused attach gets, then the | 2212 | // the byte must be gone before the fd, not at the pump's price. |
| 2553 | // close, with the name on stderr where an operator (not | ||
| 2554 | // the wire) reads it. Bounded write like the attach | ||
| 2555 | // refusal above: the byte must be gone before the fd is, | ||
| 2556 | // but not at the price of the whole pump. | ||
| 2557 | std.debug.print("mux d: status_req for unknown session: {s}\n", .{name}); | 2213 | std.debug.print("mux d: status_req for unknown session: {s}\n", .{name}); |
| 2558 | return self.refuseObserver(i); | 2214 | return self.refuseObserver(i); |
| 2559 | }; | 2215 | }; |
| @@ -2565,26 +2221,18 @@ pub const Server = struct { | |||
| 2565 | } | 2221 | } |
| 2566 | 2222 | ||
| 2567 | /// `payload` is 1 byte (0 = plain, 1 = vt) ++ an optional session-name | 2223 | /// `payload` is 1 byte (0 = plain, 1 = vt) ++ an optional session-name |
| 2568 | /// tail, same pattern as attach/status_req/await_req — empty names the | 2224 | /// tail; empty names the default. Resolved via `findSession`, never |
| 2569 | /// default. Resolved via `findSession`, never `resolveSession`: a dump | 2225 | /// `resolveSession`: a read that could spawn a shell would make |
| 2570 | /// is a read, and a read that could spawn a shell would make `mux d dump | 2226 | /// `mux d dump --session typo` a way to stand one up. An unknown name |
| 2571 | /// --session typo` a way to accidentally stand one up. An unknown name | 2227 | /// answers IN WORDS, because `dump_reply` is the only frame this verb |
| 2572 | /// answers IN WORDS rather than as a dropped connection — the wire | 2228 | /// gets. The name quoted is the RESOLVED one. Caller owns the result. |
| 2573 | /// carries no separate channel for "no", and `dump_reply` is the only | ||
| 2574 | /// frame this verb ever gets, so the words have to travel inside it. | ||
| 2575 | /// The name in the message is the RESOLVED one (after ""→default), so | ||
| 2576 | /// it names what was actually looked up, not what was typed. | ||
| 2577 | /// Caller owns the result. | ||
| 2578 | fn buildDump(self: *Server, payload: []const u8) ![]const u8 { | 2229 | fn buildDump(self: *Server, payload: []const u8) ![]const u8 { |
| 2579 | const want_vt = payload.len >= 1 and payload[0] == 1; | 2230 | const want_vt = payload.len >= 1 and payload[0] == 1; |
| 2580 | const wire_name = if (payload.len >= 2) payload[1..] else ""; | 2231 | const wire_name = if (payload.len >= 2) payload[1..] else ""; |
| 2581 | const si = self.sessions.find(wire_name) orelse { | 2232 | const si = self.sessions.find(wire_name) orelse { |
| 2582 | // Through safeName like the two status_req arms. This reply goes | 2233 | // Through `safeName` like the two `status_req` arms: `mux d dump` |
| 2583 | // back to the peer that asked, so it is self-inflicted rather | 2234 | // prints this straight to a console, and one rule for "a name in a |
| 2584 | // than an injection into someone else's terminal — but `mux d | 2235 | // message" beats remembering which site was the safe one. |
| 2585 | // dump` prints it straight to a console, and one rule for | ||
| 2586 | // "a name in a message" is cheaper than remembering which of | ||
| 2587 | // the three sites was the safe one. | ||
| 2588 | const name = SessionTable.safeName(wire_name); | 2236 | const name = SessionTable.safeName(wire_name); |
| 2589 | return std.fmt.allocPrint(self.alloc, "mux d: no such session: {s}\n", .{name}); | 2237 | return std.fmt.allocPrint(self.alloc, "mux d: no such session: {s}\n", .{name}); |
| 2590 | }; | 2238 | }; |
| @@ -2667,10 +2315,8 @@ pub const Server = struct { | |||
| 2667 | 2315 | ||
| 2668 | /// The only place a send is counted, so the four paths cannot drift on | 2316 | /// The only place a send is counted, so the four paths cannot drift on |
| 2669 | /// what a byte means. `to` is one client, null every client of THIS | 2317 | /// what a byte means. `to` is one client, null every client of THIS |
| 2670 | /// session — where a failed write drops only that client. Counted bytes | 2318 | /// session. Counted bytes exclude the 5-byte header here and in the |
| 2671 | /// are payload actually written, the 5-byte header excluded here and in | 2319 | /// counterfactual alike (~8% understated), as the bench measures it. |
| 2672 | /// the counterfactual (~8% understated). The counterfactual accrues once | ||
| 2673 | /// per EVENT and only if someone got it, as the bench measures it. | ||
| 2674 | fn countedSend(self: *Server, si: usize, t: proto.MsgType, payload: []const u8, to: ?usize) void { | 2320 | fn countedSend(self: *Server, si: usize, t: proto.MsgType, payload: []const u8, to: ?usize) void { |
| 2675 | var sent = false; | 2321 | var sent = false; |
| 2676 | for (0..max_clients) |i| { | 2322 | for (0..max_clients) |i| { |
| @@ -2698,19 +2344,14 @@ pub const Server = struct { | |||
| 2698 | } | 2344 | } |
| 2699 | 2345 | ||
| 2700 | /// Per-update path: diff and broadcast a delta; discontinuities resync. | 2346 | /// Per-update path: diff and broadcast a delta; discontinuities resync. |
| 2701 | /// The tracker advances even with nobody attached, so a client that | 2347 | /// The tracker advances with nobody attached, so a quiet detacher can |
| 2702 | /// detached quietly can still be caught up by delta when it returns — | 2348 | /// still be caught up — but WITHOUT diffing, since rendering every row |
| 2703 | /// but with nobody attached it advances WITHOUT diffing, because the | 2349 | /// to hash it would be thrown away. The returner gets a whole-grid delta. |
| 2704 | /// diff's cost is rendering every row and the result would be thrown | ||
| 2705 | /// away. The returning client is caught up by a delta covering the | ||
| 2706 | /// whole grid instead of one naming the rows that moved. | ||
| 2707 | pub fn sendUpdate(self: *Server, si: usize) void { | 2350 | pub fn sendUpdate(self: *Server, si: usize) void { |
| 2708 | const s = self.ses(si); | 2351 | const s = self.ses(si); |
| 2709 | // Asked before the diff, not after. update() renders every row to | 2352 | // Before the diff, not after: `update()` renders every row to hash |
| 2710 | // hash it, and with nobody attached that payload has nowhere to go | 2353 | // it, and with nobody attached that payload has nowhere to go — ~60% |
| 2711 | // — it was ~60% of the daemon's cycles on a full-width repaint. | 2354 | // of the daemon's cycles on a full-width repaint. |
| 2712 | // The check used to sit in the .advanced arm below, which is after | ||
| 2713 | // the whole loop has already run. | ||
| 2714 | if (!self.hasClientsIn(si)) { | 2355 | if (!self.hasClientsIn(si)) { |
| 2715 | // Still a discontinuity's caller: resyncSnapshot's rebuild is | 2356 | // Still a discontinuity's caller: resyncSnapshot's rebuild is |
| 2716 | // documented to happen with nobody attached, so the tracker is | 2357 | // documented to happen with nobody attached, so the tracker is |
| @@ -2740,14 +2381,9 @@ pub const Server = struct { | |||
| 2740 | } | 2381 | } |
| 2741 | 2382 | ||
| 2742 | /// Fold the engine's OSC 133 events into the command tracker and tell | 2383 | /// Fold the engine's OSC 133 events into the command tracker and tell |
| 2743 | /// attached clients about transitions. Runs after sendUpdate so | 2384 | /// clients about transitions. After `sendUpdate`, so `tracker.seq` |
| 2744 | /// tracker.seq already covers the same pty chunk — the off-by-one the | 2385 | /// already covers the same pty chunk. The pump's pty-read arm is the only |
| 2745 | /// spec pins (seq sampled post-feed). | 2386 | /// caller: marks from a test that feeds an engine directly sit pending. |
| 2746 | /// | ||
| 2747 | /// The pump's pty-read arm is the only caller, so marks produced by a | ||
| 2748 | /// test that feeds a session's engine directly (srv.ses(si).eng.feed) | ||
| 2749 | /// sit pending until a read arm runs — they are not lost, but they are | ||
| 2750 | /// not folded yet. | ||
| 2751 | fn drainMarkEvents(self: *Server, si: usize) void { | 2387 | fn drainMarkEvents(self: *Server, si: usize) void { |
| 2752 | const s = self.ses(si); | 2388 | const s = self.ses(si); |
| 2753 | for (s.eng.markEvents()) |ev| { | 2389 | for (s.eng.markEvents()) |ev| { |
| @@ -2778,57 +2414,24 @@ pub const Server = struct { | |||
| 2778 | s.eng.clearMarkEvents(); | 2414 | s.eng.clearMarkEvents(); |
| 2779 | } | 2415 | } |
| 2780 | 2416 | ||
| 2781 | /// Ship the engine's side-channel events to this session's clients. | 2417 | /// Ship the engine's side-channel events to THIS session's clients, in |
| 2782 | /// Runs in the pty-read arm beside drainMarkEvents, for the same | 2418 | /// the pty-read arm beside `drainMarkEvents` because the events describe |
| 2783 | /// reason: the events describe the chunk that was just fed. | 2419 | /// the chunk just fed. A clipboard push crossing sessions would set the |
| 2784 | /// | 2420 | /// user's clipboard from a session they are not looking at. |
| 2785 | /// This session's clients alone. A clipboard write belongs to the shell | ||
| 2786 | /// that produced it, and a push crossing sessions would set the user's | ||
| 2787 | /// clipboard from a session they are not looking at. | ||
| 2788 | fn drainSideEvents(self: *Server, si: usize) void { | 2421 | fn drainSideEvents(self: *Server, si: usize) void { |
| 2789 | const s = self.ses(si); | 2422 | const s = self.ses(si); |
| 2790 | // This used to bail here when the session had no clients, on the | 2423 | // No bail when the session has no clients: with nobody attached is |
| 2791 | // reasoning that encoding for nobody is pure waste. It was right | 2424 | // when a recorded event matters MOST, since that is the gap a |
| 2792 | // while the events were live-only, and the pending slots are exactly | 2425 | // reconnecting client asks to be caught up across. Recording needs the |
| 2793 | // what makes it wrong: with nobody attached is when a recorded event | 2426 | // encode — the slot holds the wire payload. Bounded: one payload per |
| 2794 | // MATTERS MOST, because that is the gap a reconnecting client will | 2427 | // kind survives, and one buffer serves the whole drain. |
| 2795 | // ask to be caught up across. Encoding is what recording needs — the | ||
| 2796 | // slot holds the wire payload — so "skip the work when nobody is | ||
| 2797 | // listening" and "remember it for whoever comes back" cannot both be | ||
| 2798 | // had, and the feature is the one worth paying for. | ||
| 2799 | // | ||
| 2800 | // What the removal costs is bounded, which is why it is affordable: | ||
| 2801 | // the per-event encode is transient and reuses the one buffer below, | ||
| 2802 | // and what survives the call is at most one payload per kind, freed | ||
| 2803 | // as its replacement lands. A detached session ringing its bell in a | ||
| 2804 | // loop churns an allocation per ring where it used to churn none — | ||
| 2805 | // and holds one bell payload, not a log of them. | ||
| 2806 | // | ||
| 2807 | // One buffer for the whole drain rather than one per event: a burst | ||
| 2808 | // of bells is a single allocation that the first event sizes. | ||
| 2809 | var payload: std.ArrayList(u8) = .empty; | 2428 | var payload: std.ArrayList(u8) = .empty; |
| 2810 | defer payload.deinit(self.alloc); | 2429 | defer payload.deinit(self.alloc); |
| 2811 | // At most one bell per drain. N rings inside one 64 KiB chunk are one | 2430 | // At most one bell per DRAIN: N rings inside one 64 KiB chunk are one |
| 2812 | // ring to a human, and the bell is the cheapest event a session can | 2431 | // ring to a human, and `cat` on a binary produces ~256 of them per |
| 2813 | // emit in bulk: 0x07 is ~1/256 of random bytes, so `cat` on a binary | 2432 | // chunk. Per drain and not per session — a bell a second later is a |
| 2814 | // produces ~256 frames per chunk — 6 bytes on the wire and a | 2433 | // separate ring. The pending slot is unaffected: it is one slot per |
| 2815 | // writeAllFd on every client's stdout apiece, for a sound that cannot | 2434 | // kind at a `tracker.seq` that does not move inside this loop. |
| 2816 | // ring 256 times. sampleTermModes states the discipline this restores: | ||
| 2817 | // "bytes proportional to what changed". Modes dedup, titles are | ||
| 2818 | // sampled, the grid folds into one bounded delta; the bell was the | ||
| 2819 | // only client-bound stream left that did not. | ||
| 2820 | // | ||
| 2821 | // Per DRAIN, not per session. A bell a second later is a separate ring | ||
| 2822 | // and gets its own frame — only a burst the user could not have heard | ||
| 2823 | // apart folds. A session-lifetime flag would silence every bell after | ||
| 2824 | // the first, which is a different feature and a broken one. | ||
| 2825 | // | ||
| 2826 | // The pending slot is unaffected, which is worth stating because it is | ||
| 2827 | // the half that could have gone wrong: it is one slot per kind stamped | ||
| 2828 | // with tracker.seq, and tracker.seq does not move inside this loop. So | ||
| 2829 | // 256 rings recorded the old way left exactly what one ring leaves — | ||
| 2830 | // the same payload at the same seq, after 255 pointless dupe-and-free | ||
| 2831 | // pairs. Coalescing removes the churn and changes no recorded state. | ||
| 2832 | var rang = false; | 2435 | var rang = false; |
| 2833 | for (s.eng.sideEvents()) |ev| { | 2436 | for (s.eng.sideEvents()) |ev| { |
| 2834 | if (ev.kind == .bell) { | 2437 | if (ev.kind == .bell) { |
| @@ -2837,16 +2440,10 @@ pub const Server = struct { | |||
| 2837 | } | 2440 | } |
| 2838 | payload.clearRetainingCapacity(); | 2441 | payload.clearRetainingCapacity(); |
| 2839 | switch (ev.kind) { | 2442 | switch (ev.kind) { |
| 2840 | // What a failed encode gives up is this one event: a | 2443 | // A failed encode gives up this one event, silently: there is |
| 2841 | // clipboard copy the user asked for that silently does not | 2444 | // nowhere in this file to say it. `continue` and not `return`, |
| 2842 | // happen. Silently because there is nowhere to say it — this | 2445 | // because a 64 KiB clipboard can exhaust memory while the |
| 2843 | // file makes no log calls and `upgrade.Counters` counts no | 2446 | // 1-byte bell behind it would have gone out fine. |
| 2844 | // errors, and a mechanism invented for this one path would | ||
| 2845 | // be the only one of its kind. `continue` rather than | ||
| 2846 | // `return` because the failure is per-event and the sizes | ||
| 2847 | // are wildly uneven: a 64 KiB clipboard can exhaust memory | ||
| 2848 | // while the 1-byte bell queued behind it would have gone | ||
| 2849 | // out fine. | ||
| 2850 | .clipboard => proto.encodeClipboardEvent( | 2447 | .clipboard => proto.encodeClipboardEvent( |
| 2851 | &payload, | 2448 | &payload, |
| 2852 | self.alloc, | 2449 | self.alloc, |
| @@ -2858,12 +2455,9 @@ pub const Server = struct { | |||
| 2858 | for (0..max_clients) |i| { | 2455 | for (0..max_clients) |i| { |
| 2859 | if (self.inSession(i, si)) _ = self.queueFrame(i, .term_event, payload.items); | 2456 | if (self.inSession(i, si)) _ = self.queueFrame(i, .term_event, payload.items); |
| 2860 | } | 2457 | } |
| 2861 | // After the live clients, and unconditionally: a session can have | 2458 | // After the live clients and unconditionally: one client watching |
| 2862 | // one client watching while another is away mid-reconnect, so | 2459 | // is no reason to leave a reconnecting one's gap empty. Stamped at |
| 2863 | // "somebody got it live" is no reason to leave the gap empty. | 2460 | // the `tracker.seq` `sendUpdate` already advanced over this chunk. |
| 2864 | // The stamp is tracker.seq, which sendUpdate has already advanced | ||
| 2865 | // over this same pty chunk — the off-by-one drainMarkEvents | ||
| 2866 | // documents, relied on here for the same reason. | ||
| 2867 | s.recordPending(self.alloc, ev.kind, payload.items); | 2461 | s.recordPending(self.alloc, ev.kind, payload.items); |
| 2868 | } | 2462 | } |
| 2869 | // Unconditional, and outside the loop: an encode that failed on OOM | 2463 | // Unconditional, and outside the loop: an encode that failed on OOM |
| @@ -2891,11 +2485,9 @@ pub const Server = struct { | |||
| 2891 | } | 2485 | } |
| 2892 | 2486 | ||
| 2893 | /// No history is kept: a reattaching client needs the current value, which | 2487 | /// No history is kept: a reattaching client needs the current value, which |
| 2894 | /// is also why sendResync sends it unconditionally. | 2488 | /// is why `sendResync` sends it unconditionally. The early return is the |
| 2895 | /// | 2489 | /// point — modes change twice in a session's life, chunks arrive by the |
| 2896 | /// The early return is the whole point. Modes change perhaps twice in a | 2490 | /// thousand, and a frame per chunk is not proportional to what changed. |
| 2897 | /// session's life while chunks arrive by the thousand, and a frame per | ||
| 2898 | /// chunk would break the "bytes proportional to what changed" discipline. | ||
| 2899 | fn sampleTermModes(self: *Server, si: usize) void { | 2491 | fn sampleTermModes(self: *Server, si: usize) void { |
| 2900 | const s = self.ses(si); | 2492 | const s = self.ses(si); |
| 2901 | const now = sampledModes(s.eng); | 2493 | const now = sampledModes(s.eng); |
| @@ -2909,14 +2501,10 @@ pub const Server = struct { | |||
| 2909 | } | 2501 | } |
| 2910 | } | 2502 | } |
| 2911 | 2503 | ||
| 2912 | /// An empty title is NOT sent, and that is the whole of mux's policy on | 2504 | /// An empty title is NOT sent: clearing would wipe whatever the user's own |
| 2913 | /// clearing. `ESC]0;BEL` on the client would wipe whatever the user's own | 2505 | /// terminal had in its title bar, and silence is not "set it to empty". |
| 2914 | /// terminal had in its title bar, and a session that never set a title has | 2506 | /// Accepted consequence — a session that genuinely clears its title leaves |
| 2915 | /// said nothing that entitles mux to do that — silence is not "set it to | 2507 | /// the last one standing. `sendResync` must apply the same two rules. |
| 2916 | /// empty". The consequence accepted: a session that sets a title and then | ||
| 2917 | /// genuinely clears it leaves the last one standing. `sendResync` applies | ||
| 2918 | /// the same two rules; they must agree, or an attach would assert | ||
| 2919 | /// something the sampler would never have sent. | ||
| 2920 | fn sampleTermTitle(self: *Server, si: usize) void { | 2508 | fn sampleTermTitle(self: *Server, si: usize) void { |
| 2921 | const s = self.ses(si); | 2509 | const s = self.ses(si); |
| 2922 | const now = s.eng.title(); | 2510 | const now = s.eng.title(); |
| @@ -2986,11 +2574,9 @@ pub const Server = struct { | |||
| 2986 | .history_rows = s.eng.historyRows(), | 2574 | .history_rows = s.eng.historyRows(), |
| 2987 | .alt_screen = s.eng.onAltScreen(), | 2575 | .alt_screen = s.eng.onAltScreen(), |
| 2988 | // Live read, then the last one that worked, then a default. The | 2576 | // Live read, then the last one that worked, then a default. The |
| 2989 | // middle term is the one worth having: mode_sent is refreshed | 2577 | // middle term is the point: `mode_sent` is a real tcgetattr from |
| 2990 | // from a real tcgetattr every pump, so if this read is the one | 2578 | // last pump, so a failed read reports a remembered truth rather |
| 2991 | // that fails there is a remembered truth to report instead of a | 2579 | // than a fabricated "canonical and echoing". |
| 2992 | // fabricated "canonical and echoing" that may be the opposite of | ||
| 2993 | // what the session is doing. | ||
| 2994 | .mode = self.readPtyMode(si) orelse | 2580 | .mode = self.readPtyMode(si) orelse |
| 2995 | (s.mode_sent orelse .{ .icanon = true, .echo = true }), | 2581 | (s.mode_sent orelse .{ .icanon = true, .echo = true }), |
| 2996 | .cmd = self.cmdState(si, if (s.cmd.marks_seen) .marks else .pgid), | 2582 | .cmd = self.cmdState(si, if (s.cmd.marks_seen) .marks else .pgid), |
| @@ -3051,27 +2637,17 @@ pub const Server = struct { | |||
| 3051 | self.countedSend(si, .snapshot, payload, i); | 2637 | self.countedSend(si, .snapshot, payload, i); |
| 3052 | } | 2638 | } |
| 3053 | 2639 | ||
| 3054 | /// Attach/reattach for the client in slot `i`. Three cases: | 2640 | /// Attach/reattach for the client in slot `i`. Three cases: the size |
| 3055 | /// - the size changed: latest wins, so the new grid is broadcast to | 2641 | /// changed, so latest wins and everyone gets a snapshot; the size held |
| 3056 | /// everyone as a snapshot; | 2642 | /// and `have_seq` is serviceable, so this client alone gets a delta; or |
| 3057 | /// - same size and a serviceable have_seq: a delta, to this client | 2643 | /// it is unserviceable and this client alone gets a snapshot — the case a |
| 3058 | /// only, since only it is behind; | 2644 | /// FIRST attach takes, which is why it must not broadcast. Serviceable |
| 3059 | /// - same size and an unserviceable have_seq: a snapshot, again to | 2645 | /// means `have_epoch` names THIS daemon instance, or a client holding a |
| 3060 | /// this client only. This is the case a FIRST attach takes — the | 2646 | /// restarted daemon's seq would be told it is current. |
| 3061 | /// shipped client opens with have_seq=0 — which is exactly why it | ||
| 3062 | /// must not broadcast. | ||
| 3063 | /// | ||
| 3064 | /// Serviceable means the seq is ours to interpret: `have_epoch` must | ||
| 3065 | /// name THIS daemon instance. Without that check a client holding | ||
| 3066 | /// seq 900 from a daemon that has since been restarted would be told | ||
| 3067 | /// "you are current" against a session it has never seen a byte of. | ||
| 3068 | fn sendResync(self: *Server, si: usize, i: usize, have_seq: u64, have_epoch: u64, size_changed: bool) void { | 2647 | fn sendResync(self: *Server, si: usize, i: usize, have_seq: u64, have_epoch: u64, size_changed: bool) void { |
| 3069 | // After whichever content this call ends up sending. Modes and the | 2648 | // After whichever content this call sends. Modes and the title are |
| 3070 | // title are state, not history: a client returning to a session must | 2649 | // state, not history, so a snapshot needs them as much as a delta. One |
| 3071 | // be told what is true now, and one that got a full snapshot needs it | 2650 | // registration ahead of all three returns, so a fourth cannot skip it. |
| 3072 | // exactly as much as one that got a delta. One registration ahead of | ||
| 3073 | // all three returns below, so that a fourth cannot be added without | ||
| 3074 | // them. | ||
| 3075 | defer self.sendSampledStateTo(si, i); | 2651 | defer self.sendSampledStateTo(si, i); |
| 3076 | if (size_changed) { | 2652 | if (size_changed) { |
| 3077 | self.resyncSnapshot(si); | 2653 | self.resyncSnapshot(si); |
| @@ -3108,16 +2684,11 @@ pub const Server = struct { | |||
| 3108 | } | 2684 | } |
| 3109 | } | 2685 | } |
| 3110 | 2686 | ||
| 3111 | /// The delta branch of `sendResync` is the only caller: a client the grid | 2687 | /// The delta branch of `sendResync` is the only caller: a client that |
| 3112 | /// believes watched continuously is owed its gap, while one repainted from | 2688 | /// watched continuously is owed its gap, while one repainted from scratch |
| 3113 | /// scratch is a stranger — replaying a stale clipboard write would hijack | 2689 | /// is a stranger whose clipboard a replay would hijack. Wire order is |
| 3114 | /// its user's clipboard now. | 2690 | /// delta → events, since a terminal ACTS on a bell. `> have_seq`, not |
| 3115 | /// | 2691 | /// `>=`: an event at the client's own seq is one it saw. |
| 3116 | /// Wire order is delta → events: the host terminal ACTS on a bell or | ||
| 3117 | /// clipboard set, and acting before the repaint dings about a screen the | ||
| 3118 | /// user cannot see yet. | ||
| 3119 | /// | ||
| 3120 | /// `> have_seq`, not `>=`: an event at the client's own seq is one it saw. | ||
| 3121 | fn replayPending(self: *Server, si: usize, i: usize, have_seq: u64) void { | 2692 | fn replayPending(self: *Server, si: usize, i: usize, have_seq: u64) void { |
| 3122 | const s = self.ses(si); | 2693 | const s = self.ses(si); |
| 3123 | // Enum declaration order, so clipboard precedes bell. Fixed rather | 2694 | // Enum declaration order, so clipboard precedes bell. Fixed rather |
| @@ -3211,12 +2782,9 @@ pub const Server = struct { | |||
| 3211 | var recs: std.ArrayList(upgrade.SessionRec) = .empty; | 2782 | var recs: std.ArrayList(upgrade.SessionRec) = .empty; |
| 3212 | defer recs.deinit(a); | 2783 | defer recs.deinit(a); |
| 3213 | for (&self.sessions.table) |*slot| { | 2784 | for (&self.sessions.table) |*slot| { |
| 3214 | // BY POINTER, and it is load-bearing: `slot.* orelse continue` | 2785 | // BY POINTER: `slot.* orelse continue` copies the Session, and |
| 3215 | // copies the Session, and `name()` slices that copy's name_buf, | 2786 | // `name()` slices that copy's `name_buf`, which dies with the |
| 3216 | // which dies with the iteration. Every record then held a | 2787 | // iteration — every record would dangle into one reused stack slot. |
| 3217 | // dangling slice into one reused stack slot, so an upgrade | ||
| 3218 | // renamed every session after the first to the LAST one's name, | ||
| 3219 | // truncated to its own length — right lengths, wrong bytes. | ||
| 3220 | if (slot.* == null) continue; | 2788 | if (slot.* == null) continue; |
| 3221 | const s = &slot.*.?; | 2789 | const s = &slot.*.?; |
| 3222 | const vt = try s.eng.dumpState(a); | 2790 | const vt = try s.eng.dumpState(a); |
| @@ -3259,18 +2827,13 @@ pub const Server = struct { | |||
| 3259 | for (recs.items) |r| a.free(r.vt); | 2827 | for (recs.items) |r| a.free(r.vt); |
| 3260 | } | 2828 | } |
| 3261 | 2829 | ||
| 3262 | // Validation returns a refusal reason or null (accept). The checks run | 2830 | // A refusal reason, or null to accept. Ordered cheapest refusal first, |
| 3263 | // in order so a cheaper refusal comes before a dearer one; each reason | 2831 | // and each reason distinct so the requester can tell what failed. |
| 3264 | // is distinct so the requester can tell what failed. Checks 3–4 | ||
| 3265 | // (child-run --version and --check) live in a helper so the e2e legs | ||
| 3266 | // drive them unchanged when chunk D lands --check. | ||
| 3267 | pub fn validateUpgrade(self: *Server, req: proto.UpgradeReq, my_version: []const u8) ?[]const u8 { | 2832 | pub fn validateUpgrade(self: *Server, req: proto.UpgradeReq, my_version: []const u8) ?[]const u8 { |
| 3268 | const a = self.alloc; | 2833 | const a = self.alloc; |
| 3269 | // 0. No session mid-hangup. `endSession` closed that master already, | 2834 | // 0. No session mid-hangup: `endSession` already closed that master, |
| 3270 | // and the exec's clearCloexec — like the new binary's sealFd on the | 2835 | // and the exec's `clearCloexec` is `unreachable` on -1, not an error. |
| 3271 | // pty_fd the manifest would carry — is `unreachable` on -1, not an | 2836 | // Before the child-run checks, which cost two spawns. |
| 3272 | // error. Before the child-run checks, which cost two spawns. The | ||
| 3273 | // wait is bounded by term_grace_ms, hence "retry". | ||
| 3274 | for (self.sessions.table) |slot| { | 2837 | for (self.sessions.table) |slot| { |
| 3275 | if (slot) |session| { | 2838 | if (slot) |session| { |
| 3276 | if (session.pty.master < 0) return a.dupe(u8, "session ending, retry") catch null; | 2839 | if (session.pty.master < 0) return a.dupe(u8, "session ending, retry") catch null; |
| @@ -3392,26 +2955,17 @@ pub const Server = struct { | |||
| 3392 | } | 2955 | } |
| 3393 | } | 2956 | } |
| 3394 | 2957 | ||
| 3395 | // The exec itself. NOTHING from Server.deinit runs on this path — no | 2958 | // The exec itself. NOTHING from `Server.deinit` runs here: the process |
| 3396 | // unlink, no deleteTree, no SIGTERM (`Server.deinit`). The | 2959 | // that owns the socket path, the shim dir and the agent dir never exits, |
| 3397 | // process that owns the socket path, the shim dir and the agent dir | 2960 | // it becomes the new binary. Every sink is bare-closed, NEVER sent |
| 3398 | // never exits; it becomes the new binary. | 2961 | // `exit_status` — that is a dying shell's word and makes clients exit |
| 3399 | // | 2962 | // instead of redial. A failed exec must leave a working daemon. |
| 3400 | // Bare-close every client sink and observer — NEVER exit_status: that | ||
| 3401 | // is a dying shell's word and makes clients exit instead of redial. | ||
| 3402 | // QUIC gets closeAll (loud goodbye) so WAN clients redial now, not | ||
| 3403 | // after the idle timeout. | ||
| 3404 | // | ||
| 3405 | // On exec FAILURE: log, restore FD_CLOEXEC on everything cleared, close | ||
| 3406 | // the memfd, keep serving — a failed exec must leave a working daemon. | ||
| 3407 | fn execUpgrade(self: *Server, path: []const u8, memfd: std.posix.fd_t) void { | 2963 | fn execUpgrade(self: *Server, path: []const u8, memfd: std.posix.fd_t) void { |
| 3408 | const a = self.alloc; | 2964 | const a = self.alloc; |
| 3409 | 2965 | ||
| 3410 | // Every return below is a FAILED exec — execveZ does not return on | 2966 | // Every return below is a FAILED exec, so the arming is spent either |
| 3411 | // success — so the arming is spent either way and is released here | 2967 | // way. A `pending_upgrade` left set would put the run loop into an |
| 3412 | // once, rather than at each failure return. A pending_upgrade left | 2968 | // exec on a loop. |
| 3413 | // set would put the run loop into an exec on a loop, and `path` is | ||
| 3414 | // this Server's copy of a frame payload that is long gone. | ||
| 3415 | defer { | 2969 | defer { |
| 3416 | a.free(path); | 2970 | a.free(path); |
| 3417 | std.posix.close(memfd); | 2971 | std.posix.close(memfd); |
| @@ -3501,12 +3055,9 @@ pub const Server = struct { | |||
| 3501 | 3055 | ||
| 3502 | pub const stats_session_fmt = " session {s} clients={d} seq={d}"; | 3056 | pub const stats_session_fmt = " session {s} clients={d} seq={d}"; |
| 3503 | 3057 | ||
| 3504 | /// Widest text `fmt` can print: every `{d}` a 20-digit u64, every `{s}` | 3058 | /// Widest text `fmt` can print: every `{d}` a 20-digit u64, every `{s}` a |
| 3505 | /// a full-length name. Read off the format string, never counted by | 3059 | /// full-length name. Read off the format string, because a hand count does |
| 3506 | /// hand — a hand count does not move when a counter is added, and | 3060 | /// not move when a counter is added and too small drops the client. |
| 3507 | /// `statsText` writes into a fixed buffer, so too small is | ||
| 3508 | /// `error.WriteFailed`, which `.stats_req` answers by dropping the | ||
| 3509 | /// client. | ||
| 3510 | fn fmtWorstCase(comptime fmt: []const u8) usize { | 3061 | fn fmtWorstCase(comptime fmt: []const u8) usize { |
| 3511 | comptime { | 3062 | comptime { |
| 3512 | var n: usize = 0; | 3063 | var n: usize = 0; |
| @@ -3534,11 +3085,9 @@ pub const Server = struct { | |||
| 3534 | /// join uses, which is the slack that lets sessionsText be infallible. | 3085 | /// join uses, which is the slack that lets sessionsText be infallible. |
| 3535 | pub const sessions_text_len = max_sessions * (proto.session_name_max + 1); | 3086 | pub const sessions_text_len = max_sessions * (proto.session_name_max + 1); |
| 3536 | 3087 | ||
| 3537 | /// What a `sessions_reply` is rendered into, wherever it is rendered: | 3088 | /// What a `sessions_reply` is rendered into, wherever it is rendered. The |
| 3538 | /// the client arm has a send queue and the observer arm does not, so the | 3089 | /// client and observer arms cannot share a function, but they must not each |
| 3539 | /// two answers cannot share a function, but they must not each spell the | 3090 | /// spell the size: a caller that copied an array literal slips the assert. |
| 3540 | /// size. A third caller that copied an array literal would slip past the | ||
| 3541 | /// assert below; one that names this type cannot. | ||
| 3542 | pub const SessionsBuf = [sessions_text_len]u8; | 3091 | pub const SessionsBuf = [sessions_text_len]u8; |
| 3543 | comptime { | 3092 | comptime { |
| 3544 | // The wall reads a reply into a buffer of its own; a daemon that | 3093 | // The wall reads a reply into a buffer of its own; a daemon that |
| @@ -3557,17 +3106,10 @@ pub const Server = struct { | |||
| 3557 | } | 3106 | } |
| 3558 | 3107 | ||
| 3559 | /// Text, but machine-parsed: the bench harness and the e2e tests split on | 3108 | /// Text, but machine-parsed: the bench harness and the e2e tests split on |
| 3560 | /// these key=value pairs. Renaming or reordering fields breaks them. | 3109 | /// these key=value pairs, so renaming or reordering fields breaks them. |
| 3561 | /// | 3110 | /// Byte counters accrue when a frame is ACCEPTED INTO A CLIENT'S QUEUE, |
| 3562 | /// Byte counters are accrued when a frame is ACCEPTED INTO A CLIENT'S | 3111 | /// which `pending_cap` bounds. Daemon-global, with per-session segments in |
| 3563 | /// QUEUE, not when the kernel takes it — "sent" is a small lie, and | 3112 | /// slot order — and still one line, so no caller trips on a newline. |
| 3564 | /// pending_cap bounds it: no client can be more than one cap behind before | ||
| 3565 | /// it is dropped. | ||
| 3566 | /// | ||
| 3567 | /// Daemon-global: a leading `seq=` would be ONE session's tracker, which | ||
| 3568 | /// has no honest answer once there can be more than one, so it moved into | ||
| 3569 | /// a per-session segment in slot order. Still one line, so nothing here | ||
| 3570 | /// introduces a newline for a caller to trip on. | ||
| 3571 | pub fn statsText(self: *const Server, buf: []u8) ![]const u8 { | 3113 | pub fn statsText(self: *const Server, buf: []u8) ![]const u8 { |
| 3572 | var w: std.Io.Writer = .fixed(buf); | 3114 | var w: std.Io.Writer = .fixed(buf); |
| 3573 | try w.print( | 3115 | try w.print( |
| @@ -3604,10 +3146,8 @@ test { | |||
| 3604 | _ = @import("upgrade.zig"); | 3146 | _ = @import("upgrade.zig"); |
| 3605 | 3147 | ||
| 3606 | // Reaching a file is what registers its tests; build.zig gates the list. | 3148 | // Reaching a file is what registers its tests; build.zig gates the list. |
| 3607 | // quic_server.zig is chained with these rather than with the three | 3149 | // quic_server.zig chains here rather than above: its suite binds real UDP |
| 3608 | // child files above: its suite binds real UDP sockets, so a wedge in it | 3150 | // sockets, so a wedge in it costs the same silence a daemon test's does. |
| 3609 | // costs the same silence a daemon test's does. The table ran it after | ||
| 3610 | // the daemon for that reason while it was still a row. | ||
| 3611 | _ = @import("server_test_agent.zig"); | 3151 | _ = @import("server_test_agent.zig"); |
| 3612 | _ = @import("server_test_attach.zig"); | 3152 | _ = @import("server_test_attach.zig"); |
| 3613 | _ = @import("server_test_await.zig"); | 3153 | _ = @import("server_test_await.zig"); |