╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │174 fn h(ctx: Context, stream: std.Io.net.Stream) void { │ │175 defer stream.close(ctx.io); │ │176 web.handleConnection(ctx.io, ctx.allocator, stream, ctx.host, ctx.err) catch |request_err| { │ │177 - serve_common.logError(ctx.err, "web ui request failed: {s}\n", .{@errorName(request_err)}); │ │177 + serve_common.logError(ctx.io, ctx.err, "web ui request failed: {s}\n", .{@errorName(request_err)});│ │178 }; │ │179 } │ │180 }.h; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/serve_common.zig│ ╰────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │24 return .{ .ok = try std.fs.path.join(allocator, &.{ repo_root_path, &event_id_hex }) }; │ │25 } │ │26 │ │27 -pub fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { │ │27 +// every connection task shares one error writer, so writing to it takes a │ │28 +// lock. uncancelable, since a log line shouldn't be a cancelation point. │ │29 +var log_mutex: std.Io.Mutex = .init; │ │30 + │ │31 +pub fn logError(io: std.Io, err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void {│ │32 + log_mutex.lockUncancelable(io); │ │33 + defer log_mutex.unlock(io); │ │34 err.print(fmt, args) catch return; │ │35 err.flush() catch {}; │ │36 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │70 while (true) { │ │71 const stream = self.net_server.accept(self.io) catch |accept_err| { │ │72 if (accept_err == error.Canceled) return; │ │67 - logError(self.err, "{s} accept failed: {s}\n", .{ self.name, @errorName(accept_err) }); │ │73 + logError(self.io, self.err, "{s} accept failed: {s}\n", .{ self.name, @errorName(accept_err) });│ │74 continue; │ │75 }; │ │76 self.tasks.async(self.io, Conn.run, .{Conn{ .context = self.context, .stream = stream }}); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/serve_http.zig│ ╰──────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │26 fn h(ctx: Context, stream: std.Io.net.Stream) void { │ │27 defer stream.close(ctx.io); │ │28 handleConnection(repo_kind, any_repo_opts, ctx.io, ctx.allocator, ctx.repo_root_path, ctx.admin_repo_path, stream, ctx.err) catch |request_err| {│ │29 - serve_common.logError(ctx.err, "connection failed: {s}\n", .{@errorName(request_err)}); │ │29 + serve_common.logError(ctx.io, ctx.err, "connection failed: {s}\n", .{@errorName(request_err)}); │ │30 }; │ │31 } │ │32 }.h; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │64 }; │ │65 │ │66 handleGitRequest(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, &http_server, &request) catch |request_err| {│ │67 - try err.print("request failed: {s}\n", .{@errorName(request_err)}); │ │68 - try err.flush(); │ │67 + serve_common.logError(io, err, "request failed: {s}\n", .{@errorName(request_err)}); │ │68 if (http_server.reader.state == .received_head) { │ │69 http_server.reader.state = .ready; │ │70 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │29 err: *std.Io.Writer, │ │30 │ │31 pub fn handleSession(self: *const SessionHandler, sess: *ssh.SessionCtx, request: ssh.Request) !void { │ │32 - serve_common.logError(self.err, "ssh session: kind={s} key={s}\n", .{ @tagName(request), sess.fingerprint }); │ │32 + serve_common.logError(sess.conn.io, self.err, "ssh session: kind={s} key={s}\n", .{ @tagName(request), sess.fingerprint });│ │33 switch (request) { │ │34 .shell => |pty_maybe| { │ │35 const pty = pty_maybe orelse { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │74 const prev_count = active_connections.fetchAdd(1, .monotonic); │ │75 defer _ = active_connections.fetchSub(1, .monotonic); │ │76 if (prev_count >= max_connections) { │ │77 - serve_common.logError(ctx.err, "ssh: connection limit reached, dropping\n", .{}); │ │77 + serve_common.logError(ctx.io, ctx.err, "ssh: connection limit reached, dropping\n", .{});│ │78 return; │ │79 } │ │80 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │81 var idle_state = ssh.IdleState{}; │ │82 var watchdog_future = std.Io.concurrent(ctx.io, watchdog, .{ ctx.io, &stream, &idle_state }) catch |spawn_err| {│ │83 - serve_common.logError(ctx.err, "ssh: watchdog spawn failed: {s}\n", .{@errorName(spawn_err)}); │ │83 + serve_common.logError(ctx.io, ctx.err, "ssh: watchdog spawn failed: {s}\n", .{@errorName(spawn_err)}); │ │84 return; │ │85 }; │ │86 defer watchdog_future.cancel(ctx.io); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 98 &idle_state, │ │ 99 ctx.session_handler, │ │100 ) catch |session_err| { │ │101 - serve_common.logError(ctx.err, "ssh session failed: {s}\n", .{@errorName(session_err)}); │ │101 + serve_common.logError(ctx.io, ctx.err, "ssh session failed: {s}\n", .{@errorName(session_err)});│ │102 }; │ │103 } │ │104 }.h; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │162 // runs as the function unwinds — so any error it returns leaves the TUI torn │ │163 // down and we can surface the failure on the restored screen before exiting. │ │164 runTui(handler, sess, pty) catch |err| { │ │165 - serve_common.logError(handler.err, "ssh tui session failed: {s}\n", .{@errorName(err)}); │ │165 + serve_common.logError(sess.conn.io, handler.err, "ssh tui session failed: {s}\n", .{@errorName(err)}); │ │166 var buf: [256]u8 = undefined; │ │167 const msg = std.fmt.bufPrint(&buf, "haxy ssh: {s}\r\n", .{@errorName(err)}) catch "haxy ssh: internal error\r\n";│ │168 sess.writeBytes(msg) catch {}; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯