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 4a9cee442..348a6286d 100644 --- a/Managers/UTMQemuSystem.m +++ b/Managers/UTMQemuSystem.m @@ -735,6 +735,10 @@ - (void)argsFromConfiguration { [self pushArgv:@"-device"]; [self pushArgv:@"virtio-rng-pci"]; } + + if (self.runAsSnapshot) { + [self pushArgv:@"-snapshot"]; + } } - (void)argsFromUser { diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index 527e0c5ad..7b41d16f3 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -300,6 +300,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]; } @@ -309,9 +318,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; @@ -554,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); }]; }); } diff --git a/Managers/UTMVirtualMachine.h b/Managers/UTMVirtualMachine.h index 45a4a4402..b232b63cc 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 cf2b9ed24..1e276d0c4 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: @@ -274,6 +279,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/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/Shared/VMContextMenuModifier.swift b/Platform/Shared/VMContextMenuModifier.swift index 37481803f..19a051049 100644 --- a/Platform/Shared/VMContextMenuModifier.swift +++ b/Platform/Shared/VMContextMenuModifier.swift @@ -48,11 +48,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") + }.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 50307ffb6..f0c04eebf 100644 --- a/Platform/iOS/Display/VMToolbarActions.swift +++ b/Platform/iOS/Display/VMToolbarActions.swift @@ -213,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/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 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 092c01853..d889a7eab 100644 --- a/Platform/macOS/Display/VMDisplayQemuDisplayController.swift +++ b/Platform/macOS/Display/VMDisplayQemuDisplayController.swift @@ -23,6 +23,18 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { vm?.config as? UTMQemuConfiguration } + var defaultSubtitle: String { + if qemuVM.isRunningAsSnapshot { + return NSLocalizedString("Disposable Mode", comment: "VMDisplayQemuDisplayController") + } else { + return "" + } + } + + override var shouldSaveOnPause: Bool { + !qemuVM.isRunningAsSnapshot + } + override func enterLive() { qemuVM.ioDelegate = self startPauseToolbarItem.isEnabled = true @@ -36,6 +48,7 @@ class VMDisplayQemuWindowController: VMDisplayWindowController { sharedFolderToolbarItem.isEnabled = qemuVM.hasShareDirectoryEnabled usbToolbarItem.isEnabled = qemuVM.hasUsbRedirection window!.title = vmQemuConfig.name + window!.subtitle = defaultSubtitle super.enterLive() } } 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 {