cluster4npu/main.py

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