Reconnect the app's command channel after sleep; list clients from it

This commit is contained in:
Ian Gulliver
2026-10-18 09:16:31 -07:00
parent 931d1b9a96
commit 1363a3604d
11 changed files with 444 additions and 388 deletions
+3
View File
@@ -95,6 +95,8 @@ func errorText(_ error: any Error) -> String {
return String(describing: e)
}
private let clientID = UUID().uuidString
final class API: Sendable {
let origin: URL
let session: URLSession
@@ -104,6 +106,7 @@ final class API: Sendable {
self.origin = origin
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Byre-Client": clientID,
"Byre-Commit": Bundle.main.object(forInfoDictionaryKey: "ByreCommit") as! String,
"Byre-Host": Host.current().localizedName ?? ProcessInfo.processInfo.hostName,
]
+39 -1
View File
@@ -7,6 +7,8 @@ typealias ClientRequest = Components.Schemas.ClientRequest
typealias ClientReply = Components.Schemas.ClientReply
private let retryAfter = Duration.seconds(2)
private let checkEvery = Duration.seconds(10)
private let staleAfter: TimeInterval = 25
private let maxOutput = 64 << 10
private let maxMessage = 256 << 20
private let maxRead = 64 << 20
@@ -18,6 +20,8 @@ final class Remote {
private let hands = Hands()
private var ws: URLSessionWebSocketTask?
private var stopped = false
private var heard = Date()
private var checker: Task<Void, Never>?
init(api: API) {
self.api = api
@@ -30,12 +34,22 @@ final class Remote {
let task = api.session.webSocketTask(with: api.url("/api/clients/connect", scheme: "wss"))
task.maximumMessageSize = maxMessage
ws = task
heard = Date()
task.resume()
receive(task)
if checker == nil {
checker = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: checkEvery)
self?.check()
}
}
}
}
func stop() {
stopped = true
checker?.cancel()
ws?.cancel(with: .normalClosure, reason: nil)
ws = nil
Task { [hands] in
@@ -43,6 +57,30 @@ final class Remote {
}
}
// A socket byre closed while this Mac slept stays open here until something is sent on it.
private func check() {
guard let task = ws else {
return
}
if Date().timeIntervalSince(heard) >= staleAfter {
log.notice("client connection got no pong for \(Int(staleAfter))s, reconnecting")
ws = nil
task.cancel(with: .goingAway, reason: nil)
start()
return
}
task.sendPing { error in
guard error == nil else {
return
}
Task { @MainActor in
if self.ws === task {
self.heard = Date()
}
}
}
}
private func receive(_ task: URLSessionWebSocketTask) {
task.receive { result in
Task { @MainActor in
@@ -59,7 +97,7 @@ final class Remote {
case .success:
self.receive(task)
case .failure(let error):
log.info("client connection closed: \(errorText(error), privacy: .public)")
log.notice("client connection closed:\(errorText(error), privacy: .public)")
self.ws = nil
try? await Task.sleep(for: retryAfter)
self.start()