Auto-start nav-site via PM2 or systemd after deploy.

Run pm2 start or restart when PM2 is available, fall back to systemd nav-site, and verify /login responds before finishing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-12 11:58:32 +08:00
parent 6897f88afd
commit 8839808845
2 changed files with 133 additions and 8 deletions
+131 -7
View File
@@ -3,12 +3,17 @@
from __future__ import annotations from __future__ import annotations
import argparse
import json
import os import os
import platform import platform
import secrets import secrets
import shutil import shutil
import subprocess import subprocess
import sys import sys
import time
import urllib.error
import urllib.request
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
@@ -183,7 +188,96 @@ def smoke_test(py_exe: str) -> None:
_ok("应用启动检查通过") _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") port = os.environ.get("NAV_PORT", "5070")
print() print()
print("=" * 50) print("=" * 50)
@@ -192,16 +286,33 @@ def print_summary() -> None:
print(" 默认账号: admin / admin123") print(" 默认账号: admin / admin123")
print(" 生产环境请尽快在「系统设置」中修改密码") print(" 生产环境请尽快在「系统设置」中修改密码")
print() print()
print("启动方式(任选其一):") if started_via:
if platform.system() == "Windows": print(f" 服务状态: 已自动启动({started_via}")
print(f" {venv_python()} app.py") if shutil.which("pm2"):
print(" 查看状态: pm2 status")
print(" 查看日志: pm2 logs nav-site")
print(" 开机自启: pm2 save && pm2 startup # 按提示执行一次 sudo 命令")
else: else:
print(f" {venv_python()} app.py") print(" 服务状态: 未自动启动(未检测到 pm2 / systemd nav-site")
print(" pm2 start ecosystem.config.cjs") print(" 手动启动:")
print(f" {venv_python()} app.py")
if shutil.which("pm2"):
print(" pm2 start ecosystem.config.cjs")
print("=" * 50) 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: def main() -> None:
args = parse_args()
if not (ROOT / "app.py").is_file(): if not (ROOT / "app.py").is_file():
_fail( _fail(
"请在 LocalNav 项目目录内运行,或使用 scripts/install.py 自动克隆安装:\n" "请在 LocalNav 项目目录内运行,或使用 scripts/install.py 自动克隆安装:\n"
@@ -219,7 +330,20 @@ def main() -> None:
py_exe = ensure_venv(base_py) py_exe = ensure_venv(base_py)
install_requirements(py_exe) install_requirements(py_exe)
smoke_test(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__": if __name__ == "__main__":
+2 -1
View File
@@ -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_ADMIN_USERNAME=admin``NAV_ADMIN_PASSWORD=admin123` |
| 中控自动登录 | 默认 `NAV_HUB_AUTO_LOGIN=1` | | 中控自动登录 | 默认 `NAV_HUB_AUTO_LOGIN=1` |
| 内网 Cookie | 默认 `NAV_COOKIES_INSECURE_HTTP=1`(便于 `http://IP:端口` 访问) | | 内网 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 已安装后的更新 ### 5.3 已安装后的更新