UTMAppleVirtualMachine.swift 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  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. import Combine
  17. import Virtualization
  18. @available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
  19. @available(macOS 11, *)
  20. final class UTMAppleVirtualMachine: UTMVirtualMachine {
  21. struct Capabilities: UTMVirtualMachineCapabilities {
  22. var supportsProcessKill: Bool {
  23. false
  24. }
  25. var supportsSnapshots: Bool {
  26. false
  27. }
  28. var supportsScreenshots: Bool {
  29. true
  30. }
  31. var supportsDisposibleMode: Bool {
  32. false
  33. }
  34. var supportsRecoveryMode: Bool {
  35. true
  36. }
  37. var supportsRemoteSession: Bool {
  38. false
  39. }
  40. }
  41. static let capabilities = Capabilities()
  42. private(set) var pathUrl: URL {
  43. didSet {
  44. if isScopedAccess {
  45. oldValue.stopAccessingSecurityScopedResource()
  46. }
  47. isScopedAccess = pathUrl.startAccessingSecurityScopedResource()
  48. }
  49. }
  50. private(set) var isShortcut: Bool = false
  51. let isRunningAsDisposible: Bool = false
  52. weak var delegate: (any UTMVirtualMachineDelegate)?
  53. var onConfigurationChange: (() -> Void)?
  54. var onStateChange: (() -> Void)?
  55. private(set) var config: UTMAppleConfiguration {
  56. willSet {
  57. onConfigurationChange?()
  58. }
  59. }
  60. private(set) var registryEntry: UTMRegistryEntry {
  61. willSet {
  62. onConfigurationChange?()
  63. }
  64. }
  65. private(set) var state: UTMVirtualMachineState = .stopped {
  66. willSet {
  67. onStateChange?()
  68. }
  69. didSet {
  70. delegate?.virtualMachine(self, didTransitionToState: state)
  71. }
  72. }
  73. private(set) var screenshot: UTMVirtualMachineScreenshot? {
  74. willSet {
  75. onStateChange?()
  76. }
  77. }
  78. private(set) var snapshotUnsupportedError: Error?
  79. private var isScopedAccess: Bool = false
  80. private weak var screenshotTimer: Timer?
  81. private let vmQueue = DispatchQueue(label: "VZVirtualMachineQueue", qos: .userInteractive)
  82. /// This variable MUST be synchronized by `vmQueue`
  83. private(set) var apple: VZVirtualMachine?
  84. private var installProgress: Progress?
  85. private var progressObserver: NSKeyValueObservation?
  86. private var sharedDirectoriesChanged: AnyCancellable?
  87. weak var screenshotDelegate: UTMScreenshotProvider?
  88. private var activeResourceUrls: [String: URL] = [:]
  89. private var removableDrives: [String: Any] = [:]
  90. @MainActor var isHeadless: Bool {
  91. config.displays.isEmpty && config.serials.filter({ $0.mode == .builtin }).isEmpty
  92. }
  93. @MainActor required init(packageUrl: URL, configuration: UTMAppleConfiguration, isShortcut: Bool = false) throws {
  94. self.isScopedAccess = packageUrl.startAccessingSecurityScopedResource()
  95. // load configuration
  96. self.config = configuration
  97. self.pathUrl = packageUrl
  98. self.isShortcut = isShortcut
  99. self.registryEntry = UTMRegistryEntry.empty
  100. self.registryEntry = loadRegistry()
  101. self.screenshot = loadScreenshot()
  102. }
  103. deinit {
  104. if isScopedAccess {
  105. pathUrl.stopAccessingSecurityScopedResource()
  106. }
  107. }
  108. @MainActor func reload(from packageUrl: URL?) throws {
  109. let packageUrl = packageUrl ?? pathUrl
  110. guard let newConfig = try UTMAppleConfiguration.load(from: packageUrl) as? UTMAppleConfiguration else {
  111. throw UTMConfigurationError.invalidBackend
  112. }
  113. config = newConfig
  114. pathUrl = packageUrl
  115. updateConfigFromRegistry()
  116. }
  117. private func _start(options: UTMVirtualMachineStartOptions) async throws {
  118. let boot = await config.system.boot
  119. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) -> Void in
  120. vmQueue.async {
  121. guard let apple = self.apple else {
  122. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  123. return
  124. }
  125. #if os(macOS) && arch(arm64)
  126. if #available(macOS 13, *), boot.operatingSystem == .macOS {
  127. let vzoptions = VZMacOSVirtualMachineStartOptions()
  128. vzoptions.startUpFromMacOSRecovery = options.contains(.bootRecovery)
  129. apple.start(options: vzoptions) { result in
  130. if let result = result {
  131. continuation.resume(with: .failure(result))
  132. } else {
  133. continuation.resume()
  134. }
  135. }
  136. return
  137. }
  138. #endif
  139. apple.start { result in
  140. continuation.resume(with: result)
  141. }
  142. }
  143. }
  144. try? updateLastModified()
  145. }
  146. func start(options: UTMVirtualMachineStartOptions = []) async throws {
  147. guard state == .stopped else {
  148. return
  149. }
  150. state = .starting
  151. do {
  152. let isSuspended = await registryEntry.isSuspended
  153. try await beginAccessingResources()
  154. try await createAppleVM()
  155. if isSuspended && !options.contains(.bootRecovery) {
  156. try await restoreSnapshot()
  157. } else {
  158. try await _start(options: options)
  159. }
  160. if #available(macOS 15, *) {
  161. try await attachExternalDrives()
  162. }
  163. if #available(macOS 12, *) {
  164. Task { @MainActor in
  165. let tag = config.shareDirectoryTag
  166. sharedDirectoriesChanged = config.sharedDirectoriesPublisher.sink { [weak self] newShares in
  167. guard let self = self else {
  168. return
  169. }
  170. self.vmQueue.async {
  171. self.updateSharedDirectories(with: newShares, tag: tag)
  172. }
  173. }
  174. }
  175. }
  176. state = .started
  177. if screenshotTimer == nil {
  178. screenshotTimer = startScreenshotTimer()
  179. }
  180. } catch {
  181. await stopAccesingResources()
  182. state = .stopped
  183. try? await deleteSnapshot()
  184. throw error
  185. }
  186. }
  187. @available(macOS 12, *)
  188. private func _forceStop() async throws {
  189. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  190. vmQueue.async {
  191. guard let apple = self.apple else {
  192. continuation.resume() // already stopped
  193. return
  194. }
  195. apple.stop { error in
  196. if let error = error {
  197. continuation.resume(throwing: error)
  198. } else {
  199. self.guestDidStop(apple)
  200. continuation.resume()
  201. }
  202. }
  203. }
  204. }
  205. }
  206. private func _requestStop() async throws {
  207. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  208. vmQueue.async {
  209. guard let apple = self.apple else {
  210. continuation.resume() // already stopped
  211. return
  212. }
  213. do {
  214. try apple.requestStop()
  215. continuation.resume()
  216. } catch {
  217. continuation.resume(throwing: error)
  218. }
  219. }
  220. }
  221. }
  222. func stop(usingMethod method: UTMVirtualMachineStopMethod = .request) async throws {
  223. if let installProgress = installProgress {
  224. installProgress.cancel()
  225. return
  226. }
  227. guard state == .started || state == .paused else {
  228. return
  229. }
  230. guard method != .request else {
  231. return try await _requestStop()
  232. }
  233. guard #available(macOS 12, *) else {
  234. throw UTMAppleVirtualMachineError.operationNotAvailable
  235. }
  236. state = .stopping
  237. do {
  238. try await _forceStop()
  239. state = .stopped
  240. } catch {
  241. state = .stopped
  242. throw error
  243. }
  244. }
  245. private func _restart() async throws {
  246. guard #available(macOS 12, *) else {
  247. return
  248. }
  249. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  250. vmQueue.async {
  251. guard let apple = self.apple else {
  252. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  253. return
  254. }
  255. apple.stop { error in
  256. if let error = error {
  257. continuation.resume(throwing: error)
  258. } else {
  259. apple.start { result in
  260. continuation.resume(with: result)
  261. }
  262. }
  263. }
  264. }
  265. }
  266. }
  267. func restart() async throws {
  268. guard state == .started || state == .paused else {
  269. return
  270. }
  271. state = .stopping
  272. do {
  273. try await _restart()
  274. state = .started
  275. } catch {
  276. state = .stopped
  277. throw error
  278. }
  279. }
  280. private func _pause() async throws {
  281. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  282. vmQueue.async {
  283. guard let apple = self.apple else {
  284. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  285. return
  286. }
  287. Task { @MainActor in
  288. await self.takeScreenshot()
  289. try? self.saveScreenshot()
  290. }
  291. apple.pause { result in
  292. continuation.resume(with: result)
  293. }
  294. }
  295. }
  296. }
  297. func pause() async throws {
  298. guard state == .started else {
  299. return
  300. }
  301. state = .pausing
  302. do {
  303. try await _pause()
  304. state = .paused
  305. } catch {
  306. state = .stopped
  307. throw error
  308. }
  309. }
  310. #if arch(arm64)
  311. @available(macOS 14, *)
  312. private func _saveSnapshot(url: URL) async throws {
  313. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  314. vmQueue.async {
  315. guard let apple = self.apple else {
  316. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  317. return
  318. }
  319. apple.saveMachineStateTo(url: url) { error in
  320. if let error = error {
  321. continuation.resume(throwing: error)
  322. } else {
  323. continuation.resume()
  324. }
  325. }
  326. }
  327. }
  328. try? updateLastModified()
  329. }
  330. #endif
  331. func saveSnapshot(name: String? = nil) async throws {
  332. guard #available(macOS 14, *) else {
  333. return
  334. }
  335. #if arch(arm64)
  336. guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
  337. return
  338. }
  339. if let snapshotUnsupportedError = snapshotUnsupportedError {
  340. throw snapshotUnsupportedError
  341. }
  342. if state == .started {
  343. try await pause()
  344. }
  345. guard state == .paused else {
  346. return
  347. }
  348. state = .saving
  349. defer {
  350. state = .paused
  351. }
  352. try await _saveSnapshot(url: vmSavedStateURL)
  353. await registryEntry.setIsSuspended(true)
  354. #endif
  355. }
  356. func deleteSnapshot(name: String? = nil) async throws {
  357. guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
  358. return
  359. }
  360. await registryEntry.setIsSuspended(false)
  361. try FileManager.default.removeItem(at: vmSavedStateURL)
  362. try? updateLastModified()
  363. }
  364. #if arch(arm64)
  365. @available(macOS 14, *)
  366. private func _restoreSnapshot(url: URL) async throws {
  367. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  368. vmQueue.async {
  369. guard let apple = self.apple else {
  370. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  371. return
  372. }
  373. apple.restoreMachineStateFrom(url: url) { error in
  374. if let error = error {
  375. continuation.resume(throwing: error)
  376. } else {
  377. continuation.resume()
  378. }
  379. }
  380. }
  381. }
  382. }
  383. #endif
  384. func restoreSnapshot(name: String? = nil) async throws {
  385. guard #available(macOS 14, *) else {
  386. throw UTMAppleVirtualMachineError.operationNotAvailable
  387. }
  388. #if arch(arm64)
  389. guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
  390. throw UTMAppleVirtualMachineError.operationNotAvailable
  391. }
  392. if state == .started {
  393. try await stop(usingMethod: .force)
  394. }
  395. guard state == .stopped || state == .starting else {
  396. throw UTMAppleVirtualMachineError.operationNotAvailable
  397. }
  398. state = .restoring
  399. do {
  400. try await _restoreSnapshot(url: vmSavedStateURL)
  401. try await _resume()
  402. } catch {
  403. state = .stopped
  404. throw error
  405. }
  406. state = .started
  407. try await deleteSnapshot(name: name)
  408. #else
  409. throw UTMAppleVirtualMachineError.operationNotAvailable
  410. #endif
  411. }
  412. private func _resume() async throws {
  413. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  414. vmQueue.async {
  415. guard let apple = self.apple else {
  416. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  417. return
  418. }
  419. apple.resume { result in
  420. continuation.resume(with: result)
  421. }
  422. }
  423. }
  424. }
  425. func resume() async throws {
  426. guard state == .paused else {
  427. return
  428. }
  429. state = .resuming
  430. do {
  431. try await _resume()
  432. state = .started
  433. } catch {
  434. state = .stopped
  435. throw error
  436. }
  437. }
  438. @discardableResult @MainActor
  439. func takeScreenshot() async -> Bool {
  440. screenshot = screenshotDelegate?.screenshot
  441. return true
  442. }
  443. func reloadScreenshotFromFile() {
  444. screenshot = loadScreenshot()
  445. }
  446. @MainActor private func createAppleVM() throws {
  447. for i in config.serials.indices {
  448. let (fd, sfd, name) = try createPty()
  449. let terminalTtyHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: false)
  450. let slaveTtyHandle = FileHandle(fileDescriptor: sfd, closeOnDealloc: false)
  451. config.serials[i].fileHandleForReading = terminalTtyHandle
  452. config.serials[i].fileHandleForWriting = terminalTtyHandle
  453. let serialPort = UTMSerialPort(portNamed: name, readFileHandle: slaveTtyHandle, writeFileHandle: slaveTtyHandle, terminalFileHandle: terminalTtyHandle)
  454. config.serials[i].interface = serialPort
  455. }
  456. let vzConfig = try config.appleVZConfiguration()
  457. vmQueue.async { [self] in
  458. apple = VZVirtualMachine(configuration: vzConfig, queue: vmQueue)
  459. apple!.delegate = self
  460. snapshotUnsupportedError = UTMAppleVirtualMachineError.operationNotAvailable
  461. #if arch(arm64)
  462. if #available(macOS 14, *) {
  463. do {
  464. try vzConfig.validateSaveRestoreSupport()
  465. snapshotUnsupportedError = nil
  466. } catch {
  467. // save this for later when we want to use snapshots
  468. snapshotUnsupportedError = error
  469. }
  470. }
  471. #endif
  472. }
  473. }
  474. @available(macOS 12, *)
  475. private func updateSharedDirectories(with newShares: [UTMAppleConfigurationSharedDirectory], tag: String) {
  476. guard let fsConfig = apple?.directorySharingDevices.first(where: { device in
  477. if let device = device as? VZVirtioFileSystemDevice {
  478. return device.tag == tag
  479. } else {
  480. return false
  481. }
  482. }) as? VZVirtioFileSystemDevice else {
  483. return
  484. }
  485. fsConfig.share = UTMAppleConfigurationSharedDirectory.makeDirectoryShare(from: newShares)
  486. }
  487. @available(macOS 12, *)
  488. func installVM(with ipswUrl: URL) async throws {
  489. guard state == .stopped else {
  490. return
  491. }
  492. state = .starting
  493. do {
  494. _ = ipswUrl.startAccessingSecurityScopedResource()
  495. defer {
  496. ipswUrl.stopAccessingSecurityScopedResource()
  497. }
  498. guard FileManager.default.isReadableFile(atPath: ipswUrl.path) else {
  499. throw UTMAppleVirtualMachineError.ipswNotReadable
  500. }
  501. try await beginAccessingResources()
  502. try await createAppleVM()
  503. #if os(macOS) && arch(arm64)
  504. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  505. vmQueue.async {
  506. guard let apple = self.apple else {
  507. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  508. return
  509. }
  510. let installer = VZMacOSInstaller(virtualMachine: apple, restoringFromImageAt: ipswUrl)
  511. self.progressObserver = installer.progress.observe(\.fractionCompleted, options: [.initial, .new]) { progress, change in
  512. self.delegate?.virtualMachine(self, didUpdateInstallationProgress: progress.fractionCompleted)
  513. }
  514. self.installProgress = installer.progress
  515. installer.install { result in
  516. continuation.resume(with: result)
  517. }
  518. }
  519. }
  520. state = .started
  521. progressObserver = nil
  522. installProgress = nil
  523. delegate?.virtualMachine(self, didCompleteInstallation: true)
  524. #else
  525. throw UTMAppleVirtualMachineError.operatingSystemInstallNotSupported
  526. #endif
  527. } catch {
  528. await stopAccesingResources()
  529. delegate?.virtualMachine(self, didCompleteInstallation: false)
  530. state = .stopped
  531. throw error
  532. }
  533. }
  534. // taken from https://github.com/evansm7/vftool/blob/main/vftool/main.m
  535. private func createPty() throws -> (Int32, Int32, String) {
  536. let errMsg = NSLocalizedString("Cannot create virtual terminal.", comment: "UTMAppleVirtualMachine")
  537. var mfd: Int32 = -1
  538. var sfd: Int32 = -1
  539. var cname = [CChar](repeating: 0, count: Int(PATH_MAX))
  540. var tos = termios()
  541. guard openpty(&mfd, &sfd, &cname, nil, nil) >= 0 else {
  542. logger.error("openpty failed: \(errno)")
  543. throw errMsg
  544. }
  545. guard tcgetattr(mfd, &tos) >= 0 else {
  546. logger.error("tcgetattr failed: \(errno)")
  547. throw errMsg
  548. }
  549. cfmakeraw(&tos)
  550. guard tcsetattr(mfd, TCSAFLUSH, &tos) >= 0 else {
  551. logger.error("tcsetattr failed: \(errno)")
  552. throw errMsg
  553. }
  554. let f = fcntl(mfd, F_GETFL)
  555. guard fcntl(mfd, F_SETFL, f | O_NONBLOCK) >= 0 else {
  556. logger.error("fnctl failed: \(errno)")
  557. throw errMsg
  558. }
  559. let name = String(cString: cname)
  560. logger.info("fd \(mfd) connected to \(name)")
  561. return (mfd, sfd, name)
  562. }
  563. @MainActor private func beginAccessingResources() throws {
  564. for i in config.drives.indices {
  565. let drive = config.drives[i]
  566. if let url = drive.imageURL, drive.isExternal {
  567. if url.startAccessingSecurityScopedResource() {
  568. activeResourceUrls[drive.id] = url
  569. } else {
  570. config.drives[i].imageURL = nil
  571. throw UTMAppleVirtualMachineError.cannotAccessResource(url)
  572. }
  573. }
  574. }
  575. for i in config.sharedDirectories.indices {
  576. let share = config.sharedDirectories[i]
  577. if let url = share.directoryURL {
  578. if url.startAccessingSecurityScopedResource() {
  579. activeResourceUrls[share.id.uuidString] = url
  580. } else {
  581. config.sharedDirectories[i].directoryURL = nil
  582. throw UTMAppleVirtualMachineError.cannotAccessResource(url)
  583. }
  584. }
  585. }
  586. }
  587. @MainActor private func stopAccesingResources() {
  588. for url in activeResourceUrls.values {
  589. url.stopAccessingSecurityScopedResource()
  590. }
  591. activeResourceUrls.removeAll()
  592. }
  593. }
  594. @available(macOS 11, *)
  595. extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
  596. func guestDidStop(_ virtualMachine: VZVirtualMachine) {
  597. vmQueue.async { [self] in
  598. apple = nil
  599. snapshotUnsupportedError = nil
  600. }
  601. removableDrives.removeAll()
  602. sharedDirectoriesChanged = nil
  603. Task { @MainActor in
  604. stopAccesingResources()
  605. for i in config.serials.indices {
  606. if let serialPort = config.serials[i].interface {
  607. serialPort.close()
  608. config.serials[i].interface = nil
  609. config.serials[i].fileHandleForReading = nil
  610. config.serials[i].fileHandleForWriting = nil
  611. }
  612. }
  613. }
  614. try? saveScreenshot()
  615. state = .stopped
  616. }
  617. func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
  618. guestDidStop(virtualMachine)
  619. delegate?.virtualMachine(self, didErrorWithMessage: error.localizedDescription)
  620. }
  621. // fake methods to adhere to NSObjectProtocol
  622. func isEqual(_ object: Any?) -> Bool {
  623. self === object as? UTMAppleVirtualMachine
  624. }
  625. var hash: Int {
  626. 0
  627. }
  628. var superclass: AnyClass? {
  629. nil
  630. }
  631. func `self`() -> Self {
  632. self
  633. }
  634. func perform(_ aSelector: Selector!) -> Unmanaged<AnyObject>! {
  635. nil
  636. }
  637. func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged<AnyObject>! {
  638. nil
  639. }
  640. func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged<AnyObject>! {
  641. nil
  642. }
  643. func isProxy() -> Bool {
  644. false
  645. }
  646. func isKind(of aClass: AnyClass) -> Bool {
  647. false
  648. }
  649. func isMember(of aClass: AnyClass) -> Bool {
  650. false
  651. }
  652. func conforms(to aProtocol: Protocol) -> Bool {
  653. aProtocol is VZVirtualMachineDelegate
  654. }
  655. func responds(to aSelector: Selector!) -> Bool {
  656. if aSelector == #selector(VZVirtualMachineDelegate.guestDidStop(_:)) {
  657. return true
  658. }
  659. if aSelector == #selector(VZVirtualMachineDelegate.virtualMachine(_:didStopWithError:)) {
  660. return true
  661. }
  662. return false
  663. }
  664. var description: String {
  665. ""
  666. }
  667. }
  668. @available(macOS 15, *)
  669. extension UTMAppleVirtualMachine {
  670. private func detachDrive(id: String) async throws {
  671. if let oldUrl = activeResourceUrls.removeValue(forKey: id) {
  672. oldUrl.stopAccessingSecurityScopedResource()
  673. }
  674. if let device = removableDrives.removeValue(forKey: id) as? any VZUSBDevice {
  675. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
  676. vmQueue.async {
  677. guard let apple = self.apple, let usbController = apple.usbControllers.first else {
  678. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  679. return
  680. }
  681. usbController.detach(device: device) { error in
  682. if let error = error {
  683. continuation.resume(throwing: error)
  684. } else {
  685. continuation.resume()
  686. }
  687. }
  688. }
  689. }
  690. }
  691. }
  692. /// Eject a removable drive
  693. /// - Parameter drive: Removable drive
  694. func eject(_ drive: UTMAppleConfigurationDrive) async throws {
  695. if state == .started {
  696. try await detachDrive(id: drive.id)
  697. }
  698. await registryEntry.removeExternalDrive(forId: drive.id)
  699. }
  700. private func attachDrive(_ drive: VZDiskImageStorageDeviceAttachment, imageURL: URL, id: String) async throws {
  701. if imageURL.startAccessingSecurityScopedResource() {
  702. activeResourceUrls[id] = imageURL
  703. }
  704. let configuration = VZUSBMassStorageDeviceConfiguration(attachment: drive)
  705. let device = VZUSBMassStorageDevice(configuration: configuration)
  706. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
  707. vmQueue.async {
  708. guard let apple = self.apple, let usbController = apple.usbControllers.first else {
  709. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  710. return
  711. }
  712. usbController.attach(device: device) { error in
  713. if let error = error {
  714. continuation.resume(throwing: error)
  715. } else {
  716. continuation.resume()
  717. }
  718. }
  719. }
  720. }
  721. removableDrives[id] = device
  722. }
  723. /// Change mount image of a removable drive
  724. /// - Parameters:
  725. /// - drive: Removable drive
  726. /// - url: New mount image
  727. func changeMedium(_ drive: UTMAppleConfigurationDrive, to url: URL) async throws {
  728. var newDrive = drive
  729. newDrive.imageURL = url
  730. let scopedAccess = url.startAccessingSecurityScopedResource()
  731. defer {
  732. if scopedAccess {
  733. url.stopAccessingSecurityScopedResource()
  734. }
  735. }
  736. let attachment = try newDrive.vzDiskImage()!
  737. if state == .started {
  738. try await detachDrive(id: drive.id)
  739. try await attachDrive(attachment, imageURL: url, id: drive.id)
  740. }
  741. let file = try UTMRegistryEntry.File(url: url)
  742. await registryEntry.setExternalDrive(file, forId: drive.id)
  743. }
  744. private func _attachExternalDrives(_ drives: [any VZUSBDevice]) -> (any Error)? {
  745. let group = DispatchGroup()
  746. var lastError: (any Error)?
  747. group.enter()
  748. vmQueue.async {
  749. defer {
  750. group.leave()
  751. }
  752. guard let apple = self.apple, let usbController = apple.usbControllers.first else {
  753. lastError = UTMAppleVirtualMachineError.operationNotAvailable
  754. return
  755. }
  756. for device in drives {
  757. group.enter()
  758. usbController.attach(device: device) { error in
  759. if let error = error {
  760. lastError = error
  761. }
  762. group.leave()
  763. }
  764. }
  765. }
  766. group.wait()
  767. return lastError
  768. }
  769. private func attachExternalDrives() async throws {
  770. let removableDrives = try await config.drives.reduce(into: [String: any VZUSBDevice]()) { devices, drive in
  771. guard drive.isExternal else {
  772. return
  773. }
  774. guard let attachment = try drive.vzDiskImage() else {
  775. return
  776. }
  777. let configuration = VZUSBMassStorageDeviceConfiguration(attachment: attachment)
  778. devices[drive.id] = VZUSBMassStorageDevice(configuration: configuration)
  779. }
  780. let drives = Array(removableDrives.values)
  781. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
  782. if let error = self._attachExternalDrives(drives) {
  783. continuation.resume(throwing: error)
  784. } else {
  785. continuation.resume()
  786. }
  787. }
  788. self.removableDrives = removableDrives
  789. }
  790. private var guestToolsId: String {
  791. "guest-tools"
  792. }
  793. var hasGuestToolsAttached: Bool {
  794. removableDrives.keys.contains(guestToolsId)
  795. }
  796. func attachGuestTools(_ imageURL: URL) async throws {
  797. try await detachDrive(id: guestToolsId)
  798. let scopedAccess = imageURL.startAccessingSecurityScopedResource()
  799. defer {
  800. if scopedAccess {
  801. imageURL.stopAccessingSecurityScopedResource()
  802. }
  803. }
  804. let attachment = try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: true)
  805. try await attachDrive(attachment, imageURL: imageURL, id: guestToolsId)
  806. }
  807. func detachGuestTools() async throws {
  808. try await detachDrive(id: guestToolsId)
  809. }
  810. }
  811. protocol UTMScreenshotProvider: AnyObject {
  812. var screenshot: UTMVirtualMachineScreenshot? { get }
  813. }
  814. enum UTMAppleVirtualMachineError: Error {
  815. case cannotAccessResource(URL)
  816. case operatingSystemInstallNotSupported
  817. case operationNotAvailable
  818. case ipswNotReadable
  819. }
  820. extension UTMAppleVirtualMachineError: LocalizedError {
  821. var errorDescription: String? {
  822. switch self {
  823. case .cannotAccessResource(let url):
  824. return String.localizedStringWithFormat(NSLocalizedString("Cannot access resource: %@", comment: "UTMAppleVirtualMachine"), url.path)
  825. case .operatingSystemInstallNotSupported:
  826. return NSLocalizedString("The operating system cannot be installed on this machine.", comment: "UTMAppleVirtualMachine")
  827. case .operationNotAvailable:
  828. return NSLocalizedString("The operation is not available.", comment: "UTMAppleVirtualMachine")
  829. case .ipswNotReadable:
  830. return NSLocalizedString("The recovery IPSW cannot be read. Please select a new IPSW in Boot settings.", comment: "UTMAppleVirtualMachine")
  831. }
  832. }
  833. }
  834. // MARK: - Registry access
  835. extension UTMAppleVirtualMachine {
  836. @MainActor func updateRegistryFromConfig() async throws {
  837. // save a copy to not collide with updateConfigFromRegistry()
  838. let configShares = config.sharedDirectories
  839. let configDrives = config.drives
  840. try await updateRegistryBasics()
  841. registryEntry.sharedDirectories.removeAll(keepingCapacity: true)
  842. for sharedDirectory in configShares {
  843. if let url = sharedDirectory.directoryURL {
  844. let file = try UTMRegistryEntry.File(url: url, isReadOnly: sharedDirectory.isReadOnly)
  845. registryEntry.sharedDirectories.append(file)
  846. }
  847. }
  848. for drive in configDrives {
  849. if drive.isExternal, let url = drive.imageURL {
  850. let file = try UTMRegistryEntry.File(url: url, isReadOnly: drive.isReadOnly)
  851. registryEntry.externalDrives[drive.id] = file
  852. } else if drive.isExternal {
  853. registryEntry.externalDrives.removeValue(forKey: drive.id)
  854. }
  855. }
  856. // remove any unreferenced drives
  857. registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in
  858. configDrives.contains(where: { $0.id == element.key && $0.isExternal })
  859. })
  860. // save IPSW reference
  861. if let url = config.system.boot.macRecoveryIpswURL {
  862. registryEntry.macRecoveryIpsw = try UTMRegistryEntry.File(url: url, isReadOnly: true)
  863. } else {
  864. registryEntry.macRecoveryIpsw = nil
  865. }
  866. }
  867. @MainActor func updateConfigFromRegistry() {
  868. config.sharedDirectories = registryEntry.sharedDirectories.map({ UTMAppleConfigurationSharedDirectory(directoryURL: $0.url, isReadOnly: $0.isReadOnly )})
  869. for i in config.drives.indices {
  870. let id = config.drives[i].id
  871. if config.drives[i].isExternal {
  872. config.drives[i].imageURL = registryEntry.externalDrives[id]?.url
  873. }
  874. }
  875. if let file = registryEntry.macRecoveryIpsw {
  876. config.system.boot.macRecoveryIpswURL = file.url
  877. }
  878. }
  879. @MainActor func changeUuid(to uuid: UUID, name: String? = nil, copyingEntry entry: UTMRegistryEntry? = nil) {
  880. config.information.uuid = uuid
  881. if let name = name {
  882. config.information.name = name
  883. }
  884. registryEntry = UTMRegistry.shared.entry(for: self)
  885. if let entry = entry {
  886. registryEntry.update(copying: entry)
  887. }
  888. }
  889. }
  890. // MARK: - Non-asynchronous version (to be removed)
  891. extension UTMAppleVirtualMachine {
  892. @available(macOS 12, *)
  893. func requestInstallVM(with url: URL) {
  894. Task {
  895. do {
  896. try await installVM(with: url)
  897. } catch {
  898. delegate?.virtualMachine(self, didErrorWithMessage: error.localizedDescription)
  899. }
  900. }
  901. }
  902. }