UTMSpiceVirtualMachine.swift 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. //
  2. // Copyright © 2024 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. /// Common methods for all SPICE virtual machines
  18. protocol UTMSpiceVirtualMachine: UTMVirtualMachine where Configuration == UTMQemuConfiguration {
  19. /// Set when VM is running with saving changes
  20. var isRunningAsDisposible: Bool { get }
  21. /// Get and set screenshot
  22. var screenshot: UTMVirtualMachineScreenshot? { get set }
  23. /// Handles IO
  24. var ioServiceDelegate: UTMSpiceIODelegate? { get set }
  25. /// SPICE interface
  26. var ioService: UTMSpiceIO? { get }
  27. /// Change input mode
  28. /// - Parameter tablet: If true, mouse events will be absolute
  29. func requestInputTablet(_ tablet: Bool)
  30. /// Eject a removable drive
  31. /// - Parameter drive: Removable drive
  32. func eject(_ drive: UTMQemuConfigurationDrive) async throws
  33. /// Change mount image of a removable drive
  34. /// - Parameters:
  35. /// - drive: Removable drive
  36. /// - url: New mount image
  37. func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws
  38. /// Release resources for accessing a path
  39. /// - Parameter path: Path to stop accessing
  40. func stopAccessingPath(_ path: String) async
  41. /// Setup access to a VirtFS shared directory
  42. ///
  43. /// Throw an exception if this is not supported.
  44. /// - Parameters:
  45. /// - bookmark: Bookmark to access
  46. /// - isSecurityScoped: Is the bookmark security scoped?
  47. func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws
  48. }
  49. // MARK: - USB redirection
  50. extension UTMSpiceVirtualMachine {
  51. var hasUsbRedirection: Bool {
  52. #if WITH_USB
  53. return jb_has_usb_entitlement()
  54. #else
  55. return false
  56. #endif
  57. }
  58. }
  59. // MARK: - Screenshot
  60. extension UTMSpiceVirtualMachine {
  61. @MainActor @discardableResult
  62. func takeScreenshot() async -> Bool {
  63. if let screenshot = await ioService?.screenshot() {
  64. self.screenshot = UTMVirtualMachineScreenshot(wrapping: screenshot.image)
  65. }
  66. return true
  67. }
  68. func reloadScreenshotFromFile() {
  69. screenshot = loadScreenshot()
  70. }
  71. }
  72. // MARK: - External drives
  73. extension UTMSpiceVirtualMachine {
  74. @MainActor func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? {
  75. registryEntry.externalDrives[drive.id]?.url
  76. }
  77. }
  78. // MARK: - Shared directory
  79. extension UTMSpiceVirtualMachine {
  80. @MainActor var sharedDirectoryURL: URL? {
  81. registryEntry.sharedDirectories.first?.url
  82. }
  83. func clearSharedDirectory() async {
  84. if let oldPath = await registryEntry.sharedDirectories.first?.path {
  85. await stopAccessingPath(oldPath)
  86. }
  87. await registryEntry.removeAllSharedDirectories()
  88. }
  89. func changeSharedDirectory(to url: URL) async throws {
  90. await clearSharedDirectory()
  91. let isScopedAccess = url.startAccessingSecurityScopedResource()
  92. defer {
  93. if isScopedAccess {
  94. url.stopAccessingSecurityScopedResource()
  95. }
  96. }
  97. let file = try await UTMRegistryEntry.File(url: url, isReadOnly: config.sharing.isDirectoryShareReadOnly)
  98. await registryEntry.setSingleSharedDirectory(file)
  99. if await config.sharing.directoryShareMode == .webdav {
  100. if let ioService = ioService {
  101. ioService.changeSharedDirectory(url)
  102. }
  103. } else if await config.sharing.directoryShareMode == .virtfs {
  104. let tempBookmark = try url.bookmarkData()
  105. try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false)
  106. }
  107. }
  108. func restoreSharedDirectory(for ioService: UTMSpiceIO) async throws {
  109. guard let share = await registryEntry.sharedDirectories.first else {
  110. return
  111. }
  112. if await config.sharing.directoryShareMode == .virtfs {
  113. if let bookmark = share.remoteBookmark {
  114. // a share bookmark was saved while QEMU was running
  115. try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true)
  116. } else {
  117. // a share bookmark was saved while QEMU was NOT running
  118. let url = try URL(resolvingPersistentBookmarkData: share.bookmark)
  119. try await changeSharedDirectory(to: url)
  120. }
  121. } else if await config.sharing.directoryShareMode == .webdav {
  122. ioService.changeSharedDirectory(share.url)
  123. }
  124. }
  125. }
  126. // MARK: - Registry syncing
  127. extension UTMSpiceVirtualMachine {
  128. @MainActor func updateRegistryFromConfig() async throws {
  129. // save a copy to not collide with updateConfigFromRegistry()
  130. let configShare = config.sharing.directoryShareUrl
  131. let configDrives = config.drives
  132. try await updateRegistryBasics()
  133. for drive in configDrives {
  134. if drive.isExternal, let url = drive.imageURL {
  135. try await changeMedium(drive, to: url)
  136. } else if drive.isExternal {
  137. try await eject(drive)
  138. }
  139. }
  140. if let url = configShare {
  141. try await changeSharedDirectory(to: url)
  142. } else {
  143. await clearSharedDirectory()
  144. }
  145. // remove any unreferenced drives
  146. registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in
  147. configDrives.contains(where: { $0.id == element.key && $0.isExternal })
  148. })
  149. }
  150. @MainActor func updateConfigFromRegistry() {
  151. config.sharing.directoryShareUrl = sharedDirectoryURL
  152. for i in config.drives.indices {
  153. let id = config.drives[i].id
  154. if config.drives[i].isExternal {
  155. config.drives[i].imageURL = registryEntry.externalDrives[id]?.url
  156. }
  157. }
  158. }
  159. }
  160. // MARK: - Headless
  161. extension UTMSpiceVirtualMachine {
  162. @MainActor var isHeadless: Bool {
  163. config.displays.isEmpty && config.serials.filter({ $0.mode == .builtin }).isEmpty
  164. }
  165. }