import logging import os import platform import signal import stat import sys import time import traceback import webbrowser from logging.handlers import RotatingFileHandler from pathlib import Path APP_NAME = "angelhome" APP_VERSION = "2.1.1" START_URL = "https://angel.33.backend-3.com" ALLOWED_HOSTS = {"angel.33.backend-3.com"} ALLOW_SUBDOMAINS = False ALLOWED_PATH_PREFIXES = ["/"] OPEN_EXTERNAL_LINKS_IN_SYSTEM_BROWSER = False OFF_THE_RECORD = False WINDOW_TITLE = APP_NAME WINDOW_WIDTH = 1200 WINDOW_HEIGHT = 800 LOG_LEVEL = logging.INFO LOG_MAX_BYTES = 2 * 1024 * 1024 LOG_BACKUP_COUNT = 3 ALLOW_CLIPBOARD = False ALLOW_GEOLOCATION = False ALLOW_NOTIFICATIONS = False ALLOW_MEDIA_AUDIO_CAPTURE = False ALLOW_MEDIA_VIDEO_CAPTURE = False ALLOW_SCREEN_CAPTURE = False ALLOW_WEBRTC = False ENABLE_SINGLE_INSTANCE_IPC = True IPC_CONNECT_TIMEOUT_MS = 1500 ACTIVATE_WINDOW_MESSAGE = "ACTIVATE" MAX_RENDER_CRASHES = 3 RENDER_CRASH_WINDOW_SECONDS = 60 BLOCK_UNKNOWN_SCHEMES = True # ---------- Logging / runtime behavior ---------- # In production/frozen mode, default to no console logging. ENABLE_CONSOLE_LOGGING_WHEN_FROZEN = False # If True, redact path too, leaving scheme://host[:port] only. REDACT_URL_PATH = False # When frozen on Windows, silence stdout/stderr by default. DETACH_STDIO_WHEN_FROZEN = True def is_frozen() -> bool: return bool(getattr(sys, "frozen", False)) def detach_stdio_if_needed() -> None: """ Best-effort suppression of terminal output in frozen production builds. Packaging still matters: - PyInstaller: use --noconsole / windowed - cx_Freeze: use Win32GUI base """ try: if not is_frozen(): return if not DETACH_STDIO_WHEN_FROZEN: return devnull_r = open(os.devnull, "r", encoding="utf-8", errors="ignore") devnull_w = open(os.devnull, "a", encoding="utf-8", errors="ignore") try: sys.stdin = devnull_r except Exception: pass try: sys.stdout = devnull_w except Exception: pass try: sys.stderr = devnull_w except Exception: pass except Exception: pass def resource_dir() -> Path: if is_frozen() and hasattr(sys, "_MEIPASS"): return Path(sys._MEIPASS) if is_frozen(): return Path(sys.executable).resolve().parent return Path(__file__).resolve().parent def base_dir() -> Path: if is_frozen(): return Path(sys.executable).resolve().parent return Path(__file__).resolve().parent def set_directory_permissions(path: Path, mode: int) -> None: try: if os.name == "posix": os.chmod(path, mode) except Exception: pass def ensure_dir(path: Path, mode: int | None = None) -> Path: path.mkdir(parents=True, exist_ok=True) if mode is not None: set_directory_permissions(path, mode) return path def harden_directory_permissions(paths: list[tuple[Path, int]]) -> None: for path, mode in paths: try: set_directory_permissions(path, mode) except Exception: pass DATA_DIR = ensure_dir(base_dir() / f"{APP_NAME}_data", 0o700) LOG_DIR = ensure_dir(DATA_DIR / "logs", 0o700) RUNTIME_DIR = ensure_dir(DATA_DIR / "runtime", 0o700) CACHE_DIR = ensure_dir(DATA_DIR / "cache", 0o700) STORAGE_DIR = ensure_dir(DATA_DIR / "storage", 0o700) LOCK_FILE = RUNTIME_DIR / f"{APP_NAME}.lock" IPC_SOCKET_NAME = f"{APP_NAME}_single_instance_socket" def configure_runtime_environment() -> None: system = platform.system().lower() existing_flags = os.environ.get("QTWEBENGINE_CHROMIUM_FLAGS", "").strip() flags = existing_flags.split() if existing_flags else [] def add_flag(flag: str) -> None: if flag not in flags: flags.append(flag) os.environ.setdefault("QT_AUTO_SCREEN_SCALE_FACTOR", "1") os.environ.setdefault("QT_ENABLE_HIGHDPI_SCALING", "1") if system == "linux": ensure_dir(RUNTIME_DIR, 0o700) try: current_mode = stat.S_IMODE(os.stat(RUNTIME_DIR).st_mode) if current_mode != 0o700: os.chmod(RUNTIME_DIR, 0o700) except Exception: pass os.environ["XDG_RUNTIME_DIR"] = str(RUNTIME_DIR) os.environ.setdefault("QTWEBENGINE_DISABLE_SANDBOX", "1") add_flag("--no-sandbox") add_flag("--disable-extensions") add_flag("--disable-plugins") add_flag("--disable-background-networking") add_flag("--disable-component-update") add_flag("--disable-sync") add_flag("--no-default-browser-check") add_flag("--no-first-run") add_flag("--password-store=basic") add_flag("--use-mock-keychain") elif system in {"windows", "darwin"}: add_flag("--disable-extensions") add_flag("--disable-plugins") add_flag("--disable-background-networking") add_flag("--disable-component-update") add_flag("--disable-sync") add_flag("--no-default-browser-check") add_flag("--no-first-run") if not ALLOW_WEBRTC: add_flag("--disable-webrtc") if flags: os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = " ".join(flags) detach_stdio_if_needed() configure_runtime_environment() from PySide6.QtCore import QCoreApplication, QLockFile, QStandardPaths, QTimer, QUrl, Slot from PySide6.QtNetwork import QLocalServer, QLocalSocket from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox from PySide6.QtWebEngineCore import ( QWebEngineCertificateError, QWebEnginePage, QWebEngineProfile, QWebEngineSettings, QWebEngineUrlRequestInfo, QWebEngineUrlRequestInterceptor, ) from PySide6.QtWebEngineWidgets import QWebEngineView def safe_url_string(url) -> str: try: return url.toString() if isinstance(url, QUrl) else str(url) except Exception: return "" def redact_url(url: QUrl | str, hide_path: bool = REDACT_URL_PATH) -> str: try: qurl = url if isinstance(url, QUrl) else QUrl(str(url)) if not qurl.isValid(): return "" scheme = (qurl.scheme() or "").lower() host = qurl.host() or "" port = qurl.port() path = qurl.path() or "/" if not scheme or not host: return "" if port and port > 0: base = f"{scheme}://{host}:{port}" else: base = f"{scheme}://{host}" if hide_path: return base return f"{base}{path}" except Exception: return "" def configure_logging() -> logging.Logger: logger = logging.getLogger(APP_NAME) logger.setLevel(LOG_LEVEL) if logger.handlers: return logger fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s") if not (is_frozen() and not ENABLE_CONSOLE_LOGGING_WHEN_FROZEN): sh = logging.StreamHandler(sys.stdout) sh.setFormatter(fmt) logger.addHandler(sh) fh = RotatingFileHandler( str(LOG_DIR / f"{APP_NAME}.log"), maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUP_COUNT, encoding="utf-8", ) fh.setFormatter(fmt) logger.addHandler(fh) logger.propagate = False return logger logger = configure_logging() def is_root() -> bool: try: return hasattr(os, "geteuid") and os.geteuid() == 0 except Exception: return False def is_allowed_host(host: str) -> bool: host = (host or "").lower().strip() if host in ALLOWED_HOSTS: return True if ALLOW_SUBDOMAINS: for allowed in ALLOWED_HOSTS: allowed = (allowed or "").lower().strip() if host == allowed or host.endswith("." + allowed): return True return False def is_allowed_path(path: str) -> bool: path = path or "/" return any(path.startswith(prefix) for prefix in ALLOWED_PATH_PREFIXES) def is_allowed_url(url: QUrl) -> bool: try: return bool( url and url.isValid() and url.scheme().lower() == "https" and is_allowed_host(url.host()) and is_allowed_path(url.path()) and not url.userName() and not url.password() ) except Exception: return False def validate_configuration() -> None: if not APP_NAME or not isinstance(APP_NAME, str): raise ValueError("APP_NAME must be a non-empty string") if not ALLOWED_HOSTS or not isinstance(ALLOWED_HOSTS, (set, list, tuple)): raise ValueError("ALLOWED_HOSTS must be a non-empty set/list/tuple") qurl = QUrl(START_URL) if not qurl.isValid(): raise ValueError(f"Invalid START_URL: {START_URL}") if not is_allowed_url(qurl): raise ValueError("START_URL is not allowed by current security policy") def open_in_system_browser(url: QUrl) -> None: try: url_str = safe_url_string(url) ok = webbrowser.open(url_str) if ok: logger.info("Opened external link in system browser: %s", redact_url(url)) else: logger.warning("System browser did not confirm opening URL: %s", redact_url(url)) except Exception: logger.exception("Failed to open URL in system browser: %s", redact_url(url)) class RequestInterceptor(QWebEngineUrlRequestInterceptor): def interceptRequest(self, info: QWebEngineUrlRequestInfo) -> None: try: url = info.requestUrl() scheme = (url.scheme() or "").lower() host = (url.host() or "").lower() method = bytes(info.requestMethod()).decode("utf-8", "ignore") if BLOCK_UNKNOWN_SCHEMES and scheme not in {"https", "wss"}: logger.warning( "[BLOCKED REQUEST] reason=scheme scheme=%s method=%s url=%s resource_type=%s", scheme, method, redact_url(url), info.resourceType(), ) info.block(True) return if not is_allowed_host(host): logger.warning( "[BLOCKED REQUEST] reason=host scheme=%s host=%s method=%s url=%s resource_type=%s", scheme, host, method, redact_url(url), info.resourceType(), ) info.block(True) return if scheme == "https": if not is_allowed_path(url.path()): logger.warning( "[BLOCKED REQUEST] reason=path method=%s url=%s resource_type=%s", method, redact_url(url), info.resourceType(), ) info.block(True) return elif scheme == "wss": if not is_allowed_host(host): logger.warning( "[BLOCKED REQUEST] reason=websocket-host method=%s url=%s", method, redact_url(url), ) info.block(True) return except Exception: logger.exception("Error while intercepting request") try: info.block(True) except Exception: pass class SingleInstance: def __init__(self, lock_path: Path): self.lock = QLockFile(str(lock_path)) try: self.lock.setStaleLockTime(0) except Exception: pass def acquire(self) -> bool: try: return self.lock.tryLock(0) except Exception: logger.exception("Failed to acquire single-instance lock") return False def release(self) -> None: try: self.lock.unlock() except Exception: logger.debug("Failed to release single-instance lock", exc_info=True) class ActivationServer: def __init__(self, server_name: str, activate_callback): self.server_name = server_name self.activate_callback = activate_callback self.server = None def start(self) -> bool: if not ENABLE_SINGLE_INSTANCE_IPC: return True try: QLocalServer.removeServer(self.server_name) except Exception: pass self.server = QLocalServer() self.server.newConnection.connect(self._handle_connection) if not self.server.listen(self.server_name): logger.warning( "Failed to start local activation server: %s", self.server.errorString() if hasattr(self.server, "errorString") else "unknown", ) return False logger.info("Activation server listening: %s", self.server_name) return True def close(self) -> None: try: if self.server is not None: self.server.close() except Exception: logger.debug("Failed to close activation server", exc_info=True) try: QLocalServer.removeServer(self.server_name) except Exception: logger.debug("Failed to remove activation server name", exc_info=True) @Slot() def _handle_connection(self) -> None: try: while self.server.hasPendingConnections(): socket_ = self.server.nextPendingConnection() if socket_ is None: continue socket_.waitForReadyRead(500) data = bytes(socket_.readAll()).decode("utf-8", "ignore").strip() logger.info("Received IPC message: %s", data) if data == ACTIVATE_WINDOW_MESSAGE: try: self.activate_callback() except Exception: logger.exception("Failed to activate existing window") socket_.disconnectFromServer() except Exception: logger.exception("Failed to handle IPC activation connection") def signal_existing_instance(server_name: str) -> bool: if not ENABLE_SINGLE_INSTANCE_IPC: return False try: socket_ = QLocalSocket() socket_.connectToServer(server_name) if not socket_.waitForConnected(IPC_CONNECT_TIMEOUT_MS): return False socket_.write(ACTIVATE_WINDOW_MESSAGE.encode("utf-8")) socket_.flush() socket_.waitForBytesWritten(IPC_CONNECT_TIMEOUT_MS) socket_.disconnectFromServer() return True except Exception: logger.debug("Failed to signal existing instance", exc_info=True) return False class LockedPage(QWebEnginePage): def __init__(self, profile: QWebEngineProfile, parent=None): super().__init__(profile, parent) try: self.certificateError.connect(self._on_certificate_error) except Exception: logger.debug("certificateError signal unavailable", exc_info=True) try: self.featurePermissionRequested.connect(self._on_feature_permission_requested) except Exception: logger.debug("featurePermissionRequested signal unavailable", exc_info=True) try: self.windowCloseRequested.connect(self._on_window_close_requested) except Exception: logger.debug("windowCloseRequested signal unavailable", exc_info=True) def javaScriptConsoleMessage(self, level, message, line_number, source_id): try: logger.warning( "[JS CONSOLE] level=%s source=%s line=%s message=%s", level, source_id, line_number, message, ) except Exception: logger.exception("Failed to log JavaScript console message") def acceptNavigationRequest(self, url, nav_type, is_main_frame): try: if is_allowed_url(url): return super().acceptNavigationRequest(url, nav_type, is_main_frame) logger.warning( "[BLOCKED NAVIGATION] url=%s is_main_frame=%s nav_type=%s", redact_url(url), is_main_frame, nav_type, ) if OPEN_EXTERNAL_LINKS_IN_SYSTEM_BROWSER and url.scheme().lower() in {"http", "https"}: open_in_system_browser(url) return False except Exception: logger.exception("Error while processing navigation request") return False def createWindow(self, _type): logger.warning("[BLOCKED POPUP] createWindow request denied") return None @Slot() def _on_window_close_requested(self): logger.warning("[BLOCKED WINDOW CLOSE] windowCloseRequested denied") @Slot(QUrl, object) def _on_feature_permission_requested(self, security_origin, feature): try: allowed = False try: feature_enum = QWebEnginePage.Feature except Exception: feature_enum = None if feature_enum is not None: feature_map = { getattr(feature_enum, "Geolocation", None): ALLOW_GEOLOCATION, getattr(feature_enum, "Notifications", None): ALLOW_NOTIFICATIONS, getattr(feature_enum, "MediaAudioCapture", None): ALLOW_MEDIA_AUDIO_CAPTURE, getattr(feature_enum, "MediaVideoCapture", None): ALLOW_MEDIA_VIDEO_CAPTURE, getattr(feature_enum, "MediaAudioVideoCapture", None): ( ALLOW_MEDIA_AUDIO_CAPTURE and ALLOW_MEDIA_VIDEO_CAPTURE ), getattr(feature_enum, "DesktopVideoCapture", None): ALLOW_SCREEN_CAPTURE, getattr(feature_enum, "DesktopAudioVideoCapture", None): ( ALLOW_SCREEN_CAPTURE and ALLOW_MEDIA_AUDIO_CAPTURE ), getattr(feature_enum, "ClipboardReadWrite", None): ALLOW_CLIPBOARD, getattr(feature_enum, "ClipboardRead", None): ALLOW_CLIPBOARD, } if feature in feature_map: allowed = bool(feature_map[feature]) logger.warning( "[FEATURE PERMISSION] origin=%s feature=%s allowed=%s", redact_url(security_origin, hide_path=True), str(feature), allowed, ) self.setFeaturePermission( security_origin, feature, QWebEnginePage.PermissionPolicy.PermissionGrantedByUser if allowed else QWebEnginePage.PermissionPolicy.PermissionDeniedByUser, ) except Exception: logger.exception("Failed to process feature permission request") try: self.setFeaturePermission( security_origin, feature, QWebEnginePage.PermissionPolicy.PermissionDeniedByUser, ) except Exception: pass @Slot(QWebEngineCertificateError) def _on_certificate_error(self, error): try: logger.error( "[TLS ERROR] %s @ %s", error.description(), redact_url(error.url()), ) error.rejectCertificate() except Exception: logger.exception("Failed to reject certificate") class MainWindow(QMainWindow): def __init__(self): super().__init__() self.render_crash_times = [] self.setWindowTitle(WINDOW_TITLE) self.resize(WINDOW_WIDTH, WINDOW_HEIGHT) self.browser = QWebEngineView(self) self.setCentralWidget(self.browser) self.profile = self._build_profile() self.page = LockedPage(self.profile, self.browser) self.browser.setPage(self.page) self._apply_webengine_settings() self._connect_browser_signals() self._load_start_url() def _build_profile(self) -> QWebEngineProfile: try: ensure_dir(CACHE_DIR, 0o700) ensure_dir(STORAGE_DIR, 0o700) if OFF_THE_RECORD: profile = QWebEngineProfile(self.browser) logger.info("Using off-the-record web profile") else: profile = QWebEngineProfile(APP_NAME, self.browser) profile.setCachePath(str(CACHE_DIR)) profile.setPersistentStoragePath(str(STORAGE_DIR)) logger.info( "Using persistent profile cache=%s storage=%s", str(CACHE_DIR), str(STORAGE_DIR), ) try: if OFF_THE_RECORD: profile.setPersistentCookiesPolicy( QWebEngineProfile.PersistentCookiesPolicy.NoPersistentCookies ) else: profile.setPersistentCookiesPolicy( QWebEngineProfile.PersistentCookiesPolicy.AllowPersistentCookies ) except Exception: logger.debug("setPersistentCookiesPolicy unavailable", exc_info=True) try: profile.setHttpCacheType( QWebEngineProfile.HttpCacheType.MemoryHttpCache if OFF_THE_RECORD else QWebEngineProfile.HttpCacheType.DiskHttpCache ) except Exception: logger.debug("setHttpCacheType unavailable", exc_info=True) try: self.request_interceptor = RequestInterceptor() profile.setUrlRequestInterceptor(self.request_interceptor) logger.info("Installed QWebEngineUrlRequestInterceptor") except Exception: logger.exception("Failed to install request interceptor") raise try: profile.downloadRequested.connect(self._deny_download) except Exception: logger.debug("downloadRequested signal unavailable", exc_info=True) return profile except Exception as exc: logger.exception("Failed to build QWebEngineProfile") raise RuntimeError("Could not initialize browser profile") from exc def _apply_webengine_settings(self) -> None: try: settings = self.browser.settings() settings.setAttribute( QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows, False ) settings.setAttribute( QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, False ) settings.setAttribute( QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, False ) settings.setAttribute( QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, False ) settings.setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, False) settings.setAttribute( QWebEngineSettings.WebAttribute.ErrorPageEnabled, True ) for attr_name, value in ( ("HyperlinkAuditingEnabled", False), ("PdfViewerEnabled", False), ("PlaybackRequiresUserGesture", True), ("JavascriptCanAccessClipboard", ALLOW_CLIPBOARD), ("WebRTCPublicInterfacesOnly", not ALLOW_WEBRTC), ("ScreenCaptureEnabled", ALLOW_SCREEN_CAPTURE), ): try: attr = getattr(QWebEngineSettings.WebAttribute, attr_name) settings.setAttribute(attr, value) except Exception: logger.debug("WebAttribute unavailable: %s", attr_name, exc_info=True) logger.info("Applied WebEngine security settings") except Exception: logger.exception("Failed to apply WebEngine settings") raise def _connect_browser_signals(self) -> None: for signal_name, handler in ( ("loadStarted", self._on_load_started), ("loadProgress", self._on_load_progress), ("loadFinished", self._on_load_finished), ("urlChanged", self._on_url_changed), ("renderProcessTerminated", self._on_render_process_terminated), ("titleChanged", self._on_title_changed), ): try: getattr(self.browser, signal_name).connect(handler) except Exception: logger.debug("Could not connect signal: %s", signal_name, exc_info=True) def _load_start_url(self) -> None: try: qurl = QUrl(START_URL) if not is_allowed_url(qurl): raise ValueError("Refusing to load disallowed START_URL") self.browser.setUrl(qurl) logger.info("Loading start URL: %s", redact_url(qurl)) except Exception: logger.exception("Failed to load start URL") QMessageBox.critical(self, APP_NAME, "Failed to load the application start URL.") raise def _deny_download(self, item) -> None: try: logger.warning("[BLOCKED DOWNLOAD] %s", redact_url(item.url())) item.cancel() except Exception: logger.exception("Failed while denying download request") def activate_and_raise(self) -> None: try: if self.isMinimized(): self.showNormal() else: self.show() self.raise_() self.activateWindow() logger.info("Activated existing main window") except Exception: logger.exception("Failed to activate main window") def _record_render_crash(self) -> int: now = time.time() self.render_crash_times.append(now) self.render_crash_times = [ t for t in self.render_crash_times if now - t <= RENDER_CRASH_WINDOW_SECONDS ] return len(self.render_crash_times) @Slot() def _on_load_started(self): logger.info("Page load started") @Slot(int) def _on_load_progress(self, progress): logger.debug("Page load progress: %s%%", progress) @Slot(bool) def _on_load_finished(self, ok): current_url = redact_url(self.browser.url()) if ok: logger.info("Page load finished successfully: %s", current_url) else: logger.error("Page load failed: %s", current_url) QTimer.singleShot( 0, lambda: QMessageBox.warning( self, APP_NAME, "The page failed to load. Please verify network and server availability.", ), ) @Slot(QUrl) def _on_url_changed(self, url): logger.info("URL changed: %s", redact_url(url)) @Slot(str) def _on_title_changed(self, title): try: self.setWindowTitle(f"{WINDOW_TITLE} - {title}" if title else WINDOW_TITLE) except Exception: logger.exception("Failed to update window title") @Slot(object, int) def _on_render_process_terminated(self, termination_status, exit_code): crash_count = self._record_render_crash() logger.error( "Render process terminated: status=%s exit_code=%s recent_crashes=%s", termination_status, exit_code, crash_count, ) if crash_count >= MAX_RENDER_CRASHES: try: QMessageBox.critical( self, APP_NAME, "The embedded browser process has crashed repeatedly.\n\n" "Automatic reload has been stopped to avoid a crash loop.\n" "Please restart the application or contact support.", ) except Exception: logger.exception("Failed showing crash-loop dialog") return try: QMessageBox.critical( self, APP_NAME, "The embedded browser process terminated unexpectedly. The page will be reloaded.", ) except Exception: logger.exception("Failed showing render-process-terminated dialog") try: self.browser.reload() except Exception: logger.exception("Failed to reload browser after render process termination") def excepthook(exc_type, exc_value, exc_tb): tb_text = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) logger.critical("Unhandled exception:\n%s", tb_text) try: QMessageBox.critical(None, APP_NAME, f"An unexpected error occurred:\n\n{exc_value}") except Exception: pass def install_signal_handlers(app: QApplication) -> None: try: signal.signal(signal.SIGINT, lambda *_: app.quit()) except Exception: logger.debug("SIGINT handler unavailable", exc_info=True) try: signal.signal(signal.SIGTERM, lambda *_: app.quit()) except Exception: logger.debug("SIGTERM handler unavailable", exc_info=True) def configure_qt_paths() -> None: try: QCoreApplication.setApplicationName(APP_NAME) QCoreApplication.setOrganizationName(APP_NAME) QCoreApplication.setApplicationVersion(APP_VERSION) if is_frozen(): helper_name = ( "QtWebEngineProcess.exe" if platform.system().lower() == "windows" else "QtWebEngineProcess" ) candidates = [ resource_dir() / helper_name, resource_dir() / "PySide6" / "Qt" / "libexec" / helper_name, resource_dir() / "Qt" / "libexec" / helper_name, base_dir() / helper_name, base_dir() / "PySide6" / "Qt" / "libexec" / helper_name, base_dir() / "Qt" / "libexec" / helper_name, ] for candidate in candidates: if candidate.exists() and candidate.is_file(): os.environ["QTWEBENGINEPROCESS_PATH"] = str(candidate) logger.info("Using QtWebEngineProcess: %s", candidate) break else: logger.warning("QtWebEngineProcess helper not found in expected locations") else: os.environ.pop("QTWEBENGINEPROCESS_PATH", None) except Exception: logger.debug("Failed to configure Qt paths/environment", exc_info=True) def harden_runtime_paths() -> None: try: harden_directory_permissions( [ (DATA_DIR, 0o700), (LOG_DIR, 0o700), (RUNTIME_DIR, 0o700), (CACHE_DIR, 0o700), (STORAGE_DIR, 0o700), ] ) logger.info("Hardened directory permissions for app data paths") except Exception: logger.exception("Failed to harden runtime paths") def main(): instance = None activation_server = None try: validate_configuration() if is_root(): logger.error("Refusing to run as root") sys.exit(1) sys.excepthook = excepthook configure_qt_paths() harden_runtime_paths() QApplication.setApplicationName(APP_NAME) QApplication.setOrganizationName(APP_NAME) app = QApplication(sys.argv) install_signal_handlers(app) instance = SingleInstance(LOCK_FILE) if not instance.acquire(): logger.warning("Another instance is already running") activated = signal_existing_instance(IPC_SOCKET_NAME) if activated: logger.info("Signaled existing instance to activate window") else: QMessageBox.warning( None, APP_NAME, "Another instance of this application is already running.", ) sys.exit(1) try: logger.info( "Qt AppDataLocation(s): %s", QStandardPaths.standardLocations( QStandardPaths.StandardLocation.AppDataLocation ), ) except Exception: logger.debug("Failed to query Qt standard paths", exc_info=True) window = MainWindow() activation_server = ActivationServer(IPC_SOCKET_NAME, window.activate_and_raise) activation_server.start() try: app.aboutToQuit.connect( lambda: activation_server.close() if activation_server else None ) except Exception: logger.debug("Could not connect activation server cleanup", exc_info=True) try: app.aboutToQuit.connect(lambda: instance.release() if instance else None) except Exception: logger.debug("Could not connect lock cleanup", exc_info=True) window.show() logger.info( "Application started platform=%s python=%s frozen=%s version=%s resource_dir=%s base_dir=%s data_dir=%s", platform.platform(), sys.version.split()[0], is_frozen(), APP_VERSION, resource_dir(), base_dir(), DATA_DIR, ) rc = app.exec() if activation_server: activation_server.close() if instance: instance.release() sys.exit(rc) except Exception as exc: logger.critical("Fatal startup error: %s", exc, exc_info=True) try: if QApplication.instance() is not None: QMessageBox.critical(None, APP_NAME, f"Fatal startup error:\n\n{exc}") except Exception: pass try: if activation_server: activation_server.close() except Exception: pass try: if instance: instance.release() except Exception: pass sys.exit(1) if __name__ == "__main__": main()