Files
ai-shiliu/app/application/services.py

158 lines
5.5 KiB
Python
Raw Normal View History

import os
import socket
import subprocess
import sys
import time
from pathlib import Path
import requests
class AppConfig:
def __init__(self):
self.project_root = Path(__file__).resolve().parents[2]
self.runtime_mode = self._normalize_runtime_mode(os.getenv("OPENCLAW_RUNTIME_MODE") or os.getenv("OPENCLAW_BACKEND_MODE"))
self.venv_root = self.project_root / ".venv"
self.venv_python = self.venv_root / "Scripts" / "python.exe"
self.venv_pythonw = self.venv_root / "Scripts" / "pythonw.exe"
self.host = os.getenv("APP_HOST", "127.0.0.1")
self.port = int(os.getenv("APP_PORT", "5000"))
def _normalize_runtime_mode(self, value):
mode = (value or "").strip().lower()
if mode in {"release", "prod", "production", "bundle", "packaged"}:
return "release"
return "dev"
@property
def base_url(self):
return f"http://{self.host}:{self.port}"
@property
def python_executable(self):
return Path(sys.executable).resolve()
@property
def python_prefix(self):
return Path(sys.prefix).resolve()
@property
def is_release_mode(self):
return self.runtime_mode == "release"
@property
def backend_entry(self):
return self.project_root / "backend_main.py"
@property
def is_running_in_project_venv(self):
if self.is_release_mode:
return False
try:
if self.python_prefix == self.venv_root.resolve():
return True
allowed_targets = {
self.venv_python.resolve(),
self.venv_pythonw.resolve(),
}
return self.python_executable in allowed_targets
except Exception:
return False
class BackendRuntime:
def __init__(self, config=None):
self.config = config or AppConfig()
@property
def host(self):
return self.config.host
@property
def port(self):
return self.config.port
@property
def base_url(self):
return self.config.base_url
@property
def backend_entry(self):
return self.config.backend_entry
def is_backend_alive(self, host=None, port=None, timeout=0.4):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
return sock.connect_ex((host or self.host, port or self.port)) == 0
finally:
sock.close()
def launch_backend_process(self):
if self.config.is_release_mode:
raise RuntimeError("当前运行模式为发布态,不能从 Python 壳层再启动 backend_main.py")
backend_python = self.config.venv_python
if not backend_python.exists():
raise RuntimeError(f"未找到后端解释器: {backend_python}")
backend_entry = self.backend_entry
if not backend_entry.exists():
raise RuntimeError(f"未找到后端入口: {backend_entry}")
env = os.environ.copy()
env["OPENCLAW_PROJECT_VENV_REEXEC"] = "1"
env.setdefault("OPENCLAW_BACKEND_MODE", "dev")
env.setdefault("OPENCLAW_RUNTIME_MODE", "dev")
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
return subprocess.Popen(
[str(backend_python), str(backend_entry)],
cwd=str(self.config.project_root),
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=creationflags,
)
def terminate_backend_process(self, process, timeout=3):
if process is None or process.poll() is not None:
return
try:
process.terminate()
process.wait(timeout=timeout)
except Exception:
if process.poll() is None:
process.kill()
class FrontendRuntime:
def __init__(self, config=None, backend_runtime=None):
self.config = config or AppConfig()
self.backend_runtime = backend_runtime or BackendRuntime(self.config)
self.frontend_dev_url = os.getenv("FRONTEND_DEV_URL", "http://127.0.0.1:5173").strip().rstrip("/")
self.frontend_dist_index = self.config.project_root / "frontend" / "dist" / "index.html"
self.backend_wait_timeout_ms = 15000
self.backend_wait_interval_ms = 200
def resolve_frontend_entry_url(self, backend_url):
if self.frontend_dev_url:
try:
requests.get(self.frontend_dev_url, timeout=1.2)
return self.frontend_dev_url
except Exception:
pass
if self.frontend_dist_index.exists():
return backend_url.rstrip("/") + "/index.html"
return backend_url.rstrip("/") + "/admin.html"
def wait_for_backend(self, process=None, timeout_ms=None, interval_ms=None):
timeout_ms = timeout_ms or self.backend_wait_timeout_ms
interval_ms = interval_ms or self.backend_wait_interval_ms
started_at = time.time() * 1000
while True:
if self.backend_runtime.is_backend_alive():
return True, ""
if process is not None and process.poll() is not None:
return False, f"独立后台进程已退出,退出码: {process.returncode}"
elapsed = time.time() * 1000 - started_at
if elapsed >= timeout_ms:
return False, f"后台启动超时,{int(elapsed)}ms 内未监听 {self.backend_runtime.host}:{self.backend_runtime.port}"
time.sleep(interval_ms / 1000.0)