VMDisplayQemuDisplayController.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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 = !vmQemuConfig.qemu.isGuestToolsInstallRequested
  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. vmQemuConfig.qemu.isGuestToolsInstallRequested = true
  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. }
  253. }
  254. for subwindow in secondaryWindows {
  255. (subwindow as! VMDisplayQemuWindowController).spiceDidChangeUsbManager(usbManager)
  256. }
  257. }
  258. func spiceDynamicResolutionSupportDidChange(_ supported: Bool) {
  259. for subwindow in secondaryWindows {
  260. (subwindow as! VMDisplayQemuWindowController).spiceDynamicResolutionSupportDidChange(supported)
  261. }
  262. }
  263. func spiceDidCreateSerial(_ serial: CSPort) {
  264. guard !isSecondary else {
  265. return
  266. }
  267. Task { @MainActor in
  268. findWindow(for: serial)
  269. }
  270. }
  271. func spiceDidDestroySerial(_ serial: CSPort) {
  272. for subwindow in secondaryWindows {
  273. (subwindow as! VMDisplayQemuWindowController).spiceDidDestroySerial(serial)
  274. }
  275. }
  276. }
  277. // MARK: - USB handling
  278. extension VMDisplayQemuWindowController: CSUSBManagerDelegate {
  279. func spiceUsbManager(_ usbManager: CSUSBManager, deviceError error: String, for device: CSUSBDevice) {
  280. logger.debug("USB device error: (\(device)) \(error)")
  281. DispatchQueue.main.async {
  282. self.showErrorAlert(error)
  283. }
  284. }
  285. func spiceUsbManager(_ usbManager: CSUSBManager, deviceAttached device: CSUSBDevice) {
  286. logger.debug("USB device attached: \(device)")
  287. if !isNoUsbPrompt {
  288. Task { @MainActor in
  289. if self.window!.isKeyWindow && self.vm.state == .started {
  290. self.showConnectPrompt(for: device)
  291. }
  292. }
  293. }
  294. }
  295. func spiceUsbManager(_ usbManager: CSUSBManager, deviceRemoved device: CSUSBDevice) {
  296. logger.debug("USB device removed: \(device)")
  297. if let i = connectedUsbDevices.firstIndex(of: device) {
  298. connectedUsbDevices.remove(at: i)
  299. }
  300. }
  301. func showConnectPrompt(for usbDevice: CSUSBDevice) {
  302. guard let usbManager = vmUsbManager else {
  303. logger.error("cannot get usb manager")
  304. return
  305. }
  306. let alert = NSAlert()
  307. alert.alertStyle = .informational
  308. alert.messageText = NSLocalizedString("USB Device", comment: "VMQemuDisplayMetalWindowController")
  309. alert.informativeText = String.localizedStringWithFormat(NSLocalizedString("Would you like to connect '%@' to this virtual machine?", comment: "VMQemuDisplayMetalWindowController"), usbDevice.name ?? usbDevice.description)
  310. alert.showsSuppressionButton = true
  311. alert.addButton(withTitle: NSLocalizedString("Confirm", comment: "VMQemuDisplayMetalWindowController"))
  312. alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "VMQemuDisplayMetalWindowController"))
  313. alert.beginSheetModal(for: window!) { response in
  314. if let suppressionButton = alert.suppressionButton,
  315. suppressionButton.state == .on {
  316. self.isNoUsbPrompt = true
  317. }
  318. guard response == .alertFirstButtonReturn else {
  319. return
  320. }
  321. Task.detached {
  322. do {
  323. try await usbManager.connectUsbDevice(usbDevice)
  324. await MainActor.run {
  325. self.connectedUsbDevices.append(usbDevice)
  326. }
  327. } catch {
  328. await MainActor.run {
  329. self.showErrorAlert(error.localizedDescription)
  330. }
  331. }
  332. }
  333. }
  334. }
  335. }
  336. /// These devices cannot be captured as enforced by macOS. Capturing results in an error. App Store Review requests that we block out the option.
  337. let usbBlockList = [
  338. (0x05ac, 0x8102), // Apple Touch Bar Backlight
  339. (0x05ac, 0x8103), // Apple Headset
  340. (0x05ac, 0x8233), // Apple T2 Controller
  341. (0x05ac, 0x8262), // Apple Ambient Light Sensor
  342. (0x05ac, 0x8263),
  343. (0x05ac, 0x8302), // Apple Touch Bar Display
  344. (0x05ac, 0x8514), // Apple FaceTime HD Camera (Built-in)
  345. (0x05ac, 0x8600), // Apple iBridge
  346. ]
  347. extension VMDisplayQemuWindowController {
  348. @IBAction override func usbButtonPressed(_ sender: Any) {
  349. let menu = NSMenu()
  350. menu.autoenablesItems = false
  351. let item = NSMenuItem()
  352. item.title = NSLocalizedString("Querying USB devices...", comment: "VMQemuDisplayMetalWindowController")
  353. item.isEnabled = false
  354. menu.addItem(item)
  355. DispatchQueue.global(qos: .userInitiated).async {
  356. let devices = self.vmUsbManager?.usbDevices ?? []
  357. DispatchQueue.main.async {
  358. self.updateUsbDevicesMenu(menu, devices: devices)
  359. }
  360. }
  361. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  362. }
  363. func updateUsbDevicesMenu(_ menu: NSMenu, devices: [CSUSBDevice]) {
  364. allUsbDevices = devices
  365. menu.removeAllItems()
  366. if devices.count == 0 {
  367. let item = NSMenuItem()
  368. item.title = NSLocalizedString("No USB devices detected.", comment: "VMQemuDisplayMetalWindowController")
  369. item.isEnabled = false
  370. menu.addItem(item)
  371. }
  372. for (i, device) in devices.enumerated() {
  373. let item = NSMenuItem()
  374. let canRedirect = vmUsbManager?.canRedirectUsbDevice(device, errorMessage: nil) ?? false
  375. let isConnected = vmUsbManager?.isUsbDeviceConnected(device) ?? false
  376. let isConnectedToSelf = connectedUsbDevices.contains(device)
  377. item.title = device.name ?? device.description
  378. let blocked = usbBlockList.contains { (usbVid, usbPid) in usbVid == device.usbVendorId && usbPid == device.usbProductId }
  379. item.isEnabled = !blocked && canRedirect && (isConnectedToSelf || !isConnected)
  380. item.state = isConnectedToSelf ? .on : .off;
  381. item.tag = i
  382. item.target = self
  383. item.action = isConnectedToSelf ? #selector(disconnectUsbDevice) : #selector(connectUsbDevice)
  384. menu.addItem(item)
  385. }
  386. menu.update()
  387. }
  388. @objc func connectUsbDevice(sender: AnyObject) {
  389. guard let menu = sender as? NSMenuItem else {
  390. logger.error("wrong sender for connectUsbDevice")
  391. return
  392. }
  393. guard let usbManager = vmUsbManager else {
  394. logger.error("cannot get usb manager")
  395. return
  396. }
  397. let device = allUsbDevices[menu.tag]
  398. Task.detached {
  399. do {
  400. try await usbManager.connectUsbDevice(device)
  401. await MainActor.run {
  402. self.connectedUsbDevices.append(device)
  403. }
  404. } catch {
  405. await MainActor.run {
  406. self.showErrorAlert(error.localizedDescription)
  407. }
  408. }
  409. }
  410. }
  411. @objc func disconnectUsbDevice(sender: AnyObject) {
  412. guard let menu = sender as? NSMenuItem else {
  413. logger.error("wrong sender for disconnectUsbDevice")
  414. return
  415. }
  416. guard let usbManager = vmUsbManager else {
  417. logger.error("cannot get usb manager")
  418. return
  419. }
  420. let device = allUsbDevices[menu.tag]
  421. connectedUsbDevices.removeAll(where: { $0 == device })
  422. Task.detached {
  423. do {
  424. try await usbManager.disconnectUsbDevice(device)
  425. } catch {
  426. await MainActor.run {
  427. self.showErrorAlert(error.localizedDescription)
  428. }
  429. }
  430. }
  431. }
  432. }
  433. // MARK: - Window management
  434. extension VMDisplayQemuWindowController {
  435. @IBAction override func windowsButtonPressed(_ sender: Any) {
  436. let menu = NSMenu()
  437. menu.autoenablesItems = false
  438. for display in qemuVM.ioService!.displays {
  439. let id = display.monitorID
  440. guard id < vmQemuConfig.displays.count else {
  441. continue
  442. }
  443. let config = vmQemuConfig.displays[id]
  444. let item = NSMenuItem()
  445. let format = NSLocalizedString("Display %lld: %@", comment: "VMDisplayQemuDisplayController")
  446. let title = String.localizedStringWithFormat(format, id + 1, config.hardware.prettyValue)
  447. let isCurrent = self is VMDisplayQemuMetalWindowController && self.id == id
  448. item.title = title
  449. item.isEnabled = !isCurrent
  450. item.state = isCurrent ? .on : .off
  451. item.tag = id
  452. item.target = self
  453. item.action = #selector(showWindowFromDisplay)
  454. menu.addItem(item)
  455. }
  456. for serial in qemuVM.ioService!.serials {
  457. guard let id = configIdForSerial(serial) else {
  458. continue
  459. }
  460. let item = NSMenuItem()
  461. let format = NSLocalizedString("Serial %lld", comment: "VMDisplayQemuDisplayController")
  462. let title = String.localizedStringWithFormat(format, id + 1)
  463. let isCurrent = self is VMDisplayQemuTerminalWindowController && self.id == id
  464. item.title = title
  465. item.isEnabled = !isCurrent
  466. item.state = isCurrent ? .on : .off
  467. item.tag = id
  468. item.target = self
  469. item.action = #selector(showWindowFromSerial)
  470. menu.addItem(item)
  471. }
  472. menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
  473. }
  474. @objc private func showWindowFromDisplay(sender: AnyObject) {
  475. let item = sender as! NSMenuItem
  476. let id = item.tag
  477. if self is VMDisplayQemuMetalWindowController && self.id == id {
  478. return
  479. }
  480. guard let display = qemuVM.ioService?.displays.first(where: { $0.monitorID == id}) else {
  481. return
  482. }
  483. if let window = findWindow(for: display) {
  484. window.showWindow(self)
  485. }
  486. }
  487. @objc private func showWindowFromSerial(sender: AnyObject) {
  488. let item = sender as! NSMenuItem
  489. let id = item.tag
  490. if self is VMDisplayQemuTerminalWindowController && self.id == id {
  491. return
  492. }
  493. guard let serial = qemuVM.ioService?.serials.first(where: { id == configIdForSerial($0) }) else {
  494. return
  495. }
  496. if let window = findWindow(for: serial) {
  497. window.showWindow(self)
  498. }
  499. }
  500. @MainActor private func findWindow(for display: CSDisplay) -> VMDisplayQemuWindowController? {
  501. let id = display.monitorID
  502. let secondaryWindows: [VMDisplayWindowController]
  503. if self is VMDisplayQemuMetalWindowController && self.id == id {
  504. return self
  505. }
  506. if let window = primaryWindow {
  507. if (window as? VMDisplayQemuMetalWindowController)?.id == id {
  508. return window as? VMDisplayQemuWindowController
  509. }
  510. secondaryWindows = window.secondaryWindows
  511. } else {
  512. secondaryWindows = self.secondaryWindows
  513. }
  514. for window in secondaryWindows {
  515. if let window = window as? VMDisplayQemuMetalWindowController {
  516. if window.id == id {
  517. // found existing window
  518. return window
  519. }
  520. }
  521. }
  522. if let newWindow = newWindow(from: display) {
  523. return newWindow
  524. } else {
  525. return nil
  526. }
  527. }
  528. @MainActor private func newWindow(from display: CSDisplay) -> VMDisplayQemuMetalWindowController? {
  529. let id = display.monitorID
  530. guard id < vmQemuConfig.displays.count else {
  531. return nil
  532. }
  533. guard let primary = (primaryWindow ?? self) as? VMDisplayQemuMetalWindowController else {
  534. return nil
  535. }
  536. let secondary = VMDisplayQemuMetalWindowController(secondaryFromDisplay: display, primary: primary, vm: qemuVM, id: id)
  537. registerSecondaryWindow(secondary)
  538. return secondary
  539. }
  540. @MainActor private func findWindow(for serial: CSPort) -> VMDisplayQemuWindowController? {
  541. let id = configIdForSerial(serial)!
  542. let secondaryWindows: [VMDisplayWindowController]
  543. if self is VMDisplayQemuTerminalWindowController && self.id == id {
  544. return self
  545. }
  546. if let window = primaryWindow {
  547. if (window as? VMDisplayQemuTerminalWindowController)?.id == id {
  548. return window as? VMDisplayQemuWindowController
  549. }
  550. secondaryWindows = window.secondaryWindows
  551. } else {
  552. secondaryWindows = self.secondaryWindows
  553. }
  554. for window in secondaryWindows {
  555. if let window = window as? VMDisplayQemuTerminalWindowController {
  556. if window.id == id {
  557. // found existing window
  558. return window
  559. }
  560. }
  561. }
  562. if let newWindow = newWindow(from: serial) {
  563. return newWindow
  564. } else {
  565. return nil
  566. }
  567. }
  568. @MainActor private func newWindow(from serial: CSPort) -> VMDisplayQemuTerminalWindowController? {
  569. guard let id = configIdForSerial(serial) else {
  570. return nil
  571. }
  572. guard id < vmQemuConfig.serials.count else {
  573. return nil
  574. }
  575. let secondary = VMDisplayQemuTerminalWindowController(secondaryFromSerialPort: serial, vm: qemuVM, id: id)
  576. registerSecondaryWindow(secondary)
  577. return secondary
  578. }
  579. }
  580. // MARK: - Computer wakeup
  581. extension VMDisplayQemuWindowController {
  582. @objc override func didWake(_ notification: NSNotification) {
  583. Task {
  584. try? await qemuVM.guestAgent?.guestSetTime(NSDate.now.timeIntervalSince1970)
  585. }
  586. }
  587. }