* feat: add optional nanobot features * test: update azure install hint expectation * fix: validate optional feature extras maintainer edit: verify requested dependency extras before treating optional features as installed, propagate restart state from feature enablement, and align docs with the new plugins enable command. * fix: bound optional feature installs maintainer edit: make optional feature installs time out as a normal install failure instead of leaving the WebUI or CLI action waiting indefinitely. * feat: slim optional channel dependencies * fix: log optional install commands * fix(webui): gate remote feature installs * docs: clarify webhook plugin example * fix(webui): harden optional feature installs * fix: install optional deps without package fallback * fix(cli): refine plugin feature controls * fix(webui): count enabled nanobot features * fix(webui): allow slow feature install routes * fix(webui): allow disabling websocket channel * fix(plugins): simplify optional feature controls * fix(webui): polish apps catalog states * fix(webui): confirm nanobot support installs * fix(webui): polish nanobot install dialog * fix(webui): suppress empty websocket handshakes * fix(webui): clarify apps plugin summary * fix(webui): localize workspace access copy * fix(plugins): polish optional feature controls (#4691) --------- Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
47 lines
1.4 KiB
Python
47 lines
1.4 KiB
Python
"""Logging helpers for the WebUI WebSocket server surface."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from websockets.exceptions import ConnectionClosed
|
|
|
|
OPENING_HANDSHAKE_FAILED_MESSAGE = "opening handshake failed"
|
|
|
|
|
|
def _exception_chain_has_disconnect(exc: BaseException | None) -> bool:
|
|
seen: set[int] = set()
|
|
while exc is not None:
|
|
ident = id(exc)
|
|
if ident in seen:
|
|
return False
|
|
seen.add(ident)
|
|
if isinstance(exc, (
|
|
BrokenPipeError,
|
|
ConnectionAbortedError,
|
|
ConnectionResetError,
|
|
ConnectionClosed,
|
|
EOFError,
|
|
)):
|
|
return True
|
|
exc = exc.__cause__ or exc.__context__
|
|
return False
|
|
|
|
|
|
class WebSocketHandshakeNoiseFilter(logging.Filter):
|
|
"""Suppress restart-time handshakes where the browser already disconnected."""
|
|
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
if record.getMessage() != OPENING_HANDSHAKE_FAILED_MESSAGE:
|
|
return True
|
|
exc_info = record.exc_info
|
|
exc = exc_info[1] if isinstance(exc_info, tuple) and len(exc_info) >= 2 else None
|
|
return not _exception_chain_has_disconnect(exc)
|
|
|
|
|
|
def websockets_server_logger() -> logging.Logger:
|
|
ws_logger = logging.getLogger("websockets.server")
|
|
if not any(isinstance(f, WebSocketHandshakeNoiseFilter) for f in ws_logger.filters):
|
|
ws_logger.addFilter(WebSocketHandshakeNoiseFilter())
|
|
return ws_logger
|