From 42fb253994fa6dbeca08fd374e139f6514ac4a4e Mon Sep 17 00:00:00 2001 From: kt programs Date: Mon, 14 Mar 2022 10:45:12 +0800 Subject: [PATCH 1/5] Add support for running VM without saving changes (-snapshot) --- Managers/UTMQemuSystem.h | 1 + Managers/UTMQemuSystem.m | 4 ++++ Managers/UTMQemuVirtualMachine.m | 12 +++++++++--- Managers/UTMVirtualMachine.h | 8 ++++++++ Managers/UTMVirtualMachine.m | 8 ++++++++ Platform/Shared/VMContextMenuModifier.swift | 11 +++++++++++ Platform/iOS/Display/VMToolbarActions.swift | 7 +++++++ Platform/iOS/Display/VMToolbarView.swift | 2 ++ .../Display/VMDisplayQemuDisplayController.swift | 10 +++++++++- 9 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Managers/UTMQemuSystem.h b/Managers/UTMQemuSystem.h index 345e1561a..e0357ac02 100644 --- a/Managers/UTMQemuSystem.h +++ b/Managers/UTMQemuSystem.h @@ -24,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSURL *imgPath; @property (nonatomic, nullable) NSString *snapshot; @property (nonatomic) NSInteger qmpPort; +@property (nonatomic) BOOL runAsSnapshot; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConfiguration:(UTMQemuConfiguration *)configuration imgPath:(NSURL *)imgPath; diff --git a/Managers/UTMQemuSystem.m b/Managers/UTMQemuSystem.m index e963a00d9..18c0b9af9 100644 --- a/Managers/UTMQemuSystem.m +++ b/Managers/UTMQemuSystem.m @@ -728,6 +728,10 @@ - (void)argsFromConfiguration { [self pushArgv:@"-rtc"]; [self pushArgv:@"base=localtime"]; } + + if (self.runAsSnapshot) { + [self pushArgv:@"-snapshot"]; + } } - (void)argsFromUser { diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 825649caf..6c65aae46 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -299,6 +299,15 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { } } + if (self.isRunningAsSnapshot) { + self.system.runAsSnapshot = self.isRunningAsSnapshot; + } else { + // Loading save states isn't possible when -snapshot is used + if (self.viewState.hasSaveState) { + self.system.snapshot = kSuspendSnapshotName; + } + } + if (!_ioService) { _ioService = [self inputOutputService]; } @@ -308,9 +317,6 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { completion(spiceError); return; } - if (self.viewState.hasSaveState) { - self.system.snapshot = kSuspendSnapshotName; - } // start QEMU (this can be in parallel with QMP connect below) __weak typeof(self) weakSelf = self; __block NSError *qemuStartError = nil; diff --git a/Managers/UTMVirtualMachine.h b/Managers/UTMVirtualMachine.h index 00db17b6d..e3e1dc618 100644 --- a/Managers/UTMVirtualMachine.h +++ b/Managers/UTMVirtualMachine.h @@ -76,6 +76,14 @@ NS_ASSUME_NONNULL_BEGIN /// This property is observable and must only be accessed on the main thread. @property (nonatomic, readonly) BOOL isBusy; +/// Whether the next start of this VM should have the -snapshot flag set +/// +/// This will be passed to UTMQemuSystem, +/// and will be cleared when the VM stops or has an error. +/// +/// This property is observable and must only be accessed on the main thread. +@property (nonatomic) BOOL isRunningAsSnapshot; + /// Checks if a save state exists /// /// This property is observable and must only be accessed on the main thread. diff --git a/Managers/UTMVirtualMachine.m b/Managers/UTMVirtualMachine.m index 6df876490..80567fd97 100644 --- a/Managers/UTMVirtualMachine.m +++ b/Managers/UTMVirtualMachine.m @@ -101,6 +101,11 @@ - (BOOL)isBusy { return (_state == kVMPausing || _state == kVMResuming || _state == kVMStarting || _state == kVMStopping); } +- (void)setIsRunningAsSnapshot:(BOOL)isNextRunAsSnapshot { + [self propertyWillChange]; + _isRunningAsSnapshot = isNextRunAsSnapshot; +} + - (NSString *)stateLabel { switch (_state) { case kVMStopped: @@ -270,6 +275,9 @@ - (void)changeState:(UTMVMState)state { if (state == kVMStarted) { [self startScreenshotTimer]; } + if (state == kVMStopped) { + [self setIsRunningAsSnapshot:NO]; + } } - (NSURL *)packageURLForName:(NSString *)name { diff --git a/Platform/Shared/VMContextMenuModifier.swift b/Platform/Shared/VMContextMenuModifier.swift index 2414b2d64..da21b355c 100644 --- a/Platform/Shared/VMContextMenuModifier.swift +++ b/Platform/Shared/VMContextMenuModifier.swift @@ -47,11 +47,22 @@ struct VMContextMenuModifier: ViewModifier { Label("Stop", systemImage: "stop.fill") }.help("Stop the running VM.") } else { + Divider() + Button { data.run(vm: vm) } label: { Label("Run", systemImage: "play.fill") }.help("Run the VM in the foreground.") + + Button { + vm.isRunningAsSnapshot = true + data.run(vm: vm) + } label: { + Label("Run without saving changes", systemImage: "play.fill") + }.help("Run the VM in the foreground, without saving data changes to disk.") + + Divider() } Button { shareItem = .utmCopy(vm) diff --git a/Platform/iOS/Display/VMToolbarActions.swift b/Platform/iOS/Display/VMToolbarActions.swift index 6480cf16d..78b81c9de 100644 --- a/Platform/iOS/Display/VMToolbarActions.swift +++ b/Platform/iOS/Display/VMToolbarActions.swift @@ -89,6 +89,13 @@ import SwiftUI } } + var isRunningAsSnapshot: Bool { + guard let viewController = viewController else { + return false + } + return viewController.vm.isRunningAsSnapshot + } + private func optionalObjectWillChange() { if #available(iOS 14, *) { self.objectWillChange.send() diff --git a/Platform/iOS/Display/VMToolbarView.swift b/Platform/iOS/Display/VMToolbarView.swift index 06c22a3eb..e334be75e 100644 --- a/Platform/iOS/Display/VMToolbarView.swift +++ b/Platform/iOS/Display/VMToolbarView.swift @@ -90,6 +90,8 @@ struct VMToolbarView: View { } label: { Label(state.isRunning ? "Pause" : "Play", systemImage: state.isRunning ? "pause" : "play") }.offset(offset(for: 6)) + .disabled(state.isRunningAsSnapshot) + .opacity(state.isRunningAsSnapshot ? 0.5 : 1.0) Button { state.restartPressed() } label: { diff --git a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift index 092c01853..d1eb592e9 100644 --- a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift +++ b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift @@ -32,10 +32,18 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { startPauseToolbarItem.isEnabled = false } #endif + if qemuVM.isRunningAsSnapshot { + startPauseToolbarItem.isEnabled = false + startPauseToolbarItem.toolTip = "\(startPauseToolbarItem.toolTip ?? "")\n(Disabled due to running without saving changes)" + } drivesToolbarItem.isEnabled = vmQemuConfig.countDrives > 0 sharedFolderToolbarItem.isEnabled = qemuVM.hasShareDirectoryEnabled usbToolbarItem.isEnabled = qemuVM.hasUsbRedirection - window!.title = vmQemuConfig.name + if qemuVM.isRunningAsSnapshot { + window!.title = "\(vmQemuConfig.name) • (Running without saving changes)" + } else { + window!.title = vmQemuConfig.name + } super.enterLive() } } From 4f4d91cfc38c90b01720a4c8385ae1defedba286 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 7 May 2022 15:28:54 -0700 Subject: [PATCH 2/5] display(qemu): tweaked snapshot mode UI --- Platform/Shared/VMContextMenuModifier.swift | 2 +- .../VMDisplayMetalWindowController.swift | 2 +- .../VMDisplayQemuDisplayController.swift | 17 +++++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Platform/Shared/VMContextMenuModifier.swift b/Platform/Shared/VMContextMenuModifier.swift index ec4638dde..19a051049 100644 --- a/Platform/Shared/VMContextMenuModifier.swift +++ b/Platform/Shared/VMContextMenuModifier.swift @@ -60,7 +60,7 @@ struct VMContextMenuModifier: ViewModifier { vm.isRunningAsSnapshot = true data.run(vm: vm) } label: { - Label("Run without saving changes", systemImage: "play.fill") + Label("Run without saving changes", systemImage: "play") }.help("Run the VM in the foreground, without saving data changes to disk.") Divider() diff --git a/Platform/macOS/Display/VMDisplayMetalWindowController.swift b/Platform/macOS/Display/VMDisplayMetalWindowController.swift index 1d4e6854d..d6994da07 100644 --- a/Platform/macOS/Display/VMDisplayMetalWindowController.swift +++ b/Platform/macOS/Display/VMDisplayMetalWindowController.swift @@ -345,7 +345,7 @@ extension VMDisplayMetalWindowController: VMMetalViewInputDelegate { syncCapsLock() qemuVM.requestInputTablet(true) metalView?.releaseMouse() - self.window?.subtitle = "" + self.window?.subtitle = defaultSubtitle } func mouseMove(absolutePoint: CGPoint, button: CSInputButton) { diff --git a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift index d1eb592e9..2f74e6987 100644 --- a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift +++ b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift @@ -23,6 +23,14 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { vm?.config as? UTMQemuConfiguration } + var defaultSubtitle: String { + if qemuVM.isRunningAsSnapshot { + return NSLocalizedString("Disposable Mode", comment: "VMDisplayQemuDisplayController") + } else { + return "" + } + } + override func enterLive() { qemuVM.ioDelegate = self startPauseToolbarItem.isEnabled = true @@ -34,16 +42,13 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { #endif if qemuVM.isRunningAsSnapshot { startPauseToolbarItem.isEnabled = false - startPauseToolbarItem.toolTip = "\(startPauseToolbarItem.toolTip ?? "")\n(Disabled due to running without saving changes)" + startPauseToolbarItem.toolTip = NSLocalizedString("Disabled when running without saving changes.", comment: "VMDisplayQemuDisplayController") } drivesToolbarItem.isEnabled = vmQemuConfig.countDrives > 0 sharedFolderToolbarItem.isEnabled = qemuVM.hasShareDirectoryEnabled usbToolbarItem.isEnabled = qemuVM.hasUsbRedirection - if qemuVM.isRunningAsSnapshot { - window!.title = "\(vmQemuConfig.name) • (Running without saving changes)" - } else { - window!.title = vmQemuConfig.name - } + window!.title = vmQemuConfig.name + window!.subtitle = defaultSubtitle super.enterLive() } } From dd12ca7650badefe92e14aa1d164df4d5b70b14a Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 7 May 2022 16:01:11 -0700 Subject: [PATCH 3/5] vm(qemu): fix pause without save state --- Managers/UTMQemuVirtualMachine.m | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 277e88a5f..7b41d16f3 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -560,14 +560,19 @@ - (void)vmPauseSave:(BOOL)save completion:(void (^)(NSError * _Nullable))complet [self changeState:kVMPausing]; [self _vmPauseWithCompletion:^(NSError *err){ if (!err) { - [self _vmSaveStateWithCompletion:^(NSError *err) { + if (save) { + [self _vmSaveStateWithCompletion:^(NSError *err) { + [self changeState:kVMPaused]; + completion(err); + }]; + } else { [self changeState:kVMPaused]; completion(err); - }]; + } } else { [self changeState:kVMStopped]; + completion(err); } - completion(err); }]; }); } From a6f184a579e3f351e98a916edfa32fb72113f711 Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 7 May 2022 16:01:28 -0700 Subject: [PATCH 4/5] appdelegate: show prompt when paused without save state --- Platform/macOS/AppDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Platform/macOS/AppDelegate.swift b/Platform/macOS/AppDelegate.swift index 333fc3669..39bc0b87e 100644 --- a/Platform/macOS/AppDelegate.swift +++ b/Platform/macOS/AppDelegate.swift @@ -29,7 +29,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } let vmList = data.vmWindows.keys - if vmList.contains(where: { $0.state == .vmStarted }) { // There is at least 1 running VM + if vmList.contains(where: { $0.state == .vmStarted || ($0.state == .vmPaused && !$0.hasSaveState) }) { // There is at least 1 running VM DispatchQueue.main.async { let alert = NSAlert() alert.alertStyle = .informational @@ -53,7 +53,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } return .terminateLater - } else if vmList.allSatisfy({ $0.state == .vmStopped || ($0.state == .vmPaused && $0.hasSaveState) }) { // All VMs are stopped or suspended + } else if vmList.allSatisfy({ $0.state == .vmStopped || $0.state == .vmPaused }) { // All VMs are stopped or suspended return .terminateNow } else { // There could be some VMs in other states (starting, pausing, etc.) return .terminateCancel From 290320f5605dd82a35a25527d0cee90d3ca1422c Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 7 May 2022 16:02:02 -0700 Subject: [PATCH 5/5] display(macOS): allow pausing without saving for snapshot mode --- Platform/Shared/ContentView.swift | 8 +++++++- Platform/iOS/Display/VMToolbarActions.swift | 10 ++-------- Platform/iOS/Display/VMToolbarView.swift | 2 -- .../macOS/Display/VMDisplayQemuDisplayController.swift | 8 ++++---- Platform/macOS/Display/VMDisplayWindowController.swift | 3 ++- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Platform/Shared/ContentView.swift b/Platform/Shared/ContentView.swift index d4c1307d4..eca440f3b 100644 --- a/Platform/Shared/ContentView.swift +++ b/Platform/Shared/ContentView.swift @@ -205,7 +205,13 @@ struct ContentView: View { break case "pause": if let vm = await findVM(), vm.state == .vmStarted { - vm.requestVmPause(save: true) + let shouldSaveOnPause: Bool + if let vm = vm as? UTMQemuVirtualMachine { + shouldSaveOnPause = !vm.isRunningAsSnapshot + } else { + shouldSaveOnPause = true + } + vm.requestVmPause(save: shouldSaveOnPause) } case "resume": if let vm = await findVM(), vm.state == .vmPaused { diff --git a/Platform/iOS/Display/VMToolbarActions.swift b/Platform/iOS/Display/VMToolbarActions.swift index 6ea91f1f4..f0c04eebf 100644 --- a/Platform/iOS/Display/VMToolbarActions.swift +++ b/Platform/iOS/Display/VMToolbarActions.swift @@ -89,13 +89,6 @@ import SwiftUI } } - var isRunningAsSnapshot: Bool { - guard let viewController = viewController else { - return false - } - return viewController.vm.isRunningAsSnapshot - } - private func optionalObjectWillChange() { if #available(iOS 14, *) { self.objectWillChange.send() @@ -220,9 +213,10 @@ import SwiftUI guard let viewController = self.viewController else { return } + let shouldSaveState = !viewController.vm.isRunningAsSnapshot if viewController.vm.state == .vmStarted { viewController.enterSuspended(isBusy: true) // early indicator - viewController.vm.requestVmPause(save: true) + viewController.vm.requestVmPause(save: shouldSaveState) } else if viewController.vm.state == .vmPaused { viewController.enterSuspended(isBusy: true) // early indicator viewController.vm.requestVmResume() diff --git a/Platform/iOS/Display/VMToolbarView.swift b/Platform/iOS/Display/VMToolbarView.swift index e334be75e..06c22a3eb 100644 --- a/Platform/iOS/Display/VMToolbarView.swift +++ b/Platform/iOS/Display/VMToolbarView.swift @@ -90,8 +90,6 @@ struct VMToolbarView: View { } label: { Label(state.isRunning ? "Pause" : "Play", systemImage: state.isRunning ? "pause" : "play") }.offset(offset(for: 6)) - .disabled(state.isRunningAsSnapshot) - .opacity(state.isRunningAsSnapshot ? 0.5 : 1.0) Button { state.restartPressed() } label: { diff --git a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift index 2f74e6987..d889a7eab 100644 --- a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift +++ b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift @@ -31,6 +31,10 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { } } + override var shouldSaveOnPause: Bool { + !qemuVM.isRunningAsSnapshot + } + override func enterLive() { qemuVM.ioDelegate = self startPauseToolbarItem.isEnabled = true @@ -40,10 +44,6 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { startPauseToolbarItem.isEnabled = false } #endif - if qemuVM.isRunningAsSnapshot { - startPauseToolbarItem.isEnabled = false - startPauseToolbarItem.toolTip = NSLocalizedString("Disabled when running without saving changes.", comment: "VMDisplayQemuDisplayController") - } drivesToolbarItem.isEnabled = vmQemuConfig.countDrives > 0 sharedFolderToolbarItem.isEnabled = qemuVM.hasShareDirectoryEnabled usbToolbarItem.isEnabled = qemuVM.hasUsbRedirection diff --git a/Platform/macOS/Display/VMDisplayWindowController.swift b/Platform/macOS/Display/VMDisplayWindowController.swift index 2068a2cca..097f68a82 100644 --- a/Platform/macOS/Display/VMDisplayWindowController.swift +++ b/Platform/macOS/Display/VMDisplayWindowController.swift @@ -34,6 +34,7 @@ class VMDisplayWindowController: NSWindowController { var isPowerForce: Bool = false var shouldAutoStartVM: Bool = true + var shouldSaveOnPause: Bool { true } var vm: UTMVirtualMachine! var onClose: ((Notification) -> Void)? @@ -63,7 +64,7 @@ class VMDisplayWindowController: NSWindowController { @IBAction func startPauseButtonPressed(_ sender: Any) { enterSuspended(isBusy: true) // early indicator if vm.state == .vmStarted { - vm.requestVmPause(save: true) + vm.requestVmPause(save: shouldSaveOnPause) } else if vm.state == .vmPaused { vm.requestVmResume() } else if vm.state == .vmStopped {