From 265199a48ead759867ac7d53152365324f67c13e Mon Sep 17 00:00:00 2001 From: shiyue Date: Sun, 23 Aug 2026 12:59:32 +0800 Subject: [PATCH] fix: fallback to CentOS 7 single executable on start failure --- README.md | 10 +- findings.md | 8 ++ progress.md | 7 ++ start.sh | 282 ++++++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 278 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8b36e86..f1b10ef 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,8 @@ cd cc-web - 自动安装 PM2 - 使用 `npm ci` 或 `npm install` 安装依赖 - 以 `ccweb` 为默认 PM2 应用名启动或重启服务 +- 启动前自动释放配置端口(默认 `8002`) +- 在 CentOS 7 上如果源码方式启动失败,自动回退到 `dist-exe/bun-linux-x64-baseline/cc-web` 临时前台运行: @@ -211,9 +213,15 @@ dist-exe/bun-linux-x64-baseline/ ```bash cd /opt/cc-web chmod +x cc-web -PORT=8002 CC_WEB_PASSWORD='请改成强密码' ./cc-web +PORT=8002 ./cc-web ``` +如果项目根目录同时包含 `start.sh` 和 `dist-exe/`,直接执行 `./start.sh` 即可。 +脚本默认先按原有 Node/PM2 方式启动;仅当源码方式启动失败且系统识别为 CentOS 7 +时,才自动切换到 `dist-exe/bun-linux-x64-baseline/cc-web`。启动前会先释放监听端口, +默认端口为 `8002`。已有 `config/auth.json` 时会直接复用,不会强制要求重新设置 +`CC_WEB_PASSWORD`。 + 这个发布包只包含 cc-web 服务本体和前端资源,**不会把 Claude/Codex CLI 打进包里**。 运行时仍调用宿主机上的 CLI: diff --git a/findings.md b/findings.md index d95cc54..236c022 100644 --- a/findings.md +++ b/findings.md @@ -80,3 +80,11 @@ - 2026-08-23 重建成功:目录二进制 SHA-256 为 `28f2c2b99ef66cb523feef171c8c2438071e724629fed44f348734b982d4a4e7`;tar.gz SHA-256 为 `d8a9de5813a6f4c8b27f05565926faa9267e9070df4241387568169d97ce5cbe`。 - `tar -xOf` 提取归档内的 `bun-linux-x64-baseline/cc-web` 后,其 SHA-256 与目录二进制完全一致;包内 `package.json` 版本为 `1.2.11`。 - 从 `./dist-exe/bun-linux-x64-baseline/cc-web` 直接启动本机等价验证通过:端口 18082 可访问,启动日志包含 `CC-Web server listening`,运行进程 `/proc//exe` 指向该发布目录且摘要一致。 + +## 2026-08-23 start.sh 启动回退修复 + +- `start.sh` 默认仍走原有 Node/PM2 源码启动;只在 `CC_WEB_START_MODE=auto`、源码启动失败且系统识别为 CentOS 7 时回退。 +- 启动前会释放解析出的监听端口,默认端口为 8002;源码路径和 single-exe 回退路径都会再次确认端口已释放。 +- single-exe 通过 `CC_WEB_APP_DIR` 指向项目根目录,复用根目录 `config/auth.json`、sessions/logs 和 `.env`,不会强制设置 `CC_WEB_PASSWORD`。 +- 回退前会比较归档内二进制 SHA-256,发现 `dist-exe/bun-linux-x64-baseline/cc-web` 过旧时自动从 `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz` 刷新。 +- 回归验证覆盖:源码/PM2 端口 18084 启动、single-exe 复用 auth.json、占用端口自动关闭后 single-exe 启动;均通过。 diff --git a/progress.md b/progress.md index 547518d..fda2689 100644 --- a/progress.md +++ b/progress.md @@ -29,3 +29,10 @@ - 已清理临时 `CentOS7重新打包 TO DO list.csv`;目标机部署仍需将新归档解压到目标目录并停止旧进程后启动新目录二进制。 - 发布技能规定的 `node --check` 六项检查、发布二进制 MCP `initialize` smoke 和 tar 清单检查均通过。 - 当前工作区仅发布归档、规划发现与进度日志有变更;未修改业务源码或构建脚本。 + +## 2026-08-23 start.sh 启动回退修复 + +- 保留原有 Node/PM2 为默认启动方式;新增启动前端口清理,默认处理 8002。 +- 新增 CentOS 7 自动回退:源码启动失败时自动校验/刷新 `dist-exe` baseline 单文件并用 nohup 启动。 +- 回退通过 `CC_WEB_APP_DIR` 复用现有 `config/auth.json`,未要求重新配置 `CC_WEB_PASSWORD`。 +- 已完成 shell 语法检查、源码 PM2 启动回归、single-exe auth 回归和占用端口清理回归。 diff --git a/start.sh b/start.sh index a256bcd..c6640ed 100755 --- a/start.sh +++ b/start.sh @@ -9,6 +9,12 @@ APP_DIR="${SCRIPT_DIR}" ENTRY_PATH="${APP_DIR}/${ENTRY_FILE}" ENV_FILE="${APP_DIR}/.env" ENV_EXAMPLE_FILE="${APP_DIR}/.env.example" +START_MODE="${CC_WEB_START_MODE:-auto}" +RELEASE_TARGET="bun-linux-x64-baseline" +RELEASE_DIR="${CC_WEB_RELEASE_DIR:-${APP_DIR}/dist-exe/${RELEASE_TARGET}}" +RELEASE_ENTRY_PATH="${RELEASE_DIR}/cc-web" +RELEASE_ARCHIVE="${CC_WEB_RELEASE_ARCHIVE:-${APP_DIR}/dist-exe/cc-web-${RELEASE_TARGET}.tar.gz}" +PORT_VALUE="" log() { printf '[cc-web pm2] %s\n' "$*" @@ -23,6 +29,84 @@ warn() { printf '[cc-web pm2] WARNING: %s\n' "$*" >&2 } +is_centos7() { + local release_file + for release_file in /etc/centos-release /etc/redhat-release; do + if [[ -r "${release_file}" ]] && grep -Eqi 'centos.*release[[:space:]]+7' "${release_file}"; then + return 0 + fi + done + return 1 +} + +resolve_port() { + local configured="${PORT:-$(get_env_value PORT)}" + if [[ ! "${configured}" =~ ^[0-9]+$ ]] || ((configured < 1 || configured > 65535)); then + configured=8002 + fi + printf '%s\n' "${configured}" +} + +port_is_open() { + local port="$1" + ( : >/dev/tcp/127.0.0.1/"${port}" ) 2>/dev/null +} + +wait_for_port() { + local port="$1" + local attempts="${2:-20}" + local index + for ((index = 0; index < attempts; index += 1)); do + if port_is_open "${port}"; then + return 0 + fi + sleep 0.5 + done + return 1 +} + +stop_port_listener() { + local port="$1" + local pids="" + + if ! port_is_open "${port}"; then + log "端口 ${port} 当前未被占用" + return 0 + fi + + if command -v fuser >/dev/null 2>&1; then + pids="$(fuser -n tcp "${port}" 2>/dev/null || true)" + log "关闭端口 ${port},占用进程: ${pids:-未知}" + fuser -k -TERM -n tcp "${port}" >/dev/null 2>&1 || true + elif command -v lsof >/dev/null 2>&1; then + pids="$(lsof -t -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)" + log "关闭端口 ${port},占用进程: ${pids:-未知}" + for pid in ${pids}; do + kill -TERM "${pid}" 2>/dev/null || true + done + else + warn "端口 ${port} 被占用,但系统没有 fuser/lsof,无法自动关闭" + return 1 + fi + + for _ in {1..10}; do + if ! port_is_open "${port}"; then + return 0 + fi + sleep 0.3 + done + + warn "端口 ${port} 未能在宽限期内释放,尝试强制关闭" + if command -v fuser >/dev/null 2>&1; then + fuser -k -KILL -n tcp "${port}" >/dev/null 2>&1 || true + else + for pid in ${pids}; do + kill -KILL "${pid}" 2>/dev/null || true + done + fi + return 0 +} + node_upgrade_hint() { cat <<'EOF' 可选处理方式: @@ -42,12 +126,11 @@ node_upgrade_hint() { node -v # CentOS 7 / glibc 2.17 不适合安装 NodeSource Node.js 22。 - # 推荐在较新的构建机生成 Bun baseline 单文件发布包,再拷贝到 CentOS 7 直接运行: + # 推荐在较新的构建机生成 Bun baseline 单文件发布包,再拷贝到 CentOS 7: npm install npm run build:single-exe - # 拷贝 dist-exe/bun-linux-x64-baseline/ 到 CentOS 7 后: - cd dist-exe/bun-linux-x64-baseline - PORT=8002 CC_WEB_PASSWORD='请改成强密码' ./cc-web + # 在项目根目录执行 ./start.sh 时,会自动优先尝试源码;源码失败后自动回退到: + # dist-exe/bun-linux-x64-baseline/cc-web EOF } @@ -260,12 +343,14 @@ install_pm2() { if command -v sudo >/dev/null 2>&1; then log "普通 npm 全局安装失败,尝试使用 sudo 安装 PM2..." - sudo npm install -g pm2 - log "PM2 安装完成" - return + if sudo npm install -g pm2; then + log "PM2 安装完成" + return + fi fi - fail "PM2 自动安装失败,且当前环境没有 sudo。请先手动安装: npm install -g pm2" + warn "PM2 自动安装失败" + return 1 } install_dependencies() { @@ -273,11 +358,18 @@ install_dependencies() { if [[ -f package-lock.json ]]; then log "安装项目依赖: npm ci" - npm ci + if npm ci; then + return 0 + fi else log "安装项目依赖: npm install" - npm install + if npm install; then + return 0 + fi fi + + warn "项目依赖安装失败" + return 1 } start_or_restart_app() { @@ -285,17 +377,140 @@ start_or_restart_app() { if pm2 describe "${APP_NAME}" >/dev/null 2>&1; then log "PM2 应用已存在,执行重启: ${APP_NAME}" - pm2 restart "${APP_NAME}" --update-env + if ! pm2 restart "${APP_NAME}" --update-env; then + warn "PM2 重启失败: ${APP_NAME}" + return 1 + fi else log "启动 PM2 应用: ${APP_NAME}" - pm2 start "${ENTRY_PATH}" --name "${APP_NAME}" --cwd "${APP_DIR}" + if ! pm2 start "${ENTRY_PATH}" --name "${APP_NAME}" --cwd "${APP_DIR}"; then + warn "PM2 启动失败: ${APP_NAME}" + return 1 + fi fi log "保存 PM2 进程快照" - pm2 save + if ! pm2 save; then + warn "PM2 进程快照保存失败" + return 1 + fi log "当前 PM2 状态" - pm2 status "${APP_NAME}" + if ! pm2 status "${APP_NAME}"; then + warn "PM2 状态检查失败: ${APP_NAME}" + return 1 + fi + + if ! wait_for_port "${PORT_VALUE}" 20; then + warn "源码服务未在端口 ${PORT_VALUE} 上监听" + return 1 + fi +} + +stop_existing_pm2_app() { + if command -v pm2 >/dev/null 2>&1 && pm2 describe "${APP_NAME}" >/dev/null 2>&1; then + log "停止失败的 PM2 应用: ${APP_NAME}" + pm2 delete "${APP_NAME}" >/dev/null 2>&1 || true + fi +} + +refresh_release_from_archive() { + [[ -f "${RELEASE_ARCHIVE}" ]] || return 0 + + mkdir -p "${APP_DIR}/dist-exe" + + if [[ -x "${RELEASE_ENTRY_PATH}" ]] && command -v sha256sum >/dev/null 2>&1; then + local current_hash archive_hash + current_hash="$(sha256sum "${RELEASE_ENTRY_PATH}" | awk '{print $1}')" + archive_hash="$(tar -xOf "${RELEASE_ARCHIVE}" "${RELEASE_TARGET}/cc-web" 2>/dev/null | sha256sum | awk '{print $1}')" + if [[ -n "${archive_hash}" && "${current_hash}" == "${archive_hash}" ]]; then + return 0 + fi + log "检测到 single-exe 发布目录不是归档中的最新版本,自动刷新" + else + log "发布目录缺少 single-exe,自动从归档解压" + fi + + if ! tar -xzf "${RELEASE_ARCHIVE}" -C "${APP_DIR}/dist-exe"; then + warn "无法解压发布归档: ${RELEASE_ARCHIVE}" + return 1 + fi + chmod +x "${RELEASE_ENTRY_PATH}" 2>/dev/null || true +} + +ensure_release_binary() { + refresh_release_from_archive || return 1 + if [[ ! -f "${RELEASE_ENTRY_PATH}" ]]; then + warn "找不到 CentOS 7 single-exe: ${RELEASE_ENTRY_PATH}" + return 1 + fi + chmod +x "${RELEASE_ENTRY_PATH}" 2>/dev/null || true + [[ -x "${RELEASE_ENTRY_PATH}" ]] +} + +start_single_exe() { + ensure_release_binary || return 1 + + stop_port_listener "${PORT_VALUE}" || true + + local runtime_log_dir="${CC_WEB_LOGS_DIR:-${RELEASE_DIR}/logs}" + local output_log="${runtime_log_dir}/cc-web-single-exe.out" + local pid_file="${runtime_log_dir}/cc-web-single-exe.pid" + mkdir -p "${runtime_log_dir}" + + log "启动 CentOS 7 single-exe: ${RELEASE_ENTRY_PATH}" + ( + cd "${RELEASE_DIR}" + exec nohup env \ + CC_WEB_APP_DIR="${APP_DIR}" \ + CC_WEB_PUBLIC_DIR="${RELEASE_DIR}/public" \ + PORT="${PORT_VALUE}" \ + "${RELEASE_ENTRY_PATH}" + ) >"${output_log}" 2>&1 & + local pid=$! + printf '%s\n' "${pid}" > "${pid_file}" + + if ! wait_for_port "${PORT_VALUE}" 20; then + warn "single-exe 未能在端口 ${PORT_VALUE} 上监听,详见: ${output_log}" + kill "${pid}" 2>/dev/null || true + wait "${pid}" 2>/dev/null || true + return 1 + fi + + log "single-exe 已启动,PID=${pid},日志: ${output_log}" + return 0 +} + +try_source_start() { + if [[ ! -f "${ENTRY_PATH}" ]]; then + warn "找不到源码入口: ${ENTRY_PATH}" + return 1 + fi + + if ! command -v node >/dev/null 2>&1; then + warn "未检测到 Node.js,跳过源码启动" + return 1 + fi + if ! command -v npm >/dev/null 2>&1; then + warn "未检测到 npm,跳过源码启动" + return 1 + fi + + local node_major + if ! node_major="$(node -p "Number(process.versions.node.split('.')[0])")"; then + warn "Node.js 无法执行,跳过源码启动" + return 1 + fi + if [[ "${node_major}" -lt 18 ]]; then + warn "Node.js 版本过低($(node -v)),跳过源码启动" + return 1 + fi + + check_runtime_config + install_pm2 || return 1 + install_dependencies || return 1 + start_or_restart_app || return 1 + return 0 } main() { @@ -303,27 +518,38 @@ main() { warn "当前正在使用 root 用户执行。该服务会启动 Claude/Codex 子进程,root 运行风险较高,建议改用非 root 用户。" fi - [[ -f "${ENTRY_PATH}" ]] || fail "找不到入口文件: ${ENTRY_PATH}" + PORT_VALUE="$(resolve_port)" + stop_port_listener "${PORT_VALUE}" || true - ensure_command node "请先安装 Node.js 18 或更高版本。 + case "${START_MODE}" in + single|release|bun) + start_single_exe || fail "single-exe 启动失败,请检查 ${RELEASE_ENTRY_PATH}" + log "完成。当前使用 CentOS 7 single-exe。" + return + ;; + auto|source|pm2) + ;; + *) + fail "不支持的 CC_WEB_START_MODE: ${START_MODE}(可选 auto/source/single)" + ;; + esac -$(node_upgrade_hint)" - ensure_command npm "请先安装 npm。" + if try_source_start; then + log "完成。若需要开机自启,请执行 pm2 startup,并按输出提示执行生成的命令。" + return + fi - local node_major - node_major="$(node -p "Number(process.versions.node.split('.')[0])")" - if [[ "${node_major}" -lt 18 ]]; then - fail "Node.js 版本过低,当前为 $(node -v),要求 >= 18。 + if [[ "${START_MODE}" != "auto" ]] || ! is_centos7; then + fail "源码方式启动失败。Node.js 18+、npm、PM2、依赖和端口 ${PORT_VALUE} 均需可用。 $(node_upgrade_hint)" fi - check_runtime_config - install_pm2 - install_dependencies - start_or_restart_app - - log "完成。若需要开机自启,请执行 pm2 startup,并按输出提示执行生成的命令。" + warn "源码方式启动失败,检测到 CentOS 7,回退到 dist-exe single-exe" + stop_existing_pm2_app + stop_port_listener "${PORT_VALUE}" || true + start_single_exe || fail "CentOS 7 single-exe 启动失败,请检查 ${RELEASE_ENTRY_PATH} 及日志" + log "完成。当前使用 CentOS 7 single-exe。" } main "$@"