Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f520fb8f | ||
|
|
ffe95020bc | ||
|
|
b262b1e664 |
+13
-9
@@ -10,15 +10,19 @@ $Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $Root
|
||||
|
||||
function Get-Python {
|
||||
$candidates = @("py -3.11", "py -3", "python")
|
||||
$candidates = @(
|
||||
@{ Command = "py"; Arguments = @("-3.11") },
|
||||
@{ Command = "py"; Arguments = @("-3") },
|
||||
@{ Command = "python"; Arguments = @() },
|
||||
@{ Command = "python3"; Arguments = @() }
|
||||
)
|
||||
foreach ($candidate in $candidates) {
|
||||
try {
|
||||
$parts = $candidate.Split(" ")
|
||||
$exe = $parts[0]
|
||||
$exeArgs = @($parts | Select-Object -Skip 1)
|
||||
$output = & $exe @exeArgs -c "import sys; print(sys.executable); print(sys.version_info[:2])" 2>$null
|
||||
if ($LASTEXITCODE -eq 0 -and $output -match "3\.11") {
|
||||
return $parts
|
||||
$exe = $candidate.Command
|
||||
$exeArgs = @($candidate.Arguments)
|
||||
$probe = & $exe @exeArgs -c "import sys; print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))" 2>$null
|
||||
if ($LASTEXITCODE -eq 0 -and (($probe | Out-String).Trim() -eq "3.11")) {
|
||||
return $candidate
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
@@ -31,8 +35,8 @@ $python = Get-Python
|
||||
|
||||
if (-not (Test-Path $venv)) {
|
||||
Write-Host "Creating build environment..."
|
||||
$pythonExe = $python[0]
|
||||
$pythonArgs = @($python | Select-Object -Skip 1)
|
||||
$pythonExe = $python.Command
|
||||
$pythonArgs = @($python.Arguments)
|
||||
& $pythonExe @pythonArgs -m venv $venv
|
||||
}
|
||||
$py = Join-Path $venv "Scripts\python.exe"
|
||||
|
||||
+18
-10
@@ -5,7 +5,9 @@ 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
|
||||
@@ -18,7 +20,7 @@ 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
|
||||
from .logging_setup import configure as configure_logging, log_path
|
||||
from .quarantine import QuarantineService
|
||||
from .storage import Storage
|
||||
from .ui import brand, theme
|
||||
@@ -50,20 +52,19 @@ def _splash_pixmap() -> QPixmap:
|
||||
brand.paint_mark(painter, QRectF(34, 34, 72, 72))
|
||||
painter.setPen(QColor(theme.TEXT))
|
||||
painter.setFont(theme.display_font(26, QFont.Weight.DemiBold))
|
||||
painter.drawText(QRectF(124, 36, 300, 40), Qt.AlignmentFlag.AlignLeft,
|
||||
Qt.AlignmentFlag.AlignVCenter, "BoltClam")
|
||||
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), Qt.AlignmentFlag.AlignLeft,
|
||||
Qt.AlignmentFlag.AlignVCenter, "ClamAV protection, without the noise.")
|
||||
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), Qt.AlignmentFlag.AlignLeft,
|
||||
Qt.AlignmentFlag.AlignVCenter, "Starting protection…")
|
||||
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), Qt.AlignmentFlag.AlignLeft,
|
||||
Qt.AlignmentFlag.AlignVCenter, f"Version {__version__} · {__build_date__}")
|
||||
painter.drawText(QRectF(36, 206, 390, 20), left_center,
|
||||
f"Version {__version__} · {__build_date__}")
|
||||
painter.end()
|
||||
return image
|
||||
|
||||
@@ -74,10 +75,17 @@ def _install_exception_hook() -> None:
|
||||
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"{exc_type.__name__}: {exc_value}\n\nDetails were written to the BoltClam log.")
|
||||
f"{detail}\n\nFull details: {log_path()}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -259,6 +259,16 @@ class DownloadWatcher(QThread):
|
||||
self._stop.set()
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
self._watch_loop()
|
||||
except Exception:
|
||||
logger.exception("Real-time watcher stopped after an error")
|
||||
try:
|
||||
self.watch_status.emit("Real-time watcher stopped")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _watch_loop(self) -> None:
|
||||
import threading
|
||||
|
||||
stop = threading.Event()
|
||||
|
||||
@@ -142,7 +142,7 @@ class Avatar(QWidget):
|
||||
painter.setPen(QColor(theme.AMBER_BRIGHT))
|
||||
font = theme.display_font(self._size * 0.30, QFont.Weight.DemiBold)
|
||||
painter.setFont(font)
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, self.initials())
|
||||
painter.drawText(rect, int(Qt.AlignmentFlag.AlignCenter), self.initials())
|
||||
|
||||
|
||||
class ProtectionDial(QWidget):
|
||||
@@ -331,9 +331,10 @@ class NavButton(QAbstractButton):
|
||||
painter.setPen(QColor(theme.TEXT if checked else theme.TEXT_SOFT))
|
||||
font = theme.body_font(10.4, QFont.Weight.DemiBold if checked else QFont.Weight.Normal)
|
||||
painter.setFont(font)
|
||||
painter.drawText(QRectF(46, 0, self.width() - 52, self.height()),
|
||||
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft,
|
||||
self.text())
|
||||
painter.drawText(
|
||||
QRectF(46, 0, self.width() - 52, self.height()),
|
||||
int(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft),
|
||||
self.text())
|
||||
|
||||
|
||||
def _draw_nav_icon(painter: QPainter, name: str, rect: QRectF, color: QColor) -> None:
|
||||
@@ -529,9 +530,10 @@ class ScanButton(QAbstractButton):
|
||||
|
||||
painter.setPen(QColor("#241704" if self.isEnabled() else "#807A68"))
|
||||
painter.setFont(theme.body_font(11.2, QFont.Weight.Bold))
|
||||
painter.drawText(QRectF(core.left(), core.center().y() + 20, core.width(), 24),
|
||||
Qt.AlignmentFlag.AlignCenter,
|
||||
"Scanning…" if self._scanning else "Start scan")
|
||||
painter.drawText(
|
||||
QRectF(core.left(), core.center().y() + 20, core.width(), 24),
|
||||
int(Qt.AlignmentFlag.AlignCenter),
|
||||
"Scanning…" if self._scanning else "Start scan")
|
||||
|
||||
|
||||
class StatTile(QFrame):
|
||||
|
||||
@@ -58,6 +58,19 @@ def test_firefox_download_preference_is_watched(tmp_path, monkeypatch):
|
||||
assert download.resolve() in [path.resolve() for path in roots]
|
||||
|
||||
|
||||
def test_watcher_stops_cleanly_instead_of_raising(monkeypatch, qapp):
|
||||
messages: list[str] = []
|
||||
watcher = realtime.DownloadWatcher(interval=0.1)
|
||||
watcher.watch_status.connect(messages.append)
|
||||
monkeypatch.setattr(realtime, "browser_download_roots", lambda extra: (_ for _ in ()).throw(
|
||||
RuntimeError("simulated watcher failure")))
|
||||
watcher.start()
|
||||
assert watcher.wait(3000)
|
||||
qapp.processEvents()
|
||||
assert not watcher.isRunning()
|
||||
assert "stopped" in " ".join(messages).lower()
|
||||
|
||||
|
||||
def test_copy_for_scan_preserves_bytes(tmp_path):
|
||||
source = tmp_path / "download.exe"
|
||||
source.write_bytes(b"sample bytes")
|
||||
|
||||
@@ -66,12 +66,24 @@ def test_main_window_builds_four_pages_and_switches_tabs(qapp, isolated_env):
|
||||
assert window.stack.currentIndex() == 3
|
||||
assert window.settings_page.channel_combo.currentData() == "release"
|
||||
assert window.about_page.rows["account"].value_label.text() == "Aaron"
|
||||
for index in range(4):
|
||||
window.stack.setCurrentIndex(index)
|
||||
qapp.processEvents()
|
||||
assert not window.stack.currentWidget().grab().isNull()
|
||||
finally:
|
||||
window._allow_close = True
|
||||
window.close()
|
||||
qapp.processEvents()
|
||||
|
||||
|
||||
def test_splash_pixmap_renders(qapp):
|
||||
from boltclam.app import _splash_pixmap
|
||||
|
||||
pixmap = _splash_pixmap()
|
||||
assert not pixmap.isNull()
|
||||
assert pixmap.width() == 460 and pixmap.height() == 250
|
||||
|
||||
|
||||
def test_login_dialog_builds_without_starting_a_worker(qapp, isolated_env):
|
||||
auth = AuthService(
|
||||
"http://127.0.0.1:9", install_id="test",
|
||||
|
||||
Reference in New Issue
Block a user