@@ -100,6 +100,8 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
100
100
/// This variable MUST be synchronized by `vmQueue`
101
101
private( set) var apple : VZVirtualMachine ?
102
102
103
+ private var saveSnapshotError : Error ?
104
+
103
105
private var installProgress : Progress ?
104
106
105
107
private var progressObserver : NSKeyValueObservation ?
@@ -173,8 +175,13 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
173
175
}
174
176
state = . starting
175
177
do {
178
+ let isSuspended = await registryEntry. isSuspended
176
179
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
+ }
178
185
if #available( macOS 12 , * ) {
179
186
Task { @MainActor in
180
187
sharedDirectoriesChanged = config. sharedDirectoriesPublisher. sink { [ weak self] newShares in
@@ -328,16 +335,108 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
328
335
}
329
336
}
330
337
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
+
331
359
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
333
383
}
334
384
335
385
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 )
337
391
}
338
392
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
+
339
414
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
341
440
}
342
441
343
442
private func _resume( ) async throws {
@@ -388,6 +487,17 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
388
487
vmQueue. async { [ self ] in
389
488
apple = VZVirtualMachine ( configuration: vzConfig, queue: vmQueue)
390
489
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
391
501
}
392
502
}
393
503
@@ -521,6 +631,7 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
521
631
func guestDidStop( _ virtualMachine: VZVirtualMachine ) {
522
632
vmQueue. async { [ self ] in
523
633
apple = nil
634
+ saveSnapshotError = nil
524
635
}
525
636
sharedDirectoriesChanged = nil
526
637
Task { @MainActor in
0 commit comments