Basic Fallback for Validation, Parsing errors

This commit is contained in:
Alan 2025-03-14 20:28:13 +00:00
parent 5d524e8060
commit 065df720d1
13 changed files with 206 additions and 50 deletions

View File

@ -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

View File

@ -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")

View File

@ -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", {})

View File

@ -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})

View File

@ -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. ***")

View File

@ -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

View File

@ -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})")

View File

@ -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}")

View File

@ -46,6 +46,17 @@ function App() {
>
<Suspense>
<Routes>
<Route
element={
<ProtectedRoute
requiredRoles={["admin"]}
configGuard={false}
/>
}
>
<Route path="/config" element={<ConfigEditor />} />
</Route>
<Route
element={
<ProtectedRoute requiredRoles={["viewer", "admin"]} />
@ -61,7 +72,6 @@ function App() {
element={<ProtectedRoute requiredRoles={["admin"]} />}
>
<Route path="/system" element={<System />} />
<Route path="/config" element={<ConfigEditor />} />
<Route path="/logs" element={<Logs />} />
<Route path="/faces" element={<FaceLibrary />} />
<Route path="/playground" element={<UIPlayground />} />

View File

@ -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 (
<div className="absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight bg-background_alt px-4 dark:text-secondary-foreground">
<div
className={`absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight ${
invalidConfig ? "bg-red-500 bg-opacity-75" : "bg-background_alt"
} px-4 dark:text-secondary-foreground`}
>
<div className="flex h-full items-center gap-2">
{cpuPercent && (
<Link to="/system#general">

View File

@ -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 <Outlet />;
}
// Authenticated mode (8971): require login
if (!auth.user) {
return <Navigate to="/login" replace />;
}
// If role is null (shouldnt happen if isAuthenticated, but type safety), fallback
// though isAuthenticated should catch this
if (auth.user.role === null) {
return <Outlet />;
}
@ -36,5 +38,9 @@ export default function ProtectedRoute({
return <Navigate to="/unauthorized" replace />;
}
if (configGuard && invalidConfig) {
return <Navigate to="/config" replace />;
}
return <Outlet />;
}

View File

@ -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 <Navigate to="/config" replace />;
}
return <Outlet />;
}

View File

@ -0,0 +1,13 @@
import { useMemo } from "react";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
export function useConfigValidator() {
const { data: config } = useSWR<FrigateConfig>("config");
const invalidConfig = useMemo(() => {
return config?.environment_vars?.INVALID_CONFIG;
}, [config]);
return invalidConfig;
}