diff --git a/scripts/deploy.py b/scripts/deploy.py index 9529bae..08347d4 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -3,12 +3,17 @@ from __future__ import annotations +import argparse +import json import os import platform import secrets import shutil import subprocess import sys +import time +import urllib.error +import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parent.parent @@ -183,7 +188,96 @@ def smoke_test(py_exe: str) -> None: _ok("应用启动检查通过") -def print_summary() -> None: +def ensure_logs_dir() -> None: + logs = ROOT / "logs" + logs.mkdir(parents=True, exist_ok=True) + + +def pm2_has_app(name: str = "nav-site") -> bool: + pm2 = shutil.which("pm2") + if not pm2: + return False + try: + proc = subprocess.run( + [pm2, "jlist"], + check=True, + capture_output=True, + text=True, + ) + apps = json.loads(proc.stdout or "[]") + return any(app.get("name") == name for app in apps) + except (subprocess.CalledProcessError, json.JSONDecodeError, OSError): + return False + + +def systemd_unit_ready(name: str = "nav-site") -> bool: + systemctl = shutil.which("systemctl") + if not systemctl: + return False + try: + proc = subprocess.run( + [systemctl, "list-unit-files", f"{name}.service", "--no-pager", "--no-legend"], + check=True, + capture_output=True, + text=True, + ) + return bool((proc.stdout or "").strip()) + except (subprocess.CalledProcessError, OSError): + return False + + +def wait_for_http(port: str, timeout: float = 15.0) -> bool: + url = f"http://127.0.0.1:{port}/login" + deadline = time.time() + timeout + while time.time() < deadline: + try: + with urllib.request.urlopen(url, timeout=2) as resp: + if resp.status == 200: + return True + except (urllib.error.URLError, TimeoutError, OSError): + time.sleep(0.8) + return False + + +def start_service(port: str) -> str | None: + """尝试自动启动服务,返回启动方式描述;失败返回 None。""" + ensure_logs_dir() + pm2 = shutil.which("pm2") + ecosystem = ROOT / "ecosystem.config.cjs" + + if pm2 and ecosystem.is_file(): + if pm2_has_app("nav-site"): + subprocess.run( + [pm2, "restart", "nav-site", "--update-env"], + check=True, + cwd=ROOT, + ) + mode = "pm2 restart nav-site" + else: + subprocess.run( + [pm2, "start", str(ecosystem)], + check=True, + cwd=ROOT, + ) + mode = "pm2 start ecosystem.config.cjs" + subprocess.run([pm2, "save"], cwd=ROOT, check=False) + if wait_for_http(port): + return mode + _warn("PM2 已启动进程,但健康检查未通过,请执行 pm2 logs nav-site 排查") + return mode + + systemctl = shutil.which("systemctl") + if systemctl and systemd_unit_ready("nav-site"): + subprocess.run([systemctl, "enable", "--now", "nav-site"], check=True) + if wait_for_http(port): + return "systemctl enable --now nav-site" + _warn("systemd 已启动 nav-site,但健康检查未通过,请执行 journalctl -u nav-site 排查") + return "systemctl enable --now nav-site" + + return None + + +def print_summary(started_via: str | None = None) -> None: port = os.environ.get("NAV_PORT", "5070") print() print("=" * 50) @@ -192,16 +286,33 @@ def print_summary() -> None: print(" 默认账号: admin / admin123") print(" 生产环境请尽快在「系统设置」中修改密码") print() - print("启动方式(任选其一):") - if platform.system() == "Windows": - print(f" {venv_python()} app.py") + if started_via: + print(f" 服务状态: 已自动启动({started_via})") + if shutil.which("pm2"): + print(" 查看状态: pm2 status") + print(" 查看日志: pm2 logs nav-site") + print(" 开机自启: pm2 save && pm2 startup # 按提示执行一次 sudo 命令") else: - print(f" {venv_python()} app.py") - print(" pm2 start ecosystem.config.cjs") + print(" 服务状态: 未自动启动(未检测到 pm2 / systemd nav-site)") + print(" 手动启动:") + print(f" {venv_python()} app.py") + if shutil.which("pm2"): + print(" pm2 start ecosystem.config.cjs") print("=" * 50) +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="LocalNav 项目内部署") + parser.add_argument( + "--no-start", + action="store_true", + help="仅安装依赖与配置,不自动启动服务", + ) + return parser.parse_args() + + def main() -> None: + args = parse_args() if not (ROOT / "app.py").is_file(): _fail( "请在 LocalNav 项目目录内运行,或使用 scripts/install.py 自动克隆安装:\n" @@ -219,7 +330,20 @@ def main() -> None: py_exe = ensure_venv(base_py) install_requirements(py_exe) smoke_test(py_exe) - print_summary() + + started_via = None + if not args.no_start: + port = os.environ.get("NAV_PORT", "5070") + try: + started_via = start_service(port) + if started_via: + _ok(f"服务已自动启动({started_via})") + else: + _warn("未能自动启动,请按下方说明手动启动") + except subprocess.CalledProcessError as exc: + _warn(f"自动启动失败: {exc}") + + print_summary(started_via) if __name__ == "__main__": diff --git a/部署与使用说明.md b/部署与使用说明.md index 60af8af..77bff6d 100644 --- a/部署与使用说明.md +++ b/部署与使用说明.md @@ -130,8 +130,9 @@ python3 scripts/install.py --repo https://git.bz121.com/dekun/LocalNav.git --dir | 默认账号 | `NAV_ADMIN_USERNAME=admin`、`NAV_ADMIN_PASSWORD=admin123` | | 中控自动登录 | 默认 `NAV_HUB_AUTO_LOGIN=1` | | 内网 Cookie | 默认 `NAV_COOKIES_INSECURE_HTTP=1`(便于 `http://IP:端口` 访问) | +| **自动启动** | 若检测到 **pm2**:`pm2 start` 或 `pm2 restart nav-site`;否则尝试 **systemd** `nav-site` 服务 | -**注意:** 脚本不会覆盖 `.env` 中已有的 `NAV_SECRET_KEY` 等配置。生产环境部署后请尽快登录并在「系统设置」中修改密码。 +**注意:** 脚本不会覆盖 `.env` 中已有的 `NAV_SECRET_KEY` 等配置。生产环境部署后请尽快登录并在「系统设置」中修改密码。仅安装不启动可加 `--no-start`。 ### 5.3 已安装后的更新