Просмотр исходного кода

data: convert VM list management to Swift 5.5 concurrency

osy 3 лет назад
Родитель
Сommit
fa4d8821cf

+ 28 - 22
Platform/Shared/ContentView.swift

@@ -43,7 +43,7 @@ struct ContentView: View {
                         selection: $data.selectedVM,
                         selection: $data.selectedVM,
                         label: { VMCardView(vm: vm) })
                         label: { VMCardView(vm: vm) })
                         .modifier(VMContextMenuModifier(vm: vm))
                         .modifier(VMContextMenuModifier(vm: vm))
-                }.onMove(perform: data.listMove)
+                }.onMove(perform: move)
                 .onDelete(perform: delete)
                 .onDelete(perform: delete)
                 
                 
                 if data.pendingVMs.count > 0 {
                 if data.pendingVMs.count > 0 {
@@ -86,7 +86,9 @@ struct ContentView: View {
             importSheetPresented = true
             importSheetPresented = true
         }.fileImporter(isPresented: $importSheetPresented, allowedContentTypes: [.UTM], onCompletion: selectImportedUTM)
         }.fileImporter(isPresented: $importSheetPresented, allowedContentTypes: [.UTM], onCompletion: selectImportedUTM)
         .onAppear {
         .onAppear {
-            data.listRefresh()
+            Task {
+                await data.listRefresh()
+            }
             #if os(macOS)
             #if os(macOS)
             NSWindow.allowsAutomaticWindowTabbing = false
             NSWindow.allowsAutomaticWindowTabbing = false
             #else
             #else
@@ -113,6 +115,10 @@ struct ContentView: View {
         })
         })
     }
     }
     
     
+    private func move(fromOffsets: IndexSet, toOffset: Int) {
+        data.listMove(fromOffsets: fromOffsets, toOffset: toOffset)
+    }
+    
     private func delete(indexSet: IndexSet) {
     private func delete(indexSet: IndexSet) {
         let selected = data.virtualMachines[indexSet]
         let selected = data.virtualMachines[indexSet]
         for vm in selected {
         for vm in selected {
@@ -130,22 +136,22 @@ struct ContentView: View {
     }
     }
     
     
     private func handleURL(url: URL) {
     private func handleURL(url: URL) {
-        if url.isFileURL {
-            importUTM(url: url)
-        } else if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
-                  let scheme = components.scheme,
-                  scheme.lowercased() == "utm" {
-            handleUTMURL(with: components)
+        data.busyWorkAsync {
+            if url.isFileURL {
+                try await importUTM(url: url)
+            } else if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+                      let scheme = components.scheme,
+                      scheme.lowercased() == "utm" {
+                try await handleUTMURL(with: components)
+            }
         }
         }
     }
     }
     
     
-    private func importUTM(url: URL) {
+    private func importUTM(url: URL) async throws {
         guard url.isFileURL else {
         guard url.isFileURL else {
             return // ignore
             return // ignore
         }
         }
-        data.busyWorkAsync {
-            try await data.importUTM(from: url)
-        }
+        try await data.importUTM(from: url)
     }
     }
     
     
     private func selectImportedUTM(result: Result<URL, Error>) {
     private func selectImportedUTM(result: Result<URL, Error>) {
@@ -155,10 +161,10 @@ struct ContentView: View {
         }
         }
     }
     }
     
     
-    private func handleUTMURL(with components: URLComponents) {
-        func findVM() -> UTMVirtualMachine? {
+    private func handleUTMURL(with components: URLComponents) async throws {
+        func findVM() async -> UTMVirtualMachine? {
             if let vmName = components.queryItems?.first(where: { $0.name == "name" })?.value {
             if let vmName = components.queryItems?.first(where: { $0.name == "name" })?.value {
-                return data.virtualMachines.first(where: { $0.title == vmName })
+                return await data.virtualMachines.first(where: { $0.title == vmName })
             } else {
             } else {
                 return nil
                 return nil
             }
             }
@@ -167,43 +173,43 @@ struct ContentView: View {
         if let action = components.host {
         if let action = components.host {
             switch action {
             switch action {
             case "start":
             case "start":
-                if let vm = findVM(), vm.state == .vmStopped {
+                if let vm = await findVM(), vm.state == .vmStopped {
                     data.run(vm: vm)
                     data.run(vm: vm)
                 }
                 }
                 break
                 break
             case "stop":
             case "stop":
-                if let vm = findVM(), vm.state == .vmStarted {
+                if let vm = await findVM(), vm.state == .vmStarted {
                     vm.quitVM(force: true)
                     vm.quitVM(force: true)
                     try? data.stop(vm: vm)
                     try? data.stop(vm: vm)
                 }
                 }
                 break
                 break
             case "restart":
             case "restart":
-                if let vm = findVM(), vm.state == .vmStarted {
+                if let vm = await findVM(), vm.state == .vmStarted {
                     DispatchQueue.global(qos: .background).async {
                     DispatchQueue.global(qos: .background).async {
                         vm.resetVM()
                         vm.resetVM()
                     }
                     }
                 }
                 }
                 break
                 break
             case "pause":
             case "pause":
-                if let vm = findVM(), vm.state == .vmStarted {
+                if let vm = await findVM(), vm.state == .vmStarted {
                     DispatchQueue.global(qos: .background).async {
                     DispatchQueue.global(qos: .background).async {
                         vm.pauseVM()
                         vm.pauseVM()
                     }
                     }
                 }
                 }
             case "resume":
             case "resume":
-                if let vm = findVM(), vm.state == .vmPaused {
+                if let vm = await findVM(), vm.state == .vmPaused {
                     DispatchQueue.global(qos: .background).async {
                     DispatchQueue.global(qos: .background).async {
                         vm.resumeVM()
                         vm.resumeVM()
                     }
                     }
                 }
                 }
                 break
                 break
             case "sendText":
             case "sendText":
-                if let vm = findVM(), vm.state == .vmStarted {
+                if let vm = await findVM(), vm.state == .vmStarted {
                     data.automationSendText(to: vm, urlComponents: components)
                     data.automationSendText(to: vm, urlComponents: components)
                 }
                 }
                 break
                 break
             case "click":
             case "click":
-                if let vm = findVM(), vm.state == .vmStarted {
+                if let vm = await findVM(), vm.state == .vmStarted {
                     data.automationSendMouse(to: vm, urlComponents: components)
                     data.automationSendMouse(to: vm, urlComponents: components)
                 }
                 }
                 break
                 break

+ 47 - 56
Platform/UTMData.swift

@@ -45,13 +45,13 @@ class UTMData: ObservableObject {
     }
     }
     
     
     /// View: show VM settings
     /// View: show VM settings
-    @Published var showSettingsModal: Bool
+    @MainActor @Published var showSettingsModal: Bool
     
     
     /// View: show new VM wizard
     /// View: show new VM wizard
-    @Published var showNewVMSheet: Bool
+    @MainActor @Published var showNewVMSheet: Bool
     
     
     /// View: show an alert message
     /// View: show an alert message
-    @Published var alertMessage: AlertMessage?
+    @MainActor @Published var alertMessage: AlertMessage?
     
     
     /// View: show busy spinner
     /// View: show busy spinner
     @MainActor @Published var busy: Bool
     @MainActor @Published var busy: Bool
@@ -60,14 +60,14 @@ class UTMData: ObservableObject {
     @MainActor @Published var selectedVM: UTMVirtualMachine?
     @MainActor @Published var selectedVM: UTMVirtualMachine?
     
     
     /// View: all VMs listed, we save a bookmark to each when array is modified
     /// View: all VMs listed, we save a bookmark to each when array is modified
-    @Published private(set) var virtualMachines: [UTMVirtualMachine] {
+    @MainActor @Published private(set) var virtualMachines: [UTMVirtualMachine] {
         didSet {
         didSet {
             listSaveToDefaults()
             listSaveToDefaults()
         }
         }
     }
     }
     
     
     /// View: all pending VMs listed (ZIP and IPSW downloads)
     /// View: all pending VMs listed (ZIP and IPSW downloads)
-    @Published private(set) var pendingVMs: [UTMPendingVirtualMachine]
+    @MainActor @Published private(set) var pendingVMs: [UTMPendingVirtualMachine]
     
     
     /// Temporary storage for QEMU removable drives settings
     /// Temporary storage for QEMU removable drives settings
     private var qemuRemovableDrivesCache: [String: URL]
     private var qemuRemovableDrivesCache: [String: URL]
@@ -111,13 +111,13 @@ class UTMData: ObservableObject {
     /// Re-loads UTM bundles from default path
     /// Re-loads UTM bundles from default path
     ///
     ///
     /// This removes stale entries (deleted/not accessible) and duplicate entries
     /// This removes stale entries (deleted/not accessible) and duplicate entries
-    func listRefresh() {
+    func listRefresh() async {
         // remove stale vm
         // remove stale vm
-        var list = virtualMachines.filter { (vm: UTMVirtualMachine) in vm.path != nil && fileManager.fileExists(atPath: vm.path!.path) }
+        var list = await virtualMachines.filter { (vm: UTMVirtualMachine) in vm.path != nil && fileManager.fileExists(atPath: vm.path!.path) }
         do {
         do {
             let files = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles)
             let files = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles)
             let newFiles = files.filter { newFile in
             let newFiles = files.filter { newFile in
-                !virtualMachines.contains { existingVM in
+                !list.contains { existingVM in
                     existingVM.path?.standardizedFileURL == newFile.standardizedFileURL
                     existingVM.path?.standardizedFileURL == newFile.standardizedFileURL
                 }
                 }
             }
             }
@@ -138,16 +138,13 @@ class UTMData: ObservableObject {
         } catch {
         } catch {
             logger.error("\(error.localizedDescription)")
             logger.error("\(error.localizedDescription)")
         }
         }
-        if virtualMachines != list {
-            DispatchQueue.main.async {
-                //self.objectWillChange.send()
-                self.virtualMachines = list
-            }
+        if await virtualMachines != list {
+            await listReplace(with: list)
         }
         }
     }
     }
     
     
     /// Load VM list (and order) from persistent storage
     /// Load VM list (and order) from persistent storage
-    private func listLoadFromDefaults() {
+    @MainActor private func listLoadFromDefaults() {
         let defaults = UserDefaults.standard
         let defaults = UserDefaults.standard
         // legacy path list
         // legacy path list
         if let files = defaults.array(forKey: "VMList") as? [String] {
         if let files = defaults.array(forKey: "VMList") as? [String] {
@@ -171,7 +168,7 @@ class UTMData: ObservableObject {
     }
     }
     
     
     /// Save VM list (and order) to persistent storage
     /// Save VM list (and order) to persistent storage
-    private func listSaveToDefaults() {
+    @MainActor private func listSaveToDefaults() {
         let defaults = UserDefaults.standard
         let defaults = UserDefaults.standard
         let bookmarks = virtualMachines.compactMap { vm -> Data? in
         let bookmarks = virtualMachines.compactMap { vm -> Data? in
             #if os(macOS)
             #if os(macOS)
@@ -186,6 +183,10 @@ class UTMData: ObservableObject {
         defaults.set(bookmarks, forKey: "VMList")
         defaults.set(bookmarks, forKey: "VMList")
     }
     }
     
     
+    @MainActor private func listReplace(with vms: [UTMVirtualMachine]) {
+        virtualMachines = vms
+    }
+    
     /// Add VM to list
     /// Add VM to list
     /// - Parameter vm: VM to add
     /// - Parameter vm: VM to add
     @MainActor private func listAdd(vm: UTMVirtualMachine) {
     @MainActor private func listAdd(vm: UTMVirtualMachine) {
@@ -227,27 +228,22 @@ class UTMData: ObservableObject {
     /// - Parameters:
     /// - Parameters:
     ///   - fromOffsets: Offsets from move from
     ///   - fromOffsets: Offsets from move from
     ///   - toOffset: Offsets to move to
     ///   - toOffset: Offsets to move to
-    func listMove(fromOffsets: IndexSet, toOffset: Int) {
-        DispatchQueue.main.async {
-            self.virtualMachines.move(fromOffsets: fromOffsets, toOffset: toOffset)
-        }
+    @MainActor func listMove(fromOffsets: IndexSet, toOffset: Int) {
+        virtualMachines.move(fromOffsets: fromOffsets, toOffset: toOffset)
     }
     }
     
     
     /// Discard and create a new list item
     /// Discard and create a new list item
     /// - Parameter vm: VM to discard
     /// - Parameter vm: VM to discard
     /// - Parameter newVM: VM to replace with
     /// - Parameter newVM: VM to replace with
-    private func listRecreate(vm: UTMVirtualMachine, with newVM: UTMVirtualMachine) {
-        DispatchQueue.main.async {
-            //self.objectWillChange.send()
-            if let index = self.virtualMachines.firstIndex(of: vm) {
-                self.virtualMachines.remove(at: index)
-                self.virtualMachines.insert(newVM, at: index)
-            } else {
-                self.virtualMachines.insert(newVM, at: 0)
-            }
-            if self.selectedVM == vm {
-                self.selectedVM = newVM
-            }
+    @MainActor private func listRecreate(vm: UTMVirtualMachine, with newVM: UTMVirtualMachine) {
+        if let index = virtualMachines.firstIndex(of: vm) {
+            virtualMachines.remove(at: index)
+            virtualMachines.insert(newVM, at: index)
+        } else {
+            virtualMachines.insert(newVM, at: 0)
+        }
+        if selectedVM == vm {
+            selectedVM = newVM
         }
         }
     }
     }
     
     
@@ -330,7 +326,7 @@ class UTMData: ObservableObject {
     
     
     /// Save an existing VM to disk
     /// Save an existing VM to disk
     /// - Parameter vm: VM to save
     /// - Parameter vm: VM to save
-    func save(vm: UTMVirtualMachine) throws {
+    func save(vm: UTMVirtualMachine) async throws {
         do {
         do {
             try vm.saveUTM()
             try vm.saveUTM()
             if let qemuVM = vm as? UTMQemuVirtualMachine {
             if let qemuVM = vm as? UTMQemuVirtualMachine {
@@ -350,7 +346,7 @@ class UTMData: ObservableObject {
                     logger.debug("Cannot create new object for \(path.path)")
                     logger.debug("Cannot create new object for \(path.path)")
                     return
                     return
                 }
                 }
-                listRecreate(vm: vm, with: newVM)
+                await listRecreate(vm: vm, with: newVM)
             }
             }
             throw error
             throw error
         }
         }
@@ -386,20 +382,17 @@ class UTMData: ObservableObject {
     /// Save a new VM to disk
     /// Save a new VM to disk
     /// - Parameters:
     /// - Parameters:
     ///   - config: New VM configuration
     ///   - config: New VM configuration
-    ///   - onCompletion: Completion handler
-    func create(config: UTMConfigurable, onCompletion: @escaping (UTMVirtualMachine) -> Void = { _ in }) throws {
-        guard !virtualMachines.contains(where: { $0.config.name == config.name }) else {
+    func create(config: UTMConfigurable) async throws -> UTMVirtualMachine {
+        guard await !virtualMachines.contains(where: { $0.config.name == config.name }) else {
             throw NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData")
             throw NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData")
         }
         }
         let vm = UTMVirtualMachine(configuration: config, withDestinationURL: documentsURL)
         let vm = UTMVirtualMachine(configuration: config, withDestinationURL: documentsURL)
-        try save(vm: vm)
+        try await save(vm: vm)
         if let qemuVM = vm as? UTMQemuVirtualMachine {
         if let qemuVM = vm as? UTMQemuVirtualMachine {
             try commitRemovableDriveImages(for: qemuVM)
             try commitRemovableDriveImages(for: qemuVM)
         }
         }
-        DispatchQueue.main.async {
-            self.virtualMachines.append(vm)
-            onCompletion(vm)
-        }
+        await listAdd(vm: vm)
+        return vm
     }
     }
     
     
     /// Delete a VM from disk
     /// Delete a VM from disk
@@ -458,18 +451,16 @@ class UTMData: ObservableObject {
     
     
     /// Open settings modal
     /// Open settings modal
     /// - Parameter vm: VM to edit settings
     /// - Parameter vm: VM to edit settings
-    func edit(vm: UTMVirtualMachine) {
-        DispatchQueue.main.async {
-            // show orphans for proper removal
-            if let config = vm.config as? UTMQemuConfiguration {
-                config.recoverOrphanedDrives()
-            }
-            self.selectedVM = vm
-            self.showNewVMSheet = false
-            // SwiftUI bug: cannot show modal at the same time as changing selected VM or it breaks
-            DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) {
-                self.showSettingsModal = true
-            }
+    @MainActor func edit(vm: UTMVirtualMachine) {
+        // show orphans for proper removal
+        if let config = vm.config as? UTMQemuConfiguration {
+            config.recoverOrphanedDrives()
+        }
+        selectedVM = vm
+        showNewVMSheet = false
+        // SwiftUI bug: cannot show modal at the same time as changing selected VM or it breaks
+        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) {
+            self.showSettingsModal = true
         }
         }
     }
     }
     
     
@@ -513,7 +504,7 @@ class UTMData: ObservableObject {
         let fileBasePath = url.deletingLastPathComponent()
         let fileBasePath = url.deletingLastPathComponent()
         let fileName = url.lastPathComponent
         let fileName = url.lastPathComponent
         let dest = documentsURL.appendingPathComponent(fileName, isDirectory: true)
         let dest = documentsURL.appendingPathComponent(fileName, isDirectory: true)
-        if let vm = virtualMachines.first(where: { vm -> Bool in
+        if let vm = await virtualMachines.first(where: { vm -> Bool in
             guard let vmPath = vm.path else {
             guard let vmPath = vm.path else {
                 return false
                 return false
             }
             }
@@ -568,12 +559,12 @@ class UTMData: ObservableObject {
         Task {
         Task {
             let task = UTMDownloadIPSWTask(for: config)
             let task = UTMDownloadIPSWTask(for: config)
             do {
             do {
-                guard !virtualMachines.contains(where: { $0.config.name == config.name }) else {
+                guard await !virtualMachines.contains(where: { $0.config.name == config.name }) else {
                     throw NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData")
                     throw NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData")
                 }
                 }
                 await listAdd(pendingVM: task.pendingVM)
                 await listAdd(pendingVM: task.pendingVM)
                 if let vm = try await task.download() {
                 if let vm = try await task.download() {
-                    try save(vm: vm)
+                    try await save(vm: vm)
                     await listAdd(vm: vm)
                     await listAdd(vm: vm)
                 }
                 }
             } catch {
             } catch {

+ 3 - 3
Platform/iOS/VMSettingsView.swift

@@ -99,11 +99,11 @@ struct VMSettingsView: View {
     
     
     func save() {
     func save() {
         presentationMode.wrappedValue.dismiss()
         presentationMode.wrappedValue.dismiss()
-        data.busyWork {
+        data.busyWorkAsync {
             if let existing = self.vm {
             if let existing = self.vm {
-                try data.save(vm: existing)
+                try await data.save(vm: existing)
             } else {
             } else {
-                try data.create(config: self.config)
+                _ = try await data.create(config: self.config)
             }
             }
         }
         }
     }
     }

+ 8 - 10
Platform/iOS/VMWizardView.swift

@@ -93,17 +93,15 @@ fileprivate struct WizardWrapper: View {
                 } else if wizardState.currentPage == .summary {
                 } else if wizardState.currentPage == .summary {
                     Button("Save") {
                     Button("Save") {
                         onDismiss()
                         onDismiss()
-                        data.busyWork {
+                        data.busyWorkAsync {
                             let config = try wizardState.generateConfig()
                             let config = try wizardState.generateConfig()
-                            try data.create(config: config) { vm in
-                                data.selectedVM = vm
-                                if wizardState.isOpenSettingsAfterCreation {
-                                    data.showSettingsModal = true
-                                }
-                                if let qemuVm = vm as? UTMQemuVirtualMachine {
-                                    data.busyWork {
-                                        try wizardState.qemuPostCreate(with: qemuVm)
-                                    }
+                            let vm = try await data.create(config: config)
+                            if wizardState.isOpenSettingsAfterCreation {
+                                data.showSettingsModal = true
+                            }
+                            if let qemuVm = vm as? UTMQemuVirtualMachine {
+                                data.busyWork {
+                                    try wizardState.qemuPostCreate(with: qemuVm)
                                 }
                                 }
                             }
                             }
                         }
                         }

+ 3 - 3
Platform/macOS/VMSettingsView.swift

@@ -56,11 +56,11 @@ struct VMSettingsView<Config: ObservableObject & UTMConfigurable>: View {
     
     
     func save() {
     func save() {
         presentationMode.wrappedValue.dismiss()
         presentationMode.wrappedValue.dismiss()
-        data.busyWork {
+        data.busyWorkAsync {
             if let existing = self.vm {
             if let existing = self.vm {
-                try data.save(vm: existing)
+                try await data.save(vm: existing)
             } else {
             } else {
-                try data.create(config: self.config)
+                _ = try await data.create(config: self.config)
             }
             }
         }
         }
     }
     }

+ 8 - 10
Platform/macOS/VMWizardView.swift

@@ -81,7 +81,7 @@ struct VMWizardView: View {
                     } else if wizardState.currentPage == .summary {
                     } else if wizardState.currentPage == .summary {
                         Button("Save") {
                         Button("Save") {
                             presentationMode.wrappedValue.dismiss()
                             presentationMode.wrappedValue.dismiss()
-                            data.busyWork {
+                            data.busyWorkAsync {
                                 let config = try wizardState.generateConfig()
                                 let config = try wizardState.generateConfig()
                                 #if arch(arm64)
                                 #if arch(arm64)
                                 if #available(macOS 12, *), wizardState.isPendingIPSWDownload {
                                 if #available(macOS 12, *), wizardState.isPendingIPSWDownload {
@@ -89,15 +89,13 @@ struct VMWizardView: View {
                                     return
                                     return
                                 }
                                 }
                                 #endif
                                 #endif
-                                try data.create(config: config) { vm in
-                                    data.selectedVM = vm
-                                    if wizardState.isOpenSettingsAfterCreation {
-                                        data.showSettingsModal = true
-                                    }
-                                    if let qemuVm = vm as? UTMQemuVirtualMachine {
-                                        data.busyWork {
-                                            try wizardState.qemuPostCreate(with: qemuVm)
-                                        }
+                                let vm = try await data.create(config: config)
+                                if wizardState.isOpenSettingsAfterCreation {
+                                    data.showSettingsModal = true
+                                }
+                                if let qemuVm = vm as? UTMQemuVirtualMachine {
+                                    data.busyWork {
+                                        try wizardState.qemuPostCreate(with: qemuVm)
                                     }
                                     }
                                 }
                                 }
                             }
                             }