@@ -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