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
|
[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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,63 +16,72 @@ 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
|
try: # pragma: no-cover - defensive import mirroring the legacy runtime.
|
||||||
# filesystem/database operations.
|
from eventlet.wsgi import HttpProtocol # type: ignore
|
||||||
eventlet.monkey_patch(thread=False)
|
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.
|
def _quiet_tls_http_mismatch(self): # type: ignore[override]
|
||||||
from eventlet.wsgi import HttpProtocol # type: ignore
|
"""Mirror the legacy suppression of noisy TLS handshake errors."""
|
||||||
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]
|
def _close_connection_quietly():
|
||||||
"""Mirror the legacy suppression of noisy TLS handshake errors."""
|
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:
|
try:
|
||||||
self.close_connection = True # type: ignore[attr-defined]
|
return _original_handle_one_request(self)
|
||||||
except Exception:
|
except ssl.SSLError as exc: # type: ignore[arg-type]
|
||||||
pass
|
reason = getattr(exc, "reason", "")
|
||||||
try:
|
reason_text = str(reason).lower() if reason else ""
|
||||||
conn = getattr(self, "socket", None) or getattr(self, "connection", None)
|
message = " ".join(str(arg) for arg in exc.args if arg).lower()
|
||||||
if conn:
|
if (
|
||||||
conn.close()
|
"http_request" in message
|
||||||
except Exception:
|
or reason_text == "http request"
|
||||||
pass
|
or "unknown ca" in message
|
||||||
|
or reason_text == "unknown ca"
|
||||||
try:
|
or "unknown_ca" in message
|
||||||
return _original_handle_one_request(self)
|
):
|
||||||
except ssl.SSLError as exc: # type: ignore[arg-type]
|
_close_connection_quietly()
|
||||||
reason = getattr(exc, "reason", "")
|
return None
|
||||||
reason_text = str(reason).lower() if reason else ""
|
raise
|
||||||
message = " ".join(str(arg) for arg in exc.args if arg).lower()
|
except ssl.SSLEOFError:
|
||||||
if (
|
_close_connection_quietly()
|
||||||
"http_request" in message
|
return None
|
||||||
or reason_text == "http request"
|
except ConnectionAbortedError:
|
||||||
or "unknown ca" in message
|
|
||||||
or reason_text == "unknown ca"
|
|
||||||
or "unknown_ca" in message
|
|
||||||
):
|
|
||||||
_close_connection_quietly()
|
_close_connection_quietly()
|
||||||
return None
|
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
|
# 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user