UTMAppleVirtualMachine.swift 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  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. if self.isScreenshotEnabled {
  288. Task { @MainActor in
  289. await self.takeScreenshot()
  290. try? self.saveScreenshot()
  291. }
  292. }
  293. apple.pause { result in
  294. continuation.resume(with: result)
  295. }
  296. }
  297. }
  298. }
  299. func pause() async throws {
  300. guard state == .started else {
  301. return
  302. }
  303. state = .pausing
  304. do {
  305. try await _pause()
  306. state = .paused
  307. } catch {
  308. state = .stopped
  309. throw error
  310. }
  311. }
  312. #if arch(arm64)
  313. @available(macOS 14, *)
  314. private func _saveSnapshot(url: URL) async throws {
  315. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  316. vmQueue.async {
  317. guard let apple = self.apple else {
  318. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  319. return
  320. }
  321. apple.saveMachineStateTo(url: url) { error in
  322. if let error = error {
  323. continuation.resume(throwing: error)
  324. } else {
  325. continuation.resume()
  326. }
  327. }
  328. }
  329. }
  330. try? updateLastModified()
  331. }
  332. #endif
  333. func saveSnapshot(name: String? = nil) async throws {
  334. guard #available(macOS 14, *) else {
  335. return
  336. }
  337. #if arch(arm64)
  338. guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
  339. return
  340. }
  341. if let snapshotUnsupportedError = snapshotUnsupportedError {
  342. throw snapshotUnsupportedError
  343. }
  344. if state == .started {
  345. try await pause()
  346. }
  347. guard state == .paused else {
  348. return
  349. }
  350. state = .saving
  351. defer {
  352. state = .paused
  353. }
  354. try await _saveSnapshot(url: vmSavedStateURL)
  355. await registryEntry.setIsSuspended(true)
  356. #endif
  357. }
  358. func deleteSnapshot(name: String? = nil) async throws {
  359. guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
  360. return
  361. }
  362. await registryEntry.setIsSuspended(false)
  363. try FileManager.default.removeItem(at: vmSavedStateURL)
  364. try? updateLastModified()
  365. }
  366. #if arch(arm64)
  367. @available(macOS 14, *)
  368. private func _restoreSnapshot(url: URL) async throws {
  369. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  370. vmQueue.async {
  371. guard let apple = self.apple else {
  372. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  373. return
  374. }
  375. apple.restoreMachineStateFrom(url: url) { error in
  376. if let error = error {
  377. continuation.resume(throwing: error)
  378. } else {
  379. continuation.resume()
  380. }
  381. }
  382. }
  383. }
  384. }
  385. #endif
  386. func restoreSnapshot(name: String? = nil) async throws {
  387. guard #available(macOS 14, *) else {
  388. throw UTMAppleVirtualMachineError.operationNotAvailable
  389. }
  390. #if arch(arm64)
  391. guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
  392. throw UTMAppleVirtualMachineError.operationNotAvailable
  393. }
  394. if state == .started {
  395. try await stop(usingMethod: .force)
  396. }
  397. guard state == .stopped || state == .starting else {
  398. throw UTMAppleVirtualMachineError.operationNotAvailable
  399. }
  400. state = .restoring
  401. do {
  402. try await _restoreSnapshot(url: vmSavedStateURL)
  403. try await _resume()
  404. } catch {
  405. state = .stopped
  406. throw error
  407. }
  408. state = .started
  409. try await deleteSnapshot(name: name)
  410. #else
  411. throw UTMAppleVirtualMachineError.operationNotAvailable
  412. #endif
  413. }
  414. private func _resume() async throws {
  415. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  416. vmQueue.async {
  417. guard let apple = self.apple else {
  418. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  419. return
  420. }
  421. apple.resume { result in
  422. continuation.resume(with: result)
  423. }
  424. }
  425. }
  426. }
  427. func resume() async throws {
  428. guard state == .paused else {
  429. return
  430. }
  431. state = .resuming
  432. do {
  433. try await _resume()
  434. state = .started
  435. } catch {
  436. state = .stopped
  437. throw error
  438. }
  439. }
  440. @discardableResult @MainActor
  441. func takeScreenshot() async -> Bool {
  442. screenshot = screenshotDelegate?.screenshot
  443. return true
  444. }
  445. func reloadScreenshotFromFile() {
  446. screenshot = loadScreenshot()
  447. }
  448. @MainActor private func createAppleVM() throws {
  449. for i in config.serials.indices {
  450. let (fd, sfd, name) = try createPty()
  451. let terminalTtyHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: false)
  452. let slaveTtyHandle = FileHandle(fileDescriptor: sfd, closeOnDealloc: false)
  453. config.serials[i].fileHandleForReading = terminalTtyHandle
  454. config.serials[i].fileHandleForWriting = terminalTtyHandle
  455. let serialPort = UTMSerialPort(portNamed: name, readFileHandle: slaveTtyHandle, writeFileHandle: slaveTtyHandle, terminalFileHandle: terminalTtyHandle)
  456. config.serials[i].interface = serialPort
  457. }
  458. let vzConfig = try config.appleVZConfiguration()
  459. vmQueue.async { [self] in
  460. apple = VZVirtualMachine(configuration: vzConfig, queue: vmQueue)
  461. apple!.delegate = self
  462. snapshotUnsupportedError = UTMAppleVirtualMachineError.operationNotAvailable
  463. #if arch(arm64)
  464. if #available(macOS 14, *) {
  465. do {
  466. try vzConfig.validateSaveRestoreSupport()
  467. snapshotUnsupportedError = nil
  468. } catch {
  469. // save this for later when we want to use snapshots
  470. snapshotUnsupportedError = error
  471. }
  472. }
  473. #endif
  474. }
  475. }
  476. @available(macOS 12, *)
  477. private func updateSharedDirectories(with newShares: [UTMAppleConfigurationSharedDirectory], tag: String) {
  478. guard let fsConfig = apple?.directorySharingDevices.first(where: { device in
  479. if let device = device as? VZVirtioFileSystemDevice {
  480. return device.tag == tag
  481. } else {
  482. return false
  483. }
  484. }) as? VZVirtioFileSystemDevice else {
  485. return
  486. }
  487. fsConfig.share = UTMAppleConfigurationSharedDirectory.makeDirectoryShare(from: newShares)
  488. }
  489. @available(macOS 12, *)
  490. func installVM(with ipswUrl: URL) async throws {
  491. guard state == .stopped else {
  492. return
  493. }
  494. state = .starting
  495. do {
  496. _ = ipswUrl.startAccessingSecurityScopedResource()
  497. defer {
  498. ipswUrl.stopAccessingSecurityScopedResource()
  499. }
  500. guard FileManager.default.isReadableFile(atPath: ipswUrl.path) else {
  501. throw UTMAppleVirtualMachineError.ipswNotReadable
  502. }
  503. try await beginAccessingResources()
  504. try await createAppleVM()
  505. #if os(macOS) && arch(arm64)
  506. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  507. vmQueue.async {
  508. guard let apple = self.apple else {
  509. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  510. return
  511. }
  512. let installer = VZMacOSInstaller(virtualMachine: apple, restoringFromImageAt: ipswUrl)
  513. self.progressObserver = installer.progress.observe(\.fractionCompleted, options: [.initial, .new]) { progress, change in
  514. self.delegate?.virtualMachine(self, didUpdateInstallationProgress: progress.fractionCompleted)
  515. }
  516. self.installProgress = installer.progress
  517. installer.install { result in
  518. continuation.resume(with: result)
  519. }
  520. }
  521. }
  522. state = .started
  523. progressObserver = nil
  524. installProgress = nil
  525. delegate?.virtualMachine(self, didCompleteInstallation: true)
  526. #else
  527. throw UTMAppleVirtualMachineError.operatingSystemInstallNotSupported
  528. #endif
  529. } catch {
  530. await stopAccesingResources()
  531. delegate?.virtualMachine(self, didCompleteInstallation: false)
  532. state = .stopped
  533. let error = error as NSError
  534. if error.domain == "VZErrorDomain" && error.code == 10006 {
  535. throw UTMAppleVirtualMachineError.deviceSupportOutdated
  536. }
  537. throw error
  538. }
  539. }
  540. // taken from https://github.com/evansm7/vftool/blob/main/vftool/main.m
  541. private func createPty() throws -> (Int32, Int32, String) {
  542. let errMsg = NSLocalizedString("Cannot create virtual terminal.", comment: "UTMAppleVirtualMachine")
  543. var mfd: Int32 = -1
  544. var sfd: Int32 = -1
  545. var cname = [CChar](repeating: 0, count: Int(PATH_MAX))
  546. var tos = termios()
  547. guard openpty(&mfd, &sfd, &cname, nil, nil) >= 0 else {
  548. logger.error("openpty failed: \(errno)")
  549. throw errMsg
  550. }
  551. guard tcgetattr(mfd, &tos) >= 0 else {
  552. logger.error("tcgetattr failed: \(errno)")
  553. throw errMsg
  554. }
  555. cfmakeraw(&tos)
  556. guard tcsetattr(mfd, TCSAFLUSH, &tos) >= 0 else {
  557. logger.error("tcsetattr failed: \(errno)")
  558. throw errMsg
  559. }
  560. let f = fcntl(mfd, F_GETFL)
  561. guard fcntl(mfd, F_SETFL, f | O_NONBLOCK) >= 0 else {
  562. logger.error("fnctl failed: \(errno)")
  563. throw errMsg
  564. }
  565. let name = String(cString: cname)
  566. logger.info("fd \(mfd) connected to \(name)")
  567. return (mfd, sfd, name)
  568. }
  569. @MainActor private func beginAccessingResources() throws {
  570. for i in config.drives.indices {
  571. let drive = config.drives[i]
  572. if let url = drive.imageURL, drive.isExternal {
  573. if url.startAccessingSecurityScopedResource() {
  574. activeResourceUrls[drive.id] = url
  575. } else {
  576. config.drives[i].imageURL = nil
  577. throw UTMAppleVirtualMachineError.cannotAccessResource(url)
  578. }
  579. }
  580. }
  581. for i in config.sharedDirectories.indices {
  582. let share = config.sharedDirectories[i]
  583. if let url = share.directoryURL {
  584. if url.startAccessingSecurityScopedResource() {
  585. activeResourceUrls[share.id.uuidString] = url
  586. } else {
  587. config.sharedDirectories[i].directoryURL = nil
  588. throw UTMAppleVirtualMachineError.cannotAccessResource(url)
  589. }
  590. }
  591. }
  592. }
  593. @MainActor private func stopAccesingResources() {
  594. for url in activeResourceUrls.values {
  595. url.stopAccessingSecurityScopedResource()
  596. }
  597. activeResourceUrls.removeAll()
  598. }
  599. }
  600. @available(macOS 11, *)
  601. extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
  602. func guestDidStop(_ virtualMachine: VZVirtualMachine) {
  603. vmQueue.async { [self] in
  604. apple = nil
  605. snapshotUnsupportedError = nil
  606. }
  607. removableDrives.removeAll()
  608. sharedDirectoriesChanged = nil
  609. Task { @MainActor in
  610. stopAccesingResources()
  611. for i in config.serials.indices {
  612. if let serialPort = config.serials[i].interface {
  613. serialPort.close()
  614. config.serials[i].interface = nil
  615. config.serials[i].fileHandleForReading = nil
  616. config.serials[i].fileHandleForWriting = nil
  617. }
  618. }
  619. }
  620. try? saveScreenshot()
  621. state = .stopped
  622. }
  623. func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
  624. guestDidStop(virtualMachine)
  625. delegate?.virtualMachine(self, didErrorWithMessage: error.localizedDescription)
  626. }
  627. // fake methods to adhere to NSObjectProtocol
  628. func isEqual(_ object: Any?) -> Bool {
  629. self === object as? UTMAppleVirtualMachine
  630. }
  631. var hash: Int {
  632. 0
  633. }
  634. var superclass: AnyClass? {
  635. nil
  636. }
  637. func `self`() -> Self {
  638. self
  639. }
  640. func perform(_ aSelector: Selector!) -> Unmanaged<AnyObject>! {
  641. nil
  642. }
  643. func perform(_ aSelector: Selector!, with object: Any!) -> Unmanaged<AnyObject>! {
  644. nil
  645. }
  646. func perform(_ aSelector: Selector!, with object1: Any!, with object2: Any!) -> Unmanaged<AnyObject>! {
  647. nil
  648. }
  649. func isProxy() -> Bool {
  650. false
  651. }
  652. func isKind(of aClass: AnyClass) -> Bool {
  653. false
  654. }
  655. func isMember(of aClass: AnyClass) -> Bool {
  656. false
  657. }
  658. func conforms(to aProtocol: Protocol) -> Bool {
  659. aProtocol is VZVirtualMachineDelegate
  660. }
  661. func responds(to aSelector: Selector!) -> Bool {
  662. if aSelector == #selector(VZVirtualMachineDelegate.guestDidStop(_:)) {
  663. return true
  664. }
  665. if aSelector == #selector(VZVirtualMachineDelegate.virtualMachine(_:didStopWithError:)) {
  666. return true
  667. }
  668. return false
  669. }
  670. var description: String {
  671. ""
  672. }
  673. }
  674. @available(macOS 15, *)
  675. extension UTMAppleVirtualMachine {
  676. private func detachDrive(id: String) async throws {
  677. if let oldUrl = activeResourceUrls.removeValue(forKey: id) {
  678. oldUrl.stopAccessingSecurityScopedResource()
  679. }
  680. if let device = removableDrives.removeValue(forKey: id) as? any VZUSBDevice {
  681. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
  682. vmQueue.async {
  683. guard let apple = self.apple, let usbController = apple.usbControllers.first else {
  684. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  685. return
  686. }
  687. usbController.detach(device: device) { error in
  688. if let error = error {
  689. continuation.resume(throwing: error)
  690. } else {
  691. continuation.resume()
  692. }
  693. }
  694. }
  695. }
  696. }
  697. }
  698. /// Eject a removable drive
  699. /// - Parameter drive: Removable drive
  700. func eject(_ drive: UTMAppleConfigurationDrive) async throws {
  701. if state == .started {
  702. try await detachDrive(id: drive.id)
  703. }
  704. await registryEntry.removeExternalDrive(forId: drive.id)
  705. }
  706. private func attachDrive(_ drive: VZDiskImageStorageDeviceAttachment, imageURL: URL, id: String) async throws {
  707. if imageURL.startAccessingSecurityScopedResource() {
  708. activeResourceUrls[id] = imageURL
  709. }
  710. let configuration = VZUSBMassStorageDeviceConfiguration(attachment: drive)
  711. let device = VZUSBMassStorageDevice(configuration: configuration)
  712. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
  713. vmQueue.async {
  714. guard let apple = self.apple, let usbController = apple.usbControllers.first else {
  715. continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
  716. return
  717. }
  718. usbController.attach(device: device) { error in
  719. if let error = error {
  720. continuation.resume(throwing: error)
  721. } else {
  722. continuation.resume()
  723. }
  724. }
  725. }
  726. }
  727. removableDrives[id] = device
  728. }
  729. /// Change mount image of a removable drive
  730. /// - Parameters:
  731. /// - drive: Removable drive
  732. /// - url: New mount image
  733. func changeMedium(_ drive: UTMAppleConfigurationDrive, to url: URL) async throws {
  734. var newDrive = drive
  735. newDrive.imageURL = url
  736. let scopedAccess = url.startAccessingSecurityScopedResource()
  737. defer {
  738. if scopedAccess {
  739. url.stopAccessingSecurityScopedResource()
  740. }
  741. }
  742. let attachment = try newDrive.vzDiskImage()!
  743. if state == .started {
  744. try await detachDrive(id: drive.id)
  745. try await attachDrive(attachment, imageURL: url, id: drive.id)
  746. }
  747. let file = try UTMRegistryEntry.File(url: url)
  748. await registryEntry.setExternalDrive(file, forId: drive.id)
  749. }
  750. private func _attachExternalDrives(_ drives: [any VZUSBDevice]) -> (any Error)? {
  751. let group = DispatchGroup()
  752. var lastError: (any Error)?
  753. group.enter()
  754. vmQueue.async {
  755. defer {
  756. group.leave()
  757. }
  758. guard let apple = self.apple, let usbController = apple.usbControllers.first else {
  759. lastError = UTMAppleVirtualMachineError.operationNotAvailable
  760. return
  761. }
  762. for device in drives {
  763. group.enter()
  764. usbController.attach(device: device) { error in
  765. if let error = error {
  766. lastError = error
  767. }
  768. group.leave()
  769. }
  770. }
  771. }
  772. group.wait()
  773. return lastError
  774. }
  775. private func attachExternalDrives() async throws {
  776. let removableDrives = try await config.drives.reduce(into: [String: any VZUSBDevice]()) { devices, drive in
  777. guard drive.isExternal else {
  778. return
  779. }
  780. guard let attachment = try drive.vzDiskImage() else {
  781. return
  782. }
  783. let configuration = VZUSBMassStorageDeviceConfiguration(attachment: attachment)
  784. devices[drive.id] = VZUSBMassStorageDevice(configuration: configuration)
  785. }
  786. let drives = Array(removableDrives.values)
  787. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
  788. if let error = self._attachExternalDrives(drives) {
  789. continuation.resume(throwing: error)
  790. } else {
  791. continuation.resume()
  792. }
  793. }
  794. self.removableDrives = removableDrives
  795. }
  796. private var guestToolsId: String {
  797. "guest-tools"
  798. }
  799. var hasGuestToolsAttached: Bool {
  800. removableDrives.keys.contains(guestToolsId)
  801. }
  802. func attachGuestTools(_ imageURL: URL) async throws {
  803. try await detachDrive(id: guestToolsId)
  804. let scopedAccess = imageURL.startAccessingSecurityScopedResource()
  805. defer {
  806. if scopedAccess {
  807. imageURL.stopAccessingSecurityScopedResource()
  808. }
  809. }
  810. let attachment = try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: true)
  811. try await attachDrive(attachment, imageURL: imageURL, id: guestToolsId)
  812. }
  813. func detachGuestTools() async throws {
  814. try await detachDrive(id: guestToolsId)
  815. }
  816. }
  817. protocol UTMScreenshotProvider: AnyObject {
  818. var screenshot: UTMVirtualMachineScreenshot? { get }
  819. }
  820. enum UTMAppleVirtualMachineError: Error {
  821. case cannotAccessResource(URL)
  822. case operatingSystemInstallNotSupported
  823. case operationNotAvailable
  824. case ipswNotReadable
  825. case deviceSupportOutdated
  826. }
  827. extension UTMAppleVirtualMachineError: LocalizedError {
  828. var errorDescription: String? {
  829. switch self {
  830. case .cannotAccessResource(let url):
  831. return String.localizedStringWithFormat(NSLocalizedString("Cannot access resource: %@", comment: "UTMAppleVirtualMachine"), url.path)
  832. case .operatingSystemInstallNotSupported:
  833. return NSLocalizedString("The operating system cannot be installed on this machine.", comment: "UTMAppleVirtualMachine")
  834. case .operationNotAvailable:
  835. return NSLocalizedString("The operation is not available.", comment: "UTMAppleVirtualMachine")
  836. case .ipswNotReadable:
  837. return NSLocalizedString("The recovery IPSW cannot be read. Please select a new IPSW in Boot settings.", comment: "UTMAppleVirtualMachine")
  838. case .deviceSupportOutdated:
  839. return NSLocalizedString("You need to update macOS to run this virtual machine. A separate pop-up should prompt you to install this update. If you are trying to install a new beta version of macOS, you must manually download the Device Support package from the Apple Developer website.", comment: "UTMAppleVirtualMachine")
  840. }
  841. }
  842. }
  843. // MARK: - Registry access
  844. extension UTMAppleVirtualMachine {
  845. @MainActor func updateRegistryFromConfig() async throws {
  846. // save a copy to not collide with updateConfigFromRegistry()
  847. let configShares = config.sharedDirectories
  848. let configDrives = config.drives
  849. try await updateRegistryBasics()
  850. registryEntry.sharedDirectories.removeAll(keepingCapacity: true)
  851. for sharedDirectory in configShares {
  852. if let url = sharedDirectory.directoryURL {
  853. let file = try UTMRegistryEntry.File(url: url, isReadOnly: sharedDirectory.isReadOnly)
  854. registryEntry.sharedDirectories.append(file)
  855. }
  856. }
  857. for drive in configDrives {
  858. if drive.isExternal, let url = drive.imageURL {
  859. let file = try UTMRegistryEntry.File(url: url, isReadOnly: drive.isReadOnly)
  860. registryEntry.externalDrives[drive.id] = file
  861. } else if drive.isExternal {
  862. registryEntry.externalDrives.removeValue(forKey: drive.id)
  863. }
  864. }
  865. // remove any unreferenced drives
  866. registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in
  867. configDrives.contains(where: { $0.id == element.key && $0.isExternal })
  868. })
  869. // save IPSW reference
  870. if let url = config.system.boot.macRecoveryIpswURL {
  871. registryEntry.macRecoveryIpsw = try UTMRegistryEntry.File(url: url, isReadOnly: true)
  872. } else {
  873. registryEntry.macRecoveryIpsw = nil
  874. }
  875. }
  876. @MainActor func updateConfigFromRegistry() {
  877. config.sharedDirectories = registryEntry.sharedDirectories.map({ UTMAppleConfigurationSharedDirectory(directoryURL: $0.url, isReadOnly: $0.isReadOnly )})
  878. for i in config.drives.indices {
  879. let id = config.drives[i].id
  880. if config.drives[i].isExternal {
  881. config.drives[i].imageURL = registryEntry.externalDrives[id]?.url
  882. }
  883. }
  884. if let file = registryEntry.macRecoveryIpsw {
  885. config.system.boot.macRecoveryIpswURL = file.url
  886. }
  887. }
  888. @MainActor func changeUuid(to uuid: UUID, name: String? = nil, copyingEntry entry: UTMRegistryEntry? = nil) {
  889. config.information.uuid = uuid
  890. if let name = name {
  891. config.information.name = name
  892. }
  893. registryEntry = UTMRegistry.shared.entry(for: self)
  894. if let entry = entry {
  895. registryEntry.update(copying: entry)
  896. }
  897. }
  898. }
  899. // MARK: - Non-asynchronous version (to be removed)
  900. extension UTMAppleVirtualMachine {
  901. @available(macOS 12, *)
  902. func requestInstallVM(with url: URL) {
  903. Task {
  904. do {
  905. try await installVM(with: url)
  906. } catch {
  907. delegate?.virtualMachine(self, didErrorWithMessage: error.localizedDescription)
  908. }
  909. }
  910. }
  911. }