Files
punktfunk/clients/apple/Sources/PunktfunkClient/Session/HostWaker.swift
T
enricobuehler dd02e1f402
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m8s
decky / build-publish (push) Successful in 20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
android / android (push) Successful in 12m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m3s
arch / build-publish (push) Successful in 12m57s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9m40s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m4s
docker / deploy-docs (push) Successful in 11s
flatpak / build-publish (push) Successful in 5m57s
deb / build-publish (push) Successful in 13m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m50s
apple / swift (push) Successful in 4m31s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m8s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m44s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m7s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m9s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m45s
release / apple (push) Successful in 21m27s
ci / rust (push) Failing after 6m47s
apple / screenshots (push) Successful in 20m18s
feat(clients): unified full-screen connect/wake takeover + iOS/tvOS Wake-on-LAN
Give instant feedback the moment a host is picked, and make the wake wait a
full-screen takeover instead of a modal card — unified into one ConnectOverlay
across every client:

- android: new ConnectOverlay (aurora backdrop; Connecting / Waking / timed-out
  phases) replaces the tiny inline "Connecting…" row and the WakeOverlay card.
  The dial phase is now cancelable and hands off to the wake wait in one frame.
- console (pf-console-ui): the connect/wake overlays become a full-screen aurora
  takeover (draw_takeover) instead of a centered card over a dim scrim; the
  Waking → Connecting handoff no longer blinks.
- apple: new ConnectOverlay mirrors it (macOS / iOS / tvOS), replacing the
  per-tile connecting spinner + the WakeOverlay card; instant "Connecting…" from
  model.phase, and the carousel is gated inactive during the dial.

Also enable Wake-on-LAN on iOS/tvOS now that the multicast entitlement is
approved: enable com.apple.developer.networking.multicast and flip
wakeOnLANAvailable to true on every platform (MACs were already learned from
mDNS, so wake works immediately).

Verified: Android compileDebugKotlin + screenshot renders; console clippy +
36 tests + rendered phases on Linux; Apple swift build + 121 tests + rendered
phases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 23:29:35 +02:00

113 lines
4.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Wake a sleeping host and WAIT for it to come back before proceeding.
//
// A magic packet is fire-and-forget, and a cold box can take 2060 s to POST, boot, and start
// advertising on mDNS again — far longer than a connect attempt will sit. The old path fired a
// packet and immediately dialed, so a genuinely-asleep host just failed. This drives a visible
// "Waking…" state instead: it (re-)sends the packet, polls the host's mDNS presence once a second,
// and on success runs `onOnline` (the real connect for a Wake-&-Connect, or nothing for an explicit
// wake-only); on timeout it parks in a retry/cancel state. One wake at a time.
import Foundation
import PunktfunkKit
import SwiftUI
@MainActor
final class HostWaker: ObservableObject {
struct Waking: Equatable {
let hostID: UUID
let hostName: String
/// Whether coming online chains into a connect (Wake & Connect) vs. just stopping.
let connectsAfter: Bool
var seconds = 0
var timedOut = false
}
/// nil = idle; non-nil drives the "Waking…" phase of `ConnectOverlay`.
@Published private(set) var waking: Waking?
/// How long to wait for the host to reappear before giving up. Generous — a cold boot + service
/// start can be a minute-plus.
private let timeoutSeconds = 90
/// Re-send the packet this often: a single one can be missed, and some NICs only wake on a fresh
/// packet after dropping into a deeper sleep state.
private let resendEverySeconds = 6
private var loop: Task<Void, Never>?
/// Captured so "Try Again" replays the exact same wait.
private var replay: (() -> Void)?
/// Wake `host` and wait for `isOnline()` to go true, then run `onOnline`. `macs`/`lastIP` target
/// the magic packet. No-ops straight to `onOnline` when there's nothing to wake with or the host
/// is already up (a race between the caller's check and here).
func start(
host: StoredHost, connectsAfter: Bool,
macs: [String], lastIP: String?,
isOnline: @escaping () -> Bool, onOnline: @escaping () -> Void
) {
guard !macs.isEmpty, !isOnline() else {
cancel()
onOnline()
return
}
replay = { [weak self] in
self?.run(host: host, connectsAfter: connectsAfter, macs: macs, lastIP: lastIP,
isOnline: isOnline, onOnline: onOnline)
}
replay?()
}
/// Stop waiting and dismiss the overlay (B / Cancel).
func cancel() {
loop?.cancel()
loop = nil
replay = nil
waking = nil
}
/// Restart the wait after a timeout (A / Try Again).
func retry() { replay?() }
private func run(
host: StoredHost, connectsAfter: Bool, macs: [String], lastIP: String?,
isOnline: @escaping () -> Bool, onOnline: @escaping () -> Void
) {
loop?.cancel()
waking = Waking(hostID: host.id, hostName: host.displayName, connectsAfter: connectsAfter)
let timeout = timeoutSeconds
let resend = resendEverySeconds
loop = Task { [weak self] in
var elapsed = 0
while !Task.isCancelled {
if elapsed % resend == 0 { Self.sendPacket(macs: macs, lastIP: lastIP) }
if isOnline() {
guard let self, !Task.isCancelled else { return }
self.waking = nil
self.loop = nil
onOnline()
return
}
if elapsed >= timeout {
self?.waking?.timedOut = true
self?.loop = nil
return
}
try? await Task.sleep(nanoseconds: 1_000_000_000)
elapsed += 1
self?.waking?.seconds = elapsed
}
}
}
/// Blocking sends (see PunktfunkConnection.wakeOnLAN) — off the main thread.
private static func sendPacket(macs: [String], lastIP: String?) {
DispatchQueue.global(qos: .userInitiated).async {
PunktfunkConnection.wakeOnLAN(macs: macs, lastKnownIP: lastIP)
}
}
#if DEBUG
/// Force a static waking state for the screenshot harness (no timers, no packets).
func debugSet(_ w: Waking) { waking = w }
#endif
}