1
0
Fork 0
mirror of synced 2024-09-30 09:17:37 +13:00

Workers: Implement wrapper QueueWorker class prototype for queueable workers

RareCore: Impelement base worker queue
This commit is contained in:
loathingKernel 2023-01-31 15:43:26 +02:00
parent 07ef43b13e
commit 6800b7e9ab
8 changed files with 135 additions and 45 deletions

View file

@ -27,8 +27,7 @@ from rare.shared import (
ImageManagerSingleton, ImageManagerSingleton,
) )
from rare.shared.image_manager import ImageSize from rare.shared.image_manager import ImageSize
from rare.shared.workers.verify import VerifyWorker from rare.shared.workers import VerifyWorker, MoveWorker
from rare.shared.workers.move import MoveWorker
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
from rare.utils.misc import get_size from rare.utils.misc import get_size
from rare.utils.steam_grades import SteamWorker from rare.utils.steam_grades import SteamWorker
@ -267,8 +266,8 @@ class GameInfo(QWidget):
) )
copy_worker.signals.progress.connect(self.__on_move_progress) copy_worker.signals.progress.connect(self.__on_move_progress)
copy_worker.signals.finished.connect(self.set_new_game) copy_worker.signals.result.connect(self.set_new_game)
copy_worker.signals.no_space_left.connect(self.warn_no_space_left) copy_worker.signals.error.connect(self.warn_no_space_left)
QThreadPool.globalInstance().start(copy_worker) QThreadPool.globalInstance().start(copy_worker)
def move_helper_clean_up(self): def move_helper_clean_up(self):
@ -300,6 +299,10 @@ class GameInfo(QWidget):
def show_menu_after_browse(self): def show_menu_after_browse(self):
self.ui.move_button.showMenu() self.ui.move_button.showMenu()
@pyqtSlot()
def __update_ui(self):
pass
@pyqtSlot(str) @pyqtSlot(str)
@pyqtSlot(RareGame) @pyqtSlot(RareGame)
def update_game(self, rgame: Union[RareGame, str]): def update_game(self, rgame: Union[RareGame, str]):
@ -307,24 +310,39 @@ class GameInfo(QWidget):
rgame = self.rcore.get_game(rgame) rgame = self.rcore.get_game(rgame)
if self.rgame is not None: if self.rgame is not None:
if (worker := self.rgame.__worker) is not None: if (worker := self.rgame.worker) is not None:
if isinstance(worker, VerifyWorker): if isinstance(worker, VerifyWorker):
try: try:
worker.signals.progress.disconnect(self.__on_verify_progress) worker.signals.progress.disconnect(self.__on_verify_progress)
except TypeError as e: except TypeError as e:
logger.warning(f"{self.rgame.app_title} verify worker: {e}") logger.warning(f"{self.rgame.app_name} verify worker: {e}")
if isinstance(worker, MoveWorker):
try:
worker.signals.progress.disconnect(self.__on_move_progress)
except TypeError as e:
logger.warning(f"{self.rgame.app_name} move worker: {e}")
self.rgame.signals.widget.update.disconnect(self.__update_ui)
self.rgame.signals.game.installed.disconnect(self.update_game) self.rgame.signals.game.installed.disconnect(self.update_game)
self.rgame.signals.game.uninstalled.disconnect(self.update_game) self.rgame.signals.game.uninstalled.disconnect(self.update_game)
self.rgame = rgame self.rgame = rgame
self.rgame.signals.widget.update.disconnect(self.__update_ui)
self.rgame.signals.game.installed.connect(self.update_game) self.rgame.signals.game.installed.connect(self.update_game)
self.rgame.signals.game.uninstalled.connect(self.update_game) self.rgame.signals.game.uninstalled.connect(self.update_game)
if (worker := self.rgame.__worker) is not None: if (worker := self.rgame.worker) is not None:
if isinstance(worker, VerifyWorker): if isinstance(worker, VerifyWorker):
self.ui.verify_stack.setCurrentWidget(self.ui.verify_progress_page) self.ui.verify_stack.setCurrentWidget(self.ui.verify_progress_page)
self.ui.verify_progress.setValue(self.rgame.progress) self.ui.verify_progress.setValue(self.rgame.progress)
worker.signals.progress.connect(self.__on_verify_progress) worker.signals.progress.connect(self.__on_verify_progress)
else: else:
self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page) self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page)
if isinstance(worker, MoveWorker):
self.ui.move_stack.setCurrentWidget(self.ui.move_progress_page)
self.ui.move_progress.setValue(self.rgame.progress)
worker.signals.progress.connect(self.__on_move_progress)
else:
self.ui.move_stack.setCurrentWidget(self.ui.move_button_page)
self.title.setTitle(self.rgame.app_title) self.title.setTitle(self.rgame.app_title)
self.image.setPixmap(rgame.pixmap) self.image.setPixmap(rgame.pixmap)
@ -383,7 +401,7 @@ class GameInfo(QWidget):
self.ui.move_stack.setCurrentWidget(self.ui.move_button_page) self.ui.move_stack.setCurrentWidget(self.ui.move_button_page)
# If a game is verifying or moving, disable both verify and moving buttons. # If a game is verifying or moving, disable both verify and moving buttons.
if rgame.__worker is not None: if rgame.worker is not None:
self.ui.verify_button.setEnabled(False) self.ui.verify_button.setEnabled(False)
self.ui.move_button.setEnabled(False) self.ui.move_button.setEnabled(False)
if self.is_moving: if self.is_moving:

View file

@ -14,8 +14,6 @@ from rare.lgndr.core import LegendaryCore
from rare.models.install import InstallOptionsModel, UninstallOptionsModel from rare.models.install import InstallOptionsModel, UninstallOptionsModel
from rare.shared.game_process import GameProcess from rare.shared.game_process import GameProcess
from rare.shared.image_manager import ImageManager from rare.shared.image_manager import ImageManager
from rare.shared.workers.move import MoveWorker
from rare.shared.workers.verify import VerifyWorker
from rare.utils.misc import get_rare_executable from rare.utils.misc import get_rare_executable
from rare.utils.paths import data_dir from rare.utils.paths import data_dir
@ -118,10 +116,11 @@ class RareGame(QObject):
if self.has_update: if self.has_update:
logger.info(f"Update available for game: {self.app_name} ({self.app_title})") logger.info(f"Update available for game: {self.app_name} ({self.app_title})")
self.progress: int = 0
self.__worker: Optional[QRunnable] = None self.__worker: Optional[QRunnable] = None
self.__state = RareGame.State.IDLE self.__state = RareGame.State.IDLE
self.progress: int = 0
self.signals.progress.start.connect(lambda: self.__on_progress_update(0))
self.signals.progress.update.connect(self.__on_progress_update)
self.game_process = GameProcess(self.game) self.game_process = GameProcess(self.game)
self.game_process.launched.connect(self.__game_launched) self.game_process.launched.connect(self.__game_launched)
@ -130,18 +129,15 @@ class RareGame(QObject):
self.game_process.connect_to_server(on_startup=True) self.game_process.connect_to_server(on_startup=True)
# self.grant_date(True) # self.grant_date(True)
def __on_progress_update(self, progress: int):
self.progress = progress
@property @property
def worker(self) -> Optional[QRunnable]: def worker(self) -> Optional[QRunnable]:
return self.__worker return self.__worker
@worker.setter @worker.setter
def worker(self, worker: Optional[QRunnable]): def worker(self, worker: Optional[QRunnable]):
if worker is None:
self.state = RareGame.State.IDLE
if isinstance(worker, VerifyWorker):
self.state = RareGame.State.VERIFYING
if isinstance(worker, MoveWorker):
self.state = RareGame.State.MOVING
self.__worker = worker self.__worker = worker
@property @property
@ -497,21 +493,13 @@ class RareGame(QObject):
def refresh_pixmap(self): def refresh_pixmap(self):
self.image_manager.download_image(self.game, self.set_pixmap, 0, True) self.image_manager.download_image(self.game, self.set_pixmap, 0, True)
def start_progress(self): def install(self) -> bool:
self.signals.progress.start.emit() if not self.is_idle:
return False
def update_progress(self, progress: int):
self.progress = progress
self.signals.progress.update.emit(progress)
def finish_progress(self, fail: bool, miss: int, app: str):
self.set_installed(True)
self.signals.progress.finish.emit(fail)
def install(self):
self.signals.game.install.emit( self.signals.game.install.emit(
InstallOptionsModel(app_name=self.app_name) InstallOptionsModel(app_name=self.app_name)
) )
return True
def repair(self, repair_and_update): def repair(self, repair_and_update):
self.signals.game.install.emit( self.signals.game.install.emit(
@ -520,10 +508,13 @@ class RareGame(QObject):
) )
) )
def uninstall(self): def uninstall(self) -> bool:
if not self.is_idle:
return False
self.signals.game.uninstall.emit( self.signals.game.uninstall.emit(
UninstallOptionsModel(app_name=self.app_name) UninstallOptionsModel(app_name=self.app_name)
) )
return True
def launch( def launch(
self, self,
@ -532,9 +523,9 @@ class RareGame(QObject):
wine_bin: Optional[str] = None, wine_bin: Optional[str] = None,
wine_pfx: Optional[str] = None, wine_pfx: Optional[str] = None,
ask_sync_saves: bool = False, ask_sync_saves: bool = False,
): ) -> bool:
if not self.can_launch: if not self.can_launch:
return return False
cmd_line = get_rare_executable() cmd_line = get_rare_executable()
executable, args = cmd_line[0], cmd_line[1:] executable, args = cmd_line[0], cmd_line[1:]
@ -554,6 +545,7 @@ class RareGame(QObject):
QProcess.startDetached(executable, args) QProcess.startDetached(executable, args)
logger.info(f"Start new Process: ({executable} {' '.join(args)})") logger.info(f"Start new Process: ({executable} {' '.join(args)})")
self.game_process.connect_to_server(on_startup=False) self.game_process.connect_to_server(on_startup=False)
return True
class RareEosOverlay(QObject): class RareEosOverlay(QObject):

View file

@ -19,6 +19,8 @@ class GlobalSignals:
overlay_installed = pyqtSignal() overlay_installed = pyqtSignal()
# none # none
update_tray = pyqtSignal() update_tray = pyqtSignal()
# none
update_statusbar = pyqtSignal()
class GameSignals(QObject): class GameSignals(QObject):
# model # model

View file

@ -3,9 +3,9 @@ import os
from argparse import Namespace from argparse import Namespace
from itertools import chain from itertools import chain
from logging import getLogger from logging import getLogger
from typing import Optional, Dict, Iterator, Callable from typing import Optional, Dict, Iterator, Callable, List
from PyQt5.QtCore import QObject from PyQt5.QtCore import QObject, QThreadPool
from legendary.lfs.eos import EOSOverlayApp from legendary.lfs.eos import EOSOverlayApp
from legendary.models.game import Game, SaveGameFile from legendary.models.game import Game, SaveGameFile
@ -13,6 +13,7 @@ from rare.lgndr.core import LegendaryCore
from rare.models.apiresults import ApiResults from rare.models.apiresults import ApiResults
from rare.models.game import RareGame, RareEosOverlay from rare.models.game import RareGame, RareEosOverlay
from rare.models.signals import GlobalSignals from rare.models.signals import GlobalSignals
from .workers import QueueWorker, VerifyWorker, MoveWorker
from .image_manager import ImageManager from .image_manager import ImageManager
logger = getLogger("RareCore") logger = getLogger("RareCore")
@ -36,12 +37,30 @@ class RareCore(QObject):
self.core(init=True) self.core(init=True)
self.image_manager(init=True) self.image_manager(init=True)
self.queue_workers: List[QueueWorker] = []
self.queue_threadpool = QThreadPool()
self.queue_threadpool.setMaxThreadCount(2)
self.__games: Dict[str, RareGame] = {} self.__games: Dict[str, RareGame] = {}
self.__eos_overlay_rgame = RareEosOverlay(self.__core, self.__image_manager, EOSOverlayApp) self.__eos_overlay_rgame = RareEosOverlay(self.__core, self.__image_manager, EOSOverlayApp)
RareCore._instance = self RareCore._instance = self
def enqueue_worker(self, rgame: RareGame, worker: QueueWorker):
if isinstance(worker, VerifyWorker):
rgame.state = RareGame.State.VERIFYING
if isinstance(worker, MoveWorker):
rgame.state = RareGame.State.MOVING
rgame.worker = worker
worker.feedback.started.connect(self.__signals.application.update_statubar)
worker.feedback.finished.connect(lambda: self.queue_workers.remove(worker))
self.queue_workers.append(worker)
self.queue_threadpool.start(worker, 0)
def queue_info(self) -> List:
return [w.worker_info() for w in self.queue_workers]
@staticmethod @staticmethod
def instance() -> 'RareCore': def instance() -> 'RareCore':
if RareCore._instance is None: if RareCore._instance is None:

View file

@ -0,0 +1,5 @@
from .install_info import InstallInfoWorker
from .move import MoveWorker
from .uninstall import UninstallWorker
from .verify import VerifyWorker
from .worker import Worker, QueueWorker

View file

@ -2,20 +2,20 @@ import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from PyQt5.QtCore import pyqtSignal, QRunnable, QObject from PyQt5.QtCore import pyqtSignal, QObject
from legendary.lfs.utils import validate_files from legendary.lfs.utils import validate_files
from legendary.models.game import VerifyResult, InstalledGame from legendary.models.game import VerifyResult, InstalledGame
from rare.lgndr.core import LegendaryCore from rare.lgndr.core import LegendaryCore
from .worker import Worker from .worker import QueueWorker
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
class MoveWorker(Worker): class MoveWorker(QueueWorker):
class Signals(QObject): class Signals(QObject):
progress = pyqtSignal(int) progress = pyqtSignal(int)
finished = pyqtSignal(str) result = pyqtSignal(str)
no_space_left = pyqtSignal() error = pyqtSignal()
def __init__( def __init__(
self, self,
@ -37,6 +37,9 @@ class MoveWorker(Worker):
self.file_list = None self.file_list = None
self.total: int = 0 self.total: int = 0
def worker_info(self):
return None
def run_real(self): def run_real(self):
root_directory = Path(self.install_path) root_directory = Path(self.install_path)
self.source_size = sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file()) self.source_size = sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file())

View file

@ -10,12 +10,12 @@ from rare.lgndr.core import LegendaryCore
from rare.lgndr.glue.arguments import LgndrVerifyGameArgs from rare.lgndr.glue.arguments import LgndrVerifyGameArgs
from rare.lgndr.glue.monkeys import LgndrIndirectStatus from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.game import RareGame from rare.models.game import RareGame
from .worker import Worker from .worker import QueueWorker
logger = getLogger("VerifyWorker") logger = getLogger("VerifyWorker")
class VerifyWorker(Worker): class VerifyWorker(QueueWorker):
class Signals(QObject): class Signals(QObject):
progress = pyqtSignal(RareGame, int, int, float, float) progress = pyqtSignal(RareGame, int, int, float, float)
result = pyqtSignal(RareGame, bool, int, int) result = pyqtSignal(RareGame, bool, int, int)
@ -35,6 +35,9 @@ class VerifyWorker(Worker):
self.rgame.signals.progress.update.emit(num * 100 // total) self.rgame.signals.progress.update.emit(num * 100 // total)
self.signals.progress.emit(self.rgame, num, total, percentage, speed) self.signals.progress.emit(self.rgame, num, total, percentage, speed)
def worker_info(self):
return None
def run_real(self): def run_real(self):
self.rgame.signals.progress.start.emit() self.rgame.signals.progress.start.emit()
cli = LegendaryCLI(self.core) cli = LegendaryCLI(self.core)

View file

@ -1,11 +1,21 @@
import sys import sys
from abc import abstractmethod from abc import abstractmethod
from enum import IntEnum
from typing import Optional from typing import Optional
from PyQt5.QtCore import QRunnable, QObject, pyqtSlot from PyQt5.QtCore import QRunnable, QObject, pyqtSlot, pyqtSignal
class Worker(QRunnable): class Worker(QRunnable):
"""
Base QRunnable class.
This class provides a base for QRunnables with signals that are automatically deleted.
To use this class you have to assign the signals object of your concrete implementation
to the `Worker.signals` attribute and implement `Worker.run_real()`
"""
def __init__(self): def __init__(self):
sys.excepthook = sys.__excepthook__ sys.excepthook = sys.__excepthook__
super(Worker, self).__init__() super(Worker, self).__init__()
@ -30,3 +40,41 @@ class Worker(QRunnable):
def run(self): def run(self):
self.run_real() self.run_real()
self.signals.deleteLater() self.signals.deleteLater()
class QueueWorker(Worker):
"""
Base queueable worker class
This class is a specialization of the `Worker` class. It provides feedback signals to know
if a worker has started or finished.
To use this class you have to assign the signals object of your concrete implementation
to the `QueueWorker.signals` attribute, implement `QueueWorker.run_real()` and `QueueWorker.worker_info()`
"""
class State(IntEnum):
UNDEFINED = 0
QUEUED = 1
ACTIVE = 2
class Signals(QObject):
started = pyqtSignal()
finished = pyqtSignal()
def __init__(self):
super(QueueWorker, self).__init__()
self.feedback = QueueWorker.Signals()
self.state = QueueWorker.State.QUEUED
@pyqtSlot()
def run(self):
self.state = QueueWorker.State.ACTIVE
self.feedback.started.emit()
super(QueueWorker, self).run()
self.feedback.finished.emit()
self.feedback.deleteLater()
@abstractmethod
def worker_info(self):
pass