Fix non-thread-safe operations in amcrest (#116859)

* Fix non-thread-safe operations in amcrest

fixes #116850

* fix locking

* fix locking

* fix locking
This commit is contained in:
J. Nick Koston 2024-05-05 15:58:38 -05:00 committed by GitHub
parent a57f4b8f42
commit 092a2de340
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 75 additions and 25 deletions

View File

@ -35,7 +35,7 @@ from homeassistant.const import (
HTTP_BASIC_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -177,7 +177,8 @@ class AmcrestChecker(ApiWrapper):
"""Return event flag that indicates if camera's API is responding.""" """Return event flag that indicates if camera's API is responding."""
return self._async_wrap_event_flag return self._async_wrap_event_flag
def _start_recovery(self) -> None: @callback
def _async_start_recovery(self) -> None:
self.available_flag.clear() self.available_flag.clear()
self.async_available_flag.clear() self.async_available_flag.clear()
async_dispatcher_send( async_dispatcher_send(
@ -222,41 +223,89 @@ class AmcrestChecker(ApiWrapper):
yield yield
except LoginError as ex: except LoginError as ex:
async with self._async_wrap_lock: async with self._async_wrap_lock:
self._handle_offline(ex) self._async_handle_offline(ex)
raise raise
except AmcrestError: except AmcrestError:
async with self._async_wrap_lock: async with self._async_wrap_lock:
self._handle_error() self._async_handle_error()
raise raise
async with self._async_wrap_lock: async with self._async_wrap_lock:
self._set_online() self._async_set_online()
def _handle_offline(self, ex: Exception) -> None: def _handle_offline_thread_safe(self, ex: Exception) -> bool:
"""Handle camera offline status shared between threads and event loop.
Returns if the camera was online as a bool.
"""
with self._wrap_lock: with self._wrap_lock:
was_online = self.available was_online = self.available
was_login_err = self._wrap_login_err was_login_err = self._wrap_login_err
self._wrap_login_err = True self._wrap_login_err = True
if not was_login_err: if not was_login_err:
_LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex)
if was_online: return was_online
self._start_recovery()
def _handle_error(self) -> None: def _handle_offline(self, ex: Exception) -> None:
"""Handle camera offline status from a thread."""
if self._handle_offline_thread_safe(ex):
self._hass.loop.call_soon_threadsafe(self._async_start_recovery)
@callback
def _async_handle_offline(self, ex: Exception) -> None:
if self._handle_offline_thread_safe(ex):
self._async_start_recovery()
def _handle_error_thread_safe(self) -> bool:
"""Handle camera error status shared between threads and event loop.
Returns if the camera was online and is now offline as
a bool.
"""
with self._wrap_lock: with self._wrap_lock:
was_online = self.available was_online = self.available
errs = self._wrap_errors = self._wrap_errors + 1 errs = self._wrap_errors = self._wrap_errors + 1
offline = not self.available offline = not self.available
_LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs)
if was_online and offline: return was_online and offline
_LOGGER.error("%s camera offline: Too many errors", self._wrap_name)
self._start_recovery()
def _set_online(self) -> None: def _handle_error(self) -> None:
"""Handle camera error status from a thread."""
if self._handle_error_thread_safe():
_LOGGER.error("%s camera offline: Too many errors", self._wrap_name)
self._hass.loop.call_soon_threadsafe(self._async_start_recovery)
@callback
def _async_handle_error(self) -> None:
"""Handle camera error status from the event loop."""
if self._handle_error_thread_safe():
_LOGGER.error("%s camera offline: Too many errors", self._wrap_name)
self._async_start_recovery()
def _set_online_thread_safe(self) -> bool:
"""Set camera online status shared between threads and event loop.
Returns if the camera was offline as a bool.
"""
with self._wrap_lock: with self._wrap_lock:
was_offline = not self.available was_offline = not self.available
self._wrap_errors = 0 self._wrap_errors = 0
self._wrap_login_err = False self._wrap_login_err = False
if was_offline: return was_offline
def _set_online(self) -> None:
"""Set camera online status from a thread."""
if self._set_online_thread_safe():
self._hass.loop.call_soon_threadsafe(self._async_signal_online)
@callback
def _async_set_online(self) -> None:
"""Set camera online status from the event loop."""
if self._set_online_thread_safe():
self._async_signal_online()
@callback
def _async_signal_online(self) -> None:
"""Signal that camera is back online."""
assert self._unsub_recheck is not None assert self._unsub_recheck is not None
self._unsub_recheck() self._unsub_recheck()
self._unsub_recheck = None self._unsub_recheck = None

View File

@ -16,7 +16,7 @@ import voluptuous as vol
from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager
from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream, async_aiohttp_proxy_stream,
@ -325,7 +325,8 @@ class AmcrestCam(Camera):
# Other Entity method overrides # Other Entity method overrides
async def async_on_demand_update(self) -> None: @callback
def async_on_demand_update(self) -> None:
"""Update state.""" """Update state."""
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)