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:
+131
-7
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user