Skip to content

Commit 94c8639

Browse files
Merge pull request #224 from root3nl/development
v2.6.3
2 parents 12d3a75 + 7617fef commit 94c8639

7 files changed

Lines changed: 161 additions & 53 deletions

File tree

src/Support.xcodeproj/project.pbxproj

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
4974BC112B17645A00A3F38A /* SupportXPCProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4947C1522B154A2F00276EFD /* SupportXPCProtocol.swift */; };
6666
4976EB892653151A006EE097 /* ChangePassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4976EB882653151A006EE097 /* ChangePassword.swift */; };
6767
4976EB8B265317C6006EE097 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4976EB8A265317C6006EE097 /* AppView.swift */; };
68+
497B01022DDDF23900F50BC7 /* InstallTaskQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497B01012DDDF23900F50BC7 /* InstallTaskQueue.swift */; };
6869
49822CE324B4C3F100E8DE54 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49822CE224B4C3F100E8DE54 /* AppDelegate.swift */; };
6970
49822CE524B4C3F100E8DE54 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49822CE424B4C3F100E8DE54 /* ContentView.swift */; };
7071
49822CE724B4C3F200E8DE54 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49822CE624B4C3F200E8DE54 /* Assets.xcassets */; };
@@ -209,6 +210,7 @@
209210
4974BC0F2B174A5700A3F38A /* ExecutionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExecutionService.swift; sourceTree = "<group>"; };
210211
4976EB882653151A006EE097 /* ChangePassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePassword.swift; sourceTree = "<group>"; };
211212
4976EB8A265317C6006EE097 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
213+
497B01012DDDF23900F50BC7 /* InstallTaskQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallTaskQueue.swift; sourceTree = "<group>"; };
212214
49822CDF24B4C3F100E8DE54 /* Support.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Support.app; sourceTree = BUILT_PRODUCTS_DIR; };
213215
49822CE224B4C3F100E8DE54 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
214216
49822CE424B4C3F100E8DE54 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -345,6 +347,7 @@
345347
isa = PBXGroup;
346348
children = (
347349
493DCCD12B08E5E500FA2480 /* AppCatalogController.swift */,
350+
497B01012DDDF23900F50BC7 /* InstallTaskQueue.swift */,
348351
);
349352
path = Controllers;
350353
sourceTree = "<group>";
@@ -669,6 +672,7 @@
669672
4972476828DBB1AB007194F0 /* StatusItemBadgeView.swift in Sources */,
670673
496FE4D12651485E007746ED /* UserInfo.swift in Sources */,
671674
4915290D259CCF7A00056B5F /* NotificationBadgeView.swift in Sources */,
675+
497B01022DDDF23900F50BC7 /* InstallTaskQueue.swift in Sources */,
672676
49857E9A24D4B58B009B6FBA /* ComputerInfo.swift in Sources */,
673677
496C2FE1271477B800D51EE1 /* NotificationNames.swift in Sources */,
674678
0A4A738E26020A6500927DAB /* MacOSVersionSubview.swift in Sources */,
@@ -754,7 +758,7 @@
754758
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
755759
CODE_SIGN_STYLE = Manual;
756760
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
757-
CURRENT_PROJECT_VERSION = 65;
761+
CURRENT_PROJECT_VERSION = 66;
758762
DEAD_CODE_STRIPPING = YES;
759763
DEVELOPMENT_TEAM = "";
760764
"DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK;
@@ -793,7 +797,7 @@
793797
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
794798
CODE_SIGN_STYLE = Manual;
795799
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
796-
CURRENT_PROJECT_VERSION = 65;
800+
CURRENT_PROJECT_VERSION = 66;
797801
DEAD_CODE_STRIPPING = YES;
798802
DEVELOPMENT_TEAM = "";
799803
"DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK;
@@ -830,7 +834,7 @@
830834
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
831835
CODE_SIGN_STYLE = Manual;
832836
COMBINE_HIDPI_IMAGES = YES;
833-
CURRENT_PROJECT_VERSION = 65;
837+
CURRENT_PROJECT_VERSION = 66;
834838
DEVELOPMENT_TEAM = "";
835839
"DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK;
836840
ENABLE_HARDENED_RUNTIME = YES;
@@ -863,7 +867,7 @@
863867
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
864868
CODE_SIGN_STYLE = Manual;
865869
COMBINE_HIDPI_IMAGES = YES;
866-
CURRENT_PROJECT_VERSION = 65;
870+
CURRENT_PROJECT_VERSION = 66;
867871
DEVELOPMENT_TEAM = "";
868872
"DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK;
869873
ENABLE_HARDENED_RUNTIME = YES;
@@ -1014,7 +1018,7 @@
10141018
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
10151019
CODE_SIGN_STYLE = Manual;
10161020
COMBINE_HIDPI_IMAGES = YES;
1017-
CURRENT_PROJECT_VERSION = 65;
1021+
CURRENT_PROJECT_VERSION = 66;
10181022
DEVELOPMENT_ASSET_PATHS = "\"Support/Preview Content\"";
10191023
DEVELOPMENT_TEAM = "";
10201024
"DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK;
@@ -1048,7 +1052,7 @@
10481052
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application";
10491053
CODE_SIGN_STYLE = Manual;
10501054
COMBINE_HIDPI_IMAGES = YES;
1051-
CURRENT_PROJECT_VERSION = 65;
1055+
CURRENT_PROJECT_VERSION = 66;
10521056
DEVELOPMENT_ASSET_PATHS = "\"Support/Preview Content\"";
10531057
DEVELOPMENT_TEAM = "";
10541058
"DEVELOPMENT_TEAM[sdk=macosx*]" = 98LJ4XBGYK;

src/Support/Controllers/AppCatalogController.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class AppCatalogController: ObservableObject {
3232
// Current apps updating
3333
@Published var appsUpdating: [String] = []
3434

35+
// Current apps in the queue
36+
@Published var appsQueued: [String] = []
37+
3538
// Show app updates
3639
@Published var showAppUpdates: Bool = false
3740

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// InstallTaskQueue.swift
3+
// Support
4+
//
5+
// Created by Jordy Witteman on 21/05/2025.
6+
//
7+
8+
import Foundation
9+
10+
class InstallTaskQueue {
11+
12+
// Create singleton
13+
static let shared = InstallTaskQueue()
14+
15+
// Create queue
16+
let queue = Queue()
17+
18+
actor Queue {
19+
private var tasks: [(id: String, task: () async -> Void)] = []
20+
private var cancelledTaskIDs: Set<String> = []
21+
private var isRunning = false
22+
23+
func enqueue(id: String, _ task: @escaping () async -> Void) {
24+
tasks.append((id, task))
25+
26+
// Check if current task is running before running the next task
27+
if !isRunning {
28+
isRunning = true
29+
Task {
30+
await runNext()
31+
}
32+
}
33+
}
34+
35+
func cancel(_ id: String) {
36+
cancelledTaskIDs.insert(id)
37+
}
38+
39+
// Run tasks
40+
private func runNext() async {
41+
while !tasks.isEmpty {
42+
let (id, task) = tasks.removeFirst()
43+
if !cancelledTaskIDs.contains(id) {
44+
await task()
45+
}
46+
cancelledTaskIDs.remove(id)
47+
}
48+
isRunning = false
49+
}
50+
}
51+
52+
// Function to add new tasks
53+
func submit(id: String, task: @escaping () async -> Void) async {
54+
await queue.enqueue(id: id, task)
55+
}
56+
57+
// Function to cancel task based on ID
58+
func cancel(taskID: String) async {
59+
await queue.cancel(taskID)
60+
}
61+
}

src/Support/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
<key>CFBundlePackageType</key>
1818
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
1919
<key>CFBundleShortVersionString</key>
20-
<string>2.6.2</string>
20+
<string>2.6.3</string>
2121
<key>CFBundleVersion</key>
22-
<string>65</string>
22+
<string>66</string>
2323
<key>LSApplicationCategoryType</key>
2424
<string>public.app-category.utilities</string>
2525
<key>LSMinimumSystemVersion</key>

src/Support/Views/AppCatalog/AppUpdatesView.swift

Lines changed: 81 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ struct AppUpdatesView: View {
4141

4242
// Get preferences or default values
4343
@StateObject var preferences = Preferences()
44+
45+
// Update cancel hover state
46+
@State private var hoveredCancelButton: Bool = false
47+
@State private var hoveredItem: String?
4448

4549
var body: some View {
4650

@@ -71,13 +75,22 @@ struct AppUpdatesView: View {
7175

7276
if appCatalogController.updateDetails.count > 0 {
7377
Button(action: {
74-
for app in appCatalogController.updateDetails {
75-
Task {
78+
Task {
79+
for app in appCatalogController.updateDetails {
7680
// Validate Catalog Agent code requirement
7781
guard verifyAppCatalogCodeRequirement() else {
7882
return
7983
}
80-
await updateApp(bundleID: app.id)
84+
85+
// Append app to queue
86+
await MainActor.run() {
87+
appCatalogController.appsQueued.append(app.id)
88+
}
89+
90+
// Update app
91+
await InstallTaskQueue.shared.submit(id: app.id) {
92+
await updateApp(bundleID: app.id)
93+
}
8194
}
8295
}
8396
}) {
@@ -202,14 +215,40 @@ struct AppUpdatesView: View {
202215
guard verifyAppCatalogCodeRequirement() else {
203216
return
204217
}
205-
await updateApp(bundleID: update.id)
218+
219+
// Append app to queue
220+
await MainActor.run() {
221+
appCatalogController.appsQueued.append(update.id)
222+
}
223+
224+
// Update app
225+
await InstallTaskQueue.shared.submit(id: update.id) {
226+
await updateApp(bundleID: update.id)
227+
}
206228
}
207229
}) {
208230
if appCatalogController.appsUpdating.contains(update.id) {
209231
ProgressView()
210232
.scaleEffect(0.6)
211233
.frame(width: 26, height: 26)
212234
.padding(.leading, 10)
235+
} else if appCatalogController.appsQueued.contains(update.id) {
236+
Image(systemName: hoveredCancelButton && (hoveredItem == update.id) ? "xmark.circle.fill" : "clock")
237+
.font(.system(size: 16))
238+
.frame(width: 26, height: 26)
239+
.onHover { hover in
240+
hoveredCancelButton = hover
241+
}
242+
.animation(.easeOut(duration: 0.2), value: hoveredCancelButton && (hoveredItem == update.id))
243+
.onTapGesture {
244+
Task {
245+
await InstallTaskQueue.shared.cancel(taskID: update.id)
246+
await MainActor.run {
247+
appCatalogController.appsQueued.removeAll(where: { $0 == update.id })
248+
}
249+
appCatalogController.logger.debug("App \(update.id, privacy: .public) update cancelled")
250+
}
251+
}
213252
} else {
214253
Image(systemName: "icloud.and.arrow.down")
215254
.font(.system(size: 16, weight: .medium))
@@ -220,7 +259,9 @@ struct AppUpdatesView: View {
220259
.buttonStyle(.plain)
221260

222261
}
223-
262+
.onHover {_ in
263+
hoveredItem = update.id
264+
}
224265
}
225266

226267
// Show update schedule information when configured
@@ -328,58 +369,57 @@ struct AppUpdatesView: View {
328369
// MARK: - Function to update app using App Catalog
329370
func updateApp(bundleID: String) async {
330371

372+
appCatalogController.logger.debug("App \(bundleID, privacy: .public) added to update queue")
373+
331374
// Command to update app
332375
let command = "'/usr/local/bin/catalog --install \(bundleID) --update-action --support-app'"
333376

377+
// Remove Bundle ID from queued array
378+
await MainActor.run {
379+
appCatalogController.appsQueued.removeAll(where: { $0 == bundleID })
380+
}
381+
334382
// Add bundle ID to apps currently updating
335383
appCatalogController.appsUpdating.append(bundleID)
336384

337385
do {
338-
try ExecutionService.executeScript(command: command) { exitCode in
339-
340-
if exitCode == 0 {
341-
appCatalogController.logger.log("App \(bundleID, privacy: .public) successfully updated")
342-
343-
// 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
344-
if appCatalogController.updateDetails.contains(where: { $0.id == bundleID } ) {
345-
if let index = appCatalogController.updateDetails.firstIndex(where: { $0.id == bundleID } ) {
346-
DispatchQueue.main.async {
347-
appCatalogController.updateDetails.remove(at: index)
348-
}
349-
}
350-
}
351-
352-
} else {
353-
appCatalogController.logger.error("Failed to update app \(bundleID, privacy: .public)")
386+
387+
let exitCode: NSNumber = try await withCheckedThrowingContinuation { continuation in
388+
try? ExecutionService.executeScript(command: command) { exitCode in
389+
continuation.resume(returning: exitCode)
354390
}
391+
}
392+
393+
if exitCode == 0 {
394+
appCatalogController.logger.log("App \(bundleID, privacy: .public) successfully updated")
355395

356-
// Stop update spinner
357-
if appCatalogController.appsUpdating.contains(bundleID) {
358-
if let index = appCatalogController.appsUpdating.firstIndex(of: bundleID) {
359-
DispatchQueue.main.async {
360-
appCatalogController.appsUpdating.remove(at: index)
361-
362-
// Check for updates again when apps currently updating is empty
363-
if appCatalogController.appsUpdating.isEmpty {
364-
// Trigger check for app updates
365-
appCatalogController.ignoreUpdateChange = true
366-
appCatalogController.getAppUpdates()
367-
}
368-
}
369-
}
396+
// 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
397+
await MainActor.run {
398+
appCatalogController.updateDetails.removeAll(where: { $0.id == bundleID })
370399
}
371400

401+
} else {
402+
appCatalogController.logger.error("Failed to update app \(bundleID, privacy: .public)")
372403
}
404+
405+
// Stop update spinner
406+
await MainActor.run {
407+
appCatalogController.appsUpdating.removeAll(where: { $0 == bundleID })
408+
409+
// Check for updates again when apps currently updating is empty
410+
if appCatalogController.appsUpdating.isEmpty {
411+
// Trigger check for app updates
412+
appCatalogController.ignoreUpdateChange = true
413+
appCatalogController.getAppUpdates()
414+
}
415+
}
416+
373417
} catch {
374418
appCatalogController.logger.log("Failed to update app \(bundleID, privacy: .public)")
375419

376420
// Stop update spinner
377-
if appCatalogController.appsUpdating.contains(bundleID) {
378-
if let index = appCatalogController.appsUpdating.firstIndex(of: bundleID) {
379-
DispatchQueue.main.async {
380-
appCatalogController.appsUpdating.remove(at: index)
381-
}
382-
}
421+
await MainActor.run {
422+
appCatalogController.appsUpdating.removeAll(where: { $0 == bundleID })
383423
}
384424

385425
// Trigger check for app updates

src/SupportHelper/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<plist version="1.0">
44
<dict>
55
<key>CFBundleShortVersionString</key>
6-
<string>2.6.2</string>
6+
<string>2.6.3</string>
77
<key>NSHumanReadableCopyright</key>
88
<string>© 2025 Root3 B.V. All rights reserved.</string>
99
<key>CFBundleIdentifier</key>
@@ -13,7 +13,7 @@
1313
<key>CFBundleName</key>
1414
<string>SupportAppPriviligedHelper</string>
1515
<key>CFBundleVersion</key>
16-
<string>65</string>
16+
<string>66</string>
1717
<key>SMAuthorizedClients</key>
1818
<array>
1919
<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>

src/SupportXPC/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<plist version="1.0">
44
<dict>
55
<key>CFBundleShortVersionString</key>
6-
<string>2.6.2</string>
6+
<string>2.6.3</string>
77
<key>CFBundleIdentifier</key>
88
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
99
<key>CFBundleName</key>
@@ -15,7 +15,7 @@
1515
<key>CFBundleExecutable</key>
1616
<string>$(EXECUTABLE_NAME)</string>
1717
<key>CFBundleVersion</key>
18-
<string>65</string>
18+
<string>66</string>
1919
<key>CFBundlePackageType</key>
2020
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
2121
<key>XPCService</key>

0 commit comments

Comments
 (0)