diff --git a/Platform/macOS/Display/VMDisplayWindowController.swift b/Platform/macOS/Display/VMDisplayWindowController.swift index 0edf9580b..ab380fbf9 100644 --- a/Platform/macOS/Display/VMDisplayWindowController.swift +++ b/Platform/macOS/Display/VMDisplayWindowController.swift @@ -14,6 +14,8 @@ // limitations under the License. // +import IOKit.pwr_mgt + class VMDisplayWindowController: NSWindowController { @IBOutlet weak var displayView: NSView! @@ -41,6 +43,9 @@ class VMDisplayWindowController: NSWindowController { var onClose: ((Notification) -> Void)? private(set) var secondaryWindows: [VMDisplayWindowController] = [] private(set) weak var primaryWindow: VMDisplayWindowController? + private var preventIdleSleepAssertion: IOPMAssertionID? + + @Setting("PreventIdleSleep") private var isPreventIdleSleep: Bool = false var isSecondary: Bool { primaryWindow != nil @@ -145,6 +150,16 @@ class VMDisplayWindowController: NSWindowController { resizeConsoleToolbarItem.isEnabled = true windowsToolbarItem.isEnabled = true window!.makeFirstResponder(displayView.subviews.first) + if isPreventIdleSleep && !isSecondary { + var preventIdleSleepAssertion: IOPMAssertionID = .zero + let success = IOPMAssertionCreateWithName(kIOPMAssertPreventUserIdleSystemSleep as CFString, + IOPMAssertionLevel(kIOPMAssertionLevelOn), + "UTM Virtual Machine Running" as CFString, + &preventIdleSleepAssertion) + if success == kIOReturnSuccess { + self.preventIdleSleepAssertion = preventIdleSleepAssertion + } + } } func enterSuspended(isBusy busy: Bool) { @@ -173,6 +188,9 @@ class VMDisplayWindowController: NSWindowController { usbToolbarItem.isEnabled = false windowsToolbarItem.isEnabled = false window!.makeFirstResponder(nil) + if let preventIdleSleepAssertion = preventIdleSleepAssertion { + IOPMAssertionRelease(preventIdleSleepAssertion) + } } // MARK: - Alert @@ -249,6 +267,9 @@ extension VMDisplayWindowController: NSWindowDelegate { secondaryWindows.forEach { secondaryWindow in secondaryWindow.close() } + if let preventIdleSleepAssertion = preventIdleSleepAssertion { + IOPMAssertionRelease(preventIdleSleepAssertion) + } onClose?(notification) } diff --git a/Platform/macOS/SettingsView.swift b/Platform/macOS/SettingsView.swift index 966b06f5f..6f152edf0 100644 --- a/Platform/macOS/SettingsView.swift +++ b/Platform/macOS/SettingsView.swift @@ -41,6 +41,7 @@ struct ApplicationSettingsView: View { @AppStorage("KeepRunningAfterLastWindowClosed") var isKeepRunningAfterLastWindowClosed = false @AppStorage("HideDockIcon") var isDockIconHidden = false @AppStorage("ShowMenuIcon") var isMenuIconShown = false + @AppStorage("PreventIdleSleep") var isPreventIdleSleep = false var body: some View { Form { @@ -60,6 +61,9 @@ struct ApplicationSettingsView: View { Text("Show menu bar icon") }).disabled(isDockIconHidden) } + Toggle(isOn: $isPreventIdleSleep, label: { + Text("Prevent system from sleeping when any VM is running") + }) } } } @@ -143,6 +147,7 @@ extension UserDefaults { @objc dynamic var KeepRunningAfterLastWindowClosed: Bool { false } @objc dynamic var ShowMenuIcon: Bool { false } @objc dynamic var HideDockIcon: Bool { false } + @objc dynamic var PreventIdleSleep: Bool { false } @objc dynamic var NoCursorCaptureAlert: Bool { false } @objc dynamic var DisplayFixed: Bool { false } @objc dynamic var CtrlRightClick: Bool { false } diff --git a/Platform/macOS/VMHeadlessSessionState.swift b/Platform/macOS/VMHeadlessSessionState.swift index 66fddfe85..3f1f51f2d 100644 --- a/Platform/macOS/VMHeadlessSessionState.swift +++ b/Platform/macOS/VMHeadlessSessionState.swift @@ -15,6 +15,7 @@ // import Foundation +import IOKit.pwr_mgt /// Represents the UI state for a single headless VM session. @MainActor class VMHeadlessSessionState: NSObject, ObservableObject { @@ -26,6 +27,9 @@ import Foundation @Published var fatalError: String? private var hasStarted: Bool = false + private var preventIdleSleepAssertion: IOPMAssertionID? + + @Setting("PreventIdleSleep") private var isPreventIdleSleep: Bool = false init(for vm: UTMVirtualMachine, onStop: ((Notification) -> Void)?) { self.vm = vm @@ -67,10 +71,23 @@ extension VMHeadlessSessionState: UTMVirtualMachineDelegate { extension VMHeadlessSessionState { private func didStart() { NotificationCenter.default.post(name: .vmSessionCreated, object: nil, userInfo: ["Session": self]) + if isPreventIdleSleep { + var preventIdleSleepAssertion: IOPMAssertionID = .zero + let success = IOPMAssertionCreateWithName(kIOPMAssertPreventUserIdleSystemSleep as CFString, + IOPMAssertionLevel(kIOPMAssertionLevelOn), + "UTM Virtual Machine Background" as CFString, + &preventIdleSleepAssertion) + if success == kIOReturnSuccess { + self.preventIdleSleepAssertion = preventIdleSleepAssertion + } + } } private func didStop() { NotificationCenter.default.post(name: .vmSessionEnded, object: nil, userInfo: ["Session": self]) + if let preventIdleSleepAssertion = preventIdleSleepAssertion { + IOPMAssertionRelease(preventIdleSleepAssertion) + } } }