Add single instance protection to prevent multiple app windows

- 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 <noreply@anthropic.com>
This commit is contained in:
Mason 2025-08-07 12:32:21 +08:00
parent 0cf0bc6350
commit 2ceedb0f45

89
main.py
View File

@ -21,9 +21,17 @@ Usage:
import sys import sys
import os import os
from PyQt5.QtWidgets import QApplication import tempfile
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtGui import QFont 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 # Add the parent directory to the path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 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 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(): def setup_application():
"""Initialize and configure the QApplication.""" """Initialize and configure the QApplication."""
# Enable high DPI support BEFORE creating QApplication # Enable high DPI support BEFORE creating QApplication
@ -60,14 +125,31 @@ def setup_application():
def main(): def main():
"""Main application entry point.""" """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: try:
# Setup the application # Setup the full application
app = setup_application() app = setup_application()
# Create and show the main dashboard login window # Create and show the main dashboard login window
dashboard = DashboardLogin() dashboard = DashboardLogin()
dashboard.show() dashboard.show()
# Clean up single instance on app exit
app.aboutToQuit.connect(single_instance.cleanup)
# Start the application event loop # Start the application event loop
sys.exit(app.exec_()) sys.exit(app.exec_())
@ -75,6 +157,7 @@ def main():
print(f"Error starting application: {e}") print(f"Error starting application: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
single_instance.cleanup()
sys.exit(1) sys.exit(1)