╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │327 cs_cipher: Cipher, │ │328 sc_cipher: Cipher, │ │329 rekey: RekeyState, │ │330 - idle: ?*IdleState = null, │ │330 + idle: *IdleState, │ │331 // set only while the TUI waits to resolve a lone escape key │ │332 read_timeout: std.Io.Timeout = .none, │ │333 // teardown ran to the peer's EOF, so nothing more can be said│ ╰───────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │334 drained: bool = false, │ │335 - │ │336 - fn setWaiting(self: *Conn, waiting: bool) void { │ │337 - if (self.idle) |idle| idle.waiting.store(waiting, .release);│ │338 - } │ │335 │ │336 fn writePacket(self: *Conn, parts: []const []const u8) !void { │ │341 - self.setWaiting(true); │ │342 - defer self.setWaiting(false); │ │337 + self.idle.waiting.store(true, .release); │ │338 + defer self.idle.waiting.store(false, .release); │ │339 try self.sc_cipher.writePacket(self.io, self.writer, parts);│ │340 // a peer that keeps reading is alive │ │345 - recordActivity(self); │ │341 + _ = self.idle.activity.fetchAdd(1, .acq_rel); │ │342 } │ │343 │ │344 fn readPacket(self: *Conn) ![]u8 { │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │349 - self.setWaiting(true); │ │350 - defer self.setWaiting(false); │ │345 + self.idle.waiting.store(true, .release); │ │346 + defer self.idle.waiting.store(false, .release); │ │347 if (self.read_timeout != .none and !try self.cs_cipher.hasBufferedPacket(self.reader)) {│ │348 // peek keeps partial packets in the reader if the timer wins. │ │349 // join both tasks before consuming bytes or changing cipher state. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │450 /// interactive sessions idle legitimately; tell the host's idle │ │451 /// watchdog to stand down for the rest of this connection. │ │452 pub fn exemptFromIdleTimeout(self: *SessionCtx) void { │ │457 - if (self.conn.idle) |idle| idle.exempt.store(true, .release); │ │453 + self.conn.idle.exempt.store(true, .release); │ │454 } │ │455 │ │456 /// pump SSH packets until something interesting (data / resize / close)│ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │480 } │ │481 } │ │482 │ │487 - /// send bytes to the client's stdout (CHANNEL_DATA). │ │483 + /// ship bytes to the client's stdout as one or more CHANNEL_DATA packets, │ │484 + /// respecting the peer's max-packet size. if the remote window is │ │485 + /// exhausted, pump incoming SSH packets until a WINDOW_ADJUST arrives │ │486 + /// (background packets are processed for their side effects — incoming │ │487 + /// CHANNEL_DATA goes into incoming_buffer for a future SessionReader). │ │488 pub fn writeBytes(self: *SessionCtx, bytes: []const u8) !void { │ │489 - try self.writeChannel(bytes, false); │ │490 - } │ │491 - │ │492 - /// send bytes to the client's stderr (CHANNEL_EXTENDED_DATA). use this for │ │493 - /// server messages on a git exec session, where stdout carries the pack │ │494 - /// protocol and stray text breaks the client's pkt-line parser. │ │495 - pub fn writeStderr(self: *SessionCtx, bytes: []const u8) !void { │ │496 - try self.writeChannel(bytes, true); │ │497 - } │ │498 - │ │499 - /// ship bytes as one or more stdout CHANNEL_DATA (or stderr │ │500 - /// CHANNEL_EXTENDED_DATA) packets, respecting the peer's max-packet size. │ │501 - /// if the remote window is exhausted, pump incoming SSH packets until a │ │502 - /// WINDOW_ADJUST arrives (background packets are processed for their side │ │503 - /// effects — incoming CHANNEL_DATA goes into incoming_buffer for a future │ │504 - /// SessionReader). │ │505 - fn writeChannel(self: *SessionCtx, bytes: []const u8, extended: bool) !void { │ │489 const conn = self.conn; │ │490 │ │508 - // byte type, uint32 recipient_channel, [if extended: uint32 type_code], │ │509 - // uint32 data length │ │510 - var header: [13]u8 = undefined; │ │511 - header[0] = if (extended) SSH_MSG_CHANNEL_EXTENDED_DATA else SSH_MSG_CHANNEL_DATA;│ │491 + // byte SSH_MSG_CHANNEL_DATA, uint32 recipient_channel, uint32 data length │ │492 + var header: [9]u8 = undefined; │ │493 + header[0] = SSH_MSG_CHANNEL_DATA; │ │494 std.mem.writeInt(u32, header[1..5], self.channel.remote_id, .big); │ │513 - if (extended) std.mem.writeInt(u32, header[5..9], SSH_EXTENDED_DATA_STDERR, .big);│ │514 - const len_start: usize = if (extended) 9 else 5; │ │495 │ │496 var rest = bytes; │ │497 while (rest.len > 0) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │504 const cap = @min(rest.len, self.channel.max_packet, self.channel.remote_window, max_packet_size);│ │505 const chunk_len: u32 = @intCast(cap); │ │506 │ │527 - std.mem.writeInt(u32, header[len_start..][0..4], chunk_len, .big); │ │528 - try conn.writePacket(&.{ header[0 .. len_start + 4], rest[0..chunk_len] }); │ │507 + std.mem.writeInt(u32, header[5..9], chunk_len, .big); │ │508 + try conn.writePacket(&.{ &header, rest[0..chunk_len] }); │ │509 │ │510 self.channel.remote_window -= chunk_len; │ │511 rest = rest[chunk_len..]; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────╮ │529 else => return err, │ │530 }; │ │531 defer conn.allocator.free(packet);│ │552 - if (packet.len < 1) return; │ │532 │ │533 switch (packet[0]) { │ │534 SSH_MSG_CHANNEL_DATA => { │ ╰───────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │592 if (self.closed) return; │ │593 self.closed = true; │ │594 const conn = self.conn; │ │616 - if (conn.idle) |idle| idle.closing.store(true, .release); │ │595 + conn.idle.closing.store(true, .release); │ │596 │ │597 // a peer CLOSE is acknowledged by the packet pump. don't send data,│ │598 // exit-status, or EOF after that acknowledgement. │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │619 }; │ │620 │ │621 fn drainConnection(conn: *Conn) void { │ │643 - if (conn.idle) |idle| idle.closing.store(true, .release); │ │622 + conn.idle.closing.store(true, .release); │ │623 // read and discard until the peer closes (TCP FIN) before letting the │ │624 // caller close the socket. a git client, after our CHANNEL_CLOSE, still│ │625 // sends its own CHANNEL_CLOSE and SSH_MSG_DISCONNECT before closing; if│ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │654 │ │655 // first ship anything already accumulated in the Writer's buffer │ │656 const buffered = w.buffered(); │ │678 - if (buffered.len > 0) { │ │679 - self.sess.writeBytes(buffered) catch |err| return self.sess.failWrite(err);│ │680 - } │ │657 + self.sess.writeBytes(buffered) catch |err| return self.sess.failWrite(err); │ │658 │ │659 // then ship the bytes passed in directly. data[0..len-1] are written │ │660 // once each; data[len-1] is written `splat` times (this is how the │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯