VMSessionState.swift 16 KB

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