Fix Engine WebUI staging and logging outputs

This commit is contained in:
2025-10-26 02:02:05 -06:00
parent 01ea3ca4a4
commit 1b6d015124
5 changed files with 107 additions and 66 deletions

View File

@@ -198,7 +198,7 @@ function Ensure-EngineWebInterface {
[string]$ProjectRoot [string]$ProjectRoot
) )
$engineSource = Join-Path $ProjectRoot 'Data\Engine\web-interface' $engineSource = Join-Path $ProjectRoot 'Engine\web-interface'
$legacySource = Join-Path $ProjectRoot 'Data\Server\WebUI' $legacySource = Join-Path $ProjectRoot 'Data\Server\WebUI'
if (-not (Test-Path $legacySource)) { if (-not (Test-Path $legacySource)) {
@@ -1108,7 +1108,7 @@ switch ($choice) {
$venvFolder = "Server" $venvFolder = "Server"
$dataSource = "Data" $dataSource = "Data"
$dataDestination = "$venvFolder\Borealis" $dataDestination = "$venvFolder\Borealis"
$customUIPath = "$dataSource\Engine\web-interface" $customUIPath = (Join-Path $scriptDir 'Engine\web-interface')
$webUIDestination = "$venvFolder\web-interface" $webUIDestination = "$venvFolder\web-interface"
$venvPython = Join-Path $venvFolder 'Scripts\python.exe' $venvPython = Join-Path $venvFolder 'Scripts\python.exe'
@@ -1408,7 +1408,7 @@ switch ($choice) {
Run-Step "Copy Borealis Engine WebUI Files into: $webUIDestination" { Run-Step "Copy Borealis Engine WebUI Files into: $webUIDestination" {
Ensure-EngineWebInterface -ProjectRoot $scriptDir Ensure-EngineWebInterface -ProjectRoot $scriptDir
$engineWebUISource = Join-Path $engineSourceAbsolute 'web-interface' $engineWebUISource = Join-Path $scriptDir 'Engine\web-interface'
if (Test-Path $engineWebUISource) { if (Test-Path $engineWebUISource) {
$webUIDestinationAbsolute = Join-Path $scriptDir $webUIDestination $webUIDestinationAbsolute = Join-Path $scriptDir $webUIDestination
if (Test-Path $webUIDestinationAbsolute) { if (Test-Path $webUIDestinationAbsolute) {

View File

@@ -331,7 +331,7 @@ PY
} }
ensure_engine_webui_source() { ensure_engine_webui_source() {
local engineSource="Data/Engine/web-interface" local engineSource="Engine/web-interface"
local legacySource="Data/Server/WebUI" local legacySource="Data/Server/WebUI"
if [[ -d "${engineSource}/src" && -f "${engineSource}/package.json" ]]; then if [[ -d "${engineSource}/src" && -f "${engineSource}/package.json" ]]; then
return 0 return 0
@@ -351,7 +351,7 @@ ensure_engine_webui_source() {
} }
prepare_webui() { prepare_webui() {
local customUIPath="Data/Engine/web-interface" local customUIPath="Engine/web-interface"
local webUIDestination="Server/web-interface" local webUIDestination="Server/web-interface"
ensure_engine_webui_source || return 1 ensure_engine_webui_source || return 1
mkdir -p "$webUIDestination" mkdir -p "$webUIDestination"

View File

@@ -112,8 +112,10 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
key_path.write_text("test-key", encoding="utf-8") key_path.write_text("test-key", encoding="utf-8")
bundle_path.write_text(bundle_contents, encoding="utf-8") bundle_path.write_text(bundle_contents, encoding="utf-8")
log_path = tmp_path / "logs" / "server.log" logs_dir = tmp_path / "logs"
log_path.parent.mkdir(parents=True, exist_ok=True) logs_dir.mkdir(parents=True, exist_ok=True)
log_path = logs_dir / "server.log"
error_log_path = logs_dir / "error.log"
config = { config = {
"DATABASE_PATH": str(db_path), "DATABASE_PATH": str(db_path),
@@ -121,6 +123,7 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
"TLS_KEY_PATH": str(key_path), "TLS_KEY_PATH": str(key_path),
"TLS_BUNDLE_PATH": str(bundle_path), "TLS_BUNDLE_PATH": str(bundle_path),
"LOG_FILE": str(log_path), "LOG_FILE": str(log_path),
"ERROR_LOG_FILE": str(error_log_path),
"API_GROUPS": ("tokens", "enrollment"), "API_GROUPS": ("tokens", "enrollment"),
} }

View File

@@ -26,8 +26,9 @@ defaults that mirror the legacy server runtime. Key environment variables are
When TLS values are not provided explicitly the Engine falls back to the When TLS values are not provided explicitly the Engine falls back to the
certificate helper shipped with the legacy server, ensuring bundling parity. certificate helper shipped with the legacy server, ensuring bundling parity.
Logs are written to ``Logs/Server/server.log`` with daily rotation so the new Logs are written to ``Logs/Engine/engine.log`` with daily rotation and
runtime integrates with existing operational practices. errors are additionally duplicated to ``Logs/Engine/error.log`` so the
runtime integrates with the platform's logging policy.
""" """
from __future__ import annotations from __future__ import annotations
@@ -48,7 +49,9 @@ except Exception: # pragma: no-cover - Engine configuration still works without
ENGINE_DIR = Path(__file__).resolve().parent ENGINE_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = ENGINE_DIR.parent.parent PROJECT_ROOT = ENGINE_DIR.parent.parent
DEFAULT_DATABASE_PATH = PROJECT_ROOT / "database.db" DEFAULT_DATABASE_PATH = PROJECT_ROOT / "database.db"
LOG_FILE_PATH = PROJECT_ROOT / "Logs" / "Server" / "server.log" LOG_ROOT = PROJECT_ROOT / "Logs" / "Engine"
LOG_FILE_PATH = LOG_ROOT / "engine.log"
ERROR_LOG_FILE_PATH = LOG_ROOT / "error.log"
def _ensure_parent(path: Path) -> None: def _ensure_parent(path: Path) -> None:
@@ -61,16 +64,27 @@ def _ensure_parent(path: Path) -> None:
def _resolve_static_folder() -> str: def _resolve_static_folder() -> str:
candidates = [ candidate_roots = [
ENGINE_DIR / "web-interface" / "build", ENGINE_DIR.parent / "Engine" / "web-interface",
ENGINE_DIR / "web-interface" / "dist",
ENGINE_DIR / "web-interface", ENGINE_DIR / "web-interface",
] ]
candidates = []
for root in candidate_roots:
absolute_root = root.resolve()
candidates.extend(
[
absolute_root / "build",
absolute_root / "dist",
absolute_root,
]
)
for candidate in candidates: for candidate in candidates:
absolute = candidate.resolve() if candidate.is_dir():
if absolute.is_dir(): return str(candidate)
return str(absolute)
return str(candidates[0].resolve()) return str(candidates[0])
def _parse_origins(raw: Optional[Any]) -> Optional[List[str]]: def _parse_origins(raw: Optional[Any]) -> Optional[List[str]]:
@@ -139,6 +153,7 @@ class EngineSettings:
tls_key_path: Optional[str] tls_key_path: Optional[str]
tls_bundle_path: Optional[str] tls_bundle_path: Optional[str]
log_file: str log_file: str
error_log_file: str
api_groups: Tuple[str, ...] api_groups: Tuple[str, ...]
raw: MutableMapping[str, Any] = field(default_factory=dict) raw: MutableMapping[str, Any] = field(default_factory=dict)
@@ -219,6 +234,9 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine
log_file = str(runtime_config.get("LOG_FILE") or LOG_FILE_PATH) log_file = str(runtime_config.get("LOG_FILE") or LOG_FILE_PATH)
_ensure_parent(Path(log_file)) _ensure_parent(Path(log_file))
error_log_file = str(runtime_config.get("ERROR_LOG_FILE") or ERROR_LOG_FILE_PATH)
_ensure_parent(Path(error_log_file))
api_groups = _parse_api_groups( api_groups = _parse_api_groups(
runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS") runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS")
) )
@@ -237,6 +255,7 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine
tls_key_path=tls_key_path if tls_key_path else None, tls_key_path=tls_key_path if tls_key_path else None,
tls_bundle_path=tls_bundle_path if tls_bundle_path else None, tls_bundle_path=tls_bundle_path if tls_bundle_path else None,
log_file=str(log_file), log_file=str(log_file),
error_log_file=str(error_log_file),
api_groups=api_groups, api_groups=api_groups,
raw=runtime_config, raw=runtime_config,
) )
@@ -244,7 +263,7 @@ def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> Engine
def initialise_engine_logger(settings: EngineSettings, name: str = "borealis.engine") -> logging.Logger: def initialise_engine_logger(settings: EngineSettings, name: str = "borealis.engine") -> logging.Logger:
"""Configure the Engine logger to write to the shared server log.""" """Configure the Engine logger to write to Engine log files."""
logger = logging.getLogger(name) logger = logging.getLogger(name)
if not logger.handlers: if not logger.handlers:
@@ -263,6 +282,16 @@ def initialise_engine_logger(settings: EngineSettings, name: str = "borealis.eng
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
logger.addHandler(file_handler) logger.addHandler(file_handler)
error_handler = TimedRotatingFileHandler(
settings.error_log_file,
when="midnight",
backupCount=0,
encoding="utf-8",
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(formatter)
logger.addHandler(error_handler)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
logger.propagate = False logger.propagate = False
return logger return logger

View File

@@ -4,8 +4,8 @@ Stage 1 introduced the structural skeleton for the Engine runtime. Stage 2
builds upon that foundation by centralising configuration handling and logging builds upon that foundation by centralising configuration handling and logging
initialisation so the Engine mirrors the legacy server's start-up behaviour. initialisation so the Engine mirrors the legacy server's start-up behaviour.
The factory delegates configuration resolution to :mod:`Data.Engine.config` The factory delegates configuration resolution to :mod:`Data.Engine.config`
and emits structured logs to ``Logs/Server/server.log`` to align with the and emits structured logs to ``Logs/Engine/engine.log`` (with an accompanying
project's operational practices. error log) to align with the project's operational practices.
""" """
from __future__ import annotations from __future__ import annotations
@@ -16,17 +16,22 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Mapping, Optional, Sequence, Tuple from typing import Any, Mapping, Optional, Sequence, Tuple
import eventlet try: # pragma: no-cover - optional dependency when running without eventlet
import eventlet # type: ignore
except Exception: # pragma: no-cover - fall back to threading mode
eventlet = None # type: ignore[assignment]
logging.getLogger(__name__).warning(
"Eventlet is not available; Engine will run Socket.IO in threading mode."
)
else: # pragma: no-cover - monkey patch only when eventlet is present
eventlet.monkey_patch(thread=False)
from flask import Flask, request from flask import Flask, request
from flask_cors import CORS from flask_cors import CORS
from flask_socketio import SocketIO from flask_socketio import SocketIO
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
# Eventlet ensures Socket.IO long-polling and WebSocket support parity with the if eventlet:
# legacy server. We keep thread pools enabled for compatibility with blocking
# filesystem/database operations.
eventlet.monkey_patch(thread=False)
try: # pragma: no-cover - defensive import mirroring the legacy runtime. try: # pragma: no-cover - defensive import mirroring the legacy runtime.
from eventlet.wsgi import HttpProtocol # type: ignore from eventlet.wsgi import HttpProtocol # type: ignore
except Exception: # pragma: no-cover - the Engine should still operate without it. except Exception: # pragma: no-cover - the Engine should still operate without it.
@@ -73,6 +78,10 @@ else:
return None return None
HttpProtocol.handle_one_request = _quiet_tls_http_mismatch # type: ignore[assignment] HttpProtocol.handle_one_request = _quiet_tls_http_mismatch # type: ignore[assignment]
else:
HttpProtocol = None # type: ignore[assignment]
_SOCKETIO_ASYNC_MODE = "eventlet" if eventlet else "threading"
# Ensure the legacy ``Modules`` package is importable when running from the # Ensure the legacy ``Modules`` package is importable when running from the
@@ -171,7 +180,7 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke
socketio = SocketIO( socketio = SocketIO(
app, app,
cors_allowed_origins="*", cors_allowed_origins="*",
async_mode="eventlet", async_mode=_SOCKETIO_ASYNC_MODE,
engineio_options={ engineio_options={
"max_http_buffer_size": 100_000_000, "max_http_buffer_size": 100_000_000,
"max_websocket_message_size": 100_000_000, "max_websocket_message_size": 100_000_000,