Fix/reliability basics - #17
Merged
Merged
Conversation
- docker/mod.rs 将 tests 模块改为 #[cfg(test)] 内部模块声明 - 消除 clippy module_inception 警告,为 CI 门禁做准备
- 新增 is_safe_entry_name 校验:拒绝绝对路径、父目录引用(..)、盘符/UNC 前缀 - restore() 在任何写盘操作前对全部条目做安全校验 - 回归测试:恶意 ZIP(../../escaped.txt)整体拒绝且不落盘 - 对应改进报告 R1(docs/IMPROVEMENT_REPORT.md)
- 删除 select_project_folder 空占位命令及其 invoke_handler 注册(前端从未调用) - load_existing_config 解析主体抽为纯函数 parse_env_to_services,3 个无断言空测试改为真断言 (多 Redis / 多 Nginx / 混合服务 + MYSQL_ROOT_PASSWORD 不误识别 + 未收录前缀回退) - config_generation_integration 占位测试替换为 5 个端到端断言(validate/env/compose/自定义变量保留) - 修复真实 bug:NGINX 版本号切片 [6..] 少一位,非默认端口的 Nginx 配置回读时被错误重置为 80 (新测试捕获;对应报告 A2 魔数切片风险的现实印证,整段重写留待后续批次) - 修复 clippy cmp_owned 警告 - 对应改进报告 A6、Q4(docs/IMPROVEMENT_REPORT.md)
- ubuntu 单 job:cargo fmt --check / clippy -D warnings / cargo test / vitest / vue-tsc+vite build - 多平台打包仍由 release.yml(tag 触发)覆盖 - 对应改进报告 Q1(docs/IMPROVEMENT_REPORT.md)
- README 中的 MIT 链接此前指向不存在的文件 - 对应改进报告 Q3(docs/IMPROVEMENT_REPORT.md)
- customExtInput 由单一字符串改为 Record<number, string>,与扩展面板状态同模式 - 新增/删除 PHP 服务时同步维护输入状态索引 - 回归测试:两个 PHP 服务的自定义扩展输入互不串扰(已验证旧代码上该测试失败) - 对应改进报告 U1(docs/IMPROVEMENT_REPORT.md)
- AGENTS.md:移除 proptest 属性测试描述(代码中 0 使用,依赖一并移除); 恢复端口冲突检测/自动分配、后端日志英文化标记为待完善; 集成测试数量、恢复引擎特性(zip-slip 防护)同步实际实现 - 新增测试规范:禁止 assert!(true) 占位,解析逻辑先抽纯函数再测 - CHANGELOG Unreleased 补记 2026-09 的修复与改进 - 对应改进报告 Q2(docs/IMPROVEMENT_REPORT.md)
冲突解决原则:以 master 的 Phase 3 实现为主体,保留本分支第一批成果。 - engine/mod.rs、engine/config_generator.rs、commands/env_config.rs: 合并两侧 import,新增 config_extractor(运行时配置基线提取器) - lib.rs:采用 master 的命令注册(新增 check_service_images_presence / pull_service_images / extract_service_config 三个 Phase 3 命令), 同时保留本分支的死代码清理(移除 select_project_folder) - 保留 restore_engine 的 zip-slip 防护与回归测试、 env_config 的 test_parse_env_to_services_mixed_services - cargo fmt 全量对齐(master 侧代码此前未按 CI 规范格式化) 验证:cargo test 111 通过 / 0 失败,cargo fmt --check 通过
sync-php-dockerfile.mjs / sync-version-manifest.mjs 首行的 #!/usr/bin/env node 在 vitest 解析 ESM 时不会被剥离,导致 scripts/__tests__ 下两个 spec 报 "Invalid or unexpected token" 而无法加载(2 个 suite 全红)。 脚本统一通过 `node scripts/...` 调用(package.json 已封装为 npm scripts), shebang 无实际作用,移除不影响可执行性。 验证:vitest 15 文件 / 176 测试全通过;node scripts/sync-php-dockerfile.mjs --check 正常
恢复逐文件覆盖 .env / docker-compose.yml / services/,中途失败会留下 半恢复状态且无法回退——对一个迁移工具来说是最伤信任的故障模式。 改动: - restore_engine.rs: RestoreResult 新增 rollback_path 字段(Option,缺省不序列化) - commands/backup.rs: 新增 create_rollback_bundle(),在 restore 前把现有配置 打包为 工作区/.restore_rollback_<时间戳>.zip(仅配置文件,不含项目与日志) - 首次恢复(无 .env/compose/services)时跳过,避免产生无意义的空包 - 回滚包生成失败不阻断恢复,只记日志——它是兜底手段而非前置条件 - 致命错误返回时把回滚包路径一并附上,用户至少能回到恢复前 配套测试(commands/backup.rs):空工作区跳过生成 / 生成的是合法非空 ZIP 验证:cargo test 全绿(单元测试 95 → 97)
后端 execute_restore 原本把多行 errors 用 \n 拼进 Err,前端弹一个 3 秒 toast——用户既看不全也来不及看,而 restored_files 明细后端算好了却没展示。 改动: - 后端:execute_restore 改为始终返回 RestoreResult,只有致命错误才走 Err - 前端:RestoreResult 类型 + restoreResult 状态;第 4 步渲染结果卡片 (已恢复文件列表 / 逐条错误明细 / 回滚包路径),成功与部分成功都展示 - 部分失败(success=false)不再静默当成功处理:停在原步骤 + warning toast - 中英文案补齐 restore.result.* / restore.partialSuccess.* / toast.partialSuccess 配套测试(RestorePage.spec.ts): - 成功时渲染文件列表与回滚包路径,不出现错误区块 - 部分失败时逐条渲染 errors,仍给出回滚包路径 验证:vitest 178 通过(新增 2)
start_environment 在 async 命令里直接跑同步子进程调用,会占住 Tauri 的 async 执行线程,同线程上的其它命令(容器列表轮询、镜像探测等)一起被拖慢。 改动: - 新增 run_blocking_command():把 FnOnce() -> Result<T, String> 的同步闭包 丢进 tokio 阻塞线程池,与 commands/backup.rs 的既有做法对齐 - docker compose down 的 Command::output() 改走该助手 - docker compose up 的 Child::wait() 改走该助手 - get_compose_logs 改为 async(内部同样 spawn_blocking)——它在启动流程中 最长 5 分钟内每 2 秒被调用一次,是最持续的线程占用来源 - 顺手修掉上一提交引入的 clippy 警告(archive.len() > 0 → !is_empty()) 未做:commands/backup.rs 里 create_backup 的 spawn_blocking + block_on 双层 嵌套未简化——那是为了绕开 BackupEngine 返回非 Send future 的约束, 改成单层 tokio::spawn 会直接编译失败,收益不抵风险。 配套测试:run_blocking_command 的成功/失败结果原样透传 验证:cargo test 98 通过,cargo clippy 零警告,cargo fmt --check 通过
该文件是另一个会话产生的临时产物(34 行文件路径列表),在合并 commit 的 git add -A 中被一并带入。文件本身保留在磁盘上,仅从版本库移除。
对应 docs/IMPROVEMENT_REPORT.md 第二批: - 第 1 项 R2 恢复前自动备份(回滚包) - 第 2 项 U2 恢复结果展示明细 - 第 3 项 R3 Docker 阻塞调用 spawn_blocking 另补记合并后修复的 scripts 单元测试加载失败问题。
后端原本用 format!("{:?}", c.state) 把 bollard 枚举序列化成 "Some(RUNNING)",
前端再靠 includes('running') 猜回来,后端自己也写注释承认这个格式问题。
Docker 或 bollard 升级即碎,且 "up"/"running" 混判。
改动:
- docker/manager.rs: 新增 ContainerState 枚举(serde rename_all=lowercase),
含 From<Option<ContainerSummaryStateEnum>> 与 Display;EMPTY/None 归 Unknown
- is_running() 只认 running —— restarting 尚未提供服务,不再算运行中
- PsContainer.state 由 String 改为 ContainerState
- 两个 list 方法逐字重复的映射闭包收敛为 to_ps_container(),此前必须双处同步
- check_all_ps_containers_running 与 start_environment 的字符串匹配改为 is_running()
- 前端新增 src/types/docker.ts 承载契约(ContainerState / isContainerRunning /
parseContainerState 兜底),App.vue、portChecker.ts 共用,删除两处重复的
isRunning() 字符串猜测
- 顺带清掉 App.vue 两个 @ts-ignore 的死 computed 与 Container 接口的包装类型
String(A6 遗留项)
配套测试:
- Rust: 枚举序列化/往返/bollard 映射/is_running/Display 共 5 个
- 前端: docker.spec.ts 4 个(含历史 "Some(RUNNING)" 降级不崩)
验证:cargo test --lib 103 通过;vitest 相关 6 通过;vue-tsc 零错误
同一段"开发模式向上爬 4 层 / 生产模式取 exe 父目录"被抄了三份: commands/mod.rs(get_project_root)、commands/workspace.rs(export_logs)、 lib.rs(日志目录)。更关键的是产品问题——workspace.json 与 php-stack.log 写在 exe 同级目录,Windows 用户装进 Program Files 后无写权限,功能直接失效。 改动: - 新增 commands/paths.rs 作为路径的唯一事实来源: app_data_dir() / log_file() / project_root() - app_data_dir 由 setup 用 Tauri 官方 app.path().app_data_dir() 注入一次, 不再自己爬目录;未注入时回退旧逻辑(测试与 setup 之前的调用) - workspace_manager 与 export_logs 改为复用该模块,两处重复逻辑删除 - 新增 migrate_legacy_config():首次启动时把旧位置的 workspace.json 复制 到 app_data_dir,只在目标不存在时复制,失败仅记录不阻断启动 - lib.rs setup 改为官方目录 + 迁移 + 迁移结果进日志 与报告的一处偏差(有意保留): A1 建议把 .user_mirror_config.json / .user_version_overrides.json 一并迁入 app_data_dir,但它们被 backup_engine 按项目根打包、restore_engine 按项目根 还原,且本质随工作区走——迁移会直接打断备份/恢复闭环。故本轮只迁移 workspace.json 与日志,.user_*.json 保持在项目根,并在报告中标注偏差。 配套测试(paths.rs,6 个):日志位于 app_data_dir、project_root 为绝对路径、 迁移复制缺失文件 / 不覆盖已有配置 / 同址空操作 / 无文件时空操作。 迁移逻辑抽成纯函数 migrate_files() 以便单测,避免在用例里改全局 OnceLock。 验证:cargo test --lib 109 通过;cargo clippy 零警告
add_file_to_zip 原本接收 &[u8],调用方一律先 fs::read 全量读入。 用户勾选"包含项目文件"后,单个大文件(数据库 dump、素材)就能让内存 随文件大小线性飙升——而备份恰恰是最可能碰到大文件的路径。 改动: - add_file_to_zip 签名改为 &Path,内部 64KB 分块读取,同一趟既喂 SHA256 也写进 ZIP,不额外多读一遍磁盘 - add_dir_to_zip 同步改为传路径(services/、logs/ 递归同样受益) - 项目文件打包失败改为记入 manifest.errors 后继续,与同循环里其它容错 分支一致(此前读失败容错、写失败却直接 ? 中断整次备份) - 顺带修掉 R7 的一处:path.file_name().unwrap() 对盘符根等无名路径会 panic,改为跳过 + warn compute_sha256(&[u8]) 保留——restore_engine 校验时内容已在内存,仍在用。 配套测试(2 个): - 300KB(跨 5 个 64KB 分块)文件:流式哈希必须等于全量读取哈希,且 ZIP 内字节数一致,防止"哈希对但丢字节" - 缺失文件返回 Err 而非 panic 验证:cargo test --lib 111 通过;cargo clippy 零警告
R6 —— 日志每次启动被 truncate 清空: 用户遇到问题后一重启,现场就没了,"导出日志"随之失去排查价值。 改动(logging.rs): - 新增 rotate_logs():当前日志 → .1 → .2,超出保留份数的丢弃,共留 3 份 - 启动时写入 "===== 启动 <时间> =====" 分隔线,便于区分多次启动的日志段 - 顺带修掉 R7 的一处:LOG_FILE.lock().unwrap() 在锁被毒化后会连锁 panic (任何一次持锁期间 panic 都会让之后每次写日志都 panic),改为 unwrap_or_else(|e| e.into_inner()) 恢复 U4 —— UI 日志与文件日志是两套东西,复制按钮语义混乱: 面板显示最近 50 条,复制的却是后端文件日志全文,两边对不上;且没有清空出口。 改动: - 复制:改为复制面板当前可见日志(所见即所得) - 新增清空:useToast 增加 clearLogs() - 新增导出:dialog.save() 选位置 + 新命令 export_logs_to() 落盘, 走文件日志,与面板显示范围无关 - 复制按钮原先硬编码中文 title,补 i18n(U5 残留项之一) - 中英文案补齐 log.copyTip/clear/clearTip/export/exportTip/cleared/ exported/exportFailed,两侧 key 数仍为 425 完全同步 配套测试: - Rust rotate_logs 4 个(代际顺延 / 超限丢弃 / 空目录空操作 / 内容不丢) - 前端 clearLogs 1 个 验证:cargo test --lib 115 通过;clippy 零警告;vue-tsc 零错误
Docker Desktop 未启动时,5 秒一次的静默轮询仍在 ping + 拉列表 + 写日志, 日志面板被 dockerCheckFailed 每 5 秒刷一条,真正的有用信息被冲掉。 同时 setInterval 永不清除,组件卸载后仍在跑。 改动: - 新增 utils/pollBackoff.ts:正常 5s;连续失败 3 次起 15s,6 次起 30s; 成功后立即回 5s。纯函数,可单测 - setInterval 改为自调度 setTimeout,间隔才能随失败次数变化; onUnmounted 清理定时器与滚动定时器 - 失败日志只在"可用 → 不可用"翻转时记一条,恢复时记一条 dockerRestored,持续不可用期间保持静默 - 容器状态灯的 animate-pulse 改为仅运行中时脉冲——停止的容器一直闪 会让人误以为还在动,也便于和轮询状态区分 配套测试(pollBackoff.spec.ts 4 个):短暂抖动不惩罚 / 3 次起 15s / 6 次起 30s / 间隔随失败次数单调不减 中英文案新增 dashboard.toast.dockerRestored,两侧 key 数 426 同步 验证:vue-tsc 零错误;vitest 相关 4 通过
IMPROVEMENT_REPORT.md:
- 每条建议标题后加 [已完成]/[部分完成]/[未开始] 标记(33 条)
- 第一批 6 项、第二批 8 项表格加状态列并全部标记完成
- 第三批 7 项标记为未开始
- 补四处「实现偏差」说明:
· A1:.user_*.json 未迁至 app_data_dir(会打断备份/恢复闭环);
app_data_dir 用 OnceLock 注入而非透传 AppHandle(避免 20 余处签名膨胀)
· R7:已修 2/3 处,version_manifest 的 expect 属编译期嵌入,保持启动即崩
· A6:容器相关部分随 R4 完成,openServiceConfig 四分支合并未做
· U5:仅完成复制按钮 title,后端日志 i18n 按报告建议明确不做
- 记录 R3 未做的部分:create_backup 的 spawn_blocking + block_on 双层嵌套
CHANGELOG Unreleased:
- 修复段补日志轮转/锁毒化/备份内存/无名路径 panic/轮询刷屏/复制语义 6 项
- 改进段补容器状态枚举契约、路径统一与 app_data_dir 迁移、
日志面板清空与导出、轮询退避、状态灯脉冲 5 项
验证:cargo fmt --check 通过、clippy 零警告、cargo test 115 单元 +
16 集成全通过;vitest 17 文件 187 测试通过;vite build 成功
问题:用户点击拉取中...后整个窗口进入未响应状态。 根因:pull_service_images 注册为同步 Tauri 命令,内部直接执行 (ConfigExtractor::pull_image 调用 Command::status())。 下载 php:8.2-fpm / nginx:1.28-alpine 这类大镜像时,当前线程被阻塞数 分钟;同步命令会占住 Tauri async runtime 的执行线程,前端 await 的 Promise 在结果返回前无法触发任何渲染或事件循环,于是弹窗冻结。 修复: - check_service_images_presence 改为 - pull_service_images 改为 - 两者都把实际调用 Docker CLI 的逻辑包进已有的 助手(spawn_blocking),让 Tauri 主线程/Rust async runtime 保持响应 - 前端无需改动:已经使用 await invoke(...) 调用 验证:cargo test --lib 115 通过;cargo clippy 零警告;vitest 187 通过; vite build 成功
问题:dev 模式下用户已把工作区设为 D:\ps,但所有运行时数据
(.env、docker-compose.yml、services/、data/、logs/、备份包)
仍写进了项目根 E:\study\php-stack。
根因:paths::project_root() 对 workspace.json 里配置的路径只做
path.exists() 校验,不存在就**静默**回退到默认目录。D:\ps 一旦
不存在(盘符卸载 / 目录被删),配置即被架空,而前端显示的仍是
配置值 D:\ps——显示与实际落点不一致,用户无从察觉。
修复:
- 新增 resolve_workspace() 返回 WorkspaceResolution{path, fell_back, reason}:
配置路径不存在时先 create_dir_all 建回来;只有创建失败才回退,
并把原因写进 reason + eprintln
- get_workspace_info 返回结构改为 WorkspaceInfo,同时给出
workspace_path(配置值)与 effective_path(实际落点)+ using_fallback
- 前端环境配置页在 using_fallback 时红字告警,写明数据实际写到了哪里
- 中英文案新增 workspace.status.fallback,两侧 key 427 同步
未做:项目根已误写的数据不自动搬迁——属用户数据,需其确认目标后再动。
验证:cargo test --lib 116 通过;clippy 零警告;vue-tsc 零错误;
vitest 187 通过
zh-CN.json / en.json 的 envConfig 下存在两个同名 toast 节点,JSON.parse 静默保留后者,导致 applySuccess / backedUp / startSuccess 等 8 条文案被 pull* 三条整体覆盖,t() 查不到 key 时界面直接显示 'envConfig.toast.applySuccess'。 - 合并两个 toast 节点,恢复被覆盖的 8 条文案(中英同步) - 新增 scripts/check-i18n-keys.mjs + npm script check:i18n: 同层级重复 key / 中英 key 集合一致性 / 源码引用 key 存在性三类检查 - 新增 10 条单元测试覆盖扫描器与真实语言包契约
官方 nginx 镜像的 entrypoint 需要 root:创建 /var/cache/nginx/*_temp 目录、 改写 conf.d/default.conf、bind 80 端口。以 nginx 身份运行 master 会直接报 [emerg] mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied), 容器在 restart: always 下无限重启。 改为 root 运行 master,worker 进程仍由 nginx.conf 的 `user nginx;` 决定, PUID/PGID 映射语义不变(写日志、写缓存的是 nginx 用户)。 同步修改 5 个 nginx 版本模板(1.24-1.28)。
1. 映射条件写反:原判断是「PUID/PGID 不等于 1000 才调整」,而默认就是 1000, 等于默认配置下永远不执行。改为「与镜像内当前 UID/GID 不一致才调整」。 2. 只认 alpine:Nginx 模板用 deluser/adduser,换成 debian 版基础镜像 (如 nginx:1.28)构建直接 deluser: not found 失败;PHP 模板反之只用 groupmod/usermod,换 alpine 版同样失败。现在按基础镜像分发命令。 另修 alpine 下 deluser 会连带删除同名组、随后 delgroup 报 unknown group 导致 set -e 中断的问题。 覆盖 nginx 1.24-1.28 与 php 5.6-8.5 共 13 个模板。 验证:alpine(nginx:1.28-alpine) 与 debian(nginx:1.28) 均构建成功且 uid/gid=1000;php:8.2-fpm 与 php:8.2-fpm-alpine 两条分支均验证通过。
工作区回退告警提升到 App 级,避免仅在环境配置页可见;execute_restore 致命失败也返回 RestoreResult,结果面板可一键载入回滚包。 Co-authored-by: Cursor <cursoragent@cursor.com>
不再静默 create_dir_all,避免盘符卸载或路径拼写错误时建出空环境;新增 path_missing 与 recreate_workspace_dir。 Co-authored-by: Cursor <cursoragent@cursor.com>
抽 validate_archive_entry_names,preview/verify/restore 共用;恶意包在预览即可暴露,不必等到执行恢复。 Co-authored-by: Cursor <cursoragent@cursor.com>
前端按 tag 串行调用 pull,UI 展示「正在拉取 xxx(n/m)」;全部失败时确认后再用 fallback 模板 apply。 Co-authored-by: Cursor <cursoragent@cursor.com>
确认框默认文案走 vue-i18n;侧边栏折叠 title 双语;WorkspaceInitDialog 适配明暗主题并以事件刷新替代 reload;UI 日志上限 50→200 并提示导出完整日志。 Co-authored-by: Cursor <cursoragent@cursor.com>
同主版本 1.x 接受;过高提示升级应用,过低提示格式弃用。避免未来格式升级时静默丢字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
去掉 PHP/MySQL/Redis/Nginx 的魔数切片前缀反解,统一走 collect_services_from_manifest,降低新增版本时 off-by-one 风险。 Co-authored-by: Cursor <cursoragent@cursor.com>
四个服务分支逻辑相同,统一为去掉 ps- 前缀;参数类型改为 string。 Co-authored-by: Cursor <cursoragent@cursor.com>
读取 manifest.services 端口映射,用本机绑定探测占用;冲突仅作预览提示,不阻断恢复、不自动改写配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
统一 normalizeError/invokeCommand,避免 e as string 变成 [object Object];BackupPage 与 RestorePage 改为走 src/api/backup。 Co-authored-by: Cursor <cursoragent@cursor.com>
新增 workspace/docker/envConfig/mirror 封装;App、EnvConfig、Mirror、Software、Workspace 对话框与 portChecker 不再直接 invoke。 Co-authored-by: Cursor <cursoragent@cursor.com>
A3 迁移后本地处理函数遮蔽了 api 导出,点击打开配置会无限递归。改为 openServiceConfigApi 别名调用。 Co-authored-by: Cursor <cursoragent@cursor.com>
去掉 EnvConfig/App/Theme/portChecker 调试输出;侧边栏导航改为 button 以支持 Tab/Enter。 Co-authored-by: Cursor <cursoragent@cursor.com>
启动时优先加载应用数据目录下 services/version_manifest.json,解析失败回退内置清单;用户无需为新增版本重新发版。 Co-authored-by: Cursor <cursoragent@cursor.com>
VersionManifest 未实现 Debug,改用 is_err() 断言。 Co-authored-by: Cursor <cursoragent@cursor.com>
配置最小 CSP;ARCHITECTURE 补应用写入表;EXTENSION_GUIDE 补服务类型 checklist 与外部清单说明;设置页增加 GitHub Releases 检查更新;同步 IMPROVEMENT_REPORT/AGENTS 已完成项。 Co-authored-by: Cursor <cursoragent@cursor.com>
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.
No description provided.