╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │19 const preauth_timeout_ticks: u32 = 6; // 30 seconds │ │20 const idle_timeout_ticks: u32 = 24; // 120 seconds │ │21 const close_drain_timeout_ticks: u32 = 2; // 5-10 seconds │ │22 +const escape_timeout: std.Io.Timeout = .{ .duration = .{ .raw = .fromMilliseconds(25), .clock = .awake } };│ │23 │ │24 var active_connections: std.atomic.Value(u32) = .init(0); │ │25 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │89 }; │ │90 defer watchdog_future.cancel(ctx.io); │ │91 │ │91 - var recv_buf: [4096]u8 = undefined; │ │92 + // timed peeks need room for a complete encrypted packet│ │93 + var recv_buf: [ssh.packet_buffer_size]u8 = undefined; │ │94 var send_buf: [4096]u8 = undefined; │ │95 var stream_reader = stream.reader(ctx.io, &recv_buf); │ │96 var stream_writer = stream.writer(ctx.io, &send_buf); │ ╰────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │230 // for each event, rebuild the widget tree and re-render so the user │ │231 // sees the effect of their input on the same iteration. │ │232 event_loop: while (true) { │ │231 - const event = try sess.nextEvent(); │ │233 + const event = try nextTuiEvent(sess, if (terminal_maybe) |*terminal| terminal else null);│ │234 if (terminal_maybe) |*terminal| { │ │233 - switch (event) { │ │235 + if (event) |ev| switch (ev) { │ │236 .data => |payload| { │ │237 defer allocator.free(payload); │ │238 try terminal.writeBytes(payload); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │237 - while (terminal.popKey()) |key| { │ │238 - try ui.inputKey(allocator, &nav.root, key, &ui_session); │ │239 - } │ │239 }, │ │240 .resize => |sz| { │ │241 terminal_size = .{ .width = sz.width_cells, .height = sz.height_cells };│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │242 terminal.pushResize(terminal_size); │ │244 - while (terminal.popKey()) |key| { │ │245 - try ui.inputKey(allocator, &nav.root, key, &ui_session); │ │246 - } │ │243 }, │ │244 .eof, .close => break :event_loop, │ │245 + }; │ │246 + while (terminal.popKey()) |key| { │ │247 + try ui.inputKey(allocator, &nav.root, key, &ui_session); │ │248 } │ │249 } else { │ │251 - switch (event) { │ │250 + switch (event.?) { │ │251 .data => |payload| { │ │252 defer allocator.free(payload); │ │253 if (std.mem.indexOfAny(u8, payload, "\r\n") == null) continue;│ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │295 } │ │296 } │ │297 │ │298 +// only a lone ESC needs a deadline; longer CSI/SS3 fragments must remain │ │299 +// buffered until complete. a timeout leaves partial SSH packets in the reader. │ │300 +pub fn nextTuiEvent(sess: *ssh.SessionCtx, terminal: ?*StreamTerminal) !?ssh.Event {│ │301 + if (terminal) |t| { │ │302 + if (t.parser.esc_len == 1) { │ │303 + sess.conn.read_timeout = escape_timeout.toDeadline(sess.conn.io); │ │304 + defer sess.conn.read_timeout = .none; │ │305 + return sess.nextEvent() catch |err| switch (err) { │ │306 + error.Timeout => { │ │307 + try t.flushEscape(); │ │308 + return null; │ │309 + }, │ │310 + else => return err, │ │311 + }; │ │312 + } │ │313 + } │ │314 + return try sess.nextEvent(); │ │315 +} │ │316 + │ │317 // --------------------------------------------------------------------------- │ │318 // git path (exec) │ │319 // --------------------------------------------------------------------------- │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯