@@ -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 ?
@@ -138,7 +140,6 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
138
140
}
139
141
140
142
private func _start( options: UTMVirtualMachineStartOptions ) async throws {
141
- try await createAppleVM ( )
142
143
let boot = await config. system. boot
143
144
try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , Error > ) -> Void in
144
145
vmQueue. async {
@@ -173,8 +174,14 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
173
174
}
174
175
state = . starting
175
176
do {
177
+ let isSuspended = await registryEntry. isSuspended
176
178
try await beginAccessingResources ( )
177
- try await _start ( options: options)
179
+ try await createAppleVM ( )
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
@@ -194,6 +201,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
194
201
} catch {
195
202
await stopAccesingResources ( )
196
203
state = . stopped
204
+ try ? await deleteSnapshot ( )
197
205
throw error
198
206
}
199
207
}
@@ -328,16 +336,109 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
328
336
}
329
337
}
330
338
339
+ #if arch(arm64)
340
+ @available ( macOS 14 , * )
341
+ private func _saveSnapshot( url: URL ) async throws {
342
+ try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , Error > ) in
343
+ vmQueue. async {
344
+ guard let apple = self . apple else {
345
+ continuation. resume ( throwing: UTMAppleVirtualMachineError . operationNotAvailable)
346
+ return
347
+ }
348
+ apple. saveMachineStateTo ( url: url) { error in
349
+ if let error = error {
350
+ continuation. resume ( throwing: error)
351
+ } else {
352
+ continuation. resume ( )
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ #endif
359
+
331
360
func saveSnapshot( name: String ? = nil ) async throws {
332
- // FIXME: implement this
361
+ guard #available( macOS 14 , * ) else {
362
+ return
363
+ }
364
+ #if arch(arm64)
365
+ guard let vmSavedStateURL = await config. system. boot. vmSavedStateURL else {
366
+ return
367
+ }
368
+ if let saveSnapshotError = saveSnapshotError {
369
+ throw saveSnapshotError
370
+ }
371
+ if state == . started {
372
+ try await pause ( )
373
+ }
374
+ guard state == . paused else {
375
+ return
376
+ }
377
+ state = . saving
378
+ defer {
379
+ state = . paused
380
+ }
381
+ try await _saveSnapshot ( url: vmSavedStateURL)
382
+ await registryEntry. setIsSuspended ( true )
383
+ #endif
333
384
}
334
385
335
386
func deleteSnapshot( name: String ? = nil ) async throws {
336
- // FIXME: implement this
387
+ guard let vmSavedStateURL = await config. system. boot. vmSavedStateURL else {
388
+ return
389
+ }
390
+ await registryEntry. setIsSuspended ( false )
391
+ try FileManager . default. removeItem ( at: vmSavedStateURL)
337
392
}
338
393
394
+ #if arch(arm64)
395
+ @available ( macOS 14 , * )
396
+ private func _restoreSnapshot( url: URL ) async throws {
397
+ try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , Error > ) in
398
+ vmQueue. async {
399
+ guard let apple = self . apple else {
400
+ continuation. resume ( throwing: UTMAppleVirtualMachineError . operationNotAvailable)
401
+ return
402
+ }
403
+ apple. restoreMachineStateFrom ( url: url) { error in
404
+ if let error = error {
405
+ continuation. resume ( throwing: error)
406
+ } else {
407
+ continuation. resume ( )
408
+ }
409
+ }
410
+ }
411
+ }
412
+ }
413
+ #endif
414
+
339
415
func restoreSnapshot( name: String ? = nil ) async throws {
340
- // FIXME: implement this
416
+ guard #available( macOS 14 , * ) else {
417
+ throw UTMAppleVirtualMachineError . operationNotAvailable
418
+ }
419
+ #if arch(arm64)
420
+ guard let vmSavedStateURL = await config. system. boot. vmSavedStateURL else {
421
+ throw UTMAppleVirtualMachineError . operationNotAvailable
422
+ }
423
+ if state == . started {
424
+ try await stop ( usingMethod: . force)
425
+ }
426
+ guard state == . stopped || state == . starting else {
427
+ throw UTMAppleVirtualMachineError . operationNotAvailable
428
+ }
429
+ state = . restoring
430
+ do {
431
+ try await _restoreSnapshot ( url: vmSavedStateURL)
432
+ try await _resume ( )
433
+ } catch {
434
+ state = . stopped
435
+ throw error
436
+ }
437
+ state = . started
438
+ try await deleteSnapshot ( name: name)
439
+ #else
440
+ throw UTMAppleVirtualMachineError . operationNotAvailable
441
+ #endif
341
442
}
342
443
343
444
private func _resume( ) async throws {
@@ -388,6 +489,17 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
388
489
vmQueue. async { [ self ] in
389
490
apple = VZVirtualMachine ( configuration: vzConfig, queue: vmQueue)
390
491
apple!. delegate = self
492
+ saveSnapshotError = nil
493
+ #if arch(arm64)
494
+ if #available( macOS 14 , * ) {
495
+ do {
496
+ try vzConfig. validateSaveRestoreSupport ( )
497
+ } catch {
498
+ // save this for later when we want to use snapshots
499
+ saveSnapshotError = error
500
+ }
501
+ }
502
+ #endif
391
503
}
392
504
}
393
505
@@ -521,6 +633,7 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
521
633
func guestDidStop( _ virtualMachine: VZVirtualMachine ) {
522
634
vmQueue. async { [ self ] in
523
635
apple = nil
636
+ saveSnapshotError = nil
524
637
}
525
638
sharedDirectoriesChanged = nil
526
639
Task { @MainActor in
0 commit comments