@@ -155,6 +155,8 @@ final class UTMQemuVirtualMachine: UTMSpiceVirtualMachine {
155
155
156
156
private var changeCursorRequestInProgress : Bool = false
157
157
158
+ private static var resourceCacheOperationQueue = DispatchQueue ( label: " Resource Cache Operation " )
159
+
158
160
#if WITH_SERVER
159
161
@Setting ( " ServerPort " ) private var serverPort : Int = 0
160
162
private var spicePort : SwiftPortmap . Port ?
@@ -275,6 +277,10 @@ extension UTMQemuVirtualMachine {
275
277
guard await isSupported else {
276
278
throw UTMQemuVirtualMachineError . emulationNotSupported
277
279
}
280
+
281
+ // create QEMU resource cache if needed
282
+ try await ensureQemuResourceCacheUpToDate ( )
283
+
278
284
let hasDebugLog = await config. qemu. hasDebugLog
279
285
// start logging
280
286
if hasDebugLog, let debugLogURL = await config. qemu. debugLogURL {
@@ -885,6 +891,87 @@ extension UTMQemuVirtualMachine {
885
891
}
886
892
}
887
893
894
+ // MARK: - Caching QEMU resources
895
+ extension UTMQemuVirtualMachine {
896
+ private func _ensureQemuResourceCacheUpToDate( ) throws {
897
+ let fm = FileManager . default
898
+ let qemuResourceUrl = Bundle . main. url ( forResource: " qemu " , withExtension: nil ) !
899
+ let cacheUrl = try fm. url ( for: . cachesDirectory, in: . userDomainMask, appropriateFor: nil , create: true )
900
+ let qemuCacheUrl = cacheUrl. appendingPathComponent ( " qemu " , isDirectory: true )
901
+
902
+ guard fm. fileExists ( atPath: qemuCacheUrl. path) else {
903
+ try fm. copyItem ( at: qemuResourceUrl, to: qemuCacheUrl)
904
+ return
905
+ }
906
+
907
+ logger. info ( " Updating QEMU resource cache... " )
908
+ // first visit all the subdirectories and create them if needed
909
+ let subdirectoryEnumerator = fm. enumerator ( at: qemuResourceUrl, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles, . producesRelativePathURLs, . includesDirectoriesPostOrder] ) !
910
+ for case let directoryURL as URL in subdirectoryEnumerator {
911
+ guard subdirectoryEnumerator. isEnumeratingDirectoryPostOrder else {
912
+ continue
913
+ }
914
+ let relativePath = directoryURL. relativePath
915
+ let destUrl = qemuCacheUrl. appendingPathComponent ( relativePath)
916
+ var isDirectory : ObjCBool = false
917
+ if fm. fileExists ( atPath: destUrl. path, isDirectory: & isDirectory) {
918
+ // old file is now a directory
919
+ if !isDirectory. boolValue {
920
+ logger. info ( " Removing file \( destUrl. path) " )
921
+ try fm. removeItem ( at: destUrl)
922
+ } else {
923
+ continue
924
+ }
925
+ }
926
+ logger. info ( " Creating directory \( destUrl. path) " )
927
+ try fm. createDirectory ( at: destUrl, withIntermediateDirectories: true )
928
+ }
929
+ // next check all the files
930
+ let fileEnumerator = fm. enumerator ( at: qemuResourceUrl, includingPropertiesForKeys: [ . contentModificationDateKey, . fileSizeKey, . isDirectoryKey] , options: [ . skipsHiddenFiles, . producesRelativePathURLs] ) !
931
+ for case let sourceUrl as URL in fileEnumerator {
932
+ let relativePath = sourceUrl. relativePath
933
+ let sourceResourceValues = try sourceUrl. resourceValues ( forKeys: [ . contentModificationDateKey, . fileSizeKey, . isDirectoryKey] )
934
+ guard !sourceResourceValues. isDirectory! else {
935
+ continue
936
+ }
937
+ let destUrl = qemuCacheUrl. appendingPathComponent ( relativePath)
938
+ if fm. fileExists ( atPath: destUrl. path) {
939
+ // first do a quick comparsion with resource keys
940
+ let destResourceValues = try destUrl. resourceValues ( forKeys: [ . contentModificationDateKey, . fileSizeKey, . isDirectoryKey] )
941
+ // old directory is now a file
942
+ if destResourceValues. isDirectory! {
943
+ logger. info ( " Removing directory \( destUrl. path) " )
944
+ try fm. removeItem ( at: destUrl)
945
+ } else if destResourceValues. contentModificationDate == sourceResourceValues. contentModificationDate && destResourceValues. fileSize == sourceResourceValues. fileSize {
946
+ // assume the file is the same
947
+ continue
948
+ } else {
949
+ logger. info ( " Removing file \( destUrl. path) " )
950
+ try fm. removeItem ( at: destUrl)
951
+ }
952
+ }
953
+ // if we are here, the file has changed
954
+ logger. info ( " Copying file \( sourceUrl. path) to \( destUrl. path) " )
955
+ try fm. copyItem ( at: sourceUrl, to: destUrl)
956
+ }
957
+ }
958
+
959
+ func ensureQemuResourceCacheUpToDate( ) async throws {
960
+ try await withCheckedThrowingContinuation { continuation in
961
+ Self . resourceCacheOperationQueue. async { [ weak self] in
962
+ do {
963
+ try self ? . _ensureQemuResourceCacheUpToDate ( )
964
+ continuation. resume ( )
965
+ } catch {
966
+ continuation. resume ( throwing: error)
967
+ }
968
+ }
969
+ }
970
+ }
971
+ }
972
+
973
+ // MARK: - Errors
974
+
888
975
enum UTMQemuVirtualMachineError : Error {
889
976
case failedToAccessShortcut
890
977
case emulationNotSupported
0 commit comments