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:
+50
-5
@@ -14,6 +14,14 @@ import sys
|
||||
import time
|
||||
import urllib.error
|
||||
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
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -239,6 +247,40 @@ def wait_for_http(port: str, timeout: float = 15.0) -> bool:
|
||||
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:
|
||||
"""尝试自动启动服务,返回启动方式描述;失败返回 None。"""
|
||||
ensure_logs_dir()
|
||||
@@ -274,11 +316,11 @@ def start_service(port: str) -> str | None:
|
||||
_warn("systemd 已启动 nav-site,但健康检查未通过,请执行 journalctl -u nav-site 排查")
|
||||
return "systemctl enable --now nav-site"
|
||||
|
||||
return None
|
||||
return start_background_process(port)
|
||||
|
||||
|
||||
def print_summary(started_via: str | None = None) -> None:
|
||||
port = os.environ.get("NAV_PORT", "5070")
|
||||
port = get_nav_port(ROOT)
|
||||
print()
|
||||
print("=" * 50)
|
||||
print("部署完成")
|
||||
@@ -288,12 +330,15 @@ def print_summary(started_via: str | None = None) -> None:
|
||||
print()
|
||||
if started_via:
|
||||
print(f" 服务状态: 已自动启动({started_via})")
|
||||
if shutil.which("pm2"):
|
||||
if shutil.which("pm2") and "pm2" in started_via:
|
||||
print(" 查看状态: pm2 status")
|
||||
print(" 查看日志: pm2 logs nav-site")
|
||||
print(" 开机自启: pm2 save && pm2 startup # 按提示执行一次 sudo 命令")
|
||||
elif "后台" in started_via:
|
||||
print(" 查看日志: tail -f logs/nav-site.log")
|
||||
print(" 建议生产环境安装 pm2 以便守护与开机自启")
|
||||
else:
|
||||
print(" 服务状态: 未自动启动(未检测到 pm2 / systemd nav-site)")
|
||||
print(" 服务状态: 未自动启动")
|
||||
print(" 手动启动:")
|
||||
print(f" {venv_python()} app.py")
|
||||
if shutil.which("pm2"):
|
||||
@@ -333,7 +378,7 @@ def main() -> None:
|
||||
|
||||
started_via = None
|
||||
if not args.no_start:
|
||||
port = os.environ.get("NAV_PORT", "5070")
|
||||
port = get_nav_port(ROOT)
|
||||
try:
|
||||
started_via = start_service(port)
|
||||
if started_via:
|
||||
|
||||
+112
-3
@@ -6,7 +6,10 @@ import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
if not project_dir:
|
||||
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()
|
||||
if pm2_state:
|
||||
return f"pm2 {pm2_state}"
|
||||
return f"pm2 {pm2_state}(未响应 :{port})"
|
||||
if systemd_unit_ready() and systemd_active():
|
||||
return "systemd active"
|
||||
return f"systemd active(未响应 :{port})"
|
||||
if systemd_unit_ready():
|
||||
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 "未运行"
|
||||
|
||||
|
||||
def stop_service() -> list[str]:
|
||||
def stop_service(project_dir: Path | None = None) -> list[str]:
|
||||
"""停止并移除进程管理中的 nav-site,返回已执行操作说明。"""
|
||||
actions: list[str] = []
|
||||
pm2 = pm2_available()
|
||||
@@ -175,4 +281,7 @@ def stop_service() -> list[str]:
|
||||
subprocess.run([systemctl, "disable", f"{APP_NAME}.service"], check=False)
|
||||
actions.append("systemctl stop/disable nav-site")
|
||||
|
||||
if project_dir and stop_background_process(project_dir):
|
||||
actions.append("已停止后台 nav-site 进程")
|
||||
|
||||
return actions
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ def action_uninstall(*, dest: str = "") -> None:
|
||||
_warn("已取消卸载")
|
||||
return
|
||||
|
||||
actions = stop_service()
|
||||
actions = stop_service(project_dir)
|
||||
if actions:
|
||||
for item in actions:
|
||||
_ok(item)
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ python3 scripts/navctl.py uninstall --dir /opt/LocalNav
|
||||
| 默认账号 | `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` 服务 |
|
||||
| **自动启动** | 优先 **pm2**;其次 **systemd**;都没有则用 **后台进程**(`logs/nav-site.pid` + `logs/nav-site.log`) |
|
||||
|
||||
**注意:** 脚本不会覆盖 `.env` 中已有的 `NAV_SECRET_KEY` 等配置。生产环境部署后请尽快登录并在「系统设置」中修改密码。仅安装不启动可在 `deploy.py` 加 `--no-start`。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user