use super::*; pub(super) const MAX_RETAINED_TASKS: usize = 64; /// 每个任务保留的进度日志行数上限。 pub(super) const MAX_TASK_LOG_LINES: usize = 200; /// 任务类型:目前覆盖官方同步、校验与 catalog 更新检查。 #[derive(Debug, Clone, Copy)] pub(super) enum TaskKind { Sync, Verify, Repair, /// catalog 更新检查:只做发现 + 拉取计划(dry-run),不下载不审计。 Refresh, } impl TaskKind { pub(super) fn method(self) -> &'static str { match self { Self::Sync => RPC_METHOD_RESOURCE_SYNC, Self::Verify => RPC_METHOD_RESOURCE_VERIFY, Self::Repair => RPC_METHOD_RESOURCE_REPAIR, Self::Refresh => RPC_METHOD_CATALOG_REFRESH, } } /// 由 daemon 基准配置派生该任务的实际同步配置。 pub(super) fn build_config( self, base: &OfficialUpdateConfig, force: bool, ) -> OfficialUpdateConfig { let mut config = base.clone(); match self { Self::Sync => { config.dry_run = false; config.force = config.force || force; } Self::Verify => { config.dry_run = true; config.plan = true; config.audit_local = true; config.repair = false; config.force = false; } Self::Repair => { config.dry_run = false; config.audit_local = true; config.repair = true; config.force = false; } Self::Refresh => { config.dry_run = true; config.plan = true; config.audit_local = false; config.repair = false; config.force = force; } } config } } /// 请求取消任务的结果。 pub(super) enum CancelOutcome { Requested, AlreadyFinished, NotFound, } /// 单个任务的可轮询记录。 #[derive(Debug, Clone, Serialize)] pub(super) struct TaskRecord { pub(super) id: String, pub(super) kind: &'static str, /// `queued` | `running` | `succeeded` | `failed` | `cancelled`。 pub(super) status: &'static str, #[serde(skip_serializing_if = "Option::is_none")] pub(super) stage: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(super) message: Option, pub(super) created_at: u64, pub(super) updated_at: u64, #[serde(skip_serializing_if = "Option::is_none")] pub(super) started_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(super) finished_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(super) error: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(super) result: Option, /// 取消标志,worker 的 should_cancel 检查它;不参与序列化。 #[serde(skip)] pub(super) cancel: Arc, /// 进度日志(有界),经 task.logs 返回;不参与 task.status 序列化。 #[serde(skip)] pub(super) log: Vec, } impl TaskRecord { pub(super) fn is_finished(&self) -> bool { matches!(self.status, "succeeded" | "failed" | "cancelled") } } struct TaskStore { tasks: HashMap, order: Vec, seq: u64, /// 任务历史持久化文件路径;`None` 表示纯内存(测试等非 daemon 场景)。 persist_path: Option, } /// daemon 任务历史持久化文件名(位于 state dir 内,`0600` 原子写)。 pub(super) const TASKS_FILE_NAME: &str = "bat-tasks.json"; /// 任务历史文件结构版本。 pub(super) const TASKS_FILE_VERSION: u32 = 1; /// 任务历史文件的持久化形态(版本化;daemon 重启后恢复任务历史用)。 #[derive(Debug, Serialize, Deserialize)] pub(super) struct PersistedTaskFile { pub(super) version: u32, /// 任务 ID 序号计数器;恢复它避免 pid 复用时新任务与历史任务撞 ID。 pub(super) seq: u64, pub(super) tasks: Vec, } #[derive(Debug, Serialize, Deserialize)] pub(super) struct PersistedTaskRecord { id: String, kind: String, pub(super) status: String, #[serde(default, skip_serializing_if = "Option::is_none")] stage: Option, #[serde(default, skip_serializing_if = "Option::is_none")] message: Option, created_at: u64, updated_at: u64, #[serde(default, skip_serializing_if = "Option::is_none")] started_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] finished_at: Option, /// `ApiError` 的序列化形态(code/kind/domain/location/message/retryable)。 #[serde(default, skip_serializing_if = "Option::is_none")] error: Option, #[serde(default, skip_serializing_if = "Option::is_none")] result: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] log: Vec, } /// 把持久化的任务类型映射回静态字符串;未识别(如未来版本新增)返回 `None`。 fn task_kind_static(kind: &str) -> Option<&'static str> { match kind { RPC_METHOD_RESOURCE_SYNC => Some(RPC_METHOD_RESOURCE_SYNC), 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), _ => None, } } /// 把持久化的任务状态映射回静态字符串;未识别返回 `None`。 fn task_status_static(status: &str) -> Option<&'static str> { match status { "queued" => Some("queued"), "running" => Some("running"), "succeeded" => Some("succeeded"), "failed" => Some("failed"), "cancelled" => Some("cancelled"), _ => None, } } impl PersistedTaskRecord { fn from_record(record: &TaskRecord) -> Self { Self { id: record.id.clone(), kind: record.kind.to_string(), status: record.status.to_string(), stage: record.stage.clone(), message: record.message.clone(), created_at: record.created_at, updated_at: record.updated_at, started_at: record.started_at, finished_at: record.finished_at, error: record .error .as_ref() .and_then(|error| serde_json::to_value(error).ok()), result: record.result.clone(), log: record.log.clone(), } } /// 还原为内存任务记录;kind/status 未识别时返回 `None`(调用方计数跳过)。 fn into_record(self) -> Option { let kind = task_kind_static(&self.kind)?; let status = task_status_static(&self.status)?; // 错误从序列化形态还原:code 经码表反查(未登记回退 internal), // location 固定为任务执行器(当前全部任务错误的唯一来源)。 let error = self.error.as_ref().map(|value| { let code = value .get("code") .and_then(serde_json::Value::as_str) .and_then(ErrorCode::from_id) .unwrap_or(ErrorCode::INTERNAL); let message = value .get("message") .and_then(serde_json::Value::as_str) .unwrap_or("<持久化错误信息缺失>") .to_string(); ApiError::new(code, "task.executor", message) }); Some(TaskRecord { id: self.id, kind, status, stage: self.stage, message: self.message, created_at: self.created_at, updated_at: self.updated_at, started_at: self.started_at, finished_at: self.finished_at, error, result: self.result, cancel: Arc::new(AtomicBool::new(false)), log: self.log, }) } } /// 读取任务历史文件。文件缺失返回 `Ok(None)`;symlink、解析失败或版本不支持返回 `Err`。 fn load_persisted_tasks(path: &Path) -> Result, String> { let Some(bytes) = read_file_no_symlink(path, "任务历史")? else { return Ok(None); }; let file: PersistedTaskFile = serde_json::from_slice(&bytes) .map_err(|error| format!("解析任务历史失败 {}:{error}", path.display()))?; if file.version != TASKS_FILE_VERSION { return Err(format!( "不支持的任务历史版本 {},文件 {}", file.version, path.display() )); } Ok(Some(file)) } /// 任务注册表句柄:包住内存存储,供 RPC handler 与 worker 共享。 /// /// 通过方法访问(而非直接摸内部 map),便于将来换成 Redis 等持久化后端。 #[derive(Clone)] pub(super) struct TaskRegistry { inner: Arc>, } impl TaskRegistry { /// 纯内存注册表(无持久化);生产 daemon 走 [`Self::with_persistence`]。 #[cfg(test)] pub(super) fn new() -> Self { Self { inner: Arc::new(Mutex::new(TaskStore { tasks: HashMap::new(), order: Vec::new(), seq: 0, persist_path: None, })), } } /// 从 state dir 恢复任务历史并启用持久化。 /// /// 中断时仍处于 queued/running 的任务标记为 `failed`(`TASK_INTERRUPTED`); /// 文件缺失按空历史处理;文件损坏或版本不支持时改名 `.corrupt` 留证并从 /// 空历史开始。返回注册表与恢复摘要(供 daemon 日志记录)。 pub(super) fn with_persistence(state_dir: &Path) -> (Self, String) { let path = state_dir.join(TASKS_FILE_NAME); let now = unix_seconds_now(); let mut seq = 0; let mut tasks = HashMap::new(); let mut order = Vec::new(); let summary = match load_persisted_tasks(&path) { Ok(None) => "无历史任务文件,从空任务历史开始".to_string(), Ok(Some(file)) => { seq = file.seq; let total = file.tasks.len(); let mut interrupted = 0usize; let mut skipped = 0usize; for persisted in file.tasks { let Some(mut record) = persisted.into_record() else { skipped += 1; continue; }; if !record.is_finished() { interrupted += 1; record.status = "failed"; record.finished_at = Some(now); record.updated_at = now; record.error = Some(ApiError::new( ErrorCode::TASK_INTERRUPTED, "task.executor", "daemon 停止/重启导致任务中断", )); record .log .push("[daemon] 任务因 daemon 停止/重启而中断".to_string()); } if tasks.insert(record.id.clone(), record.clone()).is_none() { order.push(record.id); } else { skipped += 1; } } format!("恢复任务历史 {total} 条(标记中断 {interrupted} 条,跳过无法识别 {skipped} 条)") } Err(error) => { // 保留损坏文件供诊断(改名而非覆盖),从空历史开始。 let corrupt = path.with_extension("json.corrupt"); if fs::rename(&path, &corrupt).is_ok() { format!( "任务历史不可用({error});原文件已改名保留为 {}", corrupt.display() ) } else { format!("任务历史不可用({error});且无法改名保留原文件") } } }; let registry = Self { inner: Arc::new(Mutex::new(TaskStore { tasks, order, seq, persist_path: Some(path), })), }; // 把中断标记(或空历史)立即写回,保证文件与内存视图一致。 registry.lock().persist(); (registry, summary) } fn lock(&self) -> std::sync::MutexGuard<'_, TaskStore> { self.inner .lock() .unwrap_or_else(|poison| poison.into_inner()) } /// 创建 queued 任务并返回 task_id。 pub(super) fn create(&self, kind: TaskKind) -> String { let now = unix_seconds_now(); let mut store = self.lock(); store.seq += 1; let id = format!("task-{}-{}", std::process::id(), store.seq); let record = TaskRecord { id: id.clone(), kind: kind.method(), status: "queued", stage: None, message: None, created_at: now, updated_at: now, started_at: None, finished_at: None, error: None, result: None, cancel: Arc::new(AtomicBool::new(false)), log: Vec::new(), }; store.tasks.insert(id.clone(), record); store.order.push(id.clone()); store.prune(); store.persist(); id } pub(super) fn update(&self, id: &str, update: F) { let mut store = self.lock(); let mut status_changed = false; if let Some(record) = store.tasks.get_mut(id) { let previous_status = record.status; update(record); record.updated_at = unix_seconds_now(); status_changed = record.status != previous_status; } // 只在生命周期转换时落盘;stage/message/log 的高频进度更新以内存为准, // 随下一次转换一起写入(避免每个进度事件一次磁盘写)。 if status_changed { store.persist(); } } pub(super) fn get(&self, id: &str) -> Option { self.lock().tasks.get(id).cloned() } /// 返回任务的取消标志(与 worker 共享同一 Arc)。 pub(super) fn cancel_flag(&self, id: &str) -> Option> { self.lock() .tasks .get(id) .map(|record| Arc::clone(&record.cancel)) } /// 追加一行进度日志,超出上限时丢弃最旧的。 pub(super) fn append_log(&self, id: &str, line: String) { let mut store = self.lock(); if let Some(record) = store.tasks.get_mut(id) { record.log.push(line); if record.log.len() > MAX_TASK_LOG_LINES { let overflow = record.log.len() - MAX_TASK_LOG_LINES; record.log.drain(0..overflow); } } } /// 返回任务的进度日志。 pub(super) fn logs(&self, id: &str) -> Option> { self.lock().tasks.get(id).map(|record| record.log.clone()) } /// 请求取消任务:未结束的置取消标志,已结束的原样返回,不存在返回 NotFound。 pub(super) fn request_cancel(&self, id: &str) -> CancelOutcome { let store = self.lock(); match store.tasks.get(id) { None => CancelOutcome::NotFound, Some(record) if record.is_finished() => CancelOutcome::AlreadyFinished, Some(record) => { record.cancel.store(true, Ordering::Relaxed); CancelOutcome::Requested } } } /// 返回全部任务,最新创建的在前。 pub(super) fn list(&self) -> Vec { let store = self.lock(); store .order .iter() .rev() .filter_map(|id| store.tasks.get(id).cloned()) .collect() } } impl TaskStore { /// 把当前任务历史落盘(`0600` 原子写、不跟随 symlink)。 /// /// 持久化未启用时为 no-op;写失败只记 stderr(进 daemon 日志), /// 不让持久化故障拖垮任务执行本身。 fn persist(&self) { let Some(path) = &self.persist_path else { return; }; let file = PersistedTaskFile { version: TASKS_FILE_VERSION, seq: self.seq, tasks: self .order .iter() .filter_map(|id| self.tasks.get(id)) .map(PersistedTaskRecord::from_record) .collect(), }; match serde_json::to_vec_pretty(&file) { Ok(bytes) => { if let Err(error) = write_file_atomic(path, &bytes, PRIVATE_FILE_MODE, "任务历史") { eprintln!("[daemon] 任务历史落盘失败:{error}"); } } Err(error) => eprintln!("[daemon] 任务历史序列化失败:{error}"), } } /// 裁剪最旧的已结束任务,把内存占用控制在上限内;运行中/排队中的任务不裁剪。 fn prune(&mut self) { while self.order.len() > MAX_RETAINED_TASKS { let Some(position) = self.order.iter().position(|id| { self.tasks .get(id) .map(TaskRecord::is_finished) .unwrap_or(true) }) else { break; }; let id = self.order.remove(position); self.tasks.remove(&id); } } } /// 提交给任务 worker 的作业(配置已按任务类型派生完毕)。 pub(super) struct TaskJob { pub(super) id: String, pub(super) config: OfficialUpdateConfig, /// 与任务记录共享的取消标志。 pub(super) cancel: Arc, } /// daemon 任务上下文:RPC handler 借它创建任务、入队和读取。 #[derive(Clone)] pub(super) struct DaemonTaskContext { pub(super) registry: TaskRegistry, pub(super) queue: mpsc::Sender, pub(super) base_config: OfficialUpdateConfig, pub(super) restart_controller: DaemonRestartController, } pub(super) type DaemonRestartController = fn(&Path) -> anyhow::Result; /// 任务 worker:单线程 FIFO 消费任务队列,串行执行官方同步/校验。 /// /// 每个任务执行前获取进程内 `sync_lock`,与 watch 循环互斥(等待而非撞文件锁失败); /// 进度写入任务记录;`should_cancel` 接 daemon 停止标志,停机时中止在途任务。 pub(super) fn run_task_worker( receiver: mpsc::Receiver, registry: TaskRegistry, sync_lock: Arc>, control: DaemonControl, ) { let service = OfficialUpdateService::new(); for job in receiver { registry.update(&job.id, |record| { record.status = "running"; record.started_at = Some(unix_seconds_now()); }); let cancel = Arc::clone(&job.cancel); let run_result = { let _sync_guard = sync_lock .lock() .unwrap_or_else(|poison| poison.into_inner()); let progress_registry = registry.clone(); let progress_id = job.id.clone(); let cancel_check = Arc::clone(&cancel); let stop_control = Arc::clone(&control); service.run_with_progress_and_cancellation( &job.config, |event| { progress_registry .append_log(&progress_id, format!("[{}] {}", event.stage, event.message)); progress_registry.update(&progress_id, |record| { record.stage = Some(event.stage.to_string()); record.message = Some(event.message.clone()); }); }, || { cancel_check.load(Ordering::Relaxed) || daemon_control_stop_requested(Some(&stop_control)) }, ) }; match run_result { Ok(report) => registry.update(&job.id, |record| { record.status = "succeeded"; record.finished_at = Some(unix_seconds_now()); record.result = serde_json::to_value(&report).ok(); }), Err(error) => { let cancelled = cancel.load(Ordering::Relaxed); // 下载失败携带类型化 DownloadError(含准确网络域码);其余归 internal。 let code = error .downcast_ref::() .map(bat_infrastructure::DownloadError::code) .unwrap_or(ErrorCode::INTERNAL); registry.update(&job.id, |record| { record.finished_at = Some(unix_seconds_now()); if cancelled { record.status = "cancelled"; record.error = Some(ApiError::new( ErrorCode::INTERNAL, "task.executor", "任务已取消", )); } else { record.status = "failed"; record.error = Some(ApiError::new(code, "task.executor", error.to_string())); } }); } } } }