fix: harden release builds and queue scheduling

This commit is contained in:
nimbold
2026-06-03 04:15:53 +03:30
parent 8c2dee2c13
commit 0c5e031f49
5 changed files with 132 additions and 28 deletions
+17 -2
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Resolve version
id: version
@@ -40,6 +40,20 @@ jobs:
run: |
uname -a
swift --version
xcodebuild -version
xcode-select -p
xcrun --sdk macosx --show-sdk-version
- name: Verify macOS 26 SDK
shell: bash
run: |
SDK_VERSION="$(xcrun --sdk macosx --show-sdk-version)"
SDK_MAJOR="${SDK_VERSION%%.*}"
if [[ "$SDK_MAJOR" != "26" ]]; then
echo "Expected macOS 26 SDK, got macOS $SDK_VERSION" >&2
exit 1
fi
- name: Install dependencies
run: brew install aria2 dylibbundler
@@ -54,6 +68,7 @@ jobs:
run: |
file build/Firelink.app/Contents/MacOS/Firelink
lipo -archs build/Firelink.app/Contents/MacOS/Firelink | grep -qx arm64
codesign --verify --deep --strict build/Firelink.app
- name: Create DMG
env:
@@ -62,7 +77,7 @@ jobs:
run: Scripts/create_dmg.sh
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: Firelink-${{ steps.version.outputs.version }}-mac-arm64-dmg
path: dist/*.dmg
+4
View File
@@ -71,4 +71,8 @@ cat > "$CONTENTS_DIR/Info.plist" <<PLIST
</plist>
PLIST
if command -v codesign &> /dev/null; then
codesign --force --deep --sign - "$APP_DIR"
fi
echo "Created $APP_DIR"
+5 -5
View File
@@ -31,6 +31,11 @@ final class Aria2DownloadEngine {
}
static func findExecutable() -> URL? {
if let bundled = Bundle.main.url(forResource: "aria2c", withExtension: nil),
FileManager.default.isExecutableFile(atPath: bundled.path) {
return bundled
}
let candidates = [
"/opt/homebrew/bin/aria2c",
"/usr/local/bin/aria2c",
@@ -49,11 +54,6 @@ final class Aria2DownloadEngine {
}
}
if let bundled = Bundle.main.url(forResource: "aria2c", withExtension: nil),
FileManager.default.isExecutableFile(atPath: bundled.path) {
return bundled
}
return nil
}
+95 -6
View File
@@ -15,6 +15,7 @@ final class DownloadController: ObservableObject {
private var activeHandles: [UUID: Aria2DownloadEngine.Handle] = [:]
private var automaticRetryCounts: [UUID: Int] = [:]
private var restrictQueueToAutoResume = false
private var queuePumpScope: QueuePumpScope = .idle
private var sleepActivity: SleepActivityHandle?
private var cancellables = Set<AnyCancellable>()
private let maxAutomaticRetries = 3
@@ -54,6 +55,7 @@ final class DownloadController: ObservableObject {
Task { @MainActor in
self.engineMessage = "Recovered downloads from the previous session."
self.restrictQueueToAutoResume = true
self.queuePumpScope = .all
self.pumpQueue()
}
}
@@ -153,6 +155,20 @@ final class DownloadController: ObservableObject {
func startQueue(queueID: UUID? = nil) {
engineMessage = ""
restrictQueueToAutoResume = false
if let queueID {
let queueID = normalizedQueueID(queueID)
switch queuePumpScope {
case .all:
break
case .idle:
queuePumpScope = .scoped(queueIDs: [queueID], itemIDs: [])
case .scoped(var queueIDs, let itemIDs):
queueIDs.insert(queueID)
queuePumpScope = .scoped(queueIDs: queueIDs, itemIDs: itemIDs)
}
} else {
queuePumpScope = .all
}
markQueuedDownloadsForAutoResume(queueID: queueID)
pumpQueue()
}
@@ -221,6 +237,7 @@ final class DownloadController: ObservableObject {
$0.message = ""
$0.autoResumeOnLaunch = true
}
queuePumpScope = queuePumpScope.includingItem(item.id)
automaticRetryCounts[item.id] = nil
saveDownloads()
pumpQueue()
@@ -330,7 +347,8 @@ final class DownloadController: ObservableObject {
}
let item = downloads.remove(at: source)
downloads.insert(item, at: target)
let insertionIndex = source < target ? target - 1 : target
downloads.insert(item, at: insertionIndex)
saveDownloads()
}
@@ -340,9 +358,13 @@ final class DownloadController: ObservableObject {
return
}
pruneActiveQueueScopes()
while activeCount < settings.maxConcurrentDownloads,
let next = downloads.first(where: { item in
item.status == .queued && (!restrictQueueToAutoResume || item.autoResumeOnLaunch == true)
item.status == .queued &&
(!restrictQueueToAutoResume || item.autoResumeOnLaunch == true) &&
isAllowedToStart(item)
}) {
start(next)
}
@@ -352,6 +374,8 @@ final class DownloadController: ObservableObject {
!downloads.contains(where: { $0.status == .queued && $0.autoResumeOnLaunch == true }) {
restrictQueueToAutoResume = false
}
pruneActiveQueueScopes()
}
private func start(_ item: DownloadItem) {
@@ -480,10 +504,16 @@ final class DownloadController: ObservableObject {
}
private func markQueuedDownloadsForAutoResume(queueID: UUID?) {
let normalizedID = queueID.map(normalizedQueueID)
for index in downloads.indices where downloads[index].status == .queued &&
(normalizedID == nil || normalizedQueueID(downloads[index].queueID) == normalizedID) {
downloads[index].autoResumeOnLaunch = true
if let queueID {
let normalizedID = normalizedQueueID(queueID)
for index in downloads.indices where downloads[index].status == .queued &&
validQueueID(downloads[index].queueID) == normalizedID {
downloads[index].autoResumeOnLaunch = true
}
} else {
for index in downloads.indices where downloads[index].status == .queued {
downloads[index].autoResumeOnLaunch = true
}
}
saveDownloads()
}
@@ -530,6 +560,65 @@ final class DownloadController: ObservableObject {
}
}
private func isAllowedToStart(_ item: DownloadItem) -> Bool {
switch queuePumpScope {
case .idle:
return false
case .all:
return true
case .scoped(let queueIDs, let itemIDs):
if itemIDs.contains(item.id) {
return true
}
guard let queueID = validQueueID(item.queueID) else { return false }
return queueIDs.contains(queueID)
}
}
private func pruneActiveQueueScopes() {
switch queuePumpScope {
case .idle:
return
case .all:
if !downloads.contains(where: { $0.status == .queued || $0.status == .downloading }) {
queuePumpScope = .idle
}
case .scoped(let queueIDs, let itemIDs):
let activeQueueIDs = queueIDs.filter { queueID in
downloads.contains { item in
validQueueID(item.queueID) == queueID &&
(item.status == .queued || item.status == .downloading)
}
}
let activeItemIDs = itemIDs.filter { itemID in
downloads.contains { item in
item.id == itemID && (item.status == .queued || item.status == .downloading)
}
}
queuePumpScope = activeQueueIDs.isEmpty && activeItemIDs.isEmpty
? .idle
: .scoped(queueIDs: activeQueueIDs, itemIDs: activeItemIDs)
}
}
private enum QueuePumpScope {
case idle
case all
case scoped(queueIDs: Set<UUID>, itemIDs: Set<UUID>)
func includingItem(_ itemID: UUID) -> QueuePumpScope {
switch self {
case .idle:
return .scoped(queueIDs: [], itemIDs: [itemID])
case .all:
return .all
case .scoped(let queueIDs, var itemIDs):
itemIDs.insert(itemID)
return .scoped(queueIDs: queueIDs, itemIDs: itemIDs)
}
}
}
private func updateSleepActivity() {
let shouldPreventSleep = settings.preventsSleepWhileDownloading && activeCount > 0
+11 -15
View File
@@ -131,15 +131,17 @@ final class SchedulerController: ObservableObject {
}
private func triggerQueues() {
guard !settings.targetQueueIDs.isEmpty else { return }
let runnableQueueIDs = settings.targetQueueIDs.filter { queueID in
downloadController.queues.contains(where: { $0.id == queueID }) &&
downloadController.queueItems(for: queueID).contains(where: { $0.status == .queued })
}
guard !runnableQueueIDs.isEmpty else { return }
isRunning = true
for queueID in settings.targetQueueIDs {
// Check if queue still exists
if downloadController.queues.contains(where: { $0.id == queueID }) {
downloadController.startQueue(queueID: queueID)
}
for queueID in runnableQueueIDs {
downloadController.startQueue(queueID: queueID)
}
checkIfRunningFinished()
@@ -148,15 +150,9 @@ final class SchedulerController: ObservableObject {
private func checkIfRunningFinished() {
guard isRunning else { return }
// A queue is finished if there are no items in .queued or .downloading state for that queue.
// Wait, what if the queue is empty? We don't trigger anything.
var hasActiveItems = false
for item in downloadController.downloads {
if let qid = item.queueID, settings.targetQueueIDs.contains(qid) {
if item.status == .queued || item.status == .downloading {
hasActiveItems = true
break
}
let hasActiveItems = settings.targetQueueIDs.contains { queueID in
downloadController.queueItems(for: queueID).contains {
$0.status == .queued || $0.status == .downloading
}
}