Files
nucleic/Sources/NucleicCore/Windows/PowerBlocker.swift
T

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