display(macOS): support headless boot

Resolves #2280
This commit is contained in:
osy 2022-08-02 17:20:28 -07:00
parent 1f72fbf1f1
commit c6d52e47de
5 changed files with 102 additions and 7 deletions

View File

@ -70,7 +70,7 @@ class UTMData: ObservableObject {
#if os(macOS)
/// View controller for every VM currently active
var vmWindows: [UTMVirtualMachine: VMDisplayWindowController] = [:]
var vmWindows: [UTMVirtualMachine: Any] = [:]
#else
/// View controller for currently active VM
var vmVC: Any?
@ -301,7 +301,7 @@ class UTMData: ObservableObject {
self.busy = busy
}
@MainActor private func showErrorAlert(message: String) {
@MainActor func showErrorAlert(message: String) {
alertMessage = AlertMessage(message)
}

View File

@ -26,6 +26,11 @@ struct UTMApp: App {
.onAppear {
appDelegate.data = data
}
.onReceive(.vmSessionError) { notification in
if let message = notification.userInfo?["Message"] as? String {
data.showErrorAlert(message: message)
}
}
}.commands {
VMCommands()
}

View File

@ -19,8 +19,8 @@ import Carbon.HIToolbox
@available(macOS 11, *)
extension UTMData {
func run(vm: UTMVirtualMachine) {
var window: VMDisplayWindowController? = vmWindows[vm]
@MainActor func run(vm: UTMVirtualMachine) {
var window: Any? = vmWindows[vm]
if window == nil {
let close = { (notification: Notification) -> Void in
self.vmWindows.removeValue(forKey: vm)
@ -31,6 +31,8 @@ extension UTMData {
if avm.appleConfig.displays.count > 0 ||
(avm.appleConfig.serials.count > 0 && avm.appleConfig.serials.first!.mode == .builtin) {
window = VMDisplayAppleWindowController(vm: avm, onClose: close)
} else {
window = VMHeadlessSessionState(for: avm, onStop: close)
}
}
}
@ -40,7 +42,7 @@ extension UTMData {
} else if qvm.config.qemuHasTerminal {
window = VMDisplayTerminalWindowController(vm: qvm, onClose: close)
} else {
// FIXME: do headless
window = VMHeadlessSessionState(for: qvm, onStop: close)
}
}
if window == nil {
@ -49,12 +51,15 @@ extension UTMData {
}
}
}
if let unwrappedWindow = window {
if let unwrappedWindow = window as? VMDisplayWindowController {
vmWindows[vm] = unwrappedWindow
vm.delegate = unwrappedWindow
unwrappedWindow.showWindow(nil)
unwrappedWindow.window!.makeMain()
unwrappedWindow.requestAutoStart()
} else if let unwrappedWindow = window as? VMHeadlessSessionState {
vmWindows[vm] = unwrappedWindow
unwrappedWindow.start()
} else {
logger.critical("Failed to create window controller.")
}
@ -70,7 +75,7 @@ extension UTMData {
}
func close(vm: UTMVirtualMachine) {
if let window = vmWindows.removeValue(forKey: vm) {
if let window = vmWindows.removeValue(forKey: vm) as? VMDisplayWindowController {
DispatchQueue.main.async {
window.close()
}

View File

@ -0,0 +1,81 @@
//
// Copyright © 2022 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Represents the UI state for a single headless VM session.
@MainActor class VMHeadlessSessionState: NSObject, ObservableObject {
let vm: UTMVirtualMachine
var onStop: ((Notification) -> Void)?
@Published var vmState: UTMVMState = .vmStopped
@Published var fatalError: String?
private var hasStarted: Bool = false
init(for vm: UTMVirtualMachine, onStop: ((Notification) -> Void)?) {
self.vm = vm
self.onStop = onStop
super.init()
vm.delegate = self
}
}
extension VMHeadlessSessionState: UTMVirtualMachineDelegate {
nonisolated func virtualMachine(_ vm: UTMVirtualMachine, didTransitionTo state: UTMVMState) {
Task { @MainActor in
vmState = state
if state == .vmStarted {
hasStarted = true
}
if state == .vmStopped {
if hasStarted {
stop() // graceful exit
}
hasStarted = false
}
}
}
nonisolated func virtualMachine(_ vm: UTMVirtualMachine, didErrorWithMessage message: String) {
Task { @MainActor in
fatalError = message
NotificationCenter.default.post(name: .vmSessionError, object: nil, userInfo: ["Session": self, "Message": message])
if !hasStarted {
// if we got an error and haven't started, then cleanup
stop()
}
}
}
}
extension VMHeadlessSessionState {
func start() {
NotificationCenter.default.post(name: .vmSessionCreated, object: nil, userInfo: ["Session": self])
vm.requestVmStart()
}
func stop() {
NotificationCenter.default.post(name: .vmSessionEnded, object: nil, userInfo: ["Session": self])
}
}
extension Notification.Name {
static let vmSessionCreated = Self("VMSessionCreated")
static let vmSessionEnded = Self("VMSessionEnded")
static let vmSessionError = Self("VMSessionError")
}

View File

@ -213,6 +213,7 @@
84B36D2927B790BE00C22685 /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B36D2827B790BE00C22685 /* DestructiveButton.swift */; };
84B36D2A27B790BE00C22685 /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B36D2827B790BE00C22685 /* DestructiveButton.swift */; };
84B36D2B27B790BE00C22685 /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B36D2827B790BE00C22685 /* DestructiveButton.swift */; };
84BB993A2899E8D500DF28B2 /* VMHeadlessSessionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB99392899E8D500DF28B2 /* VMHeadlessSessionState.swift */; };
84C4D9022880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C4D9012880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift */; };
84C4D9032880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C4D9012880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift */; };
84C4D9042880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C4D9012880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift */; };
@ -1659,6 +1660,7 @@
84B36D2427B704C200C22685 /* UTMDownloadVMTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMDownloadVMTask.swift; sourceTree = "<group>"; };
84B36D2827B790BE00C22685 /* DestructiveButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveButton.swift; sourceTree = "<group>"; };
84BB99382899AB1800DF28B2 /* UTMLoggingDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLoggingDelegate.h; sourceTree = "<group>"; };
84BB99392899E8D500DF28B2 /* VMHeadlessSessionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMHeadlessSessionState.swift; sourceTree = "<group>"; };
84C4D9012880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMSettingsAddDeviceMenuView.swift; sourceTree = "<group>"; };
84C584DC268E70F1000FCABF /* UTMVirtualMachine-Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMVirtualMachine-Protected.h"; sourceTree = "<group>"; };
84C584DE268E95B3000FCABF /* UTMLegacyAppleConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMLegacyAppleConfiguration.swift; sourceTree = "<group>"; };
@ -2629,6 +2631,7 @@
84C584E2268F8AE7000FCABF /* VMQEMUSettingsView.swift */,
CE2D953D24AD4F980059923A /* VMSettingsView.swift */,
CEF0300526A25A6900667B63 /* VMWizardView.swift */,
84BB99392899E8D500DF28B2 /* VMHeadlessSessionState.swift */,
53A0BDD426D79FE40010EDC5 /* SavePanel.swift */,
CE2D954124AD4F980059923A /* Info.plist */,
FFB02A8E266CB09C006CD71A /* InfoPlist.strings */,
@ -3993,6 +3996,7 @@
2C33B3AA2566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */,
CE0B6D5B24AD584D00FE012D /* qapi-visit-misc-target.c in Sources */,
CE0B6D0C24AD56C300FE012D /* error.c in Sources */,
84BB993A2899E8D500DF28B2 /* VMHeadlessSessionState.swift in Sources */,
CE2D955A24AD4F980059923A /* VMToolbarModifier.swift in Sources */,
CED814ED24C7C2850042F0F1 /* VMConfigInfoView.swift in Sources */,
2CE8EB0A2572E173000E2EBB /* qapi-visit-block-export.c in Sources */,