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
This commit is contained in:
X1a0He
2025-02-06 16:29:59 +08:00
parent 7b6bed4fa5
commit d3835005fb
8 changed files with 206 additions and 180 deletions

View File

@@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES"> shouldAutocreateTestPlan = "YES">
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"

View File

@@ -4,22 +4,6 @@
type = "1" type = "1"
version = "2.0"> version = "2.0">
<Breakpoints> <Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "18E41264-2FE9-43DB-B1ED-0F25585C8B89"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Adobe Downloader/Utils/DownloadUtils.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "442"
endingLineNumber = "442"
landmarkName = "startDownloadProcess(task:)"
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy <BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint"> BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent <BreakpointContent
@@ -52,5 +36,19 @@
landmarkType = "7"> landmarkType = "7">
</BreakpointContent> </BreakpointContent>
</BreakpointProxy> </BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "FF32C0C6-C193-4B43-831E-E6F00D1EE00D"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "Adobe Downloader/Commons/Extensions.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "22"
endingLineNumber = "22">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints> </Breakpoints>
</Bucket> </Bucket>

View File

@@ -11,7 +11,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
if event.modifierFlags.contains(.command) && event.characters?.lowercased() == "q" { if event.modifierFlags.contains(.command) && event.characters?.lowercased() == "q" {
if let mainWindow = NSApp.mainWindow, if let mainWindow = NSApp.mainWindow,
mainWindow.sheets.isEmpty && !mainWindow.isSheet { mainWindow.sheets.isEmpty && !mainWindow.isSheet {
self?.handleQuitCommand() _ = self?.applicationShouldTerminate(NSApp)
return nil return nil
} }
} }
@@ -19,19 +19,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
@MainActor private func handleQuitCommand() { func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
guard let manager = networkManager else { guard let manager = networkManager else { return .terminateNow }
NSApplication.shared.terminate(nil)
return
}
let hasActiveDownloads = manager.downloadTasks.contains { task in let hasActiveDownloads = manager.downloadTasks.contains { task in
if case .downloading = task.totalStatus { if case .downloading = task.totalStatus { return true }
return true
}
return false return false
} }
if hasActiveDownloads { if hasActiveDownloads {
Task { Task {
for task in manager.downloadTasks { for task in manager.downloadTasks {
@@ -40,6 +35,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
taskId: task.id, taskId: task.id,
reason: .other(String(localized: "程序即将退出")) reason: .other(String(localized: "程序即将退出"))
) )
await manager.saveTask(task)
} }
} }
@@ -68,6 +64,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} else { } else {
NSApplication.shared.terminate(nil) NSApplication.shared.terminate(nil)
} }
return .terminateCancel
} }
deinit { deinit {

View File

@@ -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)
}
}
}
}

View File

@@ -99,22 +99,21 @@ class NetworkManager: ObservableObject {
downloadTasks.append(task) downloadTasks.append(task)
updateDockBadge() updateDockBadge()
saveTask(task) await saveTask(task)
do { do {
try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform, saps: saps) try await downloadUtils.handleDownload(task: task, productInfo: productInfo, allowedPlatform: StorageData.shared.allowedPlatform, saps: saps)
} catch { } catch {
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: error.localizedDescription,
error: error,
timestamp: Date(),
recoverable: true
)))
await saveTask(task)
await MainActor.run { await MainActor.run {
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: error.localizedDescription,
error: error,
timestamp: Date(),
recoverable: true
)))
saveTask(task)
objectWillChange.send() objectWillChange.send()
} }
throw error
} }
} }
@@ -136,7 +135,7 @@ class NetworkManager: ObservableObject {
timestamp: Date(), timestamp: Date(),
recoverable: false recoverable: false
))) )))
saveTask(task) await saveTask(task)
} }
if removeFiles { if removeFiles {
@@ -352,19 +351,59 @@ class NetworkManager: ObservableObject {
} }
} }
func saveTask(_ task: NewDownloadTask) { func loadSavedTasks() {
TaskPersistenceManager.shared.saveTask(task) 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() objectWillChange.send()
} }
func loadSavedTasks() { func configureNetworkMonitor() {
let savedTasks = TaskPersistenceManager.shared.loadTasks() monitor.pathUpdateHandler = { [weak self] path in
for task in savedTasks { let task = { @MainActor @Sendable [weak self] in
for product in task.productsToDownload { guard let self else { return }
product.updateCompletedPackages() 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()
} }
} }

View File

@@ -113,7 +113,9 @@ class DownloadUtils {
guard timeDiff >= NetworkConstants.progressUpdateInterval else { return } guard timeDiff >= NetworkConstants.progressUpdateInterval else { return }
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) Task {
progressHandler?(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
}
lastUpdateTime = now lastUpdateTime = now
lastBytes = totalBytesWritten lastBytes = totalBytesWritten
@@ -133,29 +135,31 @@ class DownloadUtils {
} }
} }
await MainActor.run { if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) { task.setStatus(.paused(DownloadStatus.PauseInfo(
task.setStatus(.paused(DownloadStatus.PauseInfo( reason: reason,
reason: reason, timestamp: Date(),
timestamp: Date(), resumable: true
resumable: true )))
))) await networkManager?.saveTask(task)
networkManager?.saveTask(task) await MainActor.run {
networkManager?.objectWillChange.send()
} }
} }
} }
func resumeDownloadTask(taskId: UUID) async { func resumeDownloadTask(taskId: UUID) async {
if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) { 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 { await MainActor.run {
task.setStatus(.downloading(DownloadStatus.DownloadInfo( networkManager?.objectWillChange.send()
fileName: task.currentPackage?.fullPackageName ?? "",
currentPackageIndex: 0,
totalPackages: task.productsToDownload.reduce(0) { $0 + $1.packages.count },
startTime: Date(),
estimatedTimeRemaining: nil
)))
networkManager?.saveTask(task)
} }
if task.sapCode == "APRO" { if task.sapCode == "APRO" {
@@ -178,19 +182,20 @@ class DownloadUtils {
func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async { func cancelDownloadTask(taskId: UUID, removeFiles: Bool = false) async {
await cancelTracker.cancel(taskId) await cancelTracker.cancel(taskId)
await MainActor.run { if let task = await networkManager?.downloadTasks.first(where: { $0.id == taskId }) {
if let task = networkManager?.downloadTasks.first(where: { $0.id == taskId }) { if removeFiles {
if removeFiles { try? FileManager.default.removeItem(at: task.directory)
try? FileManager.default.removeItem(at: task.directory) }
}
task.setStatus(.failed(DownloadStatus.FailureInfo(
message: String(localized: "下载已取消"),
error: NetworkError.downloadCancelled,
timestamp: Date(),
recoverable: false
)))
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?.updateDockBadge()
networkManager?.objectWillChange.send() networkManager?.objectWillChange.send()
} }
@@ -259,47 +264,50 @@ class DownloadUtils {
return return
} }
Task { @MainActor in Task {
package.downloadedSize = package.downloadSize await MainActor.run {
package.progress = 1.0 package.downloadedSize = package.downloadSize
package.status = .completed package.progress = 1.0
package.downloaded = true package.status = .completed
package.downloaded = true
var totalDownloaded: Int64 = 0 var totalDownloaded: Int64 = 0
var totalSize: Int64 = 0 var totalSize: Int64 = 0
for prod in task.productsToDownload { for prod in task.productsToDownload {
for pkg in prod.packages { for pkg in prod.packages {
totalSize += pkg.downloadSize totalSize += pkg.downloadSize
if pkg.downloaded { if pkg.downloaded {
totalDownloaded += pkg.downloadSize 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()
} }
await networkManager?.saveTask(task)
task.totalSize = totalSize await MainActor.run {
task.totalDownloadedSize = totalDownloaded networkManager?.objectWillChange.send()
task.totalProgress = Double(totalDownloaded) / Double(totalSize)
task.totalSpeed = 0
let allCompleted = task.productsToDownload.allSatisfy {
product in product.packages.allSatisfy { $0.downloaded }
} }
continuation.resume()
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()
}, },
progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in progressHandler: { [weak networkManager] bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
Task { @MainActor in Task { @MainActor in
@@ -444,8 +452,8 @@ class DownloadUtils {
startTime: Date(), startTime: Date(),
estimatedTimeRemaining: nil estimatedTimeRemaining: nil
))) )))
networkManager?.saveTask(task)
} }
await networkManager?.saveTask(task)
await progress.increment() await progress.increment()
@@ -487,8 +495,8 @@ class DownloadUtils {
totalTime: Date().timeIntervalSince(task.createAt), totalTime: Date().timeIntervalSince(task.createAt),
totalSize: task.totalSize totalSize: task.totalSize
))) )))
networkManager?.saveTask(task)
} }
await networkManager?.saveTask(task)
} }
} }
@@ -925,6 +933,18 @@ class DownloadUtils {
let (errorMessage, isRecoverable) = classifyError(error) 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 { if isRecoverable && task.retryCount < NetworkConstants.maxRetryAttempts {
task.retryCount += 1 task.retryCount += 1
let nextRetryDate = Date().addingTimeInterval(TimeInterval(NetworkConstants.retryDelay / 1_000_000_000)) let nextRetryDate = Date().addingTimeInterval(TimeInterval(NetworkConstants.retryDelay / 1_000_000_000))
@@ -946,22 +966,22 @@ class DownloadUtils {
} }
} }
} else { } 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 { 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?.updateDockBadge()
networkManager?.objectWillChange.send() networkManager?.objectWillChange.send()
} }
@@ -973,7 +993,7 @@ class DownloadUtils {
case let networkError as NetworkError: case let networkError as NetworkError:
switch networkError { switch networkError {
case .noConnection: case .noConnection:
return (String(localized: "网络连接已断开"), true) return (String(localized: "网络连接已断开"), true)
case .timeout: case .timeout:
return (String(localized: "下载超时"), true) return (String(localized: "下载超时"), true)
case .serverUnreachable: case .serverUnreachable:
@@ -987,12 +1007,14 @@ class DownloadUtils {
} }
case let urlError as URLError: case let urlError as URLError:
switch urlError.code { switch urlError.code {
case .notConnectedToInternet: case .notConnectedToInternet, .networkConnectionLost, .dataNotAllowed:
return (String(localized: "网络连接已断开"), true) return (String(localized: "网络连接已断开"), true)
case .timedOut: case .timedOut:
return (String(localized: "连接超时"), true) return (String(localized: "连接超时"), true)
case .cancelled: case .cancelled:
return (String(localized: "下载已取消"), false) return (String(localized: "下载已取消"), false)
case .cannotConnectToHost, .dnsLookupFailed:
return (String(localized: "无法连接到服务器"), true)
default: default:
return (urlError.localizedDescription, true) return (urlError.localizedDescription, true)
} }

View File

@@ -23,7 +23,7 @@ class TaskPersistenceManager {
: "Adobe Downloader \(sapCode)_\(version)-\(language)-\(platform)-task.json" : "Adobe Downloader \(sapCode)_\(version)-\(language)-\(platform)-task.json"
} }
func saveTask(_ task: NewDownloadTask) { func saveTask(_ task: NewDownloadTask) async {
let fileName = getTaskFileName( let fileName = getTaskFileName(
sapCode: task.sapCode, sapCode: task.sapCode,
version: task.version, version: task.version,
@@ -34,12 +34,10 @@ class TaskPersistenceManager {
var resumeDataDict: [String: Data]? = nil var resumeDataDict: [String: Data]? = nil
Task { if let currentPackage = task.currentPackage,
if let currentPackage = task.currentPackage, let cancelTracker = self.cancelTracker,
let cancelTracker = self.cancelTracker, let resumeData = await cancelTracker.getResumeData(task.id) {
let resumeData = await cancelTracker.getResumeData(task.id) { resumeDataDict = [currentPackage.id.uuidString: resumeData]
resumeDataDict = [currentPackage.id.uuidString: resumeData]
}
} }
let taskData = TaskData( let taskData = TaskData(
@@ -151,13 +149,13 @@ class TaskPersistenceManager {
let initialStatus: DownloadStatus let initialStatus: DownloadStatus
switch taskData.totalStatus { switch taskData.totalStatus {
case .completed: case .completed(let info):
initialStatus = taskData.totalStatus initialStatus = .completed(info)
case .failed: case .failed(let info):
initialStatus = taskData.totalStatus initialStatus = .failed(info)
case .downloading: case .downloading:
initialStatus = .paused(DownloadStatus.PauseInfo( initialStatus = .paused(DownloadStatus.PauseInfo(
reason: .other(String(localized: "程序意外退出")), reason: .other(String(localized: "程序退出")),
timestamp: Date(), timestamp: Date(),
resumable: true resumable: true
)) ))

View File

@@ -11,6 +11,7 @@
}, },
"(可能导致处理 Setup 组件失败)" : { "(可能导致处理 Setup 组件失败)" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -21,6 +22,7 @@
} }
}, },
"(将导致无法使用安装功能)" : { "(将导致无法使用安装功能)" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -286,7 +288,6 @@
}, },
"Debug 模式" : { "Debug 模式" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1384,7 +1385,6 @@
} }
}, },
"将执行的命令:" : { "将执行的命令:" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1425,7 +1425,6 @@
} }
}, },
"展开全部" : { "展开全部" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1446,6 +1445,7 @@
} }
}, },
"已处理" : { "已处理" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1456,6 +1456,7 @@
} }
}, },
"已备份" : { "已备份" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1661,7 +1662,6 @@
} }
}, },
"折叠全部" : { "折叠全部" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1889,6 +1889,9 @@
} }
} }
} }
},
"无法连接到服务器" : {
}, },
"无法连接到服务器: %@" : { "无法连接到服务器: %@" : {
"comment" : "Server unreachable", "comment" : "Server unreachable",
@@ -2583,6 +2586,7 @@
} }
}, },
"程序意外退出" : { "程序意外退出" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -2601,6 +2605,9 @@
} }
} }
} }
},
"程序退出" : {
}, },
"程序重启后自动暂停" : { "程序重启后自动暂停" : {
"localizations" : { "localizations" : {