From d3835005fbd6e8c6534175ff5b28076a0d45e2b2 Mon Sep 17 00:00:00 2001 From: X1a0He Date: Thu, 6 Feb 2025 16:29:59 +0800 Subject: [PATCH] fix: Fix the issue with download tasks 1. Fixed the issue where after the task download is completed, the program will prompt that it has been paused when re-entering the program --- .../xcschemes/Adobe Downloader.xcscheme | 2 +- .../xcdebugger/Breakpoints_v2.xcbkptlist | 30 ++- Adobe Downloader/AppDelegate.swift | 19 +- Adobe Downloader/Commons/Extensions.swift | 35 ---- Adobe Downloader/NetworkManager.swift | 77 ++++++-- Adobe Downloader/Utils/DownloadUtils.swift | 186 ++++++++++-------- .../Utils/TaskPersistenceManager.swift | 22 +-- Localizables/Localizable.xcstrings | 15 +- 8 files changed, 206 insertions(+), 180 deletions(-) diff --git a/Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme b/Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme index 6ed2fb3..a71041f 100644 --- a/Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme +++ b/Adobe Downloader.xcodeproj/xcshareddata/xcschemes/Adobe Downloader.xcscheme @@ -31,7 +31,7 @@ shouldAutocreateTestPlan = "YES"> - - - - + + + + diff --git a/Adobe Downloader/AppDelegate.swift b/Adobe Downloader/AppDelegate.swift index 2cd831f..aba61a4 100644 --- a/Adobe Downloader/AppDelegate.swift +++ b/Adobe Downloader/AppDelegate.swift @@ -11,7 +11,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { if event.modifierFlags.contains(.command) && event.characters?.lowercased() == "q" { if let mainWindow = NSApp.mainWindow, mainWindow.sheets.isEmpty && !mainWindow.isSheet { - self?.handleQuitCommand() + _ = self?.applicationShouldTerminate(NSApp) return nil } } @@ -19,19 +19,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - @MainActor private func handleQuitCommand() { - guard let manager = networkManager else { - NSApplication.shared.terminate(nil) - return - } - + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + guard let manager = networkManager else { return .terminateNow } + let hasActiveDownloads = manager.downloadTasks.contains { task in - if case .downloading = task.totalStatus { - return true - } + if case .downloading = task.totalStatus { return true } return false } - + if hasActiveDownloads { Task { for task in manager.downloadTasks { @@ -40,6 +35,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { taskId: task.id, reason: .other(String(localized: "程序即将退出")) ) + await manager.saveTask(task) } } @@ -68,6 +64,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } else { NSApplication.shared.terminate(nil) } + return .terminateCancel } deinit { diff --git a/Adobe Downloader/Commons/Extensions.swift b/Adobe Downloader/Commons/Extensions.swift index 4adde40..45245c8 100644 --- a/Adobe Downloader/Commons/Extensions.swift +++ b/Adobe Downloader/Commons/Extensions.swift @@ -19,38 +19,3 @@ extension NewDownloadTask { } } } - -extension NetworkManager { - func configureNetworkMonitor() { - monitor.pathUpdateHandler = { [weak self] path in - Task { @MainActor [weak self] in - guard let self else { return } - let wasConnected = self.isConnected - self.isConnected = path.status == .satisfied - switch (wasConnected, self.isConnected) { - case (false, true): await resumePausedTasks() - case (true, false): await pauseActiveTasks() - default: break - } - } - } - monitor.start(queue: .global(qos: .utility)) - } - - private func resumePausedTasks() async { - for task in downloadTasks { - if case .paused(let info) = task.status, - info.reason == .networkIssue { - await downloadUtils.resumeDownloadTask(taskId: task.id) - } - } - } - - private func pauseActiveTasks() async { - for task in downloadTasks { - if case .downloading = task.status { - await downloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue) - } - } - } -} diff --git a/Adobe Downloader/NetworkManager.swift b/Adobe Downloader/NetworkManager.swift index a85eea2..6cece31 100644 --- a/Adobe Downloader/NetworkManager.swift +++ b/Adobe Downloader/NetworkManager.swift @@ -99,22 +99,21 @@ class NetworkManager: ObservableObject { downloadTasks.append(task) updateDockBadge() - saveTask(task) + await saveTask(task) do { try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform, saps: saps) } catch { + task.setStatus(.failed(DownloadStatus.FailureInfo( + message: error.localizedDescription, + error: error, + timestamp: Date(), + recoverable: true + ))) + await saveTask(task) await MainActor.run { - task.setStatus(.failed(DownloadStatus.FailureInfo( - message: error.localizedDescription, - error: error, - timestamp: Date(), - recoverable: true - ))) - saveTask(task) objectWillChange.send() } - throw error } } @@ -136,7 +135,7 @@ class NetworkManager: ObservableObject { timestamp: Date(), recoverable: false ))) - saveTask(task) + await saveTask(task) } if removeFiles { @@ -352,19 +351,59 @@ class NetworkManager: ObservableObject { } } - func saveTask(_ task: NewDownloadTask) { - TaskPersistenceManager.shared.saveTask(task) + func loadSavedTasks() { + Task { + let savedTasks = TaskPersistenceManager.shared.loadTasks() + await MainActor.run { + for task in savedTasks { + for product in task.productsToDownload { + product.updateCompletedPackages() + } + } + downloadTasks.append(contentsOf: savedTasks) + updateDockBadge() + } + } + } + + func saveTask(_ task: NewDownloadTask) async { + await TaskPersistenceManager.shared.saveTask(task) objectWillChange.send() } - func loadSavedTasks() { - let savedTasks = TaskPersistenceManager.shared.loadTasks() - for task in savedTasks { - for product in task.productsToDownload { - product.updateCompletedPackages() + func configureNetworkMonitor() { + monitor.pathUpdateHandler = { [weak self] path in + let task = { @MainActor @Sendable [weak self] in + guard let self else { return } + let wasConnected = self.isConnected + self.isConnected = path.status == .satisfied + switch (wasConnected, self.isConnected) { + case (false, true): + await self.resumePausedTasks() + case (true, false): + await self.pauseActiveTasks() + default: break + } + } + Task(operation: task) + } + monitor.start(queue: .global(qos: .utility)) + } + + private func resumePausedTasks() async { + for task in downloadTasks { + if case .paused(let info) = task.status, + info.reason == .networkIssue { + await downloadUtils.resumeDownloadTask(taskId: task.id) + } + } + } + + private func pauseActiveTasks() async { + for task in downloadTasks { + if case .downloading = task.status { + await downloadUtils.pauseDownloadTask(taskId: task.id, reason: .networkIssue) } } - downloadTasks.append(contentsOf: savedTasks) - updateDockBadge() } } diff --git a/Adobe Downloader/Utils/DownloadUtils.swift b/Adobe Downloader/Utils/DownloadUtils.swift index e248de0..ba5c8e9 100644 --- a/Adobe Downloader/Utils/DownloadUtils.swift +++ b/Adobe Downloader/Utils/DownloadUtils.swift @@ -113,7 +113,9 @@ class DownloadUtils { guard timeDiff >= NetworkConstants.progressUpdateInterval else { return } - progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) + Task { + progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) + } lastUpdateTime = now lastBytes = totalBytesWritten @@ -133,29 +135,31 @@ class DownloadUtils { } } - await MainActor.run { - if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) { - task.setStatus(.paused(DownloadStatus.PauseInfo( - reason: reason, - timestamp: Date(), - resumable: true - ))) - networkManager?.saveTask(task) + if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { + task.setStatus(.paused(DownloadStatus.PauseInfo( + reason: reason, + timestamp: Date(), + resumable: true + ))) + await networkManager?.saveTask(task) + await MainActor.run { + networkManager?.objectWillChange.send() } } } func resumeDownloadTask(taskId: UUID) async { if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { + task.setStatus(.downloading(DownloadStatus.DownloadInfo( + fileName: task.currentPackage?.fullPackageName ?? "", + currentPackageIndex: 0, + totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count }, + startTime: Date(), + estimatedTimeRemaining: nil + ))) + await networkManager?.saveTask(task) await MainActor.run { - task.setStatus(.downloading(DownloadStatus.DownloadInfo( - fileName: task.currentPackage?.fullPackageName ?? "", - currentPackageIndex: 0, - totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count }, - startTime: Date(), - estimatedTimeRemaining: nil - ))) - networkManager?.saveTask(task) + networkManager?.objectWillChange.send() } if task.sapCode == "APRO" { @@ -178,19 +182,20 @@ class DownloadUtils { func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async { await cancelTracker.cancel(taskId) - await MainActor.run { - if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) { - if removeFiles { - try? FileManager.default.removeItem(at: task.directory) - } - - task.setStatus(.failed(DownloadStatus.FailureInfo( - message: String(localized: "下载已取消"), - error: NetworkError.downloadCancelled, - timestamp: Date(), - recoverable: false - ))) + if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { + if removeFiles { + try? FileManager.default.removeItem(at: task.directory) + } + task.setStatus(.failed(DownloadStatus.FailureInfo( + message: String(localized: "下载已取消"), + error: NetworkError.downloadCancelled, + timestamp: Date(), + recoverable: false + ))) + + await networkManager?.saveTask(task) + await MainActor.run { networkManager?.updateDockBadge() networkManager?.objectWillChange.send() } @@ -259,47 +264,50 @@ class DownloadUtils { return } - Task { @MainActor in - package.downloadedSize = package.downloadSize - package.progress = 1.0 - package.status = .completed - package.downloaded = true + Task { + await MainActor.run { + package.downloadedSize = package.downloadSize + package.progress = 1.0 + package.status = .completed + package.downloaded = true - var totalDownloaded: Int64 = 0 - var totalSize: Int64 = 0 + var totalDownloaded: Int64 = 0 + var totalSize: Int64 = 0 - for prod in task.productsToDownload { - for pkg in prod.packages { - totalSize += pkg.downloadSize - if pkg.downloaded { - totalDownloaded += pkg.downloadSize + for prod in task.productsToDownload { + for pkg in prod.packages { + totalSize += pkg.downloadSize + if pkg.downloaded { + totalDownloaded += pkg.downloadSize + } } } + + task.totalSize = totalSize + task.totalDownloadedSize = totalDownloaded + task.totalProgress = Double(totalDownloaded) / Double(totalSize) + task.totalSpeed = 0 + + let allCompleted = task.productsToDownload.allSatisfy { + product in product.packages.allSatisfy { $0.downloaded } + } + + if allCompleted { + task.setStatus(.completed(DownloadStatus.CompletionInfo( + timestamp: Date(), + totalTime: Date().timeIntervalSince(task.createAt), + totalSize: totalSize + ))) + } + + product.updateCompletedPackages() } - - task.totalSize = totalSize - task.totalDownloadedSize = totalDownloaded - task.totalProgress = Double(totalDownloaded) / Double(totalSize) - task.totalSpeed = 0 - - let allCompleted = task.productsToDownload.allSatisfy { - product in product.packages.allSatisfy { $0.downloaded } + await networkManager?.saveTask(task) + await MainActor.run { + networkManager?.objectWillChange.send() } - - if allCompleted { - task.setStatus(.completed(DownloadStatus.CompletionInfo( - timestamp: Date(), - totalTime: Date().timeIntervalSince(task.createAt), - totalSize: totalSize - ))) - } - - product.updateCompletedPackages() - networkManager?.saveTask(task) - networkManager?.objectWillChange.send() + continuation.resume() } - - continuation.resume() }, progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in Task { @MainActor in @@ -444,8 +452,8 @@ class DownloadUtils { startTime: Date(), estimatedTimeRemaining: nil ))) - networkManager?.saveTask(task) } + await networkManager?.saveTask(task) await progress.increment() @@ -487,8 +495,8 @@ class DownloadUtils { totalTime: Date().timeIntervalSince(task.createAt), totalSize: task.totalSize ))) - networkManager?.saveTask(task) } + await networkManager?.saveTask(task) } } @@ -925,6 +933,18 @@ class DownloadUtils { let (errorMessage, isRecoverable) = classifyError(error) + if isRecoverable, + let downloadTask = await cancelTracker.downloadTasks[taskId] { + let resumeData = await withCheckedContinuation { continuation in + downloadTask.cancel(byProducingResumeData: { data in + continuation.resume(returning: data) + }) + } + if let resumeData = resumeData { + await cancelTracker.storeResumeData(taskId, data: resumeData) + } + } + if isRecoverable && task.retryCount < NetworkConstants.maxRetryAttempts { task.retryCount += 1 let nextRetryDate = Date().addingTimeInterval(TimeInterval(NetworkConstants.retryDelay / 1_000_000_000)) @@ -946,22 +966,22 @@ class DownloadUtils { } } } else { + task.setStatus(.failed(DownloadStatus.FailureInfo( + message: errorMessage, + error: error, + timestamp: Date(), + recoverable: isRecoverable + ))) + + if let currentPackage = task.currentPackage { + let destinationDir = task.directory + .appendingPathComponent("\(task.sapCode)") + let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName) + try? FileManager.default.removeItem(at: fileURL) + } + + await networkManager?.saveTask(task) await MainActor.run { - task.setStatus(.failed(DownloadStatus.FailureInfo( - message: errorMessage, - error: error, - timestamp: Date(), - recoverable: isRecoverable - ))) - - if let currentPackage = task.currentPackage { - let destinationDir = task.directory - .appendingPathComponent("\(task.sapCode)") - let fileURL = destinationDir.appendingPathComponent(currentPackage.fullPackageName) - try? FileManager.default.removeItem(at: fileURL) - } - - networkManager?.saveTask(task) networkManager?.updateDockBadge() networkManager?.objectWillChange.send() } @@ -973,7 +993,7 @@ class DownloadUtils { case let networkError as NetworkError: switch networkError { case .noConnection: - return (String(localized: "网络连接已断开"), true) + return (String(localized: "网络连接已断开"), true) case .timeout: return (String(localized: "下载超时"), true) case .serverUnreachable: @@ -987,12 +1007,14 @@ class DownloadUtils { } case let urlError as URLError: switch urlError.code { - case .notConnectedToInternet: + case .notConnectedToInternet, .networkConnectionLost, .dataNotAllowed: return (String(localized: "网络连接已断开"), true) case .timedOut: return (String(localized: "连接超时"), true) case .cancelled: return (String(localized: "下载已取消"), false) + case .cannotConnectToHost, .dnsLookupFailed: + return (String(localized: "无法连接到服务器"), true) default: return (urlError.localizedDescription, true) } diff --git a/Adobe Downloader/Utils/TaskPersistenceManager.swift b/Adobe Downloader/Utils/TaskPersistenceManager.swift index a3db46f..11c9741 100644 --- a/Adobe Downloader/Utils/TaskPersistenceManager.swift +++ b/Adobe Downloader/Utils/TaskPersistenceManager.swift @@ -23,7 +23,7 @@ class TaskPersistenceManager { : "Adobe Downloader \(sapCode)_\(version)-\(language)-\(platform)-task.json" } - func saveTask(_ task: NewDownloadTask) { + func saveTask(_ task: NewDownloadTask) async { let fileName = getTaskFileName( sapCode: task.sapCode, version: task.version, @@ -34,12 +34,10 @@ class TaskPersistenceManager { var resumeDataDict: [String: Data]? = nil - Task { - if let currentPackage = task.currentPackage, - let cancelTracker = self.cancelTracker, - let resumeData = await cancelTracker.getResumeData(task.id) { - resumeDataDict = [currentPackage.id.uuidString: resumeData] - } + if let currentPackage = task.currentPackage, + let cancelTracker = self.cancelTracker, + let resumeData = await cancelTracker.getResumeData(task.id) { + resumeDataDict = [currentPackage.id.uuidString: resumeData] } let taskData = TaskData( @@ -151,13 +149,13 @@ class TaskPersistenceManager { let initialStatus: DownloadStatus switch taskData.totalStatus { - case .completed: - initialStatus = taskData.totalStatus - case .failed: - initialStatus = taskData.totalStatus + case .completed(let info): + initialStatus = .completed(info) + case .failed(let info): + initialStatus = .failed(info) case .downloading: initialStatus = .paused(DownloadStatus.PauseInfo( - reason: .other(String(localized: "程序意外退出")), + reason: .other(String(localized: "程序退出")), timestamp: Date(), resumable: true )) diff --git a/Localizables/Localizable.xcstrings b/Localizables/Localizable.xcstrings index 2d7101e..121e815 100644 --- a/Localizables/Localizable.xcstrings +++ b/Localizables/Localizable.xcstrings @@ -11,6 +11,7 @@ }, "(可能导致处理 Setup 组件失败)" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -21,6 +22,7 @@ } }, "(将导致无法使用安装功能)" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -286,7 +288,6 @@ }, "Debug 模式" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1384,7 +1385,6 @@ } }, "将执行的命令:" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1425,7 +1425,6 @@ } }, "展开全部" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1446,6 +1445,7 @@ } }, "已处理" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1456,6 +1456,7 @@ } }, "已备份" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1661,7 +1662,6 @@ } }, "折叠全部" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1889,6 +1889,9 @@ } } } + }, + "无法连接到服务器" : { + }, "无法连接到服务器: %@" : { "comment" : "Server unreachable", @@ -2583,6 +2586,7 @@ } }, "程序意外退出" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2601,6 +2605,9 @@ } } } + }, + "程序退出" : { + }, "程序重启后自动暂停" : { "localizations" : {