Safe Haven 提交流水线状态机,七个状态严格顺序推进
为什么要做状态机
Safe Haven 的提交流水线是一个多服务协作的异步流程:开发者上传 APK(经过 staging)→ 扫描器拉取并处理 → 结果回传 → 状态推进 → 进公开目录。如果不用明确的状态机来串,很容易出现:
- 扫描器重复处理同一个 APK
- 还没扫完就被推进到 live
- 上传失败但状态卡在 pending_upload
- 需要手动回滚到某个中间状态
Safe Haven 用一个严格顺序的状态机来解决这个问题——每个状态只从一个前置状态推进而来,有明确的触发条件和可能的下一个状态。
七个状态和转换
pending_upload
↓ 文件到达 staging
pending_scan
↓ 扫描器拉取
scanning
↓ 扫描完成且通过
pending_review
↓ 自动批准定时器触发
live
scanning ──→ rejected(扫描检出恶意 / 结构验证失败)
live ──→ retired(复扫发现已存在相同 APK 哈希或过时版本代码)
| 状态 | 含义 | 进入条件 | 可能的下一个状态 |
|---|---|---|---|
| pending_upload | 条目创建,等文件到达 | 开发者注册应用或同步进来后 | pending_scan |
| pending_scan | 文件到 staging,等扫描器处理 | APK 成功到达 staging 临时目录 | scanning |
| scanning | 扫描器正在处理 | 扫描器从队列拉到任务 | live / rejected |
| pending_review | 扫描通过,等自动批准 | 扫描完成且所有规则通过 | live /(等待定时自动批准) |
| live | 进生产目录,对外可见 | pending_review 定时器触发 | retired |
| rejected | 扫描未通过,不进目录 | scanning 时检出问题 | (终态,需要重提) |
| retired | 已上架版本被淘汰 | 复扫发现重复 APK hash / 过时 versionCode | (终态) |
各状态的细节
pending_upload
条目刚创建出来,数据库里记录了基本信息(包名、版本名、版本代码、来源),但 APK 文件还没到。这个状态锁定版本参数——versionCode 和 versionName 在这一步就固定了,后面扫描时如果发现 manifest 里的值不同,服务器会自动覆盖修正。
pending_scan
APK 文件已经成功上传到 staging 目录(临时存储),数据库记录里填上了 APK 的 staging 路径。扫描队列里这个条目等待 safehaven-scanner 拉取。
pending_upload → pending_scan 的转换由文件上传完成事件触发——后端监控 staging 目录,文件到达后自动把条目推入扫描队列。
scanning
扫描器从队列拉到任务后,先把条目标记为 scanning——防止其他 worker 实例重复拉取同一个 APK。然后开始:
- 下载 staging 里的 APK
- 计算 SHA-256 哈希、比对重复
- 提取 manifest 里的 versionCode、包名
- 提取签名哈希,比对签名锁定基准
- 跑 VX-TITANIUM 引擎(如果可用)
- 查本地威胁数据库
处理完成后回传结果,状态推进。如果扫描器崩溃或超时没回传,条目会卡在 scanning 状态(需要后台超时清理脚本处理)。
pending_review
扫描通过后进入这个状态。它不是等人手动审核——Safe Haven 有 cron 定时任务在后台跑,检查 pending_review 条目:
- 条目在 review 状态停留超过一定时间且没有产生错误→ 自动批准 → live
- Safe Haven 客户端自己的提交 → 完全跳过 pending_review,扫描通过直接进 live
这个状态的意义是安全缓冲区——即使扫描器误判通过了恶意 APK,pending_review 期间如果有其他监控发现异常,还能在进公开目录前拦住。
live
进入生产目录(apps/{包名}/{versionCode}/),全局索引更新,客户端下次同步时就能看到。
rejected
扫描管线检出问题,直接进入 rejected。触发条件:
- APK SHA-256 重复
- versionCode 重复
- 包名不匹配(manifest 和数据库记录不一致)
- VX-TITANIUM 引擎检出恶意
- 本地威胁数据库检出
rejected 是终态——需要开发者手动修正后重新提交(比如换 versionCode、换签名、修正包名)。
retired
这是 live 之后的回退状态。触发条件通常在复扫管线里:
- 复扫时发现 APK SHA-256 和另一个 live 版本完全相同(去重)
- 复扫时发现 manifest 里的 versionCode 比已有的 live 版本还低(过时版本)
- 上游仓库删除了这个版本
retired 的条目会从全局索引里移除,客户端下次同步时会从本地删除对应的版本记录。
状态回滚
状态机是单向推进的,没有自动回滚机制。如果扫描器误判把一个良性 APK 推进了 rejected:
- 管理员手动把 rejected 状态改成 pending_scan 或 pending_review
- 让扫描器重新处理
- 或者直接强制改成 live(Admin 权限)
应用条目的独立状态
除了提交的七个状态,应用条目本身还有一个独立状态字段:
- Active:正常显示在目录里
- Suspended:从公开目录移除但保留数据库记录
- Removed:完全删除,连同所有提交记录
这个状态和提交状态是两回事——应用条目的 Suspended 不影响已有的 live 提交,需要单独处理。