mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 00:18:58 +03:00
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:
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user