Skip to content

Commit

Permalink
IMPROVEMENT: remove threading and queue
Browse files Browse the repository at this point in the history
  • Loading branch information
amilcarlucas committed Jan 24, 2025
1 parent 0b5d45a commit d18f695
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 189 deletions.
50 changes: 14 additions & 36 deletions ardupilot_methodic_configurator/frontend_tkinter_software_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
SPDX-License-Identifier: GPL-3.0-or-later
"""

import queue
import threading
import tkinter as tk
from tkinter import ttk
from typing import Callable, Optional
Expand All @@ -19,11 +17,10 @@ class UpdateDialog: # pylint: disable=too-many-instance-attributes
"""Dialog for displaying software update information and handling user interaction."""

def __init__(
self, version_info: str, p_queue: queue.Queue, download_callback: Optional[Callable[[], None]] = None
self, version_info: str, download_callback: Optional[Callable[[], bool]] = None
) -> None:
self.root = tk.Tk()
self.root.title("Software Update")
self.queue = p_queue
self.download_callback = download_callback
self.root.protocol("WM_DELETE_WINDOW", self.on_cancel)

Expand All @@ -50,32 +47,11 @@ def _setup_buttons(self) -> None:
self.no_btn.grid(row=3, column=1, padx=5)

def update_progress(self, value: float, status: str = "") -> None:
"""Queue progress updates to be handled by main thread."""
self.queue.put(("progress", value, status))

def _process_queue(self) -> None:
"""Process any pending progress updates."""
try:
while True:
msg_type, *args = self.queue.get_nowait()
if msg_type == "progress":
value, status = args
self.progress["value"] = value
if status:
self.status_label["text"] = status
elif msg_type == "complete":
status = args[0] if args else "Update complete!"
self.status_label["text"] = f"{status} Please restart the application."
self.root.after(2000, self.root.destroy) # Close after 2 seconds
return
elif msg_type == "error":
error_msg = args[0]
self.status_label["text"] = f"Error: {error_msg}"
self.yes_btn.config(state="normal")
self.no_btn.config(state="normal")
return
except queue.Empty:
self.root.after(100, self._process_queue)
"""Update progress directly."""
self.progress["value"] = value
if status:
self.status_label["text"] = status
self.root.update()

def on_yes(self) -> None:
self.result = True
Expand All @@ -84,12 +60,15 @@ def on_yes(self) -> None:
self.yes_btn.config(state="disabled")
self.no_btn.config(state="disabled")

# Start download in background
if self.download_callback:
threading.Thread(target=self.download_callback).start()

# Start processing queue
self._process_queue()
success = self.download_callback()
if success:
self.status_label["text"] = "Update complete! Please restart the application."
self.root.after(2000, self.root.destroy)
else:
self.status_label["text"] = "Update failed!"
self.yes_btn.config(state="normal")
self.no_btn.config(state="normal")

def on_no(self) -> None:
self.result = False
Expand All @@ -107,6 +86,5 @@ def show(self) -> bool:
bool: True if user chose to update, False otherwise
"""
self._process_queue()
self.root.mainloop()
return bool(self.result)
167 changes: 14 additions & 153 deletions ardupilot_methodic_configurator/middleware_software_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
"""

import platform
import queue
import threading
from logging import basicConfig as logging_basicConfig
from logging import debug as logging_error
from logging import getLevelName as logging_getLevelName
Expand Down Expand Up @@ -42,173 +40,36 @@ def format_version_info(current_version_str: str, latest_release: dict[str, Any]
)


class DownloadManager:
"""Manages the download process for software updates."""

def __init__(self, p_queue: queue.Queue) -> None:
self.queue = p_queue
self.is_cancelled = False
self.is_complete = False

def download_with_progress(self, url: str, filename: str) -> bool:
try:
success = download_and_install_on_windows(
download_url=url, file_name=filename, progress_callback=self._progress_callback
)
if success:
self.queue.put(("complete", "Download complete"))
self.is_complete = True
return success
except requests_RequestException as e:
self.queue.put(("error", f"Download failed: {e}"))
return False
except OSError as e:
self.queue.put(("error", f"Installation failed: {e}"))
return False

def _progress_callback(self, progress: float, status: str) -> None:
if not self.is_cancelled:
self.queue.put(("progress", progress, status))

def cancel(self) -> None:
self.is_cancelled = True


class UpdateManager: # pylint: disable=too-few-public-methods
"""Manages the software update process including user interaction and installation."""

def __init__(self) -> None:
self.queue: queue.Queue = queue.Queue()
self.dialog: Optional[UpdateDialog] = None

def _perform_download(self, latest_release: dict[str, Any]) -> bool:
if platform.system() == "Windows":
download_url = latest_release["assets"][0]["browser_download_url"]
file_name = latest_release["assets"][0]["name"]
return download_and_install_on_windows(
download_url=download_url,
file_name=file_name,
progress_callback=self.dialog.update_progress if self.dialog else None
)
return download_and_install_pip_release() == 0

def check_and_update(self, latest_release: dict[str, Any], current_version_str: str) -> bool:
"""Handle the complete update process."""
try:
latest_version = latest_release["tag_name"].lstrip("v")
if current_version_str == latest_version:
logging_info(_("Already running latest version."))
return True

version_info = format_version_info(current_version_str, latest_release)
self.dialog = UpdateDialog(
version_info, self.queue, download_callback=lambda: self._start_download(latest_release)
)

if not self.dialog.show():
logging_info(_("Update cancelled by user."))
return False

except KeyError as e:
logging_error(_("Invalid release data: %s"), e)
return False
except (OSError, ValueError) as e:
logging_error(_("Update process failed: %s"), e)
return False
return False

def _start_download(self, latest_release: dict[str, Any]) -> None:
"""Start the download process."""
try:
if platform.system() == "Windows":
self._handle_windows_update(latest_release)
else:
self._handle_pip_update()
except KeyError as e:
error_msg = f"Invalid release data: {e}"
logging_error(error_msg)
self.queue.put(("error", error_msg))
except requests_RequestException as e:
error_msg = f"Download failed: {e}"
logging_error(error_msg)
self.queue.put(("error", error_msg))
except OSError as e:
error_msg = f"File operation failed: {e}"
logging_error(error_msg)
self.queue.put(("error", error_msg))
except ValueError as e:
error_msg = f"Invalid data received: {e}"
logging_error(error_msg)
self.queue.put(("error", error_msg))

def _handle_windows_update(self, latest_release: dict[str, Any]) -> bool:
try:
download_url = latest_release["assets"][0]["browser_download_url"]
file_name = latest_release["assets"][0]["name"]

download_manager = DownloadManager(self.queue)
if download_manager is None:
return False

if self.dialog:
self.dialog.update_progress(0, "Starting download...")

def download_thread() -> None:
success = download_manager.download_with_progress(download_url, file_name)
if success:
self.queue.put(("complete",))
else:
self.queue.put(("error", "Download failed"))

thread = threading.Thread(target=download_thread)
thread.daemon = True
thread.start()

# Keep UI responsive while download proceeds
while thread.is_alive() and self.dialog is not None:
self.dialog.root.update()
if download_manager.is_complete:
break

#thread.join(timeout=1.0)

if download_manager.is_cancelled:
error_msg = "Installation canceled by user"
self.queue.put(("error", error_msg))
logging_error(error_msg)
if self.dialog:
self.dialog.root.destroy()
return False

if download_manager.is_complete:
if self.dialog:
self.dialog.root.destroy()
return True

return True

except (KeyError, IndexError) as e:
if self.dialog:
self.queue.put(("error", str(e)))
logging_error(_("Error accessing release assets: {}").format(e))
return False
except OSError as e:
self.queue.put(("error", f"File operation failed: {e}"))
return False

def _handle_pip_update(self) -> bool:
try:
if self.dialog:
self.dialog.update_progress(0, "Starting pip installation...")
self.queue.put(("progress", 0, "Installing via pip..."))

success = download_and_install_pip_release()

if success:
self.queue.put(("progress", 100, "Installation complete"))
self.queue.put(("complete",))
else:
self.queue.put(("error", "Pip installation failed"))

if self.dialog:
self.dialog.root.destroy()
self.dialog = UpdateDialog(version_info, download_callback=lambda: self._perform_download(latest_release))
return self.dialog.show()

return bool(success)
except Exception as e:
error_msg = f"Pip installation failed: {e}"
self.queue.put(("error", error_msg))
logging_error(error_msg)
if self.dialog:
self.dialog.root.destroy()
logging_error(_("Update process failed: %s"), e)
return False

def main() -> None:
Expand Down

0 comments on commit d18f695

Please sign in to comment.