120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
"""Simple publish/subscribe event bus for AYN Antivirus.
|
|
|
|
Decouples the scan engine from consumers like the CLI, logger, quarantine
|
|
manager, and real-time monitor so each component can react to events
|
|
independently.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import threading
|
|
from enum import Enum, auto
|
|
from typing import Any, Callable, Dict, List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Event types
|
|
# ---------------------------------------------------------------------------
|
|
class EventType(Enum):
|
|
"""All events emitted by the AYN engine."""
|
|
|
|
THREAT_FOUND = auto()
|
|
SCAN_STARTED = auto()
|
|
SCAN_COMPLETED = auto()
|
|
FILE_SCANNED = auto()
|
|
SIGNATURE_UPDATED = auto()
|
|
QUARANTINE_ACTION = auto()
|
|
REMEDIATION_ACTION = auto()
|
|
DASHBOARD_METRIC = auto()
|
|
|
|
|
|
# Type alias for subscriber callbacks.
|
|
Callback = Callable[[EventType, Any], None]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EventBus
|
|
# ---------------------------------------------------------------------------
|
|
class EventBus:
|
|
"""Thread-safe publish/subscribe event bus.
|
|
|
|
Usage::
|
|
|
|
bus = EventBus()
|
|
bus.subscribe(EventType.THREAT_FOUND, lambda et, data: print(data))
|
|
bus.publish(EventType.THREAT_FOUND, {"path": "/tmp/evil.elf"})
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._subscribers: Dict[EventType, List[Callback]] = {et: [] for et in EventType}
|
|
self._lock = threading.Lock()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Public API
|
|
# ------------------------------------------------------------------
|
|
def subscribe(self, event_type: EventType, callback: Callback) -> None:
|
|
"""Register *callback* to be invoked whenever *event_type* is published.
|
|
|
|
Parameters
|
|
----------
|
|
event_type:
|
|
The event to listen for.
|
|
callback:
|
|
A callable with signature ``(event_type, data) -> None``.
|
|
"""
|
|
with self._lock:
|
|
if callback not in self._subscribers[event_type]:
|
|
self._subscribers[event_type].append(callback)
|
|
|
|
def unsubscribe(self, event_type: EventType, callback: Callback) -> None:
|
|
"""Remove a previously-registered callback."""
|
|
with self._lock:
|
|
try:
|
|
self._subscribers[event_type].remove(callback)
|
|
except ValueError:
|
|
pass
|
|
|
|
def publish(self, event_type: EventType, data: Any = None) -> None:
|
|
"""Emit an event, invoking all registered callbacks synchronously.
|
|
|
|
Exceptions raised by individual callbacks are logged and swallowed so
|
|
that one faulty subscriber cannot break the pipeline.
|
|
|
|
Parameters
|
|
----------
|
|
event_type:
|
|
The event being emitted.
|
|
data:
|
|
Arbitrary payload — typically a dataclass or dict.
|
|
"""
|
|
with self._lock:
|
|
callbacks = list(self._subscribers[event_type])
|
|
|
|
for cb in callbacks:
|
|
try:
|
|
cb(event_type, data)
|
|
except Exception:
|
|
logger.exception(
|
|
"Subscriber %r raised an exception for event %s",
|
|
cb,
|
|
event_type.name,
|
|
)
|
|
|
|
def clear(self, event_type: EventType | None = None) -> None:
|
|
"""Remove all subscribers for *event_type*, or all subscribers if ``None``."""
|
|
with self._lock:
|
|
if event_type is None:
|
|
for et in EventType:
|
|
self._subscribers[et].clear()
|
|
else:
|
|
self._subscribers[event_type].clear()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module-level singleton
|
|
# ---------------------------------------------------------------------------
|
|
event_bus = EventBus()
|