Files
bigbrother/core/scheduler.py
T
n0mad1k ccc6b729de Initial public release
Full BigBrother network implant - passive SOC + active exploitation.
Personal identifiers removed; all capabilities intact.
See README.md for setup and docs/deployment.md for detailed deployment.
2026-06-26 09:52:50 -04:00

193 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Cron-like task scheduler with jitter.
Modules register periodic tasks with an interval and optional jitter range.
Tasks execute in a thread pool. Used for PCAP rotation, health checks,
baseline captures, data pruning, resource checks.
"""
import time
import random
import logging
import threading
from dataclasses import dataclass, field
from typing import Callable, Optional
from concurrent.futures import ThreadPoolExecutor
logger = logging.getLogger(__name__)
@dataclass
class ScheduledTask:
"""A registered periodic task."""
name: str
callback: Callable
interval: float # seconds between executions
jitter: float = 0.0 # max random jitter added to interval (seconds)
run_immediately: bool = False
enabled: bool = True
# Runtime state
last_run: float = 0.0
next_run: float = 0.0
run_count: int = 0
error_count: int = 0
_last_error: Optional[str] = None
class Scheduler:
"""Thread-safe periodic task scheduler with jitter support.
Tasks are checked every second and dispatched to a thread pool.
Jitter is re-randomized on each scheduling cycle to avoid patterns.
"""
def __init__(self, max_workers: int = 4):
self._tasks: dict[str, ScheduledTask] = {}
self._lock = threading.Lock()
self._running = False
self._thread: Optional[threading.Thread] = None
self._pool = ThreadPoolExecutor(max_workers=max_workers,
thread_name_prefix="bb-sched")
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def start(self) -> None:
"""Start the scheduler loop."""
if self._running:
return
self._running = True
# Initialize next_run for tasks that want immediate execution
now = time.time()
with self._lock:
for task in self._tasks.values():
if task.run_immediately:
task.next_run = now
elif task.next_run == 0.0:
task.next_run = now + task.interval + random.uniform(0, task.jitter)
self._thread = threading.Thread(
target=self._scheduler_loop, daemon=True, name="sensor-scheduler"
)
self._thread.start()
logger.info("Scheduler started (%d tasks)", len(self._tasks))
def stop(self) -> None:
"""Stop the scheduler and wait for running tasks to finish."""
self._running = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=5.0)
self._pool.shutdown(wait=False)
logger.info("Scheduler stopped")
# ------------------------------------------------------------------
# Task management
# ------------------------------------------------------------------
def register(self, name: str, callback: Callable, interval: float,
jitter: float = 0.0, run_immediately: bool = False) -> None:
"""Register a periodic task.
Args:
name: Unique task name.
callback: Callable (no arguments) to execute.
interval: Base interval in seconds.
jitter: Maximum random seconds added to interval.
run_immediately: If True, run as soon as the scheduler starts.
"""
now = time.time()
task = ScheduledTask(
name=name,
callback=callback,
interval=interval,
jitter=jitter,
run_immediately=run_immediately,
next_run=now if run_immediately else now + interval + random.uniform(0, jitter),
)
with self._lock:
self._tasks[name] = task
logger.debug("Registered task: %s (interval=%.1fs, jitter=%.1fs)",
name, interval, jitter)
def unregister(self, name: str) -> None:
"""Remove a task."""
with self._lock:
self._tasks.pop(name, None)
def enable(self, name: str) -> None:
"""Enable a disabled task."""
with self._lock:
task = self._tasks.get(name)
if task:
task.enabled = True
def disable(self, name: str) -> None:
"""Disable a task (stays registered but won't execute)."""
with self._lock:
task = self._tasks.get(name)
if task:
task.enabled = False
def run_now(self, name: str) -> None:
"""Trigger immediate execution of a task."""
with self._lock:
task = self._tasks.get(name)
if task:
task.next_run = time.time()
# ------------------------------------------------------------------
# Scheduler loop
# ------------------------------------------------------------------
def _scheduler_loop(self) -> None:
"""Check tasks every second, dispatch due tasks to thread pool."""
while self._running:
now = time.time()
with self._lock:
tasks = list(self._tasks.values())
for task in tasks:
if not task.enabled:
continue
if now >= task.next_run:
self._pool.submit(self._execute_task, task)
# Schedule next run with fresh jitter
jitter = random.uniform(0, task.jitter) if task.jitter > 0 else 0
task.next_run = now + task.interval + jitter
time.sleep(1.0)
def _execute_task(self, task: ScheduledTask) -> None:
"""Execute a task and update bookkeeping."""
try:
task.callback()
task.run_count += 1
task.last_run = time.time()
except Exception as e:
task.error_count += 1
task._last_error = str(e)
logger.exception("Scheduler task %s failed", task.name)
# ------------------------------------------------------------------
# Introspection
# ------------------------------------------------------------------
def get_tasks(self) -> dict:
"""Return info about all registered tasks."""
with self._lock:
return {
name: {
"interval": t.interval,
"jitter": t.jitter,
"enabled": t.enabled,
"run_count": t.run_count,
"error_count": t.error_count,
"last_run": t.last_run,
"next_run": t.next_run,
"last_error": t._last_error,
}
for name, t in self._tasks.items()
}