mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 13:01:58 -06:00
Fix Engine WebUI staging and logging outputs
This commit is contained in:
@@ -198,7 +198,7 @@ function Ensure-EngineWebInterface {
|
||||
[string]$ProjectRoot
|
||||
)
|
||||
|
||||
$engineSource = Join-Path $ProjectRoot 'Data\Engine\web-interface'
|
||||
$engineSource = Join-Path $ProjectRoot 'Engine\web-interface'
|
||||
$legacySource = Join-Path $ProjectRoot 'Data\Server\WebUI'
|
||||
|
||||
if (-not (Test-Path $legacySource)) {
|
||||
@@ -1108,7 +1108,7 @@ switch ($choice) {
|
||||
$venvFolder = "Server"
|
||||
$dataSource = "Data"
|
||||
$dataDestination = "$venvFolder\Borealis"
|
||||
$customUIPath = "$dataSource\Engine\web-interface"
|
||||
$customUIPath = (Join-Path $scriptDir 'Engine\web-interface')
|
||||
$webUIDestination = "$venvFolder\web-interface"
|
||||
$venvPython = Join-Path $venvFolder 'Scripts\python.exe'
|
||||
|
||||
@@ -1408,7 +1408,7 @@ switch ($choice) {
|
||||
|
||||
Run-Step "Copy Borealis Engine WebUI Files into: $webUIDestination" {
|
||||
Ensure-EngineWebInterface -ProjectRoot $scriptDir
|
||||
$engineWebUISource = Join-Path $engineSourceAbsolute 'web-interface'
|
||||
$engineWebUISource = Join-Path $scriptDir 'Engine\web-interface'
|
||||
if (Test-Path $engineWebUISource) {
|
||||
$webUIDestinationAbsolute = Join-Path $scriptDir $webUIDestination
|
||||
if (Test-Path $webUIDestinationAbsolute) {
|
||||
|
||||
@@ -331,7 +331,7 @@ PY
|
||||
}
|
||||
|
||||
ensure_engine_webui_source() {
|
||||
local engineSource="Data/Engine/web-interface"
|
||||
local engineSource="Engine/web-interface"
|
||||
local legacySource="Data/Server/WebUI"
|
||||
if [[ -d "${engineSource}/src" && -f "${engineSource}/package.json" ]]; then
|
||||
return 0
|
||||
@@ -351,7 +351,7 @@ ensure_engine_webui_source() {
|
||||
}
|
||||
|
||||
prepare_webui() {
|
||||
local customUIPath="Data/Engine/web-interface"
|
||||
local customUIPath="Engine/web-interface"
|
||||
local webUIDestination="Server/web-interface"
|
||||
ensure_engine_webui_source || return 1
|
||||
mkdir -p "$webUIDestination"
|
||||
|
||||
@@ -112,8 +112,10 @@ def engine_harness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
|
||||
key_path.write_text("test-key", encoding="utf-8")
|
||||
bundle_path.write_text(bundle_contents, encoding="utf-8")
|
||||
|
||||
log_path = tmp_path / "logs" / "server.log"
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
logs_dir = tmp_path / "logs"
|
||||
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_path = logs_dir / "server.log"
|
||||
error_log_path = logs_dir / "error.log"
|
||||
|
||||
config = {
|
||||
"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_BUNDLE_PATH": str(bundle_path),
|
||||
"LOG_FILE": str(log_path),
|
||||
"ERROR_LOG_FILE": str(error_log_path),
|
||||
"API_GROUPS": ("tokens", "enrollment"),
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
certificate helper shipped with the legacy server, ensuring bundling parity.
|
||||
Logs are written to ``Logs/Server/server.log`` with daily rotation so the new
|
||||
runtime integrates with existing operational practices.
|
||||
Logs are written to ``Logs/Engine/engine.log`` with daily rotation and
|
||||
errors are additionally duplicated to ``Logs/Engine/error.log`` so the
|
||||
runtime integrates with the platform's logging policy.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -48,7 +49,9 @@ except Exception: # pragma: no-cover - Engine configuration still works without
|
||||
ENGINE_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = ENGINE_DIR.parent.parent
|
||||
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:
|
||||
@@ -61,16 +64,27 @@ def _ensure_parent(path: Path) -> None:
|
||||
|
||||
|
||||
def _resolve_static_folder() -> str:
|
||||
candidates = [
|
||||
ENGINE_DIR / "web-interface" / "build",
|
||||
ENGINE_DIR / "web-interface" / "dist",
|
||||
candidate_roots = [
|
||||
ENGINE_DIR.parent / "Engine" / "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:
|
||||
absolute = candidate.resolve()
|
||||
if absolute.is_dir():
|
||||
return str(absolute)
|
||||
return str(candidates[0].resolve())
|
||||
if candidate.is_dir():
|
||||
return str(candidate)
|
||||
|
||||
return str(candidates[0])
|
||||
|
||||
|
||||
def _parse_origins(raw: Optional[Any]) -> Optional[List[str]]:
|
||||
@@ -139,6 +153,7 @@ class EngineSettings:
|
||||
tls_key_path: Optional[str]
|
||||
tls_bundle_path: Optional[str]
|
||||
log_file: str
|
||||
error_log_file: str
|
||||
api_groups: Tuple[str, ...]
|
||||
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)
|
||||
_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(
|
||||
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_bundle_path=tls_bundle_path if tls_bundle_path else None,
|
||||
log_file=str(log_file),
|
||||
error_log_file=str(error_log_file),
|
||||
api_groups=api_groups,
|
||||
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:
|
||||
"""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)
|
||||
if not logger.handlers:
|
||||
@@ -263,6 +282,16 @@ def initialise_engine_logger(settings: EngineSettings, name: str = "borealis.eng
|
||||
file_handler.setFormatter(formatter)
|
||||
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.propagate = False
|
||||
return logger
|
||||
|
||||
@@ -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
|
||||
initialisation so the Engine mirrors the legacy server's start-up behaviour.
|
||||
The factory delegates configuration resolution to :mod:`Data.Engine.config`
|
||||
and emits structured logs to ``Logs/Server/server.log`` to align with the
|
||||
project's operational practices.
|
||||
and emits structured logs to ``Logs/Engine/engine.log`` (with an accompanying
|
||||
error log) to align with the project's operational practices.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,63 +16,72 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
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_cors import CORS
|
||||
from flask_socketio import SocketIO
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
# Eventlet ensures Socket.IO long-polling and WebSocket support parity with the
|
||||
# legacy server. We keep thread pools enabled for compatibility with blocking
|
||||
# filesystem/database operations.
|
||||
eventlet.monkey_patch(thread=False)
|
||||
if eventlet:
|
||||
try: # pragma: no-cover - defensive import mirroring the legacy runtime.
|
||||
from eventlet.wsgi import HttpProtocol # type: ignore
|
||||
except Exception: # pragma: no-cover - the Engine should still operate without it.
|
||||
HttpProtocol = None # type: ignore[assignment]
|
||||
else:
|
||||
_original_handle_one_request = HttpProtocol.handle_one_request
|
||||
|
||||
try: # pragma: no-cover - defensive import mirroring the legacy runtime.
|
||||
from eventlet.wsgi import HttpProtocol # type: ignore
|
||||
except Exception: # pragma: no-cover - the Engine should still operate without it.
|
||||
HttpProtocol = None # type: ignore[assignment]
|
||||
else:
|
||||
_original_handle_one_request = HttpProtocol.handle_one_request
|
||||
def _quiet_tls_http_mismatch(self): # type: ignore[override]
|
||||
"""Mirror the legacy suppression of noisy TLS handshake errors."""
|
||||
|
||||
def _quiet_tls_http_mismatch(self): # type: ignore[override]
|
||||
"""Mirror the legacy suppression of noisy TLS handshake errors."""
|
||||
def _close_connection_quietly():
|
||||
try:
|
||||
self.close_connection = True # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
conn = getattr(self, "socket", None) or getattr(self, "connection", None)
|
||||
if conn:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _close_connection_quietly():
|
||||
try:
|
||||
self.close_connection = True # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
conn = getattr(self, "socket", None) or getattr(self, "connection", None)
|
||||
if conn:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return _original_handle_one_request(self)
|
||||
except ssl.SSLError as exc: # type: ignore[arg-type]
|
||||
reason = getattr(exc, "reason", "")
|
||||
reason_text = str(reason).lower() if reason else ""
|
||||
message = " ".join(str(arg) for arg in exc.args if arg).lower()
|
||||
if (
|
||||
"http_request" in message
|
||||
or reason_text == "http request"
|
||||
or "unknown ca" in message
|
||||
or reason_text == "unknown ca"
|
||||
or "unknown_ca" in message
|
||||
):
|
||||
return _original_handle_one_request(self)
|
||||
except ssl.SSLError as exc: # type: ignore[arg-type]
|
||||
reason = getattr(exc, "reason", "")
|
||||
reason_text = str(reason).lower() if reason else ""
|
||||
message = " ".join(str(arg) for arg in exc.args if arg).lower()
|
||||
if (
|
||||
"http_request" in message
|
||||
or reason_text == "http request"
|
||||
or "unknown ca" in message
|
||||
or reason_text == "unknown ca"
|
||||
or "unknown_ca" in message
|
||||
):
|
||||
_close_connection_quietly()
|
||||
return None
|
||||
raise
|
||||
except ssl.SSLEOFError:
|
||||
_close_connection_quietly()
|
||||
return None
|
||||
except ConnectionAbortedError:
|
||||
_close_connection_quietly()
|
||||
return None
|
||||
raise
|
||||
except ssl.SSLEOFError:
|
||||
_close_connection_quietly()
|
||||
return None
|
||||
except ConnectionAbortedError:
|
||||
_close_connection_quietly()
|
||||
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
|
||||
@@ -171,7 +180,7 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke
|
||||
socketio = SocketIO(
|
||||
app,
|
||||
cors_allowed_origins="*",
|
||||
async_mode="eventlet",
|
||||
async_mode=_SOCKETIO_ASYNC_MODE,
|
||||
engineio_options={
|
||||
"max_http_buffer_size": 100_000_000,
|
||||
"max_websocket_message_size": 100_000_000,
|
||||
|
||||
Reference in New Issue
Block a user