191 lines
8.5 KiB
Markdown
191 lines
8.5 KiB
Markdown
---
|
||
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` — 本设计文档
|