"""Application bootstrap: login/restore, dependency wiring, and window lifecycle.""" from __future__ import annotations import logging import os import sys import threading import time import traceback from pathlib import Path from PyQt6.QtCore import QEventLoop, QLockFile, QRectF, Qt, QTimer from PyQt6.QtGui import QColor, QFont, QPainter, QPixmap from PyQt6.QtWidgets import QApplication, QDialog, QMessageBox, QSplashScreen from . import APP_ID, __build_date__, __version__, paths from .api_client import ApiClient, ApiError, NetworkError from .auth import AuthService, CredentialStore, Session, SessionExpired from .clamav import ClamAVService from .config import AccountCache, SettingsStore from .context import AppContext from .logging_setup import configure as configure_logging, log_path from .quarantine import QuarantineService from .storage import Storage from .ui import brand, theme from .ui.login_dialog import LoginDialog from .ui.main_window import MainWindow logger = logging.getLogger(__name__) def _set_windows_app_id() -> None: if os.name != "nt": return try: import ctypes ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APP_ID) except Exception: pass def _splash_pixmap() -> QPixmap: image = QPixmap(460, 250) image.fill(Qt.GlobalColor.transparent) painter = QPainter(image) painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) painter.setBrush(QColor("#081820")) painter.setPen(QColor("#1B3946")) painter.drawRoundedRect(QRectF(1, 1, 458, 248), 22, 22) brand.paint_mark(painter, QRectF(34, 34, 72, 72)) painter.setPen(QColor(theme.TEXT)) painter.setFont(theme.display_font(26, QFont.Weight.DemiBold)) left_center = int(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) painter.drawText(QRectF(124, 36, 300, 40), left_center, "BoltClam") painter.setPen(QColor(theme.MUTED)) painter.setFont(theme.body_font(10.0)) painter.drawText(QRectF(126, 76, 300, 24), left_center, "ClamAV protection, without the noise.") painter.setPen(QColor(theme.TEXT_SOFT)) painter.setFont(theme.body_font(10.5)) painter.drawText(QRectF(36, 154, 390, 26), left_center, "Starting protection…") painter.setPen(QColor(theme.DIM)) painter.setFont(theme.mono_font(8.0)) painter.drawText(QRectF(36, 206, 390, 20), left_center, f"Version {__version__} · {__build_date__}") painter.end() return image def _install_exception_hook() -> None: def handle(exc_type, exc_value, exc_traceback): if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return logger.critical("Unhandled exception", exc_info=(exc_type, exc_value, exc_traceback)) # Qt widget calls are not safe from worker threads. Log there and # leave the UI alone; the page's own status/toast reports the failure. if threading.current_thread() is not threading.main_thread(): return detail = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)).strip() if len(detail) > 3200: detail = "…\n" + detail[-3200:] try: QMessageBox.critical( None, "BoltClam encountered an error", f"{detail}\n\nFull details: {log_path()}") except Exception: pass sys.excepthook = handle def _build_context(settings_store: SettingsStore, auth: AuthService, settings, session: Session) -> AppContext: storage = Storage() quarantine = QuarantineService(storage) clamav = ClamAVService(settings.clamav_path) api = ApiClient(settings.api_base_url, session.api_key, device_name=auth.device_name, install_id=settings.install_id) return AppContext( settings_store=settings_store, settings=settings, storage=storage, quarantine=quarantine, clamav=clamav, auth=auth, session_key=session.api_key, account=session.account, install_id=settings.install_id, api=api, offline=session.offline, ) def _restore_session(auth: AuthService, stored: str | None) -> tuple[Session | None, str]: if not stored: return None, "" try: return auth.restore(stored), "" except SessionExpired: return None, "Your previous BoltClam session expired. Sign in again to continue." except Exception as exc: cached = auth.account_cache.load() if cached: logger.info("Starting offline with the last verified account: %s", exc) return Session(stored, cached, offline=True), ( "Bolt is unreachable right now. BoltClam is using the last verified account " "and local protection still works.") logger.info("Could not restore the saved session: %s", exc) return None, f"Could not restore your BoltClam session: {exc}" def main() -> int: _set_windows_app_id() os.environ.setdefault("QT_ENABLE_HIGHDPI_SCALING", "1") app = QApplication(sys.argv) theme.setup_application(app) app.setQuitOnLastWindowClosed(False) app.setWindowIcon(brand.app_icon()) paths.ensure_dirs() lock = QLockFile(str(paths.config_dir() / "boltclam.lock")) if not lock.tryLock(150): QMessageBox.information( None, "BoltClam is already running", "Only one BoltClam can watch downloads at a time. Open the copy in the " "system tray, or quit it before starting another.") return 0 settings_store = SettingsStore() settings = settings_store.load() env_api = os.environ.get("BOLTCLAM_API_URL", "").strip() if env_api and env_api != settings.api_base_url: settings = settings_store.update(api_base_url=env_api) configure_logging(settings) _install_exception_hook() logger.info("BoltClam %s starting (%s)", __version__, paths.os_label()) splash = QSplashScreen(_splash_pixmap()) splash.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True) splash.show() app.processEvents() credentials = CredentialStore() account_cache = AccountCache() auth = AuthService( settings.api_base_url, install_id=settings.install_id, credentials=credentials, account_cache=account_cache) stored = auth.stored_key() session, notice = _restore_session(auth, stored) splash.close() if session is None: dialog = LoginDialog(auth, notice=notice) if dialog.exec() != QDialog.DialogCode.Accepted or dialog.session is None: logger.info("Sign-in cancelled at startup") return 0 session = dialog.session while True: context = _build_context(settings_store, auth, settings, session) window = MainWindow(context) loop = QEventLoop() window.finished.connect(loop.quit) window.show() loop.exec() logged_out = window.logged_out() window.deleteLater() app.processEvents() if not logged_out: break dialog = LoginDialog(auth, notice="You signed out. Sign in again to use BoltClam.") if dialog.exec() != QDialog.DialogCode.Accepted or dialog.session is None: break session = dialog.session settings = settings_store.load() logger.info("BoltClam exiting") return 0