"""Entrypoint helpers for running the Borealis Engine runtime. The bootstrapper assembles configuration via :func:`Data.Engine.config.load_runtime_config` before delegating to :func:`Data.Engine.server.create_app`. It mirrors the legacy server defaults by binding to ``0.0.0.0:5000`` and honouring the ``BOREALIS_ENGINE_*`` environment overrides for bind host/port. """ from __future__ import annotations import logging import os import shutil import subprocess import time from pathlib import Path from typing import Any, Dict, Optional from .server import EngineContext, create_app DEFAULT_HOST = "0.0.0.0" DEFAULT_PORT = 5000 def _project_root() -> Path: """Locate the repository root by discovering the Borealis bootstrap script.""" current = Path(__file__).resolve().parent for candidate in (current, *current.parents): if (candidate / "Borealis.ps1").is_file(): return candidate raise RuntimeError( "Unable to locate the Borealis project root; Borealis.ps1 was not found " "in any parent directory." ) def _build_runtime_config() -> Dict[str, Any]: api_groups_override = os.environ.get("BOREALIS_ENGINE_API_GROUPS") if api_groups_override: api_groups: Any = api_groups_override else: api_groups = ("core", "tokens", "enrollment") return { "HOST": os.environ.get("BOREALIS_ENGINE_HOST", DEFAULT_HOST), "PORT": int(os.environ.get("BOREALIS_ENGINE_PORT", DEFAULT_PORT)), "TLS_CERT_PATH": os.environ.get("BOREALIS_TLS_CERT"), "TLS_KEY_PATH": os.environ.get("BOREALIS_TLS_KEY"), "TLS_BUNDLE_PATH": os.environ.get("BOREALIS_TLS_BUNDLE"), "API_GROUPS": api_groups, } def _stage_web_interface_assets(logger: Optional[logging.Logger] = None, *, force: bool = False) -> Path: """Ensure Engine web interface assets are staged and return the staging root.""" if logger is None: logger = logging.getLogger("borealis.engine.bootstrap") if not logger.handlers: handler = logging.StreamHandler() handler.setFormatter(logging.Formatter("%(asctime)s-%(name)s-%(levelname)s: %(message)s")) logger.addHandler(handler) logger.propagate = False logger.setLevel(logging.INFO) project_root = _project_root() engine_web_root = project_root / "Engine" / "web-interface" legacy_source = project_root / "Data" / "Server" / "WebUI" if not legacy_source.is_dir(): raise RuntimeError( f"Engine web interface source missing: {legacy_source}" ) index_path = engine_web_root / "index.html" if engine_web_root.exists() and index_path.is_file() and not force: logger.info("Engine web interface already staged at %s; skipping copy.", engine_web_root) return engine_web_root if engine_web_root.exists(): shutil.rmtree(engine_web_root) shutil.copytree(legacy_source, engine_web_root) if not index_path.is_file(): raise RuntimeError( f"Engine web interface staging failed; missing {index_path}" ) logger.info("Engine web interface staged from %s to %s", legacy_source, engine_web_root) return engine_web_root def _determine_static_folder(staging_root: Path) -> str: for candidate in (staging_root / "build", staging_root / "dist", staging_root): if candidate.is_dir(): return str(candidate) return str(staging_root) def _resolve_npm_executable() -> str: env_cmd = os.environ.get("BOREALIS_NPM_CMD") if env_cmd: candidate = Path(env_cmd).expanduser() if candidate.is_file(): return str(candidate) node_dir = os.environ.get("BOREALIS_NODE_DIR") if node_dir: candidate = Path(node_dir) / "npm.cmd" if candidate.is_file(): return str(candidate) candidate = Path(node_dir) / "npm.exe" if candidate.is_file(): return str(candidate) candidate = Path(node_dir) / "npm" if candidate.is_file(): return str(candidate) if os.name == "nt": return "npm.cmd" return "npm" def _run_npm(args: list[str], cwd: Path, logger: logging.Logger) -> None: command = [_resolve_npm_executable()] + args logger.info("Running npm command: %s", " ".join(command)) start = time.time() try: completed = subprocess.run(command, cwd=str(cwd), capture_output=True, text=True, check=False) except FileNotFoundError as exc: raise RuntimeError("npm executable not found; ensure Node.js dependencies are installed.") from exc duration = time.time() - start if completed.returncode != 0: logger.error( "npm command failed (%ss): %s\nstdout: %s\nstderr: %s", f"{duration:.2f}", " ".join(command), completed.stdout.strip(), completed.stderr.strip(), ) raise RuntimeError(f"npm command {' '.join(command)} failed with exit code {completed.returncode}") stdout = completed.stdout.strip() if stdout: logger.debug("npm stdout: %s", stdout) stderr = completed.stderr.strip() if stderr: logger.debug("npm stderr: %s", stderr) logger.info("npm command completed in %.2fs", duration) def _ensure_web_ui_build(staging_root: Path, logger: logging.Logger, *, mode: str) -> str: package_json = staging_root / "package.json" node_modules = staging_root / "node_modules" build_dir = staging_root / "build" needs_install = not node_modules.is_dir() needs_build = not build_dir.is_dir() if not needs_build and package_json.is_file(): build_index = build_dir / "index.html" if build_index.is_file(): needs_build = build_index.stat().st_mtime < package_json.stat().st_mtime else: needs_build = True if mode == "developer": logger.info("Engine launched in developer mode; skipping WebUI build step.") return _determine_static_folder(staging_root) if not package_json.is_file(): logger.warning("WebUI package.json not found at %s; continuing without rebuild.", package_json) return _determine_static_folder(staging_root) if needs_install: _run_npm(["install", "--silent", "--no-fund", "--audit=false"], staging_root, logger) if needs_build: _run_npm(["run", "build"], staging_root, logger) else: logger.info("Existing WebUI build found at %s; reuse.", build_dir) return _determine_static_folder(staging_root) def _ensure_tls_material(context: EngineContext) -> None: """Ensure TLS certificate material exists, updating the context if created.""" try: # Lazy import so Engine still starts if legacy modules are unavailable. from Modules.crypto import certificates # type: ignore except Exception: return try: cert_path, key_path, bundle_path = certificates.ensure_certificate() except Exception as exc: context.logger.error("Failed to auto-provision Engine TLS certificates: %s", exc) return cert_path_str = str(cert_path) key_path_str = str(key_path) bundle_path_str = str(bundle_path) if not context.tls_cert_path or not Path(context.tls_cert_path).is_file(): context.tls_cert_path = cert_path_str if not context.tls_key_path or not Path(context.tls_key_path).is_file(): context.tls_key_path = key_path_str if not context.tls_bundle_path or not Path(context.tls_bundle_path).is_file(): context.tls_bundle_path = bundle_path_str def _prepare_tls_run_kwargs(context: EngineContext) -> Dict[str, Any]: """Validate and return TLS arguments for the Socket.IO runner.""" _ensure_tls_material(context) run_kwargs: Dict[str, Any] = {} key_path_value = context.tls_key_path if not key_path_value: return run_kwargs key_path = Path(key_path_value) if not key_path.is_file(): raise RuntimeError(f"Engine TLS key file not found: {key_path}") cert_candidates = [] if context.tls_bundle_path: cert_candidates.append(context.tls_bundle_path) if context.tls_cert_path and context.tls_cert_path not in cert_candidates: cert_candidates.append(context.tls_cert_path) if not cert_candidates: raise RuntimeError("Engine TLS certificate path not configured; ensure certificates are provisioned.") missing_candidates = [] for candidate in cert_candidates: candidate_path = Path(candidate) if candidate_path.is_file(): run_kwargs["certfile"] = str(candidate_path) run_kwargs["keyfile"] = str(key_path) return run_kwargs missing_candidates.append(str(candidate_path)) checked = ", ".join(missing_candidates) raise RuntimeError(f"Engine TLS certificate file not found. Checked: {checked}") def main() -> None: config = _build_runtime_config() mode = os.environ.get("BOREALIS_ENGINE_MODE", "production").strip().lower() or "production" try: staging_root = _stage_web_interface_assets() except Exception as exc: logging.getLogger("borealis.engine.bootstrap").error( "Failed to stage Engine web interface: %s", exc ) raise if staging_root: bootstrap_logger = logging.getLogger("borealis.engine.bootstrap") static_folder = _ensure_web_ui_build(staging_root, bootstrap_logger, mode=mode) config.setdefault("STATIC_FOLDER", static_folder) app, socketio, context = create_app(config) if staging_root: context.logger.info("Engine WebUI assets ready at %s", config["STATIC_FOLDER"]) host = config.get("HOST", DEFAULT_HOST) port = int(config.get("PORT", DEFAULT_PORT)) run_kwargs: Dict[str, Any] = {"host": host, "port": port} try: tls_kwargs = _prepare_tls_run_kwargs(context) except RuntimeError as exc: context.logger.error("TLS configuration error: %s", exc) raise else: if tls_kwargs: run_kwargs.update(tls_kwargs) context.logger.info("Engine TLS enabled using certificate %s", tls_kwargs["certfile"]) socketio.run(app, **run_kwargs) if __name__ == "__main__": # pragma: no cover - manual launch helper main()