Skip to content

Commit

Permalink
vm(apple): implement snapshot save/restore for macOS 14
Browse files Browse the repository at this point in the history
Resolves #5376
  • Loading branch information
osy committed Aug 23, 2023
1 parent 5967988 commit 6d4ea1f
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 4 deletions.
1 change: 1 addition & 0 deletions Configuration/QEMUConstant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ enum QEMUPackageFileName: String {
case debugLog = "debug.log"
case efiVariables = "efi_vars.fd"
case tpmData = "tpmdata"
case vmState = "vmstate"
}

// MARK: Supported features
Expand Down
5 changes: 5 additions & 0 deletions Configuration/UTMAppleConfigurationBoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct UTMAppleConfigurationBoot: Codable {
var linuxCommandLine: String?
var linuxInitialRamdiskURL: URL?
var efiVariableStorageURL: URL?
var vmSavedStateURL: URL?
var hasUefiBoot: Bool = false

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

init(for operatingSystem: OperatingSystem, linuxKernelURL: URL? = nil) throws {
Expand Down Expand Up @@ -189,6 +191,9 @@ extension UTMAppleConfigurationBoot {
self.efiVariableStorageURL = efiVariableStorageURL
urls.append(efiVariableStorageURL)
}
let vmSavedStateURL = dataURL.appendingPathComponent(QEMUPackageFileName.vmState.rawValue)
self.vmSavedStateURL = vmSavedStateURL
urls.append(vmSavedStateURL)
return urls
}
}
119 changes: 115 additions & 4 deletions Services/UTMAppleVirtualMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
/// This variable MUST be synchronized by `vmQueue`
private(set) var apple: VZVirtualMachine?

private var saveSnapshotError: Error?

private var installProgress: Progress?

private var progressObserver: NSKeyValueObservation?
Expand Down Expand Up @@ -173,8 +175,13 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
}
state = .starting
do {
let isSuspended = await registryEntry.isSuspended
try await beginAccessingResources()
try await _start(options: options)
if isSuspended && !options.contains(.bootRecovery) {
try await restoreSnapshot()
} else {
try await _start(options: options)
}
if #available(macOS 12, *) {
Task { @MainActor in
sharedDirectoriesChanged = config.sharedDirectoriesPublisher.sink { [weak self] newShares in
Expand Down Expand Up @@ -328,16 +335,108 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
}
}

#if arch(arm64)
@available(macOS 14, *)
private func _saveSnapshot(url: URL) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
vmQueue.async {
guard let apple = self.apple else {
continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
return
}
apple.saveMachineStateTo(url: url) { error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
}
#endif

func saveSnapshot(name: String? = nil) async throws {
// FIXME: implement this
guard #available(macOS 14, *) else {
return
}
#if arch(arm64)
guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
return
}
if let saveSnapshotError = saveSnapshotError {
throw saveSnapshotError
}
if state == .started {
try await pause()
}
guard state == .paused else {
return
}
state = .saving
defer {
state = .paused
}
try await _saveSnapshot(url: vmSavedStateURL)
await registryEntry.setIsSuspended(true)
#endif
}

func deleteSnapshot(name: String? = nil) async throws {
// FIXME: implement this
guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
return
}
try FileManager.default.removeItem(at: vmSavedStateURL)
await registryEntry.setIsSuspended(false)
}

#if arch(arm64)
@available(macOS 14, *)
private func _restoreSnapshot(url: URL) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
vmQueue.async {
guard let apple = self.apple else {
continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
return
}
apple.restoreMachineStateFrom(url: url) { error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
}
#endif

func restoreSnapshot(name: String? = nil) async throws {
// FIXME: implement this
guard #available(macOS 14, *) else {
throw UTMAppleVirtualMachineError.operationNotAvailable
}
#if arch(arm64)
guard let vmSavedStateURL = await config.system.boot.vmSavedStateURL else {
throw UTMAppleVirtualMachineError.operationNotAvailable
}
if state == .started {
try await stop(usingMethod: .force)
}
guard state == .stopped || state == .starting else {
throw UTMAppleVirtualMachineError.operationNotAvailable
}
state = .restoring
do {
try await _restoreSnapshot(url: vmSavedStateURL)
} catch {
state = .stopped
throw error
}
state = .started
try await deleteSnapshot(name: name)
#else
throw UTMAppleVirtualMachineError.operationNotAvailable
#endif
}

private func _resume() async throws {
Expand Down Expand Up @@ -388,6 +487,17 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
vmQueue.async { [self] in
apple = VZVirtualMachine(configuration: vzConfig, queue: vmQueue)
apple!.delegate = self
saveSnapshotError = nil
#if arch(arm64)
if #available(macOS 14, *) {
do {
try vzConfig.validateSaveRestoreSupport()
} catch {
// save this for later when we want to use snapshots
saveSnapshotError = error
}
}
#endif
}
}

Expand Down Expand Up @@ -521,6 +631,7 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
vmQueue.async { [self] in
apple = nil
saveSnapshotError = nil
}
sharedDirectoriesChanged = nil
Task { @MainActor in
Expand Down

0 comments on commit 6d4ea1f

Please sign in to comment.