From 065df720d10a6f03c2dcdc455c97a0beaa98a4fd Mon Sep 17 00:00:00 2001 From: Alan Date: Fri, 14 Mar 2025 20:28:13 +0000 Subject: [PATCH] Basic Fallback for Validation, Parsing errors --- docker-compose.yml | 8 ++ .../usr/local/ffmpeg/get_ffmpeg_path.py | 3 +- .../rootfs/usr/local/go2rtc/create_config.py | 3 +- .../usr/local/nginx/get_tls_settings.py | 4 +- frigate/__main__.py | 90 ++++++++++++------- frigate/api/fastapi_app.py | 39 ++++++++ frigate/app.py | 33 ++++++- frigate/util/config.py | 7 +- web/src/App.tsx | 12 ++- web/src/components/Statusbar.tsx | 13 ++- web/src/components/auth/ProtectedRoute.tsx | 20 +++-- .../components/settings/ConfigProtection.tsx | 11 +++ web/src/hooks/use-config-validator.ts | 13 +++ 13 files changed, 206 insertions(+), 50 deletions(-) create mode 100644 web/src/components/settings/ConfigProtection.tsx create mode 100644 web/src/hooks/use-config-validator.ts diff --git a/docker-compose.yml b/docker-compose.yml index 2d905d385..72dc54438 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,14 @@ services: - ./config:/config - ./debug:/media/frigate - /dev/bus/usb:/dev/bus/usb + ports: + - "8971:8971" + - "5000:5000" + - "5001:5001" + - "5173:5173" + - "8554:8554" + - "8555:8555" + mqtt: container_name: mqtt image: eclipse-mosquitto:1.6 diff --git a/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py b/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py index ed7f6a891..0a5ce26ad 100644 --- a/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py +++ b/docker/main/rootfs/usr/local/ffmpeg/get_ffmpeg_path.py @@ -3,6 +3,7 @@ import os import sys from ruamel.yaml import YAML +from ruamel.yaml.scanner import ScannerError sys.path.insert(0, "/opt/frigate") from frigate.const import ( @@ -29,7 +30,7 @@ try: config: dict[str, any] = yaml.load(raw_config) elif config_file.endswith(".json"): config: dict[str, any] = json.loads(raw_config) -except FileNotFoundError: +except (FileNotFoundError, ScannerError): config: dict[str, any] = {} path = config.get("ffmpeg", {}).get("path", "default") diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index d7c21c7f7..3e1f53901 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -6,6 +6,7 @@ import sys from pathlib import Path from ruamel.yaml import YAML +from ruamel.yaml.scanner import ScannerError sys.path.insert(0, "/opt/frigate") from frigate.const import ( @@ -44,7 +45,7 @@ try: config: dict[str, any] = yaml.load(raw_config) elif config_file.endswith(".json"): config: dict[str, any] = json.loads(raw_config) -except FileNotFoundError: +except (FileNotFoundError, ScannerError): config: dict[str, any] = {} go2rtc_config: dict[str, any] = config.get("go2rtc", {}) diff --git a/docker/main/rootfs/usr/local/nginx/get_tls_settings.py b/docker/main/rootfs/usr/local/nginx/get_tls_settings.py index f1a4c85de..03d992d09 100644 --- a/docker/main/rootfs/usr/local/nginx/get_tls_settings.py +++ b/docker/main/rootfs/usr/local/nginx/get_tls_settings.py @@ -2,8 +2,8 @@ import json import os - from ruamel.yaml import YAML +from ruamel.yaml.scanner import ScannerError yaml = YAML() @@ -22,7 +22,7 @@ try: config: dict[str, any] = yaml.load(raw_config) elif config_file.endswith(".json"): config: dict[str, any] = json.loads(raw_config) -except FileNotFoundError: +except (FileNotFoundError, ScannerError): config: dict[str, any] = {} tls_config: dict[str, any] = config.get("tls", {"enabled": True}) diff --git a/frigate/__main__.py b/frigate/__main__.py index 4143f7ae6..e3445666a 100644 --- a/frigate/__main__.py +++ b/frigate/__main__.py @@ -7,12 +7,28 @@ from typing import Union import ruamel.yaml from pydantic import ValidationError +from ruamel.yaml.scanner import ScannerError from frigate.app import FrigateApp from frigate.config import FrigateConfig from frigate.log import setup_logging from frigate.util.config import find_config_file +minimal_config = { + "mqtt": {"enabled": "false"}, + "environment_vars": { + "INVALID_CONFIG": "true", + }, + "cameras": { + "null": { + "ffmpeg": { + "inputs": [ + {"path": "/dev/null"} + ] + } + } + }, + } def main() -> None: faulthandler.enable() @@ -47,53 +63,59 @@ def main() -> None: print("*** Config Validation Errors ***") print("*************************************************************\n") # Attempt to get the original config file for line number tracking - config_path = find_config_file() - with open(config_path, "r") as f: - yaml_config = ruamel.yaml.YAML() - yaml_config.preserve_quotes = True - full_config = yaml_config.load(f) + if e.__class__ == ValidationError: - for error in e.errors(): - error_path = error["loc"] + config_path = find_config_file() + with open(config_path, "r") as f: + yaml_config = ruamel.yaml.YAML() + yaml_config.preserve_quotes = True + full_config = yaml_config.load(f) - current = full_config - line_number = "Unknown" - last_line_number = "Unknown" + for error in e.errors(): + error_path = error["loc"] - try: - for i, part in enumerate(error_path): - key: Union[int, str] = ( - int(part) if isinstance(part, str) and part.isdigit() else part - ) + current = full_config + line_number = "Unknown" + last_line_number = "Unknown" - if isinstance(current, ruamel.yaml.comments.CommentedMap): - current = current[key] - elif isinstance(current, list): - if isinstance(key, int): + try: + for i, part in enumerate(error_path): + key: Union[int, str] = ( + int(part) if isinstance(part, str) and part.isdigit() else part + ) + + if isinstance(current, ruamel.yaml.comments.CommentedMap): current = current[key] + elif isinstance(current, list): + if isinstance(key, int): + current = current[key] - if hasattr(current, "lc"): - last_line_number = current.lc.line - - if i == len(error_path) - 1: if hasattr(current, "lc"): - line_number = current.lc.line - else: - line_number = last_line_number + last_line_number = current.lc.line - except Exception as traverse_error: - print(f"Could not determine exact line number: {traverse_error}") + if i == len(error_path) - 1: + if hasattr(current, "lc"): + line_number = current.lc.line + else: + line_number = last_line_number - if current != full_config: - print(f"Line # : {line_number}") - print(f"Key : {' -> '.join(map(str, error_path))}") - print(f"Value : {error.get('input', '-')}") - print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n") + except Exception as traverse_error: + print(f"Could not determine exact line number: {traverse_error}") + + if current != full_config: + print(f"Line # : {line_number}") + print(f"Key : {' -> '.join(map(str, error_path))}") + print(f"Value : {error.get('input', '-')}") + print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n") + else: + print(f"Failed to parse config: {e}") print("*************************************************************") print("*** End Config Validation Errors ***") print("*************************************************************") - sys.exit(1) + + FrigateApp(FrigateConfig(**minimal_config)).start_config_editor() + sys.exit(0) if args.validate_config: print("*************************************************************") print("*** Your config file is valid. ***") diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 0657752dc..ac97c1b41 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -130,3 +130,42 @@ def create_fastapi_app( app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None return app + + + + +def create_config_editor_app( + frigate_config: FrigateConfig, + database: SqliteQueueDatabase, +) -> FastAPI: + + app = FastAPI( + debug=True, + swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"}, + ) + + app.add_middleware( + middleware.ContextMiddleware, + plugins=(plugins.ForwardedForPlugin(),), + ) + + @app.on_event("startup") + async def startup(): + logger.info("FastAPI started") + + # Rate limiter (used for login endpoint) + if frigate_config.auth.failed_login_rate_limit is None: + limiter.enabled = False + else: + auth.rateLimiter.set_limit(frigate_config.auth.failed_login_rate_limit) + + app.state.limiter = limiter + app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + + # Routes + app.include_router(auth.router) + app.include_router(main_app.router) + app.frigate_config = frigate_config + app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None + + return app diff --git a/frigate/app.py b/frigate/app.py index f433fd50f..dfc4e1951 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -15,7 +15,7 @@ from playhouse.sqlite_ext import SqliteExtDatabase import frigate.util as util from frigate.api.auth import hash_password -from frigate.api.fastapi_app import create_fastapi_app +from frigate.api.fastapi_app import create_config_editor_app, create_fastapi_app from frigate.camera import CameraMetrics, PTZMetrics from frigate.comms.base_communicator import Communicator from frigate.comms.config_updater import ConfigPublisher @@ -624,6 +624,37 @@ class FrigateApp: logger.info("********************************************************") logger.info("********************************************************") + + def start_config_editor(self) -> None: + logger.info(f"Starting Frigate in Config Editor Mode ({VERSION})") + self.ensure_dirs() + self.init_database() + self.bind_database() + self.init_auth() + + def stop_config_editor() -> None: + logger.info("Stopping...") + os._exit(os.EX_OK) + + try: + uvicorn.run( + create_config_editor_app( + self.config, + self.db, + ), + host="127.0.0.1", + port=5001, + log_level="error", + ) + finally: + stop_config_editor() + + + + + + + def start(self) -> None: logger.info(f"Starting Frigate ({VERSION})") diff --git a/frigate/util/config.py b/frigate/util/config.py index 7bdc0c3d6..30098f96b 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -7,6 +7,7 @@ import shutil from typing import Optional, Union from ruamel.yaml import YAML +from ruamel.yaml.scanner import ScannerError from frigate.const import CONFIG_DIR, EXPORT_DIR from frigate.util.services import get_video_properties @@ -37,7 +38,11 @@ def migrate_frigate_config(config_file: str): yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) with open(config_file, "r") as f: - config: dict[str, dict[str, any]] = yaml.load(f) + try: + config: dict[str, dict[str, any]] = yaml.load(f) + except ScannerError: + logger.debug(f"Failed to parse config at {config_file}") + return if config is None: logger.error(f"Failed to load config at {config_file}") diff --git a/web/src/App.tsx b/web/src/App.tsx index a0062549f..4e0a55e12 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -46,6 +46,17 @@ function App() { > + + } + > + } /> + + @@ -61,7 +72,6 @@ function App() { element={} > } /> - } /> } /> } /> } /> diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx index 1b20b26f6..b9ca655c8 100644 --- a/web/src/components/Statusbar.tsx +++ b/web/src/components/Statusbar.tsx @@ -9,6 +9,7 @@ import { FaCheck } from "react-icons/fa"; import { IoIosWarning } from "react-icons/io"; import { MdCircle } from "react-icons/md"; import { Link } from "react-router-dom"; +import { useConfigValidator } from "@/hooks/use-config-validator"; // <-- new import export default function Statusbar() { const { messages, addMessage, clearMessages } = useContext( @@ -28,6 +29,7 @@ export default function Statusbar() { }, [stats]); const { potentialProblems } = useStats(stats); + const invalidConfig = useConfigValidator(); // <-- use the hook useEffect(() => { clearMessages("stats"); @@ -40,7 +42,10 @@ export default function Statusbar() { problem.relevantLink, ); }); - }, [potentialProblems, addMessage, clearMessages]); + if (invalidConfig) { + addMessage("stats", "Invalid config - fix config only", "text-danger"); + } + }, [potentialProblems, invalidConfig, addMessage, clearMessages]); const { payload: reindexState } = useEmbeddingsReindexProgress(); @@ -60,7 +65,11 @@ export default function Statusbar() { }, [reindexState, addMessage, clearMessages]); return ( -
+
{cpuPercent && ( diff --git a/web/src/components/auth/ProtectedRoute.tsx b/web/src/components/auth/ProtectedRoute.tsx index c35fdaebc..6bea4deda 100644 --- a/web/src/components/auth/ProtectedRoute.tsx +++ b/web/src/components/auth/ProtectedRoute.tsx @@ -2,13 +2,19 @@ import { useContext } from "react"; import { Navigate, Outlet } from "react-router-dom"; import { AuthContext } from "@/context/auth-context"; import ActivityIndicator from "../indicators/activity-indicator"; +import { useConfigValidator } from "@/hooks/use-config-validator"; + +interface ProtectedRouteProps { + requiredRoles: ("admin" | "viewer")[]; + configGuard?: boolean; +} export default function ProtectedRoute({ requiredRoles, -}: { - requiredRoles: ("admin" | "viewer")[]; -}) { + configGuard = true, +}: ProtectedRouteProps) { const { auth } = useContext(AuthContext); + const invalidConfig = useConfigValidator(); // Use the hook to check config validity if (auth.isLoading) { return ( @@ -16,18 +22,14 @@ export default function ProtectedRoute({ ); } - // Unauthenticated mode if (!auth.isAuthenticated) { return ; } - // Authenticated mode (8971): require login if (!auth.user) { return ; } - // If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback - // though isAuthenticated should catch this if (auth.user.role === null) { return ; } @@ -36,5 +38,9 @@ export default function ProtectedRoute({ return ; } + if (configGuard && invalidConfig) { + return ; + } + return ; } diff --git a/web/src/components/settings/ConfigProtection.tsx b/web/src/components/settings/ConfigProtection.tsx new file mode 100644 index 000000000..ec901029c --- /dev/null +++ b/web/src/components/settings/ConfigProtection.tsx @@ -0,0 +1,11 @@ +import { useConfigValidator } from "@/hooks/use-config-validator"; +import { Navigate, Outlet, useLocation } from "react-router-dom"; + +export default function ConfigProtection() { + const invalidConfig = useConfigValidator(); + const location = useLocation(); + if (invalidConfig && location.pathname !== "/config") { + return ; + } + return ; +} diff --git a/web/src/hooks/use-config-validator.ts b/web/src/hooks/use-config-validator.ts new file mode 100644 index 000000000..aaf64fa53 --- /dev/null +++ b/web/src/hooks/use-config-validator.ts @@ -0,0 +1,13 @@ +import { useMemo } from "react"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; + +export function useConfigValidator() { + const { data: config } = useSWR("config"); + + const invalidConfig = useMemo(() => { + return config?.environment_vars?.INVALID_CONFIG; + }, [config]); + + return invalidConfig; +}