diff --git a/src/store/managers/async_manager.py b/src/store/managers/async_manager.py new file mode 100644 index 0000000..046bb6e --- /dev/null +++ b/src/store/managers/async_manager.py @@ -0,0 +1,41 @@ +from gi.repository import Gio + +from src.game import Game +from src.store.managers.manager import Manager +from src.utils.task import Task + + +class AsyncManager(Manager): + """Manager that can run asynchronously""" + + blocking = False + cancellable: Gio.Cancellable = None + + def __init__(self) -> None: + super().__init__() + self.cancellable = Gio.Cancellable() + + def cancel_tasks(self): + """Cancel all tasks for this manager""" + self.cancellable.cancel() + + def reset_cancellable(self): + """Reset the cancellable for this manager. + Already scheduled Tasks will no longer be cancellable.""" + self.cancellable = Gio.Cancellable() + + def run(self, game: Game) -> None: + data = (game,) + task = Task.new(self, self.cancellable, self._task_callback, data) + task.set_task_data(data) + task.run_in_thread(self._task_thread_func) + + def _task_thread_func(self, _task, _source_object, data, cancellable): + """Task thread entry point""" + game, *_rest = data + self.emit("started") + self.final_run(game) + + def _task_callback(self, _source_object, _result, _data): + """Method run after the async task is done""" + self.emit("done") diff --git a/src/store/managers/display_manager.py b/src/store/managers/display_manager.py index ba7ca39..81a0f0b 100644 --- a/src/store/managers/display_manager.py +++ b/src/store/managers/display_manager.py @@ -9,7 +9,8 @@ class DisplayManager(Manager): run_after = set((FileManager,)) - def run(self, game: Game) -> None: + def final_run(self, game: Game) -> None: # TODO decouple a game from its widget + # TODO make the display manager async shared.win.games[game.game_id] = game game.update() diff --git a/src/store/managers/file_manager.py b/src/store/managers/file_manager.py index ffd2363..12884ed 100644 --- a/src/store/managers/file_manager.py +++ b/src/store/managers/file_manager.py @@ -1,12 +1,12 @@ from src.game import Game -from src.store.managers.manager import Manager +from src.store.managers.async_manager import AsyncManager from src.store.managers.steam_api_manager import SteamAPIManager -class FileManager(Manager): +class FileManager(AsyncManager): """Manager in charge of saving a game to a file""" run_after = set((SteamAPIManager,)) - def run(self, game: Game) -> None: + def final_run(self, game: Game) -> None: game.save() diff --git a/src/store/managers/manager.py b/src/store/managers/manager.py index 376662f..8d73999 100644 --- a/src/store/managers/manager.py +++ b/src/store/managers/manager.py @@ -1,10 +1,11 @@ from abc import abstractmethod -from gi.repository import Gio + +from gi.repository import GObject from src.game import Game -class Manager: +class Manager(GObject.Object): """Class in charge of handling a post creation action for games. * May connect to signals on the game to handle them. @@ -13,26 +14,15 @@ class Manager: """ run_after: set[type["Manager"]] = set() - - cancellable: Gio.Cancellable errors: list[Exception] + blocking: bool = True def __init__(self) -> None: super().__init__() - self.cancellable = Gio.Cancellable() self.errors = [] - def cancel_tasks(self): - """Cancel all tasks for this manager""" - self.cancellable.cancel() - - def reset_cancellable(self): - """Reset the cancellable for this manager. - Alreadyn scheduled Tasks will no longer be cancellable.""" - self.cancellable = Gio.Cancellable() - def report_error(self, error: Exception): - """Report an error that happened in of run""" + """Report an error that happened in Manager.run""" self.errors.append(error) def collect_errors(self) -> list[Exception]: @@ -42,8 +32,26 @@ class Manager: return errors @abstractmethod - def run(self, game: Game, cancellable: Gio.Cancellable) -> None: + def final_run(self, game: Game) -> None: + """ + Abstract method overriden by final child classes, called by the run method. + * May block its thread + * May not raise exceptions, as they will be silently ignored + """ + + def run(self, game: Game) -> None: """Pass the game through the manager. - May block its thread. - May not raise exceptions, as they will be silently ignored.""" + In charge of calling the final_run method.""" + self.emit("started") + self.final_run(game) + self.emit("done") + + @GObject.Signal(name="started") + def started(self) -> None: + """Signal emitted when a manager is started""" + pass + + @GObject.Signal(name="done") + def done(self) -> None: + """Signal emitted when a manager is done""" pass diff --git a/src/store/managers/sgdb_manager.py b/src/store/managers/sgdb_manager.py index a87885d..d01e3f3 100644 --- a/src/store/managers/sgdb_manager.py +++ b/src/store/managers/sgdb_manager.py @@ -1,17 +1,17 @@ from requests import HTTPError from src.game import Game -from src.store.managers.manager import Manager +from src.store.managers.async_manager import AsyncManager from src.utils.steamgriddb import SGDBAuthError, SGDBError, SGDBHelper from src.store.managers.steam_api_manager import SteamAPIManager -class SGDBManager(Manager): +class SGDBManager(AsyncManager): """Manager in charge of downloading a game's cover from steamgriddb""" run_after = set((SteamAPIManager,)) - def run(self, game: Game) -> None: + def final_run(self, game: Game) -> None: try: sgdb = SGDBHelper() sgdb.conditionaly_update_cover(game) diff --git a/src/store/managers/steam_api_manager.py b/src/store/managers/steam_api_manager.py index 93e4d42..599580d 100644 --- a/src/store/managers/steam_api_manager.py +++ b/src/store/managers/steam_api_manager.py @@ -1,14 +1,14 @@ from requests import HTTPError, JSONDecodeError from src.game import Game -from src.store.managers.manager import Manager +from src.store.managers.async_manager import AsyncManager from src.utils.steam import SteamGameNotFoundError, SteamHelper, SteamNotAGameError -class SteamAPIManager(Manager): +class SteamAPIManager(AsyncManager): """Manager in charge of completing a game's data from the Steam API""" - def run(self, game: Game) -> None: + def final_run(self, game: Game) -> None: # Skip non-steam games if not game.source.startswith("steam_"): return diff --git a/src/store/pipeline.py b/src/store/pipeline.py index feea237..7453630 100644 --- a/src/store/pipeline.py +++ b/src/store/pipeline.py @@ -51,34 +51,29 @@ class Pipeline(GObject.Object): def advance(self): """Spawn tasks for managers that are able to run for a game""" - for manager in self.ready: - self.waiting.remove(manager) - self.running.add(manager) - data = (manager,) - task = Task.new(self, manager.cancellable, self.manager_task_callback, data) - task.set_task_data(data) - task.run_in_thread(self.manager_task_thread_func) + + # Separate blocking / async managers + managers = self.ready + blocking = set(filter(lambda manager: manager.blocking, managers)) + parallel = managers - parallel + + # Schedule parallel managers, then run the blocking ones + for manager in (*parallel, *blocking): + manager.run(self.game) @GObject.Signal(name="manager-started", arg_types=(object,)) def manager_started(self, manager: Manager) -> None: """Signal emitted when a manager is started""" pass - def manager_task_thread_func(self, _task, _source_object, data, cancellable): - """Thread function for manager tasks""" - manager, *_rest = data - self.emit("manager-started", manager) - manager.run(self.game) - @GObject.Signal(name="manager-done", arg_types=(object,)) def manager_done(self, manager: Manager) -> None: """Signal emitted when a manager is done""" pass - def manager_task_callback(self, _source_object, _result, data): - """Callback function for manager tasks""" - manager, *_rest = data - self.running.remove(manager) - self.done.add(manager) + def on_manager_started(self, manager: Manager) -> None: + self.emit("manager-started", manager) + + def on_manager_done(self, manager: Manager) -> None: self.emit("manager-done", manager) self.advance()