Compare commits

...

3 Commits

Author SHA1 Message Date
Josh Hawkins
77831304a7
Camera access fixes (#22987)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* only send monitoring notifications to users with camera access

* check access to similarity search event id camera

* require admin role for storage usage endpoint

* check camera access for jsmpeg and birdseye cameras

* tests

* formatting
2026-04-23 12:27:49 -06:00
Josh Hawkins
1a6d04fde7
use object-anchored snapshot crops for classification wizard examples (#22985) 2026-04-23 08:53:48 -05:00
Josh Hawkins
4a1b7a1629
enforce python-level timeout on ffprobe subprocesses (#22984) 2026-04-23 07:16:22 -06:00
14 changed files with 342 additions and 62 deletions

View File

@ -754,6 +754,15 @@ def events_search(
status_code=404, status_code=404,
) )
if search_event.camera not in allowed_cameras:
return JSONResponse(
content={
"success": False,
"message": "Event not found",
},
status_code=404,
)
thumb_result = context.search_thumbnail(search_event) thumb_result = context.search_thumbnail(search_event)
thumb_ids = {result[0]: result[1] for result in thumb_result} thumb_ids = {result[0]: result[1] for result in thumb_result}
search_results = { search_results = {

View File

@ -35,7 +35,7 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.recordings]) router = APIRouter(tags=[Tags.recordings])
@router.get("/recordings/storage", dependencies=[Depends(allow_any_authenticated())]) @router.get("/recordings/storage", dependencies=[Depends(require_role(["admin"]))])
def get_recordings_storage_usage(request: Request): def get_recordings_storage_usage(request: Request):
recording_stats = request.app.stats_emitter.get_latest_stats()["service"][ recording_stats = request.app.stats_emitter.get_latest_stats()["service"][
"storage" "storage"

View File

@ -549,6 +549,14 @@ class WebPushClient(Communicator):
logger.debug(f"Sending camera monitoring push notification for {camera_name}") logger.debug(f"Sending camera monitoring push notification for {camera_name}")
for user in self.web_pushers: for user in self.web_pushers:
if not self._user_has_camera_access(user, camera):
logger.debug(
"Skipping notification for user %s - no access to camera %s",
user,
camera,
)
continue
self.send_push_notification( self.send_push_notification(
user=user, user=user,
payload=payload, payload=payload,

View File

@ -19,6 +19,7 @@ import numpy as np
from frigate.comms.inter_process import InterProcessRequestor from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import BirdseyeModeEnum, FfmpegConfig, FrigateConfig from frigate.config import BirdseyeModeEnum, FfmpegConfig, FrigateConfig
from frigate.const import BASE_DIR, BIRDSEYE_PIPE, INSTALL_DIR, UPDATE_BIRDSEYE_LAYOUT from frigate.const import BASE_DIR, BIRDSEYE_PIPE, INSTALL_DIR, UPDATE_BIRDSEYE_LAYOUT
from frigate.output.ws_auth import ws_has_camera_access
from frigate.util.image import ( from frigate.util.image import (
SharedMemoryFrameManager, SharedMemoryFrameManager,
copy_yuv_to_position, copy_yuv_to_position,
@ -236,12 +237,14 @@ class BroadcastThread(threading.Thread):
converter: FFMpegConverter, converter: FFMpegConverter,
websocket_server: Any, websocket_server: Any,
stop_event: MpEvent, stop_event: MpEvent,
config: FrigateConfig,
): ):
super().__init__() super().__init__()
self.camera = camera self.camera = camera
self.converter = converter self.converter = converter
self.websocket_server = websocket_server self.websocket_server = websocket_server
self.stop_event = stop_event self.stop_event = stop_event
self.config = config
def run(self) -> None: def run(self) -> None:
while not self.stop_event.is_set(): while not self.stop_event.is_set():
@ -256,6 +259,7 @@ class BroadcastThread(threading.Thread):
if ( if (
not ws.terminated not ws.terminated
and ws.environ["PATH_INFO"] == f"/{self.camera}" and ws.environ["PATH_INFO"] == f"/{self.camera}"
and ws_has_camera_access(ws, self.camera, self.config)
): ):
try: try:
ws.send(buf, binary=True) ws.send(buf, binary=True)
@ -806,7 +810,11 @@ class Birdseye:
config.birdseye.restream, config.birdseye.restream,
) )
self.broadcaster = BroadcastThread( self.broadcaster = BroadcastThread(
"birdseye", self.converter, websocket_server, stop_event "birdseye",
self.converter,
websocket_server,
stop_event,
config,
) )
self.birdseye_manager = BirdsEyeFrameManager(self.config, stop_event) self.birdseye_manager = BirdsEyeFrameManager(self.config, stop_event)
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()

View File

@ -7,7 +7,8 @@ import threading
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from typing import Any from typing import Any
from frigate.config import CameraConfig, FfmpegConfig from frigate.config import CameraConfig, FfmpegConfig, FrigateConfig
from frigate.output.ws_auth import ws_has_camera_access
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -102,12 +103,14 @@ class BroadcastThread(threading.Thread):
converter: FFMpegConverter, converter: FFMpegConverter,
websocket_server: Any, websocket_server: Any,
stop_event: MpEvent, stop_event: MpEvent,
config: FrigateConfig,
): ):
super().__init__() super().__init__()
self.camera = camera self.camera = camera
self.converter = converter self.converter = converter
self.websocket_server = websocket_server self.websocket_server = websocket_server
self.stop_event = stop_event self.stop_event = stop_event
self.config = config
def run(self) -> None: def run(self) -> None:
while not self.stop_event.is_set(): while not self.stop_event.is_set():
@ -122,6 +125,7 @@ class BroadcastThread(threading.Thread):
if ( if (
not ws.terminated not ws.terminated
and ws.environ["PATH_INFO"] == f"/{self.camera}" and ws.environ["PATH_INFO"] == f"/{self.camera}"
and ws_has_camera_access(ws, self.camera, self.config)
): ):
try: try:
ws.send(buf, binary=True) ws.send(buf, binary=True)
@ -135,7 +139,11 @@ class BroadcastThread(threading.Thread):
class JsmpegCamera: class JsmpegCamera:
def __init__( def __init__(
self, config: CameraConfig, stop_event: MpEvent, websocket_server: Any self,
config: CameraConfig,
frigate_config: FrigateConfig,
stop_event: MpEvent,
websocket_server: Any,
) -> None: ) -> None:
self.config = config self.config = config
self.input: queue.Queue[bytes] = queue.Queue(maxsize=config.detect.fps) self.input: queue.Queue[bytes] = queue.Queue(maxsize=config.detect.fps)
@ -154,7 +162,11 @@ class JsmpegCamera:
config.live.quality, config.live.quality,
) )
self.broadcaster = BroadcastThread( self.broadcaster = BroadcastThread(
config.name or "", self.converter, websocket_server, stop_event config.name or "",
self.converter,
websocket_server,
stop_event,
frigate_config,
) )
self.converter.start() self.converter.start()

View File

@ -32,6 +32,7 @@ from frigate.const import (
from frigate.output.birdseye import Birdseye from frigate.output.birdseye import Birdseye
from frigate.output.camera import JsmpegCamera from frigate.output.camera import JsmpegCamera
from frigate.output.preview import PreviewRecorder from frigate.output.preview import PreviewRecorder
from frigate.output.ws_auth import ws_has_camera_access
from frigate.util.image import SharedMemoryFrameManager, get_blank_yuv_frame from frigate.util.image import SharedMemoryFrameManager, get_blank_yuv_frame
from frigate.util.process import FrigateProcess from frigate.util.process import FrigateProcess
@ -102,7 +103,7 @@ class OutputProcess(FrigateProcess):
) -> None: ) -> None:
camera_config = self.config.cameras[camera] camera_config = self.config.cameras[camera]
jsmpeg_cameras[camera] = JsmpegCamera( jsmpeg_cameras[camera] = JsmpegCamera(
camera_config, self.stop_event, websocket_server camera_config, self.config, self.stop_event, websocket_server
) )
preview_recorders[camera] = PreviewRecorder(camera_config) preview_recorders[camera] = PreviewRecorder(camera_config)
preview_write_times[camera] = 0 preview_write_times[camera] = 0
@ -262,6 +263,7 @@ class OutputProcess(FrigateProcess):
# send camera frame to ffmpeg process if websockets are connected # send camera frame to ffmpeg process if websockets are connected
if any( if any(
ws.environ["PATH_INFO"].endswith(camera) ws.environ["PATH_INFO"].endswith(camera)
and ws_has_camera_access(ws, camera, self.config)
for ws in websocket_server.manager for ws in websocket_server.manager
): ):
# write to the converter for the camera if clients are listening to the specific camera # write to the converter for the camera if clients are listening to the specific camera
@ -275,6 +277,7 @@ class OutputProcess(FrigateProcess):
self.config.birdseye.restream self.config.birdseye.restream
or any( or any(
ws.environ["PATH_INFO"].endswith("birdseye") ws.environ["PATH_INFO"].endswith("birdseye")
and ws_has_camera_access(ws, "birdseye", self.config)
for ws in websocket_server.manager for ws in websocket_server.manager
) )
) )

43
frigate/output/ws_auth.py Normal file
View File

@ -0,0 +1,43 @@
"""Authorization helpers for JSMPEG websocket clients."""
from typing import Any
from frigate.config import FrigateConfig
from frigate.models import User
def _get_valid_ws_roles(ws: Any, config: FrigateConfig) -> list[str]:
role_header = ws.environ.get("HTTP_REMOTE_ROLE", "")
roles = [
role.strip()
for role in role_header.split(config.proxy.separator)
if role.strip()
]
return [role for role in roles if role in config.auth.roles]
def ws_has_camera_access(ws: Any, camera_name: str, config: FrigateConfig) -> bool:
"""Return True when a websocket client is authorized for the camera path."""
roles = _get_valid_ws_roles(ws, config)
if not roles:
return False
roles_dict = config.auth.roles
# Birdseye is a composite stream, so only users with unrestricted access
# should receive it.
if camera_name == "birdseye":
return any(role == "admin" or not roles_dict.get(role) for role in roles)
all_camera_names = set(config.cameras.keys())
for role in roles:
if role == "admin" or not roles_dict.get(role):
return True
allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names)
if camera_name in allowed_cameras:
return True
return False

View File

@ -23,6 +23,26 @@ class TestHttpApp(BaseTestHttp):
response_json = response.json() response_json = response.json()
assert response_json == self.test_stats assert response_json == self.test_stats
def test_recordings_storage_requires_admin(self):
stats = Mock(spec=StatsEmitter)
stats.get_latest_stats.return_value = self.test_stats
app = super().create_app(stats)
app.storage_maintainer = Mock()
app.storage_maintainer.calculate_camera_usages.return_value = {
"front_door": {"usage": 2.0},
}
with AuthTestClient(app) as client:
response = client.get(
"/recordings/storage",
headers={"remote-user": "viewer", "remote-role": "viewer"},
)
assert response.status_code == 403
response = client.get("/recordings/storage")
assert response.status_code == 200
assert response.json()["front_door"]["usage_percent"] == 25.0
def test_config_set_in_memory_replaces_objects_track_list(self): def test_config_set_in_memory_replaces_objects_track_list(self):
self.minimal_config["cameras"]["front_door"]["objects"] = { self.minimal_config["cameras"]["front_door"]["objects"] = {
"track": ["person", "car"], "track": ["person", "car"],

View File

@ -219,6 +219,25 @@ class TestHttpApp(BaseTestHttp):
assert len(events) == 1 assert len(events) == 1
assert events[0]["id"] == event_id assert events[0]["id"] == event_id
def test_similarity_search_hides_unauthorized_anchor_event(self):
mock_embeddings = Mock()
self.app.frigate_config.semantic_search.enabled = True
self.app.embeddings = mock_embeddings
with AuthTestClient(self.app) as client:
super().insert_mock_event("hidden.anchor", camera="back_door")
response = client.get(
"/events/search",
params={
"search_type": "similarity",
"event_id": "hidden.anchor",
},
)
assert response.status_code == 404
assert response.json()["message"] == "Event not found"
mock_embeddings.search_thumbnail.assert_not_called()
def test_get_good_event(self): def test_get_good_event(self):
id = "123456.random" id = "123456.random"

View File

@ -145,9 +145,12 @@ class TestExecuteFindSimilarObjects(unittest.TestCase):
embeddings=embeddings, embeddings=embeddings,
frigate_config=SimpleNamespace( frigate_config=SimpleNamespace(
semantic_search=SimpleNamespace(enabled=semantic_enabled), semantic_search=SimpleNamespace(enabled=semantic_enabled),
cameras={"driveway": object()},
auth=SimpleNamespace(roles={"admin": [], "viewer": ["driveway"]}),
proxy=SimpleNamespace(separator=","),
), ),
) )
return SimpleNamespace(app=app) return SimpleNamespace(app=app, headers={})
def test_semantic_search_disabled_returns_error(self): def test_semantic_search_disabled_returns_error(self):
req = self._make_request(semantic_enabled=False) req = self._make_request(semantic_enabled=False)
@ -180,7 +183,7 @@ class TestExecuteFindSimilarObjects(unittest.TestCase):
_execute_find_similar_objects( _execute_find_similar_objects(
req, req,
{"event_id": "anchor", "cameras": ["nonexistent_cam"]}, {"event_id": "anchor", "cameras": ["nonexistent_cam"]},
allowed_cameras=["nonexistent_cam"], allowed_cameras=["driveway"],
) )
) )
self.assertEqual(result["results"], []) self.assertEqual(result["results"], [])

View File

@ -0,0 +1,57 @@
"""Tests for JSMPEG websocket authorization."""
import unittest
from types import SimpleNamespace
from frigate.config import FrigateConfig
from frigate.output.ws_auth import ws_has_camera_access
class TestWsHasCameraAccess(unittest.TestCase):
def setUp(self):
self.config = FrigateConfig(
mqtt={"host": "mqtt"},
auth={"roles": {"limited_user": ["front_door"]}},
cameras={
"front_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
"back_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}
]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
},
)
def _make_ws(self, role: str):
return SimpleNamespace(environ={"HTTP_REMOTE_ROLE": role})
def test_restricted_role_only_gets_allowed_camera(self):
ws = self._make_ws("limited_user")
self.assertTrue(ws_has_camera_access(ws, "front_door", self.config))
self.assertFalse(ws_has_camera_access(ws, "back_door", self.config))
def test_unrestricted_role_can_access_any_camera(self):
ws = self._make_ws("viewer")
self.assertTrue(ws_has_camera_access(ws, "front_door", self.config))
self.assertTrue(ws_has_camera_access(ws, "back_door", self.config))
def test_birdseye_requires_unrestricted_access(self):
self.assertTrue(
ws_has_camera_access(self._make_ws("admin"), "birdseye", self.config)
)
self.assertTrue(
ws_has_camera_access(self._make_ws("viewer"), "birdseye", self.config)
)
self.assertFalse(
ws_has_camera_access(self._make_ws("limited_user"), "birdseye", self.config)
)

View File

@ -0,0 +1,29 @@
"""Tests for camera monitoring notification authorization."""
import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock
from frigate.comms.webpush import WebPushClient
class TestCameraMonitoringNotifications(unittest.TestCase):
def test_send_camera_monitoring_filters_by_camera_access(self):
client = WebPushClient.__new__(WebPushClient)
client.config = SimpleNamespace(
cameras={"front_door": SimpleNamespace(friendly_name=None)}
)
client.web_pushers = {"allowed": [], "denied": []}
client.user_cameras = {"allowed": {"front_door"}, "denied": set()}
client.check_registrations = MagicMock()
client.cleanup_registrations = MagicMock()
client.send_push_notification = MagicMock()
client.send_camera_monitoring(
{"camera": "front_door", "message": "Monitoring condition met"}
)
self.assertEqual(client.send_push_notification.call_count, 1)
self.assertEqual(
client.send_push_notification.call_args.kwargs["user"], "allowed"
)

View File

@ -24,8 +24,12 @@ from frigate.log import redirect_output_to_logger, suppress_stderr_during
from frigate.models import Event, Recordings, ReviewSegment from frigate.models import Event, Recordings, ReviewSegment
from frigate.types import ModelStatusTypesEnum from frigate.types import ModelStatusTypesEnum
from frigate.util.downloader import ModelDownloader from frigate.util.downloader import ModelDownloader
from frigate.util.file import get_event_thumbnail_bytes from frigate.util.file import get_event_thumbnail_bytes, load_event_snapshot_image
from frigate.util.image import get_image_from_recording from frigate.util.image import (
calculate_region,
get_image_from_recording,
relative_box_to_absolute,
)
from frigate.util.process import FrigateProcess from frigate.util.process import FrigateProcess
BATCH_SIZE = 16 BATCH_SIZE = 16
@ -713,7 +717,7 @@ def collect_object_classification_examples(
This function: This function:
1. Queries events for the specified label 1. Queries events for the specified label
2. Selects 100 balanced events across different cameras and times 2. Selects 100 balanced events across different cameras and times
3. Retrieves thumbnails for selected events (with 33% center crop applied) 3. Crops each event's clean snapshot around the object bounding box
4. Selects 24 most visually distinct thumbnails 4. Selects 24 most visually distinct thumbnails
5. Saves to dataset directory 5. Saves to dataset directory
@ -832,29 +836,80 @@ def _select_balanced_events(
def _extract_event_thumbnails(events: list[Event], output_dir: str) -> list[str]: def _extract_event_thumbnails(events: list[Event], output_dir: str) -> list[str]:
""" """
Extract thumbnails from events and save to disk. Extract a training image for each event.
Preferred path: load the full-frame clean snapshot and crop around the
stored bounding box with the same calculate_region(..., max(w, h), 1.0)
call the live ObjectClassificationProcessor uses, so wizard examples
are framed like inference-time inputs.
Fallback: if no clean snapshot exists (snapshots disabled, or only a
legacy annotated JPG is on disk), center-crop the stored thumbnail
using a step ladder sized from the box/region area ratio.
Args: Args:
events: List of Event objects events: List of Event objects
output_dir: Directory to save thumbnails output_dir: Directory to save crops
Returns: Returns:
List of paths to successfully extracted thumbnail images List of paths to successfully extracted images
""" """
thumbnail_paths = [] image_paths = []
for idx, event in enumerate(events): for idx, event in enumerate(events):
try: try:
thumbnail_bytes = get_event_thumbnail_bytes(event) img = _load_event_classification_crop(event)
if img is None:
continue
resized = cv2.resize(img, (224, 224))
output_path = os.path.join(output_dir, f"thumbnail_{idx:04d}.jpg")
cv2.imwrite(output_path, resized)
image_paths.append(output_path)
except Exception as e:
logger.debug(f"Failed to extract image for event {event.id}: {e}")
continue
return image_paths
def _load_event_classification_crop(event: Event) -> np.ndarray | None:
"""Prefer a snapshot-based object crop; fall back to a center-cropped thumbnail."""
if event.data and "box" in event.data:
snapshot, _ = load_event_snapshot_image(event, clean_only=True)
if snapshot is not None:
abs_box = relative_box_to_absolute(snapshot.shape, event.data["box"])
if abs_box is not None:
xmin, ymin, xmax, ymax = abs_box
box_w = xmax - xmin
box_h = ymax - ymin
if box_w > 0 and box_h > 0:
x1, y1, x2, y2 = calculate_region(
snapshot.shape,
xmin,
ymin,
xmax,
ymax,
max(box_w, box_h),
1.0,
)
cropped = snapshot[y1:y2, x1:x2]
if cropped.size > 0:
return cropped
thumbnail_bytes = get_event_thumbnail_bytes(event)
if not thumbnail_bytes:
return None
if thumbnail_bytes:
nparr = np.frombuffer(thumbnail_bytes, np.uint8) nparr = np.frombuffer(thumbnail_bytes, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if img is None or img.size == 0:
return None
if img is not None:
height, width = img.shape[:2] height, width = img.shape[:2]
crop_size = 1.0 crop_size = 1.0
if event.data and "box" in event.data and "region" in event.data: if event.data and "box" in event.data and "region" in event.data:
box = event.data["box"] box = event.data["box"]
region = event.data["region"] region = event.data["region"]
@ -862,7 +917,6 @@ def _extract_event_thumbnails(events: list[Event], output_dir: str) -> list[str]
if len(box) == 4 and len(region) == 4: if len(box) == 4 and len(region) == 4:
box_w, box_h = box[2], box[3] box_w, box_h = box[2], box[3]
region_w, region_h = region[2], region[3] region_w, region_h = region[2], region[3]
box_area = (box_w * box_h) / (region_w * region_h) box_area = (box_w * box_h) / (region_w * region_h)
if box_area < 0.05: if box_area < 0.05:
@ -878,20 +932,10 @@ def _extract_event_thumbnails(events: list[Event], output_dir: str) -> list[str]
crop_width = int(width * crop_size) crop_width = int(width * crop_size)
crop_height = int(height * crop_size) crop_height = int(height * crop_size)
x1 = (width - crop_width) // 2 x1 = (width - crop_width) // 2
y1 = (height - crop_height) // 2 y1 = (height - crop_height) // 2
x2 = x1 + crop_width cropped = img[y1 : y1 + crop_height, x1 : x1 + crop_width]
y2 = y1 + crop_height if cropped.size == 0:
return None
cropped = img[y1:y2, x1:x2] return cropped
resized = cv2.resize(cropped, (224, 224))
output_path = os.path.join(output_dir, f"thumbnail_{idx:04d}.jpg")
cv2.imwrite(output_path, resized)
thumbnail_paths.append(output_path)
except Exception as e:
logger.debug(f"Failed to extract thumbnail for event {event.id}: {e}")
continue
return thumbnail_paths

View File

@ -726,7 +726,20 @@ def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedPro
if detailed and format_entries: if detailed and format_entries:
cmd.extend(["-show_entries", f"format={format_entries}"]) cmd.extend(["-show_entries", f"format={format_entries}"])
cmd.extend(["-loglevel", "error", clean_path]) cmd.extend(["-loglevel", "error", clean_path])
return sp.run(cmd, capture_output=True) try:
return sp.run(cmd, capture_output=True, timeout=6)
except sp.TimeoutExpired as e:
logger.info(
"ffprobe timed out while probing %s (transport=%s)",
clean_camera_user_pass(path),
rtsp_transport or "default",
)
return sp.CompletedProcess(
args=cmd,
returncode=1,
stdout=e.stdout or b"",
stderr=(e.stderr or b"") + b"\nffprobe timed out",
)
result = run() result = run()
@ -832,11 +845,23 @@ async def get_video_properties(
"-show_streams", "-show_streams",
url, url,
] ]
proc = None
try: try:
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
) )
stdout, _ = await proc.communicate() try:
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=6)
except asyncio.TimeoutError:
logger.info(
"ffprobe timed out while probing %s (transport=%s)",
clean_camera_user_pass(url),
rtsp_transport or "default",
)
proc.kill()
await proc.wait()
return False, 0, 0, None, -1
if proc.returncode != 0: if proc.returncode != 0:
return False, 0, 0, None, -1 return False, 0, 0, None, -1