Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ struct CloudStartChat {
trusted: Bool = false, tabKey: String) async throws -> String {
await CloudOutdatedMachine.updateBeforeUsingCapability(
holds: { services.agentTabs.holdsTabs(machine.daemon) },
link: { CloudMachineUpdates.handshake(machine, machines: services.machines) },
update: { await CloudMachineUpdates.updateNow(machine.machineID, services: services) })
let context = AppActionContext(services: services)
let anchor = try await CloudHandlers.terminalAnchor(on: machine, context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,20 @@ public struct CloudOutdatedMachine {
}

/// Updates a machine before using a capability that its current daemon does not advertise.
/// The update restarts the daemon, so the capability must be read from the new handshake rather
/// than from the old connection. If the update cannot run or the new link does not arrive before
/// `relink`, this returns and the caller's existing capability refusal remains the final result.
/// The update restarts the daemon, so this waits, up to `relink`, for `holds` itself (read from the
/// new handshake), not just for the link to change: a new link may come up before its handshake
/// carries the capability, and opening then was refused although the update had worked (capture at
/// a799d97a, 2026-11-02). If the update cannot run or the capability does not arrive in time, this
/// returns and the caller's existing capability refusal remains the final result.
@MainActor
public static func updateBeforeUsingCapability(
holds: @escaping @MainActor @Sendable () -> Bool,
link: @escaping @MainActor @Sendable () -> String?,
relink: Duration = .seconds(90),
update: @escaping @MainActor @Sendable () async -> Bool
) async {
guard !holds() else { return }
let before = link()
guard await update() else { return }
_ = await CloudWait.holds(within: relink) {
link().map { $0 != before } ?? false
}
_ = await CloudWait.holds(within: relink, until: holds)
}

/// Runs `copy` on the machine's link socket (`socket`, nil when not connected: `notConnected` is thrown); when
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import CmuxNextCloud
import Observation
import Testing

/// A machine whose new link comes up before its handshake carries the capability.
@Observable @MainActor
private final class Relinking {
var holdsTabs = false
}

@Suite
@MainActor
struct CloudOutdatedMachineTests {
Expand All @@ -11,7 +18,6 @@ struct CloudOutdatedMachineTests {

await CloudOutdatedMachine.updateBeforeUsingCapability(
holds: { holdsTabs },
link: { handshake },
relink: .seconds(1),
update: {
updates += 1
Expand All @@ -32,7 +38,6 @@ struct CloudOutdatedMachineTests {

await CloudOutdatedMachine.updateBeforeUsingCapability(
holds: { holdsTabs },
link: { "old" },
relink: .milliseconds(10),
update: {
updates += 1
Expand All @@ -48,7 +53,6 @@ struct CloudOutdatedMachineTests {

await CloudOutdatedMachine.updateBeforeUsingCapability(
holds: { true },
link: { "current" },
relink: .milliseconds(10),
update: {
updates += 1
Expand All @@ -57,4 +61,19 @@ struct CloudOutdatedMachineTests {

#expect(updates == 0)
}

@Test func aNewLinkWithoutTheCapabilityYetIsWaitedFor() async {
let machine = Relinking()

await CloudOutdatedMachine.updateBeforeUsingCapability(
holds: { machine.holdsTabs },
relink: .seconds(5),
update: {
// The link changed already; the capability arrives with the handshake a turn later.
Task { @MainActor in machine.holdsTabs = true }
return true
})

#expect(machine.holdsTabs)
}
}
Loading