diff --git a/loaddaemon.py b/loaddaemon.py new file mode 100644 index 0000000..6653eed --- /dev/null +++ b/loaddaemon.py @@ -0,0 +1,200 @@ +import sys +import threading +import time +import shlex +import gc +from io import StringIO +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton, QHBoxLayout +from PyQt5.QtCore import Qt, pyqtSignal, QObject, QTimer, QEvent +from PyQt5.QtGui import QFont + + +class LaunchSignals(QObject): + log_update = pyqtSignal(str) + launch_complete = pyqtSignal() + launch_aborted = pyqtSignal() + cleanup_done = pyqtSignal() + + +class AbortException(Exception): + pass + + +class StreamingCapture(StringIO): + def __init__(self, log_signal, launch_signal): + super().__init__() + self.log_signal = log_signal + self.launch_signal = launch_signal + self.launch_detected = False + self.abort_requested = False + + def write(self, text): + if self.abort_requested: + raise AbortException("Launch aborted by user") + + if text and text.strip(): + # Check if signal is still valid before emitting + try: + self.log_signal.emit(text.strip()) + except RuntimeError: + # Signal/Object might be deleted + pass + + if not self.launch_detected: + lower_text = text.lower() + if "launching" in lower_text and ("game" in lower_text or "version" in lower_text or "minecraft" in lower_text): + self.launch_detected = True + try: + self.launch_signal.emit() + except RuntimeError: + pass + + return super().write(text) + + +class LaunchWindow(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Launching Minecraft") + self.setModal(True) + self.resize(400, 150) + + layout = QVBoxLayout() + + self.status_label = QLabel("Initializing...") + self.status_label.setWordWrap(True) + self.status_label.setAlignment(Qt.AlignCenter) + layout.addWidget(self.status_label) + + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 0) # Indeterminate progress + self.progress_bar.setTextVisible(False) + layout.addWidget(self.progress_bar) + + # Add a manual close/cancel button + button_layout = QHBoxLayout() + button_layout.addStretch() + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(self.request_abort) + button_layout.addWidget(self.cancel_button) + layout.addLayout(button_layout) + + self.setLayout(layout) + + self.signals = LaunchSignals() + self.signals.log_update.connect(self.update_status) + self.signals.launch_complete.connect(self.on_launch_complete) + self.signals.launch_aborted.connect(self.on_launch_aborted) + self.signals.cleanup_done.connect(self.on_cleanup_done) + + self.launch_detected = False + self.closing_scheduled = False + self.aborting = False + self.capture_streams = [] + self.thread_running = False + + def update_status(self, text): + if len(text) > 100: + text = text[:97] + "..." + self.status_label.setText(text) + + def on_launch_complete(self): + if not self.closing_scheduled and not self.aborting: + self.closing_scheduled = True + self.status_label.setText("Game Launched! Closing window...") + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(100) + self.cancel_button.setEnabled(False) + QTimer.singleShot(3000, self.accept) + + def on_launch_aborted(self): + self.status_label.setText("Launch Aborted.") + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(0) + + def on_cleanup_done(self): + self.thread_running = False + # Now it is safe to close the window + super().reject() + + def request_abort(self): + if self.thread_running and not self.aborting: + self.aborting = True + self.status_label.setText("Aborting...") + self.cancel_button.setEnabled(False) + # Signal streams to stop + for stream in self.capture_streams: + stream.abort_requested = True + elif not self.thread_running: + super().reject() + + def reject(self): + # Handle Esc key or X button + self.request_abort() + + def closeEvent(self, event): + if self.thread_running: + event.ignore() + self.request_abort() + else: + event.accept() + + def launch_game(self, command): + self.thread_running = True + thread = threading.Thread(target=self._run_launch, args=(command,), daemon=True) + thread.start() + + def _run_launch(self, command): + try: + modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')] + for mod in modules_to_remove: + del sys.modules[mod] + gc.collect() + + from zucaro.cli.main import zucaro_cli + + old_stdout, old_stderr = sys.stdout, sys.stderr + + stdout_capture = StreamingCapture(self.signals.log_update, self.signals.launch_complete) + stderr_capture = StreamingCapture(self.signals.log_update, self.signals.launch_complete) + + self.capture_streams = [stdout_capture, stderr_capture] + + sys.stdout = stdout_capture + sys.stderr = stderr_capture + + try: + zucaro_cli.main(args=shlex.split(command), standalone_mode=False) + except AbortException: + self.signals.launch_aborted.emit() + except SystemExit: + pass + except Exception as e: + self.signals.log_update.emit(f"Error: {str(e)}") + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + + if not stdout_capture.launch_detected and not stderr_capture.launch_detected and not stdout_capture.abort_requested: + self.signals.launch_complete.emit() + + modules_to_remove = [mod for mod in sys.modules if mod.startswith('zucaro')] + for mod in modules_to_remove: + del sys.modules[mod] + gc.collect() + + self.signals.cleanup_done.emit() + + except Exception as e: + try: + self.signals.log_update.emit(f"Error launching game: {str(e)}") + self.signals.cleanup_done.emit() + except: + pass + + +def launch_instance_with_window(command, parent=None): + window = LaunchWindow(parent) + window.launch_game(command) + window.exec_() + return window diff --git a/picodulce.py b/picodulce.py index 09cca60..d11593b 100644 --- a/picodulce.py +++ b/picodulce.py @@ -14,6 +14,7 @@ import time from authser import MinecraftAuthenticator from healthcheck import HealthCheck import modulecli +import loaddaemon from PyQt5.QtWidgets import QApplication, QComboBox, QWidget, QInputDialog, QVBoxLayout, QListWidget, QSpinBox, QFileDialog, QPushButton, QMessageBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QCheckBox, QTabWidget, QFrame, QSpacerItem, QSizePolicy, QMainWindow, QGridLayout, QTextEdit, QListWidget, QListWidgetItem, QMenu from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QMovie, QPixmap, QDesktopServices, QBrush @@ -893,16 +894,14 @@ class zucaroVersionSelector(QWidget): QMessageBox.warning(self, "No Instance Selected", "Please select an instance.") return - play_thread = threading.Thread(target=self.run_game, args=(selected_instance,)) - play_thread.start() + self.launch_game_with_window(selected_instance) - def run_game(self, selected_instance): + def launch_game_with_window(self, selected_instance): try: self.current_state = selected_instance self.start_time = time.time() - # Read config with open('config.json', 'r') as config_file: config = json.load(config_file) instance_value = config.get("Instance", "default") @@ -910,11 +909,9 @@ class zucaroVersionSelector(QWidget): manage_java = config.get("ManageJava", False) java_path = config.get("JavaPath", "") - # Update last played on a thread - update_thread = threading.Thread(target=self.update_last_played, args=(selected_instance,)) + update_thread = threading.Thread(target=self.update_last_played, args=(selected_instance,), daemon=True) update_thread.start() - # Build command command = f"instance launch {instance_value} --version-override {selected_instance} --assigned-ram {max_ram}" if manage_java: command += " --manage-java" @@ -922,17 +919,14 @@ class zucaroVersionSelector(QWidget): command += f" --java {java_path}" print(f"Launching command: {command}") - - output = modulecli.run_command(command) - if not output: - raise Exception("Failed to get output from modulecli") - print(f"modulecli output: {output}") + + loaddaemon.launch_instance_with_window(command, self) except Exception as e: error_message = f"Error playing {selected_instance}: {e}" - print(error_message) # Add this for debugging + print(error_message) logging.error(error_message) - # (Show error in UI if necessary) + QMessageBox.critical(self, "Error", error_message) finally: self.current_state = "menu" self.update_total_playtime(self.start_time) diff --git a/version.json b/version.json index c307c4a..4b391de 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "0.13.7", + "version": "0.13.8", "links": [ "https://raw.githubusercontent.com/nixietab/picodulce/main/version.json", "https://raw.githubusercontent.com/nixietab/picodulce/main/picodulce.py", @@ -9,7 +9,8 @@ "https://raw.githubusercontent.com/nixietab/picodulce/main/holiday.ico", "https://raw.githubusercontent.com/nixietab/picodulce/main/authser.py", "https://raw.githubusercontent.com/nixietab/picodulce/main/healthcheck.py", - "https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py" + "https://raw.githubusercontent.com/nixietab/picodulce/main/modulecli.py", + "https://raw.githubusercontent.com/nixietab/picodulce/main/loaddaemon.py" ], "versionBleeding": "0.13.3-212" }