Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/Support.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -209,6 +210,7 @@
4974BC0F2B174A5700A3F38A /* ExecutionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecutionService.swift; sourceTree = "<group>"; };
4976EB882653151A006EE097 /* ChangePassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePassword.swift; sourceTree = "<group>"; };
4976EB8A265317C6006EE097 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
497B01012DDDF23900F50BC7 /* InstallTaskQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallTaskQueue.swift; sourceTree = "<group>"; };
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 = "<group>"; };
49822CE424B4C3F100E8DE54 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -345,6 +347,7 @@
isa = PBXGroup;
children = (
493DCCD12B08E5E500FA2480 /* AppCatalogController.swift */,
497B01012DDDF23900F50BC7 /* InstallTaskQueue.swift */,
);
path = Controllers;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -754,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;
Expand Down Expand Up @@ -793,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;
Expand Down Expand Up @@ -830,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;
Expand Down Expand Up @@ -863,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;
Expand Down Expand Up @@ -1014,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;
Expand Down Expand Up @@ -1048,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;
Expand Down
3 changes: 3 additions & 0 deletions src/Support/Controllers/AppCatalogController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
61 changes: 61 additions & 0 deletions src/Support/Controllers/InstallTaskQueue.swift
Original file line number Diff line number Diff line change
@@ -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<String> = []
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)
}
}
4 changes: 2 additions & 2 deletions src/Support/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2.6.2</string>
<string>2.6.3</string>
<key>CFBundleVersion</key>
<string>65</string>
<string>66</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
Expand Down
122 changes: 81 additions & 41 deletions src/Support/Views/AppCatalog/AppUpdatesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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)
}
}
}
}) {
Expand Down Expand Up @@ -202,14 +215,40 @@ 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) {
ProgressView()
.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))
Expand All @@ -220,7 +259,9 @@ struct AppUpdatesView: View {
.buttonStyle(.plain)

}

.onHover {_ in
hoveredItem = update.id
}
}

// Show update schedule information when configured
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/SupportHelper/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>2.6.2</string>
<string>2.6.3</string>
<key>NSHumanReadableCopyright</key>
<string>© 2025 Root3 B.V. All rights reserved.</string>
<key>CFBundleIdentifier</key>
Expand All @@ -13,7 +13,7 @@
<key>CFBundleName</key>
<string>SupportAppPriviligedHelper</string>
<key>CFBundleVersion</key>
<string>65</string>
<string>66</string>
<key>SMAuthorizedClients</key>
<array>
<string>anchor apple generic and identifier &quot;nl.root3.support&quot; 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] = &quot;98LJ4XBGYK&quot;)</string>
Expand Down
4 changes: 2 additions & 2 deletions src/SupportXPC/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>2.6.2</string>
<string>2.6.3</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
Expand All @@ -15,7 +15,7 @@
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleVersion</key>
<string>65</string>
<string>66</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>XPCService</key>
Expand Down