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:
parent
0cf0bc6350
commit
2ceedb0f45
89
main.py
89
main.py
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user