Skip to content

Commit

Permalink
Merge branch 'main' into feature/play-button
Browse files Browse the repository at this point in the history
  • Loading branch information
osy authored Jul 17, 2023
2 parents ec778d0 + e15b878 commit a2966eb
Show file tree
Hide file tree
Showing 42 changed files with 3,001 additions and 156 deletions.
16 changes: 16 additions & 0 deletions Configuration/QEMUConstant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ enum QEMUPackageFileName: String {
case images = "Images"
case debugLog = "debug.log"
case efiVariables = "efi_vars.fd"
case tpmData = "tpmdata"
}

// MARK: Supported features
Expand Down Expand Up @@ -444,6 +445,14 @@ extension QEMUArchitecture {
return false
#endif
}

var hasSecureBootSupport: Bool {
switch self {
case .x86_64, .i386: return true
case .aarch64: return true
default: return false
}
}
}

extension QEMUTarget {
Expand All @@ -460,4 +469,11 @@ extension QEMUTarget {
default: return true
}
}

var hasSecureBootSupport: Bool {
switch self.rawValue {
case "microvm": return false
default: return true
}
}
}
59 changes: 50 additions & 9 deletions Configuration/UTMQemuConfiguration+Arguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ import Virtualization // for getting network interfaces
QEMUArgumentFragment(final: string)
}

/// Return the socket file for communicating with SPICE
var spiceSocketURL: URL {
/// Shared between helper and main process to store Unix sockets
var socketURL: URL {
#if os(iOS)
let parentURL = FileManager.default.temporaryDirectory
return FileManager.default.temporaryDirectory
#else
let appGroup = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
let helper = Bundle.main.infoDictionary?["HelperIdentifier"] as? String
Expand All @@ -44,11 +44,21 @@ import Virtualization // for getting network interfaces
parentURL.appendPathComponent("tmp")
if let appGroup = appGroup, !appGroup.hasPrefix("invalid.") {
if let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
parentURL = containerURL
return containerURL
}
}
return parentURL
#endif
return parentURL.appendingPathComponent("\(information.uuid.uuidString).spice")
}

/// Return the socket file for communicating with SPICE
var spiceSocketURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("spice")
}

/// Return the socket file for communicating with SWTPM
var swtpmSocketURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("swtpm")
}

/// Combined generated and user specified arguments.
Expand Down Expand Up @@ -100,8 +110,7 @@ import Virtualization // for getting network interfaces
@QEMUArgumentBuilder private var spiceArguments: [QEMUArgument] {
f("-spice")
"unix=on"
"addr="
spiceSocketURL
"addr=\(spiceSocketURL.lastPathComponent)"
"disable-ticketing=on"
"image-compression=off"
"playback-compression=off"
Expand Down Expand Up @@ -185,7 +194,7 @@ import Virtualization // for getting network interfaces
f("chardev:term\(i)")
case .manualDevice:
f("-device")
f("\(serials[i].hardware!.rawValue),chardev=term\(i)")
f("\(serials[i].hardware?.rawValue ?? "invalid"),chardev=term\(i)")
case .monitor:
f("-mon")
f("chardev=term\(i),mode=readline")
Expand Down Expand Up @@ -286,6 +295,10 @@ import Virtualization // for getting network interfaces
system.architecture.hasUsbSupport && system.target.hasUsbSupport && input.usbBusSupport != .disabled
}

private var isSecureBootUsed: Bool {
system.architecture.hasSecureBootSupport && system.target.hasSecureBootSupport && qemu.hasTPMDevice
}

@QEMUArgumentBuilder private var machineArguments: [QEMUArgument] {
f("-machine")
system.target
Expand Down Expand Up @@ -361,7 +374,9 @@ import Virtualization // for getting network interfaces
f("ICH9-LPC.disable_s3=1") // applies for pc-q35-* types
}
if qemu.hasUefiBoot {
let bios = resourceURL.appendingPathComponent("edk2-\(system.architecture.rawValue)-code.fd")
let secure = isSecureBootUsed ? "-secure" : ""
let code = system.target.rawValue == "microvm" ? "microvm" : "code"
let bios = resourceURL.appendingPathComponent("edk2-\(system.architecture.rawValue)\(secure)-\(code).fd")
let vars = qemu.efiVarsURL ?? URL(fileURLWithPath: "/\(QEMUPackageFileName.efiVariables.rawValue)")
if !hasCustomBios && FileManager.default.fileExists(atPath: bios.path) {
f("-drive")
Expand Down Expand Up @@ -903,6 +918,32 @@ import Virtualization // for getting network interfaces
f("-device")
f("virtio-balloon-pci")
}
if qemu.hasTPMDevice {
tpmArguments
}
}

@QEMUArgumentBuilder private var tpmArguments: [QEMUArgument] {
f("-chardev")
"socket"
"id=chrtpm0"
"path=\(swtpmSocketURL.lastPathComponent)"
f()
f("-tpmdev")
"emulator"
"id=tpm0"
"chardev=chrtpm0"
f()
f("-device")
if system.target.rawValue.hasPrefix("virt") {
"tpm-crb-device"
} else if system.architecture == .ppc64 {
"tpm-spapr"
} else {
"tpm-crb"
}
"tpmdev=tpm0"
f()
}
}

Expand Down
60 changes: 41 additions & 19 deletions Configuration/UTMQemuConfigurationQEMU.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ struct UTMQemuConfigurationQEMU: Codable {
/// EFI variables if EFI boot is enabled. This property is not saved to file.
var efiVarsURL: URL?

/// TPM data file if TPM is enabled. This property is not saved to file.
var tpmDataURL: URL?

/// If true, write standard output to debug.log in the VM bundle.
var hasDebugLog: Bool = false

Expand Down Expand Up @@ -63,13 +66,17 @@ struct UTMQemuConfigurationQEMU: Codable {
/// Set to true to request guest tools install. Not saved.
var isGuestToolsInstallRequested: Bool = false

/// Set to true to request UEFI variable reset. Not saved.
var isUefiVariableResetRequested: Bool = false

enum CodingKeys: String, CodingKey {
case hasDebugLog = "DebugLog"
case hasUefiBoot = "UEFIBoot"
case hasRNGDevice = "RNGDevice"
case hasBalloonDevice = "BalloonDevice"
case hasTPMDevice = "TPMDevice"
case hasHypervisor = "Hypervisor"
case hasTSO = "TSO"
case hasRTCLocalTime = "RTCLocalTime"
case hasPS2Controller = "PS2Controller"
case machinePropertyOverride = "MachinePropertyOverride"
Expand All @@ -87,13 +94,15 @@ struct UTMQemuConfigurationQEMU: Codable {
hasBalloonDevice = try values.decode(Bool.self, forKey: .hasBalloonDevice)
hasTPMDevice = try values.decode(Bool.self, forKey: .hasTPMDevice)
hasHypervisor = try values.decode(Bool.self, forKey: .hasHypervisor)
hasTSO = try values.decodeIfPresent(Bool.self, forKey: .hasTSO) ?? false
hasRTCLocalTime = try values.decode(Bool.self, forKey: .hasRTCLocalTime)
hasPS2Controller = try values.decode(Bool.self, forKey: .hasPS2Controller)
machinePropertyOverride = try values.decodeIfPresent(String.self, forKey: .machinePropertyOverride)
additionalArguments = try values.decode([QEMUArgument].self, forKey: .additionalArguments)
if let dataURL = decoder.userInfo[.dataURL] as? URL {
debugLogURL = dataURL.appendingPathComponent(QEMUPackageFileName.debugLog.rawValue)
efiVarsURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
tpmDataURL = dataURL.appendingPathComponent(QEMUPackageFileName.tpmData.rawValue)
}
}

Expand All @@ -105,6 +114,7 @@ struct UTMQemuConfigurationQEMU: Codable {
try container.encode(hasBalloonDevice, forKey: .hasBalloonDevice)
try container.encode(hasTPMDevice, forKey: .hasTPMDevice)
try container.encode(hasHypervisor, forKey: .hasHypervisor)
try container.encode(hasTSO, forKey: .hasTSO)
try container.encode(hasRTCLocalTime, forKey: .hasRTCLocalTime)
try container.encode(hasPS2Controller, forKey: .hasPS2Controller)
try container.encodeIfPresent(machinePropertyOverride, forKey: .machinePropertyOverride)
Expand Down Expand Up @@ -153,27 +163,39 @@ extension UTMQemuConfigurationQEMU {

extension UTMQemuConfigurationQEMU {
@MainActor mutating func saveData(to dataURL: URL, for system: UTMQemuConfigurationSystem) async throws -> [URL] {
guard hasUefiBoot else {
return []
var existing: [URL] = []
if hasUefiBoot {
let fileManager = FileManager.default
// save EFI variables
let resourceURL = Bundle.main.url(forResource: "qemu", withExtension: nil)!
let templateVarsURL: URL
if system.architecture == .arm || system.architecture == .aarch64 {
templateVarsURL = resourceURL.appendingPathComponent("edk2-arm-vars.fd")
} else if system.architecture == .i386 || system.architecture == .x86_64 {
templateVarsURL = resourceURL.appendingPathComponent("edk2-i386-vars.fd")
} else {
throw UTMQemuConfigurationError.uefiNotSupported
}
let varsURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
if !fileManager.fileExists(atPath: varsURL.path) {
try await Task.detached {
try fileManager.copyItem(at: templateVarsURL, to: varsURL)
}.value
}
efiVarsURL = varsURL
existing.append(varsURL)
}
let fileManager = FileManager.default
// save EFI variables
let resourceURL = Bundle.main.url(forResource: "qemu", withExtension: nil)!
let templateVarsURL: URL
if system.architecture == .arm || system.architecture == .aarch64 {
templateVarsURL = resourceURL.appendingPathComponent("edk2-arm-vars.fd")
} else if system.architecture == .i386 || system.architecture == .x86_64 {
templateVarsURL = resourceURL.appendingPathComponent("edk2-i386-vars.fd")
} else {
throw UTMQemuConfigurationError.uefiNotSupported
let possibleTpmDataURL = dataURL.appendingPathComponent(QEMUPackageFileName.tpmData.rawValue)
if hasTPMDevice {
tpmDataURL = possibleTpmDataURL
existing.append(tpmDataURL!)
} else if FileManager.default.fileExists(atPath: possibleTpmDataURL.path) {
existing.append(possibleTpmDataURL) // do not delete any existing TPM data
}
let varsURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
if !fileManager.fileExists(atPath: varsURL.path) {
try await Task.detached {
try fileManager.copyItem(at: templateVarsURL, to: varsURL)
}.value
if hasDebugLog {
let debugLogURL = dataURL.appendingPathComponent(QEMUPackageFileName.debugLog.rawValue)
existing.append(debugLogURL)
}
efiVarsURL = varsURL
return [varsURL]
return existing
}
}
1 change: 1 addition & 0 deletions Configuration/UTMQemuConfigurationSerial.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ struct UTMQemuConfigurationSerial: Codable, Identifiable {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(mode, forKey: .mode)
try container.encode(target, forKey: .target)
try container.encodeIfPresent(hardware?.asAnyQEMUConstant(), forKey: .hardware)
// only save relevant settings
switch mode {
case .builtin:
Expand Down
11 changes: 7 additions & 4 deletions Platform/Shared/VMConfigQEMUView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,8 @@ struct VMConfigQEMUView: View {
.help("Should be on always unless the guest cannot boot because of this.")
Toggle("Balloon Device", isOn: $config.hasBalloonDevice)
.help("Should be on always unless the guest cannot boot because of this.")
#if false
Toggle("TPM Device", isOn: $config.hasTPMDevice)
.help("This is required to boot Windows 11.")
#endif
Toggle("TPM 2.0 Device", isOn: $config.hasTPMDevice)
.help("TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided.")
Toggle("Use Hypervisor", isOn: $config.hasHypervisor)
.help("Only available if host architecture matches the target. Otherwise, TCG emulation is used.")
.disabled(!system.architecture.hasHypervisorSupport)
Expand All @@ -96,6 +94,11 @@ struct VMConfigQEMUView: View {
.disabled(!supportsPs2)
.help("Instantiate PS/2 controller even when USB input is supported. Required for older Windows.")
}
DetailedSection("Maintenance", description: "Options here only apply on next boot and are not saved.") {
Toggle("Reset UEFI Variables", isOn: $config.isUefiVariableResetRequested)
.help("You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot.")
.disabled(!config.hasUefiBoot)
}
DetailedSection("QEMU Machine Properties", description: "This is appended to the -machine argument.") {
DefaultTextField("", text: $config.machinePropertyOverride.bound, prompt: "Default")
}
Expand Down
2 changes: 1 addition & 1 deletion Platform/Shared/VMContextMenuModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ struct VMContextMenuModifier: ViewModifier {
Button {
data.run(vm: vm, options: .bootRecovery)
} label: {
Label("Run Recovery", systemImage: "play")
Label("Run Recovery", systemImage: "lifepreserver.fill")
}.help("Boot into recovery mode.")
}
#endif
Expand Down
9 changes: 9 additions & 0 deletions Platform/Shared/VMWizardOSWindowsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ struct VMWizardOSWindowsView: View {
.onChange(of: wizardState.isWindows10OrHigher) { newValue in
if newValue {
wizardState.systemBootUefi = true
wizardState.systemBootTpm = true
wizardState.isGuestToolsInstallRequested = true
} else {
wizardState.systemBootTpm = false
wizardState.isGuestToolsInstallRequested = false
}
}
Expand Down Expand Up @@ -79,6 +81,13 @@ struct VMWizardOSWindowsView: View {
if !wizardState.isWindows10OrHigher {
DetailedSection("", description: "Some older systems do not support UEFI boot, such as Windows 7 and below.") {
Toggle("UEFI Boot", isOn: $wizardState.systemBootUefi)
.onChange(of: wizardState.systemBootUefi) { newValue in
if !newValue {
wizardState.systemBootTpm = false
}
}
Toggle("Secure Boot with TPM 2.0", isOn: $wizardState.systemBootTpm)
.disabled(!wizardState.systemBootUefi)
}
}

Expand Down
2 changes: 2 additions & 0 deletions Platform/Shared/VMWizardState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ enum VMWizardOS: String, Identifiable {
@Published var alertMessage: AlertMessage?
@Published var isBusy: Bool = false
@Published var systemBootUefi: Bool = true
@Published var systemBootTpm: Bool = true
@Published var isGuestToolsInstallRequested: Bool = true
@Published var useVirtualization: Bool = false {
didSet {
Expand Down Expand Up @@ -369,6 +370,7 @@ enum VMWizardOS: String, Identifiable {
if operatingSystem == .Windows {
// only change UEFI settings for Windows
config.qemu.hasUefiBoot = systemBootUefi
config.qemu.hasTPMDevice = systemBootTpm
}
if operatingSystem == .Linux && config.displays.first != nil {
// change default display to virtio-gpu if supported
Expand Down
4 changes: 4 additions & 0 deletions Platform/UTMData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ struct AlertMessage: Identifiable {
///
/// This removes stale entries (deleted/not accessible) and duplicate entries
func listRefresh() async {
// create Documents directory if it doesn't exist
if !fileManager.fileExists(atPath: Self.defaultStorageUrl.path) {
try? fileManager.createDirectory(at: Self.defaultStorageUrl, withIntermediateDirectories: false)
}
// wrap stale VMs
var list = virtualMachines
for i in list.indices.reversed() {
Expand Down
10 changes: 7 additions & 3 deletions Platform/VMData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,11 @@ import SwiftUI
var loaded: (any UTMVirtualMachine)?
let config = try UTMQemuConfiguration.load(from: url)
if let qemuConfig = config as? UTMQemuConfiguration {
loaded = try UTMQemuVirtualMachine(packageUrl: url, configuration: qemuConfig, isShortcut: isShortcut)
loaded = try UTMQemuVirtualMachine(packageUrl: url, configuration: qemuConfig, isShortcut: isShortcut(url))
}
#if os(macOS)
if let appleConfig = config as? UTMAppleConfiguration {
loaded = try UTMAppleVirtualMachine(packageUrl: url, configuration: appleConfig, isShortcut: isShortcut)
loaded = try UTMAppleVirtualMachine(packageUrl: url, configuration: appleConfig, isShortcut: isShortcut(url))
}
#endif
guard let vm = loaded else {
Expand Down Expand Up @@ -279,8 +279,12 @@ extension VMData: Hashable {
extension VMData {
/// True if the .utm is loaded outside of the default storage
var isShortcut: Bool {
isShortcut(pathUrl)
}

func isShortcut(_ url: URL) -> Bool {
let defaultStorageUrl = UTMData.defaultStorageUrl.standardizedFileURL
let parentUrl = pathUrl.deletingLastPathComponent().standardizedFileURL
let parentUrl = url.deletingLastPathComponent().standardizedFileURL
return parentUrl != defaultStorageUrl
}

Expand Down
10 changes: 10 additions & 0 deletions Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,14 @@ class VMDisplayAppleDisplayWindowController: VMDisplayAppleWindowController {
override func captureMouseButtonPressed(_ sender: Any) {
appleView!.capturesSystemKeys = captureMouseToolbarButton.state == .on
}

func windowDidEnterFullScreen(_ notification: Notification) {
captureMouseToolbarButton.state = .on
captureMouseButtonPressed(self)
}

func windowDidExitFullScreen(_ notification: Notification) {
captureMouseToolbarButton.state = .off
captureMouseButtonPressed(self)
}
}
Loading

0 comments on commit a2966eb

Please sign in to comment.