336 lines
12 KiB
Python
336 lines
12 KiB
Python
"""
|
|
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() |