UTMQemuVirtualMachine.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  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 QEMUKit
  18. private var SpiceIoServiceGuestAgentContext = 0
  19. private let kSuspendSnapshotName = "suspend"
  20. /// QEMU backend virtual machine
  21. @objc class UTMQemuVirtualMachine: UTMVirtualMachine {
  22. /// Set to true to request guest tools install.
  23. ///
  24. /// This property is observable and must only be accessed on the main thread.
  25. @Published var isGuestToolsInstallRequested: Bool = false
  26. /// Handle SPICE IO related events
  27. weak var ioServiceDelegate: UTMSpiceIODelegate? {
  28. didSet {
  29. if let ioService = ioService {
  30. ioService.delegate = ioServiceDelegate
  31. }
  32. }
  33. }
  34. /// SPICE interface
  35. private(set) var ioService: UTMSpiceIO? {
  36. didSet {
  37. oldValue?.delegate = nil
  38. ioService?.delegate = ioServiceDelegate
  39. }
  40. }
  41. private let qemuVM = QEMUVirtualMachine()
  42. private var system: UTMQemuSystem? {
  43. get async {
  44. await qemuVM.launcher as? UTMQemuSystem
  45. }
  46. }
  47. /// QEMU QMP interface
  48. var monitor: QEMUMonitor? {
  49. get async {
  50. await qemuVM.monitor
  51. }
  52. }
  53. /// QEMU Guest Agent interface
  54. var guestAgent: QEMUGuestAgent? {
  55. get async {
  56. await qemuVM.guestAgent
  57. }
  58. }
  59. private var startTask: Task<Void, any Error>?
  60. }
  61. // MARK: - Shortcut access
  62. extension UTMQemuVirtualMachine {
  63. override func accessShortcut() async throws {
  64. guard isShortcut else {
  65. return
  66. }
  67. // if VM has not started yet, we create a temporary process
  68. let system = await system ?? UTMQemu()
  69. var bookmark = await registryEntry.package.remoteBookmark
  70. let existing = bookmark != nil
  71. if !existing {
  72. // create temporary bookmark
  73. bookmark = try path.bookmarkData()
  74. } else {
  75. let bookmarkPath = await registryEntry.package.path
  76. // in case old path is still accessed
  77. system.stopAccessingPath(bookmarkPath)
  78. }
  79. let (success, newBookmark, newPath) = await system.accessData(withBookmark: bookmark!, securityScoped: existing)
  80. if success {
  81. await registryEntry.setPackageRemoteBookmark(newBookmark, path: newPath)
  82. } else if existing {
  83. // the remote bookmark is invalid but the local one still might be valid
  84. await registryEntry.setPackageRemoteBookmark(nil)
  85. try await accessShortcut()
  86. } else {
  87. throw UTMQemuVirtualMachineError.failedToAccessShortcut
  88. }
  89. }
  90. }
  91. // MARK: - VM actions
  92. extension UTMQemuVirtualMachine {
  93. private var rendererBackend: UTMQEMURendererBackend {
  94. let rawValue = UserDefaults.standard.integer(forKey: "QEMURendererBackend")
  95. return UTMQEMURendererBackend(rawValue: rawValue) ?? .qemuRendererBackendDefault
  96. }
  97. @MainActor private func qemuEnsureEfiVarsAvailable() async throws {
  98. guard let efiVarsURL = qemuConfig.qemu.efiVarsURL else {
  99. return
  100. }
  101. guard qemuConfig.isLegacy else {
  102. return
  103. }
  104. _ = try await qemuConfig.qemu.saveData(to: efiVarsURL.deletingLastPathComponent(), for: qemuConfig.system)
  105. }
  106. private func _vmStart() async throws {
  107. // check if we can actually start this VM
  108. guard isSupported else {
  109. throw UTMQemuVirtualMachineError.emulationNotSupported
  110. }
  111. // start logging
  112. if await qemuConfig.qemu.hasDebugLog, let debugLogURL = await qemuConfig.qemu.debugLogURL {
  113. logging.log(toFile: debugLogURL)
  114. }
  115. await MainActor.run {
  116. qemuConfig.qemu.isDisposable = isRunningAsSnapshot
  117. }
  118. let allArguments = await qemuConfig.allArguments
  119. let arguments = allArguments.map({ $0.string })
  120. let resources = allArguments.compactMap({ $0.fileUrls }).flatMap({ $0 })
  121. let remoteBookmarks = await remoteBookmarks
  122. let system = await UTMQemuSystem(arguments: arguments, architecture: qemuConfig.system.architecture.rawValue)
  123. system.resources = resources
  124. system.remoteBookmarks = remoteBookmarks as NSDictionary
  125. system.rendererBackend = rendererBackend
  126. try Task.checkCancellation()
  127. if isShortcut {
  128. try await accessShortcut()
  129. try Task.checkCancellation()
  130. }
  131. let ioService = UTMSpiceIO(configuration: config)
  132. try ioService.start()
  133. try Task.checkCancellation()
  134. // create EFI variables for legacy config
  135. // this is ugly code and should be removed when legacy config support is removed
  136. try await qemuEnsureEfiVarsAvailable()
  137. try Task.checkCancellation()
  138. // start QEMU
  139. await qemuVM.setDelegate(self)
  140. try await qemuVM.start(launcher: system, interface: ioService)
  141. let monitor = await monitor!
  142. try Task.checkCancellation()
  143. // load saved state if requested
  144. if !isRunningAsSnapshot, await registryEntry.isSuspended {
  145. try await monitor.qemuRestoreSnapshot(kSuspendSnapshotName)
  146. try Task.checkCancellation()
  147. }
  148. // set up SPICE sharing and removable drives
  149. try await self.restoreExternalDrives()
  150. try await self.restoreSharedDirectory()
  151. try Task.checkCancellation()
  152. // continue VM boot
  153. try await monitor.continueBoot()
  154. // delete saved state
  155. if await registryEntry.isSuspended {
  156. try? await _vmDeleteState()
  157. }
  158. // save ioService and let it set the delegate
  159. self.ioService = ioService
  160. }
  161. override func vmStart() async throws {
  162. guard state == .vmStopped else {
  163. throw UTMQemuVirtualMachineError.invalidVmState
  164. }
  165. changeState(.vmStarting)
  166. do {
  167. startTask = Task {
  168. try await _vmStart()
  169. }
  170. defer {
  171. startTask = nil
  172. }
  173. try await startTask!.value
  174. changeState(.vmStarted)
  175. } catch {
  176. // delete suspend state on error
  177. await registryEntry.setIsSuspended(false)
  178. changeState(.vmStopped)
  179. throw error
  180. }
  181. }
  182. override func vmStop(force: Bool) async throws {
  183. if force {
  184. // prevent deadlock force stopping during startup
  185. ioService?.disconnect()
  186. }
  187. guard state != .vmStopped else {
  188. return // nothing to do
  189. }
  190. guard force || state == .vmStarted else {
  191. throw UTMQemuVirtualMachineError.invalidVmState
  192. }
  193. if !force {
  194. changeState(.vmStopping)
  195. }
  196. defer {
  197. changeState(.vmStopped)
  198. }
  199. if force {
  200. await qemuVM.kill()
  201. } else {
  202. try await qemuVM.stop()
  203. }
  204. }
  205. private func _vmReset() async throws {
  206. if await registryEntry.isSuspended {
  207. try? await _vmDeleteState()
  208. }
  209. guard let monitor = await qemuVM.monitor else {
  210. throw UTMQemuVirtualMachineError.invalidVmState
  211. }
  212. try await monitor.qemuReset()
  213. }
  214. override func vmReset() async throws {
  215. guard state == .vmStarted || state == .vmPaused else {
  216. throw UTMQemuVirtualMachineError.invalidVmState
  217. }
  218. changeState(.vmStopping)
  219. do {
  220. try await _vmReset()
  221. changeState(.vmStarted)
  222. } catch {
  223. changeState(.vmStopped)
  224. throw error
  225. }
  226. }
  227. private func _vmPause() async throws {
  228. guard let monitor = await monitor else {
  229. throw UTMQemuVirtualMachineError.invalidVmState
  230. }
  231. await updateScreenshot()
  232. await saveScreenshot()
  233. try await monitor.qemuStop()
  234. }
  235. override func vmPause(save: Bool) async throws {
  236. guard state == .vmStarted else {
  237. throw UTMQemuVirtualMachineError.invalidVmState
  238. }
  239. changeState(.vmPausing)
  240. do {
  241. try await _vmPause()
  242. if save {
  243. try? await _vmSaveState()
  244. }
  245. changeState(.vmPaused)
  246. } catch {
  247. changeState(.vmStopped)
  248. throw error
  249. }
  250. }
  251. private func _vmSaveState() async throws {
  252. guard let monitor = await monitor else {
  253. throw UTMQemuVirtualMachineError.invalidVmState
  254. }
  255. do {
  256. let result = try await monitor.qemuSaveSnapshot(kSuspendSnapshotName)
  257. if result.localizedCaseInsensitiveContains("Error") {
  258. throw UTMQemuVirtualMachineError.qemuError(result)
  259. }
  260. await registryEntry.setIsSuspended(true)
  261. await saveScreenshot()
  262. } catch {
  263. throw UTMQemuVirtualMachineError.saveSnapshotFailed(error)
  264. }
  265. }
  266. override func vmSaveState() async throws {
  267. guard state == .vmPaused || state == .vmStarted else {
  268. throw UTMQemuVirtualMachineError.invalidVmState
  269. }
  270. try await _vmSaveState()
  271. }
  272. private func _vmDeleteState() async throws {
  273. if let monitor = await monitor { // if QEMU is running
  274. let result = try await monitor.qemuDeleteSnapshot(kSuspendSnapshotName)
  275. if result.localizedCaseInsensitiveContains("Error") {
  276. throw UTMQemuVirtualMachineError.qemuError(result)
  277. }
  278. }
  279. await registryEntry.setIsSuspended(false)
  280. }
  281. override func vmDeleteState() async throws {
  282. try await _vmDeleteState()
  283. }
  284. private func _vmResume() async throws {
  285. guard let monitor = await monitor else {
  286. throw UTMQemuVirtualMachineError.invalidVmState
  287. }
  288. try await monitor.qemuResume()
  289. if await registryEntry.isSuspended {
  290. try? await _vmDeleteState()
  291. }
  292. }
  293. override func vmResume() async throws {
  294. guard state == .vmPaused else {
  295. throw UTMQemuVirtualMachineError.invalidVmState
  296. }
  297. changeState(.vmResuming)
  298. do {
  299. try await _vmResume()
  300. changeState(.vmStarted)
  301. } catch {
  302. changeState(.vmStopped)
  303. throw error
  304. }
  305. }
  306. override func vmGuestPowerDown() async throws {
  307. guard let monitor = await monitor else {
  308. throw UTMQemuVirtualMachineError.invalidVmState
  309. }
  310. try await monitor.qemuPowerDown()
  311. }
  312. /// Attempt to cancel the current operation
  313. ///
  314. /// Currently only `vmStart()` can be cancelled.
  315. func cancelOperation() {
  316. startTask?.cancel()
  317. }
  318. }
  319. // MARK: - VM delegate
  320. extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate {
  321. func qemuVMDidStart(_ qemuVM: QEMUVirtualMachine) {
  322. // not used
  323. }
  324. func qemuVMWillStop(_ qemuVM: QEMUVirtualMachine) {
  325. // not used
  326. }
  327. func qemuVMDidStop(_ qemuVM: QEMUVirtualMachine) {
  328. changeState(.vmStopped)
  329. }
  330. func qemuVM(_ qemuVM: QEMUVirtualMachine, didError error: Error) {
  331. delegate?.virtualMachine(self, didErrorWithMessage: error.localizedDescription)
  332. }
  333. func qemuVM(_ qemuVM: QEMUVirtualMachine, didCreatePttyDevice path: String, label: String) {
  334. let scanner = Scanner(string: label)
  335. guard scanner.scanString("term") != nil else {
  336. logger.error("Invalid terminal device '\(label)'")
  337. return
  338. }
  339. var term: Int = -1
  340. guard scanner.scanInt(&term) else {
  341. logger.error("Cannot get index from terminal device '\(label)'")
  342. return
  343. }
  344. let index = term
  345. Task { @MainActor in
  346. guard index >= 0 && index < qemuConfig.serials.count else {
  347. logger.error("Serial device '\(path)' out of bounds for index \(index)")
  348. return
  349. }
  350. qemuConfig.serials[index].pttyDevice = URL(fileURLWithPath: path)
  351. }
  352. }
  353. }
  354. // MARK: - Input device switching
  355. extension UTMQemuVirtualMachine {
  356. func requestInputTablet(_ tablet: Bool) {
  357. }
  358. }
  359. // MARK: - USB redirection
  360. extension UTMQemuVirtualMachine {
  361. var hasUsbRedirection: Bool {
  362. return jb_has_usb_entitlement()
  363. }
  364. }
  365. // MARK: - Screenshot
  366. extension UTMQemuVirtualMachine {
  367. @MainActor
  368. override func updateScreenshot() {
  369. ioService?.screenshot(completion: { screenshot in
  370. self.screenshot = screenshot
  371. })
  372. }
  373. @MainActor
  374. override func saveScreenshot() {
  375. super.saveScreenshot()
  376. }
  377. }
  378. // MARK: - Display details
  379. extension UTMQemuVirtualMachine {
  380. internal var qemuConfig: UTMQemuConfiguration {
  381. config.qemuConfig!
  382. }
  383. @MainActor override var detailsTitleLabel: String {
  384. qemuConfig.information.name
  385. }
  386. @MainActor override var detailsSubtitleLabel: String {
  387. detailsSystemTargetLabel
  388. }
  389. @MainActor override var detailsNotes: String? {
  390. qemuConfig.information.notes
  391. }
  392. @MainActor override var detailsSystemTargetLabel: String {
  393. qemuConfig.system.target.prettyValue
  394. }
  395. @MainActor override var detailsSystemArchitectureLabel: String {
  396. qemuConfig.system.architecture.prettyValue
  397. }
  398. @MainActor override var detailsSystemMemoryLabel: String {
  399. let bytesInMib = Int64(1048576)
  400. return ByteCountFormatter.string(fromByteCount: Int64(qemuConfig.system.memorySize) * bytesInMib, countStyle: .binary)
  401. }
  402. /// Check if a QEMU target is supported
  403. /// - Parameter systemArchitecture: QEMU architecture
  404. /// - Returns: true if UTM is compiled with the supporting binaries
  405. internal static func isSupported(systemArchitecture: QEMUArchitecture) -> Bool {
  406. let arch = systemArchitecture.rawValue
  407. let bundleURL = Bundle.main.bundleURL
  408. #if os(macOS)
  409. let contentsURL = bundleURL.appendingPathComponent("Contents", isDirectory: true)
  410. let base = "Versions/A/"
  411. #else
  412. let contentsURL = bundleURL
  413. let base = ""
  414. #endif
  415. let frameworksURL = contentsURL.appendingPathComponent("Frameworks", isDirectory: true)
  416. let framework = frameworksURL.appendingPathComponent("qemu-" + arch + "-softmmu.framework/" + base + "qemu-" + arch + "-softmmu", isDirectory: false)
  417. return FileManager.default.fileExists(atPath: framework.path)
  418. }
  419. /// Check if the current VM target is supported by the host
  420. @objc var isSupported: Bool {
  421. return UTMQemuVirtualMachine.isSupported(systemArchitecture: qemuConfig._system.architecture)
  422. }
  423. }
  424. // MARK: - External drives
  425. extension UTMQemuVirtualMachine {
  426. func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool = false) async throws {
  427. guard drive.isExternal else {
  428. return
  429. }
  430. if let qemu = await monitor, qemu.isConnected {
  431. try qemu.ejectDrive("drive\(drive.id)", force: isForced)
  432. }
  433. if let oldPath = await registryEntry.externalDrives[drive.id]?.path {
  434. await system?.stopAccessingPath(oldPath)
  435. }
  436. await registryEntry.removeExternalDrive(forId: drive.id)
  437. }
  438. func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws {
  439. _ = url.startAccessingSecurityScopedResource()
  440. defer {
  441. url.stopAccessingSecurityScopedResource()
  442. }
  443. let tempBookmark = try url.bookmarkData()
  444. try await eject(drive, isForced: true)
  445. let file = try UTMRegistryEntry.File(url: url, isReadOnly: drive.isReadOnly)
  446. await registryEntry.setExternalDrive(file, forId: drive.id)
  447. try await changeMedium(drive, with: tempBookmark, url: url, isSecurityScoped: false)
  448. }
  449. private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, url: URL?, isSecurityScoped: Bool) async throws {
  450. let system = await system ?? UTMQemu()
  451. let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped)
  452. guard let bookmark = bookmark, let path = path, success else {
  453. throw UTMQemuVirtualMachineError.accessDriveImageFailed
  454. }
  455. await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id)
  456. if let qemu = await monitor, qemu.isConnected {
  457. try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path)
  458. }
  459. }
  460. func restoreExternalDrives() async throws {
  461. guard await system != nil else {
  462. throw UTMQemuVirtualMachineError.invalidVmState
  463. }
  464. for drive in await qemuConfig.drives {
  465. if !drive.isExternal {
  466. continue
  467. }
  468. let id = drive.id
  469. if let bookmark = await registryEntry.externalDrives[id]?.remoteBookmark {
  470. // an image bookmark was saved while QEMU was running
  471. try await changeMedium(drive, with: bookmark, url: nil, isSecurityScoped: true)
  472. } else if let localBookmark = await registryEntry.externalDrives[id]?.bookmark {
  473. // an image bookmark was saved while QEMU was NOT running
  474. let url = try URL(resolvingPersistentBookmarkData: localBookmark)
  475. try await changeMedium(drive, to: url)
  476. } else {
  477. // a placeholder image might have been mounted
  478. try await eject(drive)
  479. }
  480. }
  481. }
  482. @objc func restoreExternalDrivesAndShares(completion: @escaping (Error?) -> Void) {
  483. Task.detached {
  484. do {
  485. try await self.restoreExternalDrives()
  486. try await self.restoreSharedDirectory()
  487. completion(nil)
  488. } catch {
  489. completion(error)
  490. }
  491. }
  492. }
  493. @MainActor func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? {
  494. registryEntry.externalDrives[drive.id]?.url
  495. }
  496. }
  497. // MARK: - Shared directory
  498. extension UTMQemuVirtualMachine {
  499. @MainActor var sharedDirectoryURL: URL? {
  500. registryEntry.sharedDirectories.first?.url
  501. }
  502. func clearSharedDirectory() async {
  503. if let oldPath = await registryEntry.sharedDirectories.first?.path {
  504. await system?.stopAccessingPath(oldPath)
  505. }
  506. await registryEntry.removeAllSharedDirectories()
  507. }
  508. func changeSharedDirectory(to url: URL) async throws {
  509. await clearSharedDirectory()
  510. _ = url.startAccessingSecurityScopedResource()
  511. defer {
  512. url.stopAccessingSecurityScopedResource()
  513. }
  514. let file = try await UTMRegistryEntry.File(url: url, isReadOnly: qemuConfig.sharing.isDirectoryShareReadOnly)
  515. await registryEntry.setSingleSharedDirectory(file)
  516. if await qemuConfig.sharing.directoryShareMode == .webdav {
  517. if let ioService = ioService {
  518. ioService.changeSharedDirectory(url)
  519. }
  520. } else if await qemuConfig.sharing.directoryShareMode == .virtfs {
  521. let tempBookmark = try url.bookmarkData()
  522. try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false)
  523. }
  524. }
  525. func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws {
  526. let system = await system ?? UTMQemu()
  527. let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped)
  528. guard let bookmark = bookmark, let _ = path, success else {
  529. throw UTMQemuVirtualMachineError.accessDriveImageFailed
  530. }
  531. await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark)
  532. }
  533. func restoreSharedDirectory() async throws {
  534. guard let share = await registryEntry.sharedDirectories.first else {
  535. return
  536. }
  537. if await qemuConfig.sharing.directoryShareMode == .virtfs {
  538. if let bookmark = share.remoteBookmark {
  539. // a share bookmark was saved while QEMU was running
  540. try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true)
  541. } else {
  542. // a share bookmark was saved while QEMU was NOT running
  543. let url = try URL(resolvingPersistentBookmarkData: share.bookmark)
  544. try await changeSharedDirectory(to: url)
  545. }
  546. } else if await qemuConfig.sharing.directoryShareMode == .webdav {
  547. if let ioService = ioService {
  548. ioService.changeSharedDirectory(share.url)
  549. }
  550. }
  551. }
  552. }
  553. // MARK: - Registry syncing
  554. extension UTMQemuVirtualMachine {
  555. @MainActor override func updateRegistryFromConfig() async throws {
  556. // save a copy to not collide with updateConfigFromRegistry()
  557. let configShare = qemuConfig.sharing.directoryShareUrl
  558. let configDrives = qemuConfig.drives
  559. try await super.updateRegistryFromConfig()
  560. for drive in configDrives {
  561. if drive.isExternal, let url = drive.imageURL {
  562. try await changeMedium(drive, to: url)
  563. }
  564. }
  565. if let url = configShare {
  566. try await changeSharedDirectory(to: url)
  567. }
  568. // remove any unreferenced drives
  569. registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in
  570. configDrives.contains(where: { $0.id == element.key && $0.isExternal })
  571. })
  572. }
  573. @MainActor override func updateConfigFromRegistry() {
  574. super.updateConfigFromRegistry()
  575. qemuConfig.sharing.directoryShareUrl = sharedDirectoryURL
  576. for i in qemuConfig.drives.indices {
  577. let id = qemuConfig.drives[i].id
  578. if qemuConfig.drives[i].isExternal {
  579. qemuConfig.drives[i].imageURL = registryEntry.externalDrives[id]?.url
  580. }
  581. }
  582. }
  583. @MainActor @objc var remoteBookmarks: [URL: Data] {
  584. var dict = [URL: Data]()
  585. for file in registryEntry.externalDrives.values {
  586. if let bookmark = file.remoteBookmark {
  587. dict[file.url] = bookmark
  588. }
  589. }
  590. for file in registryEntry.sharedDirectories {
  591. if let bookmark = file.remoteBookmark {
  592. dict[file.url] = bookmark
  593. }
  594. }
  595. return dict
  596. }
  597. }
  598. enum UTMQemuVirtualMachineError: Error {
  599. case failedToAccessShortcut
  600. case emulationNotSupported
  601. case qemuError(String)
  602. case accessDriveImageFailed
  603. case accessShareFailed
  604. case invalidVmState
  605. case saveSnapshotFailed(Error)
  606. }
  607. extension UTMQemuVirtualMachineError: LocalizedError {
  608. var errorDescription: String? {
  609. switch self {
  610. case .failedToAccessShortcut:
  611. return NSLocalizedString("Failed to access data from shortcut.", comment: "UTMQemuVirtualMachine")
  612. case .emulationNotSupported:
  613. return NSLocalizedString("This build of UTM does not support emulating the architecture of this VM.", comment: "UTMQemuVirtualMachine")
  614. case .qemuError(let message):
  615. return message
  616. case .accessDriveImageFailed: return NSLocalizedString("Failed to access drive image path.", comment: "UTMQemuVirtualMachine")
  617. case .accessShareFailed: return NSLocalizedString("Failed to access shared directory.", comment: "UTMQemuVirtualMachine")
  618. case .invalidVmState: return NSLocalizedString("The virtual machine is in an invalid state.", comment: "UTMQemuVirtualMachine")
  619. case .saveSnapshotFailed(let error):
  620. return String.localizedStringWithFormat(NSLocalizedString("Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@", comment: "UTMQemuVirtualMachine"), error.localizedDescription)
  621. }
  622. }
  623. }