Move more things out of FrigateApp (#13897)

* Moved FrigateApp.init_config() into FrigateConfig.load()

* Move frigate config loading into main

* Store PlusApi in FrigateConfig

* Register SIGTERM handler in main

* Ensure logging is setup during config parsing

* Removed pointless try

* Moved config initialization out of FrigateApp

* Made FrigateApp.shm_frame_count into a function

* Removed log calls from signal handlers

python's logging calls are not re-entrant, which caused at least one of
these to deadlock randomly.

* Reopen stdout/err on process fork

This helps avoid deadlocks (https://github.com/python/cpython/issues/91776).

* Make mypy happy

* Whoops. I might have forgotten to save.

Truly an amateur mistake.

* Always call FrigateApp.stop()
This commit is contained in:
gtsiam 2024-09-24 15:07:47 +03:00 committed by GitHub
parent a7ed90f042
commit dc54981784
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 215 additions and 213 deletions

View File

@ -1,10 +1,16 @@
import argparse
import faulthandler import faulthandler
import logging import logging
import signal
import sys
import threading import threading
from flask import cli from flask import cli
from pydantic import ValidationError
from frigate.app import FrigateApp from frigate.app import FrigateApp
from frigate.config import FrigateConfig
from frigate.log import log_thread
def main() -> None: def main() -> None:
@ -20,8 +26,50 @@ def main() -> None:
threading.current_thread().name = "frigate" threading.current_thread().name = "frigate"
cli.show_server_banner = lambda *x: None cli.show_server_banner = lambda *x: None
# Make sure we exit cleanly on SIGTERM.
signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit())
run()
@log_thread()
def run() -> None:
# Parse the cli arguments.
parser = argparse.ArgumentParser(
prog="Frigate",
description="An NVR with realtime local object detection for IP cameras.",
)
parser.add_argument("--validate-config", action="store_true")
args = parser.parse_args()
# Load the configuration.
try:
config = FrigateConfig.load()
except ValidationError as e:
print("*************************************************************")
print("*************************************************************")
print("*** Your config file is not valid! ***")
print("*** Please check the docs at ***")
print("*** https://docs.frigate.video/configuration/ ***")
print("*************************************************************")
print("*************************************************************")
print("*** Config Validation Errors ***")
print("*************************************************************")
for error in e.errors():
location = ".".join(str(item) for item in error["loc"])
print(f"{location}: {error['msg']}")
print("*************************************************************")
print("*** End Config Validation Errors ***")
print("*************************************************************")
sys.exit(1)
if args.validate_config:
print("*************************************************************")
print("*** Your config file is valid. ***")
print("*************************************************************")
sys.exit(0)
# Run the main application. # Run the main application.
FrigateApp().start() FrigateApp(config).start()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -28,7 +28,6 @@ from frigate.const import CONFIG_DIR
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, Timeline from frigate.models import Event, Timeline
from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter from frigate.stats.emitter import StatsEmitter
from frigate.storage import StorageMaintainer from frigate.storage import StorageMaintainer
@ -61,7 +60,6 @@ def create_app(
storage_maintainer: StorageMaintainer, storage_maintainer: StorageMaintainer,
onvif: OnvifController, onvif: OnvifController,
external_processor: ExternalEventProcessor, external_processor: ExternalEventProcessor,
plus_api: PlusApi,
stats_emitter: StatsEmitter, stats_emitter: StatsEmitter,
): ):
app = Flask(__name__) app = Flask(__name__)
@ -89,7 +87,6 @@ def create_app(
app.storage_maintainer = storage_maintainer app.storage_maintainer = storage_maintainer
app.onvif = onvif app.onvif = onvif
app.external_processor = external_processor app.external_processor = external_processor
app.plus_api = plus_api
app.camera_error_image = None app.camera_error_image = None
app.stats_emitter = stats_emitter app.stats_emitter = stats_emitter
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
@ -199,7 +196,7 @@ def config():
for zone_name, zone in config_obj.cameras[camera_name].zones.items(): for zone_name, zone in config_obj.cameras[camera_name].zones.items():
camera_dict["zones"][zone_name]["color"] = zone.color camera_dict["zones"][zone_name]["color"] = zone.color
config["plus"] = {"enabled": current_app.plus_api.is_active()} config["plus"] = {"enabled": current_app.frigate_config.plus_api.is_active()}
config["model"]["colormap"] = config_obj.model.colormap config["model"]["colormap"] = config_obj.model.colormap
for detector_config in config["detectors"].values(): for detector_config in config["detectors"].values():
@ -362,7 +359,7 @@ def config_set():
if json.get("requires_restart", 1) == 0: if json.get("requires_restart", 1) == 0:
current_app.frigate_config = FrigateConfig.parse_object( current_app.frigate_config = FrigateConfig.parse_object(
config_obj, plus_api=current_app.plus_api config_obj, plus_api=current_app.frigate_config.plus_api
) )
return make_response( return make_response(

View File

@ -612,7 +612,7 @@ def set_retain(id):
@EventBp.route("/events/<id>/plus", methods=("POST",)) @EventBp.route("/events/<id>/plus", methods=("POST",))
def send_to_plus(id): def send_to_plus(id):
if not current_app.plus_api.is_active(): if not current_app.frigate_config.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set" message = "PLUS_API_KEY environment variable is not set"
logger.error(message) logger.error(message)
return make_response( return make_response(
@ -680,7 +680,7 @@ def send_to_plus(id):
) )
try: try:
plus_id = current_app.plus_api.upload_image(image, event.camera) plus_id = current_app.frigate_config.plus_api.upload_image(image, event.camera)
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
return make_response( return make_response(
@ -696,7 +696,7 @@ def send_to_plus(id):
box = event.data["box"] box = event.data["box"]
try: try:
current_app.plus_api.add_annotation( current_app.frigate_config.plus_api.add_annotation(
event.plus_id, event.plus_id,
box, box,
event.label, event.label,
@ -720,7 +720,7 @@ def send_to_plus(id):
@EventBp.route("/events/<id>/false_positive", methods=("PUT",)) @EventBp.route("/events/<id>/false_positive", methods=("PUT",))
def false_positive(id): def false_positive(id):
if not current_app.plus_api.is_active(): if not current_app.frigate_config.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set" message = "PLUS_API_KEY environment variable is not set"
logger.error(message) logger.error(message)
return make_response( return make_response(
@ -769,7 +769,7 @@ def false_positive(id):
) )
try: try:
current_app.plus_api.add_false_positive( current_app.frigate_config.plus_api.add_false_positive(
event.plus_id, event.plus_id,
region, region,
box, box,

View File

@ -294,7 +294,7 @@ def submit_recording_snapshot_to_plus(camera_name: str, frame_time: str):
) )
nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR) nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR)
current_app.plus_api.upload_image(nd, camera_name) current_app.frigate_config.plus_api.upload_image(nd, camera_name)
return make_response( return make_response(
jsonify( jsonify(

View File

@ -1,23 +1,17 @@
import argparse
import datetime import datetime
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import os import os
import secrets import secrets
import shutil import shutil
import signal
import sys
import traceback
from multiprocessing import Queue from multiprocessing import Queue
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from types import FrameType from typing import Any
from typing import Optional
import psutil import psutil
from peewee_migrate import Router from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase from playhouse.sqliteq import SqliteQueueDatabase
from pydantic import ValidationError
from frigate.api.app import create_app from frigate.api.app import create_app
from frigate.api.auth import hash_password from frigate.api.auth import hash_password
@ -28,7 +22,6 @@ from frigate.comms.mqtt import MqttClient
from frigate.comms.webpush import WebPushClient from frigate.comms.webpush import WebPushClient
from frigate.comms.ws import WebSocketClient from frigate.comms.ws import WebSocketClient
from frigate.comms.zmq_proxy import ZmqProxy from frigate.comms.zmq_proxy import ZmqProxy
from frigate.config import FrigateConfig
from frigate.const import ( from frigate.const import (
CACHE_DIR, CACHE_DIR,
CLIPS_DIR, CLIPS_DIR,
@ -43,7 +36,6 @@ from frigate.events.audio import listen_to_audio
from frigate.events.cleanup import EventCleanup from frigate.events.cleanup import EventCleanup
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.events.maintainer import EventProcessor from frigate.events.maintainer import EventProcessor
from frigate.log import log_thread
from frigate.models import ( from frigate.models import (
Event, Event,
Export, Export,
@ -58,7 +50,6 @@ from frigate.models import (
from frigate.object_detection import ObjectDetectProcess from frigate.object_detection import ObjectDetectProcess
from frigate.object_processing import TrackedObjectProcessor from frigate.object_processing import TrackedObjectProcessor
from frigate.output.output import output_frames from frigate.output.output import output_frames
from frigate.plus import PlusApi
from frigate.ptz.autotrack import PtzAutoTrackerThread from frigate.ptz.autotrack import PtzAutoTrackerThread
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.record.cleanup import RecordingCleanup from frigate.record.cleanup import RecordingCleanup
@ -70,8 +61,7 @@ from frigate.stats.util import stats_init
from frigate.storage import StorageMaintainer from frigate.storage import StorageMaintainer
from frigate.timeline import TimelineProcessor from frigate.timeline import TimelineProcessor
from frigate.types import CameraMetricsTypes, PTZMetricsTypes from frigate.types import CameraMetricsTypes, PTZMetricsTypes
from frigate.util.builtin import empty_and_close_queue, save_default_config from frigate.util.builtin import empty_and_close_queue
from frigate.util.config import migrate_frigate_config
from frigate.util.object import get_camera_regions_grid from frigate.util.object import get_camera_regions_grid
from frigate.version import VERSION from frigate.version import VERSION
from frigate.video import capture_camera, track_camera from frigate.video import capture_camera, track_camera
@ -81,22 +71,19 @@ logger = logging.getLogger(__name__)
class FrigateApp: class FrigateApp:
def __init__(self) -> None: # TODO: Fix FrigateConfig usage, so we can properly annotate it here without mypy erroring out.
def __init__(self, config: Any) -> None:
self.stop_event: MpEvent = mp.Event() self.stop_event: MpEvent = mp.Event()
self.detection_queue: Queue = mp.Queue() self.detection_queue: Queue = mp.Queue()
self.detectors: dict[str, ObjectDetectProcess] = {} self.detectors: dict[str, ObjectDetectProcess] = {}
self.detection_out_events: dict[str, MpEvent] = {} self.detection_out_events: dict[str, MpEvent] = {}
self.detection_shms: list[mp.shared_memory.SharedMemory] = [] self.detection_shms: list[mp.shared_memory.SharedMemory] = []
self.log_queue: Queue = mp.Queue() self.log_queue: Queue = mp.Queue()
self.plus_api = PlusApi()
self.camera_metrics: dict[str, CameraMetricsTypes] = {} self.camera_metrics: dict[str, CameraMetricsTypes] = {}
self.ptz_metrics: dict[str, PTZMetricsTypes] = {} self.ptz_metrics: dict[str, PTZMetricsTypes] = {}
self.processes: dict[str, int] = {} self.processes: dict[str, int] = {}
self.region_grids: dict[str, list[list[dict[str, int]]]] = {} self.region_grids: dict[str, list[list[dict[str, int]]]] = {}
self.config = config
def set_environment_vars(self) -> None:
for key, value in self.config.environment_vars.items():
os.environ[key] = value
def ensure_dirs(self) -> None: def ensure_dirs(self) -> None:
for d in [ for d in [
@ -113,24 +100,7 @@ class FrigateApp:
else: else:
logger.debug(f"Skipping directory: {d}") logger.debug(f"Skipping directory: {d}")
def init_config(self) -> None: def init_camera_metrics(self) -> None:
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
# Check if we can use .yaml instead of .yml
config_file_yaml = config_file.replace(".yml", ".yaml")
if os.path.isfile(config_file_yaml):
config_file = config_file_yaml
if not os.path.isfile(config_file):
print("No config file found, saving default config")
config_file = config_file_yaml
save_default_config(config_file)
# check if the config file needs to be migrated
migrate_frigate_config(config_file)
self.config = FrigateConfig.parse_file(config_file, plus_api=self.plus_api)
for camera_name in self.config.cameras.keys(): for camera_name in self.config.cameras.keys():
# create camera_metrics # create camera_metrics
self.camera_metrics[camera_name] = { self.camera_metrics[camera_name] = {
@ -190,17 +160,6 @@ class FrigateApp:
} }
self.ptz_metrics[camera_name]["ptz_motor_stopped"].set() self.ptz_metrics[camera_name]["ptz_motor_stopped"].set()
def set_log_levels(self) -> None:
logging.getLogger().setLevel(self.config.logger.default.value.upper())
for log, level in self.config.logger.logs.items():
logging.getLogger(log).setLevel(level.value.upper())
if "werkzeug" not in self.config.logger.logs:
logging.getLogger("werkzeug").setLevel("ERROR")
if "ws4py" not in self.config.logger.logs:
logging.getLogger("ws4py").setLevel("ERROR")
def init_queues(self) -> None: def init_queues(self) -> None:
# Queue for cameras to push tracked objects to # Queue for cameras to push tracked objects to
self.detected_frames_queue: Queue = mp.Queue( self.detected_frames_queue: Queue = mp.Queue(
@ -374,19 +333,6 @@ class FrigateApp:
self.inter_config_updater = ConfigPublisher() self.inter_config_updater = ConfigPublisher()
self.inter_zmq_proxy = ZmqProxy() self.inter_zmq_proxy = ZmqProxy()
def init_web_server(self) -> None:
self.flask_app = create_app(
self.config,
self.db,
self.embeddings,
self.detected_frames_processor,
self.storage_maintainer,
self.onvif_controller,
self.external_event_processor,
self.plus_api,
self.stats_emitter,
)
def init_onvif(self) -> None: def init_onvif(self) -> None:
self.onvif_controller = OnvifController(self.config, self.ptz_metrics) self.onvif_controller = OnvifController(self.config, self.ptz_metrics)
@ -527,7 +473,7 @@ class FrigateApp:
capture_process = mp.Process( capture_process = mp.Process(
target=capture_camera, target=capture_camera,
name=f"camera_capture:{name}", name=f"camera_capture:{name}",
args=(name, config, self.shm_frame_count, self.camera_metrics[name]), args=(name, config, self.shm_frame_count(), self.camera_metrics[name]),
) )
capture_process.daemon = True capture_process.daemon = True
self.camera_metrics[name]["capture_process"] = capture_process self.camera_metrics[name]["capture_process"] = capture_process
@ -590,7 +536,7 @@ class FrigateApp:
self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event) self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event)
self.frigate_watchdog.start() self.frigate_watchdog.start()
def check_shm(self) -> None: def shm_frame_count(self) -> int:
total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1) total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1)
# required for log files + nginx cache # required for log files + nginx cache
@ -610,17 +556,19 @@ class FrigateApp:
1, 1,
) )
self.shm_frame_count = min(50, int(available_shm / (cam_total_frame_size))) shm_frame_count = min(50, int(available_shm / (cam_total_frame_size)))
logger.debug( logger.debug(
f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {self.shm_frame_count} frames for each camera in SHM" f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {shm_frame_count} frames for each camera in SHM"
) )
if self.shm_frame_count < 10: if shm_frame_count < 10:
logger.warning( logger.warning(
f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + cam_total_frame_size)}MB." f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + cam_total_frame_size)}MB."
) )
return shm_frame_count
def init_auth(self) -> None: def init_auth(self) -> None:
if self.config.auth.enabled: if self.config.auth.enabled:
if User.select().count() == 0: if User.select().count() == 0:
@ -657,49 +605,15 @@ class FrigateApp:
logger.info("********************************************************") logger.info("********************************************************")
logger.info("********************************************************") logger.info("********************************************************")
@log_thread()
def start(self) -> None: def start(self) -> None:
parser = argparse.ArgumentParser(
prog="Frigate",
description="An NVR with realtime local object detection for IP cameras.",
)
parser.add_argument("--validate-config", action="store_true")
args = parser.parse_args()
logger.info(f"Starting Frigate ({VERSION})") logger.info(f"Starting Frigate ({VERSION})")
try: # Ensure global state.
self.ensure_dirs() self.ensure_dirs()
try: self.config.install()
self.init_config()
except Exception as e: # Start frigate services.
print("*************************************************************") self.init_camera_metrics()
print("*************************************************************")
print("*** Your config file is not valid! ***")
print("*** Please check the docs at ***")
print("*** https://docs.frigate.video/configuration/index ***")
print("*************************************************************")
print("*************************************************************")
print("*** Config Validation Errors ***")
print("*************************************************************")
if isinstance(e, ValidationError):
for error in e.errors():
location = ".".join(str(item) for item in error["loc"])
print(f"{location}: {error['msg']}")
else:
print(e)
print(traceback.format_exc())
print("*************************************************************")
print("*** End Config Validation Errors ***")
print("*************************************************************")
sys.exit(1)
if args.validate_config:
print("*************************************************************")
print("*** Your config file is valid. ***")
print("*************************************************************")
sys.exit(0)
self.set_environment_vars()
self.set_log_levels()
self.init_queues() self.init_queues()
self.init_database() self.init_database()
self.init_onvif() self.init_onvif()
@ -711,42 +625,37 @@ class FrigateApp:
self.check_db_data_migrations() self.check_db_data_migrations()
self.init_inter_process_communicator() self.init_inter_process_communicator()
self.init_dispatcher() self.init_dispatcher()
except Exception as e:
print(e)
sys.exit(1)
self.start_detectors() self.start_detectors()
self.start_video_output_processor() self.start_video_output_processor()
self.start_ptz_autotracker() self.start_ptz_autotracker()
self.init_historical_regions() self.init_historical_regions()
self.start_detected_frames_processor() self.start_detected_frames_processor()
self.start_camera_processors() self.start_camera_processors()
self.check_shm()
self.start_camera_capture_processes() self.start_camera_capture_processes()
self.start_audio_processors() self.start_audio_processors()
self.start_storage_maintainer() self.start_storage_maintainer()
self.init_external_event_processor() self.init_external_event_processor()
self.start_stats_emitter() self.start_stats_emitter()
self.init_web_server()
self.start_timeline_processor() self.start_timeline_processor()
self.start_event_processor() self.start_event_processor()
self.start_event_cleanup() self.start_event_cleanup()
self.start_record_cleanup() self.start_record_cleanup()
self.start_watchdog() self.start_watchdog()
self.init_auth() self.init_auth()
# Flask only listens for SIGINT, so we need to catch SIGTERM and send SIGINT
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
os.kill(os.getpid(), signal.SIGINT)
signal.signal(signal.SIGTERM, receiveSignal)
try: try:
self.flask_app.run(host="127.0.0.1", port=5001, debug=False, threaded=True) create_app(
except KeyboardInterrupt: self.config,
pass self.db,
self.embeddings,
logger.info("Flask has exited...") self.detected_frames_processor,
self.storage_maintainer,
self.onvif_controller,
self.external_event_processor,
self.stats_emitter,
).run(host="127.0.0.1", port=5001, debug=False, threaded=True)
finally:
self.stop() self.stop()
def stop(self) -> None: def stop(self) -> None:

View File

@ -45,13 +45,18 @@ from frigate.ffmpeg_presets import (
parse_preset_input, parse_preset_input,
parse_preset_output_record, parse_preset_output_record,
) )
from frigate.plus import PlusApi
from frigate.util.builtin import ( from frigate.util.builtin import (
deep_merge, deep_merge,
escape_special_characters, escape_special_characters,
generate_color_palette, generate_color_palette,
get_ffmpeg_arg_list, get_ffmpeg_arg_list,
) )
from frigate.util.config import StreamInfoRetriever, get_relative_coordinates from frigate.util.config import (
StreamInfoRetriever,
get_relative_coordinates,
migrate_frigate_config,
)
from frigate.util.image import create_mask from frigate.util.image import create_mask
from frigate.util.services import auto_detect_hwaccel from frigate.util.services import auto_detect_hwaccel
@ -59,6 +64,25 @@ logger = logging.getLogger(__name__)
yaml = YAML() yaml = YAML()
DEFAULT_CONFIG_FILES = ["/config/config.yaml", "/config/config.yml"]
DEFAULT_CONFIG = """
mqtt:
enabled: False
cameras:
name_of_your_camera: # <------ Name the camera
enabled: True
ffmpeg:
inputs:
- path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection
roles:
- detect
detect:
enabled: False # <---- disable detection until you have a working camera feed
width: 1280
height: 720
"""
# TODO: Identify what the default format to display timestamps is # TODO: Identify what the default format to display timestamps is
DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S" DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
# German Style: # German Style:
@ -1272,6 +1296,19 @@ class LoggerConfig(FrigateBaseModel):
default_factory=dict, title="Log level for specified processes." default_factory=dict, title="Log level for specified processes."
) )
def install(self):
"""Install global logging state."""
logging.getLogger().setLevel(self.default.value.upper())
log_levels = {
"werkzeug": LogLevelEnum.error,
"ws4py": LogLevelEnum.error,
**self.logs,
}
for log, level in log_levels.items():
logging.getLogger(log).setLevel(level.value.upper())
class CameraGroupConfig(FrigateBaseModel): class CameraGroupConfig(FrigateBaseModel):
"""Represents a group of cameras.""" """Represents a group of cameras."""
@ -1492,11 +1529,22 @@ class FrigateConfig(FrigateBaseModel):
) )
version: Optional[str] = Field(default=None, title="Current config version.") version: Optional[str] = Field(default=None, title="Current config version.")
_plus_api: PlusApi
@property
def plus_api(self) -> PlusApi:
return self._plus_api
@model_validator(mode="after") @model_validator(mode="after")
def post_validation(self, info: ValidationInfo) -> Self: def post_validation(self, info: ValidationInfo) -> Self:
plus_api = None # Load plus api from context, if possible.
self._plus_api = None
if isinstance(info.context, dict): if isinstance(info.context, dict):
plus_api = info.context.get("plus_api") self._plus_api = info.context.get("plus_api")
# Ensure self._plus_api is set, if no explicit value is provided.
if self._plus_api is None:
self._plus_api = PlusApi()
# set notifications state # set notifications state
self.notifications.enabled_in_config = self.notifications.enabled self.notifications.enabled_in_config = self.notifications.enabled
@ -1691,7 +1739,7 @@ class FrigateConfig(FrigateBaseModel):
enabled_labels.update(camera.objects.track) enabled_labels.update(camera.objects.track)
self.model.create_colormap(sorted(enabled_labels)) self.model.create_colormap(sorted(enabled_labels))
self.model.check_and_load_plus_model(plus_api) self.model.check_and_load_plus_model(self.plus_api)
for key, detector in self.detectors.items(): for key, detector in self.detectors.items():
adapter = TypeAdapter(DetectorConfig) adapter = TypeAdapter(DetectorConfig)
@ -1726,7 +1774,7 @@ class FrigateConfig(FrigateBaseModel):
detector_config.model = ModelConfig.model_validate(merged_model) detector_config.model = ModelConfig.model_validate(merged_model)
detector_config.model.check_and_load_plus_model( detector_config.model.check_and_load_plus_model(
plus_api, detector_config.type self.plus_api, detector_config.type
) )
detector_config.model.compute_model_hash() detector_config.model.compute_model_hash()
self.detectors[key] = detector_config self.detectors[key] = detector_config
@ -1743,8 +1791,38 @@ class FrigateConfig(FrigateBaseModel):
return v return v
@classmethod @classmethod
def parse_file(cls, config_path, **kwargs): def load(cls, **kwargs):
with open(config_path) as f: config_path = os.environ.get("CONFIG_FILE")
# No explicit configuration file, try to find one in the default paths.
if config_path is None:
for path in DEFAULT_CONFIG_FILES:
if os.path.isfile(path):
config_path = path
break
# No configuration file found, create one.
new_config = False
if config_path is None:
logger.info("No config file found, saving default config")
config_path = DEFAULT_CONFIG_FILES[-1]
new_config = True
else:
# Check if the config file needs to be migrated.
migrate_frigate_config(config_path)
# Finally, load the resulting configuration file.
with open(config_path, "a+") as f:
# Only write the default config if the opened file is non-empty. This can happen as
# a race condition. It's extremely unlikely, but eh. Might as well check it.
if new_config and f.tell() == 0:
f.write(DEFAULT_CONFIG)
logger.info(
"Created default config file, see the getting started docs \
for configuration https://docs.frigate.video/guides/getting_started"
)
f.seek(0)
return FrigateConfig.parse(f, **kwargs) return FrigateConfig.parse(f, **kwargs)
@classmethod @classmethod
@ -1775,10 +1853,17 @@ class FrigateConfig(FrigateBaseModel):
# Validate and return the config dict. # Validate and return the config dict.
return cls.parse_object(config, **context) return cls.parse_object(config, **context)
@classmethod
def parse_object(cls, obj: Any, **context):
return cls.model_validate(obj, context=context)
@classmethod @classmethod
def parse_yaml(cls, config_yaml, **context): def parse_yaml(cls, config_yaml, **context):
return cls.parse(config_yaml, is_json=False, **context) return cls.parse(config_yaml, is_json=False, **context)
@classmethod
def parse_object(cls, obj: Any, *, plus_api: Optional[PlusApi] = None):
return cls.model_validate(obj, context={"plus_api": plus_api})
def install(self):
"""Install global state from the config."""
self.logger.install()
for key, value in self.environment_vars.items():
os.environ[key] = value

View File

@ -76,16 +76,8 @@ def listen_to_audio(
stop_event = mp.Event() stop_event = mp.Event()
audio_threads: list[threading.Thread] = [] audio_threads: list[threading.Thread] = []
def exit_process() -> None:
for thread in audio_threads:
thread.join()
logger.info("Exiting audio detector...")
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
logger.debug(f"Audio process received signal {signalNumber}")
stop_event.set() stop_event.set()
exit_process()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)
signal.signal(signal.SIGINT, receiveSignal) signal.signal(signal.SIGINT, receiveSignal)
@ -104,6 +96,11 @@ def listen_to_audio(
audio_threads.append(audio) audio_threads.append(audio)
audio.start() audio.start()
for thread in audio_threads:
thread.join()
logger.info("Exiting audio detector...")
class AudioTfl: class AudioTfl:
def __init__(self, stop_event: mp.Event, num_threads=2): def __init__(self, stop_event: mp.Event, num_threads=2):

View File

@ -2,6 +2,7 @@ import atexit
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import os import os
import sys
import threading import threading
from collections import deque from collections import deque
from contextlib import AbstractContextManager, ContextDecorator from contextlib import AbstractContextManager, ContextDecorator
@ -68,6 +69,19 @@ class log_thread(AbstractContextManager, ContextDecorator):
self._stop_thread() self._stop_thread()
# When a multiprocessing.Process exits, python tries to flush stdout and stderr. However, if the
# process is created after a thread (for example a logging thread) is created and the process fork
# happens while an internal lock is held, the stdout/err flush can cause a deadlock.
#
# https://github.com/python/cpython/issues/91776
def reopen_std_streams() -> None:
sys.stdout = os.fdopen(1, "w")
sys.stderr = os.fdopen(2, "w")
os.register_at_fork(after_in_child=reopen_std_streams)
# based on https://codereview.stackexchange.com/a/17959 # based on https://codereview.stackexchange.com/a/17959
class LogPipe(threading.Thread): class LogPipe(threading.Thread):
def __init__(self, log_name: str): def __init__(self, log_name: str):

View File

@ -92,7 +92,6 @@ def run_detector(
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber, frame): def receiveSignal(signalNumber, frame):
logger.info("Signal to exit detection process...")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -38,7 +38,6 @@ def output_frames(
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber, frame): def receiveSignal(signalNumber, frame):
logger.debug(f"Output frames process received signal {signalNumber}")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -22,7 +22,6 @@ def manage_recordings(config: FrigateConfig) -> None:
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
logger.debug(f"Recording manager process received signal {signalNumber}")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -20,7 +20,6 @@ def manage_review_segments(config: FrigateConfig) -> None:
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
logger.debug(f"Manage review segments process received signal {signalNumber}")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -13,7 +13,6 @@ from playhouse.sqliteq import SqliteQueueDatabase
from frigate.api.app import create_app from frigate.api.app import create_app
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.models import Event, Recordings from frigate.models import Event, Recordings
from frigate.plus import PlusApi
from frigate.stats.emitter import StatsEmitter from frigate.stats.emitter import StatsEmitter
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
@ -121,7 +120,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -158,7 +156,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -180,7 +177,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -201,7 +197,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -224,7 +219,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -251,7 +245,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
morning_id = "123456.random" morning_id = "123456.random"
@ -290,7 +283,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -326,7 +318,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -352,7 +343,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
@ -370,7 +360,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -392,7 +381,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
stats, stats,
) )

View File

@ -258,37 +258,6 @@ def find_by_key(dictionary, target_key):
return None return None
def save_default_config(location: str) -> None:
try:
with open(location, "w") as f:
f.write(
"""
mqtt:
enabled: False
cameras:
name_of_your_camera: # <------ Name the camera
enabled: True
ffmpeg:
inputs:
- path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection
roles:
- detect
detect:
enabled: False # <---- disable detection until you have a working camera feed
width: 1280
height: 720
"""
)
except PermissionError:
logger.error("Unable to write default config to /config")
return
logger.info(
"Created default config file, see the getting started docs for configuration https://docs.frigate.video/guides/getting_started"
)
def get_tomorrow_at_time(hour: int) -> datetime.datetime: def get_tomorrow_at_time(hour: int) -> datetime.datetime:
"""Returns the datetime of the following day at 2am.""" """Returns the datetime of the following day at 2am."""
try: try:

View File

@ -390,7 +390,6 @@ def capture_camera(name, config: CameraConfig, shm_frame_count: int, process_inf
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber, frame): def receiveSignal(signalNumber, frame):
logger.debug(f"Capture camera received signal {signalNumber}")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)