""" Main application entry point for the Cluster4NPU UI application. This module initializes the PyQt5 application, applies the theme, and launches the main dashboard window. It serves as the primary entry point for the modularized UI application. Main Components: - Application initialization and configuration - Theme application and font setup - Main window instantiation and display - Application event loop management Usage: python -m cluster4npu_ui.main # Or directly: from main import main main() """ import sys import os import tempfile from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt, QSharedMemory # Import fcntl only on Unix-like systems try: import fcntl HAS_FCNTL = True except ImportError: HAS_FCNTL = False # Add the parent directory to the path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from config.theme import apply_theme from ui.windows.login import DashboardLogin class SingleInstance: """Enhanced single instance handler with better error recovery.""" def __init__(self, app_name="Cluster4NPU"): self.app_name = app_name self.shared_memory = QSharedMemory(app_name) self.lock_file = None self.lock_fd = None self.process_check_enabled = True def is_running(self): """Check if another instance is already running with recovery mechanisms.""" # First, try to detect and clean up stale instances if self._detect_and_cleanup_stale_instances(): print("Cleaned up stale application instances") # Try shared memory approach if self._check_shared_memory(): return True # Try file locking approach if self._check_file_lock(): return True return False def _detect_and_cleanup_stale_instances(self): """Detect and clean up stale instances that might have crashed.""" cleaned_up = False try: import psutil # Check if there are any actual running processes app_processes = [] for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'create_time']): try: if 'python' in proc.info['name'].lower(): cmdline = proc.info['cmdline'] if cmdline and any('main.py' in arg for arg in cmdline): app_processes.append(proc) except (psutil.NoSuchProcess, psutil.AccessDenied): continue # If no actual app processes are running, clean up stale locks if not app_processes: cleaned_up = self._force_cleanup_locks() except ImportError: # psutil not available, try basic cleanup cleaned_up = self._force_cleanup_locks() except Exception as e: print(f"Warning: Could not detect stale instances: {e}") return cleaned_up def _force_cleanup_locks(self): """Force cleanup of stale locks.""" cleaned_up = False # Try to clean up shared memory try: if self.shared_memory.attach(): self.shared_memory.detach() cleaned_up = True except: pass # Try to clean up lock file try: lock_file = os.path.join(tempfile.gettempdir(), f"{self.app_name}.lock") if os.path.exists(lock_file): os.unlink(lock_file) cleaned_up = True except: pass return cleaned_up def _check_shared_memory(self): """Check shared memory for running instance.""" try: # Try to attach to existing shared memory if self.shared_memory.attach(): # Check if the shared memory is actually valid try: # Try to read from it to verify it's not corrupted data = self.shared_memory.data() if data is not None: return True # Valid instance found else: # Corrupted shared memory, clean it up self.shared_memory.detach() except: # Error reading, clean up self.shared_memory.detach() # Try to create new shared memory if not self.shared_memory.create(1): # Could not create, but attachment failed too - might be corruption return False except Exception as e: print(f"Warning: Shared memory check failed: {e}") return False return False def _check_file_lock(self): """Check file lock for running instance.""" try: self.lock_file = os.path.join(tempfile.gettempdir(), f"{self.app_name}.lock") if HAS_FCNTL: # Unix-like systems try: self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR) fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) return False # Successfully locked, no other instance except (OSError, IOError): return True # Could not lock, another instance exists else: # Windows try: self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR) return False # Successfully created, no other instance except (OSError, IOError): # File exists, but check if the process that created it is still running if self._is_lock_file_stale(): # Stale lock file, remove it and try again try: os.unlink(self.lock_file) self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR) return False except: pass return True except Exception as e: print(f"Warning: File lock check failed: {e}") return False def _is_lock_file_stale(self): """Check if the lock file is from a stale process.""" try: if not os.path.exists(self.lock_file): return True # Check file age - if older than 5 minutes, consider it stale import time file_age = time.time() - os.path.getmtime(self.lock_file) if file_age > 300: # 5 minutes return True # On Windows, we can't easily check if the process is still running # without additional information, so we rely on age check return False except: return True # If we can't check, assume it's stale def cleanup(self): """Enhanced cleanup with better error handling.""" try: if self.shared_memory.isAttached(): self.shared_memory.detach() except Exception as e: print(f"Warning: Could not detach shared memory: {e}") try: if self.lock_fd is not None: if HAS_FCNTL: fcntl.lockf(self.lock_fd, fcntl.LOCK_UN) os.close(self.lock_fd) self.lock_fd = None except Exception as e: print(f"Warning: Could not close lock file descriptor: {e}") try: if self.lock_file and os.path.exists(self.lock_file): os.unlink(self.lock_file) except Exception as e: print(f"Warning: Could not remove lock file: {e}") def force_cleanup(self): """Force cleanup of all locks (use when app crashed).""" print("Force cleaning up application locks...") self._force_cleanup_locks() print("Force cleanup completed") def setup_application(): """Initialize and configure the QApplication.""" # Enable high DPI support BEFORE creating QApplication QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) # Create QApplication if it doesn't exist if not QApplication.instance(): app = QApplication(sys.argv) else: app = QApplication.instance() # Set application properties app.setApplicationName("Cluster4NPU") app.setApplicationVersion("1.0.0") app.setOrganizationName("Cluster4NPU Team") # Set application font app.setFont(QFont("Arial", 9)) # Apply the harmonious theme apply_theme(app) return app def main(): """Main application entry point.""" # Check for command line arguments if '--force-cleanup' in sys.argv or '--cleanup' in sys.argv: print("Force cleanup mode enabled") single_instance = SingleInstance() single_instance.force_cleanup() print("Cleanup completed. You can now start the application normally.") sys.exit(0) # Check for help argument if '--help' in sys.argv or '-h' in sys.argv: print("Cluster4NPU Application") print("Usage: python main.py [options]") print("Options:") print(" --force-cleanup, --cleanup Force cleanup of stale application locks") print(" --help, -h Show this help message") sys.exit(0) # Create a minimal QApplication first for the message box temp_app = QApplication(sys.argv) if not QApplication.instance() else QApplication.instance() # Check for single instance single_instance = SingleInstance() if single_instance.is_running(): reply = QMessageBox.question( None, "Application Already Running", "Cluster4NPU is already running. \n\n" "Would you like to:\n" "• Click 'Yes' to force cleanup and restart\n" "• Click 'No' to cancel startup", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: print("User requested force cleanup...") single_instance.force_cleanup() print("Cleanup completed, proceeding with startup...") # Create a new instance checker after cleanup single_instance = SingleInstance() if single_instance.is_running(): QMessageBox.critical( None, "Cleanup Failed", "Could not clean up the existing instance. Please restart your computer." ) sys.exit(1) else: sys.exit(0) try: # Setup the full application app = setup_application() # Create and show the main dashboard login window dashboard = DashboardLogin() dashboard.show() # Clean up single instance on app exit app.aboutToQuit.connect(single_instance.cleanup) # Start the application event loop sys.exit(app.exec_()) except Exception as e: print(f"Error starting application: {e}") import traceback traceback.print_exc() single_instance.cleanup() sys.exit(1) if __name__ == '__main__': main()