fix restart failing under non-root

restart_frigate() called psutil.Process(1).terminate() to signal s6-svscan, but s6-svscan runs as root while frigate runs as uid 1000, so the call raised AccessDenied. That exception escaped every caller: the UI restart button dropped its websocket client, MQTT restart and Save & Restart just logged and did nothing, and the watchdog crashed its own monitoring thread on a dead detector. This catches AccessDenied and falls through to the existing SIGINT branch, which exits the process for s6 to restart it.
This commit is contained in:
Josh Hawkins
2026-09-18 06:55:36 -05:00
parent 56c6416556
commit ac0f6c9dd3
2 changed files with 65 additions and 3 deletions
+55
View File
@@ -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)
+10 -3
View File
@@ -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):