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 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)