Skip to content

Commit 6d4ea1f

Browse files
committed
vm(apple): implement snapshot save/restore for macOS 14
Resolves #5376
1 parent 5967988 commit 6d4ea1f

File tree

3 files changed

+121
-4
lines changed

3 files changed

+121
-4
lines changed

Configuration/QEMUConstant.swift

+1
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ enum QEMUPackageFileName: String {
390390
case debugLog = "debug.log"
391391
case efiVariables = "efi_vars.fd"
392392
case tpmData = "tpmdata"
393+
case vmState = "vmstate"
393394
}
394395

395396
// MARK: Supported features

Configuration/UTMAppleConfigurationBoot.swift

+5
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct UTMAppleConfigurationBoot: Codable {
3939
var linuxCommandLine: String?
4040
var linuxInitialRamdiskURL: URL?
4141
var efiVariableStorageURL: URL?
42+
var vmSavedStateURL: URL?
4243
var hasUefiBoot: Bool = false
4344

4445
/// IPSW for installing macOS. Not saved.
@@ -78,6 +79,7 @@ struct UTMAppleConfigurationBoot: Codable {
7879
if let efiVariableStoragePath = try container.decodeIfPresent(String.self, forKey: .efiVariableStoragePath) {
7980
efiVariableStorageURL = dataURL.appendingPathComponent(efiVariableStoragePath)
8081
}
82+
vmSavedStateURL = dataURL.appendingPathComponent(QEMUPackageFileName.vmState.rawValue)
8183
}
8284

8385
init(for operatingSystem: OperatingSystem, linuxKernelURL: URL? = nil) throws {
@@ -189,6 +191,9 @@ extension UTMAppleConfigurationBoot {
189191
self.efiVariableStorageURL = efiVariableStorageURL
190192
urls.append(efiVariableStorageURL)
191193
}
194+
let vmSavedStateURL = dataURL.appendingPathComponent(QEMUPackageFileName.vmState.rawValue)
195+
self.vmSavedStateURL = vmSavedStateURL
196+
urls.append(vmSavedStateURL)
192197
return urls
193198
}
194199
}

Services/UTMAppleVirtualMachine.swift

+115-4
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
100100
/// This variable MUST be synchronized by `vmQueue`
101101
private(set) var apple: VZVirtualMachine?
102102

103+
private var saveSnapshotError: Error?
104+
103105
private var installProgress: Progress?
104106

105107
private var progressObserver: NSKeyValueObservation?
@@ -173,8 +175,13 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
173175
}
174176
state = .starting
175177
do {
178+
let isSuspended = await registryEntry.isSuspended
176179
try await beginAccessingResources()
177-
try await _start(options: options)
180+
if isSuspended && !options.contains(.bootRecovery) {
181+
try await restoreSnapshot()
182+
} else {
183+
try await _start(options: options)
184+
}
178185
if #available(macOS 12, *) {
179186
Task { @MainActor in
180187
sharedDirectoriesChanged = config.sharedDirectoriesPublisher.sink { [weak self] newShares in
@@ -328,16 +335,108 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
328335
}
329336
}
330337

338+
#if arch(arm64)
339+
@available(macOS 14, *)
340+
private func _saveSnapshot(url: URL) async throws {
341+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
342+
vmQueue.async {
343+
guard let apple = self.apple else {
344+
continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
345+
return
346+
}
347+
apple.saveMachineStateTo(url: url) { error in
348+
if let error = error {
349+
continuation.resume(throwing: error)
350+
} else {
351+
continuation.resume()
352+
}
353+
}
354+
}
355+
}
356+
}
357+
#endif
358+
331359
func saveSnapshot(name: String? = nil) async throws {
332-
// FIXME: implement this
360+
guard #available(macOS 14, *) else {
361+
return
362+
}
363+
#if arch(arm64)
364+
guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
365+
return
366+
}
367+
if let saveSnapshotError = saveSnapshotError {
368+
throw saveSnapshotError
369+
}
370+
if state == .started {
371+
try await pause()
372+
}
373+
guard state == .paused else {
374+
return
375+
}
376+
state = .saving
377+
defer {
378+
state = .paused
379+
}
380+
try await _saveSnapshot(url: vmSavedStateURL)
381+
await registryEntry.setIsSuspended(true)
382+
#endif
333383
}
334384

335385
func deleteSnapshot(name: String? = nil) async throws {
336-
// FIXME: implement this
386+
guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
387+
return
388+
}
389+
try FileManager.default.removeItem(at: vmSavedStateURL)
390+
await registryEntry.setIsSuspended(false)
337391
}
338392

393+
#if arch(arm64)
394+
@available(macOS 14, *)
395+
private func _restoreSnapshot(url: URL) async throws {
396+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
397+
vmQueue.async {
398+
guard let apple = self.apple else {
399+
continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
400+
return
401+
}
402+
apple.restoreMachineStateFrom(url: url) { error in
403+
if let error = error {
404+
continuation.resume(throwing: error)
405+
} else {
406+
continuation.resume()
407+
}
408+
}
409+
}
410+
}
411+
}
412+
#endif
413+
339414
func restoreSnapshot(name: String? = nil) async throws {
340-
// FIXME: implement this
415+
guard #available(macOS 14, *) else {
416+
throw UTMAppleVirtualMachineError.operationNotAvailable
417+
}
418+
#if arch(arm64)
419+
guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
420+
throw UTMAppleVirtualMachineError.operationNotAvailable
421+
}
422+
if state == .started {
423+
try await stop(usingMethod: .force)
424+
}
425+
guard state == .stopped || state == .starting else {
426+
throw UTMAppleVirtualMachineError.operationNotAvailable
427+
}
428+
state = .restoring
429+
do {
430+
try await _restoreSnapshot(url: vmSavedStateURL)
431+
} catch {
432+
state = .stopped
433+
throw error
434+
}
435+
state = .started
436+
try await deleteSnapshot(name: name)
437+
#else
438+
throw UTMAppleVirtualMachineError.operationNotAvailable
439+
#endif
341440
}
342441

343442
private func _resume() async throws {
@@ -388,6 +487,17 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
388487
vmQueue.async { [self] in
389488
apple = VZVirtualMachine(configuration: vzConfig, queue: vmQueue)
390489
apple!.delegate = self
490+
saveSnapshotError = nil
491+
#if arch(arm64)
492+
if #available(macOS 14, *) {
493+
do {
494+
try vzConfig.validateSaveRestoreSupport()
495+
} catch {
496+
// save this for later when we want to use snapshots
497+
saveSnapshotError = error
498+
}
499+
}
500+
#endif
391501
}
392502
}
393503

@@ -521,6 +631,7 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
521631
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
522632
vmQueue.async { [self] in
523633
apple = nil
634+
saveSnapshotError = nil
524635
}
525636
sharedDirectoriesChanged = nil
526637
Task { @MainActor in

0 commit comments

Comments
 (0)