Compare commits

...
Author SHA1 Message Date
dependabot[bot]andGitHub 96191bab5c Bump source-map-js from 1.2.1 to 1.2.2 in /web
Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-06 21:21:51 +00:00
Josh HawkinsandGitHub 28d8f5bbfa wake the mqtt worker when a publish is queued (#24580)
publish() only queued the message, and the worker was blocked waiting on the broker socket for up to a second, so on a quiet connection messages went out up to 1s late. A socketpair registered in the worker's selector now interrupts the wait when a publish is queued or stop() is called.
2026-10-06 15:20:07 -06:00
Josh HawkinsandGitHub 8b89b79ef5 Deps updates (#24581)
* update fastapi and unpin starlette

fastapi 0.116 capped starlette below 0.49, which kept us on a version with several open advisories, the earliest of which is fixed in 0.49.1. fastapi moves to 0.142 and the starlette pin is removed so the latest (1.7.0 today) is pulled in.

fastapi 0.137 and later stopped flattening included routers into app.routes, so the spec generator found no routes to classify and every endpoint failed validation. The generator now reads routes from the routers directly. The spec is regenerated on the new versions.

* pin starlette
2026-10-06 15:19:31 -06:00
7 changed files with 108 additions and 44 deletions
+2 -2
View File
@@ -2,9 +2,9 @@ aiofiles == 25.1.*
click == 8.5.*
# FastAPI
aiohttp == 3.12.*
starlette == 0.47.*
starlette == 1.7.*
starlette-context == 0.5.*
fastapi[standard-no-fastapi-cloud-cli] == 0.116.*
fastapi[standard-no-fastapi-cloud-cli] == 0.142.*
uvicorn == 0.52.*
slowapi == 0.1.*
joserfc == 1.6.*
+7 -2
View File
@@ -8017,7 +8017,7 @@ components:
properties:
file:
type: string
format: binary
contentMediaType: application/octet-stream
title: File
type: object
required:
@@ -8027,7 +8027,7 @@ components:
properties:
file:
type: string
format: binary
contentMediaType: application/octet-stream
title: File
type: object
required:
@@ -9601,6 +9601,11 @@ components:
type:
type: string
title: Error Type
input:
title: Input
ctx:
type: object
title: Context
type: object
required:
- loc
+29 -6
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import logging
import queue
import selectors
import socket
import threading
import time
from collections.abc import Callable
@@ -57,6 +58,11 @@ class MqttClient(Communicator):
self._next_connect_time = 0.0
self._last_on_connect_dispatch = 0.0
# lets other threads interrupt the worker's socket wait
self._wake_recv, self._wake_send = socket.socketpair()
self._wake_recv.setblocking(False)
self._wake_send.setblocking(False)
def subscribe(self, receiver: Callable) -> None:
"""Wrapper for allowing dispatcher to subscribe."""
self._dispatcher = receiver
@@ -86,6 +92,7 @@ class MqttClient(Communicator):
return
self._publish_queue.put(QueuedPublish(full_topic, payload, retain))
self._wake_worker()
def stop(self) -> None:
if self._worker is None:
@@ -101,9 +108,11 @@ class MqttClient(Communicator):
publish_done,
)
)
self._wake_worker()
publish_done.wait(MQTT_SHUTDOWN_FLUSH_TIMEOUT)
self._stop_event.set()
self._wake_worker()
if self.client is not None:
try:
@@ -369,11 +378,17 @@ class MqttClient(Communicator):
exc_info=True,
)
def _wake_worker(self) -> None:
try:
self._wake_send.send(b"\0")
except BlockingIOError:
# the buffer is full, so a wake is already pending
pass
def _loop_client(self, timeout: float) -> int:
"""Drive Paho without select()'s limit on socket file descriptors."""
assert self.client is not None
client = self.client
if client is None:
return mqtt.MQTT_ERR_NO_CONN
sock = client.socket()
if sock is None:
return mqtt.MQTT_ERR_NO_CONN
@@ -385,11 +400,19 @@ class MqttClient(Communicator):
pending = hasattr(sock, "pending") and sock.pending() > 0
with selectors.DefaultSelector() as selector:
selector.register(sock, events)
ready = selector.select(0.0 if pending else timeout)
selector.register(self._wake_recv, selectors.EVENT_READ)
ready = {
key.fileobj: mask
for key, mask in selector.select(0.0 if pending else timeout)
}
ready_events = 0
for _, mask in ready:
ready_events |= mask
if self._wake_recv in ready:
try:
self._wake_recv.recv(4096)
except BlockingIOError:
pass
ready_events = ready.get(sock, 0)
if pending or ready_events & selectors.EVENT_READ:
result = client.loop_read()
if result != mqtt.MQTT_ERR_SUCCESS or client.socket() is None:
+16 -3
View File
@@ -126,12 +126,18 @@ class TestMqttClientLifecycle(unittest.TestCase):
os.makedirs(MODEL_CACHE_DIR)
self.config = build_config()
self.client = MqttClient(self.config)
self.client = self._build_client()
self.receiver = RuntimeSnapshotReceiver()
self.client.attach_dispatcher(build_dispatcher(self.config, []))
def test_subscribe_stores_receiver_without_starting_worker(self) -> None:
def _build_client(self) -> MqttClient:
client = MqttClient(self.config)
self.addCleanup(client._wake_recv.close)
self.addCleanup(client._wake_send.close)
return client
def test_subscribe_stores_receiver_without_starting_worker(self) -> None:
client = self._build_client()
with patch.object(client, "_start_worker") as mock_start_worker:
client.subscribe(self.receiver._receive)
@@ -142,7 +148,7 @@ class TestMqttClientLifecycle(unittest.TestCase):
mock_start_worker.assert_not_called()
def test_attach_dispatcher_supplies_command_surface(self) -> None:
client = MqttClient(self.config)
client = self._build_client()
self.assertFalse(client._is_supported_command_topic("front/detect/set"))
@@ -295,6 +301,13 @@ class TestMqttClientLifecycle(unittest.TestCase):
self.assertEqual(self.client._subscription_mid, 42)
self.client.client.subscribe.assert_called_once_with("frigate/#", qos=0)
def test_publish_wakes_worker(self) -> None:
self.client.connected = True
self.client.publish("events", "payload")
self.assertEqual(self.client._wake_recv.recv(16), b"\0")
def test_handle_connect_event_reconnects_on_recoverable_subscribe_error(
self,
) -> None:
+19 -3
View File
@@ -2,6 +2,7 @@ import fcntl
import resource
import selectors
import socket
import time
import unittest
from unittest.mock import MagicMock, patch
@@ -23,6 +24,11 @@ class TestMqttNetworkLoop(unittest.TestCase):
self.sock, self.peer = socket.socketpair()
self.addCleanup(self.sock.close)
self.addCleanup(self.peer.close)
self.transport._wake_recv, self.transport._wake_send = socket.socketpair()
self.transport._wake_recv.setblocking(False)
self.transport._wake_send.setblocking(False)
self.addCleanup(self.transport._wake_recv.close)
self.addCleanup(self.transport._wake_send.close)
self.client.socket.return_value = self.sock
def test_high_fd_handles_connack_suback_publish_and_puback(self) -> None:
@@ -123,8 +129,18 @@ class TestMqttNetworkLoop(unittest.TestCase):
self.assertEqual(self.transport._loop_client(0), mqtt.MQTT_ERR_CONN_LOST)
self.client.loop_misc.assert_not_called()
def test_missing_client_or_socket_reports_no_connection(self) -> None:
def test_missing_socket_reports_no_connection(self) -> None:
self.client.socket.return_value = None
self.assertEqual(self.transport._loop_client(0), mqtt.MQTT_ERR_NO_CONN)
self.transport.client = None
self.assertEqual(self.transport._loop_client(0), mqtt.MQTT_ERR_NO_CONN)
def test_wake_interrupts_wait_and_is_consumed(self) -> None:
self.transport._wake_worker()
start = time.monotonic()
self.assertEqual(self.transport._loop_client(5), mqtt.MQTT_ERR_SUCCESS)
self.assertLess(time.monotonic() - start, 1)
self.client.loop_read.assert_not_called()
start = time.monotonic()
self.transport._loop_client(0.2)
self.assertGreater(time.monotonic() - start, 0.15)
+32 -25
View File
@@ -136,33 +136,36 @@ ACCESS_NOTES = {
}
# Mirrors the router set wired up in frigate.api.fastapi_app.
ROUTERS = [
auth.router,
camera.router,
chat.router,
classification.router,
review.router,
main_app.router,
preview.router,
notification.router,
export.router,
hardware.router,
notices.router,
event.router,
media.router,
motion_search.router,
record.router,
debug_replay.router,
]
def build_app() -> FastAPI:
"""Build a bare app with every router mounted.
This mirrors the router set wired up in frigate.api.fastapi_app. It omits
the global admin dependency and all runtime state; the OpenAPI route table
and the per-route dependencies are all we need to export and classify.
It omits the global admin dependency and all runtime state; the OpenAPI
route table and the per-route dependencies are all we need to export and
classify.
"""
app = FastAPI()
routers = [
auth.router,
camera.router,
chat.router,
classification.router,
review.router,
main_app.router,
preview.router,
notification.router,
export.router,
hardware.router,
notices.router,
event.router,
media.router,
motion_search.router,
record.router,
debug_replay.router,
]
for router in routers:
for router in ROUTERS:
app.include_router(router)
return app
@@ -318,13 +321,17 @@ def _classify_base(
def build_access_map(
app: FastAPI,
exempt_paths: set[str],
exempt_prefixes: tuple[str, ...],
) -> dict[tuple[str, str], dict]:
"""Map (path, lowercase method) -> classification details."""
access_map: dict[tuple[str, str], dict] = {}
for route in app.routes:
# app.routes holds opaque wrappers for included routers on newer FastAPI.
# The routers mount without a prefix, so their own routes carry final paths.
routes = [route for router in ROUTERS for route in router.routes]
for route in routes:
if not isinstance(route, APIRoute):
continue
level, roles, flag = classify_route(route, exempt_paths, exempt_prefixes)
@@ -515,7 +522,7 @@ def render(spec: dict) -> str:
def build_spec() -> tuple[dict, dict, list, list, list]:
app = build_app()
exempt_paths, exempt_prefixes = read_exempt_rules()
access_map = build_access_map(app, exempt_paths, exempt_prefixes)
access_map = build_access_map(exempt_paths, exempt_prefixes)
spec = base_document(app.openapi())
normalized = strip_volatile_defaults(spec)
+3 -3
View File
@@ -9910,9 +9910,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"