From 55d3cd95770ac18afac68fed296be5f4c2770d4c Mon Sep 17 00:00:00 2001 From: "Eugene Cheah (PicoCreator)" Date: Sat, 9 Apr 2022 03:01:48 +0800 Subject: [PATCH 1/8] Adding the isRunAsSnapshot config to DiskImage (without its logic) --- Configuration/UTMAppleConfiguration.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index f43cd724d..2463e7167 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -864,6 +864,7 @@ struct DiskImage: Codable, Hashable, Identifiable { var sizeMib: Int var isReadOnly: Bool var isExternal: Bool + var isRunAsSnapshot: Bool var imageURL: URL? private var uuid = UUID() // for identifiable @@ -871,6 +872,7 @@ struct DiskImage: Codable, Hashable, Identifiable { case sizeMib case isReadOnly case isExternal + case isRunAsSnapshot case imagePath case imageBookmark } @@ -891,12 +893,14 @@ struct DiskImage: Codable, Hashable, Identifiable { sizeMib = newSize isReadOnly = false isExternal = false + isRunAsSnapshot = false } init(importImage url: URL, isReadOnly: Bool = false, isExternal: Bool = false) { self.imageURL = url self.isReadOnly = isReadOnly self.isExternal = isExternal + self.isRunAsSnapshot = false if let attributes = try? url.resourceValues(forKeys: [.fileSizeKey]), let fileSize = attributes.fileSize { sizeMib = fileSize / bytesInMib } else { @@ -912,6 +916,10 @@ struct DiskImage: Codable, Hashable, Identifiable { sizeMib = try container.decode(Int.self, forKey: .sizeMib) isReadOnly = try container.decode(Bool.self, forKey: .isReadOnly) isExternal = try container.decode(Bool.self, forKey: .isExternal) + + // isRunAsSnapshot : is backwards compatible with older configs + isRunAsSnapshot = try container.decodeIfPresent(Bool.self, forKey: .isRunAsSnapshot) ?? false + if !isExternal, let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath) { imageURL = dataURL.appendingPathComponent(imagePath) } else if let bookmark = try container.decodeIfPresent(Data.self, forKey: .imageBookmark) { @@ -925,6 +933,7 @@ struct DiskImage: Codable, Hashable, Identifiable { try container.encode(sizeMib, forKey: .sizeMib) try container.encode(isReadOnly, forKey: .isReadOnly) try container.encode(isExternal, forKey: .isExternal) + try container.encode(isRunAsSnapshot, forKey: .isRunAsSnapshot) if !isExternal { try container.encodeIfPresent(imageURL?.lastPathComponent, forKey: .imagePath) } else { From 835c6b684b4c00cd70371d5433f9762e4de682f2 Mon Sep 17 00:00:00 2001 From: "Eugene Cheah (PicoCreator)" Date: Sat, 9 Apr 2022 10:12:24 +0800 Subject: [PATCH 2/8] Add toggle option on UI at a per drive level (without the active logic) --- Platform/macOS/VMConfigAppleDriveDetailsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Platform/macOS/VMConfigAppleDriveDetailsView.swift b/Platform/macOS/VMConfigAppleDriveDetailsView.swift index f4e1e2159..34a8d4dfd 100644 --- a/Platform/macOS/VMConfigAppleDriveDetailsView.swift +++ b/Platform/macOS/VMConfigAppleDriveDetailsView.swift @@ -25,6 +25,7 @@ struct VMConfigAppleDriveDetailsView: View { TextField("Name", text: .constant(diskImage.imageURL?.lastPathComponent ?? NSLocalizedString("(New Drive)", comment: "VMConfigAppleDriveDetailsView"))) .disabled(true) Toggle("Read Only?", isOn: $diskImage.isReadOnly) + Toggle("Run using a snapshot? (similar to qemu --snapshot)", isOn: $diskImage.isRunAsSnapshot) Button(action: onDelete) { Label("Delete Drive", systemImage: "externaldrive.badge.minus") .foregroundColor(.red) From 306e9ade68d19ea401cfbbf95ec30a0b2f2a0db8 Mon Sep 17 00:00:00 2001 From: "Eugene Cheah (PicoCreator)" Date: Sat, 9 Apr 2022 10:29:22 +0800 Subject: [PATCH 3/8] isRunAsSnapshot config at the UI system level (without the VM logic) --- Configuration/UTMAppleConfiguration.swift | 7 ++++++- Platform/macOS/VMConfigAppleSystemView.swift | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 2463e7167..3fc7fc0a7 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -275,7 +275,8 @@ final class UTMAppleConfiguration: UTMConfigurable, Codable, ObservableObject { @Published var isSerialEnabled: Bool @Published var isConsoleDisplay: Bool - + @Published var isRunAsSnapshot: Bool + @available(macOS 12, *) var isKeyboardEnabled: Bool { get { @@ -335,6 +336,7 @@ final class UTMAppleConfiguration: UTMConfigurable, Codable, ObservableObject { case isEntropyEnabled case isSerialEnabled case isConsoleDisplay + case isRunAsSnapshot case isKeyboardEnabled case isPointingEnabled } @@ -357,6 +359,7 @@ final class UTMAppleConfiguration: UTMConfigurable, Codable, ObservableObject { isAppleVirtualization = true isSerialEnabled = false isConsoleDisplay = false + isRunAsSnapshot = false memorySize = 4 * 1024 * 1024 * 1024 cpuCount = 4 } @@ -398,6 +401,7 @@ final class UTMAppleConfiguration: UTMConfigurable, Codable, ObservableObject { isEntropyEnabled = try values.decode(Bool.self, forKey: .isEntropyEnabled) isSerialEnabled = try values.decode(Bool.self, forKey: .isSerialEnabled) isConsoleDisplay = try values.decode(Bool.self, forKey: .isConsoleDisplay) + isRunAsSnapshot = try values.decodeIfPresent(Bool.self, forKey: .isRunAsSnapshot) ?? false name = try values.decode(String.self, forKey: .name) architecture = try values.decode(String.self, forKey: .architecture) icon = try values.decodeIfPresent(String.self, forKey: .icon) @@ -435,6 +439,7 @@ final class UTMAppleConfiguration: UTMConfigurable, Codable, ObservableObject { try container.encode(isEntropyEnabled, forKey: .isEntropyEnabled) try container.encode(isSerialEnabled, forKey: .isSerialEnabled) try container.encode(isConsoleDisplay, forKey: .isConsoleDisplay) + try container.encode(isRunAsSnapshot, forKey: .isRunAsSnapshot) try container.encode(name, forKey: .name) try container.encode(architecture, forKey: .architecture) try container.encodeIfPresent(icon, forKey: .icon) diff --git a/Platform/macOS/VMConfigAppleSystemView.swift b/Platform/macOS/VMConfigAppleSystemView.swift index 5cd3f6f0c..8be262eeb 100644 --- a/Platform/macOS/VMConfigAppleSystemView.swift +++ b/Platform/macOS/VMConfigAppleSystemView.swift @@ -75,6 +75,7 @@ struct VMConfigAppleSystemView: View { Toggle("Enable Keyboard", isOn: $config.isKeyboardEnabled) Toggle("Enable Pointer", isOn: $config.isPointingEnabled) } + Toggle("Enable 'Run using a snapshot' on all drives", isOn: $config.isRunAsSnapshot) } } } From 7664cd649e8a627de6c023050deb145df4e7b8cf Mon Sep 17 00:00:00 2001 From: "Eugene Cheah (PicoCreator)" Date: Sat, 9 Apr 2022 10:50:01 +0800 Subject: [PATCH 4/8] Added the VM cleanup logic for isRunAsSnapshot --- Configuration/UTMAppleConfiguration.swift | 22 ++++++++++++++++++++++ Managers/UTMAppleVirtualMachine.swift | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 3fc7fc0a7..b593a3ddb 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -621,6 +621,14 @@ final class UTMAppleConfiguration: UTMConfigurable, Codable, ObservableObject { } return urls } + + /// Remove the snapshot URL image's that were previously genereated, + /// this should be done as part of a VM cleanup. + func cleanupDriveSnapshot() throws { + for i in diskImages.indices { + try diskImages[i].cleanupDriveSnapshot() + } + } } struct Bootloader: Codable { @@ -955,6 +963,20 @@ struct DiskImage: Codable, Hashable, Identifiable { } } + /// Returns the snapshot equivalent URL for the current image + /// Does not actually prepare the snapshot (this is done via setupDriveSnapshot) + func snapshotURL() throws -> URL? { + return imageURL?.appendingPathComponent(".snapshot") + } + + /// Remove the snapshot URL image, this can be done as part of VM cleanup + func cleanupDriveSnapshot() throws { + if let snapshotURL = try snapshotURL() { + // The file may not exists, if so nothing should happens + try FileManager.default.removeItem(at: snapshotURL) + } + } + func vzDiskImage() throws -> VZDiskImageStorageDeviceAttachment? { if let imageURL = imageURL { return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) diff --git a/Managers/UTMAppleVirtualMachine.swift b/Managers/UTMAppleVirtualMachine.swift index e7ec08f32..bb090354d 100644 --- a/Managers/UTMAppleVirtualMachine.swift +++ b/Managers/UTMAppleVirtualMachine.swift @@ -190,6 +190,10 @@ import Virtualization } } } + + // This perform any cleanup for the "--snapshot" feature, + // if it was initialized previously + try appleConfig.cleanupDriveSnapshot() } override func vmStop(force: Bool) async throws { From c036086fd2e1d7354d90e2d51b007659fc2c2906 Mon Sep 17 00:00:00 2001 From: "Eugene Cheah (PicoCreator)" Date: Mon, 11 Apr 2022 08:53:34 -0700 Subject: [PATCH 5/8] WIP prototype, it copies the file, but does not boot somehow --- Configuration/UTMAppleConfiguration.swift | 57 +++++++++++++++++++++-- Managers/UTMAppleVirtualMachine.swift | 10 ++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index b593a3ddb..917f5734f 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -629,6 +629,26 @@ final class UTMAppleConfiguration: UTMConfigurable, Codable, ObservableObject { try diskImages[i].cleanupDriveSnapshot() } } + + /// Perform a snapshot clone of the current image URL to the snapshot URL + /// this is required for the snapshotURL image to "work" + /// + /// This is only done if the provided parm, or system config is true + /// + /// The "runAsSnapshot" is meant to be used with the + /// "runAsSnapshot" context menu feature, which would perform + /// the snapshot run "on demand" without config change + func setupDriveSnapshot(runAsSnapshot: Bool = false) throws { + var runAsSnapshot = runAsSnapshot + if isRunAsSnapshot { + runAsSnapshot = true + } + + for i in diskImages.indices { + // Setup the --snapshot on a per drive level + try diskImages[i].setupDriveSnapshot(runAsSnapshot: runAsSnapshot) + } + } } struct Bootloader: Codable { @@ -966,7 +986,7 @@ struct DiskImage: Codable, Hashable, Identifiable { /// Returns the snapshot equivalent URL for the current image /// Does not actually prepare the snapshot (this is done via setupDriveSnapshot) func snapshotURL() throws -> URL? { - return imageURL?.appendingPathComponent(".snapshot") + return imageURL?.appendingPathExtension("snapshot.img") } /// Remove the snapshot URL image, this can be done as part of VM cleanup @@ -977,12 +997,39 @@ struct DiskImage: Codable, Hashable, Identifiable { } } - func vzDiskImage() throws -> VZDiskImageStorageDeviceAttachment? { - if let imageURL = imageURL { - return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) + /// Perform a snapshot clone of the current image URL to the snapshot URL + /// this is required for the snapshotURL image to "work" + /// + /// This is only done if the provided parm, or drive config is true + func setupDriveSnapshot(runAsSnapshot: Bool = false) throws { + // does nothing if runAsSnapshot is false + if runAsSnapshot == false && isRunAsSnapshot == false { + return + } + + // Make a copy of the provided imageURL, as snapshot + if let snapshotURL = try snapshotURL(), let imageURL = imageURL { + // lets setup the snapshot file + // AFAICT this does a shallow copy on APFS drives + try FileManager.default.copyItem(at: imageURL, to: snapshotURL) + } + } + + /// Return the vzDiskImage, if either the runAsSnapshot or + /// the isRunAsSnapshot drive config is true, return using the snapshot URL instead + func vzDiskImage(runAsSnapshot: Bool = false) throws -> VZDiskImageStorageDeviceAttachment? { + if runAsSnapshot == true || isRunAsSnapshot == true { + // Assume the usage of snapshot URL + if let snapshotURL = try snapshotURL() { + return try VZDiskImageStorageDeviceAttachment(url: snapshotURL, readOnly: isReadOnly) + } } else { - return nil + // Assume standard (non-snapshot) behaviour + if let imageURL = imageURL { + return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) + } } + return nil } func hash(into hasher: inout Hasher) { diff --git a/Managers/UTMAppleVirtualMachine.swift b/Managers/UTMAppleVirtualMachine.swift index bb090354d..18ee5aaaa 100644 --- a/Managers/UTMAppleVirtualMachine.swift +++ b/Managers/UTMAppleVirtualMachine.swift @@ -328,6 +328,16 @@ import Virtualization fsConfig.share = self?.makeDirectoryShare(from: newShares) } } + + // This perform any reset's needed for the + // "--snapshot" feature (if its in use) + // + // The "runAsSnapshot" is meant to be used with the + // "runAsSnapshot" context menu feature, which would perform + // the snapshot run "on demand" without config change + // - when said context menu feature is implemented. + try appleConfig.setupDriveSnapshot(runAsSnapshot: false) + apple = VZVirtualMachine(configuration: appleConfig.apple, queue: vmQueue) apple.delegate = self } From b4a9498f67a4f2a68bd38759cf3d9f401fd43427 Mon Sep 17 00:00:00 2001 From: "Eugene Cheah (PicoCreator)" Date: Mon, 11 Apr 2022 09:25:07 -0700 Subject: [PATCH 6/8] Working prototype XD --- Configuration/UTMAppleConfiguration.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 917f5734f..0b04f14e0 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -639,15 +639,29 @@ final class UTMAppleConfiguration: UTMConfigurable, Codable, ObservableObject { /// "runAsSnapshot" context menu feature, which would perform /// the snapshot run "on demand" without config change func setupDriveSnapshot(runAsSnapshot: Bool = false) throws { + // Seems like there is edge cases where cleanup didn't occur (hard crash/force close) + // So we should always perform a full cleanup, before setup + try cleanupDriveSnapshot() + + // Logic to detrimine if runAsSnapshot should be assumed var runAsSnapshot = runAsSnapshot if isRunAsSnapshot { runAsSnapshot = true } + // Perform snapshots on a per drive level for i in diskImages.indices { // Setup the --snapshot on a per drive level try diskImages[i].setupDriveSnapshot(runAsSnapshot: runAsSnapshot) } + + // Seems like this needs to be reinit? So that config changes properlly applies + apple.storageDevices = diskImages.compactMap({ diskImage in + guard let attachment = try? diskImage.vzDiskImage(runAsSnapshot: runAsSnapshot) else { + return nil + } + return VZVirtioBlockDeviceConfiguration(attachment: attachment) + }) } } @@ -993,7 +1007,10 @@ struct DiskImage: Codable, Hashable, Identifiable { func cleanupDriveSnapshot() throws { if let snapshotURL = try snapshotURL() { // The file may not exists, if so nothing should happens - try FileManager.default.removeItem(at: snapshotURL) + // + // apperantly despite documentation saying it will return false. + // it will return with an error if removal fails (does not exist,etc) + try? FileManager.default.removeItem(at: snapshotURL) } } From 2b3e8e2b56f995298589d8b28ea46362362daac9 Mon Sep 17 00:00:00 2001 From: "Eugene Cheah (PicoCreator)" Date: Mon, 11 Apr 2022 20:17:53 -0700 Subject: [PATCH 7/8] clarifying remoteItem documentation --- Configuration/UTMAppleConfiguration.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 0b04f14e0..fb86b79d7 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -1010,6 +1010,8 @@ struct DiskImage: Codable, Hashable, Identifiable { // // apperantly despite documentation saying it will return false. // it will return with an error if removal fails (does not exist,etc) + // + // try? surpresses and ignores the error try? FileManager.default.removeItem(at: snapshotURL) } } From 368764afc895e8944e43347474ed40f5da45de05 Mon Sep 17 00:00:00 2001 From: "Eugene Cheah (PicoCreator)" Date: Mon, 11 Apr 2022 20:19:00 -0700 Subject: [PATCH 8/8] further clarification on "removeItem" --- Configuration/UTMAppleConfiguration.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index fb86b79d7..870ba06f9 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -1006,9 +1006,8 @@ struct DiskImage: Codable, Hashable, Identifiable { /// Remove the snapshot URL image, this can be done as part of VM cleanup func cleanupDriveSnapshot() throws { if let snapshotURL = try snapshotURL() { - // The file may not exists, if so nothing should happens - // - // apperantly despite documentation saying it will return false. + // The file may not exists, if so nothing should happens. Also, + // despite documentation saying "removeItem" will return false. // it will return with an error if removal fails (does not exist,etc) // // try? surpresses and ignores the error