mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
feat(rpc): 补齐 issue #1 资源后端接口
This commit is contained in:
+3
-3
@@ -264,11 +264,11 @@ cargo run -p bat-infrastructure --bin bat -- \
|
|||||||
|
|
||||||
## 6. 当前阻塞项
|
## 6. 当前阻塞项
|
||||||
|
|
||||||
GitHub issue 状态:当前 open 的是 #1(P1)、#2(P2)、#3(P2)、#17(P2)、#19(P2)。其中 #17 的实现已合入 HEAD,但 issue 本身尚未关闭,需在验收后再同步关闭。
|
GitHub issue 状态:#1 已升为 P0;#17 的实现已合入 HEAD,但 issue 本身尚未关闭,需在验收后再同步关闭。其他 open issue 的实时标签以 GitHub 为准。
|
||||||
|
|
||||||
下一阶段必须优先完成:
|
下一阶段必须优先完成:
|
||||||
|
|
||||||
1. Issue #1(P1,主体已实现):`bat.sock` Unix socket JSON-RPC 已扩展为面向 Go 服务层的 Rust Resource Backend API。统一 envelope(`ok`、`status`、`error`、`data`、`request_id`)与 `BAT-ERR` 错误码模型已落地;`daemon.*`(status/logs/stop/reload/refresh)、`resource.*`(state/sync/verify/manifest)、`catalog.*`(status/refresh/diff/versions)、`task.*`(status/list/cancel/logs)已实现,长任务返回 `task_id` 可轮询(任务执行器单 worker FIFO,与 watch 循环互斥;任务历史持久化于 `<state-dir>/bat-tasks.json`,daemon 重启后仍可查,中断任务标记 `task_interrupted`);错误码已接入下载、launcher/metadata、server-info/marker 与配置校验路径。剩余:`patch.*` / `unityfs.*`(被引擎阻塞)、`resource.repair`(待引擎独立修复模式)、`task.create`(按设计由语义方法创建)、Redis 任务后端(`.env` 已预留配置键,接入时机另议)。Go 层通过 RPC 调用 Rust backend,不走 FFI(FFI 降级说明见 `docs/architecture/official-resource-backend.md` §7)。
|
1. Issue #1(P0,主体已实现):`bat.sock` Unix socket JSON-RPC 已扩展为面向 Go 服务层的 Rust Resource Backend API。统一 envelope(`ok`、`status`、`error`、`data`、`request_id`)与 `BAT-ERR` 错误码模型已落地;`daemon.*`(status/logs/stop/reload/refresh/doctor)、`resource.*`(state/sync/verify/repair/manifest/list)、`catalog.*`(status/refresh/diff/versions)、`task.*`(status/list/cancel/logs)已实现,长任务返回 `task_id` 可轮询(任务执行器单 worker FIFO,与 watch 循环互斥;任务历史持久化于 `<state-dir>/bat-tasks.json`,daemon 重启后仍可查,中断任务标记 `task_interrupted`);错误码已接入下载、launcher/metadata、server-info/marker 与配置校验路径。剩余:`patch.*` / `unityfs.*`(被引擎阻塞)、`task.create`(按设计由语义方法创建)、`daemon.restart` / `daemon.clean-stable`(由 CLI 侧按进程生命周期显式执行,live RPC 内不做自重启或在线清理)、Redis 任务后端(`.env` 已预留配置键,接入时机另议)。Go 层通过 RPC 调用 Rust backend,不走 FFI(FFI 降级说明见 `docs/architecture/official-resource-backend.md` §7)。
|
||||||
2. `cmd/bat` Go CLI 骨架:当前只实现 `doctor`、`manifest inspect` 和 `sync plan` 这类试验性入口,不能视作产品级 CLI;是否继续作为长期产品入口需要单独收敛。
|
2. `cmd/bat` Go CLI 骨架:当前只实现 `doctor`、`manifest inspect` 和 `sync plan` 这类试验性入口,不能视作产品级 CLI;是否继续作为长期产品入口需要单独收敛。
|
||||||
3. 官方同步结果接入 CAS + ResourceRepository 的用户级工作流(G-011 剩余部分:自动导入触发、schema 迁移、CLI 查询)。
|
3. 官方同步结果接入 CAS + ResourceRepository 的用户级工作流(G-011 剩余部分:自动导入触发、schema 迁移、CLI 查询)。
|
||||||
4. Issue #3(P2):AssetBundle UnityFS 基础解析校验。
|
4. Issue #3(P2):AssetBundle UnityFS 基础解析校验。
|
||||||
@@ -283,7 +283,7 @@ GitHub issue 状态:当前 open 的是 #1(P1)、#2(P2)、#3(P2)、
|
|||||||
|
|
||||||
立即任务:
|
立即任务:
|
||||||
|
|
||||||
1. Issue #1 收尾:协议基础设施、最小方法集及 `catalog.*`/`task.*` 全量、错误码模型与文档(USERGUIDE §5/§6、架构文档 §7)均已完成;剩余 `patch.*`/`unityfs.*`(待引擎)与任务持久化按后续里程碑推进。
|
1. Issue #1 收尾:协议基础设施、最小方法集、`catalog.*`、`task.*`、`resource.repair`、任务持久化、错误码模型与文档(USERGUIDE §5/§6、架构文档 §7)均已完成;剩余 `patch.*`/`unityfs.*`(待引擎)以及 `task.create`、`daemon.restart`、`daemon.clean-stable` 的设计边界确认。
|
||||||
2. 明确 Go 产品入口的边界:是继续推进独立 `bat` CLI,还是保留当前 Rust `bat` 为用户 CLI、Go 只做服务层与 `bat-api`。
|
2. 明确 Go 产品入口的边界:是继续推进独立 `bat` CLI,还是保留当前 Rust `bat` 为用户 CLI、Go 只做服务层与 `bat-api`。
|
||||||
3. 跟进官方同步长期运行测试,收集并归档运行报告。
|
3. 跟进官方同步长期运行测试,收集并归档运行报告。
|
||||||
4. 开始 AssetBundle parser 的 UnityFS header/block/directory(issue #3),并继续扩展 Addressables catalog 可校验字段(issue #2)。
|
4. 开始 AssetBundle parser 的 UnityFS header/block/directory(issue #3),并继续扩展 Addressables catalog 可校验字段(issue #2)。
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ BlueArchiveToolkit 不是一次性脚本,也不是演示项目。最终交付
|
|||||||
4. `bat-cas-engine` 已完成 CAS V1:原子写入、BLAKE3 Hash、SQLite 引用计数、GC、并发测试、损坏检测。
|
4. `bat-cas-engine` 已完成 CAS V1:原子写入、BLAKE3 Hash、SQLite 引用计数、GC、并发测试、损坏检测。
|
||||||
5. `bat-infrastructure` 已改为 CAS 仓储适配层,不再重复实现对象存储。
|
5. `bat-infrastructure` 已改为 CAS 仓储适配层,不再重复实现对象存储。
|
||||||
6. `bat-infrastructure` 已提供官方资源 pull/update 服务,正式入口是 Rust binary `bat`。
|
6. `bat-infrastructure` 已提供官方资源 pull/update 服务,正式入口是 Rust binary `bat`。
|
||||||
7. `bat` 支持 `--auto-discover`、`--watch`、`--daemon`、默认 1 小时间隔、本地 manifest audit/repair、官方 seed `.hash` 校验、snapshot/cache,以及基于 Unix socket JSON-RPC 的 `status/stop/restart/reload/refresh/logs/verify/repair/doctor/clean-stable` 运维命令。
|
7. `bat` 支持 `--auto-discover`、`--watch`、`--daemon`、默认 1 小时间隔、本地 manifest audit/repair、官方 seed `.hash` 校验、snapshot/cache,以及基于 Unix socket JSON-RPC 的 live control/backend 方法(`daemon.status/logs/stop/reload/refresh/doctor`、`resource.sync/verify/repair/state/manifest/list`、`catalog.*`、`task.*`);`restart` 和 `clean-stable` 仍由 CLI 侧按进程生命周期显式执行。
|
||||||
8. `bat-ffi` 已提供 Manifest inspect 和官方 sync plan 的可选无状态粗粒度 JSON C ABI helper。
|
8. `bat-ffi` 已提供 Manifest inspect 和官方 sync plan 的可选无状态粗粒度 JSON C ABI helper。
|
||||||
9. 文档已整理:根目录保留入口文档,历史报告进入 `docs/reports/historical/`,误嵌套的 `docs/docs` 已合并。
|
9. 文档已整理:根目录保留入口文档,历史报告进入 `docs/reports/historical/`,误嵌套的 `docs/docs` 已合并。
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
- `bat-adapters` Unity、Manifest、Client 集成框架,以及当前真实形态 Addressables catalog 解析覆盖,含 `m_Crc` 提取和 UnityFS 基础校验。
|
- `bat-adapters` Unity、Manifest、Client 集成框架,以及当前真实形态 Addressables catalog 解析覆盖,含 `m_Crc` 提取和 UnityFS 基础校验。
|
||||||
- `bat-cas-engine` CAS V1:原子写入、BLAKE3 校验、引用计数、GC、并发写入测试、损坏检测。
|
- `bat-cas-engine` CAS V1:原子写入、BLAKE3 校验、引用计数、GC、并发写入测试、损坏检测。
|
||||||
- `bat-infrastructure` CAS 适配层、SQLite Resource Repository、资源导入服务、官方资源 pull/update 服务。
|
- `bat-infrastructure` CAS 适配层、SQLite Resource Repository、资源导入服务、官方资源 pull/update 服务。
|
||||||
- `bat`:官方资源自动发现、全量拉取、原子发布到 `current -> versions/<id>`、本地 manifest audit/repair、`.part` 断点续传、403/404/5xx 分类重试、指数退避、顺序下载、下载 quarantine 诊断、ZIP 结构校验、官方 seed `.hash` 校验、snapshot/cache、`--watch` 常驻更新、`--daemon` 后台运行,以及 Unix socket JSON-RPC 后台控制命令 `status/stop/restart/reload/refresh/logs/verify/repair/doctor/clean-stable`。
|
- `bat`:官方资源自动发现、全量拉取、原子发布到 `current -> versions/<id>`、本地 manifest audit/repair、`.part` 断点续传、403/404/5xx 分类重试、指数退避、顺序下载、下载 quarantine 诊断、ZIP 结构校验、官方 seed `.hash` 校验、snapshot/cache、`--watch` 常驻更新、`--daemon` 后台运行,以及 Unix socket JSON-RPC live control/backend 方法(`daemon.status/logs/stop/reload/refresh/doctor`、`resource.sync/verify/repair/state/manifest/list`、`catalog.*`、`task.*`)。
|
||||||
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
- 官方同步会维护 `<output>/official-version-state.json`,明确记录当前已完成版本、正在拉取版本、上一个可用版本和失败版本。
|
||||||
- 资源导入链路可将 manifest 条目写入 CAS + `ResourceRepository`,AssetBundle 会记录 UnityFS 摘要,TextAsset/Table/Media 会按类型分类索引。
|
- 资源导入链路可将 manifest 条目写入 CAS + `ResourceRepository`,AssetBundle 会记录 UnityFS 摘要,TextAsset/Table/Media 会按类型分类索引。
|
||||||
- `bat-ffi` 可选无状态 C ABI 兼容层:仅保留 Manifest inspect 和官方 sync plan 的粗粒度 JSON helper,不作为 Go CLI 或生产同步的主集成边界。
|
- `bat-ffi` 可选无状态 C ABI 兼容层:仅保留 Manifest inspect 和官方 sync plan 的粗粒度 JSON helper,不作为 Go CLI 或生产同步的主集成边界。
|
||||||
@@ -80,7 +80,7 @@ cargo run -p bat-infrastructure --bin bat -- \
|
|||||||
--error-retry 60s
|
--error-retry 60s
|
||||||
```
|
```
|
||||||
|
|
||||||
后台自动运行可以把 `--watch` 换成 `--daemon`。默认资源目录是 `./bat-resources`,默认后台状态目录是 `/tmp/bat-pid`。daemon 会在状态目录下创建 `bat.sock` 作为 Unix socket JSON-RPC 控制通道,同时写入 `bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl` 和短生命周期的 `bat-control.lock`;`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志,`bat-status.json` 保存最后成功时间、下次检查时间、最后错误摘要和当前下载进度,`bat-control.lock` 用于串行化 `status/stop/restart/reload/logs/refresh` 等控制命令:
|
后台自动运行可以把 `--watch` 换成 `--daemon`。默认资源目录是 `./bat-resources`,默认后台状态目录是 `/tmp/bat-pid`。daemon 会在状态目录下创建 `bat.sock` 作为 Unix socket JSON-RPC 控制通道,同时写入 `bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl` 和短生命周期的 `bat-control.lock`;`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志,`bat-status.json` 保存最后成功时间、下次检查时间、最后错误摘要和当前下载进度,`bat-control.lock` 用于串行化 `status/stop/restart/reload/logs/refresh/repair` 等控制命令:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run -p bat-infrastructure --bin bat -- \
|
cargo run -p bat-infrastructure --bin bat -- \
|
||||||
@@ -94,7 +94,7 @@ cargo run -p bat-infrastructure --bin bat -- reload
|
|||||||
cargo run -p bat-infrastructure --bin bat -- stop
|
cargo run -p bat-infrastructure --bin bat -- stop
|
||||||
```
|
```
|
||||||
|
|
||||||
`status`、`stop`、`logs`、`reload` 和默认形态的 `refresh` 会优先连接 live RPC socket;socket 不可用时,状态和停止命令会回退到 PID/状态文件兼容路径。`reload` 不再强制重启进程,而是让后台 watch 循环重新自动发现并执行强制刷新:空闲睡眠时立即唤醒,正在同步时排队到当前轮结束后执行。确实需要替换启动参数时使用 `restart` 或给 `reload` 显式传入同步参数。后台 daemon 正在管理某个资源目录时,前台 `run/watch/refresh/repair` 不能直接写同一目录;默认形态的 `refresh` 会改走 RPC,显式参数导致无法走 RPC 时需要先 `stop`。
|
`status`、`stop`、`logs`、`reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 live RPC socket;socket 不可用时,状态和停止命令会回退到 PID/状态文件兼容路径。`reload` 不再强制重启进程,而是让后台 watch 循环重新自动发现并执行强制刷新:空闲睡眠时立即唤醒,正在同步时排队到当前轮结束后执行。确实需要替换启动参数时使用 `restart` 或给 `reload` 显式传入同步参数。后台 daemon 正在管理某个资源目录时,前台 `run/watch/refresh/repair` 不能直接写同一目录;默认形态的 `refresh`/`repair` 会改走 RPC,显式参数导致无法走 RPC 时需要先 `stop`。
|
||||||
|
|
||||||
`bat` 会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
`bat` 会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -271,10 +271,12 @@ HTTPS_PROXY=http://user:pass@127.0.0.1:7890 bat --auto-discover --daemon
|
|||||||
| `daemon.stop` | ✅ | 请求停止(`accepted`) |
|
| `daemon.stop` | ✅ | 请求停止(`accepted`) |
|
||||||
| `daemon.reload` | ✅ | 请求重新发现并强制刷新(`accepted`) |
|
| `daemon.reload` | ✅ | 请求重新发现并强制刷新(`accepted`) |
|
||||||
| `daemon.refresh` | ✅ | 请求刷新检查(`params.force`,`accepted`) |
|
| `daemon.refresh` | ✅ | 请求刷新检查(`params.force`,`accepted`) |
|
||||||
|
| `daemon.doctor` | ✅ | 返回运行时诊断报告(只读,不清理、不重启) |
|
||||||
| `resource.state` | ✅ | 资源发布根 + 版本状态 + 上次同步结果 |
|
| `resource.state` | ✅ | 资源发布根 + 版本状态 + 上次同步结果 |
|
||||||
| `resource.sync` | ✅ | 触发同步任务(`params.force`),返回 `task_id` |
|
| `resource.sync` | ✅ | 触发同步任务(`params.force`),返回 `task_id` |
|
||||||
| `resource.verify` | ✅ | 触发校验任务(dry-run + audit),返回 `task_id` |
|
| `resource.verify` | ✅ | 触发校验任务(dry-run + audit),返回 `task_id` |
|
||||||
| `resource.manifest` | ✅ | 当前版本下载 manifest 分页查询(`params.offset` 默认 0、`params.limit` 默认 100/上限 1000) |
|
| `resource.repair` | ✅ | 触发本地 manifest 审计 + 修复任务,返回 `task_id`;不继承 `force` |
|
||||||
|
| `resource.manifest` / `resource.list` | ✅ | 当前版本下载 manifest 分页查询(`params.offset` 默认 0、`params.limit` 默认 100/上限 1000) |
|
||||||
| `catalog.status` | ✅ | 当前已发布版本的 catalog 概览(app/bundle 版本、addressables 根、端点与 marker 计数、launcher 元数据) |
|
| `catalog.status` | ✅ | 当前已发布版本的 catalog 概览(app/bundle 版本、addressables 根、端点与 marker 计数、launcher 元数据) |
|
||||||
| `catalog.versions` | ✅ | 版本历史:current / in_progress / previous / failed |
|
| `catalog.versions` | ✅ | 版本历史:current / in_progress / previous / failed |
|
||||||
| `catalog.diff` | ✅ | 当前 snapshot 相对上一个可用版本的差异(base_delta + extended_delta + 变更端点 URL) |
|
| `catalog.diff` | ✅ | 当前 snapshot 相对上一个可用版本的差异(base_delta + extended_delta + 变更端点 URL) |
|
||||||
@@ -283,14 +285,14 @@ HTTPS_PROXY=http://user:pass@127.0.0.1:7890 bat --auto-discover --daemon
|
|||||||
| `task.list` | ✅ | 列出全部任务(最新在前) |
|
| `task.list` | ✅ | 列出全部任务(最新在前) |
|
||||||
| `task.cancel` | ✅ | 请求取消任务(`params.task_id`);协作式,在同步检查点生效 |
|
| `task.cancel` | ✅ | 请求取消任务(`params.task_id`);协作式,在同步检查点生效 |
|
||||||
| `task.logs` | ✅ | 返回任务的进度日志(`params.task_id`,有界) |
|
| `task.logs` | ✅ | 返回任务的进度日志(`params.task_id`,有界) |
|
||||||
| `resource.repair` / `patch.*` / `unityfs.*` / `task.create` | ⏳ | 已规划,返回 `BAT-ERR-700003`(not implemented);repair 待引擎支持独立修复模式,patch/unityfs 待引擎实现 |
|
| `daemon.restart` / `daemon.clean-stable` / `patch.*` / `unityfs.*` / `task.create` | ⏳ | 已规划,返回 `BAT-ERR-700003`(not implemented);restart/clean-stable 仍由 CLI 侧按进程生命周期显式执行,patch/unityfs 待引擎实现,task.create 暂不开放通用任务入口 |
|
||||||
| 未知方法 | — | `BAT-ERR-700001`(unknown method) |
|
| 未知方法 | — | `BAT-ERR-700001`(unknown method) |
|
||||||
|
|
||||||
只读查询(`resource.state` / `resource.manifest` / `catalog.status` / `catalog.versions` / `catalog.diff`)在尚无已发布版本或对应文件不存在时返回 `ok: true` 且 `data.available: false`(正常状态而非错误,便于调用方直接分支)。
|
只读查询(`daemon.doctor` / `resource.state` / `resource.manifest` / `resource.list` / `catalog.status` / `catalog.versions` / `catalog.diff`)在尚无已发布版本或对应文件不存在时返回 `ok: true` 且 `data.available: false`(正常状态而非错误,便于调用方直接分支)。
|
||||||
|
|
||||||
### 任务模型
|
### 任务模型
|
||||||
|
|
||||||
`resource.sync` / `resource.verify` / `catalog.refresh` 是**异步任务**:入队即返回 `{ "task_id": "task-<pid>-<seq>", "kind": "resource.sync" }`(`status: "accepted"`),实际执行由后台任务 worker 串行完成,通过 `task.status` / `task.list` 轮询。任务记录:
|
`resource.sync` / `resource.verify` / `resource.repair` / `catalog.refresh` 是**异步任务**:入队即返回 `{ "task_id": "task-<pid>-<seq>", "kind": "resource.sync" }`(`status: "accepted"`),实际执行由后台任务 worker 串行完成,通过 `task.status` / `task.list` 轮询。任务记录:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "id": "task-1234-1", "kind": "resource.sync",
|
{ "id": "task-1234-1", "kind": "resource.sync",
|
||||||
@@ -324,4 +326,8 @@ printf '{"jsonrpc":"2.0","id":4,"method":"catalog.versions"}\n' \
|
|||||||
# 分页读取当前版本的下载 manifest
|
# 分页读取当前版本的下载 manifest
|
||||||
printf '{"jsonrpc":"2.0","id":5,"method":"resource.manifest","params":{"offset":0,"limit":50}}\n' \
|
printf '{"jsonrpc":"2.0","id":5,"method":"resource.manifest","params":{"offset":0,"limit":50}}\n' \
|
||||||
| socat - UNIX-CONNECT:/tmp/bat-pid/bat.sock
|
| socat - UNIX-CONNECT:/tmp/bat-pid/bat.sock
|
||||||
|
|
||||||
|
# 触发本地资源审计+修复任务
|
||||||
|
printf '{"jsonrpc":"2.0","id":6,"method":"resource.repair"}\n' \
|
||||||
|
| socat - UNIX-CONNECT:/tmp/bat-pid/bat.sock
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -208,7 +208,7 @@
|
|||||||
11. 下载、manifest、本地 BLAKE3、ZIP 和官方 `.hash` 校验完成后写入新的 snapshot。
|
11. 下载、manifest、本地 BLAKE3、ZIP 和官方 `.hash` 校验完成后写入新的 snapshot。
|
||||||
12. 将 staging rename 为 `<output>/versions/<id>`,再原子替换 `<output>/current` symlink 指向该 versioned 目录。
|
12. 将 staging rename 为 `<output>/versions/<id>`,再原子替换 `<output>/current` symlink 指向该 versioned 目录。
|
||||||
|
|
||||||
该入口不安装、不执行官方启动器,也不读取生产外的本地客户端目录。Rust 正式 binary `bat` 支持单次运行、`--watch` 常驻模式、`--daemon` 后台模式,以及 `status`、`stop`、`restart`、`reload`、`logs`、`refresh`、`verify`、`repair`、`doctor`、`clean-stable` 管理命令。`--daemon` 会在后台状态目录下创建 `bat.sock`,使用 Unix socket JSON-RPC 作为 live control plane;`bat.pid`、`bat-status.json` 和 `bat-daemon.log` 是快照、诊断和兼容 fallback;`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志;`bat-control.lock` 串行化控制命令,并在 stale/corrupt 时由下一次控制命令或 `clean-stable` 恢复。`bat-status.json` 和 `status` 子命令包含最后成功时间、下次检查时间、最后错误摘要、当前阶段和当前下载 URL 进度。PID、status、log 和控制锁文件创建时使用私有权限,读取和写入时不跟随 symlink。`status`、`stop`、`logs`、`reload` 和默认形态的 `refresh` 优先走 RPC;`reload` 会唤醒或排队 watch 循环重新自动发现并强制刷新,`restart` 才负责重启进程或替换启动参数;显式 `--proxy` / `--no-proxy` 会作为启动参数保存并在后台重启时复用。后台 daemon 管理某个资源目录时,前台 `run/watch/refresh/repair` 不允许直接写入同一目录;默认形态 `refresh` 会通过 RPC 触发后台刷新。正常情况下默认每 1 小时执行一次检查;每天北京时间(UTC+8)`03:00`、`16:00`、`18:00` 会中断普通 sleep 并强制执行一次自动刷新,该轮注入 `force=true`。远端和本地一致时静默等待下次检查,不一致时自动下载或 repair。下载、发现或校验失败时不等待完整正常周期,默认 60 秒后重试;如果固定时间强制刷新失败,会保留 pending force 并按失败重试周期继续重试,可用 `--error-retry` 或 `--error-retry-seconds` 调整。默认资源输出目录是 `./bat-resources`,默认后台状态目录是 `/tmp/bat-pid`,二者通过 `--output` 和 `--state-dir` 分别配置。单次运行仍保留为核心幂等路径,systemd service、容器或 Go 进程可以只负责守护该常驻进程;cron/systemd timer 调单次模式只是可选集成方式。项目是否热更新、热重载或重启进程,由上层业务集成决定。
|
该入口不安装、不执行官方启动器,也不读取生产外的本地客户端目录。Rust 正式 binary `bat` 支持单次运行、`--watch` 常驻模式、`--daemon` 后台模式,以及 `status`、`stop`、`restart`、`reload`、`logs`、`refresh`、`verify`、`repair`、`doctor`、`clean-stable` 管理命令。`--daemon` 会在后台状态目录下创建 `bat.sock`,使用 Unix socket JSON-RPC 作为 live control plane;`bat.pid`、`bat-status.json` 和 `bat-daemon.log` 是快照、诊断和兼容 fallback;`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志;`bat-control.lock` 串行化控制命令,并在 stale/corrupt 时由下一次控制命令或 `clean-stable` 恢复。`bat-status.json` 和 `status` 子命令包含最后成功时间、下次检查时间、最后错误摘要、当前阶段和当前下载 URL 进度。PID、status、log 和控制锁文件创建时使用私有权限,读取和写入时不跟随 symlink。`status`、`stop`、`logs`、`reload`、默认形态的 `refresh` 和默认形态的 `repair` 优先走 RPC;`reload` 会唤醒或排队 watch 循环重新自动发现并强制刷新,默认 `repair` 会通过 `resource.repair` 入队本地 manifest 审计+修复任务,`restart` 才负责重启进程或替换启动参数;显式 `--proxy` / `--no-proxy` 会作为启动参数保存并在后台重启时复用。后台 daemon 管理某个资源目录时,前台 `run/watch/refresh/repair` 不允许直接写入同一目录;默认形态 `refresh` 会通过 RPC 触发后台刷新,默认形态 `repair` 会通过 RPC 入队任务。正常情况下默认每 1 小时执行一次检查;每天北京时间(UTC+8)`03:00`、`16:00`、`18:00` 会中断普通 sleep 并强制执行一次自动刷新,该轮注入 `force=true`。远端和本地一致时静默等待下次检查,不一致时自动下载或 repair。下载、发现或校验失败时不等待完整正常周期,默认 60 秒后重试;如果固定时间强制刷新失败,会保留 pending force 并按失败重试周期继续重试,可用 `--error-retry` 或 `--error-retry-seconds` 调整。默认资源输出目录是 `./bat-resources`,默认后台状态目录是 `/tmp/bat-pid`,二者通过 `--output` 和 `--state-dir` 分别配置。单次运行仍保留为核心幂等路径,systemd service、容器或 Go 进程可以只负责守护该常驻进程;cron/systemd timer 调单次模式只是可选集成方式。项目是否热更新、热重载或重启进程,由上层业务集成决定。
|
||||||
|
|
||||||
对应实现主要在:
|
对应实现主要在:
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ Linux 生产路径:
|
|||||||
- pull plan 会同时包含 discovery URLs 和 content URLs
|
- pull plan 会同时包含 discovery URLs 和 content URLs
|
||||||
- 全量样本下是 `2` 个 discovery URL + `5` 个内容 URL = `7` 个 URL
|
- 全量样本下是 `2` 个 discovery URL + `5` 个内容 URL = `7` 个 URL
|
||||||
- `OfficialUpdateService` 能持久化 v2 snapshot,并在远端 marker 内容变化时触发下载决策
|
- `OfficialUpdateService` 能持久化 v2 snapshot,并在远端 marker 内容变化时触发下载决策
|
||||||
- `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress log,stdout 默认输出人类可读摘要;progress log 覆盖代理决策、下载已完成计数、单文件开始/完成状态、下载中断失败分类和校验结果摘要;支持 `--proxy` / `--no-proxy` 控制 curl 传输代理,支持 `--json` 输出稳定 JSON,支持 `--no-progress` 关闭进度日志,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,支持 `--daemon` Unix socket JSON-RPC 控制、`status`、`stop`、`restart`、`reload`、`logs`、`refresh --force`、`verify`、`repair`、`doctor`、`clean-stable`,非 dry-run 使用 `.official-sync.lock` 防止并发写资源目录,控制命令使用 `bat-control.lock` 防止并发状态修改,资源发布使用 `.staging`、`versions` 和 `current` 原子切换,daemon 写 `bat-events.jsonl` 结构化日志并在 `status` 中暴露下载进度、失败类型、HTTP 状态和调度状态
|
- `bat` 默认向 stderr 输出 `BlueArchiveToolkit` ASCII banner 和 progress log,stdout 默认输出人类可读摘要;progress log 覆盖代理决策、下载已完成计数、单文件开始/完成状态、下载中断失败分类和校验结果摘要;支持 `--proxy` / `--no-proxy` 控制 curl 传输代理,支持 `--json` 输出稳定 JSON,支持 `--no-progress` 关闭进度日志,支持 `--no-banner` 只关闭横幅,支持 `--watch --interval 1h --error-retry 60s` 常驻运行,支持 `--daemon` Unix socket JSON-RPC live control/backend(`daemon.status/logs/stop/reload/refresh/doctor`、`resource.sync/verify/repair/state/manifest/list`、`catalog.*`、`task.*`);`restart` 与 `clean-stable` 仍由 CLI 侧按进程生命周期显式执行,非 dry-run 使用 `.official-sync.lock` 防止并发写资源目录,控制命令使用 `bat-control.lock` 防止并发状态修改,资源发布使用 `.staging`、`versions` 和 `current` 原子切换,daemon 写 `bat-events.jsonl` 结构化日志并在 `status` 中暴露下载进度、失败类型、HTTP 状态和调度状态
|
||||||
- curl 失败分类和重试策略已覆盖 404 不重试、5xx 重试耗尽后 quarantine、launcher primary CDN 失败后切换 official backup CDN
|
- curl 失败分类和重试策略已覆盖 404 不重试、5xx 重试耗尽后 quarantine、launcher primary CDN 失败后切换 official backup CDN
|
||||||
- `official-version-state.json` 已覆盖当前完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,重新拉取或成功发布后清理同版本失败记录,同版本失败 staging 会在路径安全且未发布时复用,`bat status` 会暴露版本状态摘要和最近历史失败原因
|
- `official-version-state.json` 已覆盖当前完成版本、正在拉取版本、上一个可用版本和失败版本;同一 app version、bundle version 和 Addressables root 的失败只保留最新一条,重新拉取或成功发布后清理同版本失败记录,同版本失败 staging 会在路径安全且未发布时复用,`bat status` 会暴露版本状态摘要和最近历史失败原因
|
||||||
- 资源导入链路已覆盖 CAS 写入、`ResourceRepository` 索引、AssetBundle UnityFS 摘要,以及 TextAsset/Table/Media 分类
|
- 资源导入链路已覆盖 CAS 写入、`ResourceRepository` 索引、AssetBundle UnityFS 摘要,以及 TextAsset/Table/Media 分类
|
||||||
@@ -289,16 +289,17 @@ JSON-RPC 2.0 服务,是面向上层服务(Go 层)的**主要跨语言边
|
|||||||
- `error` 为统一 `ApiError`:`code`(`BAT-ERR-<6 位>`)、`kind`、
|
- `error` 为统一 `ApiError`:`code`(`BAT-ERR-<6 位>`)、`kind`、
|
||||||
`domain`、`location`、`message`、`retryable`。码表以
|
`domain`、`location`、`message`、`retryable`。码表以
|
||||||
`core/src/error_code.rs` 为准。
|
`core/src/error_code.rs` 为准。
|
||||||
- 长任务(`resource.sync` / `resource.verify` / `catalog.refresh`)
|
- 长任务(`resource.sync` / `resource.verify` / `resource.repair` / `catalog.refresh`)
|
||||||
入队即返回 `task_id`,经 `task.status` / `task.list` / `task.logs`
|
入队即返回 `task_id`,经 `task.status` / `task.list` / `task.logs`
|
||||||
轮询,`task.cancel` 协作式取消。任务执行器是单 worker FIFO,与
|
轮询,`task.cancel` 协作式取消。任务执行器是单 worker FIFO,与
|
||||||
watch 循环经进程内锁互斥。任务历史持久化于 `<state-dir>/bat-tasks.json`
|
watch 循环经进程内锁互斥。任务历史持久化于 `<state-dir>/bat-tasks.json`
|
||||||
(版本化、`0600` 原子写,生命周期转换时落盘),daemon 重启后历史任务
|
(版本化、`0600` 原子写,生命周期转换时落盘),daemon 重启后历史任务
|
||||||
仍可经 `task.*` 查询,中断任务标记 `task_interrupted`(700005)。
|
仍可经 `task.*` 查询,中断任务标记 `task_interrupted`(700005)。
|
||||||
- 方法命名空间与实现状态、请求/响应示例见 `USERGUIDE.md` §6:
|
- 方法命名空间与实现状态、请求/响应示例见 `USERGUIDE.md` §6:
|
||||||
`daemon.*` / `resource.*` / `catalog.*` / `task.*` 已实现;
|
`daemon.status/logs/stop/reload/refresh/doctor`、`resource.state/sync/verify/repair/manifest/list`、
|
||||||
`patch.*` / `unityfs.*` 待引擎;`task.create` / `resource.repair`
|
`catalog.*` 与 `task.status/list/cancel/logs` 已实现;`patch.*` / `unityfs.*`
|
||||||
按设计暂缓。
|
待引擎;`task.create` 按设计暂不开放通用任务入口;`daemon.restart` /
|
||||||
|
`daemon.clean-stable` 仍由 CLI 侧按进程生命周期显式执行。
|
||||||
|
|
||||||
### 7.2 Go 层职责边界
|
### 7.2 Go 层职责边界
|
||||||
|
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ sudo -u bat /opt/bluearchive-toolkit/bin/bat reload --state-dir /var/lib/bluearc
|
|||||||
sudo -u bat /opt/bluearchive-toolkit/bin/bat stop --state-dir /var/lib/bluearchive-toolkit/daemon-state
|
sudo -u bat /opt/bluearchive-toolkit/bin/bat stop --state-dir /var/lib/bluearchive-toolkit/daemon-state
|
||||||
```
|
```
|
||||||
|
|
||||||
`--daemon` 会在 `--state-dir` 下创建 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl` 和短生命周期的 `bat-control.lock`。`bat.sock` 是 Unix socket JSON-RPC 控制通道;`status`、`stop`、`logs`、`reload` 和默认形态的 `refresh` 会优先连接 live daemon。PID、状态和日志文件保留为快照、诊断和 socket 不可用时的兼容路径;`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志;`bat-status.json` 会暴露最后成功时间、下次检查时间、最后错误摘要和当前下载进度;`bat-control.lock` 串行化控制命令,并能在 stale/corrupt 时由下一次控制命令或 `clean-stable` 恢复。`reload` 默认不会重启进程,而是让 watch 循环重新自动发现并强制刷新:空闲睡眠时立即唤醒,正在同步时排队到当前轮结束后执行;需要替换启动参数或 binary 时用 `restart`。
|
`--daemon` 会在 `--state-dir` 下创建 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl` 和短生命周期的 `bat-control.lock`。`bat.sock` 是 Unix socket JSON-RPC 控制通道;`status`、`stop`、`logs`、`reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 live daemon。PID、状态和日志文件保留为快照、诊断和 socket 不可用时的兼容路径;`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志;`bat-status.json` 会暴露最后成功时间、下次检查时间、最后错误摘要和当前下载进度;`bat-control.lock` 串行化控制命令,并能在 stale/corrupt 时由下一次控制命令或 `clean-stable` 恢复。`reload` 默认不会重启进程,而是让 watch 循环重新自动发现并强制刷新:空闲睡眠时立即唤醒,正在同步时排队到当前轮结束后执行;需要替换启动参数或 binary 时用 `restart`。
|
||||||
|
|
||||||
不要同时运行 systemd `--watch` 和 standalone `--daemon` 指向同一个 `--output`。二者都会被资源锁和 live daemon 互斥保护,但生产运维上应保持单一 owner。
|
不要同时运行 systemd `--watch` 和 standalone `--daemon` 指向同一个 `--output`。二者都会被资源锁和 live daemon 互斥保护,但生产运维上应保持单一 owner。
|
||||||
|
|
||||||
@@ -287,7 +287,7 @@ sudo -u bat /opt/bluearchive-toolkit/bin/bat doctor --output /var/lib/bluearchiv
|
|||||||
sudo -u bat /opt/bluearchive-toolkit/bin/bat clean-stable --output /var/lib/bluearchive-toolkit/official --state-dir /run/bluearchive-toolkit
|
sudo -u bat /opt/bluearchive-toolkit/bin/bat clean-stable --output /var/lib/bluearchive-toolkit/official --state-dir /run/bluearchive-toolkit
|
||||||
```
|
```
|
||||||
|
|
||||||
如果后台 daemon 正在运行,并且 `refresh` 没有显式指定另一套同步参数,`refresh` / `refresh --force` 会通过 RPC 唤醒或排队后台进程;带 `--output`、server-info、connection-group、app-version、platforms、snapshot、curl 或 unzip 等显式参数时,`refresh` 会作为一次性前台同步运行,但不能写入 live daemon 正在管理的同一资源目录,否则会返回 locked。`verify` 发现远端变化、本地缺失或校验失败时返回非 0;`repair` 会走官方同步链路重新下载必要文件,但同样不能和 live daemon 并行写同一资源目录;`clean-stable` 只清理 `.part`、`.tmp`、失效或损坏的 PID/socket/锁,不删除正式资源。
|
如果后台 daemon 正在运行,并且 `refresh` 没有显式指定另一套同步参数,`refresh` / `refresh --force` 会通过 RPC 唤醒或排队后台进程;默认形态的 `repair` 会通过 `resource.repair` RPC 入队本地 manifest 审计+修复任务并返回 `task_id`。带 `--output`、server-info、connection-group、app-version、platforms、snapshot、curl、unzip 或其它显式同步参数时,`refresh` / `repair` 会作为一次性前台命令运行,但不能写入 live daemon 正在管理的同一资源目录,否则会返回 locked。`verify` 发现远端变化、本地缺失或校验失败时返回非 0;`clean-stable` 只清理 `.part`、`.tmp`、失效或损坏的 PID/socket/锁,不删除正式资源。
|
||||||
|
|
||||||
### 升级
|
### 升级
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ target/release/bat \
|
|||||||
--watch
|
--watch
|
||||||
```
|
```
|
||||||
|
|
||||||
默认资源输出目录是 `./bat-resources`,默认后台状态目录是 `/tmp/bat-pid`。资源输出目录是发布根目录:非 dry-run 同步先写 `<output>/.staging/<id>`,校验完成后移动到 `<output>/versions/<id>`,再原子切换 `<output>/current` symlink;生产读取方应读取 `current`。后台状态目录会保存 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl` 和短生命周期的 `bat-control.lock`;其中 `bat.sock` 是 live daemon 的 Unix socket JSON-RPC 控制通道,`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志,`bat-status.json` 保存最后成功时间、下次检查时间、最后错误摘要和当前下载进度,`bat-control.lock` 串行化 `status/stop/restart/reload/logs/refresh` 等控制命令。生产资源输出目录必须是独立目录;需要覆盖时用 `--output <资源目录>`,不要使用已有游戏客户端目录、官方启动器安装目录、人工维护资源目录,或开发机上的 `/home/wanye/D/BlueArchive`。
|
默认资源输出目录是 `./bat-resources`,默认后台状态目录是 `/tmp/bat-pid`。资源输出目录是发布根目录:非 dry-run 同步先写 `<output>/.staging/<id>`,校验完成后移动到 `<output>/versions/<id>`,再原子切换 `<output>/current` symlink;生产读取方应读取 `current`。后台状态目录会保存 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl` 和短生命周期的 `bat-control.lock`;其中 `bat.sock` 是 live daemon 的 Unix socket JSON-RPC 控制通道,`bat-events.jsonl` 是带轮转的结构化 JSONL 事件日志,`bat-status.json` 保存最后成功时间、下次检查时间、最后错误摘要和当前下载进度,`bat-control.lock` 串行化 `status/stop/restart/reload/logs/refresh/repair` 等控制命令。生产资源输出目录必须是独立目录;需要覆盖时用 `--output <资源目录>`,不要使用已有游戏客户端目录、官方启动器安装目录、人工维护资源目录,或开发机上的 `/home/wanye/D/BlueArchive`。
|
||||||
|
|
||||||
同步流程会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
同步流程会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ cargo run -p bat-infrastructure --bin bat -- reload
|
|||||||
cargo run -p bat-infrastructure --bin bat -- stop
|
cargo run -p bat-infrastructure --bin bat -- stop
|
||||||
```
|
```
|
||||||
|
|
||||||
`status`、`stop`、`logs`、`reload` 和默认形态的 `refresh` 会优先连接 `bat.sock`,通过 Unix socket JSON-RPC 和 live daemon 通信;socket 不可用时,`status`、`stop` 会回退到 PID/状态文件兼容路径。`status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近历史失败版本和原因、文本日志路径、结构化日志路径和轮转日志路径;正在重新拉取同一版本时,对应旧失败不会作为当前历史失败摘要展示;人类输出不会把完整 `official-version-state.json` 内联打印成 JSON。控制命令会通过 `bat-control.lock` 做跨进程互斥,失效或损坏的控制锁会在下次控制命令或 `clean-stable` 时恢复。`restart` 会停止旧后台进程并按保存参数或显式参数重新启动;`reload` 在未显式传入同步参数时不会重启进程,而是唤醒或排队 watch 循环重新执行自动发现和强制刷新:空闲睡眠时立即执行,正在同步时等当前轮结束;如果显式传入 `--proxy` 或 `--no-proxy`,会按新代理配置重启后台进程。所有命令默认输出人类可读摘要,脚本集成时加 `--json`。
|
`status`、`stop`、`logs`、`reload`、默认形态的 `refresh` 和默认形态的 `repair` 会优先连接 `bat.sock`,通过 Unix socket JSON-RPC 和 live daemon 通信;socket 不可用时,`status`、`stop` 会回退到 PID/状态文件兼容路径。`status` 会显示最后成功时间、下次检查时间、最后错误摘要、当前阶段、当前下载 URL 进度、版本状态摘要、最近历史失败版本和原因、文本日志路径、结构化日志路径和轮转日志路径;正在重新拉取同一版本时,对应旧失败不会作为当前历史失败摘要展示;人类输出不会把完整 `official-version-state.json` 内联打印成 JSON。控制命令会通过 `bat-control.lock` 做跨进程互斥,失效或损坏的控制锁会在下次控制命令或 `clean-stable` 时恢复。`restart` 会停止旧后台进程并按保存参数或显式参数重新启动;`reload` 在未显式传入同步参数时不会重启进程,而是唤醒或排队 watch 循环重新执行自动发现和强制刷新:空闲睡眠时立即执行,正在同步时等当前轮结束;如果显式传入 `--proxy` 或 `--no-proxy`,会按新代理配置重启后台进程。所有命令默认输出人类可读摘要,脚本集成时加 `--json`。
|
||||||
|
|
||||||
如果要把后台状态目录改到其他位置,使用 `--state-dir <目录>`:
|
如果要把后台状态目录改到其他位置,使用 `--state-dir <目录>`:
|
||||||
|
|
||||||
|
|||||||
@@ -471,7 +471,7 @@
|
|||||||
## 6. 当前关闭顺序建议
|
## 6. 当前关闭顺序建议
|
||||||
|
|
||||||
1. issue #24:失败 staging 复用已补离线回归;继续核对 issue 口径、状态与后续是否仍有真实场景无法复现的残余。
|
1. issue #24:失败 staging 复用已补离线回归;继续核对 issue 口径、状态与后续是否仍有真实场景无法复现的残余。
|
||||||
2. issue #1:Rust daemon/backend API 边界收口,确认 `patch.*` / `unityfs.*` / `resource.repair` / `task.create` 的剩余阻塞。
|
2. issue #1:Rust daemon/backend API 边界收口;`resource.repair`、`resource.list`、`daemon.doctor` 已补齐,剩余确认 `patch.*` / `unityfs.*`(待引擎)、`task.create`(暂不开放通用入口)以及 `daemon.restart` / `daemon.clean-stable`(CLI 生命周期入口)的 issue 验收口径。
|
||||||
3. issue #17/#20/#21/#22:多线程下载入口已按最新决定移除,下载回归顺序执行并保留指数退避与单调进度上报;daemon 子进程不再透传并发参数,但 GitHub issue 仍 open。
|
3. issue #17/#20/#21/#22:多线程下载入口已按最新决定移除,下载回归顺序执行并保留指数退避与单调进度上报;daemon 子进程不再透传并发参数,但 GitHub issue 仍 open。
|
||||||
4. issue #2 / G-007:继续扩大 Addressables 可校验字段和结构变体覆盖。
|
4. issue #2 / G-007:继续扩大 Addressables 可校验字段和结构变体覆盖。
|
||||||
5. issue #3 / G-005:把 UnityFS 基础摘要推进到 `bat-assetbundle` 引擎级解析。
|
5. issue #3 / G-005:把 UnityFS 基础摘要推进到 `bat-assetbundle` 引擎级解析。
|
||||||
|
|||||||
@@ -641,13 +641,18 @@ struct DaemonRpcAck {
|
|||||||
// 规范方法名采用国际惯例的 `<namespace>.<action>`。`bat.*` 保留为向后兼容别名。
|
// 规范方法名采用国际惯例的 `<namespace>.<action>`。`bat.*` 保留为向后兼容别名。
|
||||||
const RPC_METHOD_STATUS: &str = "daemon.status";
|
const RPC_METHOD_STATUS: &str = "daemon.status";
|
||||||
const RPC_METHOD_STOP: &str = "daemon.stop";
|
const RPC_METHOD_STOP: &str = "daemon.stop";
|
||||||
|
const RPC_METHOD_RESTART: &str = "daemon.restart";
|
||||||
const RPC_METHOD_RELOAD: &str = "daemon.reload";
|
const RPC_METHOD_RELOAD: &str = "daemon.reload";
|
||||||
const RPC_METHOD_REFRESH: &str = "daemon.refresh";
|
const RPC_METHOD_REFRESH: &str = "daemon.refresh";
|
||||||
const RPC_METHOD_LOGS: &str = "daemon.logs";
|
const RPC_METHOD_LOGS: &str = "daemon.logs";
|
||||||
|
const RPC_METHOD_DOCTOR: &str = "daemon.doctor";
|
||||||
|
const RPC_METHOD_CLEAN_STABLE: &str = "daemon.clean-stable";
|
||||||
const RPC_METHOD_RESOURCE_STATE: &str = "resource.state";
|
const RPC_METHOD_RESOURCE_STATE: &str = "resource.state";
|
||||||
const RPC_METHOD_RESOURCE_SYNC: &str = "resource.sync";
|
const RPC_METHOD_RESOURCE_SYNC: &str = "resource.sync";
|
||||||
const RPC_METHOD_RESOURCE_VERIFY: &str = "resource.verify";
|
const RPC_METHOD_RESOURCE_VERIFY: &str = "resource.verify";
|
||||||
|
const RPC_METHOD_RESOURCE_REPAIR: &str = "resource.repair";
|
||||||
const RPC_METHOD_RESOURCE_MANIFEST: &str = "resource.manifest";
|
const RPC_METHOD_RESOURCE_MANIFEST: &str = "resource.manifest";
|
||||||
|
const RPC_METHOD_RESOURCE_LIST: &str = "resource.list";
|
||||||
const RPC_METHOD_CATALOG_STATUS: &str = "catalog.status";
|
const RPC_METHOD_CATALOG_STATUS: &str = "catalog.status";
|
||||||
const RPC_METHOD_CATALOG_VERSIONS: &str = "catalog.versions";
|
const RPC_METHOD_CATALOG_VERSIONS: &str = "catalog.versions";
|
||||||
const RPC_METHOD_CATALOG_DIFF: &str = "catalog.diff";
|
const RPC_METHOD_CATALOG_DIFF: &str = "catalog.diff";
|
||||||
@@ -667,6 +672,7 @@ const MAX_TASK_LOG_LINES: usize = 200;
|
|||||||
enum TaskKind {
|
enum TaskKind {
|
||||||
Sync,
|
Sync,
|
||||||
Verify,
|
Verify,
|
||||||
|
Repair,
|
||||||
/// catalog 更新检查:只做发现 + 拉取计划(dry-run),不下载不审计。
|
/// catalog 更新检查:只做发现 + 拉取计划(dry-run),不下载不审计。
|
||||||
Refresh,
|
Refresh,
|
||||||
}
|
}
|
||||||
@@ -676,6 +682,7 @@ impl TaskKind {
|
|||||||
match self {
|
match self {
|
||||||
Self::Sync => RPC_METHOD_RESOURCE_SYNC,
|
Self::Sync => RPC_METHOD_RESOURCE_SYNC,
|
||||||
Self::Verify => RPC_METHOD_RESOURCE_VERIFY,
|
Self::Verify => RPC_METHOD_RESOURCE_VERIFY,
|
||||||
|
Self::Repair => RPC_METHOD_RESOURCE_REPAIR,
|
||||||
Self::Refresh => RPC_METHOD_CATALOG_REFRESH,
|
Self::Refresh => RPC_METHOD_CATALOG_REFRESH,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -695,6 +702,12 @@ impl TaskKind {
|
|||||||
config.repair = false;
|
config.repair = false;
|
||||||
config.force = false;
|
config.force = false;
|
||||||
}
|
}
|
||||||
|
Self::Repair => {
|
||||||
|
config.dry_run = false;
|
||||||
|
config.audit_local = true;
|
||||||
|
config.repair = true;
|
||||||
|
config.force = false;
|
||||||
|
}
|
||||||
Self::Refresh => {
|
Self::Refresh => {
|
||||||
config.dry_run = true;
|
config.dry_run = true;
|
||||||
config.plan = true;
|
config.plan = true;
|
||||||
@@ -800,6 +813,7 @@ fn task_kind_static(kind: &str) -> Option<&'static str> {
|
|||||||
match kind {
|
match kind {
|
||||||
RPC_METHOD_RESOURCE_SYNC => Some(RPC_METHOD_RESOURCE_SYNC),
|
RPC_METHOD_RESOURCE_SYNC => Some(RPC_METHOD_RESOURCE_SYNC),
|
||||||
RPC_METHOD_RESOURCE_VERIFY => Some(RPC_METHOD_RESOURCE_VERIFY),
|
RPC_METHOD_RESOURCE_VERIFY => Some(RPC_METHOD_RESOURCE_VERIFY),
|
||||||
|
RPC_METHOD_RESOURCE_REPAIR => Some(RPC_METHOD_RESOURCE_REPAIR),
|
||||||
RPC_METHOD_CATALOG_REFRESH => Some(RPC_METHOD_CATALOG_REFRESH),
|
RPC_METHOD_CATALOG_REFRESH => Some(RPC_METHOD_CATALOG_REFRESH),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -1233,21 +1247,28 @@ fn canonical_rpc_method(method: &str) -> &str {
|
|||||||
match method {
|
match method {
|
||||||
"bat.status" => RPC_METHOD_STATUS,
|
"bat.status" => RPC_METHOD_STATUS,
|
||||||
"bat.stop" => RPC_METHOD_STOP,
|
"bat.stop" => RPC_METHOD_STOP,
|
||||||
|
"bat.restart" => RPC_METHOD_RESTART,
|
||||||
"bat.reload" => RPC_METHOD_RELOAD,
|
"bat.reload" => RPC_METHOD_RELOAD,
|
||||||
"bat.refresh" => RPC_METHOD_REFRESH,
|
"bat.refresh" => RPC_METHOD_REFRESH,
|
||||||
"bat.logs" => RPC_METHOD_LOGS,
|
"bat.logs" => RPC_METHOD_LOGS,
|
||||||
|
"bat.doctor" => RPC_METHOD_DOCTOR,
|
||||||
|
"bat.clean-stable" => RPC_METHOD_CLEAN_STABLE,
|
||||||
|
RPC_METHOD_RESOURCE_LIST => RPC_METHOD_RESOURCE_MANIFEST,
|
||||||
other => other,
|
other => other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 判断方法是否属于已规划但尚未实现的命名空间/动作(返回 not_implemented 而非 unknown)。
|
/// 判断方法是否属于已规划但尚未实现的命名空间/动作(返回 not_implemented 而非 unknown)。
|
||||||
fn is_pending_rpc_method(method: &str) -> bool {
|
fn is_pending_rpc_method(method: &str) -> bool {
|
||||||
// task.create:任务统一由 resource.sync / resource.verify / catalog.refresh
|
// task.create:任务统一由 resource.sync / resource.verify / resource.repair / catalog.refresh
|
||||||
// 等语义方法创建,通用创建接口暂不开放。
|
// 等语义方法创建,通用创建接口暂不开放。
|
||||||
// resource.repair:引擎尚无独立修复模式(sync 自带审计+重下)。
|
// daemon.restart / daemon.clean-stable:CLI 侧按进程生命周期处理;
|
||||||
|
// live RPC 内不做自重启或在线清理。
|
||||||
// patch.* / unityfs.*:被 bat-patch / bat-assetbundle 引擎阻塞。
|
// patch.* / unityfs.*:被 bat-patch / bat-assetbundle 引擎阻塞。
|
||||||
matches!(method, "task.create" | "resource.repair")
|
matches!(
|
||||||
|| method.starts_with("patch.")
|
method,
|
||||||
|
"task.create" | RPC_METHOD_RESTART | RPC_METHOD_CLEAN_STABLE
|
||||||
|
) || method.starts_with("patch.")
|
||||||
|| method.starts_with("unityfs.")
|
|| method.starts_with("unityfs.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1831,6 +1852,12 @@ fn dispatch_rpc_method(
|
|||||||
.and_then(|report| serde_json::to_value(report).map_err(anyhow::Error::from)),
|
.and_then(|report| serde_json::to_value(report).map_err(anyhow::Error::from)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
RPC_METHOD_DOCTOR => rpc_envelope_from_result(
|
||||||
|
request_id,
|
||||||
|
"daemon.doctor",
|
||||||
|
build_doctor_report(state_dir, &tasks.base_config)
|
||||||
|
.and_then(|report| serde_json::to_value(report).map_err(anyhow::Error::from)),
|
||||||
|
),
|
||||||
RPC_METHOD_STOP => {
|
RPC_METHOD_STOP => {
|
||||||
daemon_control_mark_stop_requested(control);
|
daemon_control_mark_stop_requested(control);
|
||||||
let _ = update_daemon_state_only(state_dir, "stopping");
|
let _ = update_daemon_state_only(state_dir, "stopping");
|
||||||
@@ -1879,6 +1906,9 @@ fn dispatch_rpc_method(
|
|||||||
RPC_METHOD_RESOURCE_VERIFY => {
|
RPC_METHOD_RESOURCE_VERIFY => {
|
||||||
enqueue_task_envelope(tasks, TaskKind::Verify, false, request_id)
|
enqueue_task_envelope(tasks, TaskKind::Verify, false, request_id)
|
||||||
}
|
}
|
||||||
|
RPC_METHOD_RESOURCE_REPAIR => {
|
||||||
|
enqueue_task_envelope(tasks, TaskKind::Repair, false, request_id)
|
||||||
|
}
|
||||||
RPC_METHOD_RESOURCE_MANIFEST => {
|
RPC_METHOD_RESOURCE_MANIFEST => {
|
||||||
let (offset, limit) = match rpc_page_params(request.params.as_ref()) {
|
let (offset, limit) = match rpc_page_params(request.params.as_ref()) {
|
||||||
Ok(page) => page,
|
Ok(page) => page,
|
||||||
@@ -3006,19 +3036,27 @@ struct CommandReport<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_sync_command(options: &CliOptions, command_name: &'static str) -> anyhow::Result<()> {
|
fn run_sync_command(options: &CliOptions, command_name: &'static str) -> anyhow::Result<()> {
|
||||||
if refresh_should_use_daemon_rpc(options, command_name)
|
if let Some(rpc_method) = sync_command_rpc_method(options, command_name)
|
||||||
&& daemon_rpc_available(&options.state_dir)
|
.filter(|_| daemon_rpc_available(&options.state_dir))
|
||||||
{
|
{
|
||||||
let _control_lock = DaemonControlLock::acquire(&options.state_dir)?;
|
let _control_lock = DaemonControlLock::acquire(&options.state_dir)?;
|
||||||
let report = daemon_rpc_call(
|
let params = if rpc_method == RPC_METHOD_REFRESH {
|
||||||
&options.state_dir,
|
Some(serde_json::json!({ "force": options.config.force }))
|
||||||
RPC_METHOD_REFRESH,
|
} else {
|
||||||
Some(serde_json::json!({ "force": options.config.force })),
|
None
|
||||||
)?;
|
};
|
||||||
|
let report = daemon_rpc_call(&options.state_dir, rpc_method, params)?;
|
||||||
print_json_value(options.output_format, &report)?;
|
print_json_value(options.output_format, &report)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
run_sync_command_foreground(options, command_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_sync_command_foreground(
|
||||||
|
options: &CliOptions,
|
||||||
|
command_name: &'static str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
assert_no_live_daemon_output_conflict(options, command_name)?;
|
assert_no_live_daemon_output_conflict(options, command_name)?;
|
||||||
let mut config = options.config.clone();
|
let mut config = options.config.clone();
|
||||||
if command_name == "repair" {
|
if command_name == "repair" {
|
||||||
@@ -3044,13 +3082,9 @@ fn run_sync_command(options: &CliOptions, command_name: &'static str) -> anyhow:
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_should_use_daemon_rpc(options: &CliOptions, command_name: &str) -> bool {
|
fn sync_command_rpc_method(options: &CliOptions, command_name: &str) -> Option<&'static str> {
|
||||||
if command_name != "refresh" {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let defaults = OfficialUpdateConfig::default();
|
let defaults = OfficialUpdateConfig::default();
|
||||||
options.command == CliCommand::Refresh
|
let default_daemon_shape = !options.watch
|
||||||
&& !options.watch
|
|
||||||
&& !options.daemon
|
&& !options.daemon
|
||||||
&& !options.daemon_child
|
&& !options.daemon_child
|
||||||
&& !options.output_explicit
|
&& !options.output_explicit
|
||||||
@@ -3068,7 +3102,19 @@ fn refresh_should_use_daemon_rpc(options: &CliOptions, command_name: &str) -> bo
|
|||||||
&& !options.config.dry_run
|
&& !options.config.dry_run
|
||||||
&& !options.config.plan
|
&& !options.config.plan
|
||||||
&& options.config.audit_local == defaults.audit_local
|
&& options.config.audit_local == defaults.audit_local
|
||||||
&& options.config.repair == defaults.repair
|
&& options.config.repair == defaults.repair;
|
||||||
|
if !default_daemon_shape {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match (options.command, command_name) {
|
||||||
|
(CliCommand::Refresh, "refresh") => Some(RPC_METHOD_REFRESH),
|
||||||
|
(CliCommand::Repair, "repair") if !options.config.force => Some(RPC_METHOD_RESOURCE_REPAIR),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_should_use_daemon_rpc(options: &CliOptions, command_name: &str) -> bool {
|
||||||
|
sync_command_rpc_method(options, command_name) == Some(RPC_METHOD_REFRESH)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_report<T>(format: OutputFormat, report: &T) -> anyhow::Result<()>
|
fn print_report<T>(format: OutputFormat, report: &T) -> anyhow::Result<()>
|
||||||
@@ -3806,30 +3852,29 @@ struct DoctorReport {
|
|||||||
checks: Vec<DoctorCheck>,
|
checks: Vec<DoctorCheck>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
fn build_doctor_report(
|
||||||
|
state_dir: &Path,
|
||||||
|
config: &OfficialUpdateConfig,
|
||||||
|
) -> anyhow::Result<DoctorReport> {
|
||||||
let mut checks = vec![
|
let mut checks = vec![
|
||||||
path_check("state_dir", &options.state_dir, "后台状态目录可用"),
|
path_check("state_dir", state_dir, "后台状态目录可用"),
|
||||||
path_check(
|
path_check("output_root", &config.output_root, "资源输出目录可用"),
|
||||||
"output_root",
|
|
||||||
&options.config.output_root,
|
|
||||||
"资源输出目录可用",
|
|
||||||
),
|
|
||||||
safety_check(
|
safety_check(
|
||||||
"output_root_safety",
|
"output_root_safety",
|
||||||
validate_output_root(&options.config.output_root),
|
validate_output_root(&config.output_root),
|
||||||
"资源输出目录安全边界通过",
|
"资源输出目录安全边界通过",
|
||||||
),
|
),
|
||||||
safety_check(
|
safety_check(
|
||||||
"state_dir_safety",
|
"state_dir_safety",
|
||||||
validate_runtime_state_dir(&options.state_dir),
|
validate_runtime_state_dir(state_dir),
|
||||||
"后台状态目录安全边界通过",
|
"后台状态目录安全边界通过",
|
||||||
),
|
),
|
||||||
command_check("curl", &options.config.curl_command),
|
command_check("curl", &config.curl_command),
|
||||||
proxy_check(&options.config.curl_proxy),
|
proxy_check(&config.curl_proxy),
|
||||||
command_check("unzip", &options.config.unzip_command),
|
command_check("unzip", &config.unzip_command),
|
||||||
];
|
];
|
||||||
|
|
||||||
let pid_path = daemon_pid_path(&options.state_dir);
|
let pid_path = daemon_pid_path(state_dir);
|
||||||
let daemon_running = read_pid_file(&pid_path)
|
let daemon_running = read_pid_file(&pid_path)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
@@ -3857,9 +3902,9 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
let socket_path = daemon_socket_path(&options.state_dir);
|
let socket_path = daemon_socket_path(state_dir);
|
||||||
let socket_exists = daemon_socket_path_exists(&socket_path).unwrap_or(false);
|
let socket_exists = daemon_socket_path_exists(&socket_path).unwrap_or(false);
|
||||||
let socket_available = daemon_rpc_available(&options.state_dir);
|
let socket_available = daemon_rpc_available(state_dir);
|
||||||
checks.push(DoctorCheck {
|
checks.push(DoctorCheck {
|
||||||
name: "daemon_rpc",
|
name: "daemon_rpc",
|
||||||
ok: if daemon_running {
|
ok: if daemon_running {
|
||||||
@@ -3884,7 +3929,7 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let lock_path = options.config.lock_path();
|
let lock_path = config.lock_path();
|
||||||
checks.push(match classify_pid_lock_file(&lock_path)? {
|
checks.push(match classify_pid_lock_file(&lock_path)? {
|
||||||
PidLockState::Missing => DoctorCheck {
|
PidLockState::Missing => DoctorCheck {
|
||||||
name: "resource_lock",
|
name: "resource_lock",
|
||||||
@@ -3917,7 +3962,7 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let control_lock_path = daemon_control_lock_path(&options.state_dir);
|
let control_lock_path = daemon_control_lock_path(state_dir);
|
||||||
checks.push(match classify_pid_lock_file(&control_lock_path)? {
|
checks.push(match classify_pid_lock_file(&control_lock_path)? {
|
||||||
PidLockState::Missing => DoctorCheck {
|
PidLockState::Missing => DoctorCheck {
|
||||||
name: "daemon_control_lock",
|
name: "daemon_control_lock",
|
||||||
@@ -3950,7 +3995,7 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let status_path = daemon_status_path(&options.state_dir);
|
let status_path = daemon_status_path(state_dir);
|
||||||
// ok 与 message 从同一次解析结果派生,避免“ok=false 却提示可解析”的自相矛盾。
|
// ok 与 message 从同一次解析结果派生,避免“ok=false 却提示可解析”的自相矛盾。
|
||||||
let daemon_status_result = read_daemon_status_file(&status_path);
|
let daemon_status_result = read_daemon_status_file(&status_path);
|
||||||
checks.push(DoctorCheck {
|
checks.push(DoctorCheck {
|
||||||
@@ -3964,7 +4009,7 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let healthy = checks.iter().all(|check| check.ok);
|
let healthy = checks.iter().all(|check| check.ok);
|
||||||
let report = DoctorReport {
|
Ok(DoctorReport {
|
||||||
command: "doctor",
|
command: "doctor",
|
||||||
status: if healthy { "ok" } else { "issues_found" },
|
status: if healthy { "ok" } else { "issues_found" },
|
||||||
message: if healthy {
|
message: if healthy {
|
||||||
@@ -3974,7 +4019,12 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
|||||||
},
|
},
|
||||||
healthy,
|
healthy,
|
||||||
checks,
|
checks,
|
||||||
};
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
||||||
|
let report = build_doctor_report(&options.state_dir, &options.config)?;
|
||||||
|
let healthy = report.healthy;
|
||||||
print_report(options.output_format, &report)?;
|
print_report(options.output_format, &report)?;
|
||||||
Ok(healthy)
|
Ok(healthy)
|
||||||
}
|
}
|
||||||
@@ -5919,11 +5969,13 @@ mod tests {
|
|||||||
"404",
|
"404",
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
let repair_id = registry.create(TaskKind::Repair);
|
||||||
|
registry.update(&repair_id, |record| record.status = "succeeded");
|
||||||
drop(registry);
|
drop(registry);
|
||||||
|
|
||||||
// 重启:恢复历史;running 任务标记中断;错误码经持久化往返保留。
|
// 重启:恢复历史;running 任务标记中断;错误码经持久化往返保留。
|
||||||
let (registry, summary) = TaskRegistry::with_persistence(state_dir);
|
let (registry, summary) = TaskRegistry::with_persistence(state_dir);
|
||||||
assert!(summary.contains("恢复任务历史 3 条"), "{summary}");
|
assert!(summary.contains("恢复任务历史 4 条"), "{summary}");
|
||||||
assert!(summary.contains("标记中断 1 条"), "{summary}");
|
assert!(summary.contains("标记中断 1 条"), "{summary}");
|
||||||
let finished = registry.get(&finished_id).unwrap();
|
let finished = registry.get(&finished_id).unwrap();
|
||||||
assert_eq!(finished.status, "succeeded");
|
assert_eq!(finished.status, "succeeded");
|
||||||
@@ -5944,16 +5996,19 @@ mod tests {
|
|||||||
failed.error.as_ref().unwrap().code(),
|
failed.error.as_ref().unwrap().code(),
|
||||||
ErrorCode::HTTP_NOT_FOUND
|
ErrorCode::HTTP_NOT_FOUND
|
||||||
);
|
);
|
||||||
|
let repair = registry.get(&repair_id).unwrap();
|
||||||
|
assert_eq!(repair.kind, "resource.repair");
|
||||||
|
assert_eq!(repair.status, "succeeded");
|
||||||
|
|
||||||
// seq 持久化:重启后(本测试内 pid 相同)新任务不与历史撞 ID。
|
// seq 持久化:重启后(本测试内 pid 相同)新任务不与历史撞 ID。
|
||||||
let new_id = registry.create(TaskKind::Sync);
|
let new_id = registry.create(TaskKind::Sync);
|
||||||
assert!(
|
assert!(
|
||||||
[&finished_id, &running_id, &failed_id]
|
[&finished_id, &running_id, &failed_id, &repair_id]
|
||||||
.iter()
|
.iter()
|
||||||
.all(|id| **id != new_id),
|
.all(|id| **id != new_id),
|
||||||
"新任务 ID {new_id} 与历史撞号"
|
"新任务 ID {new_id} 与历史撞号"
|
||||||
);
|
);
|
||||||
assert_eq!(registry.list().len(), 4);
|
assert_eq!(registry.list().len(), 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -6368,8 +6423,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn canonical_rpc_method_resolves_aliases() {
|
fn canonical_rpc_method_resolves_aliases() {
|
||||||
assert_eq!(canonical_rpc_method("bat.status"), RPC_METHOD_STATUS);
|
assert_eq!(canonical_rpc_method("bat.status"), RPC_METHOD_STATUS);
|
||||||
|
assert_eq!(canonical_rpc_method("bat.doctor"), RPC_METHOD_DOCTOR);
|
||||||
assert_eq!(canonical_rpc_method("bat.refresh"), RPC_METHOD_REFRESH);
|
assert_eq!(canonical_rpc_method("bat.refresh"), RPC_METHOD_REFRESH);
|
||||||
assert_eq!(canonical_rpc_method("daemon.status"), RPC_METHOD_STATUS);
|
assert_eq!(canonical_rpc_method("daemon.status"), RPC_METHOD_STATUS);
|
||||||
|
assert_eq!(
|
||||||
|
canonical_rpc_method("resource.list"),
|
||||||
|
RPC_METHOD_RESOURCE_MANIFEST
|
||||||
|
);
|
||||||
assert_eq!(canonical_rpc_method("resource.state"), "resource.state");
|
assert_eq!(canonical_rpc_method("resource.state"), "resource.state");
|
||||||
assert_eq!(canonical_rpc_method("unknown.method"), "unknown.method");
|
assert_eq!(canonical_rpc_method("unknown.method"), "unknown.method");
|
||||||
}
|
}
|
||||||
@@ -6379,17 +6439,20 @@ mod tests {
|
|||||||
assert!(is_pending_rpc_method("patch.apply"));
|
assert!(is_pending_rpc_method("patch.apply"));
|
||||||
assert!(is_pending_rpc_method("unityfs.inspect"));
|
assert!(is_pending_rpc_method("unityfs.inspect"));
|
||||||
assert!(is_pending_rpc_method("task.create"));
|
assert!(is_pending_rpc_method("task.create"));
|
||||||
assert!(is_pending_rpc_method("resource.repair"));
|
assert!(is_pending_rpc_method("daemon.restart"));
|
||||||
// sync/verify、task.cancel/logs、catalog.* 与 resource.manifest 已实现,
|
assert!(is_pending_rpc_method("daemon.clean-stable"));
|
||||||
// 不再是 pending。
|
// sync/verify/repair、task.cancel/logs、catalog.* 与 resource.manifest 已实现。
|
||||||
assert!(!is_pending_rpc_method("resource.sync"));
|
assert!(!is_pending_rpc_method("resource.sync"));
|
||||||
assert!(!is_pending_rpc_method("resource.verify"));
|
assert!(!is_pending_rpc_method("resource.verify"));
|
||||||
|
assert!(!is_pending_rpc_method("resource.repair"));
|
||||||
assert!(!is_pending_rpc_method("resource.manifest"));
|
assert!(!is_pending_rpc_method("resource.manifest"));
|
||||||
|
assert!(!is_pending_rpc_method("resource.list"));
|
||||||
assert!(!is_pending_rpc_method("catalog.status"));
|
assert!(!is_pending_rpc_method("catalog.status"));
|
||||||
assert!(!is_pending_rpc_method("catalog.refresh"));
|
assert!(!is_pending_rpc_method("catalog.refresh"));
|
||||||
assert!(!is_pending_rpc_method("task.cancel"));
|
assert!(!is_pending_rpc_method("task.cancel"));
|
||||||
assert!(!is_pending_rpc_method("task.logs"));
|
assert!(!is_pending_rpc_method("task.logs"));
|
||||||
assert!(!is_pending_rpc_method("daemon.status"));
|
assert!(!is_pending_rpc_method("daemon.status"));
|
||||||
|
assert!(!is_pending_rpc_method("daemon.doctor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -6502,6 +6565,36 @@ mod tests {
|
|||||||
assert_eq!(value["error"]["code"], "BAT-ERR-700004");
|
assert_eq!(value["error"]["code"], "BAT-ERR-700004");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_daemon_doctor_returns_report() {
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let state_dir = temp.path().join("state");
|
||||||
|
let output_root = temp.path().join("output");
|
||||||
|
let mut base_config = OfficialUpdateConfig::default();
|
||||||
|
base_config.output_root = output_root;
|
||||||
|
let (queue, _rx) = mpsc::channel::<TaskJob>();
|
||||||
|
let context = DaemonTaskContext {
|
||||||
|
registry: TaskRegistry::new(),
|
||||||
|
queue,
|
||||||
|
base_config,
|
||||||
|
};
|
||||||
|
|
||||||
|
let envelope = dispatch_rpc_method(
|
||||||
|
&rpc_request("daemon.doctor", None),
|
||||||
|
&state_dir,
|
||||||
|
&new_daemon_control(),
|
||||||
|
&context,
|
||||||
|
"req-doctor-1".to_string(),
|
||||||
|
);
|
||||||
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
|
assert_eq!(value["ok"], true);
|
||||||
|
assert_eq!(value["status"], "ok");
|
||||||
|
assert_eq!(value["data"]["command"], "doctor");
|
||||||
|
assert!(value["data"]["healthy"].is_boolean());
|
||||||
|
let checks = value["data"]["checks"].as_array().unwrap();
|
||||||
|
assert!(checks.iter().any(|check| check["name"] == "daemon_rpc"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dispatch_resource_sync_enqueues_task() {
|
fn dispatch_resource_sync_enqueues_task() {
|
||||||
let temp = tempfile::TempDir::new().unwrap();
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
@@ -6554,6 +6647,48 @@ mod tests {
|
|||||||
assert_eq!(status_value["data"]["status"], "queued");
|
assert_eq!(status_value["data"]["status"], "queued");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_resource_repair_enqueues_repair_task() {
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let control = new_daemon_control();
|
||||||
|
// 保留 rx 让 send 成功(不启动 worker,任务停留在 queued)。
|
||||||
|
let (queue, rx) = mpsc::channel::<TaskJob>();
|
||||||
|
let mut base_config = OfficialUpdateConfig::default();
|
||||||
|
base_config.force = true;
|
||||||
|
base_config.dry_run = true;
|
||||||
|
base_config.audit_local = false;
|
||||||
|
base_config.repair = false;
|
||||||
|
let context = DaemonTaskContext {
|
||||||
|
registry: TaskRegistry::new(),
|
||||||
|
queue,
|
||||||
|
base_config,
|
||||||
|
};
|
||||||
|
|
||||||
|
let envelope = dispatch_rpc_method(
|
||||||
|
&rpc_request("resource.repair", None),
|
||||||
|
temp.path(),
|
||||||
|
&control,
|
||||||
|
&context,
|
||||||
|
"req-repair-1".to_string(),
|
||||||
|
);
|
||||||
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
|
assert_eq!(value["ok"], true);
|
||||||
|
assert_eq!(value["status"], "accepted");
|
||||||
|
assert_eq!(value["data"]["kind"], "resource.repair");
|
||||||
|
let task_id = value["data"]["task_id"].as_str().unwrap();
|
||||||
|
let record = context.registry.get(task_id).unwrap();
|
||||||
|
assert_eq!(record.kind, "resource.repair");
|
||||||
|
assert_eq!(record.status, "queued");
|
||||||
|
|
||||||
|
// repair 任务强制开启本地审计+修复,但不会继承 force/dry-run。
|
||||||
|
let job = rx.try_recv().unwrap();
|
||||||
|
assert_eq!(job.id, task_id);
|
||||||
|
assert!(!job.config.dry_run);
|
||||||
|
assert!(job.config.audit_local);
|
||||||
|
assert!(job.config.repair);
|
||||||
|
assert!(!job.config.force);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn task_registry_create_update_and_prune() {
|
fn task_registry_create_update_and_prune() {
|
||||||
let registry = TaskRegistry::new();
|
let registry = TaskRegistry::new();
|
||||||
@@ -7092,21 +7227,41 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refresh_rpc_selection_only_for_default_daemon_shape() {
|
fn sync_command_rpc_selection_only_for_default_daemon_shape() {
|
||||||
let options = parse(&["bat", "refresh"]).unwrap();
|
let options = parse(&["bat", "refresh"]).unwrap();
|
||||||
assert!(refresh_should_use_daemon_rpc(&options, "refresh"));
|
assert!(refresh_should_use_daemon_rpc(&options, "refresh"));
|
||||||
|
assert_eq!(
|
||||||
|
sync_command_rpc_method(&options, "refresh"),
|
||||||
|
Some(RPC_METHOD_REFRESH)
|
||||||
|
);
|
||||||
|
|
||||||
let options = parse(&["bat", "refresh", "--force"]).unwrap();
|
let options = parse(&["bat", "refresh", "--force"]).unwrap();
|
||||||
assert!(refresh_should_use_daemon_rpc(&options, "refresh"));
|
assert!(refresh_should_use_daemon_rpc(&options, "refresh"));
|
||||||
|
assert_eq!(
|
||||||
|
sync_command_rpc_method(&options, "refresh"),
|
||||||
|
Some(RPC_METHOD_REFRESH)
|
||||||
|
);
|
||||||
|
|
||||||
let options = parse(&["bat", "refresh", "--output", "/tmp/other"]).unwrap();
|
let options = parse(&["bat", "refresh", "--output", "/tmp/other"]).unwrap();
|
||||||
assert!(!refresh_should_use_daemon_rpc(&options, "refresh"));
|
assert!(!refresh_should_use_daemon_rpc(&options, "refresh"));
|
||||||
|
assert_eq!(sync_command_rpc_method(&options, "refresh"), None);
|
||||||
|
|
||||||
let options = parse(&["bat", "refresh", "--server-info-file", "ProdNotice.json"]).unwrap();
|
let options = parse(&["bat", "refresh", "--server-info-file", "ProdNotice.json"]).unwrap();
|
||||||
assert!(!refresh_should_use_daemon_rpc(&options, "refresh"));
|
assert!(!refresh_should_use_daemon_rpc(&options, "refresh"));
|
||||||
|
assert_eq!(sync_command_rpc_method(&options, "refresh"), None);
|
||||||
|
|
||||||
let options = parse(&["bat", "repair"]).unwrap();
|
let options = parse(&["bat", "repair"]).unwrap();
|
||||||
assert!(!refresh_should_use_daemon_rpc(&options, "repair"));
|
assert!(!refresh_should_use_daemon_rpc(&options, "repair"));
|
||||||
|
assert_eq!(
|
||||||
|
sync_command_rpc_method(&options, "repair"),
|
||||||
|
Some(RPC_METHOD_RESOURCE_REPAIR)
|
||||||
|
);
|
||||||
|
|
||||||
|
let options = parse(&["bat", "repair", "--output", "/tmp/other"]).unwrap();
|
||||||
|
assert_eq!(sync_command_rpc_method(&options, "repair"), None);
|
||||||
|
|
||||||
|
let options = parse(&["bat", "repair", "--force"]).unwrap();
|
||||||
|
assert_eq!(sync_command_rpc_method(&options, "repair"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -7591,17 +7746,34 @@ mod tests {
|
|||||||
let value = serde_json::to_value(&envelope).unwrap();
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
assert_eq!(value["ok"], false);
|
assert_eq!(value["ok"], false);
|
||||||
assert_eq!(value["error"]["code"], "BAT-ERR-700002");
|
assert_eq!(value["error"]["code"], "BAT-ERR-700002");
|
||||||
|
|
||||||
|
let envelope = dispatch_rpc_method(
|
||||||
|
&rpc_request(
|
||||||
|
"resource.list",
|
||||||
|
Some(serde_json::json!({ "offset": 2, "limit": 1 })),
|
||||||
|
),
|
||||||
|
&state_dir,
|
||||||
|
&new_daemon_control(),
|
||||||
|
&test_task_context(),
|
||||||
|
"req-man-3".to_string(),
|
||||||
|
);
|
||||||
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
|
assert_eq!(value["ok"], true);
|
||||||
|
assert_eq!(value["data"]["available"], true);
|
||||||
|
let entries = value["data"]["entries"].as_array().unwrap();
|
||||||
|
assert_eq!(entries.len(), 1);
|
||||||
|
assert_eq!(entries[0]["destination"], "c");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dispatch_resource_repair_reports_not_implemented() {
|
fn dispatch_daemon_clean_stable_reports_not_implemented() {
|
||||||
let temp = tempfile::TempDir::new().unwrap();
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
let envelope = dispatch_rpc_method(
|
let envelope = dispatch_rpc_method(
|
||||||
&rpc_request("resource.repair", None),
|
&rpc_request("daemon.clean-stable", None),
|
||||||
temp.path(),
|
temp.path(),
|
||||||
&new_daemon_control(),
|
&new_daemon_control(),
|
||||||
&test_task_context(),
|
&test_task_context(),
|
||||||
"req-rep-1".to_string(),
|
"req-clean-1".to_string(),
|
||||||
);
|
);
|
||||||
let value = serde_json::to_value(&envelope).unwrap();
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
assert_eq!(value["ok"], false);
|
assert_eq!(value["ok"], false);
|
||||||
|
|||||||
Reference in New Issue
Block a user