diff --git a/.github/GenPKG.sh b/.github/GenPKG.sh index 2daa014b..a17e5031 100755 --- a/.github/GenPKG.sh +++ b/.github/GenPKG.sh @@ -1,5 +1,5 @@ export PYTHONPATH=$PWD -version=$(python3 Rare --version) +version=$(python3 rare --version) cd .github git clone https://aur.archlinux.org/rare.git cd .. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d077e817..6a683a50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,9 @@ -# This workflow will upload a Python Package using Twine when a release -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: New Release on: release: types: [ published ] - workflow_dispatch: jobs: pypy-deploy: @@ -61,9 +58,10 @@ jobs: pip3 install pyinstaller setuptools wheel pip3 install -r requirements.txt - name: Prepare - run: cp Rare/__main__.py ./ + run: cp rare/__main__.py ./ - name: Build run: pyinstaller + --icon=rare/styles/Logo.ico --onefile --name Rare --add-data="Rare/languages/*;Rare/languages" @@ -91,7 +89,7 @@ jobs: python3-requests python3-pyqt5 python3-pil - python3-qtawesome-common + python3-qtawesome python3-setuptools python3-wheel @@ -101,7 +99,7 @@ jobs: python3 setup.py --command-packages=stdeb.command bdist_deb - name: move file - run: mv deb_dist/*.deb rare.deb + run: mv deb_dist/*.deb Rare.deb - name: Upload files to GitHub uses: svenstaro/upload-release-action@2.2.1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7da7f6ba..51190ea6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,8 @@ exmples: Select one Card of the project and implement it or make other changes -##Git crash-course -To contribute fork the repository and clone **your** repo. Then make your changes, add it to git with `git add .` and upload it to Github with `git commit -m "message"` and `git push` or with your IDE. +## Git crash-course +To contribute fork the repository and clone **your** repo. Then make your changes, add it to git with `git add File.xy` and upload it to GitHub with `git commit -m "message"` and `git push`. +Some IDEs can do this automatically. -If you uploaded your changes, create a pull request +If you uploaded your changes, create a pull request to dev-branch diff --git a/MANIFEST.in b/MANIFEST.in index 5d5c4807..3ee6d4e8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include README.md -include rare/languages/de.qm +include rare/languages/*.qm include rare/styles/* \ No newline at end of file diff --git a/rare/__init__.py b/rare/__init__.py index 03cd8cf2..e8c05130 100644 --- a/rare/__init__.py +++ b/rare/__init__.py @@ -1,5 +1,5 @@ import os -__version__ = "1.0_beta2" +__version__ = "1.0.0" style_path = os.path.join(os.path.dirname(__file__), "styles/") lang_path = os.path.join(os.path.dirname(__file__), "languages/") diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index 0266f81b..29c8483a 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/components/tabs/settings/rare.py b/rare/components/tabs/settings/rare.py index e8f39eac..5b1b9426 100644 --- a/rare/components/tabs/settings/rare.py +++ b/rare/components/tabs/settings/rare.py @@ -64,8 +64,14 @@ class RareSettings(QScrollArea): self.cloud_sync.setChecked(self.settings.value("auto_sync_cloud", True, bool)) self.cloud_sync_widget = SettingsWidget(self.tr("Auto sync with cloud"), self.cloud_sync) self.layout.addWidget(self.cloud_sync_widget) - self.cloud_sync.stateChanged.connect(lambda: self.settings.setValue(f"auto_sync_cloud", - self.cloud_sync.isChecked())) + self.cloud_sync.stateChanged.connect( + lambda: self.settings.setValue(f"auto_sync_cloud", self.cloud_sync.isChecked())) + + self.save_size = QCheckBox(self.tr("Save size")) + self.save_size.setChecked(self.settings.value("save_size", False, bool)) + self.save_size_widget = SettingsWidget(self.tr("Save size of window after restart"), self.save_size) + self.layout.addWidget(self.save_size_widget) + self.save_size.stateChanged.connect(self.save_window_size) self.save_size = QCheckBox(self.tr("Save size")) self.save_size.setChecked(self.settings.value("save_size", False, bool)) diff --git a/rare/components/tray_icon.py b/rare/components/tray_icon.py index c3393bdc..6d568dec 100644 --- a/rare/components/tray_icon.py +++ b/rare/components/tray_icon.py @@ -1,12 +1,12 @@ -from PyQt5.QtCore import QCoreApplication +from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction -from qtawesome import icon +from rare import style_path class TrayIcon(QSystemTrayIcon): def __init__(self, parent): super(TrayIcon, self).__init__(parent) - self.setIcon(icon("ei.cogs", color="white")) # TODO change icon to logo + self.setIcon(QIcon(style_path+"Logo.png")) self.setVisible(True) self.setToolTip("Rare") diff --git a/rare/languages/de.qm b/rare/languages/de.qm index e3e7550f..bdc9f860 100644 Binary files a/rare/languages/de.qm and b/rare/languages/de.qm differ diff --git a/rare/styles/Logo.ico b/rare/styles/Logo.ico new file mode 100644 index 00000000..532e800c Binary files /dev/null and b/rare/styles/Logo.ico differ diff --git a/rare/styles/Logo.png b/rare/styles/Logo.png index 4470660b..c608b988 100644 Binary files a/rare/styles/Logo.png and b/rare/styles/Logo.png differ 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 diff --git a/setup.py b/setup.py index a564bddc..2d08904c 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ setuptools.setup( version=version, author="Dummerle", license="GPL-3", + description="A gui for Legendary", long_description=long_description, long_description_content_type="text/markdown", include_package_data=True, @@ -32,5 +33,6 @@ setuptools.setup( "PyQt5", "QtAwesome", "notify-py", + "psutil" ], )