╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 + │ │26 +const any_repo_opts: rp.AnyRepoOpts(.xit) = .{ .ProgressCtx = *push.PushProgress }; │ │27 │ │28 pub const SessionHandler = struct { │ │29 admin_repo_path: []const u8, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │34 git_ssh_prefix: []const u8, │ │35 err: *std.Io.Writer, │ │36 │ │37 + /// returning without an exit leaves the protocol layer to send status 0 │ │38 pub fn handleSession(self: *const SessionHandler, sess: *ssh.SessionCtx, request: ssh.Request) !void { │ │39 serve_common.logError(sess.conn.io, self.err, "ssh session: kind={s} key={s}\n", .{ @tagName(request), sess.fingerprint });│ │40 switch (request) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │204 const msg = std.fmt.bufPrint(&buf, "haxy ssh: {s}\r\n", .{@errorName(err)}) catch "haxy ssh: internal error\r\n";│ │205 sess.writeBytes(msg) catch {}; │ │206 try sess.exit(1); │ │204 - return; │ │207 }; │ │206 - try sess.exit(0); │ │208 } │ │209 │ │210 fn runTui(handler: *const SessionHandler, sess: *ssh.SessionCtx, pty: ssh.PtySize) !void { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │214 // some clients (notably scripted ssh with no local controlling tty) │ │215 // allocate a pty without ever sending a non-zero size. fall back to a │ │216 // conventional 80x24 in that case so render emits something. │ │216 - const effective_pty = ssh.PtySize{ │ │217 - .width_cells = if (pty.width_cells == 0) 80 else pty.width_cells, │ │218 - .height_cells = if (pty.height_cells == 0) 24 else pty.height_cells, │ │217 + var terminal_size = Size{ │ │218 + .width = if (pty.width_cells == 0) 80 else pty.width_cells, │ │219 + .height = if (pty.height_cells == 0) 24 else pty.height_cells, │ │220 }; │ │221 │ │222 // session-lifetime allocations (login, prefs); Nav owns the per-page arenas.│ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │246 // flushes, restoring the client's screen. as a function-scoped defer it runs │ │247 // before runTui returns — on the normal path and on any error — so the │ │248 // caller can write to the client afterward (and sess.exit can close cleanly). │ │248 - var terminal_size = Size{ │ │249 - .width = effective_pty.width_cells, │ │250 - .height = effective_pty.height_cells, │ │251 - }; │ │249 var terminal_maybe: ?StreamTerminal = try StreamTerminal.init(allocator, &session_writer.interface, terminal_size);│ │250 defer if (terminal_maybe) |*terminal| terminal.deinit(); │ │251 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │257 // sees the effect of their input on the same iteration. │ │258 event_loop: while (true) { │ │259 const event = try nextTuiEvent(sess, if (terminal_maybe) |*terminal| terminal else null); │ │260 + if (event) |ev| switch (ev) { │ │261 + .data => |payload| { │ │262 + defer allocator.free(payload); │ │263 + if (terminal_maybe) |*terminal| { │ │264 + try terminal.writeBytes(payload); │ │265 + } else { │ │266 + // the copyable text stays up until enter │ │267 + if (std.mem.indexOfAny(u8, payload, "\r\n") == null) continue; │ │268 + terminal_maybe = try StreamTerminal.init(allocator, &session_writer.interface, terminal_size);│ │269 + } │ │270 + }, │ │271 + .resize => |sz| { │ │272 + terminal_size = .{ .width = sz.width_cells, .height = sz.height_cells }; │ │273 + if (terminal_maybe) |*terminal| terminal.pushResize(terminal_size) else continue; │ │274 + }, │ │275 + .eof, .close => break :event_loop, │ │276 + }; │ │277 if (terminal_maybe) |*terminal| { │ │264 - if (event) |ev| switch (ev) { │ │265 - .data => |payload| { │ │266 - defer allocator.free(payload); │ │267 - try terminal.writeBytes(payload); │ │268 - }, │ │269 - .resize => |sz| { │ │270 - terminal_size = .{ .width = sz.width_cells, .height = sz.height_cells }; │ │271 - terminal.pushResize(terminal_size); │ │272 - }, │ │273 - .eof, .close => break :event_loop, │ │274 - }; │ │278 while (terminal.popKey()) |key| { │ │279 try ui.inputKey(allocator, &nav.root, key, &ui_session); │ │280 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │278 - } else { │ │279 - switch (event orelse unreachable) { │ │280 - .data => |payload| { │ │281 - defer allocator.free(payload); │ │282 - if (std.mem.indexOfAny(u8, payload, "\r\n") == null) continue; │ │283 - terminal_maybe = try StreamTerminal.init(allocator, &session_writer.interface, terminal_size);│ │284 - }, │ │285 - .resize => |sz| { │ │286 - terminal_size = .{ .width = sz.width_cells, .height = sz.height_cells }; │ │287 - continue; │ │288 - }, │ │289 - .eof, .close => break :event_loop, │ │290 - } │ │281 } │ │282 │ │283 if (ui_session.host_request) |request| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │337 // git path (exec) │ │338 // ---------------------------------------------------------------------------│ │339 │ │350 -pub const GitService = enum { upload_pack, receive_pack }; │ │340 +const GitService = enum { upload_pack, receive_pack }; │ │341 │ │352 -pub const ParsedGitCommand = struct { │ │342 +const ParsedGitCommand = struct { │ │343 service: GitService, │ │344 dir: []u8, │ │345 │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │356 - pub fn deinit(self: ParsedGitCommand, allocator: std.mem.Allocator) void {│ │346 + fn deinit(self: ParsedGitCommand, allocator: std.mem.Allocator) void { │ │347 allocator.free(self.dir); │ │348 } │ │349 }; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │353 const io = sess.conn.io; │ │354 const protocol_version = xit.net_server_common.parseProtocolVersion(exec.git_protocol); │ │355 │ │356 + // the repo's pack functions consume the channel like any other stream. │ │357 + // whatever they leave buffered is flushed before the exit status goes out. │ │358 + var reader_buf: [4096]u8 = undefined; │ │359 + var writer_buf: [4096]u8 = undefined; │ │360 + var reader = ssh.SessionReader.init(sess, &reader_buf); │ │361 + var writer = ssh.SessionWriter.init(sess, &writer_buf); │ │362 + defer writer.interface.flush() catch {}; │ │363 + │ │364 const parsed = parseGitCommand(allocator, exec.command) catch return writeError(sess, "unsupported command (expected git-upload-pack or git-receive-pack)");│ │365 defer parsed.deinit(allocator); │ │366 const path = std.mem.trimStart(u8, parsed.dir, "/"); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯