From da8ef8dab812cf1802da615c60ae2e893f36a4ea Mon Sep 17 00:00:00 2001 From: ChemicalXandco <32775248+ChemicalXandco@users.noreply.github.com> Date: Sun, 11 Apr 2021 16:03:55 +0100 Subject: [PATCH] Add stop download button (#33) Add stop download button Not a perfect solution, but it should work --- rare/components/tabs/downloads/__init__.py | 9 +- .../tabs/downloads/download_thread.py | 90 +++++++++++++++---- rare/utils/models.py | 3 - requirements.txt | 1 + 4 files changed, 80 insertions(+), 23 deletions(-) diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index 504fd81c..23f30a55 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -50,9 +50,9 @@ class DownloadTab(QWidget): self.mini_layout.addWidget(self.prog_bar) self.kill_button = QPushButton(self.tr("Stop Download")) - # self.mini_layout.addWidget(self.kill_button) self.kill_button.setDisabled(True) self.kill_button.clicked.connect(self.stop_download) + self.mini_layout.addWidget(self.kill_button) self.layout.addLayout(self.mini_layout) @@ -85,7 +85,7 @@ class DownloadTab(QWidget): self.dl_queue = dl_queue def stop_download(self): - self.thread.kill = True + self.thread.kill() def install_game(self, options: InstallOptions): @@ -227,11 +227,12 @@ class DownloadTab(QWidget): else: self.queue_widget.update_queue(self.dl_queue) - elif text == "error": - QMessageBox.warning(self, "warn", "Download error") + elif text[:5] == "error": + QMessageBox.warning(self, "warn", "Download error: "+text[6:]) elif text == "stop": self.reset_infos() + self.active_game = None def reset_infos(self): self.kill_button.setDisabled(True) diff --git a/rare/components/tabs/downloads/download_thread.py b/rare/components/tabs/downloads/download_thread.py index 026dbbd4..bd11bbc2 100644 --- a/rare/components/tabs/downloads/download_thread.py +++ b/rare/components/tabs/downloads/download_thread.py @@ -1,18 +1,19 @@ import os import queue import subprocess +import sys import time from logging import getLogger from multiprocessing import Queue as MPQueue +from queue import Empty +import psutil from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtWidgets import QMessageBox -from rare.utils.models import KillDownloadException - from custom_legendary.core import LegendaryCore from custom_legendary.downloader.manager import DLManager -from custom_legendary.models.downloading import UIUpdate +from custom_legendary.models.downloading import UIUpdate, WriterTask logger = getLogger("Download") @@ -20,7 +21,6 @@ logger = getLogger("Download") class DownloadThread(QThread): status = pyqtSignal(str) statistics = pyqtSignal(UIUpdate) - kill = False def __init__(self, dlm: DLManager, core: LegendaryCore, status_queue: MPQueue, igame, repair=False, repair_file=None): @@ -31,37 +31,92 @@ class DownloadThread(QThread): self.igame = igame self.repair = repair self.repair_file = repair_file + self._kill = False def run(self): start_time = time.time() + dl_stopped = False try: self.dlm.start() time.sleep(1) while self.dlm.is_alive(): - if self.kill: - # raise KillDownloadException() - # TODO kill download queue, workers - pass + if self._kill: + self.status.emit("stop") + logger.info("Download stopping...") + + # The code below is a temporary solution. + # It should be removed once legendary supports stopping downloads more gracefully. + + self.dlm.running = False + + # send conditions to unlock threads if they aren't already + for cond in self.dlm.conditions: + with cond: + cond.notify() + + # make sure threads are dead. + for t in self.dlm.threads: + t.join(timeout=5.0) + if t.is_alive(): + logger.warning(f'Thread did not terminate! {repr(t)}') + + # clean up all the queues, otherwise this process won't terminate properly + for name, q in zip(('Download jobs', 'Writer jobs', 'Download results', 'Writer results'), + (self.dlm.dl_worker_queue, self.dlm.writer_queue, self.dlm.dl_result_q, self.dlm.writer_result_q)): + logger.debug(f'Cleaning up queue "{name}"') + try: + while True: + _ = q.get_nowait() + except Empty: + q.close() + q.join_thread() + except AttributeError: + logger.warning(f'Queue {name} did not close') + + if self.dlm.writer_queue: + # cancel installation + self.dlm.writer_queue.put_nowait(WriterTask('', kill=True)) + + # forcibly kill DL workers that are not actually dead yet + for child in self.dlm.children: + if child.exitcode is None: + child.terminate() + + if self.dlm.shared_memory: + # close up shared memory + self.dlm.shared_memory.close() + self.dlm.shared_memory.unlink() + self.dlm.shared_memory = None + + self.dlm.kill() + + # force kill any threads that are somehow still alive + for proc in psutil.process_iter(): + # check whether the process name matches + if sys.platform in ['linux', 'darwin'] and proc.name() == 'DownloadThread': + proc.kill() + elif sys.platform == 'win32' and proc.name() == 'python.exe' and proc.create_time() >= start_time: + proc.kill() + + logger.info("Download stopped. It can be continued later.") + dl_stopped = True try: - self.statistics.emit(self.status_queue.get(timeout=1)) + if not dl_stopped: + self.statistics.emit(self.status_queue.get(timeout=1)) except queue.Empty: pass self.dlm.join() - except KillDownloadException: - self.status.emit("stop") - logger.info("Downlaod can be continued later") - self.dlm.kill() - return - except Exception as e: logger.error(f"Installation failed after {time.time() - start_time:.02f} seconds: {e}") - self.status.emit("error") + self.status.emit("error "+str(e)) return else: + if dl_stopped: + return self.status.emit("dl_finished") end_t = time.time() @@ -115,3 +170,6 @@ class DownloadThread(QThread): else: logger.info('Automatic installation not available on Linux.') + def kill(self): + self._kill = True + diff --git a/rare/utils/models.py b/rare/utils/models.py index 5b1a8f76..bd0e4d40 100644 --- a/rare/utils/models.py +++ b/rare/utils/models.py @@ -13,6 +13,3 @@ class InstallOptions: self.ignore_free_space = ignore_free_space self.force = force - -class KillDownloadException(Exception): - pass diff --git a/requirements.txt b/requirements.txt index ca0c11d7..cf9940ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ Pillow PyQt5 QtAwesome notify-py +psutil