mirror of
				https://github.com/bunny-lab-io/Borealis.git
				synced 2025-10-26 15:41:58 -06:00 
			
		
		
		
	Fix Engine WebUI staging and logging outputs
This commit is contained in:
		| @@ -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