From 4f5e30e6cf71f584081cd19bb7d88b679272d9de Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:41:39 +0200 Subject: [PATCH] Stream Deno upgrade output and add GUI thread Add streaming progress support to Deno upgrades and hook it into the GUI. upgrade_deno() now accepts an optional progress_callback and uses subprocess.Popen to read stdout line-by-line (line-buffered, utf-8, errors replaced), collecting output and invoking the callback as lines arrive. Added DenoUpdateThread (QThread) with finished/progress/error signals and replaced the previous background thread usage in the updater UI with this QThread. GUI slots strip ANSI escapes, truncate long messages, and update status text; buttons are re-enabled on finish/error. Improved logging and error handling for upgrade failures. --- ytsage/core/ytsage_deno.py | 42 +++++++++--- .../ytsage_dialogs_updater.py | 68 ++++++++++++++----- 2 files changed, 82 insertions(+), 28 deletions(-) diff --git a/ytsage/core/ytsage_deno.py b/ytsage/core/ytsage_deno.py index 2e5edc4..e18c5a9 100644 --- a/ytsage/core/ytsage_deno.py +++ b/ytsage/core/ytsage_deno.py @@ -683,10 +683,13 @@ def compare_deno_versions(current: str, latest: str) -> bool: return False -def upgrade_deno() -> tuple[bool, str]: +def upgrade_deno(progress_callback=None) -> tuple[bool, str]: """ Upgrade Deno to the latest version using 'deno upgrade' command. + Args: + progress_callback: Optional function to call with output lines for progress tracking + Returns: tuple: (success: bool, output: str) - Success status and command output """ @@ -700,23 +703,42 @@ def upgrade_deno() -> tuple[bool, str]: logger.info(f"Upgrading Deno using: {deno_path}") - # Run deno upgrade command - result = subprocess.run( + # Run deno upgrade command with output capturing + process = subprocess.Popen( [str(deno_path), "upgrade"], - capture_output=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, - timeout=300, # 5 minutes timeout - creationflags=SUBPROCESS_CREATIONFLAGS + creationflags=SUBPROCESS_CREATIONFLAGS, + bufsize=1, # Line buffered + encoding='utf-8', + errors='replace' ) - output = result.stdout + result.stderr + full_output = [] - if result.returncode == 0: + # Read output line by line as it is generated + while True: + line = process.stdout.readline() + if not line and process.poll() is not None: + break + + if line: + line_str = line.strip() + if line_str: + full_output.append(line_str) + logger.debug(f"Deno upgrade output: {line_str}") + if progress_callback: + progress_callback(line_str) + + return_code = process.poll() + output = "\n".join(full_output) + + if return_code == 0: logger.info("Deno upgrade successful") - logger.debug(f"Upgrade output: {output}") return True, output else: - logger.error(f"Deno upgrade failed with code {result.returncode}") + logger.error(f"Deno upgrade failed with code {return_code}") logger.error(f"Output: {output}") return False, output diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py index e73a2e8..39a29c7 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_updater.py @@ -183,6 +183,21 @@ class DenoCheckThread(QThread): self.error.emit(str(e)) +class DenoUpdateThread(QThread): + finished = Signal(bool, str) + progress = Signal(str) + error = Signal(str) + + def run(self): + try: + success, output = upgrade_deno(progress_callback=self.progress.emit) + self.finished.emit(success, output) + except Exception as e: + logger.exception(f"Error updating Deno: {e}") + self.error.emit(str(e)) + + + class UpdaterTabWidget(QWidget): """Widget for the Updater tab in Custom Options dialog.""" @@ -930,24 +945,41 @@ class UpdaterTabWidget(QWidget): "background-color: #2a2d36; border-radius: 4px; margin: 5px 0;" ) - # Run update in background thread - def update_thread(): - try: - success, output = upgrade_deno() - - # Update UI in main thread - self.deno_check_button.setEnabled(True) - self.deno_update_button.setEnabled(True) - self._handle_deno_update_result(success, output) - - except Exception as e: - logger.exception(f"Error updating Deno: {e}") - self.deno_check_button.setEnabled(True) - self.deno_update_button.setEnabled(True) - self._handle_deno_update_result(False, str(e)) - - thread = threading.Thread(target=update_thread, daemon=True) - thread.start() + # Use QThread for updates with progress reporting + self.deno_update_thread = DenoUpdateThread() + self.deno_update_thread.progress.connect(self._on_deno_update_progress) + self.deno_update_thread.finished.connect(self._on_deno_update_finished) + self.deno_update_thread.error.connect(self._on_deno_update_error) + self.deno_update_thread.start() + + @Slot(str) + def _on_deno_update_progress(self, message: str) -> None: + """Handle Deno update progress messages.""" + # Clean up message for display + # Strip ANSI escape codes (colors) + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + display_msg = ansi_escape.sub('', message).strip() + + if not display_msg: + return + + # If message is too long, truncate it + if len(display_msg) > 70: + display_msg = display_msg[:67] + "..." + + self.deno_status_label.setText(display_msg) + + @Slot(bool, str) + def _on_deno_update_finished(self, success: bool, output: str) -> None: + self.deno_check_button.setEnabled(True) + self.deno_update_button.setEnabled(True) + self._handle_deno_update_result(success, output) + + @Slot(str) + def _on_deno_update_error(self, error: str) -> None: + self.deno_check_button.setEnabled(True) + self.deno_update_button.setEnabled(True) + self._handle_deno_update_result(False, error) def _handle_deno_update_result(self, success: bool, output: str) -> None: """Handle Deno update completion."""