mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-06 06:42:51 +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():
|
def restart_frigate():
|
||||||
proc = psutil.Process(1)
|
proc = psutil.Process(1)
|
||||||
|
|
||||||
# if this is running via s6, sigterm pid 1
|
# if this is running via s6, sigterm pid 1
|
||||||
if proc.name() == "s6-svscan":
|
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
|
# otherwise, just try and exit frigate
|
||||||
else:
|
os.kill(os.getpid(), signal.SIGINT)
|
||||||
os.kill(os.getpid(), signal.SIGINT)
|
|
||||||
|
|
||||||
|
|
||||||
def print_stack(sig, frame):
|
def print_stack(sig, frame):
|
||||||
|
|||||||
Reference in New Issue
Block a user