Skip to content

Commit

Permalink
vm(qemu): load QEMU resources from Caches
Browse files Browse the repository at this point in the history
For reasons still unknown, in the App Store build, we can no longer read the
BIOS from /Applications/UTM.app/Contents/Resources/qemu as a bookmark even
though this had worked fine in the past. As a workaround, we will now copy
all the data files to the App Sandbox's Cache and then share a bookmark to
that.

Resolves #6861
  • Loading branch information
osy committed Dec 3, 2024
1 parent 5bf27b8 commit ff07e74
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Configuration/UTMQemuConfiguration+Arguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ import Virtualization // for getting network interfaces
}

private var resourceURL: URL {
Bundle.main.url(forResource: "qemu", withExtension: nil)!
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("qemu", isDirectory: true)
}

private var soundBackend: UTMQEMUSoundBackend {
Expand Down
87 changes: 87 additions & 0 deletions Services/UTMQemuVirtualMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ final class UTMQemuVirtualMachine: UTMSpiceVirtualMachine {

private var changeCursorRequestInProgress: Bool = false

private static var resourceCacheOperationQueue = DispatchQueue(label: "Resource Cache Operation")

#if WITH_SERVER
@Setting("ServerPort") private var serverPort: Int = 0
private var spicePort: SwiftPortmap.Port?
Expand Down Expand Up @@ -275,6 +277,10 @@ extension UTMQemuVirtualMachine {
guard await isSupported else {
throw UTMQemuVirtualMachineError.emulationNotSupported
}

// create QEMU resource cache if needed
try await ensureQemuResourceCacheUpToDate()

let hasDebugLog = await config.qemu.hasDebugLog
// start logging
if hasDebugLog, let debugLogURL = await config.qemu.debugLogURL {
Expand Down Expand Up @@ -885,6 +891,87 @@ extension UTMQemuVirtualMachine {
}
}

// MARK: - Caching QEMU resources
extension UTMQemuVirtualMachine {
private func _ensureQemuResourceCacheUpToDate() throws {
let fm = FileManager.default
let qemuResourceUrl = Bundle.main.url(forResource: "qemu", withExtension: nil)!
let cacheUrl = try fm.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let qemuCacheUrl = cacheUrl.appendingPathComponent("qemu", isDirectory: true)

guard fm.fileExists(atPath: qemuCacheUrl.path) else {
try fm.copyItem(at: qemuResourceUrl, to: qemuCacheUrl)
return
}

logger.info("Updating QEMU resource cache...")
// first visit all the subdirectories and create them if needed
let subdirectoryEnumerator = fm.enumerator(at: qemuResourceUrl, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .producesRelativePathURLs, .includesDirectoriesPostOrder])!
for case let directoryURL as URL in subdirectoryEnumerator {
guard subdirectoryEnumerator.isEnumeratingDirectoryPostOrder else {
continue
}
let relativePath = directoryURL.relativePath
let destUrl = qemuCacheUrl.appendingPathComponent(relativePath)
var isDirectory: ObjCBool = false
if fm.fileExists(atPath: destUrl.path, isDirectory: &isDirectory) {
// old file is now a directory
if !isDirectory.boolValue {
logger.info("Removing file \(destUrl.path)")
try fm.removeItem(at: destUrl)
} else {
continue
}
}
logger.info("Creating directory \(destUrl.path)")
try fm.createDirectory(at: destUrl, withIntermediateDirectories: true)
}
// next check all the files
let fileEnumerator = fm.enumerator(at: qemuResourceUrl, includingPropertiesForKeys: [.contentModificationDateKey, .fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .producesRelativePathURLs])!
for case let sourceUrl as URL in fileEnumerator {
let relativePath = sourceUrl.relativePath
let sourceResourceValues = try sourceUrl.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey, .isDirectoryKey])
guard !sourceResourceValues.isDirectory! else {
continue
}
let destUrl = qemuCacheUrl.appendingPathComponent(relativePath)
if fm.fileExists(atPath: destUrl.path) {
// first do a quick comparsion with resource keys
let destResourceValues = try destUrl.resourceValues(forKeys: [.contentModificationDateKey, .fileSizeKey, .isDirectoryKey])
// old directory is now a file
if destResourceValues.isDirectory! {
logger.info("Removing directory \(destUrl.path)")
try fm.removeItem(at: destUrl)
} else if destResourceValues.contentModificationDate == sourceResourceValues.contentModificationDate && destResourceValues.fileSize == sourceResourceValues.fileSize {
// assume the file is the same
continue
} else {
logger.info("Removing file \(destUrl.path)")
try fm.removeItem(at: destUrl)
}
}
// if we are here, the file has changed
logger.info("Copying file \(sourceUrl.path) to \(destUrl.path)")
try fm.copyItem(at: sourceUrl, to: destUrl)
}
}

func ensureQemuResourceCacheUpToDate() async throws {
try await withCheckedThrowingContinuation { continuation in
Self.resourceCacheOperationQueue.async { [weak self] in
do {
try self?._ensureQemuResourceCacheUpToDate()
continuation.resume()
} catch {
continuation.resume(throwing: error)
}
}
}
}
}

// MARK: - Errors

enum UTMQemuVirtualMachineError: Error {
case failedToAccessShortcut
case emulationNotSupported
Expand Down

0 comments on commit ff07e74

Please sign in to comment.