data: refactor UTMVirtualMachine to VMData for use in Views
This replaces the functionality of UTMWrappedVirtualMachine and moves some of the presentation logic from UTMVirtualMachine.
This commit is contained in:
parent
aa203e18b7
commit
d498239b38
|
@ -416,7 +416,9 @@ extension UTMQemuVirtualMachine {
|
|||
@MainActor
|
||||
override func updateScreenshot() {
|
||||
ioService?.screenshot(completion: { screenshot in
|
||||
self.screenshot = screenshot
|
||||
Task { @MainActor in
|
||||
self.screenshot = screenshot
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,22 @@ class UTMRegistry: NSObject {
|
|||
return newEntry
|
||||
}
|
||||
|
||||
/// Gets an existing registry entry or create a new entry for a legacy bookmark
|
||||
/// - Parameters:
|
||||
/// - uuid: UUID
|
||||
/// - name: VM name
|
||||
/// - path: VM path string
|
||||
/// - bookmark: VM bookmark
|
||||
/// - Returns: Either an existing registry entry or a new entry
|
||||
func entry(uuid: UUID, name: String, path: String, bookmark: Data? = nil) -> UTMRegistryEntry {
|
||||
if let entry = entries[uuid.uuidString] {
|
||||
return entry
|
||||
}
|
||||
let newEntry = UTMRegistryEntry(uuid: uuid, name: name, path: path, bookmark: bookmark)
|
||||
entries[uuid.uuidString] = newEntry
|
||||
return newEntry
|
||||
}
|
||||
|
||||
/// Get an existing registry entry for a UUID
|
||||
/// - Parameter uuidString: UUID
|
||||
/// - Returns: An existing registry entry or nil if it does not exist
|
||||
|
|
|
@ -50,17 +50,16 @@ import Foundation
|
|||
case macRecoveryIpsw = "MacRecoveryIpsw"
|
||||
}
|
||||
|
||||
init(newFrom vm: UTMVirtualMachine) {
|
||||
init(uuid: UUID, name: String, path: String, bookmark: Data? = nil) {
|
||||
_name = name
|
||||
let package: File?
|
||||
let path = vm.path
|
||||
if let wrappedVM = vm as? UTMWrappedVirtualMachine {
|
||||
package = try? File(path: path.path, bookmark: wrappedVM.bookmark)
|
||||
if let bookmark = bookmark {
|
||||
package = try? File(path: path, bookmark: bookmark)
|
||||
} else {
|
||||
package = try? File(url: path)
|
||||
package = nil
|
||||
}
|
||||
_name = vm.config.name
|
||||
_package = package ?? File(path: path.path)
|
||||
uuid = vm.config.uuid
|
||||
_package = package ?? File(path: path)
|
||||
self.uuid = uuid
|
||||
_isSuspended = false
|
||||
_externalDrives = [:]
|
||||
_sharedDirectories = []
|
||||
|
@ -69,6 +68,13 @@ import Foundation
|
|||
_hasMigratedConfig = false
|
||||
}
|
||||
|
||||
convenience init(newFrom vm: UTMVirtualMachine) {
|
||||
self.init(uuid: vm.config.uuid, name: vm.config.name, path: vm.path.path)
|
||||
if let package = try? File(url: vm.path) {
|
||||
_package = package
|
||||
}
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
_name = try container.decode(String.self, forKey: .name)
|
||||
|
|
|
@ -110,7 +110,7 @@ extension UTMVirtualMachine: ObservableObject {
|
|||
}
|
||||
|
||||
/// Called when we have a duplicate UUID
|
||||
@MainActor func changeUuid(to uuid: UUID, name: String? = nil) {
|
||||
@MainActor func changeUuid(to uuid: UUID, name: String? = nil, copyFromExisting existing: UTMRegistryEntry? = nil) {
|
||||
if let qemuConfig = config.qemuConfig {
|
||||
qemuConfig.information.uuid = uuid
|
||||
if let name = name {
|
||||
|
@ -125,6 +125,9 @@ extension UTMVirtualMachine: ObservableObject {
|
|||
fatalError("Invalid configuration.")
|
||||
}
|
||||
registryEntry = UTMRegistry.shared.entry(for: self)
|
||||
if let existing = existing {
|
||||
registryEntry.update(copying: existing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -132,9 +132,9 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
@MainActor private func handleUTMURL(with components: URLComponents) async throws {
|
||||
func findVM() async -> UTMVirtualMachine? {
|
||||
func findVM() -> VMData? {
|
||||
if let vmName = components.queryItems?.first(where: { $0.name == "name" })?.value {
|
||||
return await data.virtualMachines.first(where: { $0.detailsTitleLabel == vmName })
|
||||
return data.virtualMachines.first(where: { $0.detailsTitleLabel == vmName })
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
@ -143,48 +143,48 @@ struct ContentView: View {
|
|||
if let action = components.host {
|
||||
switch action {
|
||||
case "start":
|
||||
if let vm = await findVM(), vm.state == .vmStopped {
|
||||
if let vm = findVM(), vm.wrapped?.state == .vmStopped {
|
||||
data.run(vm: vm)
|
||||
}
|
||||
break
|
||||
case "stop":
|
||||
if let vm = await findVM(), vm.state == .vmStarted {
|
||||
vm.requestVmStop(force: true)
|
||||
if let vm = findVM(), vm.wrapped?.state == .vmStarted {
|
||||
vm.wrapped!.requestVmStop(force: true)
|
||||
data.stop(vm: vm)
|
||||
}
|
||||
break
|
||||
case "restart":
|
||||
if let vm = await findVM(), vm.state == .vmStarted {
|
||||
vm.requestVmReset()
|
||||
if let vm = findVM(), vm.wrapped?.state == .vmStarted {
|
||||
vm.wrapped!.requestVmReset()
|
||||
}
|
||||
break
|
||||
case "pause":
|
||||
if let vm = await findVM(), vm.state == .vmStarted {
|
||||
if let vm = findVM(), vm.wrapped?.state == .vmStarted {
|
||||
let shouldSaveOnPause: Bool
|
||||
if let vm = vm as? UTMQemuVirtualMachine {
|
||||
if let vm = vm.wrapped as? UTMQemuVirtualMachine {
|
||||
shouldSaveOnPause = !vm.isRunningAsSnapshot
|
||||
} else {
|
||||
shouldSaveOnPause = true
|
||||
}
|
||||
vm.requestVmPause(save: shouldSaveOnPause)
|
||||
vm.wrapped!.requestVmPause(save: shouldSaveOnPause)
|
||||
}
|
||||
case "resume":
|
||||
if let vm = await findVM(), vm.state == .vmPaused {
|
||||
vm.requestVmResume()
|
||||
if let vm = findVM(), vm.wrapped?.state == .vmPaused {
|
||||
vm.wrapped!.requestVmResume()
|
||||
}
|
||||
break
|
||||
case "sendText":
|
||||
if let vm = await findVM(), vm.state == .vmStarted {
|
||||
data.automationSendText(to: vm, urlComponents: components)
|
||||
if let vm = findVM(), vm.wrapped?.state == .vmStarted {
|
||||
data.automationSendText(to: vm.wrapped!, urlComponents: components)
|
||||
}
|
||||
break
|
||||
case "click":
|
||||
if let vm = await findVM(), vm.state == .vmStarted {
|
||||
data.automationSendMouse(to: vm, urlComponents: components)
|
||||
if let vm = findVM(), vm.wrapped?.state == .vmStarted {
|
||||
data.automationSendMouse(to: vm.wrapped!, urlComponents: components)
|
||||
}
|
||||
break
|
||||
case "downloadVM":
|
||||
data.downloadUTMZip(from: components)
|
||||
await data.downloadUTMZip(from: components)
|
||||
break
|
||||
default:
|
||||
return
|
||||
|
@ -199,8 +199,9 @@ extension ContentView: DropDelegate {
|
|||
}
|
||||
|
||||
func performDrop(info: DropInfo) -> Bool {
|
||||
let urls = urlsFrom(info: info)
|
||||
data.busyWorkAsync {
|
||||
for url in await urlsFrom(info: info) {
|
||||
for url in urls {
|
||||
|
||||
try await data.importUTM(from: url)
|
||||
}
|
||||
|
|
|
@ -17,20 +17,20 @@
|
|||
import SwiftUI
|
||||
|
||||
struct UTMUnavailableVMView: View {
|
||||
@ObservedObject var wrappedVM: UTMWrappedVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
@EnvironmentObject private var data: UTMData
|
||||
|
||||
var body: some View {
|
||||
UTMPlaceholderVMView(title: wrappedVM.detailsTitleLabel,
|
||||
subtitle: wrappedVM.detailsSubtitleLabel,
|
||||
UTMPlaceholderVMView(title: vm.detailsTitleLabel,
|
||||
subtitle: vm.detailsSubtitleLabel,
|
||||
progress: nil,
|
||||
imageOverlaySystemName: "questionmark.circle.fill",
|
||||
popover: { WrappedVMDetailsView(path: wrappedVM.path.path, onRemove: remove) },
|
||||
popover: { WrappedVMDetailsView(path: vm.pathUrl.path, onRemove: remove) },
|
||||
onRemove: remove)
|
||||
}
|
||||
|
||||
private func remove() {
|
||||
data.listRemove(vm: wrappedVM)
|
||||
data.listRemove(vm: vm)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,6 @@ fileprivate struct WrappedVMDetailsView: View {
|
|||
|
||||
struct UTMUnavailableVMView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UTMUnavailableVMView(wrappedVM: UTMWrappedVirtualMachine(bookmark: Data(), name: "Wrapped VM", path: URL(fileURLWithPath: "/")))
|
||||
UTMUnavailableVMView(vm: VMData(wrapping: UTMWrappedVirtualMachine(bookmark: Data(), name: "Wrapped VM", path: URL(fileURLWithPath: "/"))))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct VMCardView: View {
|
||||
@ObservedObject var vm: UTMVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
@EnvironmentObject private var data: UTMData
|
||||
|
||||
#if os(macOS)
|
||||
|
@ -47,7 +47,7 @@ struct VMCardView: View {
|
|||
}.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
Spacer()
|
||||
if vm.state == .vmStopped {
|
||||
if vm.isStopped {
|
||||
Button {
|
||||
data.run(vm: vm)
|
||||
} label: {
|
||||
|
@ -113,6 +113,6 @@ struct Logo: View {
|
|||
|
||||
struct VMCardView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VMCardView(vm: UTMVirtualMachine(newConfig: UTMQemuConfiguration(), destinationURL: URL(fileURLWithPath: "/")))
|
||||
VMCardView(vm: VMData(wrapping: UTMVirtualMachine(newConfig: UTMQemuConfiguration(), destinationURL: URL(fileURLWithPath: "/"))))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ enum ConfirmAction: Int, Identifiable {
|
|||
}
|
||||
|
||||
struct VMConfirmActionModifier: ViewModifier {
|
||||
let vm: UTMVirtualMachine
|
||||
let vm: VMData
|
||||
@Binding var confirmAction: ConfirmAction?
|
||||
let onConfirm: () -> Void
|
||||
@EnvironmentObject private var data: UTMData
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct VMContextMenuModifier: ViewModifier {
|
||||
@ObservedObject var vm: UTMVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
@EnvironmentObject private var data: UTMData
|
||||
@State private var showSharePopup = false
|
||||
@State private var confirmAction: ConfirmAction?
|
||||
|
@ -27,7 +27,7 @@ struct VMContextMenuModifier: ViewModifier {
|
|||
content.contextMenu {
|
||||
#if os(macOS)
|
||||
Button {
|
||||
NSWorkspace.shared.activateFileViewerSelecting([vm.path])
|
||||
NSWorkspace.shared.activateFileViewerSelecting([vm.pathUrl])
|
||||
} label: {
|
||||
Label("Show in Finder", systemImage: "folder")
|
||||
}.help("Reveal where the VM is stored.")
|
||||
|
@ -38,14 +38,20 @@ struct VMContextMenuModifier: ViewModifier {
|
|||
data.edit(vm: vm)
|
||||
} label: {
|
||||
Label("Edit", systemImage: "slider.horizontal.3")
|
||||
}.disabled(vm.hasSaveState || vm.state != .vmStopped)
|
||||
}.disabled(vm.hasSuspendState || !vm.isModifyAllowed)
|
||||
.help("Modify settings for this VM.")
|
||||
if vm.hasSaveState || vm.state != .vmStopped {
|
||||
if vm.hasSuspendState || !vm.isStopped {
|
||||
Button {
|
||||
confirmAction = .confirmStopVM
|
||||
} label: {
|
||||
Label("Stop", systemImage: "stop.fill")
|
||||
}.help("Stop the running VM.")
|
||||
} else if !vm.isModifyAllowed { // paused
|
||||
Button {
|
||||
data.run(vm: vm)
|
||||
} label: {
|
||||
Label("Resume", systemImage: "playpause.fill")
|
||||
}.help("Resume running VM.")
|
||||
} else {
|
||||
Divider()
|
||||
|
||||
|
@ -56,7 +62,7 @@ struct VMContextMenuModifier: ViewModifier {
|
|||
}.help("Run the VM in the foreground.")
|
||||
|
||||
#if os(macOS) && arch(arm64)
|
||||
if #available(macOS 13, *), let appleConfig = vm.config.appleConfig, appleConfig.system.boot.operatingSystem == .macOS {
|
||||
if #available(macOS 13, *), let appleConfig = vm.config as? UTMAppleConfiguration, appleConfig.system.boot.operatingSystem == .macOS {
|
||||
Button {
|
||||
appleConfig.system.boot.startUpFromMacOSRecovery = true
|
||||
data.run(vm: vm)
|
||||
|
@ -66,9 +72,9 @@ struct VMContextMenuModifier: ViewModifier {
|
|||
}
|
||||
#endif
|
||||
|
||||
if !vm.config.isAppleVirtualization {
|
||||
if let qemuVM = vm.wrapped as? UTMQemuVirtualMachine {
|
||||
Button {
|
||||
vm.isRunningAsSnapshot = true
|
||||
qemuVM.isRunningAsSnapshot = true
|
||||
data.run(vm: vm)
|
||||
} label: {
|
||||
Label("Run without saving changes", systemImage: "play")
|
||||
|
@ -76,7 +82,7 @@ struct VMContextMenuModifier: ViewModifier {
|
|||
}
|
||||
|
||||
#if os(iOS)
|
||||
if let qemuVM = vm as? UTMQemuVirtualMachine {
|
||||
if let qemuVM = vm.wrapped as? UTMQemuVirtualMachine {
|
||||
Button {
|
||||
qemuVM.isGuestToolsInstallRequested = true
|
||||
} label: {
|
||||
|
@ -100,7 +106,7 @@ struct VMContextMenuModifier: ViewModifier {
|
|||
confirmAction = .confirmMoveVM
|
||||
} label: {
|
||||
Label("Move…", systemImage: "arrow.down.doc")
|
||||
}.disabled(vm.state != .vmStopped)
|
||||
}.disabled(!vm.isModifyAllowed)
|
||||
.help("Move this VM from internal storage to elsewhere.")
|
||||
}
|
||||
#endif
|
||||
|
@ -122,14 +128,14 @@ struct VMContextMenuModifier: ViewModifier {
|
|||
confirmAction = .confirmDeleteShortcut
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}.disabled(vm.state != .vmStopped)
|
||||
}.disabled(!vm.isModifyAllowed)
|
||||
.help("Delete this shortcut. The underlying data will not be deleted.")
|
||||
} else {
|
||||
DestructiveButton {
|
||||
confirmAction = .confirmDeleteVM
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}.disabled(vm.state != .vmStopped)
|
||||
}.disabled(!vm.isModifyAllowed)
|
||||
.help("Delete this VM and all its data.")
|
||||
}
|
||||
}
|
||||
|
@ -140,10 +146,10 @@ struct VMContextMenuModifier: ViewModifier {
|
|||
showSharePopup.toggle()
|
||||
}
|
||||
})
|
||||
.onChange(of: (vm as? UTMQemuVirtualMachine)?.isGuestToolsInstallRequested) { newValue in
|
||||
.onChange(of: (vm.wrapped as? UTMQemuVirtualMachine)?.isGuestToolsInstallRequested) { newValue in
|
||||
if newValue == true {
|
||||
data.busyWorkAsync {
|
||||
try await data.mountSupportTools(for: vm as! UTMQemuVirtualMachine)
|
||||
try await data.mountSupportTools(for: vm.wrapped as! UTMQemuVirtualMachine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct VMDetailsView: View {
|
||||
@ObservedObject var vm: UTMVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
@EnvironmentObject private var data: UTMData
|
||||
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
|
||||
#if !os(macOS)
|
||||
|
@ -62,10 +62,10 @@ struct VMDetailsView: View {
|
|||
.padding([.leading, .trailing])
|
||||
}.padding([.leading, .trailing])
|
||||
#if os(macOS)
|
||||
if let appleVM = vm as? UTMAppleVirtualMachine {
|
||||
if let appleVM = vm.wrapped as? UTMAppleVirtualMachine {
|
||||
VMAppleRemovableDrivesView(vm: appleVM, config: appleVM.appleConfig, registryEntry: appleVM.registryEntry)
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
} else if let qemuVM = vm as? UTMQemuVirtualMachine {
|
||||
} else if let qemuVM = vm.wrapped as? UTMQemuVirtualMachine {
|
||||
VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig)
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
}
|
||||
|
@ -83,13 +83,13 @@ struct VMDetailsView: View {
|
|||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
#if os(macOS)
|
||||
if let appleVM = vm as? UTMAppleVirtualMachine {
|
||||
if let appleVM = vm.wrapped as? UTMAppleVirtualMachine {
|
||||
VMAppleRemovableDrivesView(vm: appleVM, config: appleVM.appleConfig, registryEntry: appleVM.registryEntry)
|
||||
} else if let qemuVM = vm as? UTMQemuVirtualMachine {
|
||||
} else if let qemuVM = vm.wrapped as? UTMQemuVirtualMachine {
|
||||
VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig)
|
||||
}
|
||||
#else
|
||||
let qemuVM = vm as! UTMQemuVirtualMachine
|
||||
let qemuVM = vm.wrapped as! UTMQemuVirtualMachine
|
||||
VMRemovableDrivesView(vm: qemuVM, config: qemuVM.qemuConfig)
|
||||
#endif
|
||||
}.padding([.leading, .trailing, .bottom])
|
||||
|
@ -98,12 +98,12 @@ struct VMDetailsView: View {
|
|||
.modifier(VMOptionalNavigationTitleModifier(vm: vm))
|
||||
.modifier(VMToolbarModifier(vm: vm, bottom: !regularScreenSizeClass))
|
||||
.sheet(isPresented: $data.showSettingsModal) {
|
||||
if let qemuConfig = vm.config.qemuConfig {
|
||||
if let qemuConfig = vm.config as? UTMQemuConfiguration {
|
||||
VMSettingsView(vm: vm, config: qemuConfig)
|
||||
.environmentObject(data)
|
||||
}
|
||||
#if os(macOS)
|
||||
if let appleConfig = vm.config.appleConfig {
|
||||
if let appleConfig = vm.config as? UTMAppleConfiguration {
|
||||
VMSettingsView(vm: vm, config: appleConfig)
|
||||
.environmentObject(data)
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ struct VMDetailsView: View {
|
|||
|
||||
/// Returns just the content under macOS but adds the title on iOS. #3099
|
||||
private struct VMOptionalNavigationTitleModifier: ViewModifier {
|
||||
@ObservedObject var vm: UTMVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
#if os(macOS)
|
||||
|
@ -127,7 +127,7 @@ private struct VMOptionalNavigationTitleModifier: ViewModifier {
|
|||
}
|
||||
|
||||
struct Screenshot: View {
|
||||
@ObservedObject var vm: UTMVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
let large: Bool
|
||||
@EnvironmentObject private var data: UTMData
|
||||
|
||||
|
@ -135,13 +135,13 @@ struct Screenshot: View {
|
|||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.black)
|
||||
if vm.screenshot != nil {
|
||||
if let screenshotImage = vm.screenshotImage {
|
||||
#if os(macOS)
|
||||
Image(nsImage: vm.screenshot!.image)
|
||||
Image(nsImage: screenshotImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
#else
|
||||
Image(uiImage: vm.screenshot!.image)
|
||||
Image(uiImage: screenshotImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
#endif
|
||||
|
@ -151,7 +151,7 @@ struct Screenshot: View {
|
|||
.blendMode(.hardLight)
|
||||
if vm.isBusy {
|
||||
Spinner(size: .large)
|
||||
} else if vm.state == .vmStopped {
|
||||
} else if vm.isStopped {
|
||||
Button(action: { data.run(vm: vm) }, label: {
|
||||
Label("Run", systemImage: "play.circle.fill")
|
||||
.labelStyle(.iconOnly)
|
||||
|
@ -164,7 +164,7 @@ struct Screenshot: View {
|
|||
}
|
||||
|
||||
struct Details: View {
|
||||
@ObservedObject var vm: UTMVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
let sizeLabel: String
|
||||
@EnvironmentObject private var data: UTMData
|
||||
|
||||
|
@ -174,7 +174,7 @@ struct Details: View {
|
|||
HStack {
|
||||
plainLabel("Path", systemImage: "folder")
|
||||
Spacer()
|
||||
Text(vm.path.path)
|
||||
Text(vm.pathUrl.path)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ struct Details: View {
|
|||
.foregroundColor(.secondary)
|
||||
}
|
||||
#if os(macOS)
|
||||
if let appleConfig = vm.config.appleConfig {
|
||||
if let appleConfig = vm.config as? UTMAppleConfiguration {
|
||||
ForEach(appleConfig.serials) { serial in
|
||||
if serial.mode == .ptty {
|
||||
HStack {
|
||||
|
@ -221,21 +221,21 @@ struct Details: View {
|
|||
}
|
||||
}
|
||||
#endif
|
||||
if let qemuConfig = vm.config.qemuConfig {
|
||||
if let qemuConfig = vm.config as? UTMQemuConfiguration {
|
||||
ForEach(qemuConfig.serials) { serial in
|
||||
if serial.mode == .tcpClient {
|
||||
HStack {
|
||||
plainLabel("Serial (Client)", systemImage: "network")
|
||||
Spacer()
|
||||
let address = "\(serial.tcpHostAddress ?? "example.com"):\(serial.tcpPort ?? 1234)"
|
||||
OptionalSelectableText(vm.state == .vmStarted ? address : nil)
|
||||
OptionalSelectableText(vm.wrapped?.state == .vmStarted ? address : nil)
|
||||
}
|
||||
} else if serial.mode == .tcpServer {
|
||||
HStack {
|
||||
plainLabel("Serial (Server)", systemImage: "network")
|
||||
Spacer()
|
||||
let address = "\(serial.tcpPort ?? 1234)"
|
||||
OptionalSelectableText(vm.state == .vmStarted ? address : nil)
|
||||
OptionalSelectableText(vm.wrapped?.state == .vmStarted ? address : nil)
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
|
@ -302,7 +302,7 @@ struct VMDetailsView_Previews: PreviewProvider {
|
|||
@State static private var config = UTMQemuConfiguration()
|
||||
|
||||
static var previews: some View {
|
||||
VMDetailsView(vm: UTMVirtualMachine(newConfig: config, destinationURL: URL(fileURLWithPath: "")))
|
||||
VMDetailsView(vm: VMData(wrapping: UTMVirtualMachine(newConfig: config, destinationURL: URL(fileURLWithPath: ""))))
|
||||
.onAppear {
|
||||
config.sharing.directoryShareMode = .webdav
|
||||
var drive = UTMQemuConfigurationDrive()
|
||||
|
|
|
@ -44,8 +44,8 @@ struct VMNavigationListView: View {
|
|||
|
||||
@ViewBuilder private var listBody: some View {
|
||||
ForEach(data.virtualMachines) { vm in
|
||||
if let wrappedVM = vm as? UTMWrappedVirtualMachine {
|
||||
UTMUnavailableVMView(wrappedVM: wrappedVM)
|
||||
if !vm.isLoaded {
|
||||
UTMUnavailableVMView(vm: vm)
|
||||
} else {
|
||||
if #available(iOS 16, macOS 13, *) {
|
||||
VMCardView(vm: vm)
|
||||
|
|
|
@ -216,7 +216,7 @@ struct VMRemovableDrivesView_Previews: PreviewProvider {
|
|||
@State static private var config = UTMQemuConfiguration()
|
||||
|
||||
static var previews: some View {
|
||||
VMDetailsView(vm: UTMVirtualMachine(newConfig: config, destinationURL: URL(fileURLWithPath: "")))
|
||||
VMDetailsView(vm: VMData(wrapping: UTMVirtualMachine(newConfig: config, destinationURL: URL(fileURLWithPath: ""))))
|
||||
.onAppear {
|
||||
config.sharing.directoryShareMode = .webdav
|
||||
var drive = UTMQemuConfigurationDrive()
|
||||
|
|
|
@ -40,16 +40,16 @@ struct VMShareItemModifier: ViewModifier {
|
|||
|
||||
enum ShareItem {
|
||||
case debugLog(URL)
|
||||
case utmCopy(UTMVirtualMachine)
|
||||
case utmMove(UTMVirtualMachine)
|
||||
case utmCopy(VMData)
|
||||
case utmMove(VMData)
|
||||
case qemuCommand(String)
|
||||
|
||||
func toActivityItem() -> Any {
|
||||
@MainActor func toActivityItem() -> Any {
|
||||
switch self {
|
||||
case .debugLog(let url):
|
||||
return url
|
||||
case .utmCopy(let vm), .utmMove(let vm):
|
||||
return vm.path
|
||||
return vm.pathUrl
|
||||
case .qemuCommand(let command):
|
||||
return command
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import SwiftUI
|
|||
|
||||
// Lots of dirty hacks to work around SwiftUI bugs introduced in Beta 2
|
||||
struct VMToolbarModifier: ViewModifier {
|
||||
@ObservedObject var vm: UTMVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
let bottom: Bool
|
||||
@State private var showSharePopup = false
|
||||
@State private var confirmAction: ConfirmAction?
|
||||
|
@ -55,7 +55,7 @@ struct VMToolbarModifier: ViewModifier {
|
|||
Label("Remove", systemImage: "trash")
|
||||
.labelStyle(.iconOnly)
|
||||
}.help("Remove selected shortcut")
|
||||
.disabled(vm.state != .vmStopped)
|
||||
.disabled(!vm.isModifyAllowed)
|
||||
.padding(.leading, padding)
|
||||
} else {
|
||||
DestructiveButton {
|
||||
|
@ -64,7 +64,7 @@ struct VMToolbarModifier: ViewModifier {
|
|||
Label("Delete", systemImage: "trash")
|
||||
.labelStyle(.iconOnly)
|
||||
}.help("Delete selected VM")
|
||||
.disabled(vm.state != .vmStopped)
|
||||
.disabled(!vm.isModifyAllowed)
|
||||
.padding(.leading, padding)
|
||||
}
|
||||
#if !os(macOS)
|
||||
|
@ -92,7 +92,7 @@ struct VMToolbarModifier: ViewModifier {
|
|||
Label("Move", systemImage: "arrow.down.doc")
|
||||
.labelStyle(.iconOnly)
|
||||
}.help("Move selected VM")
|
||||
.disabled(vm.state != .vmStopped)
|
||||
.disabled(!vm.isModifyAllowed)
|
||||
.padding(.leading, padding)
|
||||
}
|
||||
#endif
|
||||
|
@ -109,7 +109,7 @@ struct VMToolbarModifier: ViewModifier {
|
|||
Spacer()
|
||||
}
|
||||
#endif
|
||||
if vm.hasSaveState || vm.state != .vmStopped {
|
||||
if vm.hasSuspendState || !vm.isStopped {
|
||||
Button {
|
||||
confirmAction = .confirmStopVM
|
||||
} label: {
|
||||
|
@ -138,7 +138,7 @@ struct VMToolbarModifier: ViewModifier {
|
|||
Label("Edit", systemImage: "slider.horizontal.3")
|
||||
.labelStyle(.iconOnly)
|
||||
}.help("Edit selected VM")
|
||||
.disabled(vm.hasSaveState || vm.state != .vmStopped)
|
||||
.disabled(vm.hasSuspendState || !vm.isModifyAllowed)
|
||||
.padding(.leading, padding)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,10 +24,6 @@ import SwiftUI
|
|||
#if canImport(AltKit) && !WITH_QEMU_TCI
|
||||
import AltKit
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
typealias UTMAppleConfiguration = UTMQemuConfiguration
|
||||
typealias UTMAppleVirtualMachine = UTMQemuVirtualMachine
|
||||
#endif
|
||||
|
||||
struct AlertMessage: Identifiable {
|
||||
var message: String
|
||||
|
@ -40,37 +36,37 @@ struct AlertMessage: Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
class UTMData: ObservableObject {
|
||||
@MainActor class UTMData: ObservableObject {
|
||||
|
||||
/// Sandbox location for storing .utm bundles
|
||||
static var defaultStorageUrl: URL {
|
||||
nonisolated static var defaultStorageUrl: URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
}
|
||||
|
||||
/// View: show VM settings
|
||||
@MainActor @Published var showSettingsModal: Bool
|
||||
@Published var showSettingsModal: Bool
|
||||
|
||||
/// View: show new VM wizard
|
||||
@MainActor @Published var showNewVMSheet: Bool
|
||||
@Published var showNewVMSheet: Bool
|
||||
|
||||
/// View: show an alert message
|
||||
@MainActor @Published var alertMessage: AlertMessage?
|
||||
@Published var alertMessage: AlertMessage?
|
||||
|
||||
/// View: show busy spinner
|
||||
@MainActor @Published var busy: Bool
|
||||
@Published var busy: Bool
|
||||
|
||||
/// View: currently selected VM
|
||||
@MainActor @Published var selectedVM: UTMVirtualMachine?
|
||||
@Published var selectedVM: VMData?
|
||||
|
||||
/// View: all VMs listed, we save a bookmark to each when array is modified
|
||||
@MainActor @Published private(set) var virtualMachines: [UTMVirtualMachine] {
|
||||
@Published private(set) var virtualMachines: [VMData] {
|
||||
didSet {
|
||||
listSaveToDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
/// View: all pending VMs listed (ZIP and IPSW downloads)
|
||||
@MainActor @Published private(set) var pendingVMs: [UTMPendingVirtualMachine]
|
||||
@Published private(set) var pendingVMs: [UTMPendingVirtualMachine]
|
||||
|
||||
#if os(macOS)
|
||||
/// View controller for every VM currently active
|
||||
|
@ -84,19 +80,18 @@ class UTMData: ObservableObject {
|
|||
#endif
|
||||
|
||||
/// Shortcut for accessing FileManager.default
|
||||
private var fileManager: FileManager {
|
||||
nonisolated private var fileManager: FileManager {
|
||||
FileManager.default
|
||||
}
|
||||
|
||||
/// Shortcut for accessing storage URL from instance
|
||||
private var documentsURL: URL {
|
||||
nonisolated private var documentsURL: URL {
|
||||
UTMData.defaultStorageUrl
|
||||
}
|
||||
|
||||
/// Queue to run `busyWork` tasks
|
||||
private var busyQueue: DispatchQueue
|
||||
|
||||
@MainActor
|
||||
init() {
|
||||
self.busyQueue = DispatchQueue(label: "UTM Busy Queue", qos: .userInitiated)
|
||||
self.showSettingsModal = false
|
||||
|
@ -115,11 +110,11 @@ class UTMData: ObservableObject {
|
|||
/// This removes stale entries (deleted/not accessible) and duplicate entries
|
||||
func listRefresh() async {
|
||||
// wrap stale VMs
|
||||
var list = await virtualMachines
|
||||
var list = virtualMachines
|
||||
for i in list.indices.reversed() {
|
||||
let vm = list[i]
|
||||
if !fileManager.fileExists(atPath: vm.path.path) {
|
||||
list[i] = await UTMWrappedVirtualMachine(from: vm.registryEntry)
|
||||
if let registryEntry = vm.registryEntry, !fileManager.fileExists(atPath: registryEntry.package.path) {
|
||||
list[i] = VMData(from: registryEntry)
|
||||
}
|
||||
}
|
||||
// now look for and add new VMs in default storage
|
||||
|
@ -127,7 +122,7 @@ class UTMData: ObservableObject {
|
|||
let files = try fileManager.contentsOfDirectory(at: UTMData.defaultStorageUrl, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles)
|
||||
let newFiles = files.filter { newFile in
|
||||
!list.contains { existingVM in
|
||||
existingVM.path.standardizedFileURL == newFile.standardizedFileURL
|
||||
existingVM.pathUrl.standardizedFileURL == newFile.standardizedFileURL
|
||||
}
|
||||
}
|
||||
for file in newFiles {
|
||||
|
@ -137,22 +132,23 @@ class UTMData: ObservableObject {
|
|||
guard UTMVirtualMachine.isVirtualMachine(url: file) else {
|
||||
continue
|
||||
}
|
||||
await Task.yield()
|
||||
let vm = UTMVirtualMachine(url: file)
|
||||
if let vm = vm {
|
||||
let vm = VMData(wrapping: vm)
|
||||
if uuidHasCollision(with: vm, in: list) {
|
||||
if let index = list.firstIndex(where: { $0 is UTMWrappedVirtualMachine && $0.id == vm.id }) {
|
||||
if let index = list.firstIndex(where: { !$0.isLoaded && $0.id == vm.id }) {
|
||||
// we have a stale VM with the same UUID, so we replace that entry with this one
|
||||
list[index] = vm
|
||||
// update the registry with the new bookmark
|
||||
try? await vm.updateRegistryFromConfig()
|
||||
try? await vm.wrapped!.updateRegistryFromConfig()
|
||||
continue
|
||||
} else {
|
||||
// duplicate is not stale so we need a new UUID
|
||||
await uuidRegenerate(for: vm)
|
||||
list.insert(vm, at: 0)
|
||||
uuidRegenerate(for: vm)
|
||||
}
|
||||
} else {
|
||||
list.insert(vm, at: 0)
|
||||
}
|
||||
list.insert(vm, at: 0)
|
||||
} else {
|
||||
logger.error("Failed to create object for \(file)")
|
||||
}
|
||||
|
@ -161,16 +157,16 @@ class UTMData: ObservableObject {
|
|||
logger.error("\(error.localizedDescription)")
|
||||
}
|
||||
// replace the VM list with our new one
|
||||
if await virtualMachines != list {
|
||||
await listReplace(with: list)
|
||||
if virtualMachines != list {
|
||||
listReplace(with: list)
|
||||
}
|
||||
// prune the registry
|
||||
let uuids = list.map({ $0.registryEntry.uuid.uuidString })
|
||||
let uuids = list.compactMap({ $0.registryEntry?.uuid.uuidString })
|
||||
UTMRegistry.shared.prune(exceptFor: Set(uuids))
|
||||
}
|
||||
|
||||
/// Load VM list (and order) from persistent storage
|
||||
@MainActor private func listLoadFromDefaults() {
|
||||
private func listLoadFromDefaults() {
|
||||
let defaults = UserDefaults.standard
|
||||
guard defaults.object(forKey: "VMList") == nil else {
|
||||
listLegacyLoadFromDefaults()
|
||||
|
@ -192,66 +188,58 @@ class UTMData: ObservableObject {
|
|||
guard let entry = UTMRegistry.shared.entry(for: uuidString) else {
|
||||
return nil
|
||||
}
|
||||
let wrappedVM = UTMWrappedVirtualMachine(from: entry)
|
||||
if let vm = wrappedVM.unwrap() {
|
||||
if vm.registryEntry.uuid != wrappedVM.registryEntry.uuid {
|
||||
// we had a duplicate UUID so we change it
|
||||
vm.changeUuid(to: wrappedVM.registryEntry.uuid)
|
||||
}
|
||||
return vm
|
||||
} else {
|
||||
return wrappedVM
|
||||
}
|
||||
let vm = VMData(from: entry)
|
||||
try? vm.load()
|
||||
return vm
|
||||
}
|
||||
}
|
||||
|
||||
/// Load VM list (and order) from persistent storage (legacy)
|
||||
@MainActor private func listLegacyLoadFromDefaults() {
|
||||
private func listLegacyLoadFromDefaults() {
|
||||
let defaults = UserDefaults.standard
|
||||
// legacy path list
|
||||
if let files = defaults.array(forKey: "VMList") as? [String] {
|
||||
virtualMachines = files.uniqued().compactMap({ file in
|
||||
let url = documentsURL.appendingPathComponent(file, isDirectory: true)
|
||||
return UTMVirtualMachine(url: url)
|
||||
if let wrapped = UTMVirtualMachine(url: url) {
|
||||
return VMData(wrapping: wrapped)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
// bookmark list
|
||||
if let list = defaults.array(forKey: "VMList") {
|
||||
virtualMachines = list.compactMap { item in
|
||||
var wrappedVM: UTMWrappedVirtualMachine?
|
||||
let vm: VMData?
|
||||
if let bookmark = item as? Data {
|
||||
wrappedVM = UTMWrappedVirtualMachine(bookmark: bookmark)
|
||||
vm = VMData(bookmark: bookmark)
|
||||
} else if let dict = item as? [String: Any] {
|
||||
wrappedVM = UTMWrappedVirtualMachine(from: dict)
|
||||
}
|
||||
if let wrappedVM = wrappedVM, let vm = wrappedVM.unwrap() {
|
||||
// legacy VMs don't have UUID stored so we made a fake UUID
|
||||
if wrappedVM.registryEntry.uuid != vm.registryEntry.uuid {
|
||||
UTMRegistry.shared.remove(entry: wrappedVM.registryEntry)
|
||||
}
|
||||
return vm
|
||||
vm = VMData(from: dict)
|
||||
} else {
|
||||
return wrappedVM
|
||||
vm = nil
|
||||
}
|
||||
try? vm?.load()
|
||||
return vm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Save VM list (and order) to persistent storage
|
||||
@MainActor private func listSaveToDefaults() {
|
||||
private func listSaveToDefaults() {
|
||||
let defaults = UserDefaults.standard
|
||||
let wrappedVMs = virtualMachines.map { $0.registryEntry.uuid.uuidString }
|
||||
let wrappedVMs = virtualMachines.map { $0.id.uuidString }
|
||||
defaults.set(wrappedVMs, forKey: "VMEntryList")
|
||||
}
|
||||
|
||||
@MainActor private func listReplace(with vms: [UTMVirtualMachine]) {
|
||||
private func listReplace(with vms: [VMData]) {
|
||||
virtualMachines = vms
|
||||
}
|
||||
|
||||
/// Add VM to list
|
||||
/// - Parameter vm: VM to add
|
||||
/// - Parameter at: Optional index to add to, otherwise will be added to the end
|
||||
@MainActor private func listAdd(vm: UTMVirtualMachine, at index: Int? = nil) {
|
||||
private func listAdd(vm: VMData, at index: Int? = nil) {
|
||||
if uuidHasCollision(with: vm) {
|
||||
uuidRegenerate(for: vm)
|
||||
}
|
||||
|
@ -264,14 +252,14 @@ class UTMData: ObservableObject {
|
|||
|
||||
/// Select VM in list
|
||||
/// - Parameter vm: VM to select
|
||||
@MainActor public func listSelect(vm: UTMVirtualMachine) {
|
||||
public func listSelect(vm: VMData) {
|
||||
selectedVM = vm
|
||||
}
|
||||
|
||||
/// Remove a VM from list
|
||||
/// - Parameter vm: VM to remove
|
||||
/// - Returns: Index of item removed or nil if already removed
|
||||
@MainActor @discardableResult public func listRemove(vm: UTMVirtualMachine) -> Int? {
|
||||
@discardableResult public func listRemove(vm: VMData) -> Int? {
|
||||
let index = virtualMachines.firstIndex(of: vm)
|
||||
if let index = index {
|
||||
virtualMachines.remove(at: index)
|
||||
|
@ -286,7 +274,7 @@ class UTMData: ObservableObject {
|
|||
/// Add pending VM to list
|
||||
/// - Parameter pendingVM: Pending VM to add
|
||||
/// - Parameter at: Optional index to add to, otherwise will be added to the end
|
||||
@MainActor private func listAdd(pendingVM: UTMPendingVirtualMachine, at index: Int? = nil) {
|
||||
private func listAdd(pendingVM: UTMPendingVirtualMachine, at index: Int? = nil) {
|
||||
if let index = index {
|
||||
pendingVMs.insert(pendingVM, at: index)
|
||||
} else {
|
||||
|
@ -297,7 +285,7 @@ class UTMData: ObservableObject {
|
|||
/// Remove pending VM from list
|
||||
/// - Parameter pendingVM: Pending VM to remove
|
||||
/// - Returns: Index of item removed or nil if already removed
|
||||
@MainActor @discardableResult private func listRemove(pendingVM: UTMPendingVirtualMachine) -> Int? {
|
||||
@discardableResult private func listRemove(pendingVM: UTMPendingVirtualMachine) -> Int? {
|
||||
let index = pendingVMs.firstIndex(where: { $0.id == pendingVM.id })
|
||||
if let index = index {
|
||||
pendingVMs.remove(at: index)
|
||||
|
@ -309,7 +297,7 @@ class UTMData: ObservableObject {
|
|||
/// - Parameters:
|
||||
/// - fromOffsets: Offsets from move from
|
||||
/// - toOffset: Offsets to move to
|
||||
@MainActor func listMove(fromOffsets: IndexSet, toOffset: Int) {
|
||||
func listMove(fromOffsets: IndexSet, toOffset: Int) {
|
||||
virtualMachines.move(fromOffsets: fromOffsets, toOffset: toOffset)
|
||||
}
|
||||
|
||||
|
@ -318,7 +306,7 @@ class UTMData: ObservableObject {
|
|||
/// Generate a unique VM name
|
||||
/// - Parameter base: Base name
|
||||
/// - Returns: Unique name for a non-existing item in the default storage path
|
||||
func newDefaultVMName(base: String = NSLocalizedString("Virtual Machine", comment: "UTMData")) -> String {
|
||||
nonisolated func newDefaultVMName(base: String = NSLocalizedString("Virtual Machine", comment: "UTMData")) -> String {
|
||||
let nameForId = { (i: Int) in i <= 1 ? base : "\(base) \(i)" }
|
||||
for i in 1..<1000 {
|
||||
let name = nameForId(i)
|
||||
|
@ -336,7 +324,7 @@ class UTMData: ObservableObject {
|
|||
/// - destUrl: Destination directory where duplicates will be checked
|
||||
/// - withExtension: Optionally change the file extension
|
||||
/// - Returns: Unique filename that is not used in the destUrl
|
||||
static func newImage(from sourceUrl: URL, to destUrl: URL, withExtension: String? = nil) -> URL {
|
||||
nonisolated static func newImage(from sourceUrl: URL, to destUrl: URL, withExtension: String? = nil) -> URL {
|
||||
let name = sourceUrl.deletingPathExtension().lastPathComponent
|
||||
let ext = withExtension ?? sourceUrl.pathExtension
|
||||
let strFromInt = { (i: Int) in i == 1 ? "" : "-\(i)" }
|
||||
|
@ -358,20 +346,20 @@ class UTMData: ObservableObject {
|
|||
|
||||
// MARK: - Other view states
|
||||
|
||||
@MainActor private func setBusyIndicator(_ busy: Bool) {
|
||||
private func setBusyIndicator(_ busy: Bool) {
|
||||
self.busy = busy
|
||||
}
|
||||
|
||||
@MainActor func showErrorAlert(message: String) {
|
||||
func showErrorAlert(message: String) {
|
||||
alertMessage = AlertMessage(message)
|
||||
}
|
||||
|
||||
@MainActor func newVM() {
|
||||
func newVM() {
|
||||
showSettingsModal = false
|
||||
showNewVMSheet = true
|
||||
}
|
||||
|
||||
@MainActor func showSettingsForCurrentVM() {
|
||||
func showSettingsForCurrentVM() {
|
||||
#if os(iOS)
|
||||
// SwiftUI bug: cannot show modal at the same time as changing selected VM or it breaks
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
|
@ -386,9 +374,9 @@ class UTMData: ObservableObject {
|
|||
|
||||
/// Save an existing VM to disk
|
||||
/// - Parameter vm: VM to save
|
||||
@MainActor func save(vm: UTMVirtualMachine) async throws {
|
||||
func save(vm: VMData) async throws {
|
||||
do {
|
||||
try await vm.saveUTM()
|
||||
try await vm.save()
|
||||
} catch {
|
||||
// refresh the VM object as it is now stale
|
||||
let origError = error
|
||||
|
@ -396,14 +384,15 @@ class UTMData: ObservableObject {
|
|||
try discardChanges(for: vm)
|
||||
} catch {
|
||||
// if we can't discard changes, recreate the VM from scratch
|
||||
let path = vm.path
|
||||
guard let newVM = UTMVirtualMachine(url: path) else {
|
||||
let path = vm.pathUrl
|
||||
guard let newWrapped = UTMVirtualMachine(url: path) else {
|
||||
logger.debug("Cannot create new object for \(path.path)")
|
||||
throw origError
|
||||
}
|
||||
let index = await listRemove(vm: vm)
|
||||
await listAdd(vm: newVM, at: index)
|
||||
await listSelect(vm: newVM)
|
||||
let newVM = VMData(wrapping: newWrapped)
|
||||
let index = listRemove(vm: vm)
|
||||
listAdd(vm: newVM, at: index)
|
||||
listSelect(vm: newVM)
|
||||
}
|
||||
throw origError
|
||||
}
|
||||
|
@ -411,11 +400,11 @@ class UTMData: ObservableObject {
|
|||
|
||||
/// Discard changes to VM configuration
|
||||
/// - Parameter vm: VM configuration to discard
|
||||
@MainActor func discardChanges(for vm: UTMVirtualMachine? = nil) throws {
|
||||
if let vm = vm {
|
||||
try vm.reloadConfiguration()
|
||||
func discardChanges(for vm: VMData) throws {
|
||||
if let wrapped = vm.wrapped {
|
||||
try wrapped.reloadConfiguration()
|
||||
if uuidHasCollision(with: vm) {
|
||||
vm.changeUuid(to: UUID())
|
||||
wrapped.changeUuid(to: UUID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -423,61 +412,54 @@ class UTMData: ObservableObject {
|
|||
/// Save a new VM to disk
|
||||
/// - Parameters:
|
||||
/// - config: New VM configuration
|
||||
func create<Config: UTMConfiguration>(config: Config) async throws -> UTMVirtualMachine {
|
||||
guard await !virtualMachines.contains(where: { !$0.isShortcut && $0.config.name == config.information.name }) else {
|
||||
throw NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData")
|
||||
}
|
||||
let vm: UTMVirtualMachine
|
||||
if config is UTMQemuConfiguration {
|
||||
vm = UTMQemuVirtualMachine(newConfig: config, destinationURL: Self.defaultStorageUrl)
|
||||
} else if config is UTMAppleConfiguration {
|
||||
vm = UTMAppleVirtualMachine(newConfig: config, destinationURL: Self.defaultStorageUrl)
|
||||
} else {
|
||||
fatalError("Unknown configuration.")
|
||||
func create<Config: UTMConfiguration>(config: Config) async throws -> VMData {
|
||||
guard !virtualMachines.contains(where: { !$0.isShortcut && $0.config?.information.name == config.information.name }) else {
|
||||
throw UTMDataError.virtualMachineAlreadyExists
|
||||
}
|
||||
let vm = VMData(creatingFromConfig: config, destinationUrl: Self.defaultStorageUrl)
|
||||
try await save(vm: vm)
|
||||
await listAdd(vm: vm)
|
||||
await listSelect(vm: vm)
|
||||
listAdd(vm: vm)
|
||||
listSelect(vm: vm)
|
||||
return vm
|
||||
}
|
||||
|
||||
/// Delete a VM from disk
|
||||
/// - Parameter vm: VM to delete
|
||||
/// - Returns: Index of item removed in VM list or nil if not in list
|
||||
@discardableResult func delete(vm: UTMVirtualMachine, alsoRegistry: Bool = true) async throws -> Int? {
|
||||
if let _ = vm as? UTMWrappedVirtualMachine {
|
||||
} else {
|
||||
try fileManager.removeItem(at: vm.path)
|
||||
@discardableResult func delete(vm: VMData, alsoRegistry: Bool = true) async throws -> Int? {
|
||||
if vm.isLoaded {
|
||||
try fileManager.removeItem(at: vm.pathUrl)
|
||||
}
|
||||
|
||||
// close any open window
|
||||
close(vm: vm)
|
||||
|
||||
if alsoRegistry {
|
||||
UTMRegistry.shared.remove(entry: vm.registryEntry)
|
||||
if alsoRegistry, let registryEntry = vm.registryEntry {
|
||||
UTMRegistry.shared.remove(entry: registryEntry)
|
||||
}
|
||||
return await listRemove(vm: vm)
|
||||
return listRemove(vm: vm)
|
||||
}
|
||||
|
||||
/// Save a copy of the VM and all data to default storage location
|
||||
/// - Parameter vm: VM to clone
|
||||
/// - Returns: The new VM
|
||||
@discardableResult func clone(vm: UTMVirtualMachine) async throws -> UTMVirtualMachine {
|
||||
@discardableResult func clone(vm: VMData) async throws -> VMData {
|
||||
let newName: String = newDefaultVMName(base: vm.detailsTitleLabel)
|
||||
let newPath = UTMVirtualMachine.virtualMachinePath(newName, inParentURL: documentsURL)
|
||||
|
||||
try copyItemWithCopyfile(at: vm.path, to: newPath)
|
||||
guard let newVM = UTMVirtualMachine(url: newPath) else {
|
||||
throw NSLocalizedString("Failed to clone VM.", comment: "UTMData")
|
||||
try copyItemWithCopyfile(at: vm.pathUrl, to: newPath)
|
||||
guard let wrapped = UTMVirtualMachine(url: newPath) else {
|
||||
throw UTMDataError.cloneFailed
|
||||
}
|
||||
await newVM.changeUuid(to: UUID(), name: newName)
|
||||
try await newVM.saveUTM()
|
||||
var index = await virtualMachines.firstIndex(of: vm)
|
||||
wrapped.changeUuid(to: UUID(), name: newName)
|
||||
let newVM = VMData(wrapping: wrapped)
|
||||
try await newVM.save()
|
||||
var index = virtualMachines.firstIndex(of: vm)
|
||||
if index != nil {
|
||||
index! += 1
|
||||
}
|
||||
await listAdd(vm: newVM, at: index)
|
||||
await listSelect(vm: newVM)
|
||||
listAdd(vm: newVM, at: index)
|
||||
listSelect(vm: newVM)
|
||||
return newVM
|
||||
}
|
||||
|
||||
|
@ -485,8 +467,8 @@ class UTMData: ObservableObject {
|
|||
/// - Parameters:
|
||||
/// - vm: VM to copy
|
||||
/// - url: Location to copy to (must be writable)
|
||||
func export(vm: UTMVirtualMachine, to url: URL) throws {
|
||||
let sourceUrl = vm.path
|
||||
func export(vm: VMData, to url: URL) throws {
|
||||
let sourceUrl = vm.pathUrl
|
||||
if fileManager.fileExists(atPath: url.path) {
|
||||
try fileManager.removeItem(at: url)
|
||||
}
|
||||
|
@ -497,28 +479,27 @@ class UTMData: ObservableObject {
|
|||
/// - Parameters:
|
||||
/// - vm: VM to move
|
||||
/// - url: Location to move to (must be writable)
|
||||
func move(vm: UTMVirtualMachine, to url: URL) async throws {
|
||||
func move(vm: VMData, to url: URL) async throws {
|
||||
try export(vm: vm, to: url)
|
||||
guard let newVM = UTMVirtualMachine(url: url) else {
|
||||
throw NSLocalizedString("Unable to add a shortcut to the new location.", comment: "UTMData")
|
||||
guard let wrapped = UTMVirtualMachine(url: url) else {
|
||||
throw UTMDataError.shortcutCreationFailed
|
||||
}
|
||||
await MainActor.run {
|
||||
newVM.isShortcut = true
|
||||
}
|
||||
try await newVM.updateRegistryFromConfig()
|
||||
try await newVM.accessShortcut()
|
||||
wrapped.isShortcut = true
|
||||
try await wrapped.updateRegistryFromConfig()
|
||||
try await wrapped.accessShortcut()
|
||||
let newVM = VMData(wrapping: wrapped)
|
||||
|
||||
let oldSelected = await selectedVM
|
||||
let oldSelected = selectedVM
|
||||
let index = try await delete(vm: vm, alsoRegistry: false)
|
||||
await listAdd(vm: newVM, at: index)
|
||||
listAdd(vm: newVM, at: index)
|
||||
if oldSelected == vm {
|
||||
await listSelect(vm: newVM)
|
||||
listSelect(vm: newVM)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open settings modal
|
||||
/// - Parameter vm: VM to edit settings
|
||||
@MainActor func edit(vm: UTMVirtualMachine) {
|
||||
func edit(vm: VMData) {
|
||||
listSelect(vm: vm)
|
||||
showNewVMSheet = false
|
||||
showSettingsForCurrentVM()
|
||||
|
@ -526,21 +507,22 @@ class UTMData: ObservableObject {
|
|||
|
||||
/// Copy configuration but not data from existing VM to a new VM
|
||||
/// - Parameter vm: Existing VM to copy configuration from
|
||||
@MainActor func template(vm: UTMVirtualMachine) async throws {
|
||||
let copy = try UTMQemuConfiguration.load(from: vm.path)
|
||||
func template(vm: VMData) async throws {
|
||||
let copy = try UTMQemuConfiguration.load(from: vm.pathUrl)
|
||||
if let copy = copy as? UTMQemuConfiguration {
|
||||
copy.information.name = self.newDefaultVMName(base: copy.information.name)
|
||||
copy.information.uuid = UUID()
|
||||
copy.drives = []
|
||||
_ = try await create(config: copy)
|
||||
} else if let copy = copy as? UTMAppleConfiguration {
|
||||
}
|
||||
#if os(macOS)
|
||||
if let copy = copy as? UTMAppleConfiguration {
|
||||
copy.information.name = self.newDefaultVMName(base: copy.information.name)
|
||||
copy.information.uuid = UUID()
|
||||
copy.drives = []
|
||||
_ = try await create(config: copy)
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
#endif
|
||||
showSettingsForCurrentVM()
|
||||
}
|
||||
|
||||
|
@ -549,8 +531,8 @@ class UTMData: ObservableObject {
|
|||
/// Calculate total size of VM and data
|
||||
/// - Parameter vm: VM to calculate size
|
||||
/// - Returns: Size in bytes
|
||||
func computeSize(for vm: UTMVirtualMachine) -> Int64 {
|
||||
let path = vm.path
|
||||
func computeSize(for vm: VMData) -> Int64 {
|
||||
let path = vm.pathUrl
|
||||
guard let enumerator = fileManager.enumerator(at: path, includingPropertiesForKeys: [.totalFileSizeKey]) else {
|
||||
logger.error("failed to create enumerator for \(path)")
|
||||
return 0
|
||||
|
@ -596,51 +578,44 @@ class UTMData: ObservableObject {
|
|||
let fileBasePath = url.deletingLastPathComponent()
|
||||
let fileName = url.lastPathComponent
|
||||
let dest = documentsURL.appendingPathComponent(fileName, isDirectory: true)
|
||||
if let vm = await virtualMachines.first(where: { vm -> Bool in
|
||||
return vm.path.standardizedFileURL == url.standardizedFileURL
|
||||
if let vm = virtualMachines.first(where: { vm -> Bool in
|
||||
return vm.pathUrl.standardizedFileURL == url.standardizedFileURL
|
||||
}) {
|
||||
logger.info("found existing vm!")
|
||||
if let wrappedVM = vm as? UTMWrappedVirtualMachine {
|
||||
if !vm.isLoaded {
|
||||
logger.info("existing vm is wrapped")
|
||||
await MainActor.run {
|
||||
if let unwrappedVM = wrappedVM.unwrap() {
|
||||
let index = listRemove(vm: wrappedVM)
|
||||
listAdd(vm: unwrappedVM, at: index)
|
||||
listSelect(vm: unwrappedVM)
|
||||
}
|
||||
}
|
||||
try vm.load()
|
||||
} else {
|
||||
logger.info("existing vm is not wrapped")
|
||||
await listSelect(vm: vm)
|
||||
listSelect(vm: vm)
|
||||
}
|
||||
return
|
||||
}
|
||||
// check if VM is valid
|
||||
guard let _ = UTMVirtualMachine(url: url) else {
|
||||
throw NSLocalizedString("Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM.", comment: "UTMData")
|
||||
throw UTMDataError.importFailed
|
||||
}
|
||||
let vm: UTMVirtualMachine?
|
||||
let wrapped: UTMVirtualMachine?
|
||||
if (fileBasePath.resolvingSymlinksInPath().path == documentsURL.appendingPathComponent("Inbox", isDirectory: true).path) {
|
||||
logger.info("moving from Inbox")
|
||||
try fileManager.moveItem(at: url, to: dest)
|
||||
vm = UTMVirtualMachine(url: dest)
|
||||
wrapped = UTMVirtualMachine(url: dest)
|
||||
} else if asShortcut {
|
||||
logger.info("loading as a shortcut")
|
||||
vm = UTMVirtualMachine(url: url)
|
||||
await MainActor.run {
|
||||
vm?.isShortcut = true
|
||||
}
|
||||
try await vm?.accessShortcut()
|
||||
wrapped = UTMVirtualMachine(url: url)
|
||||
wrapped?.isShortcut = true
|
||||
try await wrapped?.accessShortcut()
|
||||
} else {
|
||||
logger.info("copying to Documents")
|
||||
try fileManager.copyItem(at: url, to: dest)
|
||||
vm = UTMVirtualMachine(url: dest)
|
||||
wrapped = UTMVirtualMachine(url: dest)
|
||||
}
|
||||
guard let vm = vm else {
|
||||
throw NSLocalizedString("Failed to parse imported VM.", comment: "UTMData")
|
||||
guard let wrapped = wrapped else {
|
||||
throw UTMDataError.importParseFailed
|
||||
}
|
||||
await listAdd(vm: vm)
|
||||
await listSelect(vm: vm)
|
||||
let vm = VMData(wrapping: wrapped)
|
||||
listAdd(vm: vm)
|
||||
listSelect(vm: vm)
|
||||
}
|
||||
|
||||
func copyItemWithCopyfile(at srcURL: URL, to dstURL: URL) throws {
|
||||
|
@ -658,30 +633,29 @@ class UTMData: ObservableObject {
|
|||
/// Create a new VM using configuration and downloaded IPSW
|
||||
/// - Parameter config: Apple VM configuration
|
||||
@available(macOS 12, *)
|
||||
@MainActor func downloadIPSW(using config: UTMAppleConfiguration) {
|
||||
func downloadIPSW(using config: UTMAppleConfiguration) async {
|
||||
let task = UTMDownloadIPSWTask(for: config)
|
||||
guard !virtualMachines.contains(where: { !$0.isShortcut && $0.config.name == config.information.name }) else {
|
||||
guard !virtualMachines.contains(where: { !$0.isShortcut && $0.config?.information.name == config.information.name }) else {
|
||||
showErrorAlert(message: NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData"))
|
||||
return
|
||||
}
|
||||
listAdd(pendingVM: task.pendingVM)
|
||||
Task {
|
||||
do {
|
||||
if let vm = try await task.download() {
|
||||
try await self.save(vm: vm)
|
||||
listAdd(vm: vm)
|
||||
}
|
||||
} catch {
|
||||
showErrorAlert(message: error.localizedDescription)
|
||||
do {
|
||||
if let wrapped = try await task.download() {
|
||||
let vm = VMData(wrapping: wrapped)
|
||||
try await self.save(vm: vm)
|
||||
listAdd(vm: vm)
|
||||
}
|
||||
listRemove(pendingVM: task.pendingVM)
|
||||
} catch {
|
||||
showErrorAlert(message: error.localizedDescription)
|
||||
}
|
||||
listRemove(pendingVM: task.pendingVM)
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Create a new VM by downloading a .zip and extracting it
|
||||
/// - Parameter components: Download URL components
|
||||
@MainActor func downloadUTMZip(from components: URLComponents) {
|
||||
func downloadUTMZip(from components: URLComponents) async {
|
||||
guard let urlParameter = components.queryItems?.first(where: { $0.name == "url" })?.value,
|
||||
let url = URL(string: urlParameter) else {
|
||||
showErrorAlert(message: NSLocalizedString("Failed to parse download URL.", comment: "UTMData"))
|
||||
|
@ -689,34 +663,32 @@ class UTMData: ObservableObject {
|
|||
}
|
||||
let task = UTMDownloadVMTask(for: url)
|
||||
listAdd(pendingVM: task.pendingVM)
|
||||
Task {
|
||||
do {
|
||||
if let vm = try await task.download() {
|
||||
listAdd(vm: vm)
|
||||
}
|
||||
} catch {
|
||||
showErrorAlert(message: error.localizedDescription)
|
||||
do {
|
||||
if let wrapped = try await task.download() {
|
||||
let vm = VMData(wrapping: wrapped)
|
||||
try await self.save(vm: vm)
|
||||
listAdd(vm: vm)
|
||||
}
|
||||
listRemove(pendingVM: task.pendingVM)
|
||||
} catch {
|
||||
showErrorAlert(message: error.localizedDescription)
|
||||
}
|
||||
listRemove(pendingVM: task.pendingVM)
|
||||
}
|
||||
|
||||
@MainActor func mountSupportTools(for vm: UTMQemuVirtualMachine) async throws {
|
||||
func mountSupportTools(for vm: UTMQemuVirtualMachine) async throws {
|
||||
let task = UTMDownloadSupportToolsTask(for: vm)
|
||||
if task.hasExistingSupportTools {
|
||||
vm.isGuestToolsInstallRequested = false
|
||||
_ = try await task.mountTools()
|
||||
} else {
|
||||
listAdd(pendingVM: task.pendingVM)
|
||||
Task {
|
||||
do {
|
||||
_ = try await task.download()
|
||||
} catch {
|
||||
showErrorAlert(message: error.localizedDescription)
|
||||
}
|
||||
vm.isGuestToolsInstallRequested = false
|
||||
listRemove(pendingVM: task.pendingVM)
|
||||
do {
|
||||
_ = try await task.download()
|
||||
} catch {
|
||||
showErrorAlert(message: error.localizedDescription)
|
||||
}
|
||||
vm.isGuestToolsInstallRequested = false
|
||||
listRemove(pendingVM: task.pendingVM)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -759,25 +731,27 @@ class UTMData: ObservableObject {
|
|||
|
||||
// MARK: - UUID migration
|
||||
|
||||
@MainActor private func uuidHasCollision(with vm: UTMVirtualMachine) -> Bool {
|
||||
private func uuidHasCollision(with vm: VMData) -> Bool {
|
||||
return uuidHasCollision(with: vm, in: virtualMachines)
|
||||
}
|
||||
|
||||
private func uuidHasCollision(with vm: UTMVirtualMachine, in list: [UTMVirtualMachine]) -> Bool {
|
||||
private func uuidHasCollision(with vm: VMData, in list: [VMData]) -> Bool {
|
||||
for otherVM in list {
|
||||
if otherVM == vm {
|
||||
return false
|
||||
} else if otherVM.registryEntry.uuid == vm.registryEntry.uuid {
|
||||
} else if let lhs = otherVM.registryEntry?.uuid, let rhs = vm.registryEntry?.uuid, lhs == rhs {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@MainActor private func uuidRegenerate(for vm: UTMVirtualMachine) {
|
||||
let oldEntry = vm.registryEntry
|
||||
vm.changeUuid(to: UUID())
|
||||
vm.registryEntry.update(copying: oldEntry)
|
||||
private func uuidRegenerate(for vm: VMData) {
|
||||
guard let vm = vm.wrapped else {
|
||||
return
|
||||
}
|
||||
let previous = vm.registryEntry
|
||||
vm.changeUuid(to: UUID(), copyFromExisting: previous)
|
||||
}
|
||||
|
||||
// MARK: - Other utility functions
|
||||
|
@ -846,7 +820,7 @@ class UTMData: ObservableObject {
|
|||
/// - Parameters:
|
||||
/// - vm: VM to send mouse/tablet coordinates to
|
||||
/// - components: Data (see UTM Wiki for details)
|
||||
@MainActor func automationSendMouse(to vm: UTMVirtualMachine, urlComponents components: URLComponents) {
|
||||
func automationSendMouse(to vm: UTMVirtualMachine, urlComponents components: URLComponents) {
|
||||
guard let qemuVm = vm as? UTMQemuVirtualMachine else { return } // FIXME: implement for Apple VM
|
||||
guard !qemuVm.qemuConfig.displays.isEmpty else { return }
|
||||
guard let queryItems = components.queryItems else { return }
|
||||
|
@ -938,9 +912,9 @@ class UTMData: ObservableObject {
|
|||
ServerManager.shared.stopDiscovering()
|
||||
}
|
||||
if event.wait(timeout: .now() + 10) == .timedOut {
|
||||
throw NSLocalizedString("Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled.", comment: "UTMData")
|
||||
throw UTMDataError.altServerNotFound
|
||||
} else if let error = connectError {
|
||||
throw String.localizedStringWithFormat(NSLocalizedString("AltJIT error: %@", comment: "UTMData"), error.localizedDescription)
|
||||
throw UTMDataError.altJitError(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -969,15 +943,15 @@ class UTMData: ObservableObject {
|
|||
Main.jitAvailable = true
|
||||
}
|
||||
} catch is DecodingError {
|
||||
throw NSLocalizedString("Failed to decode JitStreamer response.", comment: "ContentView")
|
||||
throw UTMDataError.jitStreamerDecodeFailed
|
||||
} catch {
|
||||
throw NSLocalizedString("Failed to attach to JitStreamer.", comment: "ContentView")
|
||||
throw UTMDataError.jitStreamerAttachFailed
|
||||
}
|
||||
if let attachError = attachError {
|
||||
throw attachError
|
||||
}
|
||||
} else {
|
||||
throw String.localizedStringWithFormat(NSLocalizedString("Invalid JitStreamer attach URL:\n%@", comment: "ContentView"), urlString)
|
||||
throw UTMDataError.jitStreamerUrlInvalid(urlString)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -987,3 +961,44 @@ class UTMData: ObservableObject {
|
|||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
enum UTMDataError: Error {
|
||||
case virtualMachineAlreadyExists
|
||||
case cloneFailed
|
||||
case shortcutCreationFailed
|
||||
case importFailed
|
||||
case importParseFailed
|
||||
case altServerNotFound
|
||||
case altJitError(String)
|
||||
case jitStreamerDecodeFailed
|
||||
case jitStreamerAttachFailed
|
||||
case jitStreamerUrlInvalid(String)
|
||||
}
|
||||
|
||||
extension UTMDataError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .virtualMachineAlreadyExists:
|
||||
return NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData")
|
||||
case .cloneFailed:
|
||||
return NSLocalizedString("Failed to clone VM.", comment: "UTMData")
|
||||
case .shortcutCreationFailed:
|
||||
return NSLocalizedString("Unable to add a shortcut to the new location.", comment: "UTMData")
|
||||
case .importFailed:
|
||||
return NSLocalizedString("Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM.", comment: "UTMData")
|
||||
case .importParseFailed:
|
||||
return NSLocalizedString("Failed to parse imported VM.", comment: "UTMData")
|
||||
case .altServerNotFound:
|
||||
return NSLocalizedString("Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled.", comment: "UTMData")
|
||||
case .altJitError(let message):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("AltJIT error: %@", comment: "UTMData"), message)
|
||||
case .jitStreamerDecodeFailed:
|
||||
return NSLocalizedString("Failed to decode JitStreamer response.", comment: "UTMData")
|
||||
case .jitStreamerAttachFailed:
|
||||
return NSLocalizedString("Failed to attach to JitStreamer.", comment: "UTMData")
|
||||
case .jitStreamerUrlInvalid(let urlString):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("Invalid JitStreamer attach URL:\n%@", comment: "UTMData"), urlString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,401 @@
|
|||
//
|
||||
// Copyright © 2023 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 Combine
|
||||
import SwiftUI
|
||||
|
||||
/// Model wrapping a single UTMVirtualMachine for use in views
|
||||
@MainActor class VMData: ObservableObject {
|
||||
/// Underlying virtual machine
|
||||
private(set) var wrapped: UTMVirtualMachine? {
|
||||
willSet {
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
didSet {
|
||||
subscribeToChildren()
|
||||
}
|
||||
}
|
||||
|
||||
/// Virtual machine configuration
|
||||
var config: (any UTMConfiguration)? {
|
||||
wrapped?.config.wrappedValue as? (any UTMConfiguration)
|
||||
}
|
||||
|
||||
/// Current path of the VM
|
||||
var pathUrl: URL {
|
||||
if let wrapped = wrapped {
|
||||
return wrapped.path
|
||||
} else if let registryEntry = registryEntry {
|
||||
return registryEntry.package.url
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
/// Virtual machine state
|
||||
var registryEntry: UTMRegistryEntry? {
|
||||
wrapped?.registryEntry ??
|
||||
registryEntryWrapped
|
||||
}
|
||||
|
||||
/// Registry entry before loading
|
||||
private var registryEntryWrapped: UTMRegistryEntry?
|
||||
|
||||
/// Set when we use a temporary UUID because we loaded a legacy entry
|
||||
private var uuidUnknown: Bool = false
|
||||
|
||||
/// Display VM as "deleted" for UI elements
|
||||
///
|
||||
/// This is a workaround for SwiftUI bugs not hiding deleted elements.
|
||||
@Published var isDeleted: Bool = false
|
||||
|
||||
/// Allows changes in the config, registry, and VM to be reflected
|
||||
private var observers: [AnyCancellable] = []
|
||||
|
||||
/// No default init
|
||||
private init() {
|
||||
|
||||
}
|
||||
|
||||
/// Create a VM from an existing object
|
||||
/// - Parameter vm: VM to wrap
|
||||
convenience init(wrapping vm: UTMVirtualMachine) {
|
||||
self.init()
|
||||
self.wrapped = vm
|
||||
subscribeToChildren()
|
||||
}
|
||||
|
||||
/// Create a new wrapped UTM VM from a registry entry
|
||||
/// - Parameter registryEntry: Registry entry
|
||||
convenience init(from registryEntry: UTMRegistryEntry) {
|
||||
self.init()
|
||||
self.registryEntryWrapped = registryEntry
|
||||
subscribeToChildren()
|
||||
}
|
||||
|
||||
/// Create a new wrapped UTM VM from a dictionary (legacy support)
|
||||
/// - Parameter info: Dictionary info
|
||||
convenience init?(from info: [String: Any]) {
|
||||
guard let bookmark = info["Bookmark"] as? Data,
|
||||
let name = info["Name"] as? String,
|
||||
let pathString = info["Path"] as? String else {
|
||||
return nil
|
||||
}
|
||||
let legacyEntry = UTMRegistry.shared.entry(uuid: UUID(), name: name, path: pathString, bookmark: bookmark)
|
||||
self.init(from: legacyEntry)
|
||||
uuidUnknown = true
|
||||
}
|
||||
|
||||
/// Create a new wrapped UTM VM from only the bookmark data (legacy support)
|
||||
/// - Parameter bookmark: Bookmark data
|
||||
convenience init(bookmark: Data) {
|
||||
self.init()
|
||||
let uuid = UUID()
|
||||
let name = NSLocalizedString("(Unavailable)", comment: "VMData")
|
||||
let pathString = "/\(UUID().uuidString)"
|
||||
let legacyEntry = UTMRegistry.shared.entry(uuid: uuid, name: name, path: pathString, bookmark: bookmark)
|
||||
self.init(from: legacyEntry)
|
||||
uuidUnknown = true
|
||||
}
|
||||
|
||||
/// Create a new VM from a configuration
|
||||
/// - Parameter config: Configuration to create new VM
|
||||
convenience init<Config: UTMConfiguration>(creatingFromConfig config: Config, destinationUrl: URL) {
|
||||
self.init()
|
||||
if config is UTMQemuConfiguration {
|
||||
wrapped = UTMQemuVirtualMachine(newConfig: config, destinationURL: destinationUrl)
|
||||
}
|
||||
#if os(macOS)
|
||||
if config is UTMAppleConfiguration {
|
||||
wrapped = UTMAppleVirtualMachine(newConfig: config, destinationURL: destinationUrl)
|
||||
}
|
||||
#endif
|
||||
subscribeToChildren()
|
||||
}
|
||||
|
||||
/// Loads the VM from file
|
||||
///
|
||||
/// If the VM is already loaded, it will return true without doing anything.
|
||||
/// - Returns: If load was successful
|
||||
func load() throws {
|
||||
guard !isLoaded else {
|
||||
return
|
||||
}
|
||||
guard let vm = UTMVirtualMachine(url: pathUrl) else {
|
||||
throw VMDataError.virtualMachineNotLoaded
|
||||
}
|
||||
vm.isShortcut = isShortcut
|
||||
if let oldEntry = registryEntry, oldEntry.uuid != vm.registryEntry.uuid {
|
||||
if uuidUnknown {
|
||||
// legacy VMs don't have UUID stored so we made a fake UUID
|
||||
UTMRegistry.shared.remove(entry: oldEntry)
|
||||
} else {
|
||||
// persistent uuid does not match indicating a cloned or legacy VM with a duplicate UUID
|
||||
vm.changeUuid(to: oldEntry.uuid, copyFromExisting: oldEntry)
|
||||
}
|
||||
}
|
||||
wrapped = vm
|
||||
uuidUnknown = false
|
||||
}
|
||||
|
||||
/// Saves the VM to file
|
||||
func save() async throws {
|
||||
guard let wrapped = wrapped else {
|
||||
throw VMDataError.virtualMachineNotLoaded
|
||||
}
|
||||
try await wrapped.saveUTM()
|
||||
}
|
||||
|
||||
/// Listen to changes in the underlying object and propogate upwards
|
||||
private func subscribeToChildren() {
|
||||
var s: [AnyCancellable] = []
|
||||
if let config = config as? UTMQemuConfiguration {
|
||||
s.append(config.objectWillChange.sink { [weak self] in
|
||||
self?.objectWillChange.send()
|
||||
})
|
||||
}
|
||||
#if os(macOS)
|
||||
if let config = config as? UTMAppleConfiguration {
|
||||
s.append(config.objectWillChange.sink { [weak self] in
|
||||
self?.objectWillChange.send()
|
||||
})
|
||||
}
|
||||
#endif
|
||||
if let registryEntry = registryEntry {
|
||||
s.append(registryEntry.objectWillChange.sink { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.objectWillChange.send()
|
||||
self.wrapped?.updateConfigFromRegistry()
|
||||
})
|
||||
}
|
||||
// observe KVO publisher for state changes
|
||||
if let wrapped = wrapped {
|
||||
s.append(wrapped.publisher(for: \.state).sink { [weak self] _ in
|
||||
self?.objectWillChange.send()
|
||||
})
|
||||
s.append(wrapped.publisher(for: \.screenshot).sink { [weak self] _ in
|
||||
self?.objectWillChange.send()
|
||||
})
|
||||
}
|
||||
observers = s
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
enum VMDataError: Error {
|
||||
case virtualMachineNotLoaded
|
||||
}
|
||||
|
||||
extension VMDataError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .virtualMachineNotLoaded:
|
||||
return NSLocalizedString("Virtual machine not loaded.", comment: "VMData")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Identity
|
||||
extension VMData: Identifiable {
|
||||
public var id: UUID {
|
||||
registryEntry?.uuid ??
|
||||
config?.information.uuid ??
|
||||
UUID()
|
||||
}
|
||||
}
|
||||
|
||||
extension VMData: Equatable {
|
||||
static func == (lhs: VMData, rhs: VMData) -> Bool {
|
||||
if lhs.isLoaded && rhs.isLoaded {
|
||||
return lhs.wrapped == rhs.wrapped
|
||||
}
|
||||
if let lhsEntry = lhs.registryEntryWrapped, let rhsEntry = rhs.registryEntryWrapped {
|
||||
return lhsEntry == rhsEntry
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension VMData: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(wrapped)
|
||||
hasher.combine(registryEntryWrapped)
|
||||
hasher.combine(isDeleted)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VM State
|
||||
extension VMData {
|
||||
/// True if the .utm is loaded outside of the default storage
|
||||
var isShortcut: Bool {
|
||||
if let wrapped = wrapped {
|
||||
return wrapped.isShortcut
|
||||
} else {
|
||||
let defaultStorageUrl = UTMData.defaultStorageUrl.standardizedFileURL
|
||||
let parentUrl = pathUrl.deletingLastPathComponent().standardizedFileURL
|
||||
return parentUrl != defaultStorageUrl
|
||||
}
|
||||
}
|
||||
|
||||
/// VM is loaded
|
||||
var isLoaded: Bool {
|
||||
wrapped != nil
|
||||
}
|
||||
|
||||
/// VM is stopped
|
||||
var isStopped: Bool {
|
||||
if let state = wrapped?.state {
|
||||
return state == .vmStopped || state == .vmPaused
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// VM can be modified
|
||||
var isModifyAllowed: Bool {
|
||||
if let state = wrapped?.state {
|
||||
return state == .vmStopped
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Display VM as "busy" for UI elements
|
||||
var isBusy: Bool {
|
||||
wrapped?.state == .vmPausing ||
|
||||
wrapped?.state == .vmResuming ||
|
||||
wrapped?.state == .vmStarting ||
|
||||
wrapped?.state == .vmStopping
|
||||
}
|
||||
|
||||
/// VM has been suspended before
|
||||
var hasSuspendState: Bool {
|
||||
registryEntry?.isSuspended ?? false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Home UI elements
|
||||
extension VMData {
|
||||
#if os(macOS)
|
||||
typealias PlatformImage = NSImage
|
||||
#else
|
||||
typealias PlatformImage = UIImage
|
||||
#endif
|
||||
|
||||
/// Unavailable string
|
||||
private var unavailable: String {
|
||||
NSLocalizedString("Unavailable", comment: "VMData")
|
||||
}
|
||||
|
||||
/// Display title for UI elements
|
||||
var detailsTitleLabel: String {
|
||||
config?.information.name ??
|
||||
registryEntry?.name ??
|
||||
unavailable
|
||||
}
|
||||
|
||||
/// Display subtitle for UI elements
|
||||
var detailsSubtitleLabel: String {
|
||||
detailsSystemTargetLabel
|
||||
}
|
||||
|
||||
/// Display icon path for UI elements
|
||||
var detailsIconUrl: URL? {
|
||||
config?.information.iconURL ?? nil
|
||||
}
|
||||
|
||||
/// Display user-specified notes for UI elements
|
||||
var detailsNotes: String? {
|
||||
config?.information.notes ?? nil
|
||||
}
|
||||
|
||||
/// Display VM target system for UI elements
|
||||
var detailsSystemTargetLabel: String {
|
||||
if let qemuConfig = config as? UTMQemuConfiguration {
|
||||
return qemuConfig.system.target.prettyValue
|
||||
}
|
||||
#if os(macOS)
|
||||
if let appleConfig = config as? UTMAppleConfiguration {
|
||||
return appleConfig.system.boot.operatingSystem.rawValue
|
||||
}
|
||||
#endif
|
||||
return unavailable
|
||||
}
|
||||
|
||||
/// Display VM architecture for UI elements
|
||||
var detailsSystemArchitectureLabel: String {
|
||||
if let qemuConfig = config as? UTMQemuConfiguration {
|
||||
return qemuConfig.system.architecture.prettyValue
|
||||
}
|
||||
#if os(macOS)
|
||||
if let appleConfig = config as? UTMAppleConfiguration {
|
||||
return appleConfig.system.architecture
|
||||
}
|
||||
#endif
|
||||
return unavailable
|
||||
}
|
||||
|
||||
/// Display RAM (formatted) for UI elements
|
||||
var detailsSystemMemoryLabel: String {
|
||||
let bytesInMib = Int64(1048576)
|
||||
if let qemuConfig = config as? UTMQemuConfiguration {
|
||||
return ByteCountFormatter.string(fromByteCount: Int64(qemuConfig.system.memorySize) * bytesInMib, countStyle: .binary)
|
||||
}
|
||||
#if os(macOS)
|
||||
if let appleConfig = config as? UTMAppleConfiguration {
|
||||
return ByteCountFormatter.string(fromByteCount: Int64(appleConfig.system.memorySize) * bytesInMib, countStyle: .binary)
|
||||
}
|
||||
#endif
|
||||
return unavailable
|
||||
}
|
||||
|
||||
/// Display current VM state as a string for UI elements
|
||||
var stateLabel: String {
|
||||
guard let state = wrapped?.state else {
|
||||
return unavailable
|
||||
}
|
||||
switch state {
|
||||
case .vmStopped:
|
||||
if registryEntry?.hasSaveState == true {
|
||||
return NSLocalizedString("Suspended", comment: "VMData");
|
||||
} else {
|
||||
return NSLocalizedString("Stopped", comment: "VMData");
|
||||
}
|
||||
case .vmStarting:
|
||||
return NSLocalizedString("Starting", comment: "VMData")
|
||||
case .vmStarted:
|
||||
return NSLocalizedString("Started", comment: "VMData")
|
||||
case .vmPausing:
|
||||
return NSLocalizedString("Pausing", comment: "VMData")
|
||||
case .vmPaused:
|
||||
return NSLocalizedString("Paused", comment: "VMData")
|
||||
case .vmResuming:
|
||||
return NSLocalizedString("Resuming", comment: "VMData")
|
||||
case .vmStopping:
|
||||
return NSLocalizedString("Stopping", comment: "VMData")
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
/// If non-null, is the most recent screenshot image of the running VM
|
||||
var screenshotImage: PlatformImage? {
|
||||
wrapped?.screenshot?.image
|
||||
}
|
||||
}
|
|
@ -18,18 +18,24 @@ import Foundation
|
|||
import SwiftUI
|
||||
|
||||
extension UTMData {
|
||||
@MainActor func run(vm: UTMVirtualMachine) {
|
||||
let session = VMSessionState(for: vm as! UTMQemuVirtualMachine)
|
||||
func run(vm: VMData) {
|
||||
guard let wrapped = vm.wrapped else {
|
||||
return
|
||||
}
|
||||
let session = VMSessionState(for: wrapped as! UTMQemuVirtualMachine)
|
||||
session.start()
|
||||
}
|
||||
|
||||
func stop(vm: UTMVirtualMachine) {
|
||||
if vm.hasSaveState {
|
||||
vm.requestVmDeleteState()
|
||||
func stop(vm: VMData) {
|
||||
guard let wrapped = vm.wrapped else {
|
||||
return
|
||||
}
|
||||
if wrapped.hasSaveState {
|
||||
wrapped.requestVmDeleteState()
|
||||
}
|
||||
}
|
||||
|
||||
func close(vm: UTMVirtualMachine) {
|
||||
func close(vm: VMData) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct VMSettingsView: View {
|
||||
let vm: UTMVirtualMachine
|
||||
let vm: VMData
|
||||
@ObservedObject var config: UTMQemuConfiguration
|
||||
|
||||
@State private var isResetConfig: Bool = false
|
||||
|
@ -242,6 +242,6 @@ struct VMSettingsView_Previews: PreviewProvider {
|
|||
@State static private var config = UTMQemuConfiguration()
|
||||
|
||||
static var previews: some View {
|
||||
VMSettingsView(vm: UTMVirtualMachine(), config: config)
|
||||
VMSettingsView(vm: VMData(wrapping: UTMVirtualMachine()), config: config)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,11 +94,12 @@ fileprivate struct WizardWrapper: View {
|
|||
data.busyWorkAsync {
|
||||
let config = try await wizardState.generateConfig()
|
||||
if let qemuConfig = config.qemuConfig {
|
||||
let vm = try await data.create(config: qemuConfig) as! UTMQemuVirtualMachine
|
||||
let vm = try await data.create(config: qemuConfig)
|
||||
let wrapped = await vm.wrapped as! UTMQemuVirtualMachine
|
||||
if #available(iOS 15, *) {
|
||||
// This is broken on iOS 14
|
||||
await MainActor.run {
|
||||
vm.isGuestToolsInstallRequested = wizardState.isGuestToolsInstallRequested
|
||||
wrapped.isGuestToolsInstallRequested = wizardState.isGuestToolsInstallRequested
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
// limitations under the License.
|
||||
//
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@MainActor class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
var data: UTMData?
|
||||
|
||||
@Setting("KeepRunningAfterLastWindowClosed") private var isKeepRunningAfterLastWindowClosed: Bool = false
|
||||
|
|
|
@ -49,7 +49,7 @@ struct SavePanel: NSViewRepresentable {
|
|||
savePanel.allowedContentTypes = [.appleLog]
|
||||
case .utmCopy(let vm), .utmMove(let vm):
|
||||
savePanel.title = NSLocalizedString("Select where to save UTM Virtual Machine:", comment: "SavePanel")
|
||||
savePanel.nameFieldStringValue = vm.path.lastPathComponent
|
||||
savePanel.nameFieldStringValue = vm.pathUrl.lastPathComponent
|
||||
savePanel.allowedContentTypes = [.UTM]
|
||||
case .qemuCommand:
|
||||
savePanel.title = NSLocalizedString("Select where to export QEMU command:", comment: "SavePanel")
|
||||
|
|
|
@ -19,7 +19,10 @@ import Carbon.HIToolbox
|
|||
|
||||
@available(macOS 11, *)
|
||||
extension UTMData {
|
||||
@MainActor func run(vm: UTMVirtualMachine, startImmediately: Bool = true) {
|
||||
func run(vm: VMData, startImmediately: Bool = true) {
|
||||
guard let vm = vm.wrapped else {
|
||||
return
|
||||
}
|
||||
var window: Any? = vmWindows[vm]
|
||||
if window == nil {
|
||||
let close = { (notification: Notification) -> Void in
|
||||
|
@ -65,24 +68,34 @@ extension UTMData {
|
|||
} else if let unwrappedWindow = window as? VMHeadlessSessionState {
|
||||
vmWindows[vm] = unwrappedWindow
|
||||
if startImmediately {
|
||||
vm.requestVmStart()
|
||||
if vm.state == .vmPaused {
|
||||
vm.requestVmResume()
|
||||
} else {
|
||||
vm.requestVmStart()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.critical("Failed to create window controller.")
|
||||
}
|
||||
}
|
||||
|
||||
func stop(vm: UTMVirtualMachine) {
|
||||
if vm.hasSaveState {
|
||||
vm.requestVmDeleteState()
|
||||
func stop(vm: VMData) {
|
||||
guard let wrapped = vm.wrapped else {
|
||||
return
|
||||
}
|
||||
vm.vmStop(force: false, completion: { _ in
|
||||
if wrapped.hasSaveState {
|
||||
wrapped.requestVmDeleteState()
|
||||
}
|
||||
wrapped.vmStop(force: false, completion: { _ in
|
||||
self.close(vm: vm)
|
||||
})
|
||||
}
|
||||
|
||||
func close(vm: UTMVirtualMachine) {
|
||||
if let window = vmWindows.removeValue(forKey: vm) as? VMDisplayWindowController {
|
||||
func close(vm: VMData) {
|
||||
guard let wrapped = vm.wrapped else {
|
||||
return
|
||||
}
|
||||
if let window = vmWindows.removeValue(forKey: wrapped) as? VMDisplayWindowController {
|
||||
DispatchQueue.main.async {
|
||||
window.close()
|
||||
}
|
||||
|
|
|
@ -52,25 +52,25 @@ struct UTMMenuBarExtraScene: Scene {
|
|||
}
|
||||
|
||||
private struct VMMenuItem: View {
|
||||
@ObservedObject var vm: UTMVirtualMachine
|
||||
@ObservedObject var vm: VMData
|
||||
@EnvironmentObject private var data: UTMData
|
||||
|
||||
var body: some View {
|
||||
Menu(vm.detailsTitleLabel) {
|
||||
if vm.state == .vmStopped || vm.state == .vmPaused {
|
||||
if vm.isStopped {
|
||||
Button("Start") {
|
||||
data.run(vm: vm)
|
||||
}
|
||||
} else if vm.state == .vmStarted {
|
||||
} else if !vm.isBusy {
|
||||
Button("Stop") {
|
||||
data.stop(vm: vm)
|
||||
}
|
||||
Button("Suspend") {
|
||||
let isSnapshot = (vm as? UTMQemuVirtualMachine)?.isRunningAsSnapshot ?? false
|
||||
vm.requestVmPause(save: !isSnapshot)
|
||||
let isSnapshot = (vm.wrapped as? UTMQemuVirtualMachine)?.isRunningAsSnapshot ?? false
|
||||
vm.wrapped!.requestVmPause(save: !isSnapshot)
|
||||
}
|
||||
Button("Reset") {
|
||||
vm.requestVmReset()
|
||||
vm.wrapped!.requestVmReset()
|
||||
}
|
||||
} else {
|
||||
Text("Busy…")
|
||||
|
|
|
@ -18,7 +18,7 @@ import SwiftUI
|
|||
|
||||
@available(macOS 11, *)
|
||||
struct VMSettingsView<Config: UTMConfiguration>: View {
|
||||
let vm: UTMVirtualMachine?
|
||||
let vm: VMData?
|
||||
@ObservedObject var config: Config
|
||||
|
||||
@EnvironmentObject private var data: UTMData
|
||||
|
@ -51,7 +51,7 @@ struct VMSettingsView<Config: UTMConfiguration>: View {
|
|||
|
||||
func save() {
|
||||
data.busyWorkAsync {
|
||||
if let existing = await self.vm {
|
||||
if let existing = self.vm {
|
||||
try await data.save(vm: existing)
|
||||
} else {
|
||||
_ = try await data.create(config: self.config)
|
||||
|
@ -64,8 +64,10 @@ struct VMSettingsView<Config: UTMConfiguration>: View {
|
|||
|
||||
func cancel() {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
data.busyWorkAsync {
|
||||
try await data.discardChanges(for: self.vm)
|
||||
if let vm = vm {
|
||||
data.busyWorkAsync {
|
||||
try await data.discardChanges(for: vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,9 +97,10 @@ struct VMWizardView: View {
|
|||
}
|
||||
#endif
|
||||
if let qemuConfig = config.qemuConfig {
|
||||
let vm = try await data.create(config: qemuConfig) as! UTMQemuVirtualMachine
|
||||
let vm = try await data.create(config: qemuConfig)
|
||||
let wrapped = await vm.wrapped as! UTMQemuVirtualMachine
|
||||
await MainActor.run {
|
||||
vm.isGuestToolsInstallRequested = wizardState.isGuestToolsInstallRequested
|
||||
wrapped.isGuestToolsInstallRequested = wizardState.isGuestToolsInstallRequested
|
||||
}
|
||||
} else if let appleConfig = config.appleConfig {
|
||||
_ = try await data.create(config: appleConfig)
|
||||
|
|
|
@ -33,7 +33,7 @@ import Foundation
|
|||
}
|
||||
let wrapper = UTMScriptingConfigImpl(vm.config.wrappedValue as! any UTMConfiguration)
|
||||
try wrapper.updateConfiguration(from: newConfiguration)
|
||||
try await data.save(vm: vm)
|
||||
try await data.save(vm: box)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,11 @@ import QEMUKitInternal
|
|||
@MainActor
|
||||
@objc(UTMScriptingVirtualMachineImpl)
|
||||
class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable {
|
||||
@nonobjc var vm: UTMVirtualMachine
|
||||
@nonobjc var box: VMData
|
||||
@nonobjc var data: UTMData
|
||||
@nonobjc var vm: UTMVirtualMachine! {
|
||||
box.wrapped
|
||||
}
|
||||
|
||||
@objc var id: String {
|
||||
vm.id.uuidString
|
||||
|
@ -94,8 +97,8 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable {
|
|||
uniqueID: id)
|
||||
}
|
||||
|
||||
init(for vm: UTMVirtualMachine, data: UTMData) {
|
||||
self.vm = vm
|
||||
init(for vm: VMData, data: UTMData) {
|
||||
self.box = vm
|
||||
self.data = data
|
||||
}
|
||||
|
||||
|
@ -108,7 +111,7 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable {
|
|||
}
|
||||
vm.isRunningAsSnapshot = true
|
||||
}
|
||||
data.run(vm: vm, startImmediately: false)
|
||||
data.run(vm: box, startImmediately: false)
|
||||
if vm.state == .vmStopped {
|
||||
try await vm.vmStart()
|
||||
} else if vm.state == .vmPaused {
|
||||
|
@ -156,7 +159,7 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable {
|
|||
guard vm.state == .vmStopped else {
|
||||
throw ScriptingError.notStopped
|
||||
}
|
||||
try await data.delete(vm: vm, alsoRegistry: true)
|
||||
try await data.delete(vm: box, alsoRegistry: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,9 +169,9 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable {
|
|||
guard vm.state == .vmStopped else {
|
||||
throw ScriptingError.notStopped
|
||||
}
|
||||
let newVM = try await data.clone(vm: vm)
|
||||
let newVM = try await data.clone(vm: box)
|
||||
if let properties = properties, let newConfiguration = properties["configuration"] as? [AnyHashable : Any] {
|
||||
let wrapper = UTMScriptingConfigImpl(newVM.config.wrappedValue as! any UTMConfiguration)
|
||||
let wrapper = UTMScriptingConfigImpl(newVM.config!)
|
||||
try wrapper.updateConfiguration(from: newConfiguration)
|
||||
try await data.save(vm: newVM)
|
||||
}
|
||||
|
|
|
@ -148,6 +148,9 @@
|
|||
8471772827CD3CAB00D3A50B /* DetailedSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471772727CD3CAB00D3A50B /* DetailedSection.swift */; };
|
||||
8471772927CD3CAB00D3A50B /* DetailedSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471772727CD3CAB00D3A50B /* DetailedSection.swift */; };
|
||||
8471772A27CD3CAB00D3A50B /* DetailedSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471772727CD3CAB00D3A50B /* DetailedSection.swift */; };
|
||||
847BF9AA2A49C783000BD9AA /* VMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BF9A92A49C783000BD9AA /* VMData.swift */; };
|
||||
847BF9AB2A49C783000BD9AA /* VMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BF9A92A49C783000BD9AA /* VMData.swift */; };
|
||||
847BF9AC2A49C783000BD9AA /* VMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BF9A92A49C783000BD9AA /* VMData.swift */; };
|
||||
84818C0C2898A07A009EDB67 /* AVFAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84818C0B2898A07A009EDB67 /* AVFAudio.framework */; };
|
||||
84818C0D2898A07F009EDB67 /* AVFAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84818C0B2898A07A009EDB67 /* AVFAudio.framework */; };
|
||||
848308D5278A1F2200E3E474 /* Virtualization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848308D4278A1F2200E3E474 /* Virtualization.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
|
@ -1319,6 +1322,7 @@
|
|||
845F170C289CB3DE00944904 /* VMDisplayTerminal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayTerminal.swift; sourceTree = "<group>"; };
|
||||
8471770527CC974F00D3A50B /* DefaultTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTextField.swift; sourceTree = "<group>"; };
|
||||
8471772727CD3CAB00D3A50B /* DetailedSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailedSection.swift; sourceTree = "<group>"; };
|
||||
847BF9A92A49C783000BD9AA /* VMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMData.swift; sourceTree = "<group>"; };
|
||||
84818C0B2898A07A009EDB67 /* AVFAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFAudio.framework; path = System/Library/Frameworks/AVFAudio.framework; sourceTree = SDKROOT; };
|
||||
848308D4278A1F2200E3E474 /* Virtualization.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Virtualization.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk/System/Library/Frameworks/Virtualization.framework; sourceTree = DEVELOPER_DIR; };
|
||||
848A98AF286A0F74006F0550 /* UTMAppleConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMAppleConfiguration.swift; sourceTree = "<group>"; };
|
||||
|
@ -2120,6 +2124,7 @@
|
|||
CEB63A7924F469E300CAF323 /* UTMJailbreak.m */,
|
||||
CEB63A7824F468BA00CAF323 /* UTMJailbreak.h */,
|
||||
CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */,
|
||||
847BF9A92A49C783000BD9AA /* VMData.swift */,
|
||||
CE2D955624AD4F980059923A /* Swift-Bridging-Header.h */,
|
||||
CE550BD52259479D0063E575 /* Assets.xcassets */,
|
||||
521F3EFB2414F73800130500 /* Localizable.strings */,
|
||||
|
@ -2824,6 +2829,7 @@
|
|||
8443EFF22845641600B2E6E2 /* UTMQemuConfigurationDrive.swift in Sources */,
|
||||
8443EFFA28456F3B00B2E6E2 /* UTMQemuConfigurationSharing.swift in Sources */,
|
||||
CE772AAC25C8B0F600E4E379 /* ContentView.swift in Sources */,
|
||||
847BF9AA2A49C783000BD9AA /* VMData.swift in Sources */,
|
||||
CE2D927A24AD46670059923A /* UTMLegacyQemuConfiguration+System.m in Sources */,
|
||||
843BF8302844853E0029D60D /* UTMQemuConfigurationNetwork.swift in Sources */,
|
||||
CE2D927C24AD46670059923A /* UTMQemu.m in Sources */,
|
||||
|
@ -3141,6 +3147,7 @@
|
|||
CE020BA424AEDC7C00B44AB6 /* UTMData.swift in Sources */,
|
||||
CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */,
|
||||
848A98C8287206AE006F0550 /* VMConfigAppleVirtualizationView.swift in Sources */,
|
||||
847BF9AC2A49C783000BD9AA /* VMData.swift in Sources */,
|
||||
CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */,
|
||||
CE2D958824AD4F990059923A /* VMConfigPortForwardForm.swift in Sources */,
|
||||
845F170D289CB3DE00944904 /* VMDisplayTerminal.swift in Sources */,
|
||||
|
@ -3281,6 +3288,7 @@
|
|||
841E997628AA1191003C6CB6 /* UTMRegistry.swift in Sources */,
|
||||
CEF0304F26A2AFBF00667B63 /* BigButtonStyle.swift in Sources */,
|
||||
84018691288A73300050AC51 /* VMDisplayViewController.m in Sources */,
|
||||
847BF9AB2A49C783000BD9AA /* VMData.swift in Sources */,
|
||||
84909A8E27CACD5C005605F1 /* UTMPlaceholderVMView.swift in Sources */,
|
||||
CEA45EF4263519B5002FA97D /* VMConfigSoundView.swift in Sources */,
|
||||
8432329928C3017F00CFBC97 /* GlobalFileImporter.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue