From fbf1dc72b8383900a63c567651dc843b5486c6d0 Mon Sep 17 00:00:00 2001 From: X1a0He Date: Sat, 19 Jul 2025 23:14:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=83=E4=BA=86=E4=B8=A4=E5=9D=97?= =?UTF-8?q?=E8=A5=BF=E7=93=9C=EF=BC=8C=E8=A7=A3=E5=86=B3=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Adobe Downloader/Models/NewDownloadTask.swift | 2 +- .../Utils/ChunkedDownloadManager.swift | 87 ++++++++++++------- Adobe Downloader/Utils/NewDownloadUtils.swift | 18 ++-- .../Utils/TaskPersistenceManager.swift | 30 +++---- Adobe Downloader/Views/AppCardView.swift | 32 +++---- 5 files changed, 92 insertions(+), 77 deletions(-) diff --git a/Adobe Downloader/Models/NewDownloadTask.swift b/Adobe Downloader/Models/NewDownloadTask.swift index b256790..03fe7a6 100644 --- a/Adobe Downloader/Models/NewDownloadTask.swift +++ b/Adobe Downloader/Models/NewDownloadTask.swift @@ -6,7 +6,7 @@ // import Foundation -class NewDownloadTask: Identifiable, ObservableObject { +class NewDownloadTask: Identifiable, ObservableObject, @unchecked Sendable { let id = UUID() var productId: String let productVersion: String diff --git a/Adobe Downloader/Utils/ChunkedDownloadManager.swift b/Adobe Downloader/Utils/ChunkedDownloadManager.swift index 3e62cf4..29eecb9 100644 --- a/Adobe Downloader/Utils/ChunkedDownloadManager.swift +++ b/Adobe Downloader/Utils/ChunkedDownloadManager.swift @@ -98,7 +98,7 @@ extension ValidationInfo.SegmentInfo: Codable { } } -class ChunkedDownloadManager { +class ChunkedDownloadManager: @unchecked Sendable { static let shared = ChunkedDownloadManager() private var chunkSize: Int64 { @@ -113,7 +113,26 @@ class ChunkedDownloadManager { } private var activeTasks: [String: Task] = [:] - private let taskLock = NSLock() + + private let taskQueue = DispatchQueue(label: "com.x1a0he.macOS.Adobe-Downloader.chunkDownloadTasks", attributes: .concurrent) + + private func setActiveTask(packageIdentifier: String, task: Task) async { + await withCheckedContinuation { continuation in + taskQueue.async(flags: .barrier) { [weak self] in + self?.activeTasks[packageIdentifier] = task + continuation.resume() + } + } + } + + private func removeActiveTask(packageIdentifier: String) async { + await withCheckedContinuation { continuation in + taskQueue.async(flags: .barrier) { [weak self] in + self?.activeTasks.removeValue(forKey: packageIdentifier) + continuation.resume() + } + } + } private init() { let containerURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! @@ -212,9 +231,13 @@ class ChunkedDownloadManager { } private func writeDataToFile(data: Data, destinationURL: URL, offset: Int64, totalSize: Int64? = nil) async throws { - return try await withCheckedThrowingContinuation { continuation in - DispatchQueue.global(qos: .utility).async { + return try await withCheckedThrowingContinuation { [weak self] continuation in + DispatchQueue.global(qos: .utility).async { [weak self] in do { + guard let self = self else { + continuation.resume(throwing: NSError(domain: "ChunkedDownloadManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Manager deallocated"])) + return + } let directory = destinationURL.deletingLastPathComponent() if !self.fileManager.fileExists(atPath: directory.path) { try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true) @@ -265,7 +288,7 @@ class ChunkedDownloadManager { } if fileManager.fileExists(atPath: destinationURL.path) { - if let expectedHash = chunk.expectedHash { + if chunk.expectedHash != nil { if validateCompleteChunkFromFile(destinationURL: destinationURL, chunk: chunk) { modifiedChunk.downloadedSize = chunk.size modifiedChunk.isCompleted = true @@ -304,9 +327,7 @@ class ChunkedDownloadManager { headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } - return try await withTaskCancellationHandler { - modifiedChunk.isPaused = true - } operation: { + return try await withTaskCancellationHandler(operation: { let (data, response) = try await URLSession.shared.data(for: request) try Task.checkCancellation() @@ -329,7 +350,7 @@ class ChunkedDownloadManager { if modifiedChunk.downloadedSize >= chunk.size { modifiedChunk.isCompleted = true - if let expectedHash = chunk.expectedHash { + if chunk.expectedHash != nil { if !validateCompleteChunkFromFile(destinationURL: destinationURL, chunk: chunk) { throw NetworkError.invalidData("分片哈希校验失败: \(chunk.index)") } @@ -343,26 +364,34 @@ class ChunkedDownloadManager { } return modifiedChunk - } + }, onCancel: {}) } func pauseDownload(packageIdentifier: String) { - taskLock.lock() - defer { taskLock.unlock() } - - if let task = activeTasks[packageIdentifier] { - task.cancel() - activeTasks.removeValue(forKey: packageIdentifier) + Task { + await withCheckedContinuation { [weak self] continuation in + self?.taskQueue.async(flags: .barrier) { [weak self] in + if let task = self?.activeTasks[packageIdentifier] { + task.cancel() + self?.activeTasks.removeValue(forKey: packageIdentifier) + } + continuation.resume() + } + } } } func cancelDownload(packageIdentifier: String) { - taskLock.lock() - defer { taskLock.unlock() } - - if let task = activeTasks[packageIdentifier] { - task.cancel() - activeTasks.removeValue(forKey: packageIdentifier) + Task { + await withCheckedContinuation { [weak self] continuation in + self?.taskQueue.async(flags: .barrier) { [weak self] in + if let task = self?.activeTasks[packageIdentifier] { + task.cancel() + self?.activeTasks.removeValue(forKey: packageIdentifier) + } + continuation.resume() + } + } } clearChunkedDownloadState(packageIdentifier: packageIdentifier) @@ -471,7 +500,7 @@ class ChunkedDownloadManager { cancellationHandler: (() async -> Bool)? = nil ) async throws { let downloadTask = Task { - let (supportsRange, totalSize, etag) = try await checkRangeSupport(url: url, headers: headers) + let (supportsRange, totalSize, _) = try await checkRangeSupport(url: url, headers: headers) guard totalSize > 0 else { throw NetworkError.invalidData("无法获取文件大小") @@ -548,7 +577,7 @@ class ChunkedDownloadManager { return false } - if let expectedHash = chunk.expectedHash, + if chunk.expectedHash != nil && fileManager.fileExists(atPath: destinationURL.path) { if validateCompleteChunkFromFile(destinationURL: destinationURL, chunk: chunk) { return false @@ -582,14 +611,12 @@ class ChunkedDownloadManager { clearChunkedDownloadState(packageIdentifier: packageIdentifier) } - taskLock.lock() - activeTasks[packageIdentifier] = downloadTask - taskLock.unlock() + await setActiveTask(packageIdentifier: packageIdentifier, task: downloadTask) defer { - taskLock.lock() - activeTasks.removeValue(forKey: packageIdentifier) - taskLock.unlock() + Task { + await removeActiveTask(packageIdentifier: packageIdentifier) + } } try await downloadTask.value diff --git a/Adobe Downloader/Utils/NewDownloadUtils.swift b/Adobe Downloader/Utils/NewDownloadUtils.swift index 26ede66..f0c8416 100644 --- a/Adobe Downloader/Utils/NewDownloadUtils.swift +++ b/Adobe Downloader/Utils/NewDownloadUtils.swift @@ -183,13 +183,6 @@ class NewDownloadUtils { ) } - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - completionHandler(.becomeDownload) - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask) { - } - func cleanup() { completionHandler = { _, _, _ in } progressHandler = nil @@ -370,13 +363,12 @@ class NewDownloadUtils { } } - let initialProgress = await progressManager.getTotalProgress() - + let packagesSnapshot = allPackages await MainActor.run { - let totalPackages = allPackages.count - task.currentPackage = allPackages.first?.package + let totalPackages = packagesSnapshot.count + task.currentPackage = packagesSnapshot.first?.package task.setStatus(.downloading(DownloadStatus.DownloadInfo( - fileName: allPackages.first?.package.fullPackageName ?? "", + fileName: packagesSnapshot.first?.package.fullPackageName ?? "", currentPackageIndex: 0, totalPackages: totalPackages, startTime: Date(), @@ -1295,7 +1287,7 @@ class NewDownloadUtils { let taskPackageMap = await globalCancelTracker.getTaskPackageMap() - for (downloadTaskId, (downloadTask, _, _)) in taskPackageMap { + for (_, (downloadTask, _, _)) in taskPackageMap { downloadTask.cancel() } diff --git a/Adobe Downloader/Utils/TaskPersistenceManager.swift b/Adobe Downloader/Utils/TaskPersistenceManager.swift index 468ef6c..eea1b10 100644 --- a/Adobe Downloader/Utils/TaskPersistenceManager.swift +++ b/Adobe Downloader/Utils/TaskPersistenceManager.swift @@ -1,6 +1,6 @@ import Foundation -class TaskPersistenceManager { +class TaskPersistenceManager: @unchecked Sendable { static let shared = TaskPersistenceManager() private let fileManager = FileManager.default @@ -33,9 +33,9 @@ class TaskPersistenceManager { platform: task.platform ) - await withCheckedContinuation { continuation in - taskCacheQueue.async(flags: .barrier) { - self.taskCache[fileName] = task + await withCheckedContinuation { [weak self] continuation in + self?.taskCacheQueue.async(flags: .barrier) { [weak self] in + self?.taskCache[fileName] = task continuation.resume() } } @@ -100,18 +100,18 @@ class TaskPersistenceManager { for file in files where file.pathExtension == "json" { let fileName = file.lastPathComponent - let cachedTask = await withCheckedContinuation { continuation in - taskCacheQueue.sync { - continuation.resume(returning: self.taskCache[fileName]) + let cachedTask = await withCheckedContinuation { [weak self] continuation in + self?.taskCacheQueue.sync { [weak self] in + continuation.resume(returning: self?.taskCache[fileName]) } } if let cachedTask = cachedTask { tasks.append(cachedTask) } else if let task = await loadTask(from: file) { - await withCheckedContinuation { continuation in - taskCacheQueue.async(flags: .barrier) { - self.taskCache[fileName] = task + await withCheckedContinuation { [weak self] continuation in + self?.taskCacheQueue.async(flags: .barrier) { [weak self] in + self?.taskCache[fileName] = task continuation.resume() } } @@ -219,8 +219,8 @@ class TaskPersistenceManager { ) let fileURL = tasksDirectory.appendingPathComponent(fileName) - taskCacheQueue.async(flags: .barrier) { - self.taskCache.removeValue(forKey: fileName) + taskCacheQueue.async(flags: .barrier) { [weak self] in + self?.taskCache.removeValue(forKey: fileName) } try? fileManager.removeItem(at: fileURL) @@ -277,9 +277,9 @@ class TaskPersistenceManager { ) task.displayInstallButton = true - await withCheckedContinuation { continuation in - taskCacheQueue.async(flags: .barrier) { - self.taskCache[fileName] = task + await withCheckedContinuation { [weak self] continuation in + self?.taskCacheQueue.async(flags: .barrier) { [weak self] in + self?.taskCache[fileName] = task continuation.resume() } } diff --git a/Adobe Downloader/Views/AppCardView.swift b/Adobe Downloader/Views/AppCardView.swift index 7d40fa8..721e63c 100644 --- a/Adobe Downloader/Views/AppCardView.swift +++ b/Adobe Downloader/Views/AppCardView.swift @@ -609,24 +609,20 @@ struct AlertModifier: ViewModifier { } private func startRedownload() async { - do { - globalNetworkManager.downloadTasks.removeAll { task in - task.productId == viewModel.uniqueProduct.id && - task.productVersion == viewModel.pendingVersion && - task.language == viewModel.pendingLanguage - } - - if let existingPath = viewModel.existingFilePath { - try? FileManager.default.removeItem(at: existingPath) - } - - await MainActor.run { - viewModel.selectedVersion = viewModel.pendingVersion - viewModel.selectedLanguage = viewModel.pendingLanguage - viewModel.showVersionPicker = true - } - } catch { - viewModel.handleError(error) + globalNetworkManager.downloadTasks.removeAll { task in + task.productId == viewModel.uniqueProduct.id && + task.productVersion == viewModel.pendingVersion && + task.language == viewModel.pendingLanguage + } + + if let existingPath = viewModel.existingFilePath { + try? FileManager.default.removeItem(at: existingPath) + } + + await MainActor.run { + viewModel.selectedVersion = viewModel.pendingVersion + viewModel.selectedLanguage = viewModel.pendingLanguage + viewModel.showVersionPicker = true } } }