""" scheduler.py — Background scan scheduler using APScheduler. """ import logging import threading from apscheduler.schedulers.background import BackgroundScheduler from config import Settings from scanner import ( create_scan_record, has_any_scan_completed, run_full_scan, run_incremental_scan, ) logger = logging.getLogger(__name__) _scheduler: BackgroundScheduler | None = None _scan_lock = threading.Lock() _active_scan_id: int | None = None def get_active_scan_id() -> int | None: return _active_scan_id def is_scan_running() -> bool: return _active_scan_id is not None def _run_scan_protected(scan_type: str, settings: Settings) -> int | None: """Try to acquire lock and run a scan. Returns scan_id or None if already running.""" global _active_scan_id if not _scan_lock.acquire(blocking=False): logger.warning("Scan already in progress — skipping %s scan request", scan_type) return None scan_id = create_scan_record(scan_type) _active_scan_id = scan_id logger.info("Scan %s started (id=%d)", scan_type, scan_id) def _run(): global _active_scan_id try: if scan_type == "full": run_full_scan(settings, scan_id) else: run_incremental_scan(settings, scan_id) finally: _active_scan_id = None _scan_lock.release() logger.info("Scan %s finished (id=%d)", scan_type, scan_id) thread = threading.Thread(target=_run, daemon=True, name=f"scan-{scan_type}-{scan_id}") thread.start() return scan_id def trigger_full_scan(settings: Settings) -> int | None: return _run_scan_protected("full", settings) def trigger_incremental_scan(settings: Settings) -> int | None: return _run_scan_protected("incremental", settings) def start_scheduler(settings: Settings) -> None: global _scheduler _scheduler = BackgroundScheduler(timezone="UTC") _scheduler.add_job( func=lambda: trigger_incremental_scan(settings), trigger="interval", hours=settings.scan_interval_hours, id="incremental_scan", replace_existing=True, ) _scheduler.start() logger.info( "Scheduler started — incremental scan every %d hour(s)", settings.scan_interval_hours, ) # Trigger initial full scan if the database has never been scanned if not has_any_scan_completed(): logger.info("No prior completed scan found — launching initial full scan") trigger_full_scan(settings) def stop_scheduler() -> None: if _scheduler and _scheduler.running: _scheduler.shutdown(wait=False) logger.info("Scheduler stopped")