From cbd88bd45f5106ba5a49249d782b1f2a658eb763 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Sat, 4 Jan 2025 23:34:53 +0530 Subject: [PATCH 1/5] format fix --- openadapt/app/tray.py | 143 +++++++++++++++++++++++++++++++++--- openadapt/entrypoint.py | 147 ++++++++++++++++++++++++++++++++++--- openadapt/splash_screen.py | 84 +++++++++++++++++++++ 3 files changed, 352 insertions(+), 22 deletions(-) create mode 100644 openadapt/splash_screen.py diff --git a/openadapt/app/tray.py b/openadapt/app/tray.py index 805a39b60..dd52c91ac 100644 --- a/openadapt/app/tray.py +++ b/openadapt/app/tray.py @@ -6,15 +6,16 @@ from datetime import datetime from functools import partial from pprint import pformat -from threading import Thread +from threading import Event, Thread from typing import Any, Callable import inspect import multiprocessing import os import sys +import time from pyqttoast import Toast, ToastButtonAlignment, ToastIcon, ToastPosition, ToastPreset -from PySide6.QtCore import QMargins, QObject, QSize, Qt, QThread, Signal +from PySide6.QtCore import QMargins, QObject, QSize, Qt, QThread, QTimer, Signal from PySide6.QtGui import QAction, QColor, QFont, QIcon, QPixmap from PySide6.QtWidgets import ( QApplication, @@ -32,8 +33,9 @@ QVBoxLayout, QWidget, ) +import requests -from openadapt.app import quick_record, stop_record, FPATH +from openadapt.app import FPATH, quick_record, stop_record from openadapt.app.dashboard.run import cleanup as cleanup_dashboard from openadapt.app.dashboard.run import run as run_dashboard from openadapt.build_utils import is_running_from_executable @@ -97,6 +99,97 @@ def run(self) -> None: self.data.emit(data) +class DashboardMonitor(QObject): + """Monitor dashboard initialization.""" + + ready = Signal() + + def __init__(self, app: QApplication = None, port=5173): + super().__init__() + self.stop_flag = Event() + self.port = port + self._is_ready = False + + if app is not None: + self.monitor_thread = QThread() + self.moveToThread(self.monitor_thread) + self.monitor_thread.started.connect(self.monitor_startup) + self.monitor_thread.finished.connect(self.on_thread_finished) + else: + self.monitor_thread = None + + print("DEBUG(DashboardMonitor): Signal ready created") + + # Connect to our own ready signal to update state + self.ready.connect(self._update_ready_state) + + def _update_ready_state(self): + """Update internal ready state when signal is emitted.""" + self._is_ready = True + print("DEBUG(DashboardMonitor): Ready state updated") + + def on_thread_finished(self): + """Handle thread finished signal.""" + logger.info("Dashboard monitor thread finished") + + def monitor_startup(self): + """Monitor dashboard startup process.""" + logger.info("Starting dashboard monitoring") + start_time = time.time() + try: + while not self.stop_flag.is_set(): + try: + elapsed_time = time.time() - start_time + print( + "DEBUG(DashboardMonitor): Checking dashboard. Elapsed:" + f" {elapsed_time:.2f}s" + ) + + response = requests.get(f"http://localhost:{self.port}", timeout=1) + if response.status_code == 200: + logger.info("Dashboard is ready!") + + # Emit signal in main thread + QTimer.singleShot(0, self.on_dashboard_ready) + break + except requests.RequestException as e: + logger.debug(f"Connection attempt failed: {e}") + time.sleep(0.5) + + if time.time() - start_time > 30: + logger.warning("Monitoring timeout") + break + finally: + self.monitor_thread.quit() + + def on_dashboard_ready(self): + """Handle dashboard being ready.""" + try: + self.ready.emit() + logger.info("Emitting ready signal") + except Exception as e: + logger.error(f"Error emitting signal: {e}") + + def check_ready_state(self): + """Check if dashboard is ready and re-emit if necessary.""" + if self._is_ready: + QTimer.singleShot(0, self.on_dashboard_ready) + + def stop(self): + """Stop monitoring and cleanup thread.""" + logger.info("Stopping dashboard monitor") + self.stop_flag.set() + + if self.monitor_thread and self.monitor_thread.isRunning(): + try: + self.monitor_thread.quit() + if not self.monitor_thread.wait(1000): + logger.warning("Dashboard monitor thread did not stop cleanly") + self.monitor_thread.terminate() + except Exception as e: + logger.error(f"Error stopping dashboard monitor thread: {e}") + + class SystemTrayIcon: """System tray icon for OpenAdapt.""" @@ -107,9 +200,12 @@ class SystemTrayIcon: # storing actions is required to prevent garbage collection recording_actions = {"visualize": [], "replay": []} - def __init__(self) -> None: + def __init__(self, app: QApplication = None) -> None: """Initialize the system tray icon.""" - self.app = QApplication([]) + if app is None: + self.app = QApplication([]) + else: + self.app = app if sys.platform == "darwin": # hide Dock icon while allowing focus on dialogs @@ -492,13 +588,36 @@ def populate_menu(self, menu: QMenu, action: Callable, action_type: str) -> None def launch_dashboard(self) -> None: """Launch the web dashboard.""" - if self.dashboard_thread: - if is_running_from_executable(): - return - cleanup_dashboard() - self.dashboard_thread.join() - self.dashboard_thread = run_dashboard() - self.dashboard_thread.start() + try: + if self.dashboard_thread: + if is_running_from_executable(): + return + cleanup_dashboard() + self.dashboard_thread.join() + + # Start dashboard + self.dashboard_thread = run_dashboard() + self.dashboard_thread.start() + logger.info("Dashboard thread started") + + # Initialize dashboard monitor + self.dashboard_monitor = DashboardMonitor(app=self.app) + + # Connect ready signal BEFORE starting monitoring + self.dashboard_monitor.ready.connect( + self.on_dashboard_ready, Qt.ConnectionType.QueuedConnection + ) + + # Start monitoring + self.dashboard_monitor.monitor_startup() + logger.info("Dashboard monitor started") + + except Exception as e: + logger.error(f"Launch dashboard error: {e}") + + def on_dashboard_ready(self): + """Handle dashboard being ready.""" + logger.info("Dashboard is ready - performing final setup") def run(self) -> None: """Run the system tray icon.""" diff --git a/openadapt/entrypoint.py b/openadapt/entrypoint.py index 1d05a5cdc..6711a29aa 100644 --- a/openadapt/entrypoint.py +++ b/openadapt/entrypoint.py @@ -1,31 +1,158 @@ """Entrypoint for OpenAdapt.""" import multiprocessing +import sys -if __name__ == "__main__": - # This needs to be called before any code that uses multiprocessing - multiprocessing.freeze_support() +from PySide6.QtCore import QObject, Qt, QTimer, Signal +from PySide6.QtWidgets import QApplication +from openadapt.app import tray from openadapt.build_utils import redirect_stdout_stderr -from openadapt.error_reporting import configure_error_reporting from openadapt.custom_logger import logger +from openadapt.error_reporting import configure_error_reporting +from openadapt.splash_screen import LoadingScreen -def run_openadapt() -> None: - """Run OpenAdapt.""" - with redirect_stdout_stderr(): +class LoadingManager(QObject): + """Manages the loading stages and progress updates.""" + + progress_updated = Signal(int, str) + loading_complete = Signal() + + def __init__(self, splash_screen, app): + super().__init__() + self.splash = splash_screen + self.app = app + self.progress_updated.connect(self._update_progress) + + def _update_progress(self, value, message): + """Update progress bar and process events.""" + self.splash.update_progress(value) + self.splash.update_status(message) + self.app.processEvents() + logger.debug(f"Progress: {value}% - {message}") + + def start_loading_sequence(self): + """Execute the loading sequence with visible progress updates.""" + # Initial setup - 0% + self.progress_updated.emit(0, "Initializing...") + + # Configuration - 20% try: - from openadapt.alembic.context_loader import load_alembic_context - from openadapt.app import tray from openadapt.config import print_config print_config() + self.progress_updated.emit(20, "Loading configuration...") + except Exception as e: + logger.error(f"Configuration error: {e}") + return False + + # Error reporting setup - 40% + try: configure_error_reporting() + self.progress_updated.emit(40, "Configuring error reporting...") + except Exception as e: + logger.error(f"Error reporting setup failed: {e}") + return False + + # Database context - 60% + try: + from openadapt.alembic.context_loader import load_alembic_context + load_alembic_context() - tray._run() + self.progress_updated.emit(60, "Loading database context...") + except Exception as e: + logger.error(f"Database context loading failed: {e}") + return False + + # System tray setup - 80% + try: + self.progress_updated.emit(80, "Setting up system tray...") + tray_instance = tray.SystemTrayIcon(app=self.app) + + except Exception as e: + logger.error(f"System tray setup failed: {e}") + return False + + # Final setup - 90% + self.progress_updated.emit(90, "Finalizing setup...") + + return tray_instance + + +def run_openadapt() -> None: + """Run OpenAdapt with improved progress visibility.""" + with redirect_stdout_stderr(): + try: + app = QApplication(sys.argv) + + # Create and show splash screen + splash = LoadingScreen() + splash.show() + + # Initialize loading manager + loading_manager = LoadingManager(splash, app) + + # Start loading sequence + tray_instance = loading_manager.start_loading_sequence() + if not tray_instance: + raise Exception("Loading sequence failed") + + def on_dashboard_ready(): + logger.info("Dashboard ready - closing splash screen") + loading_manager.progress_updated.emit(100, "Ready!") + + # Use QTimer for smooth transition + QTimer.singleShot( + 500, + lambda: ( + logger.info("Hiding splash screen"), + splash.hide(), + splash.deleteLater(), + ( + tray_instance.dashboard_monitor.stop() + if hasattr(tray_instance, "dashboard_monitor") + else None + ), + ), + ) + + # Connect dashboard monitor signals + if hasattr(tray_instance, "dashboard_monitor"): + + def on_dashboard_ready_wrapper(): + logger.debug("Dashboard ready wrapper called") + on_dashboard_ready() + + try: + tray_instance.dashboard_monitor.ready.connect( + on_dashboard_ready_wrapper, Qt.ConnectionType.AutoConnection + ) + logger.debug("Signal handler connected") + + # If dashboard monitor thread is not running, assume it's already ready + if ( + not hasattr(tray_instance.dashboard_monitor, "monitor_thread") + or not tray_instance.dashboard_monitor.monitor_thread.isRunning() + ): + logger.debug( + "Dashboard appears to be already ready, calling handler" + " directly" + ) + on_dashboard_ready_wrapper() + + except Exception as e: + logger.error(f"Failed to connect signal: {str(e)}") + + app.exec() + except Exception as exc: logger.exception(exc) + if "splash" in locals(): + splash.hide() + sys.exit(1) if __name__ == "__main__": + multiprocessing.freeze_support() run_openadapt() diff --git a/openadapt/splash_screen.py b/openadapt/splash_screen.py new file mode 100644 index 000000000..1060e8762 --- /dev/null +++ b/openadapt/splash_screen.py @@ -0,0 +1,84 @@ +"""Modern minimal splash screen for OpenAdapt with improved responsiveness.""" + +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QColor, QPixmap +from PySide6.QtWidgets import QApplication, QLabel, QProgressBar, QSplashScreen + + +class LoadingScreen(QSplashScreen): + """A minimal splash screen for.""" + + def __init__(self): + pixmap = QPixmap(400, 100) + pixmap.fill(QColor(0, 0, 0, 180)) + + super().__init__(pixmap) + + self.title_label = QLabel("OpenAdapt", self) + self.title_label.setGeometry(0, 15, 400, 30) + self.title_label.setAlignment(Qt.AlignCenter) + self.title_label.setStyleSheet(""" + QLabel { + color: #FFFFFF; + font-family: Arial; + font-size: 20px; + font-weight: bold; + } + """) + + self.progress_bar = QProgressBar(self) + self.progress_bar.setGeometry(50, 55, 300, 6) + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + self.progress_bar.setTextVisible(False) + + self.status_label = QLabel(self) + self.status_label.setGeometry(0, 70, 400, 20) + self.status_label.setAlignment(Qt.AlignCenter) + + self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint) + + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: none; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + } + QProgressBar::chunk { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #2196F3, stop:1 #64B5F6); + border-radius: 3px; + } + """) + + self.status_label.setStyleSheet(""" + QLabel { + color: #CCCCCC; + font-size: 11px; + font-family: Arial; + } + """) + + def update_status(self, message: str): + """Update the status message displayed on the splash screen.""" + self.status_label.setText(message) + self.repaint() + QApplication.processEvents() + + def update_progress(self, value: int): + """Update progress value with immediate visual feedback.""" + value = max(0, min(100, value)) + # for smooth progress updates + QTimer.singleShot(0, lambda: self._do_update(value)) + + def _do_update(self, value): + """Internal method to perform the actual progress update.""" + self.progress_bar.setValue(value) + self.repaint() + QApplication.processEvents() + + def show(self): + """Show the splash screen.""" + super().show() + self.raise_() + QApplication.processEvents() From f7f15c33b5b3e0e0773c93f36aba8cd0496572c3 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Sun, 5 Jan 2025 03:48:46 +0530 Subject: [PATCH 2/5] black fix --- openadapt/app/tray.py | 28 ++++++++++++++++++++-------- openadapt/entrypoint.py | 38 ++++++++++++++++++++++++++------------ openadapt/splash_screen.py | 11 ++++++----- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/openadapt/app/tray.py b/openadapt/app/tray.py index dd52c91ac..d5b3e9277 100644 --- a/openadapt/app/tray.py +++ b/openadapt/app/tray.py @@ -104,7 +104,19 @@ class DashboardMonitor(QObject): ready = Signal() - def __init__(self, app: QApplication = None, port=5173): + def __init__(self, app: QApplication = None, port: int = 5173) -> None: + """Initializes the DashboardMonitor. + + Args: + app (QApplication, optional): The QApplication instance. Defaults to None. + port (int, optional): The port number for the dashboard. Defaults to 5173. + + Attributes: + stop_flag (Event): An event flag to signal stopping the monitor. + port (int): The port number for the dashboard. + _is_ready (bool): A flag indicating if the monitor is ready. + monitor_thread (QThread or None): The thread for monitoring. + """ super().__init__() self.stop_flag = Event() self.port = port @@ -123,16 +135,16 @@ def __init__(self, app: QApplication = None, port=5173): # Connect to our own ready signal to update state self.ready.connect(self._update_ready_state) - def _update_ready_state(self): + def _update_ready_state(self) -> None: """Update internal ready state when signal is emitted.""" self._is_ready = True print("DEBUG(DashboardMonitor): Ready state updated") - def on_thread_finished(self): + def on_thread_finished(self) -> None: """Handle thread finished signal.""" logger.info("Dashboard monitor thread finished") - def monitor_startup(self): + def monitor_startup(self) -> None: """Monitor dashboard startup process.""" logger.info("Starting dashboard monitoring") start_time = time.time() @@ -162,7 +174,7 @@ def monitor_startup(self): finally: self.monitor_thread.quit() - def on_dashboard_ready(self): + def on_dashboard_ready(self) -> None: """Handle dashboard being ready.""" try: self.ready.emit() @@ -170,12 +182,12 @@ def on_dashboard_ready(self): except Exception as e: logger.error(f"Error emitting signal: {e}") - def check_ready_state(self): + def check_ready_state(self) -> None: """Check if dashboard is ready and re-emit if necessary.""" if self._is_ready: QTimer.singleShot(0, self.on_dashboard_ready) - def stop(self): + def stop(self) -> None: """Stop monitoring and cleanup thread.""" logger.info("Stopping dashboard monitor") self.stop_flag.set() @@ -615,7 +627,7 @@ def launch_dashboard(self) -> None: except Exception as e: logger.error(f"Launch dashboard error: {e}") - def on_dashboard_ready(self): + def on_dashboard_ready(self) -> None: """Handle dashboard being ready.""" logger.info("Dashboard is ready - performing final setup") diff --git a/openadapt/entrypoint.py b/openadapt/entrypoint.py index 6711a29aa..7ac480ff2 100644 --- a/openadapt/entrypoint.py +++ b/openadapt/entrypoint.py @@ -6,10 +6,8 @@ from PySide6.QtCore import QObject, Qt, QTimer, Signal from PySide6.QtWidgets import QApplication -from openadapt.app import tray from openadapt.build_utils import redirect_stdout_stderr from openadapt.custom_logger import logger -from openadapt.error_reporting import configure_error_reporting from openadapt.splash_screen import LoadingScreen @@ -19,20 +17,26 @@ class LoadingManager(QObject): progress_updated = Signal(int, str) loading_complete = Signal() - def __init__(self, splash_screen, app): + def __init__(self, splash_screen: LoadingScreen, app: QApplication) -> None: + """Initializes the main application entry point. + + Args: + splash_screen: The splash screen to be displayed during startup. + app: The main application instance. + """ super().__init__() self.splash = splash_screen self.app = app self.progress_updated.connect(self._update_progress) - def _update_progress(self, value, message): + def _update_progress(self, value: int, message: str) -> None: """Update progress bar and process events.""" self.splash.update_progress(value) self.splash.update_status(message) self.app.processEvents() logger.debug(f"Progress: {value}% - {message}") - def start_loading_sequence(self): + def start_loading_sequence(self) -> None: """Execute the loading sequence with visible progress updates.""" # Initial setup - 0% self.progress_updated.emit(0, "Initializing...") @@ -41,16 +45,18 @@ def start_loading_sequence(self): try: from openadapt.config import print_config - print_config() self.progress_updated.emit(20, "Loading configuration...") + print_config() except Exception as e: logger.error(f"Configuration error: {e}") return False # Error reporting setup - 40% try: - configure_error_reporting() + from openadapt.error_reporting import configure_error_reporting + self.progress_updated.emit(40, "Configuring error reporting...") + configure_error_reporting() except Exception as e: logger.error(f"Error reporting setup failed: {e}") return False @@ -67,6 +73,8 @@ def start_loading_sequence(self): # System tray setup - 80% try: + from openadapt.app import tray + self.progress_updated.emit(80, "Setting up system tray...") tray_instance = tray.SystemTrayIcon(app=self.app) @@ -98,7 +106,7 @@ def run_openadapt() -> None: if not tray_instance: raise Exception("Loading sequence failed") - def on_dashboard_ready(): + def on_dashboard_ready() -> None: logger.info("Dashboard ready - closing splash screen") loading_manager.progress_updated.emit(100, "Ready!") @@ -120,7 +128,14 @@ def on_dashboard_ready(): # Connect dashboard monitor signals if hasattr(tray_instance, "dashboard_monitor"): - def on_dashboard_ready_wrapper(): + def on_dashboard_ready_wrapper() -> None: + """Wrapper function that logs a debug message. + + calls the on_dashboard_ready function. + + Returns: + None + """ logger.debug("Dashboard ready wrapper called") on_dashboard_ready() @@ -130,14 +145,13 @@ def on_dashboard_ready_wrapper(): ) logger.debug("Signal handler connected") - # If dashboard monitor thread is not running, assume it's already ready if ( not hasattr(tray_instance.dashboard_monitor, "monitor_thread") or not tray_instance.dashboard_monitor.monitor_thread.isRunning() ): logger.debug( - "Dashboard appears to be already ready, calling handler" - " directly" + "Dashboard appears to be already ready, " + "calling handler directly" ) on_dashboard_ready_wrapper() diff --git a/openadapt/splash_screen.py b/openadapt/splash_screen.py index 1060e8762..964d0fc03 100644 --- a/openadapt/splash_screen.py +++ b/openadapt/splash_screen.py @@ -8,7 +8,8 @@ class LoadingScreen(QSplashScreen): """A minimal splash screen for.""" - def __init__(self): + def __init__(self) -> None: + """Initialize the loading screen.""" pixmap = QPixmap(400, 100) pixmap.fill(QColor(0, 0, 0, 180)) @@ -59,25 +60,25 @@ def __init__(self): } """) - def update_status(self, message: str): + def update_status(self, message: str) -> None: """Update the status message displayed on the splash screen.""" self.status_label.setText(message) self.repaint() QApplication.processEvents() - def update_progress(self, value: int): + def update_progress(self, value: int) -> None: """Update progress value with immediate visual feedback.""" value = max(0, min(100, value)) # for smooth progress updates QTimer.singleShot(0, lambda: self._do_update(value)) - def _do_update(self, value): + def _do_update(self, value: int) -> None: """Internal method to perform the actual progress update.""" self.progress_bar.setValue(value) self.repaint() QApplication.processEvents() - def show(self): + def show(self) -> None: """Show the splash screen.""" super().show() self.raise_() From e78224370093355d9be00bfc132874c39d0d0086 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Sun, 5 Jan 2025 04:06:32 +0530 Subject: [PATCH 3/5] black and flake8 fix --- openadapt/entrypoint.py | 2 +- openadapt/splash_screen.py | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/openadapt/entrypoint.py b/openadapt/entrypoint.py index 7ac480ff2..1f4a74a8b 100644 --- a/openadapt/entrypoint.py +++ b/openadapt/entrypoint.py @@ -147,7 +147,7 @@ def on_dashboard_ready_wrapper() -> None: if ( not hasattr(tray_instance.dashboard_monitor, "monitor_thread") - or not tray_instance.dashboard_monitor.monitor_thread.isRunning() + or not tray_instance.dashboard_monitor.monitor_thread.isRunning() # noqa: E501 ): logger.debug( "Dashboard appears to be already ready, " diff --git a/openadapt/splash_screen.py b/openadapt/splash_screen.py index 964d0fc03..add3446b2 100644 --- a/openadapt/splash_screen.py +++ b/openadapt/splash_screen.py @@ -18,14 +18,16 @@ def __init__(self) -> None: self.title_label = QLabel("OpenAdapt", self) self.title_label.setGeometry(0, 15, 400, 30) self.title_label.setAlignment(Qt.AlignCenter) - self.title_label.setStyleSheet(""" + self.title_label.setStyleSheet( + """ QLabel { color: #FFFFFF; font-family: Arial; font-size: 20px; font-weight: bold; } - """) + """ + ) self.progress_bar = QProgressBar(self) self.progress_bar.setGeometry(50, 55, 300, 6) @@ -39,7 +41,8 @@ def __init__(self) -> None: self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint) - self.progress_bar.setStyleSheet(""" + self.progress_bar.setStyleSheet( + """ QProgressBar { border: none; background: rgba(255, 255, 255, 0.1); @@ -50,15 +53,18 @@ def __init__(self) -> None: stop:0 #2196F3, stop:1 #64B5F6); border-radius: 3px; } - """) + """ + ) - self.status_label.setStyleSheet(""" + self.status_label.setStyleSheet( + """ QLabel { color: #CCCCCC; font-size: 11px; font-family: Arial; } - """) + """ + ) def update_status(self, message: str) -> None: """Update the status message displayed on the splash screen.""" From da8b81b4942661e3769100b8b102864d81eb4b93 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Tue, 7 Jan 2025 20:05:08 +0530 Subject: [PATCH 4/5] fix: Fix for Windows ProactorEventLoop --- openadapt/app/tray.py | 17 +++++++++++++++-- openadapt/entrypoint.py | 12 ++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/openadapt/app/tray.py b/openadapt/app/tray.py index d5b3e9277..9e00e319d 100644 --- a/openadapt/app/tray.py +++ b/openadapt/app/tray.py @@ -3,6 +3,21 @@ usage: `python -m openadapt.app.tray` or `poetry run app` """ +import sys +import asyncio +import os + +# Fix for Windows ProactorEventLoop +if sys.platform == "win32": + try: + if isinstance( + asyncio.get_event_loop_policy(), asyncio.WindowsProactorEventLoopPolicy + ): + # Use WindowsSelectorEventLoopPolicy instead + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + except Exception as e: + print(f"Failed to set event loop policy: {e}") + from datetime import datetime from functools import partial from pprint import pformat @@ -10,8 +25,6 @@ from typing import Any, Callable import inspect import multiprocessing -import os -import sys import time from pyqttoast import Toast, ToastButtonAlignment, ToastIcon, ToastPosition, ToastPreset diff --git a/openadapt/entrypoint.py b/openadapt/entrypoint.py index 1f4a74a8b..f7b26d7a4 100644 --- a/openadapt/entrypoint.py +++ b/openadapt/entrypoint.py @@ -43,9 +43,10 @@ def start_loading_sequence(self) -> None: # Configuration - 20% try: + self.progress_updated.emit(20, "Loading configuration...") + from openadapt.config import print_config - self.progress_updated.emit(20, "Loading configuration...") print_config() except Exception as e: logger.error(f"Configuration error: {e}") @@ -53,9 +54,10 @@ def start_loading_sequence(self) -> None: # Error reporting setup - 40% try: + self.progress_updated.emit(40, "Configuring error reporting...") + from openadapt.error_reporting import configure_error_reporting - self.progress_updated.emit(40, "Configuring error reporting...") configure_error_reporting() except Exception as e: logger.error(f"Error reporting setup failed: {e}") @@ -63,19 +65,21 @@ def start_loading_sequence(self) -> None: # Database context - 60% try: + self.progress_updated.emit(60, "Loading database context...") + from openadapt.alembic.context_loader import load_alembic_context load_alembic_context() - self.progress_updated.emit(60, "Loading database context...") except Exception as e: logger.error(f"Database context loading failed: {e}") return False # System tray setup - 80% try: + self.progress_updated.emit(80, "Setting up system tray...") + from openadapt.app import tray - self.progress_updated.emit(80, "Setting up system tray...") tray_instance = tray.SystemTrayIcon(app=self.app) except Exception as e: From 0208df47604f5c48943fd2fb36b41f0c5532a722 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Sun, 12 Jan 2025 03:28:43 +0530 Subject: [PATCH 5/5] fix: used multiprocessing to fix UI freeze --- openadapt/app/tray.py | 154 +++--------------------------------- openadapt/entrypoint.py | 171 ++++++++++++++++------------------------ 2 files changed, 79 insertions(+), 246 deletions(-) diff --git a/openadapt/app/tray.py b/openadapt/app/tray.py index 9e00e319d..6240a2f97 100644 --- a/openadapt/app/tray.py +++ b/openadapt/app/tray.py @@ -21,14 +21,13 @@ from datetime import datetime from functools import partial from pprint import pformat -from threading import Event, Thread +from threading import Thread from typing import Any, Callable import inspect import multiprocessing -import time from pyqttoast import Toast, ToastButtonAlignment, ToastIcon, ToastPosition, ToastPreset -from PySide6.QtCore import QMargins, QObject, QSize, Qt, QThread, QTimer, Signal +from PySide6.QtCore import QMargins, QObject, QSize, Qt, QThread, Signal from PySide6.QtGui import QAction, QColor, QFont, QIcon, QPixmap from PySide6.QtWidgets import ( QApplication, @@ -46,7 +45,6 @@ QVBoxLayout, QWidget, ) -import requests from openadapt.app import FPATH, quick_record, stop_record from openadapt.app.dashboard.run import cleanup as cleanup_dashboard @@ -112,109 +110,6 @@ def run(self) -> None: self.data.emit(data) -class DashboardMonitor(QObject): - """Monitor dashboard initialization.""" - - ready = Signal() - - def __init__(self, app: QApplication = None, port: int = 5173) -> None: - """Initializes the DashboardMonitor. - - Args: - app (QApplication, optional): The QApplication instance. Defaults to None. - port (int, optional): The port number for the dashboard. Defaults to 5173. - - Attributes: - stop_flag (Event): An event flag to signal stopping the monitor. - port (int): The port number for the dashboard. - _is_ready (bool): A flag indicating if the monitor is ready. - monitor_thread (QThread or None): The thread for monitoring. - """ - super().__init__() - self.stop_flag = Event() - self.port = port - self._is_ready = False - - if app is not None: - self.monitor_thread = QThread() - self.moveToThread(self.monitor_thread) - self.monitor_thread.started.connect(self.monitor_startup) - self.monitor_thread.finished.connect(self.on_thread_finished) - else: - self.monitor_thread = None - - print("DEBUG(DashboardMonitor): Signal ready created") - - # Connect to our own ready signal to update state - self.ready.connect(self._update_ready_state) - - def _update_ready_state(self) -> None: - """Update internal ready state when signal is emitted.""" - self._is_ready = True - print("DEBUG(DashboardMonitor): Ready state updated") - - def on_thread_finished(self) -> None: - """Handle thread finished signal.""" - logger.info("Dashboard monitor thread finished") - - def monitor_startup(self) -> None: - """Monitor dashboard startup process.""" - logger.info("Starting dashboard monitoring") - start_time = time.time() - try: - while not self.stop_flag.is_set(): - try: - elapsed_time = time.time() - start_time - print( - "DEBUG(DashboardMonitor): Checking dashboard. Elapsed:" - f" {elapsed_time:.2f}s" - ) - - response = requests.get(f"http://localhost:{self.port}", timeout=1) - if response.status_code == 200: - logger.info("Dashboard is ready!") - - # Emit signal in main thread - QTimer.singleShot(0, self.on_dashboard_ready) - break - except requests.RequestException as e: - logger.debug(f"Connection attempt failed: {e}") - time.sleep(0.5) - - if time.time() - start_time > 30: - logger.warning("Monitoring timeout") - break - finally: - self.monitor_thread.quit() - - def on_dashboard_ready(self) -> None: - """Handle dashboard being ready.""" - try: - self.ready.emit() - logger.info("Emitting ready signal") - except Exception as e: - logger.error(f"Error emitting signal: {e}") - - def check_ready_state(self) -> None: - """Check if dashboard is ready and re-emit if necessary.""" - if self._is_ready: - QTimer.singleShot(0, self.on_dashboard_ready) - - def stop(self) -> None: - """Stop monitoring and cleanup thread.""" - logger.info("Stopping dashboard monitor") - self.stop_flag.set() - - if self.monitor_thread and self.monitor_thread.isRunning(): - try: - self.monitor_thread.quit() - if not self.monitor_thread.wait(1000): - logger.warning("Dashboard monitor thread did not stop cleanly") - self.monitor_thread.terminate() - except Exception as e: - logger.error(f"Error stopping dashboard monitor thread: {e}") - - class SystemTrayIcon: """System tray icon for OpenAdapt.""" @@ -225,13 +120,11 @@ class SystemTrayIcon: # storing actions is required to prevent garbage collection recording_actions = {"visualize": [], "replay": []} - def __init__(self, app: QApplication = None) -> None: + def __init__(self) -> None: """Initialize the system tray icon.""" - if app is None: + self.app = QApplication.instance() + if not self.app: self.app = QApplication([]) - else: - self.app = app - if sys.platform == "darwin": # hide Dock icon while allowing focus on dialogs # (must come after QApplication()) @@ -613,36 +506,13 @@ def populate_menu(self, menu: QMenu, action: Callable, action_type: str) -> None def launch_dashboard(self) -> None: """Launch the web dashboard.""" - try: - if self.dashboard_thread: - if is_running_from_executable(): - return - cleanup_dashboard() - self.dashboard_thread.join() - - # Start dashboard - self.dashboard_thread = run_dashboard() - self.dashboard_thread.start() - logger.info("Dashboard thread started") - - # Initialize dashboard monitor - self.dashboard_monitor = DashboardMonitor(app=self.app) - - # Connect ready signal BEFORE starting monitoring - self.dashboard_monitor.ready.connect( - self.on_dashboard_ready, Qt.ConnectionType.QueuedConnection - ) - - # Start monitoring - self.dashboard_monitor.monitor_startup() - logger.info("Dashboard monitor started") - - except Exception as e: - logger.error(f"Launch dashboard error: {e}") - - def on_dashboard_ready(self) -> None: - """Handle dashboard being ready.""" - logger.info("Dashboard is ready - performing final setup") + if self.dashboard_thread: + if is_running_from_executable(): + return + cleanup_dashboard() + self.dashboard_thread.join() + self.dashboard_thread = run_dashboard() + self.dashboard_thread.start() def run(self) -> None: """Run the system tray icon.""" diff --git a/openadapt/entrypoint.py b/openadapt/entrypoint.py index f7b26d7a4..de8f64f6f 100644 --- a/openadapt/entrypoint.py +++ b/openadapt/entrypoint.py @@ -2,11 +2,13 @@ import multiprocessing import sys +import requests -from PySide6.QtCore import QObject, Qt, QTimer, Signal +from PySide6.QtCore import QObject, QTimer, Signal from PySide6.QtWidgets import QApplication from openadapt.build_utils import redirect_stdout_stderr +from openadapt.config import config from openadapt.custom_logger import logger from openadapt.splash_screen import LoadingScreen @@ -18,159 +20,120 @@ class LoadingManager(QObject): loading_complete = Signal() def __init__(self, splash_screen: LoadingScreen, app: QApplication) -> None: - """Initializes the main application entry point. - - Args: - splash_screen: The splash screen to be displayed during startup. - app: The main application instance. - """ + """Initialize the loading manager.""" super().__init__() self.splash = splash_screen self.app = app self.progress_updated.connect(self._update_progress) + self.loading_complete.connect(self._on_loading_complete) + self.dashboard_check_attempts = 0 + self.is_ready = False + self.dashboard_timer = None def _update_progress(self, value: int, message: str) -> None: """Update progress bar and process events.""" - self.splash.update_progress(value) - self.splash.update_status(message) - self.app.processEvents() - logger.debug(f"Progress: {value}% - {message}") - - def start_loading_sequence(self) -> None: + if not self.is_ready: + self.splash.update_progress(value) + self.splash.update_status(message) + self.app.processEvents() + logger.debug(f"Progress: {value}% - {message}") + + def _on_loading_complete(self) -> None: + """Handle completion of loading sequence.""" + self.is_ready = True + if self.dashboard_timer: + self.dashboard_timer.stop() + QTimer.singleShot(500, self.splash.hide) + QTimer.singleShot(600, self.app.quit) + + def check_dashboard(self) -> None: + """Check if dashboard is responsive and handle loading completion.""" + try: + url = f"http://localhost:{config.DASHBOARD_CLIENT_PORT}" + response = requests.get(url, timeout=1) + if response.status_code == 200: + self.progress_updated.emit(100, "Ready!") + self.loading_complete.emit() + return + except requests.RequestException: + pass + + def start_dashboard_monitoring(self) -> None: + """Start dashboard monitoring using Qt timer.""" + self.dashboard_timer = QTimer(self) + self.dashboard_timer.timeout.connect(self.check_dashboard) + self.dashboard_timer.start(100) # Check every 100ms + + def start_loading_sequence(self) -> bool: """Execute the loading sequence with visible progress updates.""" - # Initial setup - 0% - self.progress_updated.emit(0, "Initializing...") - - # Configuration - 20% try: - self.progress_updated.emit(20, "Loading configuration...") + # Initial setup - 0% + self.progress_updated.emit(0, "Initializing...") + # Configuration - 20% + self.progress_updated.emit(20, "Loading configuration...") from openadapt.config import print_config print_config() - except Exception as e: - logger.error(f"Configuration error: {e}") - return False - # Error reporting setup - 40% - try: + # Error reporting setup - 40% self.progress_updated.emit(40, "Configuring error reporting...") - from openadapt.error_reporting import configure_error_reporting configure_error_reporting() - except Exception as e: - logger.error(f"Error reporting setup failed: {e}") - return False - # Database context - 60% - try: + # Database context - 60% self.progress_updated.emit(60, "Loading database context...") - from openadapt.alembic.context_loader import load_alembic_context load_alembic_context() - except Exception as e: - logger.error(f"Database context loading failed: {e}") - return False - # System tray setup - 80% - try: - self.progress_updated.emit(80, "Setting up system tray...") + # System tray setup - 80% + self.progress_updated.emit(80, "Starting system tray...") - from openadapt.app import tray - - tray_instance = tray.SystemTrayIcon(app=self.app) + return True except Exception as e: - logger.error(f"System tray setup failed: {e}") + logger.error(f"Loading sequence failed: {e}") return False - # Final setup - 90% - self.progress_updated.emit(90, "Finalizing setup...") - return tray_instance +def run_tray() -> None: + """Run the openadapt tray.""" + from openadapt.app import tray + + tray._run() def run_openadapt() -> None: - """Run OpenAdapt with improved progress visibility.""" + """Run OpenAdapt.""" with redirect_stdout_stderr(): try: app = QApplication(sys.argv) - - # Create and show splash screen splash = LoadingScreen() splash.show() - # Initialize loading manager loading_manager = LoadingManager(splash, app) - # Start loading sequence - tray_instance = loading_manager.start_loading_sequence() - if not tray_instance: + if not loading_manager.start_loading_sequence(): raise Exception("Loading sequence failed") - def on_dashboard_ready() -> None: - logger.info("Dashboard ready - closing splash screen") - loading_manager.progress_updated.emit(100, "Ready!") - - # Use QTimer for smooth transition - QTimer.singleShot( - 500, - lambda: ( - logger.info("Hiding splash screen"), - splash.hide(), - splash.deleteLater(), - ( - tray_instance.dashboard_monitor.stop() - if hasattr(tray_instance, "dashboard_monitor") - else None - ), - ), - ) - - # Connect dashboard monitor signals - if hasattr(tray_instance, "dashboard_monitor"): - - def on_dashboard_ready_wrapper() -> None: - """Wrapper function that logs a debug message. - - calls the on_dashboard_ready function. - - Returns: - None - """ - logger.debug("Dashboard ready wrapper called") - on_dashboard_ready() - - try: - tray_instance.dashboard_monitor.ready.connect( - on_dashboard_ready_wrapper, Qt.ConnectionType.AutoConnection - ) - logger.debug("Signal handler connected") - - if ( - not hasattr(tray_instance.dashboard_monitor, "monitor_thread") - or not tray_instance.dashboard_monitor.monitor_thread.isRunning() # noqa: E501 - ): - logger.debug( - "Dashboard appears to be already ready, " - "calling handler directly" - ) - on_dashboard_ready_wrapper() - - except Exception as e: - logger.error(f"Failed to connect signal: {str(e)}") + tray_process = multiprocessing.Process(target=run_tray, daemon=False) + tray_process.start() + + if not tray_process.is_alive(): + raise Exception("Tray process failed to start") + + loading_manager.start_dashboard_monitoring() app.exec() except Exception as exc: logger.exception(exc) - if "splash" in locals(): - splash.hide() + if "tray_process" in locals() and tray_process and tray_process.is_alive(): + tray_process.terminate() sys.exit(1) if __name__ == "__main__": - multiprocessing.freeze_support() run_openadapt()