Handle SIGINT with forkserver (#18860)

* Pass stopevent from main start

* Share stop event across processes

* preload modules

* remove explicit os._exit call

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
Nicolas Mowen 2025-06-24 11:41:11 -06:00 committed by GitHub
parent a22e24f24b
commit 47a0097e95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 68 additions and 46 deletions

View File

@ -23,6 +23,10 @@ def main() -> None:
setup_logging(manager) setup_logging(manager)
threading.current_thread().name = "frigate" threading.current_thread().name = "frigate"
stop_event = mp.Event()
# send stop event on SIGINT
signal.signal(signal.SIGINT, lambda sig, frame: stop_event.set())
# Make sure we exit cleanly on SIGTERM. # Make sure we exit cleanly on SIGTERM.
signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit()) signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit())
@ -110,9 +114,23 @@ def main() -> None:
sys.exit(0) sys.exit(0)
# Run the main application. # Run the main application.
FrigateApp(config, manager).start() FrigateApp(config, manager, stop_event).start()
if __name__ == "__main__": if __name__ == "__main__":
mp.set_forkserver_preload(
[
# Standard library and core dependencies
"sqlite3",
# Third-party libraries commonly used in Frigate
"numpy",
"cv2",
"peewee",
"zmq",
"ruamel.yaml",
# Frigate core modules
"frigate.camera.maintainer",
]
)
mp.set_start_method("forkserver", force=True) mp.set_start_method("forkserver", force=True)
main() main()

View File

@ -79,10 +79,12 @@ logger = logging.getLogger(__name__)
class FrigateApp: class FrigateApp:
def __init__(self, config: FrigateConfig, manager: SyncManager) -> None: def __init__(
self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent
) -> None:
self.metrics_manager = manager self.metrics_manager = manager
self.audio_process: Optional[mp.Process] = None self.audio_process: Optional[mp.Process] = None
self.stop_event: MpEvent = mp.Event() self.stop_event = stop_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_shms: list[mp.shared_memory.SharedMemory] = [] self.detection_shms: list[mp.shared_memory.SharedMemory] = []
@ -223,14 +225,14 @@ class FrigateApp:
self.processes["go2rtc"] = proc.info["pid"] self.processes["go2rtc"] = proc.info["pid"]
def init_recording_manager(self) -> None: def init_recording_manager(self) -> None:
recording_process = RecordProcess(self.config) recording_process = RecordProcess(self.config, self.stop_event)
self.recording_process = recording_process self.recording_process = recording_process
recording_process.start() recording_process.start()
self.processes["recording"] = recording_process.pid or 0 self.processes["recording"] = recording_process.pid or 0
logger.info(f"Recording process started: {recording_process.pid}") logger.info(f"Recording process started: {recording_process.pid}")
def init_review_segment_manager(self) -> None: def init_review_segment_manager(self) -> None:
review_segment_process = ReviewProcess(self.config) review_segment_process = ReviewProcess(self.config, self.stop_event)
self.review_segment_process = review_segment_process self.review_segment_process = review_segment_process
review_segment_process.start() review_segment_process.start()
self.processes["review_segment"] = review_segment_process.pid or 0 self.processes["review_segment"] = review_segment_process.pid or 0
@ -250,8 +252,7 @@ class FrigateApp:
return return
embedding_process = EmbeddingProcess( embedding_process = EmbeddingProcess(
self.config, self.config, self.embeddings_metrics, self.stop_event
self.embeddings_metrics,
) )
self.embedding_process = embedding_process self.embedding_process = embedding_process
embedding_process.start() embedding_process.start()
@ -387,6 +388,7 @@ class FrigateApp:
list(self.config.cameras.keys()), list(self.config.cameras.keys()),
self.config, self.config,
detector_config, detector_config,
self.stop_event,
) )
def start_ptz_autotracker(self) -> None: def start_ptz_autotracker(self) -> None:
@ -410,7 +412,7 @@ class FrigateApp:
self.detected_frames_processor.start() self.detected_frames_processor.start()
def start_video_output_processor(self) -> None: def start_video_output_processor(self) -> None:
output_processor = OutputProcess(self.config) output_processor = OutputProcess(self.config, self.stop_event)
self.output_processor = output_processor self.output_processor = output_processor
output_processor.start() output_processor.start()
logger.info(f"Output process started: {output_processor.pid}") logger.info(f"Output process started: {output_processor.pid}")
@ -436,7 +438,7 @@ class FrigateApp:
if audio_cameras: if audio_cameras:
self.audio_process = AudioProcessor( self.audio_process = AudioProcessor(
self.config, audio_cameras, self.camera_metrics self.config, audio_cameras, self.camera_metrics, self.stop_event
) )
self.audio_process.start() self.audio_process.start()
self.processes["audio_detector"] = self.audio_process.pid or 0 self.processes["audio_detector"] = self.audio_process.pid or 0
@ -658,4 +660,3 @@ class FrigateApp:
_stop_logging() _stop_logging()
self.metrics_manager.shutdown() self.metrics_manager.shutdown()
os._exit(os.EX_OK)

View File

@ -165,6 +165,7 @@ class CameraMaintainer(threading.Thread):
self.camera_metrics[name], self.camera_metrics[name],
self.ptz_metrics[name], self.ptz_metrics[name],
self.region_grids[name], self.region_grids[name],
self.stop_event,
) )
self.camera_processes[config.name] = camera_process self.camera_processes[config.name] = camera_process
camera_process.start() camera_process.start()
@ -184,7 +185,9 @@ class CameraMaintainer(threading.Thread):
frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1] frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1]
self.frame_manager.create(f"{config.name}_frame{i}", frame_size) self.frame_manager.create(f"{config.name}_frame{i}", frame_size)
capture_process = CameraCapture(config, count, self.camera_metrics[name]) capture_process = CameraCapture(
config, count, self.camera_metrics[name], self.stop_event
)
capture_process.daemon = True capture_process.daemon = True
self.capture_processes[name] = capture_process self.capture_processes[name] = capture_process
capture_process.start() capture_process.start()

View File

@ -5,6 +5,7 @@ import json
import logging import logging
import os import os
import threading import threading
from multiprocessing.synchronize import Event as MpEvent
from typing import Any, Union from typing import Any, Union
import regex import regex
@ -28,9 +29,12 @@ logger = logging.getLogger(__name__)
class EmbeddingProcess(FrigateProcess): class EmbeddingProcess(FrigateProcess):
def __init__( def __init__(
self, config: FrigateConfig, metrics: DataProcessorMetrics | None self,
config: FrigateConfig,
metrics: DataProcessorMetrics | None,
stop_event: MpEvent,
) -> None: ) -> None:
super().__init__(name="frigate.embeddings_manager", daemon=True) super().__init__(stop_event, name="frigate.embeddings_manager", daemon=True)
self.config = config self.config = config
self.metrics = metrics self.metrics = metrics

View File

@ -7,6 +7,7 @@ import string
import threading import threading
import time import time
from multiprocessing.managers import DictProxy from multiprocessing.managers import DictProxy
from multiprocessing.synchronize import Event as MpEvent
from typing import Any, Tuple from typing import Any, Tuple
import numpy as np import numpy as np
@ -84,8 +85,9 @@ class AudioProcessor(FrigateProcess):
config: FrigateConfig, config: FrigateConfig,
cameras: list[CameraConfig], cameras: list[CameraConfig],
camera_metrics: DictProxy, camera_metrics: DictProxy,
stop_event: MpEvent,
): ):
super().__init__(name="frigate.audio_manager", daemon=True) super().__init__(stop_event, name="frigate.audio_manager", daemon=True)
self.camera_metrics = camera_metrics self.camera_metrics = camera_metrics
self.cameras = cameras self.cameras = cameras

View File

@ -95,8 +95,9 @@ class DetectorRunner(FrigateProcess):
start_time: Value, start_time: Value,
config: FrigateConfig, config: FrigateConfig,
detector_config: BaseDetectorConfig, detector_config: BaseDetectorConfig,
stop_event: MpEvent,
) -> None: ) -> None:
super().__init__(name=name, daemon=True) super().__init__(stop_event, name=name, daemon=True)
self.detection_queue = detection_queue self.detection_queue = detection_queue
self.cameras = cameras self.cameras = cameras
self.avg_speed = avg_speed self.avg_speed = avg_speed
@ -166,6 +167,7 @@ class ObjectDetectProcess:
cameras: list[str], cameras: list[str],
config: FrigateConfig, config: FrigateConfig,
detector_config: BaseDetectorConfig, detector_config: BaseDetectorConfig,
stop_event: MpEvent,
): ):
self.name = name self.name = name
self.cameras = cameras self.cameras = cameras
@ -175,6 +177,7 @@ class ObjectDetectProcess:
self.detect_process: FrigateProcess | None = None self.detect_process: FrigateProcess | None = None
self.config = config self.config = config
self.detector_config = detector_config self.detector_config = detector_config
self.stop_event = stop_event
self.start_or_restart() self.start_or_restart()
def stop(self): def stop(self):
@ -202,6 +205,7 @@ class ObjectDetectProcess:
self.detection_start, self.detection_start,
self.config, self.config,
self.detector_config, self.detector_config,
self.stop_event,
) )
self.detect_process.start() self.detect_process.start()

View File

@ -5,6 +5,7 @@ import logging
import os import os
import shutil import shutil
import threading import threading
from multiprocessing.synchronize import Event as MpEvent
from wsgiref.simple_server import make_server from wsgiref.simple_server import make_server
from ws4py.server.wsgirefserver import ( from ws4py.server.wsgirefserver import (
@ -72,8 +73,8 @@ def check_disabled_camera_update(
class OutputProcess(FrigateProcess): class OutputProcess(FrigateProcess):
def __init__(self, config: FrigateConfig) -> None: def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None:
super().__init__(name="frigate.output", daemon=True) super().__init__(stop_event, name="frigate.output", daemon=True)
self.config = config self.config = config
def run(self) -> None: def run(self) -> None:

View File

@ -1,6 +1,7 @@
"""Run recording maintainer and cleanup.""" """Run recording maintainer and cleanup."""
import logging import logging
from multiprocessing.synchronize import Event as MpEvent
from playhouse.sqliteq import SqliteQueueDatabase from playhouse.sqliteq import SqliteQueueDatabase
@ -13,8 +14,8 @@ logger = logging.getLogger(__name__)
class RecordProcess(FrigateProcess): class RecordProcess(FrigateProcess):
def __init__(self, config: FrigateConfig) -> None: def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None:
super().__init__(name="frigate.recording_manager", daemon=True) super().__init__(stop_event, name="frigate.recording_manager", daemon=True)
self.config = config self.config = config
def run(self) -> None: def run(self) -> None:

View File

@ -1,6 +1,7 @@
"""Run recording maintainer and cleanup.""" """Run recording maintainer and cleanup."""
import logging import logging
from multiprocessing.synchronize import Event as MpEvent
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.review.maintainer import ReviewSegmentMaintainer from frigate.review.maintainer import ReviewSegmentMaintainer
@ -10,8 +11,8 @@ logger = logging.getLogger(__name__)
class ReviewProcess(FrigateProcess): class ReviewProcess(FrigateProcess):
def __init__(self, config: FrigateConfig) -> None: def __init__(self, config: FrigateConfig, stop_event: MpEvent) -> None:
super().__init__(name="frigate.review_segment_manager", daemon=True) super().__init__(stop_event, name="frigate.review_segment_manager", daemon=True)
self.config = config self.config = config
def run(self) -> None: def run(self) -> None:

View File

@ -1,10 +1,9 @@
import faulthandler import faulthandler
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import signal
import sys
import threading import threading
from logging.handlers import QueueHandler from logging.handlers import QueueHandler
from multiprocessing.synchronize import Event as MpEvent
from typing import Callable, Optional from typing import Callable, Optional
from setproctitle import setproctitle from setproctitle import setproctitle
@ -16,6 +15,7 @@ from frigate.config.logger import LoggerConfig
class BaseProcess(mp.Process): class BaseProcess(mp.Process):
def __init__( def __init__(
self, self,
stop_event: MpEvent,
*, *,
name: Optional[str] = None, name: Optional[str] = None,
target: Optional[Callable] = None, target: Optional[Callable] = None,
@ -23,6 +23,7 @@ class BaseProcess(mp.Process):
kwargs: dict = {}, kwargs: dict = {},
daemon: Optional[bool] = None, daemon: Optional[bool] = None,
): ):
self.stop_event = stop_event
super().__init__( super().__init__(
name=name, target=target, args=args, kwargs=kwargs, daemon=daemon name=name, target=target, args=args, kwargs=kwargs, daemon=daemon
) )
@ -42,14 +43,6 @@ class BaseProcess(mp.Process):
class FrigateProcess(BaseProcess): class FrigateProcess(BaseProcess):
logger: logging.Logger logger: logging.Logger
@property
def stop_event(self) -> threading.Event:
# Lazily create the stop_event. This allows the signal handler to tell if anyone is
# monitoring the stop event, and to raise a SystemExit if not.
if "stop_event" not in self.__dict__:
self.__dict__["stop_event"] = threading.Event()
return self.__dict__["stop_event"]
def before_start(self) -> None: def before_start(self) -> None:
self.__log_queue = frigate.log.log_listener.queue self.__log_queue = frigate.log.log_listener.queue
@ -58,18 +51,7 @@ class FrigateProcess(BaseProcess):
threading.current_thread().name = f"process:{self.name}" threading.current_thread().name = f"process:{self.name}"
faulthandler.enable() faulthandler.enable()
def receiveSignal(signalNumber, frame): # setup logging
# Get the stop_event through the dict to bypass lazy initialization.
stop_event = self.__dict__.get("stop_event")
if stop_event is not None:
# Someone is monitoring stop_event. We should set it.
stop_event.set()
else:
# Nobody is monitoring stop_event. We should raise SystemExit.
sys.exit()
signal.signal(signal.SIGTERM, receiveSignal)
signal.signal(signal.SIGINT, receiveSignal)
self.logger = logging.getLogger(self.name) self.logger = logging.getLogger(self.name)
logging.basicConfig(handlers=[], force=True) logging.basicConfig(handlers=[], force=True)
logging.getLogger().addHandler(QueueHandler(self.__log_queue)) logging.getLogger().addHandler(QueueHandler(self.__log_queue))

View File

@ -439,9 +439,13 @@ class CameraCaptureRunner(threading.Thread):
class CameraCapture(FrigateProcess): class CameraCapture(FrigateProcess):
def __init__( def __init__(
self, config: CameraConfig, shm_frame_count: int, camera_metrics: CameraMetrics self,
config: CameraConfig,
shm_frame_count: int,
camera_metrics: CameraMetrics,
stop_event: MpEvent,
) -> None: ) -> None:
super().__init__(name=f"frigate.capture:{config.name}", daemon=True) super().__init__(stop_event, name=f"frigate.capture:{config.name}", daemon=True)
self.config = config self.config = config
self.shm_frame_count = shm_frame_count self.shm_frame_count = shm_frame_count
self.camera_metrics = camera_metrics self.camera_metrics = camera_metrics
@ -472,8 +476,9 @@ class CameraTracker(FrigateProcess):
camera_metrics: CameraMetrics, camera_metrics: CameraMetrics,
ptz_metrics: PTZMetrics, ptz_metrics: PTZMetrics,
region_grid: list[list[dict[str, Any]]], region_grid: list[list[dict[str, Any]]],
stop_event: MpEvent,
) -> None: ) -> None:
super().__init__(name=f"frigate.process:{config.name}", daemon=True) super().__init__(stop_event, name=f"frigate.process:{config.name}", daemon=True)
self.config = config self.config = config
self.model_config = model_config self.model_config = model_config
self.labelmap = labelmap self.labelmap = labelmap