VMDisplayQemuDisplayController.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  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. class VMDisplayQemuWindowController: VMDisplayWindowController {
  17. private(set) var id: Int = 0
  18. private weak var vmUsbManager: CSUSBManager?
  19. private var allUsbDevices: [CSUSBDevice] = []
  20. private var connectedUsbDevices: [CSUSBDevice] = []
  21. @Setting("NoUsbPrompt") private var isNoUsbPrompt: Bool = false
  22. var qemuVM: UTMQemuVirtualMachine! {
  23. vm as? UTMQemuVirtualMachine
  24. }
  25. var vmQemuConfig: UTMQemuConfiguration! {
  26. qemuVM?.config
  27. }
  28. var defaultTitle: String {
  29. vmQemuConfig.information.name
  30. }
  31. var defaultSubtitle: String {
  32. if qemuVM.isRunningAsDisposible {
  33. return NSLocalizedString("Disposable Mode", comment: "VMDisplayQemuDisplayController")
  34. } else {
  35. return ""
  36. }
  37. }
  38. convenience init(vm: UTMQemuVirtualMachine, id: Int) {
  39. self.init(vm: vm, onClose: nil)
  40. self.id = id
  41. }
  42. override func enterLive() {
  43. if !isSecondary {
  44. qemuVM.ioServiceDelegate = self
  45. }
  46. drivesToolbarItem.isEnabled = vmQemuConfig.drives.count > 0
  47. sharedFolderToolbarItem.isEnabled = vmQemuConfig.sharing.directoryShareMode == .webdav // virtfs cannot dynamically change
  48. usbToolbarItem.isEnabled = qemuVM.hasUsbRedirection
  49. window!.title = defaultTitle
  50. window!.subtitle = defaultSubtitle
  51. super.enterLive()
  52. }
  53. override func enterSuspended(isBusy busy: Bool) {
  54. if vm.state == .stopped {
  55. connectedUsbDevices.removeAll()
  56. allUsbDevices.removeAll()
  57. if isSecondary {
  58. close()
  59. }
  60. }
  61. super.enterSuspended(isBusy: busy)
  62. }
  63. }
  64. // MARK: - Removable drives
  65. @objc extension VMDisplayQemuWindowController {
  66. @IBAction override func drivesButtonPressed(_ sender: Any) {
  67. let menu = NSMenu()
  68. menu.autoenablesItems = false
  69. let item = NSMenuItem()
  70. item.title = NSLocalizedString("Querying drives status...", comment: "VMDisplayWindowController")
  71. item.isEnabled = false
  72. menu.addItem(item)
  73. updateDrivesMenu(menu, drives: vmQemuConfig.drives)
  74. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  75. }
  76. @nonobjc func updateDrivesMenu(_ menu: NSMenu, drives: [UTMQemuConfigurationDrive]) {
  77. menu.removeAllItems()
  78. if drives.count == 0 {
  79. let item = NSMenuItem()
  80. item.title = NSLocalizedString("No drives connected.", comment: "VMDisplayWindowController")
  81. item.isEnabled = false
  82. menu.addItem(item)
  83. } else {
  84. let item = NSMenuItem()
  85. item.title = NSLocalizedString("Install Windows Guest Tools…", comment: "VMDisplayWindowController")
  86. item.isEnabled = true
  87. item.target = self
  88. item.action = #selector(installWindowsGuestTools)
  89. menu.addItem(item)
  90. }
  91. for i in drives.indices {
  92. let drive = drives[i]
  93. if drive.imageType != .disk && drive.imageType != .cd && !drive.isExternal {
  94. continue // skip non-disks
  95. }
  96. let item = NSMenuItem()
  97. item.title = label(for: drive)
  98. if !drive.isExternal {
  99. item.isEnabled = false
  100. } else {
  101. let submenu = NSMenu()
  102. submenu.autoenablesItems = false
  103. let eject = NSMenuItem(title: NSLocalizedString("Eject", comment: "VMDisplayWindowController"),
  104. action: #selector(ejectDrive),
  105. keyEquivalent: "")
  106. eject.target = self
  107. eject.tag = i
  108. eject.isEnabled = qemuVM.externalImageURL(for: drive) != nil
  109. submenu.addItem(eject)
  110. let change = NSMenuItem(title: NSLocalizedString("Change", comment: "VMDisplayWindowController"),
  111. action: #selector(changeDriveImage),
  112. keyEquivalent: "")
  113. change.target = self
  114. change.tag = i
  115. change.isEnabled = true
  116. submenu.addItem(change)
  117. item.submenu = submenu
  118. }
  119. menu.addItem(item)
  120. }
  121. menu.update()
  122. }
  123. func ejectDrive(sender: AnyObject) {
  124. guard let menu = sender as? NSMenuItem else {
  125. logger.error("wrong sender for ejectDrive")
  126. return
  127. }
  128. let drive = vmQemuConfig.drives[menu.tag]
  129. Task.detached(priority: .background) { [self] in
  130. do {
  131. try await qemuVM.eject(drive)
  132. } catch {
  133. Task { @MainActor in
  134. showErrorAlert(error.localizedDescription)
  135. }
  136. }
  137. }
  138. }
  139. func openDriveImage(forDriveIndex index: Int) {
  140. let drive = vmQemuConfig.drives[index]
  141. let openPanel = NSOpenPanel()
  142. openPanel.title = NSLocalizedString("Select Drive Image", comment: "VMDisplayWindowController")
  143. openPanel.allowedContentTypes = [.data]
  144. openPanel.beginSheetModal(for: window!) { response in
  145. guard response == .OK else {
  146. return
  147. }
  148. guard let url = openPanel.url else {
  149. logger.debug("no file selected")
  150. return
  151. }
  152. Task.detached(priority: .background) { [self] in
  153. do {
  154. try await qemuVM.changeMedium(drive, to: url)
  155. } catch {
  156. Task { @MainActor in
  157. showErrorAlert(error.localizedDescription)
  158. }
  159. }
  160. }
  161. }
  162. }
  163. func changeDriveImage(sender: AnyObject) {
  164. guard let menu = sender as? NSMenuItem else {
  165. logger.error("wrong sender for ejectDrive")
  166. return
  167. }
  168. openDriveImage(forDriveIndex: menu.tag)
  169. }
  170. @nonobjc private func label(for drive: UTMQemuConfigurationDrive) -> String {
  171. let imageURL = qemuVM.externalImageURL(for: drive) ?? drive.imageURL
  172. return String.localizedStringWithFormat(NSLocalizedString("%@ (%@): %@", comment: "VMDisplayQemuDisplayController"),
  173. drive.imageType.prettyValue,
  174. drive.interface.prettyValue,
  175. imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "VMDisplayQemuDisplayController"))
  176. }
  177. @MainActor private func installWindowsGuestTools(sender: AnyObject) {
  178. NotificationCenter.default.post(name: NSNotification.InstallGuestTools, object: self.qemuVM)
  179. }
  180. }
  181. // MARK: - Shared folders
  182. extension VMDisplayQemuWindowController {
  183. @IBAction override func sharedFolderButtonPressed(_ sender: Any) {
  184. let openPanel = NSOpenPanel()
  185. openPanel.title = NSLocalizedString("Select Shared Folder", comment: "VMDisplayWindowController")
  186. openPanel.canChooseDirectories = true
  187. openPanel.canChooseFiles = false
  188. openPanel.beginSheetModal(for: window!) { response in
  189. guard response == .OK else {
  190. return
  191. }
  192. guard let url = openPanel.url else {
  193. logger.debug("no directory selected")
  194. return
  195. }
  196. Task.detached(priority: .background) { [self] in
  197. do {
  198. try await self.qemuVM.changeSharedDirectory(to: url)
  199. } catch {
  200. Task { @MainActor in
  201. self.showErrorAlert(error.localizedDescription)
  202. }
  203. }
  204. }
  205. }
  206. }
  207. }
  208. // MARK: - SPICE base implementation
  209. extension VMDisplayQemuWindowController: UTMSpiceIODelegate {
  210. private func configIdForSerial(_ serial: CSPort) -> Int? {
  211. let prefix = "com.utmapp.terminal."
  212. guard serial.name?.hasPrefix(prefix) ?? false else {
  213. return nil
  214. }
  215. return Int(serial.name!.dropFirst(prefix.count))
  216. }
  217. func spiceDidCreateInput(_ input: CSInput) {
  218. for subwindow in secondaryWindows {
  219. (subwindow as! VMDisplayQemuWindowController).spiceDidCreateInput(input)
  220. }
  221. }
  222. func spiceDidDestroyInput(_ input: CSInput) {
  223. for subwindow in secondaryWindows {
  224. (subwindow as! VMDisplayQemuWindowController).spiceDidDestroyInput(input)
  225. }
  226. }
  227. func spiceDidCreateDisplay(_ display: CSDisplay) {
  228. guard !isSecondary else {
  229. return
  230. }
  231. Task { @MainActor in
  232. findWindow(for: display)
  233. }
  234. }
  235. func spiceDidUpdateDisplay(_ display: CSDisplay) {
  236. for subwindow in secondaryWindows {
  237. (subwindow as! VMDisplayQemuWindowController).spiceDidUpdateDisplay(display)
  238. }
  239. }
  240. func spiceDidDestroyDisplay(_ display: CSDisplay) {
  241. for subwindow in secondaryWindows {
  242. (subwindow as! VMDisplayQemuWindowController).spiceDidDestroyDisplay(display)
  243. }
  244. }
  245. func spiceDidChangeUsbManager(_ usbManager: CSUSBManager?) {
  246. if usbManager != vmUsbManager {
  247. connectedUsbDevices.removeAll()
  248. allUsbDevices.removeAll()
  249. vmUsbManager = usbManager
  250. if let usbManager = usbManager {
  251. usbManager.delegate = self
  252. autoConnectUsbDevices()
  253. }
  254. }
  255. for subwindow in secondaryWindows {
  256. (subwindow as! VMDisplayQemuWindowController).spiceDidChangeUsbManager(usbManager)
  257. }
  258. }
  259. func spiceDynamicResolutionSupportDidChange(_ supported: Bool) {
  260. for subwindow in secondaryWindows {
  261. (subwindow as! VMDisplayQemuWindowController).spiceDynamicResolutionSupportDidChange(supported)
  262. }
  263. }
  264. func spiceDidCreateSerial(_ serial: CSPort) {
  265. guard !isSecondary else {
  266. return
  267. }
  268. Task { @MainActor in
  269. findWindow(for: serial)
  270. }
  271. }
  272. func spiceDidDestroySerial(_ serial: CSPort) {
  273. for subwindow in secondaryWindows {
  274. (subwindow as! VMDisplayQemuWindowController).spiceDidDestroySerial(serial)
  275. }
  276. }
  277. }
  278. // MARK: - USB handling
  279. extension VMDisplayQemuWindowController: CSUSBManagerDelegate {
  280. func spiceUsbManager(_ usbManager: CSUSBManager, deviceError error: String, for device: CSUSBDevice) {
  281. logger.debug("USB device error: (\(device)) \(error)")
  282. DispatchQueue.main.async {
  283. self.showErrorAlert(error)
  284. }
  285. }
  286. func spiceUsbManager(_ usbManager: CSUSBManager, deviceAttached device: CSUSBDevice) {
  287. logger.debug("USB device attached: \(device)")
  288. if !isNoUsbPrompt {
  289. Task { @MainActor in
  290. if self.window!.isKeyWindow && self.vm.state == .started {
  291. self.showConnectPrompt(for: device)
  292. }
  293. }
  294. }
  295. }
  296. func spiceUsbManager(_ usbManager: CSUSBManager, deviceRemoved device: CSUSBDevice) {
  297. logger.debug("USB device removed: \(device)")
  298. if let i = connectedUsbDevices.firstIndex(of: device) {
  299. connectedUsbDevices.remove(at: i)
  300. }
  301. }
  302. func showConnectPrompt(for usbDevice: CSUSBDevice) {
  303. guard let usbManager = vmUsbManager else {
  304. logger.error("cannot get usb manager")
  305. return
  306. }
  307. let alert = NSAlert()
  308. alert.alertStyle = .informational
  309. alert.messageText = NSLocalizedString("USB Device", comment: "VMQemuDisplayMetalWindowController")
  310. alert.informativeText = String.localizedStringWithFormat(NSLocalizedString("Would you like to connect '%@' to this virtual machine?", comment: "VMQemuDisplayMetalWindowController"), usbDevice.name ?? usbDevice.description)
  311. alert.showsSuppressionButton = true
  312. alert.addButton(withTitle: NSLocalizedString("Confirm", comment: "VMQemuDisplayMetalWindowController"))
  313. alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "VMQemuDisplayMetalWindowController"))
  314. alert.beginSheetModal(for: window!) { response in
  315. if let suppressionButton = alert.suppressionButton,
  316. suppressionButton.state == .on {
  317. self.isNoUsbPrompt = true
  318. }
  319. guard response == .alertFirstButtonReturn else {
  320. return
  321. }
  322. Task.detached {
  323. do {
  324. try await usbManager.connectUsbDevice(usbDevice)
  325. await MainActor.run {
  326. self.connectedUsbDevices.append(usbDevice)
  327. }
  328. } catch {
  329. await MainActor.run {
  330. self.showErrorAlert(error.localizedDescription)
  331. }
  332. }
  333. }
  334. }
  335. }
  336. }
  337. /// These devices cannot be captured as enforced by macOS. Capturing results in an error. App Store Review requests that we block out the option.
  338. let usbBlockList = [
  339. (0x05ac, 0x8102), // Apple Touch Bar Backlight
  340. (0x05ac, 0x8103), // Apple Headset
  341. (0x05ac, 0x8233), // Apple T2 Controller
  342. (0x05ac, 0x8262), // Apple Ambient Light Sensor
  343. (0x05ac, 0x8263),
  344. (0x05ac, 0x8302), // Apple Touch Bar Display
  345. (0x05ac, 0x8514), // Apple FaceTime HD Camera (Built-in)
  346. (0x05ac, 0x8600), // Apple iBridge
  347. ]
  348. extension VMDisplayQemuWindowController {
  349. @IBAction override func usbButtonPressed(_ sender: Any) {
  350. let menu = NSMenu()
  351. menu.autoenablesItems = false
  352. let item = NSMenuItem()
  353. item.title = NSLocalizedString("Querying USB devices...", comment: "VMQemuDisplayMetalWindowController")
  354. item.isEnabled = false
  355. menu.addItem(item)
  356. DispatchQueue.global(qos: .userInitiated).async {
  357. let devices = self.vmUsbManager?.usbDevices ?? []
  358. DispatchQueue.main.async {
  359. self.updateUsbDevicesMenu(menu, devices: devices)
  360. }
  361. }
  362. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  363. }
  364. func updateUsbDevicesMenu(_ menu: NSMenu, devices: [CSUSBDevice]) {
  365. allUsbDevices = devices
  366. menu.removeAllItems()
  367. if devices.count == 0 {
  368. let item = NSMenuItem()
  369. item.title = NSLocalizedString("No USB devices detected.", comment: "VMQemuDisplayMetalWindowController")
  370. item.isEnabled = false
  371. menu.addItem(item)
  372. }
  373. for (i, device) in devices.enumerated() {
  374. let item = NSMenuItem()
  375. let canRedirect = vmUsbManager?.canRedirectUsbDevice(device, errorMessage: nil) ?? false
  376. let isConnected = vmUsbManager?.isUsbDeviceConnected(device) ?? false
  377. let isConnectedToSelf = connectedUsbDevices.contains(device)
  378. item.title = device.name ?? device.description
  379. let blocked = usbBlockList.contains { (usbVid, usbPid) in usbVid == device.usbVendorId && usbPid == device.usbProductId }
  380. item.isEnabled = !blocked && canRedirect && (isConnectedToSelf || !isConnected)
  381. item.state = isConnectedToSelf ? .on : .off
  382. item.tag = i
  383. let submenu = NSMenu()
  384. let connectItem = NSMenuItem()
  385. connectItem.title = isConnectedToSelf ? NSLocalizedString("Disconnect…", comment: "VMDisplayQemuDisplayController") : NSLocalizedString("Connect…", comment: "VMDisplayQemuDisplayController")
  386. connectItem.isEnabled = !blocked && canRedirect && (isConnectedToSelf || !isConnected)
  387. connectItem.tag = i
  388. connectItem.target = self
  389. connectItem.action = isConnectedToSelf ? #selector(disconnectUsbDevice) : #selector(connectUsbDevice)
  390. submenu.addItem(connectItem)
  391. let autoItem = NSMenuItem()
  392. autoItem.title = NSLocalizedString("Auto connect on start", comment: "VMDisplayQemuDisplayController")
  393. autoItem.isEnabled = !blocked && canRedirect
  394. autoItem.state = isAutoConnect(device) ? .on : .off
  395. autoItem.tag = i
  396. autoItem.target = self
  397. autoItem.action = #selector(setAutoConnect)
  398. submenu.addItem(autoItem)
  399. item.submenu = submenu
  400. menu.addItem(item)
  401. }
  402. menu.update()
  403. }
  404. @objc func connectUsbDevice(sender: AnyObject) {
  405. guard let menu = sender as? NSMenuItem else {
  406. logger.error("wrong sender for connectUsbDevice")
  407. return
  408. }
  409. guard let usbManager = vmUsbManager else {
  410. logger.error("cannot get usb manager")
  411. return
  412. }
  413. let device = allUsbDevices[menu.tag]
  414. Task.detached {
  415. self.withErrorAlert {
  416. try await usbManager.connectUsbDevice(device)
  417. await MainActor.run {
  418. self.connectedUsbDevices.append(device)
  419. }
  420. }
  421. }
  422. }
  423. @objc func disconnectUsbDevice(sender: AnyObject) {
  424. guard let menu = sender as? NSMenuItem else {
  425. logger.error("wrong sender for disconnectUsbDevice")
  426. return
  427. }
  428. guard let usbManager = vmUsbManager else {
  429. logger.error("cannot get usb manager")
  430. return
  431. }
  432. let device = allUsbDevices[menu.tag]
  433. connectedUsbDevices.removeAll(where: { $0 == device })
  434. Task.detached {
  435. self.withErrorAlert {
  436. try await usbManager.disconnectUsbDevice(device)
  437. }
  438. }
  439. }
  440. func isAutoConnect(_ device: CSUSBDevice) -> Bool {
  441. return qemuVM.isAutoConnect(for: device)
  442. }
  443. @objc func setAutoConnect(sender: AnyObject) {
  444. guard let menu = sender as? NSMenuItem else {
  445. logger.error("wrong sender for autoConnect")
  446. return
  447. }
  448. let device = allUsbDevices[menu.tag]
  449. qemuVM.setAutoConnect(!qemuVM.isAutoConnect(for: device), for: device)
  450. }
  451. func autoConnectUsbDevices() {
  452. guard let usbManager = vmUsbManager else {
  453. return
  454. }
  455. DispatchQueue.global(qos: .userInitiated).async {
  456. guard let devices = self.vmUsbManager?.usbDevices else {
  457. return
  458. }
  459. let filtered = devices.filter({ self.isAutoConnect($0) })
  460. for device in filtered {
  461. self.withErrorAlert {
  462. try await usbManager.connectUsbDevice(device)
  463. await MainActor.run {
  464. self.connectedUsbDevices.append(device)
  465. }
  466. }
  467. }
  468. }
  469. }
  470. }
  471. // MARK: - Window management
  472. extension VMDisplayQemuWindowController {
  473. @IBAction override func windowsButtonPressed(_ sender: Any) {
  474. let menu = NSMenu()
  475. menu.autoenablesItems = false
  476. for display in qemuVM.ioService!.displays {
  477. let id = display.monitorID
  478. guard id < vmQemuConfig.displays.count else {
  479. continue
  480. }
  481. let config = vmQemuConfig.displays[id]
  482. let item = NSMenuItem()
  483. let format = NSLocalizedString("Display %lld: %@", comment: "VMDisplayQemuDisplayController")
  484. let title = String.localizedStringWithFormat(format, id + 1, config.hardware.prettyValue)
  485. let isCurrent = self is VMDisplayQemuMetalWindowController && self.id == id
  486. item.title = title
  487. item.isEnabled = !isCurrent
  488. item.state = isCurrent ? .on : .off
  489. item.tag = id
  490. item.target = self
  491. item.action = #selector(showWindowFromDisplay)
  492. menu.addItem(item)
  493. }
  494. for serial in qemuVM.ioService!.serials {
  495. guard let id = configIdForSerial(serial) else {
  496. continue
  497. }
  498. let item = NSMenuItem()
  499. let format = NSLocalizedString("Serial %lld", comment: "VMDisplayQemuDisplayController")
  500. let title = String.localizedStringWithFormat(format, id + 1)
  501. let isCurrent = self is VMDisplayQemuTerminalWindowController && self.id == id
  502. item.title = title
  503. item.isEnabled = !isCurrent
  504. item.state = isCurrent ? .on : .off
  505. item.tag = id
  506. item.target = self
  507. item.action = #selector(showWindowFromSerial)
  508. menu.addItem(item)
  509. }
  510. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  511. }
  512. @objc private func showWindowFromDisplay(sender: AnyObject) {
  513. let item = sender as! NSMenuItem
  514. let id = item.tag
  515. if self is VMDisplayQemuMetalWindowController && self.id == id {
  516. return
  517. }
  518. guard let display = qemuVM.ioService?.displays.first(where: { $0.monitorID == id}) else {
  519. return
  520. }
  521. if let window = findWindow(for: display) {
  522. window.showWindow(self)
  523. }
  524. }
  525. @objc private func showWindowFromSerial(sender: AnyObject) {
  526. let item = sender as! NSMenuItem
  527. let id = item.tag
  528. if self is VMDisplayQemuTerminalWindowController && self.id == id {
  529. return
  530. }
  531. guard let serial = qemuVM.ioService?.serials.first(where: { id == configIdForSerial($0) }) else {
  532. return
  533. }
  534. if let window = findWindow(for: serial) {
  535. window.showWindow(self)
  536. }
  537. }
  538. @MainActor private func findWindow(for display: CSDisplay) -> VMDisplayQemuWindowController? {
  539. let id = display.monitorID
  540. let secondaryWindows: [VMDisplayWindowController]
  541. if self is VMDisplayQemuMetalWindowController && self.id == id {
  542. return self
  543. }
  544. if let window = primaryWindow {
  545. if (window as? VMDisplayQemuMetalWindowController)?.id == id {
  546. return window as? VMDisplayQemuWindowController
  547. }
  548. secondaryWindows = window.secondaryWindows
  549. } else {
  550. secondaryWindows = self.secondaryWindows
  551. }
  552. for window in secondaryWindows {
  553. if let window = window as? VMDisplayQemuMetalWindowController {
  554. if window.id == id {
  555. // found existing window
  556. return window
  557. }
  558. }
  559. }
  560. if let newWindow = newWindow(from: display) {
  561. return newWindow
  562. } else {
  563. return nil
  564. }
  565. }
  566. @MainActor private func newWindow(from display: CSDisplay) -> VMDisplayQemuMetalWindowController? {
  567. let id = display.monitorID
  568. guard id < vmQemuConfig.displays.count else {
  569. return nil
  570. }
  571. guard let primary = (primaryWindow ?? self) as? VMDisplayQemuMetalWindowController else {
  572. return nil
  573. }
  574. let secondary = VMDisplayQemuMetalWindowController(secondaryFromDisplay: display, primary: primary, vm: qemuVM, id: id)
  575. registerSecondaryWindow(secondary)
  576. return secondary
  577. }
  578. @MainActor private func findWindow(for serial: CSPort) -> VMDisplayQemuWindowController? {
  579. guard let id = configIdForSerial(serial) else {
  580. return nil
  581. }
  582. let secondaryWindows: [VMDisplayWindowController]
  583. if self is VMDisplayQemuTerminalWindowController && self.id == id {
  584. return self
  585. }
  586. if let window = primaryWindow {
  587. if (window as? VMDisplayQemuTerminalWindowController)?.id == id {
  588. return window as? VMDisplayQemuWindowController
  589. }
  590. secondaryWindows = window.secondaryWindows
  591. } else {
  592. secondaryWindows = self.secondaryWindows
  593. }
  594. for window in secondaryWindows {
  595. if let window = window as? VMDisplayQemuTerminalWindowController {
  596. if window.id == id {
  597. // found existing window
  598. return window
  599. }
  600. }
  601. }
  602. if let newWindow = newWindow(from: serial) {
  603. return newWindow
  604. } else {
  605. return nil
  606. }
  607. }
  608. @MainActor private func newWindow(from serial: CSPort) -> VMDisplayQemuTerminalWindowController? {
  609. guard let id = configIdForSerial(serial) else {
  610. return nil
  611. }
  612. guard id < vmQemuConfig.serials.count else {
  613. return nil
  614. }
  615. let secondary = VMDisplayQemuTerminalWindowController(secondaryFromSerialPort: serial, vm: qemuVM, id: id)
  616. registerSecondaryWindow(secondary)
  617. return secondary
  618. }
  619. }
  620. // MARK: - Computer wakeup
  621. extension VMDisplayQemuWindowController {
  622. @objc override func didWake(_ notification: NSNotification) {
  623. Task {
  624. try? await qemuVM.guestAgent?.guestSetTime(NSDate.now.timeIntervalSince1970)
  625. }
  626. }
  627. }