diff --git a/libs/lume/src/LumeController.swift b/libs/lume/src/LumeController.swift index 9940888d9..f27da5ea9 100644 --- a/libs/lume/src/LumeController.swift +++ b/libs/lume/src/LumeController.swift @@ -1122,7 +1122,8 @@ final class LumeController { usbMassStoragePaths: usbMassStoragePaths, networkMode: networkMode, clipboard: clipboard) - Logger.info("VM started successfully", metadata: ["name": normalizedName]) + SharedVM.shared.removeVM(name: normalizedName) + Logger.info("VM exited", metadata: ["name": normalizedName]) } catch { SharedVM.shared.removeVM(name: normalizedName) Logger.error("Failed to run VM", metadata: ["error": error.localizedDescription]) diff --git a/libs/lume/src/VM/VM.swift b/libs/lume/src/VM/VM.swift index 1a2c8ee18..a7a9fcc61 100644 --- a/libs/lume/src/VM/VM.swift +++ b/libs/lume/src/VM/VM.swift @@ -325,9 +325,24 @@ class VM { await clipboardWatcher?.start() } - while true { - try await Task.sleep(nanoseconds: UInt64(1e9)) + // Block until the guest OS shuts down or crashes + let guestError = await service.waitForGuestStop() + + // Clean up after guest stop + await clipboardWatcher?.stop() + clipboardWatcher = nil + virtualizationService = nil + vncService.stop() + + Logger.info("Releasing file lock after guest stop", metadata: ["name": vmDirContext.name]) + flock(fileHandle.fileDescriptor, LOCK_UN) + try? fileHandle.close() + unlockConfigFile() + + if let guestError { + throw guestError } + Logger.info("Guest initiated shutdown", metadata: ["name": vmDirContext.name]) } catch { Logger.error( "Failed in VM.run", @@ -1051,9 +1066,17 @@ class VM { } } - while true { - try await Task.sleep(nanoseconds: UInt64(1e9)) + let guestError = await service.waitForGuestStop() + + virtualizationService = nil + vncService.stop() + flock(fileHandle.fileDescriptor, LOCK_UN) + try? fileHandle.close() + + if let guestError { + throw guestError } + Logger.info("Guest initiated shutdown", metadata: ["name": vmDirContext.name]) } catch { Logger.error( "Failed to create/start VM with USB storage", diff --git a/libs/lume/src/Virtualization/VMVirtualizationService.swift b/libs/lume/src/Virtualization/VMVirtualizationService.swift index cf00bc4d7..c57f4f926 100644 --- a/libs/lume/src/Virtualization/VMVirtualizationService.swift +++ b/libs/lume/src/Virtualization/VMVirtualizationService.swift @@ -28,14 +28,18 @@ protocol VMVirtualizationService { func pause() async throws func resume() async throws func getVirtualMachine() -> Any + func waitForGuestStop() async -> Error? } /// Base implementation of VMVirtualizationService using VZVirtualMachine @MainActor -class BaseVirtualizationService: VMVirtualizationService { +class BaseVirtualizationService: NSObject, VMVirtualizationService, VZVirtualMachineDelegate { let virtualMachine: VZVirtualMachine let recoveryMode: Bool // Store whether we should start in recovery mode + private var guestStopContinuation: CheckedContinuation? + private var pendingGuestStop: (fired: Bool, error: Error?) = (false, nil) + var state: VZVirtualMachine.State { virtualMachine.state } @@ -43,6 +47,43 @@ class BaseVirtualizationService: VMVirtualizationService { init(virtualMachine: VZVirtualMachine, recoveryMode: Bool = false) { self.virtualMachine = virtualMachine self.recoveryMode = recoveryMode + super.init() + self.virtualMachine.delegate = self + } + + func waitForGuestStop() async -> Error? { + if pendingGuestStop.fired { + return pendingGuestStop.error + } + return await withCheckedContinuation { continuation in + guestStopContinuation = continuation + } + } + + // MARK: - VZVirtualMachineDelegate + + nonisolated func guestDidStop(_ virtualMachine: VZVirtualMachine) { + Task { @MainActor in + Logger.info("Guest initiated shutdown") + if let continuation = guestStopContinuation { + guestStopContinuation = nil + continuation.resume(returning: nil) + } else { + pendingGuestStop = (true, nil) + } + } + } + + nonisolated func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) { + Task { @MainActor in + Logger.error("Guest stopped with error", metadata: ["error": error.localizedDescription]) + if let continuation = guestStopContinuation { + guestStopContinuation = nil + continuation.resume(returning: error) + } else { + pendingGuestStop = (true, error) + } + } } func start() async throws { diff --git a/libs/lume/tests/Mocks/MockVMVirtualizationService.swift b/libs/lume/tests/Mocks/MockVMVirtualizationService.swift index 7e24a6e33..afec40969 100644 --- a/libs/lume/tests/Mocks/MockVMVirtualizationService.swift +++ b/libs/lume/tests/Mocks/MockVMVirtualizationService.swift @@ -62,4 +62,40 @@ final class MockVMVirtualizationService: VMVirtualizationService { func getVirtualMachine() -> Any { return "mock_vm" } -} \ No newline at end of file + + private var guestStopContinuation: CheckedContinuation? + private var pendingGuestStopResult: Error?? + + func waitForGuestStop() async -> Error? { + if let result = pendingGuestStopResult { + pendingGuestStopResult = nil + currentState = .stopped + return result + } + return await withCheckedContinuation { continuation in + guestStopContinuation = continuation + } + } + + /// Simulate a normal guest shutdown. + func simulateGuestStop() { + currentState = .stopped + if let continuation = guestStopContinuation { + guestStopContinuation = nil + continuation.resume(returning: nil) + } else { + pendingGuestStopResult = .some(nil) + } + } + + /// Simulate a guest crash with the given error. + func simulateGuestError(_ error: Error) { + currentState = .stopped + if let continuation = guestStopContinuation { + guestStopContinuation = nil + continuation.resume(returning: error) + } else { + pendingGuestStopResult = .some(error) + } + } +}