VMSessionState.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. //
  2. // Copyright © 2022 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. import Foundation
  17. import AVFAudio
  18. import SwiftUI
  19. /// Represents the UI state for a single VM session.
  20. @MainActor class VMSessionState: NSObject, ObservableObject {
  21. struct ID: Hashable, Codable {
  22. private var id = UUID()
  23. }
  24. struct WindowID: Hashable, Codable {
  25. private var id = UUID()
  26. }
  27. struct GlobalWindowID: Hashable, Codable {
  28. private(set) var sessionID: VMSessionState.ID
  29. private(set) var windowID: VMSessionState.WindowID
  30. }
  31. static private(set) var allActiveSessions: [ID: VMSessionState] = [:]
  32. let id: ID = ID()
  33. let vm: UTMQemuVirtualMachine
  34. var qemuConfig: UTMQemuConfiguration {
  35. vm.config
  36. }
  37. @Published var vmState: UTMVirtualMachineState = .stopped
  38. @Published var fatalError: String?
  39. @Published var nonfatalError: String?
  40. @Published var primaryInput: CSInput?
  41. #if WITH_USB
  42. private var primaryUsbManager: CSUSBManager?
  43. private var usbManagerQueue = DispatchQueue(label: "USB Manager Queue", qos: .utility)
  44. @Published var mostRecentConnectedDevice: CSUSBDevice?
  45. @Published var allUsbDevices: [CSUSBDevice] = []
  46. @Published var connectedUsbDevices: [CSUSBDevice] = []
  47. #endif
  48. @Published var isUsbBusy: Bool = false
  49. @Published var devices: [VMWindowState.Device] = []
  50. @Published var windows: [GlobalWindowID] = []
  51. @Published var primaryWindow: WindowID?
  52. @Published var activeWindow: WindowID?
  53. @Published var windowDeviceMap: [WindowID: VMWindowState.Device] = [:]
  54. @Published var externalWindowBinding: Binding<VMWindowState>?
  55. @Published var hasShownMemoryWarning: Bool = false
  56. private var hasAutosave: Bool = false
  57. init(for vm: UTMQemuVirtualMachine) {
  58. self.vm = vm
  59. super.init()
  60. vm.delegate = self
  61. vm.ioServiceDelegate = self
  62. }
  63. func newWindow() -> GlobalWindowID {
  64. GlobalWindowID(sessionID: id, windowID: WindowID())
  65. }
  66. func registerWindow(_ window: WindowID, isExternal: Bool = false) {
  67. let globalWindow = GlobalWindowID(sessionID: id, windowID: window)
  68. windows.append(globalWindow)
  69. if !isExternal, primaryWindow == nil {
  70. primaryWindow = window
  71. }
  72. if !isExternal, activeWindow == nil {
  73. activeWindow = window
  74. }
  75. assignDefaultDisplay(for: window, isExternal: isExternal)
  76. }
  77. func removeWindow(_ window: WindowID) {
  78. let globalWindow = GlobalWindowID(sessionID: id, windowID: window)
  79. windows.removeAll { $0 == globalWindow }
  80. if primaryWindow == window {
  81. primaryWindow = windows.first?.windowID
  82. }
  83. if activeWindow == window {
  84. activeWindow = windows.first?.windowID
  85. }
  86. windowDeviceMap.removeValue(forKey: window)
  87. }
  88. private func assignDefaultDisplay(for window: WindowID, isExternal: Bool) {
  89. // default first to next GUI, then to next serial
  90. let filtered = devices.filter {
  91. if case .display(_, _) = $0 {
  92. return true
  93. } else {
  94. return false
  95. }
  96. }
  97. for device in filtered {
  98. if !windowDeviceMap.values.contains(device) {
  99. windowDeviceMap[window] = device
  100. return
  101. }
  102. }
  103. if isExternal {
  104. return // no serial device for external display
  105. }
  106. for device in devices {
  107. if !windowDeviceMap.values.contains(device) {
  108. windowDeviceMap[window] = device
  109. return
  110. }
  111. }
  112. }
  113. }
  114. extension VMSessionState: UTMVirtualMachineDelegate {
  115. nonisolated func virtualMachine(_ vm: any UTMVirtualMachine, didTransitionToState state: UTMVirtualMachineState) {
  116. Task { @MainActor in
  117. vmState = state
  118. if state == .stopped {
  119. #if WITH_USB
  120. clearDevices()
  121. #endif
  122. }
  123. }
  124. }
  125. nonisolated func virtualMachine(_ vm: any UTMVirtualMachine, didErrorWithMessage message: String) {
  126. Task { @MainActor in
  127. fatalError = message
  128. }
  129. }
  130. nonisolated func virtualMachine(_ vm: any UTMVirtualMachine, didCompleteInstallation success: Bool) {
  131. }
  132. nonisolated func virtualMachine(_ vm: any UTMVirtualMachine, didUpdateInstallationProgress progress: Double) {
  133. }
  134. }
  135. extension VMSessionState: UTMSpiceIODelegate {
  136. nonisolated func spiceDidCreateInput(_ input: CSInput) {
  137. Task { @MainActor in
  138. guard primaryInput == nil else {
  139. return
  140. }
  141. primaryInput = input
  142. }
  143. }
  144. nonisolated func spiceDidDestroyInput(_ input: CSInput) {
  145. Task { @MainActor in
  146. guard primaryInput == input else {
  147. return
  148. }
  149. primaryInput = nil
  150. }
  151. }
  152. nonisolated func spiceDidCreateDisplay(_ display: CSDisplay) {
  153. Task { @MainActor in
  154. assert(display.monitorID < qemuConfig.displays.count)
  155. let device = VMWindowState.Device.display(display, display.monitorID)
  156. devices.append(device)
  157. // associate with the next available window
  158. for window in windows {
  159. let windowId = window.windowID
  160. if windowDeviceMap[windowId] == nil {
  161. if windowId == primaryWindow && !display.isPrimaryDisplay {
  162. // prefer the primary display for the primary window
  163. continue
  164. }
  165. if windowId != primaryWindow && display.isPrimaryDisplay {
  166. // don't assign primary display to non-primary window either
  167. continue
  168. }
  169. windowDeviceMap[windowId] = device
  170. }
  171. }
  172. }
  173. }
  174. nonisolated func spiceDidDestroyDisplay(_ display: CSDisplay) {
  175. Task { @MainActor in
  176. let device = VMWindowState.Device.display(display, display.monitorID)
  177. devices.removeAll { $0 == device }
  178. for window in windows {
  179. let windowId = window.windowID
  180. if windowDeviceMap[windowId] == device {
  181. windowDeviceMap[windowId] = nil
  182. }
  183. }
  184. }
  185. }
  186. nonisolated func spiceDidUpdateDisplay(_ display: CSDisplay) {
  187. // nothing to do
  188. }
  189. nonisolated private func configIdForSerial(_ serial: CSPort) -> Int? {
  190. let prefix = "com.utmapp.terminal."
  191. guard serial.name?.hasPrefix(prefix) ?? false else {
  192. return nil
  193. }
  194. return Int(serial.name!.dropFirst(prefix.count))
  195. }
  196. nonisolated func spiceDidCreateSerial(_ serial: CSPort) {
  197. Task { @MainActor in
  198. guard let id = configIdForSerial(serial) else {
  199. logger.error("cannot setup window for serial '\(serial.name ?? "(null)")'")
  200. return
  201. }
  202. let device = VMWindowState.Device.serial(serial, id)
  203. assert(id < qemuConfig.serials.count)
  204. assert(qemuConfig.serials[id].mode == .builtin && qemuConfig.serials[id].terminal != nil)
  205. devices.append(device)
  206. // associate with the next available window
  207. for window in windows {
  208. let windowId = window.windowID
  209. if windowDeviceMap[windowId] == nil {
  210. if windowId == primaryWindow && !qemuConfig.displays.isEmpty {
  211. // prefer a GUI display over console for primary if both are available
  212. continue
  213. }
  214. if windowId == externalWindowBinding?.wrappedValue.id {
  215. // do not set serial with external display
  216. continue
  217. }
  218. windowDeviceMap[windowId] = device
  219. }
  220. }
  221. }
  222. }
  223. nonisolated func spiceDidDestroySerial(_ serial: CSPort) {
  224. Task { @MainActor in
  225. guard let id = configIdForSerial(serial) else {
  226. return
  227. }
  228. let device = VMWindowState.Device.serial(serial, id)
  229. devices.removeAll { $0 == device }
  230. for window in windows {
  231. let windowId = window.windowID
  232. if windowDeviceMap[windowId] == device {
  233. windowDeviceMap[windowId] = nil
  234. }
  235. }
  236. }
  237. }
  238. #if WITH_USB
  239. nonisolated func spiceDidChangeUsbManager(_ usbManager: CSUSBManager?) {
  240. Task { @MainActor in
  241. primaryUsbManager?.delegate = nil
  242. primaryUsbManager = usbManager
  243. usbManager?.delegate = self
  244. refreshDevices()
  245. }
  246. }
  247. #endif
  248. }
  249. #if WITH_USB
  250. extension VMSessionState: CSUSBManagerDelegate {
  251. nonisolated func spiceUsbManager(_ usbManager: CSUSBManager, deviceError error: String, for device: CSUSBDevice) {
  252. Task { @MainActor in
  253. nonfatalError = error
  254. refreshDevices()
  255. }
  256. }
  257. nonisolated func spiceUsbManager(_ usbManager: CSUSBManager, deviceAttached device: CSUSBDevice) {
  258. Task { @MainActor in
  259. if vmState == .started {
  260. mostRecentConnectedDevice = device
  261. }
  262. allUsbDevices.append(device)
  263. }
  264. }
  265. nonisolated func spiceUsbManager(_ usbManager: CSUSBManager, deviceRemoved device: CSUSBDevice) {
  266. Task { @MainActor in
  267. connectedUsbDevices.removeAll(where: { $0 == device })
  268. allUsbDevices.removeAll(where: { $0 == device })
  269. }
  270. }
  271. private func withUsbManagerSerialized<T>(_ task: @escaping () async throws -> T, onSuccess: @escaping @MainActor (T) -> Void = { _ in }, onError: @escaping @MainActor (Error) -> Void = { _ in }) {
  272. usbManagerQueue.async {
  273. let event = DispatchSemaphore(value: 0)
  274. Task.detached { [self] in
  275. await MainActor.run {
  276. isUsbBusy = true
  277. }
  278. do {
  279. let result = try await task()
  280. await MainActor.run {
  281. isUsbBusy = false
  282. onSuccess(result)
  283. }
  284. } catch {
  285. await MainActor.run {
  286. isUsbBusy = false
  287. onError(error)
  288. }
  289. }
  290. event.signal()
  291. }
  292. event.wait()
  293. }
  294. }
  295. func refreshDevices() {
  296. guard let usbManager = self.primaryUsbManager else {
  297. logger.error("no usb manager connected")
  298. return
  299. }
  300. withUsbManagerSerialized {
  301. let devices = usbManager.usbDevices
  302. for device in devices {
  303. let name = device.name // cache descriptor read
  304. logger.debug("found device: \(name ?? "(unknown)")")
  305. }
  306. return devices
  307. } onSuccess: { devices in
  308. self.allUsbDevices = devices
  309. }
  310. }
  311. func connectDevice(_ usbDevice: CSUSBDevice) {
  312. guard let usbManager = self.primaryUsbManager else {
  313. logger.error("no usb manager connected")
  314. return
  315. }
  316. guard !connectedUsbDevices.contains(usbDevice) else {
  317. logger.warning("connecting a device that is already connected")
  318. return
  319. }
  320. withUsbManagerSerialized {
  321. try await usbManager.connectUsbDevice(usbDevice)
  322. } onSuccess: {
  323. self.connectedUsbDevices.append(usbDevice)
  324. } onError: { error in
  325. self.nonfatalError = error.localizedDescription
  326. }
  327. }
  328. func disconnectDevice(_ usbDevice: CSUSBDevice) {
  329. guard let usbManager = self.primaryUsbManager else {
  330. logger.error("no usb manager connected")
  331. return
  332. }
  333. guard usbManager.isUsbDeviceConnected(usbDevice) else {
  334. logger.warning("disconnecting a device that is not connected")
  335. return
  336. }
  337. withUsbManagerSerialized {
  338. self.connectedUsbDevices.removeAll(where: { $0 == usbDevice })
  339. try await usbManager.disconnectUsbDevice(usbDevice)
  340. } onError: { error in
  341. self.nonfatalError = error.localizedDescription
  342. }
  343. }
  344. private func clearDevices() {
  345. Task { @MainActor in
  346. connectedUsbDevices.removeAll()
  347. allUsbDevices.removeAll()
  348. }
  349. }
  350. }
  351. #endif
  352. extension VMSessionState {
  353. func start(options: UTMVirtualMachineStartOptions = []) {
  354. let audioSession = AVAudioSession.sharedInstance()
  355. do {
  356. let preferDeviceMicrophone = UserDefaults.standard.bool(forKey: "PreferDeviceMicrophone")
  357. var options: AVAudioSession.CategoryOptions = [.mixWithOthers, .defaultToSpeaker, .allowBluetoothA2DP, .allowAirPlay]
  358. if !preferDeviceMicrophone {
  359. options.insert(.allowBluetooth)
  360. }
  361. try audioSession.setCategory(.playAndRecord, options: options)
  362. try audioSession.setActive(true)
  363. } catch {
  364. logger.warning("Error starting audio session: \(error.localizedDescription)")
  365. }
  366. Self.allActiveSessions[id] = self
  367. NotificationCenter.default.post(name: .vmSessionCreated, object: nil, userInfo: ["Session": self])
  368. vm.requestVmStart(options: options)
  369. }
  370. @objc private func suspend() {
  371. // dummy function for selector
  372. }
  373. func stop() {
  374. let audioSession = AVAudioSession.sharedInstance()
  375. do {
  376. try audioSession.setActive(false)
  377. } catch {
  378. logger.warning("Error stopping audio session: \(error.localizedDescription)")
  379. }
  380. // tell other screens to shut down
  381. Self.allActiveSessions.removeValue(forKey: id)
  382. NotificationCenter.default.post(name: .vmSessionEnded, object: nil, userInfo: ["Session": self])
  383. // animate to home screen
  384. let app = UIApplication.shared
  385. app.performSelector(onMainThread: #selector(suspend), with: nil, waitUntilDone: true)
  386. // wait 2 seconds while app is going background
  387. Thread.sleep(forTimeInterval: 2)
  388. // exit app when app is in background
  389. exit(0)
  390. }
  391. func powerDown() {
  392. Task {
  393. try? await vm.deleteSnapshot(name: nil)
  394. try await vm.stop(usingMethod: .force)
  395. self.stop()
  396. }
  397. }
  398. func pauseResume() {
  399. let shouldSaveState = !vm.isRunningAsDisposible
  400. if vm.state == .started {
  401. vm.requestVmPause(save: shouldSaveState)
  402. } else if vm.state == .paused {
  403. vm.requestVmResume()
  404. }
  405. }
  406. func reset() {
  407. vm.requestVmReset()
  408. }
  409. func didReceiveMemoryWarning() {
  410. let shouldAutosave = UserDefaults.standard.bool(forKey: "AutosaveLowMemory")
  411. if shouldAutosave {
  412. logger.info("Saving VM state on low memory warning.")
  413. Task {
  414. // ignore error
  415. try? await vm.saveSnapshot(name: nil)
  416. }
  417. }
  418. }
  419. func didEnterBackground() {
  420. logger.info("Entering background")
  421. let shouldAutosaveBackground = UserDefaults.standard.bool(forKey: "AutosaveBackground")
  422. if shouldAutosaveBackground && vmState == .started {
  423. logger.info("Saving snapshot")
  424. var task: UIBackgroundTaskIdentifier = .invalid
  425. task = UIApplication.shared.beginBackgroundTask {
  426. logger.info("Background task end")
  427. UIApplication.shared.endBackgroundTask(task)
  428. task = .invalid
  429. }
  430. Task {
  431. do {
  432. try await vm.saveSnapshot()
  433. self.hasAutosave = true
  434. logger.info("Save snapshot complete")
  435. } catch {
  436. logger.error("error saving snapshot: \(error)")
  437. }
  438. UIApplication.shared.endBackgroundTask(task)
  439. task = .invalid
  440. }
  441. }
  442. }
  443. func didEnterForeground() {
  444. logger.info("Entering foreground!")
  445. if (hasAutosave && vmState == .started) {
  446. logger.info("Deleting snapshot")
  447. vm.requestVmDeleteState()
  448. }
  449. }
  450. }
  451. extension Notification.Name {
  452. static let vmSessionCreated = Self("VMSessionCreated")
  453. static let vmSessionEnded = Self("VMSessionEnded")
  454. }