Skip to content

Commit

Permalink
home: save wrapped VMs with name and path information
Browse files Browse the repository at this point in the history
This allows us to show unavailable VMs without automatically deleting
the shortcut.

Resolves #3686
  • Loading branch information
osy committed Feb 26, 2022
1 parent 50aa6f6 commit 1375f47
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 32 deletions.
17 changes: 12 additions & 5 deletions Managers/UTMVirtualMachine.m
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ @interface UTMVirtualMachine ()

@implementation UTMVirtualMachine

@synthesize bookmark = _bookmark;

// MARK: - Observable properties

- (void)setState:(UTMVMState)state {
Expand Down Expand Up @@ -122,11 +124,13 @@ - (BOOL)hasSaveState {
// MARK: - Other properties

- (NSData *)bookmark {
NSData *bookmark = [self.path bookmarkDataWithOptions:kUTMBookmarkCreationOptions
includingResourceValuesForKeys:nil
relativeToURL:nil
error:nil];
return bookmark;
if (!_bookmark) {
_bookmark = [self.path bookmarkDataWithOptions:kUTMBookmarkCreationOptions
includingResourceValuesForKeys:nil
relativeToURL:nil
error:nil];
}
return _bookmark;
}

- (void)setPath:(NSURL *)path {
Expand Down Expand Up @@ -173,6 +177,9 @@ + (UTMVirtualMachine *)virtualMachineWithBookmark:(NSData *)bookmark {
return nil;
}
UTMVirtualMachine *vm = [UTMVirtualMachine virtualMachineWithURL:url];
if (!stale) {
vm->_bookmark = bookmark;
}
return vm;
}

Expand Down
128 changes: 128 additions & 0 deletions Managers/UTMWrappedVirtualMachine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// Copyright © 2022 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

/// Represents a UTM Virtual Machine that is a placeholder and cannot be started
@objc class UTMWrappedVirtualMachine: UTMVirtualMachine {
override var detailsTitleLabel: String {
_name
}

override var detailsSubtitleLabel: String {
NSLocalizedString("Unavailable", comment: "UTMUnavailableVirtualMachine")
}

override var detailsNotes: String? {
""
}

override var detailsSystemTargetLabel: String {
""
}

override var detailsSystemArchitectureLabel: String {
""
}

override var detailsSystemMemoryLabel: String {
""
}

override var hasSaveState: Bool {
false
}

override var bookmark: Data? {
_bookmark
}

/// Represent a serialized dictionary for saving the VM to a list
public var serialized: [String: Any] {
return ["Name": _name,
"Path": _path.path,
"Bookmark": _bookmark];
}

private var _bookmark: Data

private var _name: String

private var _path: URL {
didSet {
self.path = _path
}
}

/// Create a new wrapped UTM VM
/// - Parameters:
/// - bookmark: Bookmark data for this VM
/// - name: Name of this VM
/// - path: Path where the VM is located
init(bookmark: Data, name: String, path: URL) {
_bookmark = bookmark
_name = name
_path = path
super.init()
self.path = path
}

/// Create a new wrapped UTM VM from an existing UTM VM
/// - Parameter vm: Existing VM
convenience init?(placeholderFor vm: UTMVirtualMachine) {
guard let bookmark = vm.bookmark else {
return nil
}
guard let path = vm.path else {
return nil
}
self.init(bookmark: bookmark, name: vm.detailsTitleLabel, path: path)
}

/// Create a new wrapped UTM VM from a dictionary
/// - 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
}
self.init(bookmark: bookmark, name: name, path: URL(fileURLWithPath: pathString))
}

/// Create a new wrapped UTM VM from only the bookmark data (legacy support)
/// - Parameter bookmark: Bookmark data
convenience init(bookmark: Data) {
self.init(bookmark: bookmark,
name: NSLocalizedString("(Unavailable)", comment: "UTMWrappedVirtualMachine"),
path: URL(fileURLWithPath: "/\(UUID().uuidString)"))
}

/// Unwrap to a fully formed UTM VM
/// - Returns: New UTM VM if it is valid and can be accessed
@available(iOS 14, macOS 11, *)
public func unwrap() -> UTMVirtualMachine? {
guard let vm = UTMVirtualMachine(bookmark: _bookmark) else {
return nil
}
let defaultStorageUrl = UTMData.defaultStorageUrl.standardizedFileURL
let parentUrl = vm.path!.deletingLastPathComponent().standardizedFileURL
if parentUrl != defaultStorageUrl {
vm.isShortcut = true
}
return vm
}
}
16 changes: 10 additions & 6 deletions Platform/Shared/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,16 @@ struct ContentView: View {
NavigationView {
List {
ForEach(data.virtualMachines) { vm in
NavigationLink(
destination: VMDetailsView(vm: vm),
tag: vm,
selection: $data.selectedVM,
label: { VMCardView(vm: vm) })
.modifier(VMContextMenuModifier(vm: vm))
if let wrappedVM = vm as? UTMWrappedVirtualMachine {
UTMPlaceholderVMView(wrappedVM: wrappedVM)
} else {
NavigationLink(
destination: VMDetailsView(vm: vm),
tag: vm,
selection: $data.selectedVM,
label: { VMCardView(vm: vm) })
.modifier(VMContextMenuModifier(vm: vm))
}
}.onMove(perform: move)
.onDelete(perform: delete)

Expand Down
34 changes: 34 additions & 0 deletions Platform/Shared/UTMPlaceholderVMView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Copyright © 2022 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftUI

@available(iOS 14, macOS 11, *)
struct UTMPlaceholderVMView: View {
@ObservedObject var wrappedVM: UTMWrappedVirtualMachine
@EnvironmentObject private var data: UTMData

var body: some View {
Text("Unavailable VM") // FIXME: remove placeholder
}
}

@available(iOS 14, macOS 11, *)
struct UTMPlaceholderVMView_Previews: PreviewProvider {
static var previews: some View {
UTMPlaceholderVMView(wrappedVM: UTMWrappedVirtualMachine(bookmark: Data(), name: "Wrapped", path: URL(fileURLWithPath: "/path")))
}
}
52 changes: 31 additions & 21 deletions Platform/UTMData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,22 @@ class UTMData: ObservableObject {
///
/// This removes stale entries (deleted/not accessible) and duplicate entries
func listRefresh() async {
// remove stale vm
var list = await virtualMachines.filter { (vm: UTMVirtualMachine) in vm.path != nil && fileManager.fileExists(atPath: vm.path!.path) }
// wrap stale VMs
var list = await virtualMachines
for i in list.indices.reversed() {
let vm = list[i]
if !fileManager.fileExists(atPath: vm.path!.path) {
if let wrappedVM = UTMWrappedVirtualMachine(placeholderFor: vm) {
list[i] = wrappedVM
} else {
// we cannot even make a placeholder, then remove the element
list.remove(at: i)
}
}
}
// now look for and add new VMs in default storage
do {
let files = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles)
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
Expand All @@ -138,6 +150,7 @@ class UTMData: ObservableObject {
} catch {
logger.error("\(error.localizedDescription)")
}
// replace the VM list with our new one
if await virtualMachines != list {
await listReplace(with: list)
}
Expand All @@ -154,33 +167,30 @@ class UTMData: ObservableObject {
})
}
// bookmark list
if let bookmarks = defaults.array(forKey: "VMList") as? [Data] {
let documentsURL = self.documentsURL.standardizedFileURL
virtualMachines = bookmarks.compactMap { bookmark in
let vm = UTMVirtualMachine(bookmark: bookmark)
let parentUrl = vm?.path!.deletingLastPathComponent().standardizedFileURL
if parentUrl != documentsURL {
vm?.isShortcut = true
if let list = defaults.array(forKey: "VMList") {
virtualMachines = list.compactMap { item in
var wrappedVM: UTMWrappedVirtualMachine?
if let bookmark = item as? Data {
wrappedVM = UTMWrappedVirtualMachine(bookmark: bookmark)
} else if let dict = item as? [String: Any] {
wrappedVM = UTMWrappedVirtualMachine(from: dict)
}
if let vm = wrappedVM?.unwrap() {
return vm
} else {
return wrappedVM
}
return vm
}
}
}

/// Save VM list (and order) to persistent storage
@MainActor private func listSaveToDefaults() {
let defaults = UserDefaults.standard
let bookmarks = virtualMachines.compactMap { vm -> Data? in
#if os(macOS)
if let appleVM = vm as? UTMAppleVirtualMachine {
if appleVM.isShortcut {
return nil // FIXME: Apple VMs do not support shortcuts
}
}
#endif
return vm.bookmark
let wrappedVMs = virtualMachines.compactMap { vm -> [String: Any]? in
UTMWrappedVirtualMachine(placeholderFor: vm)?.serialized
}
defaults.set(bookmarks, forKey: "VMList")
defaults.set(wrappedVMs, forKey: "VMList")
}

@MainActor private func listReplace(with vms: [UTMVirtualMachine]) {
Expand Down
16 changes: 16 additions & 0 deletions UTM.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@
848F71EC277A2F47006A0240 /* UTMSerialPortDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F71EB277A2F47006A0240 /* UTMSerialPortDelegate.swift */; };
848F71ED277A2F47006A0240 /* UTMSerialPortDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F71EB277A2F47006A0240 /* UTMSerialPortDelegate.swift */; };
848F71EE277A2F47006A0240 /* UTMSerialPortDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F71EB277A2F47006A0240 /* UTMSerialPortDelegate.swift */; };
84909A8927CABA54005605F1 /* UTMWrappedVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A8827CABA54005605F1 /* UTMWrappedVirtualMachine.swift */; };
84909A8A27CABA54005605F1 /* UTMWrappedVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A8827CABA54005605F1 /* UTMWrappedVirtualMachine.swift */; };
84909A8B27CABA54005605F1 /* UTMWrappedVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A8827CABA54005605F1 /* UTMWrappedVirtualMachine.swift */; };
84909A8D27CACD5C005605F1 /* UTMPlaceholderVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */; };
84909A8E27CACD5C005605F1 /* UTMPlaceholderVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */; };
84909A8F27CACD5C005605F1 /* UTMPlaceholderVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */; };
84A381AA268CB30C0048EE4D /* VMConfigDrivesButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A381A9268CB30C0048EE4D /* VMConfigDrivesButtons.swift */; };
84B36D1E27B3264600C22685 /* CocoaSpice in Frameworks */ = {isa = PBXBuildFile; productRef = 84B36D1D27B3264600C22685 /* CocoaSpice */; };
84B36D2027B3264E00C22685 /* CocoaSpiceNoUsb in Frameworks */ = {isa = PBXBuildFile; productRef = 84B36D1F27B3264E00C22685 /* CocoaSpiceNoUsb */; };
Expand Down Expand Up @@ -1560,6 +1566,8 @@
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; };
848F71E7277A2A4E006A0240 /* UTMSerialPort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSerialPort.swift; sourceTree = "<group>"; };
848F71EB277A2F47006A0240 /* UTMSerialPortDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSerialPortDelegate.swift; sourceTree = "<group>"; };
84909A8827CABA54005605F1 /* UTMWrappedVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMWrappedVirtualMachine.swift; sourceTree = "<group>"; };
84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMPlaceholderVMView.swift; sourceTree = "<group>"; };
84A381A9268CB30C0048EE4D /* VMConfigDrivesButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigDrivesButtons.swift; sourceTree = "<group>"; };
84B36D2427B704C200C22685 /* UTMDownloadVMTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMDownloadVMTask.swift; sourceTree = "<group>"; };
84B36D2827B790BE00C22685 /* DestructiveButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveButton.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3029,6 +3037,7 @@
CE059DC7243E9E3400338317 /* UTMLocationManager.m */,
CE020BB524B14F8400B44AB6 /* UTMVirtualMachineExtension.swift */,
835AA7B026AB7C85007A0411 /* UTMPendingVirtualMachine.swift */,
84909A8827CABA54005605F1 /* UTMWrappedVirtualMachine.swift */,
);
path = Managers;
sourceTree = "<group>";
Expand Down Expand Up @@ -3155,6 +3164,7 @@
844EC0FA2773EE49003C104A /* UTMDownloadIPSWTask.swift */,
83034C0626AB630F006B4BAF /* UTMPendingVMView.swift */,
83F02320278A4EDE00B157AE /* UTMPendingVMDetailsView.swift */,
84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */,
);
path = Shared;
sourceTree = "<group>";
Expand Down Expand Up @@ -3642,6 +3652,7 @@
CE2D928324AD46670059923A /* qapi-events-audio.c in Sources */,
CE2D928524AD46670059923A /* qapi-events-ui.c in Sources */,
CE2D928624AD46670059923A /* qapi-visit-misc.c in Sources */,
84909A8D27CACD5C005605F1 /* UTMPlaceholderVMView.swift in Sources */,
CEB63A8424F46E6E00CAF323 /* VMConfigDisplayViewController.m in Sources */,
CE2D928824AD46670059923A /* qapi-commands-block.c in Sources */,
CE7D972C24B2B17D0080CB69 /* BusyOverlay.swift in Sources */,
Expand Down Expand Up @@ -3719,6 +3730,7 @@
CE2D92B724AD46670059923A /* qapi-visit-common.c in Sources */,
CEF83EBE24F9C3BF00557D15 /* UTMQemuVirtualMachine+Drives.m in Sources */,
CE2D92B824AD46670059923A /* qapi-events-trace.c in Sources */,
84909A8927CABA54005605F1 /* UTMWrappedVirtualMachine.swift in Sources */,
2C6D9E142571AFE5003298E6 /* UTMQcow2.c in Sources */,
2CE8EAFE2572E14D000E2EBB /* qapi-types-block-export.c in Sources */,
4B224B9D279D4D8100B63CFF /* InListButtonStyle.swift in Sources */,
Expand Down Expand Up @@ -4005,6 +4017,7 @@
CE0B6CFC24AD568400FE012D /* UTMQemuConfigurationPortForward.m in Sources */,
8469CAD1277D345700BA5601 /* qapi-visit-compat.c in Sources */,
CEF0306C26A2AFDF00667B63 /* VMWizardStartView.swift in Sources */,
84909A8B27CABA54005605F1 /* UTMWrappedVirtualMachine.swift in Sources */,
84FCABBC268CE05E0036196C /* UTMQemuVirtualMachine.m in Sources */,
CE0B6D7924AD584D00FE012D /* qapi-visit-rdma.c in Sources */,
CE8813D624CD265700532628 /* VMShareFileModifier.swift in Sources */,
Expand Down Expand Up @@ -4123,6 +4136,7 @@
CE0B6D2924AD57FC00FE012D /* qapi-commands-machine-target.c in Sources */,
CEF0306F26A2AFDF00667B63 /* VMWizardOSView.swift in Sources */,
83034C0926AB630F006B4BAF /* UTMPendingVMView.swift in Sources */,
84909A8F27CACD5C005605F1 /* UTMPlaceholderVMView.swift in Sources */,
CE0B6D7324AD584D00FE012D /* qapi-events-migration.c in Sources */,
CE020BA424AEDC7C00B44AB6 /* UTMData.swift in Sources */,
CE0B6D1A24AD57FC00FE012D /* qapi-commands-qdev.c in Sources */,
Expand Down Expand Up @@ -4339,6 +4353,7 @@
CEA45ED4263519B5002FA97D /* qapi-types-block-core.c in Sources */,
CEA45ED5263519B5002FA97D /* qapi-events-transaction.c in Sources */,
CEA45ED6263519B5002FA97D /* VMConfigStringPicker.swift in Sources */,
84909A8A27CABA54005605F1 /* UTMWrappedVirtualMachine.swift in Sources */,
CEA45ED7263519B5002FA97D /* qapi-commands-dump.c in Sources */,
CEF0307226A2B04400667B63 /* VMWizardView.swift in Sources */,
83034C0826AB630F006B4BAF /* UTMPendingVMView.swift in Sources */,
Expand Down Expand Up @@ -4370,6 +4385,7 @@
CEA45EF1263519B5002FA97D /* VMListViewCell.m in Sources */,
CEF0304F26A2AFBF00667B63 /* BigButtonStyle.swift in Sources */,
CEA45EF2263519B5002FA97D /* qapi-events-common.c in Sources */,
84909A8E27CACD5C005605F1 /* UTMPlaceholderVMView.swift in Sources */,
CEA45EF3263519B5002FA97D /* UTMTerminalIO.m in Sources */,
CEA45EF4263519B5002FA97D /* VMConfigSoundView.swift in Sources */,
CEA45EF5263519B5002FA97D /* qapi-events-crypto.c in Sources */,
Expand Down

0 comments on commit 1375f47

Please sign in to comment.