Files
happy-life-star/docs/superpowers/specs/2026-06-02-deploy-nginx-site-enable-fix-design.md
T

191 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
author: 部署脚本修复会话
created_at: 2026-06-02
purpose: 修复 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)`
签名:
```python
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()`(不修改底层),只是用 Python `if/elif/else` 在函数内串起来
### 变量安全校验
`enable_nginx_site(remote_conf, domain)` 函数入口处对 `domain` 做基本校验,防止变量污染导致误操作:
```python
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,不在网络慢)
## 验证计划
### 单元级(脚本内)
部署后通过以下方式验证:
1. **干净服务器场景**:远程 `/etc/nginx/sites-enabled/{domain}.conf` 不存在 → 期望成功
2. **断链场景**:手动 `ln -s /nonexistent /etc/nginx/sites-enabled/{domain}.conf` → 期望步骤 2 清理 + 步骤 3 重建
3. **目录场景**:手动 `mkdir /etc/nginx/sites-enabled/{domain}.conf` → 期望步骤 2 `rm -rf` 清理 + 步骤 3 成功
4. **文件场景**:手动 `touch /etc/nginx/sites-enabled/{domain}.conf` → 期望步骤 2 `rm -f` 清理 + 步骤 3 成功
5. **正常 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 "验证规则(强制)")。
## 风险与回滚
### 风险
1. **远程 rm -rf 误删**:仅针对 `/etc/nginx/sites-enabled/{domain}.conf` 单个路径(已通过 `{domain}` 限定),不会影响其他站点
2. **步骤 2 误判**:如果 `ls -ld` 输出解析错误,错误地删了文件 → 步骤 3 重新创建,整体仍自愈
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` — 本设计文档