59 lines
2.8 KiB
Swift
59 lines
2.8 KiB
Swift
#if os(Windows)
|
|
import Foundation
|
|
import WinSDK
|
|
|
|
/// Keeps Windows awake while a turn is in flight (docs/WINDOWS_PORT.md §4.3, §11).
|
|
///
|
|
/// The macOS story is `SleepBlocker` + `NucleicPowerHelper`: an IOKit assertion for idle sleep
|
|
/// plus a root helper leasing `pmset disablesleep` for the lid-close case. Neither ports (D4), and
|
|
/// neither needs to — Windows exposes the whole capability as one unprivileged call.
|
|
/// `SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)` tells the power manager "keep the
|
|
/// system running", which survives until it is cleared with a bare `ES_CONTINUOUS`. The display is
|
|
/// deliberately *not* requested: a headless host has nothing to show, and holding the screen on
|
|
/// would be a battery bug, not a feature.
|
|
///
|
|
/// **Default-on, unlike the Mac.** `SleepBlocker` is opt-in because it governs the machine a
|
|
/// person is sitting at, and the lid-close override needs consent. `nucleic-hostd` is a background
|
|
/// service whose entire purpose is running turns to completion, and a PC that suspends mid-turn
|
|
/// kills the agent, the container, and the sync connection at once. So the assertion is held
|
|
/// whenever a turn is in flight unless the user explicitly sets `nucleic.power.blockSleep` to
|
|
/// false — the same key the Mac's Smart Sleep toggle writes, so one lever governs both.
|
|
///
|
|
/// Threading: `SetThreadExecutionState` is per-thread and the state dies with its thread, so every
|
|
/// call must come from one long-lived thread. `Hostd` drives this from `@MainActor` (the process's
|
|
/// main thread, which lives as long as the host does); ``reconcile(turnsInFlight:)`` is
|
|
/// main-actor-isolated so that cannot be got wrong by accident.
|
|
@MainActor
|
|
public enum PowerBlocker {
|
|
/// Same key as the Mac's Smart Sleep toggle (`SleepBlocker.enabledKey`). Absent → enabled.
|
|
public static let enabledKey = "nucleic.power.blockSleep"
|
|
|
|
private static let continuous: DWORD = 0x8000_0000
|
|
private static let systemRequired: DWORD = 0x0000_0001
|
|
|
|
private static var holding = false
|
|
|
|
public static var isEnabled: Bool {
|
|
guard UserDefaults.standard.object(forKey: enabledKey) != nil else { return true }
|
|
return UserDefaults.standard.bool(forKey: enabledKey)
|
|
}
|
|
|
|
/// Hold the assertion while `turnsInFlight > 0`, release it otherwise. Idempotent — repeated
|
|
/// calls at the same level are free, so a caller can drive this from a coarse poll.
|
|
public static func reconcile(turnsInFlight: Int) {
|
|
set(holding: turnsInFlight > 0 && isEnabled)
|
|
}
|
|
|
|
/// Drop any held assertion (host shutdown).
|
|
public static func release() {
|
|
set(holding: false)
|
|
}
|
|
|
|
private static func set(holding wanted: Bool) {
|
|
guard wanted != holding else { return }
|
|
holding = wanted
|
|
_ = SetThreadExecutionState(wanted ? continuous | systemRequired : continuous)
|
|
}
|
|
}
|
|
#endif
|