Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion libs/lume/src/LumeController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
31 changes: 27 additions & 4 deletions libs/lume/src/VM/VM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
43 changes: 42 additions & 1 deletion libs/lume/src/Virtualization/VMVirtualizationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,62 @@ 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<Error?, Never>?
private var pendingGuestStop: (fired: Bool, error: Error?) = (false, nil)

var state: VZVirtualMachine.State {
virtualMachine.state
}

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 {
Expand Down
38 changes: 37 additions & 1 deletion libs/lume/tests/Mocks/MockVMVirtualizationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,40 @@ final class MockVMVirtualizationService: VMVirtualizationService {
func getVirtualMachine() -> Any {
return "mock_vm"
}
}

private var guestStopContinuation: CheckedContinuation<Error?, Never>?
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)
}
}
}