AppDelegate.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. //
  2. // Copyright © 2021 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. @MainActor class AppDelegate: NSObject, NSApplicationDelegate {
  17. private enum TerminateError: Error {
  18. case wrapped(originalError: any Error, window: NSWindow?)
  19. }
  20. var data: UTMData?
  21. @Setting("KeepRunningAfterLastWindowClosed") private var isKeepRunningAfterLastWindowClosed: Bool = false
  22. @Setting("HideDockIcon") private var isDockIconHidden: Bool = false
  23. @Setting("NoQuitConfirmation") private var isNoQuitConfirmation: Bool = false
  24. private var runningVirtualMachines: [VMData] {
  25. guard let vmList = data?.vmWindows.keys else {
  26. return []
  27. }
  28. return vmList.filter({ $0.wrapped?.state == .started || ($0.wrapped?.state == .paused && !$0.hasSuspendState) })
  29. }
  30. @MainActor
  31. @objc var scriptingVirtualMachines: [UTMScriptingVirtualMachineImpl] {
  32. guard let data = data else {
  33. return []
  34. }
  35. return data.virtualMachines.compactMap { vm in
  36. if vm.wrapped != nil {
  37. return UTMScriptingVirtualMachineImpl(for: vm, data: data)
  38. } else {
  39. return nil
  40. }
  41. }
  42. }
  43. @MainActor
  44. @objc var isAutoTerminate: Bool {
  45. get {
  46. !isKeepRunningAfterLastWindowClosed
  47. }
  48. set {
  49. isKeepRunningAfterLastWindowClosed = !newValue
  50. }
  51. }
  52. func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
  53. !isKeepRunningAfterLastWindowClosed && runningVirtualMachines.isEmpty
  54. }
  55. func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
  56. guard let data = data else {
  57. return .terminateNow
  58. }
  59. guard !isNoQuitConfirmation else {
  60. return .terminateNow
  61. }
  62. let vmList = data.vmWindows.keys
  63. let runningList = runningVirtualMachines
  64. if !runningList.isEmpty { // There is at least 1 running VM
  65. handleTerminateAfterSaving(candidates: runningList, sender: sender)
  66. return .terminateLater
  67. } else if vmList.allSatisfy({ !$0.isLoaded || $0.wrapped?.state == .stopped }) { // All VMs are stopped or suspended
  68. return .terminateNow
  69. } else { // There could be some VMs in other states (starting, pausing, etc.)
  70. return .terminateCancel
  71. }
  72. }
  73. private func handleTerminateAfterSaving(candidates: some Sequence<VMData>, sender: NSApplication) {
  74. Task {
  75. do {
  76. try await withThrowingTaskGroup(of: Void.self) { group in
  77. for vm in candidates {
  78. group.addTask {
  79. let vc = await self.data?.vmWindows[vm] as? VMDisplayWindowController
  80. let window = await vc?.window
  81. guard let vm = await vm.wrapped else {
  82. throw UTMVirtualMachineError.notImplemented
  83. }
  84. do {
  85. try await vm.saveSnapshot(name: nil)
  86. vm.delegate = nil
  87. await vc?.enterSuspended(isBusy: false)
  88. if let window = window {
  89. await window.close()
  90. }
  91. } catch {
  92. throw TerminateError.wrapped(originalError: error, window: window)
  93. }
  94. }
  95. }
  96. try await group.waitForAll()
  97. }
  98. NSApplication.shared.reply(toApplicationShouldTerminate: true)
  99. } catch TerminateError.wrapped(let originalError, let window) {
  100. handleTerminateAfterConfirmation(sender, window: window, error: originalError)
  101. } catch {
  102. handleTerminateAfterConfirmation(sender, error: error)
  103. }
  104. }
  105. }
  106. private func handleTerminateAfterConfirmation(_ sender: NSApplication, window: NSWindow? = nil, error: Error? = nil) {
  107. let alert = NSAlert()
  108. alert.alertStyle = .informational
  109. if error == nil {
  110. alert.messageText = NSLocalizedString("Confirmation", comment: "AppDelegate")
  111. } else {
  112. alert.messageText = NSLocalizedString("Failed to save suspend state", comment: "AppDelegate")
  113. }
  114. alert.informativeText = NSLocalizedString("Quitting UTM will kill all running VMs.", comment: "VMQemuDisplayMetalWindowController")
  115. if let error = error {
  116. alert.informativeText = error.localizedDescription + "\n" + alert.informativeText
  117. }
  118. alert.addButton(withTitle: NSLocalizedString("OK", comment: "VMDisplayWindowController"))
  119. alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "VMDisplayWindowController"))
  120. alert.showsSuppressionButton = true
  121. let confirm = { (response: NSApplication.ModalResponse) in
  122. switch response {
  123. case .alertFirstButtonReturn:
  124. if alert.suppressionButton?.state == .on {
  125. self.isNoQuitConfirmation = true
  126. }
  127. NSApplication.shared.reply(toApplicationShouldTerminate: true)
  128. default:
  129. NSApplication.shared.reply(toApplicationShouldTerminate: false)
  130. }
  131. }
  132. if let window = window {
  133. alert.beginSheetModal(for: window, completionHandler: confirm)
  134. } else {
  135. let response = alert.runModal()
  136. confirm(response)
  137. }
  138. }
  139. func applicationWillTerminate(_ notification: Notification) {
  140. /// Synchronize registry
  141. UTMRegistry.shared.sync()
  142. /// Clean up caches
  143. let fileManager = FileManager.default
  144. guard let cacheUrl = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first else {
  145. return
  146. }
  147. guard let urls = try? fileManager.contentsOfDirectory(at: cacheUrl, includingPropertiesForKeys: nil, options: []) else {
  148. return
  149. }
  150. for url in urls {
  151. var isDirectory: ObjCBool = false
  152. if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) && !isDirectory.boolValue {
  153. try? fileManager.removeItem(at: url)
  154. }
  155. }
  156. }
  157. func applicationDidFinishLaunching(_ notification: Notification) {
  158. if isDockIconHidden {
  159. NSApp.setActivationPolicy(.accessory)
  160. }
  161. }
  162. func application(_ sender: NSApplication, delegateHandlesKey key: String) -> Bool {
  163. switch key {
  164. case "scriptingVirtualMachines": return true
  165. case "scriptingUsbDevices": return true
  166. case "isAutoTerminate": return true
  167. default: return false
  168. }
  169. }
  170. }