diff --git a/frigate/test/test_restart_frigate.py b/frigate/test/test_restart_frigate.py new file mode 100644 index 0000000000..5b8de5f90e --- /dev/null +++ b/frigate/test/test_restart_frigate.py @@ -0,0 +1,55 @@ +"""Tests for restarting frigate under s6.""" + +import signal +import unittest +from unittest.mock import MagicMock, patch + +import psutil + +from frigate.util.services import restart_frigate + + +class TestRestartFrigate(unittest.TestCase): + def _s6_process(self) -> MagicMock: + proc = MagicMock() + proc.name.return_value = "s6-svscan" + return proc + + @patch("frigate.util.services.os.kill") + @patch("frigate.util.services.psutil.Process") + def test_terminates_s6_when_permitted(self, mock_process, mock_kill): + proc = self._s6_process() + mock_process.return_value = proc + + restart_frigate() + + proc.terminate.assert_called_once() + mock_kill.assert_not_called() + + @patch("frigate.util.services.os.getpid", return_value=99) + @patch("frigate.util.services.os.kill") + @patch("frigate.util.services.psutil.Process") + def test_exits_self_when_s6_signal_is_denied( + self, mock_process, mock_kill, _mock_getpid + ): + """Running unprivileged, frigate cannot signal root's s6-svscan.""" + proc = self._s6_process() + proc.terminate.side_effect = psutil.AccessDenied(pid=1, name="s6-svscan") + mock_process.return_value = proc + + restart_frigate() + + mock_kill.assert_called_once_with(99, signal.SIGINT) + + @patch("frigate.util.services.os.getpid", return_value=99) + @patch("frigate.util.services.os.kill") + @patch("frigate.util.services.psutil.Process") + def test_exits_self_without_s6(self, mock_process, mock_kill, _mock_getpid): + proc = MagicMock() + proc.name.return_value = "init" + mock_process.return_value = proc + + restart_frigate() + + proc.terminate.assert_not_called() + mock_kill.assert_called_once_with(99, signal.SIGINT) diff --git a/frigate/util/services.py b/frigate/util/services.py index 7f39601851..eea71e62c3 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -35,12 +35,19 @@ logger = logging.getLogger(__name__) def restart_frigate(): proc = psutil.Process(1) + # if this is running via s6, sigterm pid 1 if proc.name() == "s6-svscan": - proc.terminate() + try: + proc.terminate() + return + except psutil.AccessDenied: + # frigate runs unprivileged, so it cannot signal root's s6-svscan. + # exiting this process instead runs frigate/finish, which halts s6 + logger.debug("Not permitted to signal s6-svscan, exiting instead") + # otherwise, just try and exit frigate - else: - os.kill(os.getpid(), signal.SIGINT) + os.kill(os.getpid(), signal.SIGINT) def print_stack(sig, frame):