Start nav-site in background when pm2 and systemd are unavailable.

Fall back to a pid-file background process after deploy, and detect running state via HTTP port checks in the management menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-12 12:06:43 +08:00
parent fa90a29576
commit d75bf5b0de
4 changed files with 164 additions and 10 deletions
+50 -5
View File
@@ -14,6 +14,14 @@ import sys
import time import time
import urllib.error import urllib.error
import urllib.request import urllib.request
from nav_common import (
get_nav_port,
http_ready,
is_process_running,
pid_file_path,
read_background_pid,
stop_background_process,
)
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
@@ -239,6 +247,40 @@ def wait_for_http(port: str, timeout: float = 15.0) -> bool:
return False return False
def start_background_process(port: str) -> str | None:
"""无 pm2/systemd 时,用 venv python 后台启动并记录 pid。"""
py = venv_python()
if not Path(py).is_file():
return None
old_pid = read_background_pid(ROOT)
if old_pid and is_process_running(old_pid):
if http_ready(port, timeout=3):
return f"后台进程已在运行 pid={old_pid}"
if old_pid and is_process_running(old_pid):
stop_background_process(ROOT)
ensure_logs_dir()
log_file = ROOT / "logs" / "nav-site.log"
pid_file = pid_file_path(ROOT)
with open(log_file, "ab") as log:
proc = subprocess.Popen(
[py, str(ROOT / "app.py")],
cwd=ROOT,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
)
pid_file.write_text(str(proc.pid), encoding="utf-8")
if wait_for_http(port):
return f"后台启动 pid={proc.pid}(日志 logs/nav-site.log"
_warn(f"后台进程已启动 pid={proc.pid},但 :{port} 暂未响应,请查看 logs/nav-site.log")
return f"后台启动 pid={proc.pid}"
def start_service(port: str) -> str | None: def start_service(port: str) -> str | None:
"""尝试自动启动服务,返回启动方式描述;失败返回 None。""" """尝试自动启动服务,返回启动方式描述;失败返回 None。"""
ensure_logs_dir() ensure_logs_dir()
@@ -274,11 +316,11 @@ def start_service(port: str) -> str | None:
_warn("systemd 已启动 nav-site,但健康检查未通过,请执行 journalctl -u nav-site 排查") _warn("systemd 已启动 nav-site,但健康检查未通过,请执行 journalctl -u nav-site 排查")
return "systemctl enable --now nav-site" return "systemctl enable --now nav-site"
return None return start_background_process(port)
def print_summary(started_via: str | None = None) -> None: def print_summary(started_via: str | None = None) -> None:
port = os.environ.get("NAV_PORT", "5070") port = get_nav_port(ROOT)
print() print()
print("=" * 50) print("=" * 50)
print("部署完成") print("部署完成")
@@ -288,12 +330,15 @@ def print_summary(started_via: str | None = None) -> None:
print() print()
if started_via: if started_via:
print(f" 服务状态: 已自动启动({started_via}") print(f" 服务状态: 已自动启动({started_via}")
if shutil.which("pm2"): if shutil.which("pm2") and "pm2" in started_via:
print(" 查看状态: pm2 status") print(" 查看状态: pm2 status")
print(" 查看日志: pm2 logs nav-site") print(" 查看日志: pm2 logs nav-site")
print(" 开机自启: pm2 save && pm2 startup # 按提示执行一次 sudo 命令") print(" 开机自启: pm2 save && pm2 startup # 按提示执行一次 sudo 命令")
elif "后台" in started_via:
print(" 查看日志: tail -f logs/nav-site.log")
print(" 建议生产环境安装 pm2 以便守护与开机自启")
else: else:
print(" 服务状态: 未自动启动(未检测到 pm2 / systemd nav-site") print(" 服务状态: 未自动启动")
print(" 手动启动:") print(" 手动启动:")
print(f" {venv_python()} app.py") print(f" {venv_python()} app.py")
if shutil.which("pm2"): if shutil.which("pm2"):
@@ -333,7 +378,7 @@ def main() -> None:
started_via = None started_via = None
if not args.no_start: if not args.no_start:
port = os.environ.get("NAV_PORT", "5070") port = get_nav_port(ROOT)
try: try:
started_via = start_service(port) started_via = start_service(port)
if started_via: if started_via:
+112 -3
View File
@@ -6,7 +6,10 @@ import json
import os import os
import platform import platform
import shutil import shutil
import socket
import subprocess import subprocess
import urllib.error
import urllib.request
from pathlib import Path from pathlib import Path
DEFAULT_REPO = "https://git.bz121.com/dekun/LocalNav.git" DEFAULT_REPO = "https://git.bz121.com/dekun/LocalNav.git"
@@ -74,6 +77,91 @@ def resolve_project_dir(explicit: str | None = None) -> Path | None:
return None return None
def pid_file_path(project_dir: Path) -> Path:
return project_dir / "logs" / "nav-site.pid"
def get_nav_port(project_dir: Path | None = None) -> str:
if project_dir:
env_file = project_dir / ".env"
if env_file.is_file():
for line in env_file.read_text(encoding="utf-8-sig").splitlines():
s = line.strip()
if s.startswith("NAV_PORT="):
val = s.split("=", 1)[1].strip().strip("\"'")
if val:
return val
return os.environ.get("NAV_PORT", "5070")
def http_ready(port: str, timeout: float = 2.0) -> bool:
url = f"http://127.0.0.1:{port}/login"
try:
with urllib.request.urlopen(url, timeout=timeout) as resp:
return resp.status == 200
except (urllib.error.URLError, TimeoutError, OSError):
return False
def port_open(port: str) -> bool:
try:
p = int(port)
except ValueError:
return False
try:
with socket.create_connection(("127.0.0.1", p), timeout=1.5):
return True
except OSError:
return False
def read_background_pid(project_dir: Path) -> int | None:
path = pid_file_path(project_dir)
if not path.is_file():
return None
try:
return int(path.read_text(encoding="utf-8").strip())
except (ValueError, OSError):
return None
def is_process_running(pid: int) -> bool:
if pid <= 0:
return False
if platform.system() == "Windows":
try:
proc = subprocess.run(
["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True,
text=True,
check=False,
)
return str(pid) in (proc.stdout or "")
except OSError:
return False
try:
os.kill(pid, 0)
return True
except OSError:
return False
def stop_background_process(project_dir: Path) -> bool:
pid = read_background_pid(project_dir)
if not pid or not is_process_running(pid):
pid_file_path(project_dir).unlink(missing_ok=True)
return False
try:
if platform.system() == "Windows":
subprocess.run(["taskkill", "/PID", str(pid), "/F"], check=False)
else:
os.kill(pid, 15)
except OSError:
pass
pid_file_path(project_dir).unlink(missing_ok=True)
return True
def pm2_available() -> str | None: def pm2_available() -> str | None:
return shutil.which("pm2") return shutil.which("pm2")
@@ -149,17 +237,35 @@ def systemd_active(name: str = APP_NAME) -> bool:
def service_status_text(project_dir: Path | None) -> str: def service_status_text(project_dir: Path | None) -> str:
if not project_dir: if not project_dir:
return "未安装" return "未安装"
port = get_nav_port(project_dir)
if http_ready(port):
pm2_state = pm2_app_status()
if pm2_state:
return f"运行中 · pm2 {pm2_state} · :{port}"
if systemd_unit_ready() and systemd_active():
return f"运行中 · systemd · :{port}"
pid = read_background_pid(project_dir)
if pid and is_process_running(pid):
return f"运行中 · 后台 pid {pid} · :{port}"
return f"运行中 · 端口 {port}"
pm2_state = pm2_app_status() pm2_state = pm2_app_status()
if pm2_state: if pm2_state:
return f"pm2 {pm2_state}" return f"pm2 {pm2_state}(未响应 :{port}"
if systemd_unit_ready() and systemd_active(): if systemd_unit_ready() and systemd_active():
return "systemd active" return f"systemd active(未响应 :{port}"
if systemd_unit_ready(): if systemd_unit_ready():
return "systemd 已配置但未运行" return "systemd 已配置但未运行"
pid = read_background_pid(project_dir)
if pid and is_process_running(pid):
return f"后台 pid {pid}(未响应 :{port}"
if port_open(port):
return f"端口 {port} 已占用(进程未知)"
return "未运行" return "未运行"
def stop_service() -> list[str]: def stop_service(project_dir: Path | None = None) -> list[str]:
"""停止并移除进程管理中的 nav-site,返回已执行操作说明。""" """停止并移除进程管理中的 nav-site,返回已执行操作说明。"""
actions: list[str] = [] actions: list[str] = []
pm2 = pm2_available() pm2 = pm2_available()
@@ -175,4 +281,7 @@ def stop_service() -> list[str]:
subprocess.run([systemctl, "disable", f"{APP_NAME}.service"], check=False) subprocess.run([systemctl, "disable", f"{APP_NAME}.service"], check=False)
actions.append("systemctl stop/disable nav-site") actions.append("systemctl stop/disable nav-site")
if project_dir and stop_background_process(project_dir):
actions.append("已停止后台 nav-site 进程")
return actions return actions
+1 -1
View File
@@ -213,7 +213,7 @@ def action_uninstall(*, dest: str = "") -> None:
_warn("已取消卸载") _warn("已取消卸载")
return return
actions = stop_service() actions = stop_service(project_dir)
if actions: if actions:
for item in actions: for item in actions:
_ok(item) _ok(item)
+1 -1
View File
@@ -128,7 +128,7 @@ python3 scripts/navctl.py uninstall --dir /opt/LocalNav
| 默认账号 | `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` 服务 | | **自动启动** | 优先 **pm2**;其次 **systemd**;都没有则用 **后台进程**`logs/nav-site.pid` + `logs/nav-site.log` |
**注意:** 脚本不会覆盖 `.env` 中已有的 `NAV_SECRET_KEY` 等配置。生产环境部署后请尽快登录并在「系统设置」中修改密码。仅安装不启动可在 `deploy.py``--no-start` **注意:** 脚本不会覆盖 `.env` 中已有的 `NAV_SECRET_KEY` 等配置。生产环境部署后请尽快登录并在「系统设置」中修改密码。仅安装不启动可在 `deploy.py``--no-start`