From 35aa1d5fc590fc346687da714217c8fc47a362bd Mon Sep 17 00:00:00 2001 From: Jordy Witteman Date: Wed, 21 May 2025 14:58:43 +0200 Subject: [PATCH 1/2] App Catalog FIFO queue App Catalog FIFO queue --- src/Support.xcodeproj/project.pbxproj | 4 + .../Controllers/AppCatalogController.swift | 3 + .../Controllers/InstallTaskQueue.swift | 61 +++++++++ src/Support/Info.plist | 2 +- .../Views/AppCatalog/AppUpdatesView.swift | 122 ++++++++++++------ src/SupportHelper/Info.plist | 2 +- src/SupportXPC/Info.plist | 2 +- 7 files changed, 152 insertions(+), 44 deletions(-) create mode 100644 src/Support/Controllers/InstallTaskQueue.swift diff --git a/src/Support.xcodeproj/project.pbxproj b/src/Support.xcodeproj/project.pbxproj index a3c780b..f54b362 100644 --- a/src/Support.xcodeproj/project.pbxproj +++ b/src/Support.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 4974BC112B17645A00A3F38A /* SupportXPCProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4947C1522B154A2F00276EFD /* SupportXPCProtocol.swift */; }; 4976EB892653151A006EE097 /* ChangePassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4976EB882653151A006EE097 /* ChangePassword.swift */; }; 4976EB8B265317C6006EE097 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4976EB8A265317C6006EE097 /* AppView.swift */; }; + 497B01022DDDF23900F50BC7 /* InstallTaskQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497B01012DDDF23900F50BC7 /* InstallTaskQueue.swift */; }; 49822CE324B4C3F100E8DE54 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49822CE224B4C3F100E8DE54 /* AppDelegate.swift */; }; 49822CE524B4C3F100E8DE54 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49822CE424B4C3F100E8DE54 /* ContentView.swift */; }; 49822CE724B4C3F200E8DE54 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49822CE624B4C3F200E8DE54 /* Assets.xcassets */; }; @@ -209,6 +210,7 @@ 4974BC0F2B174A5700A3F38A /* ExecutionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecutionService.swift; sourceTree = ""; }; 4976EB882653151A006EE097 /* ChangePassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePassword.swift; sourceTree = ""; }; 4976EB8A265317C6006EE097 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; + 497B01012DDDF23900F50BC7 /* InstallTaskQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallTaskQueue.swift; sourceTree = ""; }; 49822CDF24B4C3F100E8DE54 /* Support.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Support.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49822CE224B4C3F100E8DE54 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49822CE424B4C3F100E8DE54 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -345,6 +347,7 @@ isa = PBXGroup; children = ( 493DCCD12B08E5E500FA2480 /* AppCatalogController.swift */, + 497B01012DDDF23900F50BC7 /* InstallTaskQueue.swift */, ); path = Controllers; sourceTree = ""; @@ -669,6 +672,7 @@ 4972476828DBB1AB007194F0 /* StatusItemBadgeView.swift in Sources */, 496FE4D12651485E007746ED /* UserInfo.swift in Sources */, 4915290D259CCF7A00056B5F /* NotificationBadgeView.swift in Sources */, + 497B01022DDDF23900F50BC7 /* InstallTaskQueue.swift in Sources */, 49857E9A24D4B58B009B6FBA /* ComputerInfo.swift in Sources */, 496C2FE1271477B800D51EE1 /* NotificationNames.swift in Sources */, 0A4A738E26020A6500927DAB /* MacOSVersionSubview.swift in Sources */, diff --git a/src/Support/Controllers/AppCatalogController.swift b/src/Support/Controllers/AppCatalogController.swift index 172be8e..7758cbe 100644 --- a/src/Support/Controllers/AppCatalogController.swift +++ b/src/Support/Controllers/AppCatalogController.swift @@ -32,6 +32,9 @@ class AppCatalogController: ObservableObject { // Current apps updating @Published var appsUpdating: [String] = [] + // Current apps in the queue + @Published var appsQueued: [String] = [] + // Show app updates @Published var showAppUpdates: Bool = false diff --git a/src/Support/Controllers/InstallTaskQueue.swift b/src/Support/Controllers/InstallTaskQueue.swift new file mode 100644 index 0000000..cd8c061 --- /dev/null +++ b/src/Support/Controllers/InstallTaskQueue.swift @@ -0,0 +1,61 @@ +// +// InstallTaskQueue.swift +// Support +// +// Created by Jordy Witteman on 21/05/2025. +// + +import Foundation + +class InstallTaskQueue { + + // Create singleton + static let shared = InstallTaskQueue() + + // Create queue + let queue = Queue() + + actor Queue { + private var tasks: [(id: String, task: () async -> Void)] = [] + private var cancelledTaskIDs: Set = [] + private var isRunning = false + + func enqueue(id: String, _ task: @escaping () async -> Void) { + tasks.append((id, task)) + + // Check if current task is running before running the next task + if !isRunning { + isRunning = true + Task { + await runNext() + } + } + } + + func cancel(_ id: String) { + cancelledTaskIDs.insert(id) + } + + // Run tasks + private func runNext() async { + while !tasks.isEmpty { + let (id, task) = tasks.removeFirst() + if !cancelledTaskIDs.contains(id) { + await task() + } + cancelledTaskIDs.remove(id) + } + isRunning = false + } + } + + // Function to add new tasks + func submit(id: String, task: @escaping () async -> Void) async { + await queue.enqueue(id: id, task) + } + + // Function to cancel task based on ID + func cancel(taskID: String) async { + await queue.cancel(taskID) + } +} diff --git a/src/Support/Info.plist b/src/Support/Info.plist index 81f5ced..bd5bde8 100644 --- a/src/Support/Info.plist +++ b/src/Support/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.6.2 + 2.6.3 CFBundleVersion 65 LSApplicationCategoryType diff --git a/src/Support/Views/AppCatalog/AppUpdatesView.swift b/src/Support/Views/AppCatalog/AppUpdatesView.swift index d433ec4..62f5b88 100644 --- a/src/Support/Views/AppCatalog/AppUpdatesView.swift +++ b/src/Support/Views/AppCatalog/AppUpdatesView.swift @@ -41,6 +41,10 @@ struct AppUpdatesView: View { // Get preferences or default values @StateObject var preferences = Preferences() + + // Update cancel hover state + @State private var hoveredCancelButton: Bool = false + @State private var hoveredItem: String? var body: some View { @@ -71,13 +75,22 @@ struct AppUpdatesView: View { if appCatalogController.updateDetails.count > 0 { Button(action: { - for app in appCatalogController.updateDetails { - Task { + Task { + for app in appCatalogController.updateDetails { // Validate Catalog Agent code requirement guard verifyAppCatalogCodeRequirement() else { return } - await updateApp(bundleID: app.id) + + // Append app to queue + await MainActor.run() { + appCatalogController.appsQueued.append(app.id) + } + + // Update app + await InstallTaskQueue.shared.submit(id: app.id) { + await updateApp(bundleID: app.id) + } } } }) { @@ -202,7 +215,16 @@ struct AppUpdatesView: View { guard verifyAppCatalogCodeRequirement() else { return } - await updateApp(bundleID: update.id) + + // Append app to queue + await MainActor.run() { + appCatalogController.appsQueued.append(update.id) + } + + // Update app + await InstallTaskQueue.shared.submit(id: update.id) { + await updateApp(bundleID: update.id) + } } }) { if appCatalogController.appsUpdating.contains(update.id) { @@ -210,6 +232,23 @@ struct AppUpdatesView: View { .scaleEffect(0.6) .frame(width: 26, height: 26) .padding(.leading, 10) + } else if appCatalogController.appsQueued.contains(update.id) { + Image(systemName: hoveredCancelButton && (hoveredItem == update.id) ? "xmark.circle.fill" : "clock") + .font(.system(size: 16)) + .frame(width: 26, height: 26) + .onHover { hover in + hoveredCancelButton = hover + } + .animation(.easeOut(duration: 0.2), value: hoveredCancelButton && (hoveredItem == update.id)) + .onTapGesture { + Task { + await InstallTaskQueue.shared.cancel(taskID: update.id) + await MainActor.run { + appCatalogController.appsQueued.removeAll(where: { $0 == update.id }) + } + appCatalogController.logger.debug("App \(update.id, privacy: .public) update cancelled") + } + } } else { Image(systemName: "icloud.and.arrow.down") .font(.system(size: 16, weight: .medium)) @@ -220,7 +259,9 @@ struct AppUpdatesView: View { .buttonStyle(.plain) } - + .onHover {_ in + hoveredItem = update.id + } } // Show update schedule information when configured @@ -328,58 +369,57 @@ struct AppUpdatesView: View { // MARK: - Function to update app using App Catalog func updateApp(bundleID: String) async { + appCatalogController.logger.debug("App \(bundleID, privacy: .public) added to update queue") + // Command to update app let command = "'/usr/local/bin/catalog --install \(bundleID) --update-action --support-app'" + // Remove Bundle ID from queued array + await MainActor.run { + appCatalogController.appsQueued.removeAll(where: { $0 == bundleID }) + } + // Add bundle ID to apps currently updating appCatalogController.appsUpdating.append(bundleID) do { - try ExecutionService.executeScript(command: command) { exitCode in - - if exitCode == 0 { - appCatalogController.logger.log("App \(bundleID, privacy: .public) successfully updated") - - // Temporarily drop app from updates array so it will not show once completed. Then we check updates again to verify the update was really successful - if appCatalogController.updateDetails.contains(where: { $0.id == bundleID } ) { - if let index = appCatalogController.updateDetails.firstIndex(where: { $0.id == bundleID } ) { - DispatchQueue.main.async { - appCatalogController.updateDetails.remove(at: index) - } - } - } - - } else { - appCatalogController.logger.error("Failed to update app \(bundleID, privacy: .public)") + + let exitCode: NSNumber = try await withCheckedThrowingContinuation { continuation in + try? ExecutionService.executeScript(command: command) { exitCode in + continuation.resume(returning: exitCode) } + } + + if exitCode == 0 { + appCatalogController.logger.log("App \(bundleID, privacy: .public) successfully updated") - // Stop update spinner - if appCatalogController.appsUpdating.contains(bundleID) { - if let index = appCatalogController.appsUpdating.firstIndex(of: bundleID) { - DispatchQueue.main.async { - appCatalogController.appsUpdating.remove(at: index) - - // Check for updates again when apps currently updating is empty - if appCatalogController.appsUpdating.isEmpty { - // Trigger check for app updates - appCatalogController.ignoreUpdateChange = true - appCatalogController.getAppUpdates() - } - } - } + // Temporarily drop app from updates array so it will not show once completed. Then we check updates again to verify the update was really successful + await MainActor.run { + appCatalogController.updateDetails.removeAll(where: { $0.id == bundleID }) } + } else { + appCatalogController.logger.error("Failed to update app \(bundleID, privacy: .public)") } + + // Stop update spinner + await MainActor.run { + appCatalogController.appsUpdating.removeAll(where: { $0 == bundleID }) + + // Check for updates again when apps currently updating is empty + if appCatalogController.appsUpdating.isEmpty { + // Trigger check for app updates + appCatalogController.ignoreUpdateChange = true + appCatalogController.getAppUpdates() + } + } + } catch { appCatalogController.logger.log("Failed to update app \(bundleID, privacy: .public)") // Stop update spinner - if appCatalogController.appsUpdating.contains(bundleID) { - if let index = appCatalogController.appsUpdating.firstIndex(of: bundleID) { - DispatchQueue.main.async { - appCatalogController.appsUpdating.remove(at: index) - } - } + await MainActor.run { + appCatalogController.appsUpdating.removeAll(where: { $0 == bundleID }) } // Trigger check for app updates diff --git a/src/SupportHelper/Info.plist b/src/SupportHelper/Info.plist index 6c3eed4..ffc65f7 100644 --- a/src/SupportHelper/Info.plist +++ b/src/SupportHelper/Info.plist @@ -3,7 +3,7 @@ CFBundleShortVersionString - 2.6.2 + 2.6.3 NSHumanReadableCopyright © 2025 Root3 B.V. All rights reserved. CFBundleIdentifier diff --git a/src/SupportXPC/Info.plist b/src/SupportXPC/Info.plist index 2492fc6..2fff43c 100644 --- a/src/SupportXPC/Info.plist +++ b/src/SupportXPC/Info.plist @@ -3,7 +3,7 @@ CFBundleShortVersionString - 2.6.2 + 2.6.3 CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleName From 7617fef2a1d5dc0aec366209155551bf108a4382 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 21 May 2025 12:59:38 +0000 Subject: [PATCH 2/2] Bump build number to 66 --- src/Support.xcodeproj/project.pbxproj | 12 ++++++------ src/Support/Info.plist | 2 +- src/SupportHelper/Info.plist | 2 +- src/SupportXPC/Info.plist | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Support.xcodeproj/project.pbxproj b/src/Support.xcodeproj/project.pbxproj index f54b362..8d59c78 100644 --- a/src/Support.xcodeproj/project.pbxproj +++ b/src/Support.xcodeproj/project.pbxproj @@ -758,7 +758,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; CREATE_INFOPLIST_SECTION_IN_BINARY = YES; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK; @@ -797,7 +797,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; CREATE_INFOPLIST_SECTION_IN_BINARY = YES; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK; @@ -834,7 +834,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK; ENABLE_HARDENED_RUNTIME = YES; @@ -867,7 +867,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK; ENABLE_HARDENED_RUNTIME = YES; @@ -1018,7 +1018,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"Support/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK; @@ -1052,7 +1052,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 65; + CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"Support/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK; diff --git a/src/Support/Info.plist b/src/Support/Info.plist index bd5bde8..234edae 100644 --- a/src/Support/Info.plist +++ b/src/Support/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.6.3 CFBundleVersion - 65 + 66 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/src/SupportHelper/Info.plist b/src/SupportHelper/Info.plist index ffc65f7..3380f02 100644 --- a/src/SupportHelper/Info.plist +++ b/src/SupportHelper/Info.plist @@ -13,7 +13,7 @@ CFBundleName SupportAppPriviligedHelper CFBundleVersion - 65 + 66 SMAuthorizedClients anchor apple generic and identifier "nl.root3.support" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "98LJ4XBGYK") diff --git a/src/SupportXPC/Info.plist b/src/SupportXPC/Info.plist index 2fff43c..efb5bdd 100644 --- a/src/SupportXPC/Info.plist +++ b/src/SupportXPC/Info.plist @@ -15,7 +15,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleVersion - 65 + 66 CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) XPCService