From 2ceedb0f45ab543fedf3c9cd4291e5d560eb3fed Mon Sep 17 00:00:00 2001 From: Mason Date: Thu, 7 Aug 2025 12:32:21 +0800 Subject: [PATCH] Add single instance protection to prevent multiple app windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement SingleInstance class using QSharedMemory and file locking - Cross-platform support with fcntl on Unix/macOS and file creation on Windows - Show warning dialog when user tries to launch second instance - Automatic cleanup of resources on application exit - Graceful handling of instance detection failures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- main.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 52f55a9..fc631d7 100644 --- a/main.py +++ b/main.py @@ -21,9 +21,17 @@ Usage: import sys import os -from PyQt5.QtWidgets import QApplication +import tempfile +from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtGui import QFont -from PyQt5.QtCore import Qt +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__)))) @@ -32,6 +40,63 @@ from config.theme import apply_theme from ui.windows.login import DashboardLogin +class SingleInstance: + """Ensure only one instance of the application can run.""" + + 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 + + def is_running(self): + """Check if another instance is already running.""" + # Try to create shared memory + if self.shared_memory.attach(): + # Another instance is already running + return True + + # Try to create the shared memory + if not self.shared_memory.create(1): + # Failed to create, likely another instance exists + return True + + # Also use file locking as backup (works better on some systems) + if HAS_FCNTL: + try: + self.lock_file = os.path.join(tempfile.gettempdir(), f"{self.app_name}.lock") + 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) + except (OSError, IOError): + # Another instance is running + if self.lock_fd: + os.close(self.lock_fd) + return True + else: + # On Windows, try simple file creation + try: + self.lock_file = os.path.join(tempfile.gettempdir(), f"{self.app_name}.lock") + self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_RDWR) + except (OSError, IOError): + return True + + return False + + def cleanup(self): + """Clean up resources.""" + if self.shared_memory.isAttached(): + self.shared_memory.detach() + + if self.lock_fd: + try: + if HAS_FCNTL: + fcntl.lockf(self.lock_fd, fcntl.LOCK_UN) + os.close(self.lock_fd) + os.unlink(self.lock_file) + except: + pass + + def setup_application(): """Initialize and configure the QApplication.""" # Enable high DPI support BEFORE creating QApplication @@ -60,14 +125,31 @@ def setup_application(): def main(): """Main application entry point.""" + # 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(): + QMessageBox.warning( + None, + "Application Already Running", + "Cluster4NPU is already running. Please check your taskbar or system tray.", + ) + sys.exit(0) + try: - # Setup the application + # 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_()) @@ -75,6 +157,7 @@ def main(): print(f"Error starting application: {e}") import traceback traceback.print_exc() + single_instance.cleanup() sys.exit(1)