╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │459 var target_repo = rp.Repo(.xit, repo.self_repo_opts).open(io, allocator, .{ .path = target_path }) catch │ │460 return writeError(sess, "repo not found or has the wrong hash"); │ │461 defer target_repo.deinit(io, allocator); │ │462 - try push.receiveFork(repo.self_repo_opts, io, allocator, repo, &target_repo, &route.id, author, timestamp, reader, writer, handler.err); │ │462 + try push.receiveFork(repo.self_repo_opts, io, allocator, repo, &target_repo, &route.id, author, timestamp, reader, writer, handler.err, sess);│ │463 }, │ │464 } │ │465 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │477 protocol_version: xit.net_server_common.ProtocolVersion, │ │478 author: ?evt.CommitAuthor, │ │479 error_writer: *std.Io.Writer, │ │480 + sess: ?*ssh.SessionCtx, │ │481 ) !bool { │ │482 // a bare open() creates the directory while probing for the repo, so only│ │483 // attempt it when the path already exists │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │490 defer any_repo.deinit(io, allocator); │ │491 │ │492 switch (any_repo) { │ │492 - inline else => |*repo| try servePack(repo.self_repo_opts, repo, repo_root_path, reader, writer, io, allocator, service, protocol_version, author, error_writer), │ │493 + inline else => |*repo| try servePack(repo.self_repo_opts, repo, repo_root_path, reader, writer, io, allocator, service, protocol_version, author, error_writer, sess),│ │494 } │ │495 return true; │ │496 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │520 protocol_version: xit.net_server_common.ProtocolVersion, │ │521 author: ?evt.CommitAuthor, │ │522 error_writer: *std.Io.Writer, │ │523 + sess: ?*ssh.SessionCtx, │ │524 ) !void { │ │525 switch (service) { │ │526 .upload_pack => try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }),│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │525 - .receive_pack => try push.receivePackAndConsume(repo_opts, io, allocator, repo, reader, writer, .{ .protocol_version = protocol_version }, author, repo_root_path, error_writer), │ │527 + .receive_pack => try push.receivePackAndConsume(repo_opts, io, allocator, repo, reader, writer, .{ .protocol_version = protocol_version }, author, repo_root_path, error_writer, sess),│ │528 } │ │529 } │ │530 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │364 return self.cs_cipher.readPacket(self.allocator, self.reader); │ │365 } │ │366 }; │ │367 + │ │368 +// how long a peer probe waits for a packet that may already be on its way │ │369 +const probe_timeout: std.Io.Timeout = .{ .duration = .{ .raw = .fromMilliseconds(1), .clock = .awake } };│ │370 │ │371 /// session bridge handed to the consumer's handleSession callback. exposes a │ │372 /// pumped event API plus a byte-write API; the byte stream goes out as │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │402 │ │403 pub fn deinit(self: *SessionCtx) void { │ │404 self.incoming_buffer.deinit(self.conn.allocator); │ │405 + } │ │406 + │ │407 + /// whether the client is gone. a push spends its transaction writing │ │408 + /// rather than reading, so a peer that hung up has to be pumped for. one │ │409 + /// pending packet is processed, which is where a close is seen. │ │410 + pub fn peerGone(self: *SessionCtx) bool { │ │411 + if (self.closed or self.remote_closed) return true; │ │412 + self.conn.read_timeout = probe_timeout.toDeadline(self.conn.io); │ │413 + defer self.conn.read_timeout = .none; │ │414 + self.processOneBackgroundPacket() catch |err| return err != error.Timeout;│ │415 + return self.remote_closed; │ │416 } │ │417 │ │418 /// the std.Io adapters can only report ReadFailed or WriteFailed. recover │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────────────────────╮ │ 8 const fork = @import("../../fork.zig"); │ │ 9 const pch = @import("../../patch.zig"); │ │10 const push = @import("../../push.zig"); │ │11 +const serve_common = @import("../../serve_common.zig"); │ │12 const ui = @import("../../ui.zig"); │ │13 │ │14 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true };│ ╰──────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │155 fn testBranchMerge(selection: evt.Patch.MergeRevision) !void { │ │156 const io = std.testing.io; │ │157 const allocator = std.testing.allocator; │ │157 - const opts: rp.RepoOpts(.xit) = .{ .is_test = true, .ProgressCtx = *push.PushProgress }; │ │158 + const opts: rp.RepoOpts(.xit) = .{ .is_test = true, .ProgressCtx = *serve_common.PushProgress };│ │159 var temp = std.testing.tmpDir(.{}); │ │160 defer temp.cleanup(); │ │161 const path = try temp.dir.realPathFileAlloc(io, ".", allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │200 response.sideband = true; │ │201 var output = std.Io.Writer.Allocating.init(allocator); │ │202 defer output.deinit(); │ │202 - var progress = push.PushProgress{ .response = &response, .writer = &output.writer }; │ │203 + var progress = serve_common.PushProgress{ .response = &response, .writer = &output.writer };│ │204 const before_refresh = try repo.core.db.rootCursor().count(); │ │205 try pch.refreshBranches(.server, .xit, opts, io, allocator, &repo, null, &progress); │ │206 try std.testing.expectEqual(before_refresh + 1, try repo.core.db.rootCursor().count()); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯