fix: 修复在多线程下载器中点击取消按钮只能回到首页的问题#524
Open
xtr-hub wants to merge 8 commits intoSeaLantern-Studio:devfrom
Open
Conversation
- 修复下载取消时网络连接未正确断开的问题 - 添加取消时的临时文件清理机制 - 实现多层级取消架构(前端、后端命令、下载管理器、网络层) - 通过 JoinHandle 管理后台任务并支持强制终止 - 确保内存缓冲区和资源在取消时完全释放 - 解决"伪停止"问题,使取消操作真正停止所有后台活动 Fixes: 下载取消功能不完整的问题
Contributor
Reviewer's Guide通过跟踪生成的 Tokio 任务来实现一个真正可取消的下载工作流,将取消令牌在整个下载器中向下传递,把新的取消命令从 Tauri 后端串联到 Vue 前端,并更新 UI 的取消按钮,使其停止当前下载而不是导航回首页;同时现代化了一个启动工具中的排序调用,并新增了 新的可取消下载流程的时序图sequenceDiagram
actor User
participant DownloadFileView
participant downloadApi
participant TauriRuntime
participant cancel_download_task
participant DownloadManager
participant DownloadTaskState
participant DownloadStatus
participant TokioJoinHandle
participant MultiThreadDownloader
User ->> DownloadFileView: click_cancel_button
DownloadFileView ->> downloadApi: cancelDownloadTask(taskInfo.id)
downloadApi ->> TauriRuntime: tauriInvoke cancel_download_task idStr
TauriRuntime ->> cancel_download_task: invoke_command
cancel_download_task ->> DownloadManager: cancel_task(id)
DownloadManager ->> DownloadTaskState: read_state(id)
DownloadManager ->> DownloadStatus: status_handle.cancel()
DownloadManager ->> TokioJoinHandle: join_handle.abort()
DownloadManager ->> DownloadManager: remove_task_from_map(id)
DownloadManager ->> DownloadManager: remove_temp_file(file_path)
DownloadManager -->> cancel_download_task: Ok
cancel_download_task -->> TauriRuntime: Ok
TauriRuntime -->> downloadApi: Ok
downloadApi -->> DownloadFileView: resolve_promise
DownloadFileView ->> DownloadFileView: stop_polling_and_reset_task_state
rect rgb(230,230,250)
DownloadStatus ->> MultiThreadDownloader: expose_cancel_token
MultiThreadDownloader ->> MultiThreadDownloader: worker_uses_select_with_cancel_token
MultiThreadDownloader ->> MultiThreadDownloader: check_status.cancelled_during_chunks
end
下载管理器与下载器类型的更新类图classDiagram
class DownloadManager {
- HashMap~Uuid, Arc~DownloadTaskState~~ tasks
+ create_task(url: String, path: String, thread_count: usize) Uuid
+ get_progress(id: Uuid) Option~TaskProgressResponse~
+ get_all_progress() Vec~TaskProgressResponse~
+ cancel_task(id: Uuid) Result~(), String~
}
class DownloadTaskState {
- String _file_path
- tokio::sync::Mutex~Option~Arc~DownloadStatus~~~ status_handle
- RwLock~TaskStatus~ internal_status
- tokio::sync::Mutex~Option~JoinHandle~()~~ join_handle
}
class TaskStatus {
<<enum>>
Pending
Downloading
Completed
Error
}
class MultiThreadDownloader {
+ download(url: String, path: String, thread_count: usize) Result~Arc~DownloadStatus~, String~
+ worker(url: String, path: String, start: u64, end: u64, status: Arc~DownloadStatus~) Result~(), Box~dyn std::error::Error + Send + Sync~~
}
class DownloadStatus {
+ AtomicU64 total_size
+ AtomicU64 downloaded
+ RwLock~Option~String~~ error_message
- CancellationToken cancel_token
+ new(total_size: u64) DownloadStatus
+ cancel()
+ cancelled() bool
+ set_error(msg: String) async
}
DownloadManager --> DownloadTaskState : manages
DownloadTaskState --> TaskStatus : has
DownloadTaskState --> DownloadStatus : holds
DownloadTaskState --> TokioJoinHandle : holds
MultiThreadDownloader --> DownloadStatus : updates
class TokioJoinHandle {
+ abort()
}
class CancellationToken {
+ cancel()
+ is_cancelled() bool
}
DownloadStatus --> CancellationToken : owns
前端取消按钮处理流程图flowchart TD
BTN["User clicks_cancel_button"] --> CHECKID{taskInfo.id_exists?}
CHECKID -- Yes --> CALLAPI["downloadApi.cancelDownloadTask(id)"]
CHECKID -- No --> STOPLOADING["stopLoading"]
CALLAPI --> ONRESOLVE["on_success"]
CALLAPI --> ONERROR["on_error_showError"]
ONRESOLVE --> STOP["stop_polling"]
ONRESOLVE --> RESET["resetTask_state"]
ONRESOLVE --> STOPLOADING
ONERROR --> STOPLOADING
文件级变更
与关联 Issue 的对照评估
可能关联的 Issue
Tips and commandsInteracting with Sourcery
Customizing Your Experience访问你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideImplements a proper cancellable download workflow by tracking spawned Tokio tasks, propagating a cancellation token through the downloader, wiring a new cancel command through the Tauri backend to the Vue frontend, and updating the UI cancel button to stop the download instead of navigating home; also modernizes one startup utility sort call and adds the tokio-util dependency. Sequence diagram for the new cancellable download flowsequenceDiagram
actor User
participant DownloadFileView
participant downloadApi
participant TauriRuntime
participant cancel_download_task
participant DownloadManager
participant DownloadTaskState
participant DownloadStatus
participant TokioJoinHandle
participant MultiThreadDownloader
User ->> DownloadFileView: click_cancel_button
DownloadFileView ->> downloadApi: cancelDownloadTask(taskInfo.id)
downloadApi ->> TauriRuntime: tauriInvoke cancel_download_task idStr
TauriRuntime ->> cancel_download_task: invoke_command
cancel_download_task ->> DownloadManager: cancel_task(id)
DownloadManager ->> DownloadTaskState: read_state(id)
DownloadManager ->> DownloadStatus: status_handle.cancel()
DownloadManager ->> TokioJoinHandle: join_handle.abort()
DownloadManager ->> DownloadManager: remove_task_from_map(id)
DownloadManager ->> DownloadManager: remove_temp_file(file_path)
DownloadManager -->> cancel_download_task: Ok
cancel_download_task -->> TauriRuntime: Ok
TauriRuntime -->> downloadApi: Ok
downloadApi -->> DownloadFileView: resolve_promise
DownloadFileView ->> DownloadFileView: stop_polling_and_reset_task_state
rect rgb(230,230,250)
DownloadStatus ->> MultiThreadDownloader: expose_cancel_token
MultiThreadDownloader ->> MultiThreadDownloader: worker_uses_select_with_cancel_token
MultiThreadDownloader ->> MultiThreadDownloader: check_status.cancelled_during_chunks
end
Updated class diagram for download manager and downloader typesclassDiagram
class DownloadManager {
- HashMap~Uuid, Arc~DownloadTaskState~~ tasks
+ create_task(url: String, path: String, thread_count: usize) Uuid
+ get_progress(id: Uuid) Option~TaskProgressResponse~
+ get_all_progress() Vec~TaskProgressResponse~
+ cancel_task(id: Uuid) Result~(), String~
}
class DownloadTaskState {
- String _file_path
- tokio::sync::Mutex~Option~Arc~DownloadStatus~~~ status_handle
- RwLock~TaskStatus~ internal_status
- tokio::sync::Mutex~Option~JoinHandle~()~~ join_handle
}
class TaskStatus {
<<enum>>
Pending
Downloading
Completed
Error
}
class MultiThreadDownloader {
+ download(url: String, path: String, thread_count: usize) Result~Arc~DownloadStatus~, String~
+ worker(url: String, path: String, start: u64, end: u64, status: Arc~DownloadStatus~) Result~(), Box~dyn std::error::Error + Send + Sync~~
}
class DownloadStatus {
+ AtomicU64 total_size
+ AtomicU64 downloaded
+ RwLock~Option~String~~ error_message
- CancellationToken cancel_token
+ new(total_size: u64) DownloadStatus
+ cancel()
+ cancelled() bool
+ set_error(msg: String) async
}
DownloadManager --> DownloadTaskState : manages
DownloadTaskState --> TaskStatus : has
DownloadTaskState --> DownloadStatus : holds
DownloadTaskState --> TokioJoinHandle : holds
MultiThreadDownloader --> DownloadStatus : updates
class TokioJoinHandle {
+ abort()
}
class CancellationToken {
+ cancel()
+ is_cancelled() bool
}
DownloadStatus --> CancellationToken : owns
Flow diagram for frontend cancel button handlingflowchart TD
BTN["User clicks_cancel_button"] --> CHECKID{taskInfo.id_exists?}
CHECKID -- Yes --> CALLAPI["downloadApi.cancelDownloadTask(id)"]
CHECKID -- No --> STOPLOADING["stopLoading"]
CALLAPI --> ONRESOLVE["on_success"]
CALLAPI --> ONERROR["on_error_showError"]
ONRESOLVE --> STOP["stop_polling"]
ONRESOLVE --> RESET["resetTask_state"]
ONRESOLVE --> STOPLOADING
ONERROR --> STOPLOADING
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Contributor
There was a problem hiding this comment.
Hey - 我发现了两个问题,并给出了一些整体性的反馈:
- 在
cancel_task中,tokio::fs::remove_file(&file_path)会将缺失文件视为错误,从而导致整个取消流程失败;建议忽略NotFound(或者把删除操作改成尽力而为),这样当文件已经被删除时,取消操作仍然可以成功。 - 在
sortStartupCandidates中从sort更改为toSorted依赖于一个相对较新的 JS API;请确认目标运行环境支持Array.prototype.toSorted,否则最好回退到sort以避免兼容性问题。
供 AI 代理使用的提示
Please address the comments from this code review:
## Overall Comments
- 在 `cancel_task` 中,`tokio::fs::remove_file(&file_path)` 会将缺失文件视为错误,从而导致整个取消流程失败;建议忽略 `NotFound`(或者把删除操作改成尽力而为),这样当文件已经被删除时,取消操作仍然可以成功。
- 在 `sortStartupCandidates` 中从 `sort` 更改为 `toSorted` 依赖于一个相对较新的 JS API;请确认目标运行环境支持 `Array.prototype.toSorted`,否则最好回退到 `sort` 以避免兼容性问题。
## Individual Comments
### Comment 1
<location path="src-tauri/src/utils/downloader.rs" line_range="200-205" />
<code_context>
+ // 检查取消令牌
+ if status.cancelled() {
+ // 如果任务被取消,返回错误,文件将在上层被删除
+ return Err("任务已取消".into());
+ }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** 字符串字面量不能直接转换为声明的装箱错误类型。
该函数返回 `Result<_, Box<dyn std::error::Error + Send + Sync>>`,但 `"任务已取消".into()` 生成的是 `String`,而不是装箱后的 `Error`,因此代码将无法通过编译。你需要返回一个实现了 `Error` 的具体错误类型,例如:
```rust
use std::io;
return Err(Box::<dyn std::error::Error + Send + Sync>::from(
io::Error::new(io::ErrorKind::Interrupted, "任务已取消"),
));
```
或者使用你自己的自定义错误类型并进行装箱。请对该函数中另一个 `"任务已取消".into()` 做同样的修改。
</issue_to_address>
### Comment 2
<location path="src/components/views/create/startupUtils.ts" line_range="48" />
<code_context>
}
export function sortStartupCandidates(candidates: StartupCandidate[]): StartupCandidate[] {
- return [...candidates].sort((a, b) => {
+ return [...candidates].toSorted((a, b) => {
if (a.recommended !== b.recommended) {
return a.recommended - b.recommended;
</code_context>
<issue_to_address>
**issue (bug_risk):** 使用 `toSorted` 可能会在不同目标 JS 运行时之间产生兼容性问题。
`Array.prototype.toSorted` 相对较新,并非所有 JS 运行时都支持,因此较旧的 WebView 可能在运行时抛出异常。如果无法保证使用足够新的引擎(或提供 polyfill),建议保持使用:
```ts
return [...candidates].sort((a, b) => { /* ... */ });
```
或者在目标运行环境中明确确保已经为 `toSorted` 提供 polyfill。
</issue_to_address>帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈改进评审质量。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- In
cancel_task,tokio::fs::remove_file(&file_path)will treat a missing file as an error and fail the whole cancellation; consider ignoringNotFound(or making deletion best-effort) so that cancellation still succeeds when the file is already gone. - The change from
sorttotoSortedinsortStartupCandidatesrelies on a relatively new JS API; please confirm the target runtime supportsArray.prototype.toSortedor fall back tosortto avoid compatibility issues.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `cancel_task`, `tokio::fs::remove_file(&file_path)` will treat a missing file as an error and fail the whole cancellation; consider ignoring `NotFound` (or making deletion best-effort) so that cancellation still succeeds when the file is already gone.
- The change from `sort` to `toSorted` in `sortStartupCandidates` relies on a relatively new JS API; please confirm the target runtime supports `Array.prototype.toSorted` or fall back to `sort` to avoid compatibility issues.
## Individual Comments
### Comment 1
<location path="src-tauri/src/utils/downloader.rs" line_range="200-205" />
<code_context>
+ // 检查取消令牌
+ if status.cancelled() {
+ // 如果任务被取消,返回错误,文件将在上层被删除
+ return Err("任务已取消".into());
+ }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** String literals cannot be directly converted into the declared boxed error type.
The function returns `Result<_, Box<dyn std::error::Error + Send + Sync>>`, but `"任务已取消".into()` produces a `String`, not a boxed `Error`, so this won’t compile. You need to return a concrete error type that implements `Error`, for example:
```rust
use std::io;
return Err(Box::<dyn std::error::Error + Send + Sync>::from(
io::Error::new(io::ErrorKind::Interrupted, "任务已取消"),
));
```
or a custom error type that you box. Apply the same fix to the other `"任务已取消".into()` in this function.
</issue_to_address>
### Comment 2
<location path="src/components/views/create/startupUtils.ts" line_range="48" />
<code_context>
}
export function sortStartupCandidates(candidates: StartupCandidate[]): StartupCandidate[] {
- return [...candidates].sort((a, b) => {
+ return [...candidates].toSorted((a, b) => {
if (a.recommended !== b.recommended) {
return a.recommended - b.recommended;
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `toSorted` may introduce compatibility issues depending on the target JS runtime.
`Array.prototype.toSorted` is relatively new and not available in all JS runtimes, so older WebViews may throw at runtime. If you can’t guarantee a sufficiently modern engine (or a polyfill), stick with:
```ts
return [...candidates].sort((a, b) => { /* ... */ });
```
or explicitly ensure `toSorted` is polyfilled in your target environments.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- 在startuputils中使用sort防止js版本冲突
- 调整了删除临时下载文件的代码格式,提高可读性 - 将长链式调用拆分为多行,符合代码风格规范
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
提交前检查清单
提交前测试必读!!!.md并完成要求测试变更分类(必选其一)
feat新功能fixBug 修复docs文档/模板style代码格式(不影响功能)refactor重构(既不修复 bug 也不添加功能)perf性能优化test测试相关chore构建/CI/依赖/工具链revert回滚security安全修复影响范围(可多选)
前端 Frontend
后端 Backend
基础设施 Infrastructure
导入规范检查: 使用别名导入,避免相对路径
../变更详情
摘要
修复多线程下载器在下载途中点击取消按钮会直接回到首页而实际下载进程并未停止的bug,修复后在下载途中点击取消按钮会自动完成任务清除、页面更新、进程取消、资源回收,真正实现了下载任务的取消逻辑。
动机/背景
issue #507 考虑解决取消按钮点击后只能回到首页的问题
具体改动
添加 JoinHandle 管理:在 DownloadTaskState 中增加 join_handle 字段来跟踪后台任务
存储任务句柄:在 create_task 中将 tokio::spawn 返回的 JoinHandle 保存起来
完善取消流程:在 cancel_task 中:
调用 status_handle.cancel() 触发取消令牌
调用 handle.abort() 终止后台任务
删除临时下载文件
从任务管理器中移除任务
修正网络层取消:更新 _worker 函数中的 tokio::select! 确保网络请求响应取消信号
界面变动(如适用)
关联 Issue
示例:
close/closes/closedClose #123fix/fixes/fixedFixes #123resolve/resolves/resolvedResolve #123ref/references/refsRef #123related/relates toRelated to #123part ofPart of #123see/see alsoSee #123reRe #123addressesAddresses #123implementsImplements #123mergesMerges #123自动化审查说明
sourcery-ai 及其他 code review 工具请务必进行中英双语审查与交流。
Note: Please ensure sourcery-ai and other tools perform bilingual (Chinese & English) review.
Summary by Sourcery
在前端和后端中为下载任务实现完善的取消机制。
Bug 修复:
增强功能:
JoinHandle和取消令牌(cancellation token)的串联,用于干净地追踪并中止下载工作任务。toSortedAPI。构建:
tokio-util作为依赖,用于取消相关的工具功能。Original summary in English
Summary by Sourcery
Implement proper cancellation for download tasks across frontend and backend.
Bug Fixes:
Enhancements:
Build: