Skip to content

Commit 4646b5b

Browse files
committed
fix(lume): handle guest-initiated VM shutdown via VZVirtualMachineDelegate
1 parent 8e6fa30 commit 4646b5b

File tree

4 files changed

+79
-6
lines changed

4 files changed

+79
-6
lines changed

libs/lume/src/LumeController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,8 @@ final class LumeController {
11221122
usbMassStoragePaths: usbMassStoragePaths,
11231123
networkMode: networkMode,
11241124
clipboard: clipboard)
1125-
Logger.info("VM started successfully", metadata: ["name": normalizedName])
1125+
SharedVM.shared.removeVM(name: normalizedName)
1126+
Logger.info("VM exited", metadata: ["name": normalizedName])
11261127
} catch {
11271128
SharedVM.shared.removeVM(name: normalizedName)
11281129
Logger.error("Failed to run VM", metadata: ["error": error.localizedDescription])

libs/lume/src/VM/VM.swift

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,26 @@ class VM {
325325
await clipboardWatcher?.start()
326326
}
327327

328-
while true {
329-
try await Task.sleep(nanoseconds: UInt64(1e9))
328+
// Block until the guest OS shuts down or crashes
329+
if let guestError = await service.waitForGuestStop() {
330+
Logger.error("VM stopped unexpectedly", metadata: [
331+
"name": vmDirContext.name,
332+
"error": guestError.localizedDescription,
333+
])
334+
} else {
335+
Logger.info("Guest initiated shutdown", metadata: ["name": vmDirContext.name])
330336
}
337+
338+
// Clean up after guest stop
339+
await clipboardWatcher?.stop()
340+
clipboardWatcher = nil
341+
virtualizationService = nil
342+
vncService.stop()
343+
344+
Logger.info("Releasing file lock after guest stop", metadata: ["name": vmDirContext.name])
345+
flock(fileHandle.fileDescriptor, LOCK_UN)
346+
try? fileHandle.close()
347+
unlockConfigFile()
331348
} catch {
332349
Logger.error(
333350
"Failed in VM.run",
@@ -1051,9 +1068,19 @@ class VM {
10511068
}
10521069
}
10531070

1054-
while true {
1055-
try await Task.sleep(nanoseconds: UInt64(1e9))
1071+
if let guestError = await service.waitForGuestStop() {
1072+
Logger.error("VM stopped unexpectedly", metadata: [
1073+
"name": vmDirContext.name,
1074+
"error": guestError.localizedDescription,
1075+
])
1076+
} else {
1077+
Logger.info("Guest initiated shutdown", metadata: ["name": vmDirContext.name])
10561078
}
1079+
1080+
virtualizationService = nil
1081+
vncService.stop()
1082+
flock(fileHandle.fileDescriptor, LOCK_UN)
1083+
try? fileHandle.close()
10571084
} catch {
10581085
Logger.error(
10591086
"Failed to create/start VM with USB storage",

libs/lume/src/Virtualization/VMVirtualizationService.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,62 @@ protocol VMVirtualizationService {
2828
func pause() async throws
2929
func resume() async throws
3030
func getVirtualMachine() -> Any
31+
func waitForGuestStop() async -> Error?
3132
}
3233

3334
/// Base implementation of VMVirtualizationService using VZVirtualMachine
3435
@MainActor
35-
class BaseVirtualizationService: VMVirtualizationService {
36+
class BaseVirtualizationService: NSObject, VMVirtualizationService, VZVirtualMachineDelegate {
3637
let virtualMachine: VZVirtualMachine
3738
let recoveryMode: Bool // Store whether we should start in recovery mode
3839

40+
private var guestStopContinuation: CheckedContinuation<Error?, Never>?
41+
private var pendingGuestStop: (fired: Bool, error: Error?) = (false, nil)
42+
3943
var state: VZVirtualMachine.State {
4044
virtualMachine.state
4145
}
4246

4347
init(virtualMachine: VZVirtualMachine, recoveryMode: Bool = false) {
4448
self.virtualMachine = virtualMachine
4549
self.recoveryMode = recoveryMode
50+
super.init()
51+
self.virtualMachine.delegate = self
52+
}
53+
54+
func waitForGuestStop() async -> Error? {
55+
if pendingGuestStop.fired {
56+
return pendingGuestStop.error
57+
}
58+
return await withCheckedContinuation { continuation in
59+
guestStopContinuation = continuation
60+
}
61+
}
62+
63+
// MARK: - VZVirtualMachineDelegate
64+
65+
nonisolated func guestDidStop(_ virtualMachine: VZVirtualMachine) {
66+
Task { @MainActor in
67+
Logger.info("Guest initiated shutdown")
68+
if let continuation = guestStopContinuation {
69+
guestStopContinuation = nil
70+
continuation.resume(returning: nil)
71+
} else {
72+
pendingGuestStop = (true, nil)
73+
}
74+
}
75+
}
76+
77+
nonisolated func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
78+
Task { @MainActor in
79+
Logger.error("Guest stopped with error", metadata: ["error": error.localizedDescription])
80+
if let continuation = guestStopContinuation {
81+
guestStopContinuation = nil
82+
continuation.resume(returning: error)
83+
} else {
84+
pendingGuestStop = (true, error)
85+
}
86+
}
4687
}
4788

4889
func start() async throws {

libs/lume/tests/Mocks/MockVMVirtualizationService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,8 @@ final class MockVMVirtualizationService: VMVirtualizationService {
6262
func getVirtualMachine() -> Any {
6363
return "mock_vm"
6464
}
65+
66+
func waitForGuestStop() async -> Error? {
67+
return nil
68+
}
6569
}

0 commit comments

Comments
 (0)