8.5 KiB
author, created_at, purpose
| author | created_at | purpose |
|---|---|---|
| 部署脚本修复会话 | 2026-06-02 | 修复 deploy.py 的 nginx 站点启用命令 30s 超时问题(防御性诊断 + 清理) |
修复 deploy.py nginx 站点启用超时 — 设计文档
问题陈述
执行 python deploy.py 部署时,nginx 站点启用步骤失败:
[INFO] 启用站点配置...
[ERROR] 启用站点失败: 命令执行超时 (30s):
ssh ... root@101.200.208.45
ln -snf /etc/nginx/sites-available/lifescript.happylifeos.com.conf
/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf
&& rm -f /etc/nginx/sites-enabled/default
根因分析(用户已确认):之前能成功部署,本次突然失败。ln -snf 目标在远程服务器上已存在为目录或断链(非空 symlink 指向已被删除/修改的源文件),ln 在某些情况下会递归进入等待而 hang。&& 链式让 rm -f default 也永不执行。30s 总超时被耗尽。
方案选择
经过 brainstorming 对话,用户选择 方案 A:防御性诊断 + 清理(修改脚本使其对异常状态自愈,不依赖手动干预)。
未选方案:
- 方案 B(仅手动清理):不解决根本问题,未来重现概率高
- 方案 C(手动 + 脚本加固):用户已先用方案 A 的脚本修复,验证后再决定是否需要手动清理
- 方案 D(仅增超时):绕过问题,不修复根因
设计
核心思路
将 ln -snf ... && rm -f ... 的链式单命令拆为多步独立 SSH 调用,每步:
- 独立超时(30s 对单步足够)
- 独立返回
ok/err/stderr - 失败立即停止并打印详细诊断
- 前置诊断 + 主动清理 hang 源(目录/断链)
新增函数 enable_nginx_site(remote_conf, domain)
签名:
def enable_nginx_site(remote_conf: str, domain: str) -> bool:
"""幂等地启用 nginx 站点(sites-enabled symlink)。
流程:
1. 诊断目标路径当前状态
2. 清理异常状态(目录 / 文件 / 断链)
3. 创建 symlink
4. 移除 default 站点
5. 验证结果
Returns: True 成功;False 失败(stderr 已打印)
"""
5 步骤流程
| 步骤 | SSH 命令 | 失败处理 | 失败时是否继续 |
|---|---|---|---|
| 1. 诊断 | test -e <target> && (test -L <target> && echo "symlink" || (test -d <target> && echo "dir" || echo "file")) || echo "missing" |
仅诊断,不失败 | — |
| 2. 清理 | case 诊断结果 in symlink) unlink <target> ;; dir) rm -rf <target> ;; file) rm -f <target> ;; missing) ;; esac |
失败时打印 stderr + 提示"权限可能不足" | 否(清理失败则终止,不重建链) |
| 3. 建链 | ln -snf {remote_conf} /etc/nginx/sites-enabled/{domain}.conf |
失败时提示"scp_file 是否成功" | — |
| 4. 清默认 | rm -f /etc/nginx/sites-enabled/default |
失败时提示"default 文件被占用或权限不足" | — |
| 5. 验证 | readlink /etc/nginx/sites-enabled/{domain}.conf && ls -la /etc/nginx/sites-enabled/ |
失败时打印实际值与期望值 | — |
关键决策:
- 步骤 2 用
test -e/-L/-d/-f替代ls -ld解析:shell 内置 test 比解析ls输出更可靠,跨 Unix 工具版本差异小 - 步骤 2 失败则短路终止:清理失败时建链毫无意义(脏状态仍在),立即返回 False 让用户介入
- 有效 symlink 走
unlink而非rm -f:与目录/文件区分对待,避免误删有效链接 - 缺失(missing)跳过清理:直接进建链步骤,是最常见的健康场景
超时设置:每步独立调用 ssh_command(cmd, timeout=30),与现有 ssh_command() 默认值一致。不修改 ssh_command() / run_ssh_args() 底层,也不需要新增超时参数。30s 对单步 SSH 命令(不涉及大文件传输)足够;如未来出现网络慢问题,可在调用处单独覆盖(如 ssh_command(cmd, timeout=60))。
集成位置
修改 G:\IdeaProjects\emotion-museun\deploy.py:
- 第 250-256 行:替换
ln -snf ... && rm -f ...块为enable_nginx_site(remote_conf, DOMAIN)调用 - 第 232-266 行(
deploy_nginx函数)保持其他逻辑不变(scp_file 上传 conf、nginx -t、reload) - 新增函数
enable_nginx_site放在deploy_nginx之前 - 实施方式:5 步都是直接调用现有
ssh_command()(不修改底层),只是用 Pythonif/elif/else在函数内串起来
变量安全校验
enable_nginx_site(remote_conf, domain) 函数入口处对 domain 做基本校验,防止变量污染导致误操作:
import re
if not domain or not re.match(r'^[a-zA-Z0-9.-]+$', domain):
log_error(f"非法 domain 名称: {domain!r}")
return False
校验规则:
- 非空
- 仅包含字母、数字、点、连字符
- 不含
/、*、;、&、|、$、空格等 shell 特殊字符
remote_conf 来源固定(f-string 拼接 f"/etc/nginx/sites-available/{DOMAIN}.conf"),与 domain 一同校验。
错误信息改进
enable_nginx_site 内部用统一的 log_error 输出结构化信息,使用描述性名称而非"步骤 N":
[ERROR] 启用站点失败 - 建链步骤: Permission denied
[ERROR] 上下文: 目标 = /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf
[ERROR] 提示: 确认 scp_file 是否成功上传了 sites-available 下的 conf 文件
各步骤使用的描述性名称:
- 诊断步骤
- 清理步骤
- 建链步骤
- 清默认步骤
- 验证步骤
替代当前的"命令执行超时 (30s)",便于快速定位卡在哪一步。
范围
包含
- 修改
deploy.py的deploy_nginx()函数调用方式 - 新增
enable_nginx_site()函数(约 50-70 行) - 错误信息结构化改进
不包含(明确排除)
- 不修改其他 deploy.py 子脚本(backend-single/web/web-admin/life-script)
- 不改
ssh_command()/run_ssh_args()底层 - 不增加
--dry-run模式 - 不改 nginx 配置文件本身
- 不改默认 30s 超时(单步足够,问题在链式 hang,不在网络慢)
验证计划
单元级(脚本内)
部署后通过以下方式验证:
- 干净服务器场景:远程
/etc/nginx/sites-enabled/{domain}.conf不存在 → 期望成功 - 断链场景:手动
ln -s /nonexistent /etc/nginx/sites-enabled/{domain}.conf→ 期望步骤 2 清理 + 步骤 3 重建 - 目录场景:手动
mkdir /etc/nginx/sites-enabled/{domain}.conf→ 期望步骤 2rm -rf清理 + 步骤 3 成功 - 文件场景:手动
touch /etc/nginx/sites-enabled/{domain}.conf→ 期望步骤 2rm -f清理 + 步骤 3 成功 - 正常 symlink 场景:手动重建有效 symlink → 期望步骤 2 unlink + 步骤 3 重建
集成级
跑 python deploy.py nginx 完整流程,期望:
- 不再出现"启用站点失败"
/etc/nginx/sites-enabled/{domain}.conf是有效 symlink/etc/nginx/sites-enabled/default不存在nginx -t通过systemctl reload nginx成功
验证前置
修复后必须执行 python deploy.py nginx(或 python deploy.py all)进行端到端验证,不能仅靠代码 review。这是用户在前次 PATH 修复中明确要求的规则(CLAUDE.md "验证规则(强制)")。
风险与回滚
风险
- 远程 rm -rf 误删:仅针对
/etc/nginx/sites-enabled/{domain}.conf单个路径(已通过{domain}限定),不会影响其他站点 - 步骤 2 误判:如果
ls -ld输出解析错误,错误地删了文件 → 步骤 3 重新创建,整体仍自愈 - SSH 连接问题掩盖:如果 SSH 本身不通,新代码会立即在步骤 1 报告 stderr,不会比原代码更差
回滚
- 改动局限在
deploy.py单文件、新增 1 个函数 - 如新代码有问题,
git checkout deploy.py即可恢复
后续可选增强(不在本次范围)
enable_nginx_site通用化到disable_nginx_site(),支持deploy.py nginx disable <domain>- 把
enable_nginx_site提取到独立模块(deploy_nginx_helpers.py),便于其他 deploy.py 复用 - 增加
--dry-run模式,仅打印将执行命令不实际执行 - 步骤级重试机制(如步骤 3 失败时自动重试 1 次)
相关文件
G:\IdeaProjects\emotion-museun\deploy.py— 主修改目标(第 232-266 行deploy_nginx,新增enable_nginx_site)G:\IdeaProjects\emotion-museun\conf\emotion-museum.conf— nginx 配置文件(不修改)G:\IdeaProjects\emotion-museun\docs\superpowers\specs\2026-06-02-deploy-nginx-site-enable-fix-design.md— 本设计文档