diff --git a/rare/game_launch_helper/__init__.py b/rare/game_launch_helper/__init__.py index 7a2139b6..d1003206 100644 --- a/rare/game_launch_helper/__init__.py +++ b/rare/game_launch_helper/__init__.py @@ -15,10 +15,10 @@ from PyQt5.QtNetwork import QLocalServer, QLocalSocket from PyQt5.QtWidgets import QApplication from rare.lgndr.core import LegendaryCore +from rare.models.launcher import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel from rare.widgets.rare_app import RareApp from .console import Console from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError -from .message_models import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel logger = logging.getLogger("RareLauncher") diff --git a/rare/game_launch_helper/message_models.py b/rare/models/launcher.py similarity index 100% rename from rare/game_launch_helper/message_models.py rename to rare/models/launcher.py diff --git a/rare/shared/game_process.py b/rare/shared/game_process.py new file mode 100644 index 00000000..d7bc78b6 --- /dev/null +++ b/rare/shared/game_process.py @@ -0,0 +1,120 @@ +import datetime +import json +import logging + +from PyQt5.QtCore import QObject, pyqtSignal, QTimer, pyqtSlot +from PyQt5.QtNetwork import QLocalSocket +from PyQt5.QtWidgets import QMessageBox + +from rare.models.game import RareGame +from rare.models.launcher import ErrorModel, Actions, FinishedModel, StateChangedModel + +logger = logging.getLogger("GameProcess") + + +class GameProcess(QObject): + # str: app_name, int: exit_code + game_finished = pyqtSignal(RareGame, int) + # str: app_name + game_launched = pyqtSignal(RareGame) + + def __init__(self, rgame: RareGame, on_startup=False, always_ask_sync: bool = False): + super(GameProcess, self).__init__() + self.rgame = rgame + self.on_startup = on_startup + self.tried_connections = 0 + self.socket = QLocalSocket() + self.socket.connected.connect(self._socket_connected) + try: + self.socket.errorOccurred.connect(self._error_occurred) + except AttributeError: + QTimer.singleShot(100, lambda: self._error_occurred(QLocalSocket.UnknownSocketError) if self.socket.error() else None) + logger.warning("Do not handle errors on QLocalSocket, because of an old qt version") + self.socket.readyRead.connect(self._message_available) + self.always_ask_sync = always_ask_sync + + def close_socket(): + try: + self.socket.close() + except RuntimeError: + pass + + self.socket.disconnected.connect(close_socket) + self.timer = QTimer() + if not on_startup: + # wait a short time for process started + self.timer.timeout.connect(self.connect_to_server) + self.timer.start(200) + else: + # nothing happens, if no server available + self.connect_to_server() + + def connect_to_server(self): + self.socket.connectToServer(f"rare_{self.rgame.app_name}") + self.tried_connections += 1 + + if self.tried_connections > 50: # 10 seconds + QMessageBox.warning(None, "Error", self.tr("Connection to game process failed (Timeout)")) + self.timer.stop() + self.game_finished.emit(self.rgame, 1) + + @pyqtSlot() + def _message_available(self): + message = self.socket.readAll().data() + if not message.startswith(b"{"): + logger.error(f"Received unsupported message: {message.decode('utf-8')}") + return + try: + data = json.loads(message) + except json.JSONDecodeError as e: + logger.error(e) + logger.error("Could not load json data") + return + + action = data.get("action", -1) + + if action == -1: + logger.error("Got unexpected action") + elif action == Actions.finished: + logger.info(f"{self.rgame.app_name} {self.rgame.app_title} finished") + model = FinishedModel.from_json(data) + self.socket.close() + self._game_finished(model.exit_code) + elif action == Actions.error: + model = ErrorModel.from_json(data) + logger.error(f"Error in game {self.rgame.app_title}: {model.error_string}") + self.socket.close() + self._game_finished(1) + QMessageBox.warning(None, "Error", self.tr( + "Error in game {}: \n{}").format(self.rgame.app_title, model.error_string)) + + elif action == Actions.state_update: + model = StateChangedModel.from_json(data) + if model.new_state == StateChangedModel.States.started: + logger.info("Launched Game") + self.game_launched.emit(self.rgame) + meta_data = self.rgame.metadata + meta_data.last_played = datetime.datetime.now() + self.rgame.save_metadata() + + @pyqtSlot() + def _socket_connected(self): + self.timer.stop() + self.timer.deleteLater() + logger.info(f"Connection established for {self.rgame.app_name} ({self.rgame.app_title})") + if self.on_startup: + logger.info(f"Found {self.rgame.app_name} ({self.rgame.app_title}) running at startup") + + # FIXME run this after startup, widgets do not exist at this time + QTimer.singleShot(1000, lambda: self.game_launched.emit(self.rgame)) + + @pyqtSlot(QLocalSocket.LocalSocketError) + def _error_occurred(self, _: QLocalSocket.LocalSocketError): + if self.on_startup: + self.socket.close() + self._game_finished(-1234) # 1234 is exit code for startup + logger.error(f"{self.rgame.app_name} ({self.rgame.app_title}): {self.socket.errorString()}") + + def _game_finished(self, exit_code: int): + self.deleteLater() + self.game_finished.emit(self.rgame, exit_code) diff --git a/rare/shared/game_utils.py b/rare/shared/game_utils.py index d407d102..a6d6226e 100644 --- a/rare/shared/game_utils.py +++ b/rare/shared/game_utils.py @@ -1,134 +1,26 @@ -import datetime -import json import os import platform from logging import getLogger -from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QTimer, pyqtSlot +from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, pyqtSlot from PyQt5.QtCore import QStandardPaths from PyQt5.QtGui import QDesktopServices -from PyQt5.QtNetwork import QLocalSocket from PyQt5.QtWidgets import QMessageBox, QPushButton from legendary.core import LegendaryCore from rare.components.dialogs.uninstall_dialog import UninstallDialog -from rare.game_launch_helper import message_models from rare.lgndr.cli import LegendaryCLI from rare.lgndr.glue.arguments import LgndrUninstallGameArgs from rare.lgndr.glue.monkeys import LgndrIndirectStatus from rare.models.game import RareGame from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton +from rare.shared.game_process import GameProcess from rare.utils import config_helper, misc from .cloud_save_utils import CloudSaveUtils logger = getLogger("GameUtils") -class GameProcess(QObject): - # str: app_name, int: exit_code - game_finished = pyqtSignal(RareGame, int) - # str: app_name - game_launched = pyqtSignal(RareGame) - - def __init__(self, rgame: RareGame, on_startup=False, always_ask_sync: bool = False): - super(GameProcess, self).__init__() - self.rgame = rgame - self.on_startup = on_startup - self.tried_connections = 0 - self.socket = QLocalSocket() - self.socket.connected.connect(self._socket_connected) - try: - self.socket.errorOccurred.connect(self._error_occurred) - except AttributeError: - QTimer.singleShot(100, lambda: self._error_occurred(None) if self.socket.error() else None) - logger.warning("Do not handle errors on QLocalSocket, because of an old qt version") - self.socket.readyRead.connect(self._message_available) - self.always_ask_sync = always_ask_sync - - def close_socket(): - try: - self.socket.close() - except RuntimeError: - pass - - self.socket.disconnected.connect(close_socket) - self.timer = QTimer() - if not on_startup: - # wait a short time for process started - self.timer.timeout.connect(self.connect_to_server) - self.timer.start(200) - else: - # nothing happens, if no server available - self.connect_to_server() - - def connect_to_server(self): - self.socket.connectToServer(f"rare_{self.rgame.app_name}") - self.tried_connections += 1 - - if self.tried_connections > 50: # 10 seconds - QMessageBox.warning(None, "Error", self.tr("Connection to game process failed (Timeout)")) - self.timer.stop() - self.game_finished.emit(self.rgame, 1) - - def _message_available(self): - message = self.socket.readAll().data() - if not message.startswith(b"{"): - logger.error(f"Received unsupported message: {message.decode('utf-8')}") - return - try: - data = json.loads(message) - except json.JSONDecodeError as e: - logger.error(e) - logger.error("Could not load json data") - return - - action = data.get("action", -1) - - if action == -1: - logger.error("Got unexpected action") - elif action == message_models.Actions.finished: - logger.info(f"{self.rgame.app_name} {self.rgame.app_title} finished") - model = message_models.FinishedModel.from_json(data) - self.socket.close() - self._game_finished(model.exit_code) - elif action == message_models.Actions.error: - model = message_models.ErrorModel.from_json(data) - logger.error(f"Error in game {self.rgame.app_title}: {model.error_string}") - self.socket.close() - self._game_finished(1) - QMessageBox.warning(None, "Error", self.tr( - "Error in game {}: \n{}").format(self.rgame.app_title, model.error_string)) - - elif action == message_models.Actions.state_update: - model = message_models.StateChangedModel.from_json(data) - if model.new_state == message_models.StateChangedModel.States.started: - logger.info("Launched Game") - self.game_launched.emit(self.rgame) - meta_data = self.rgame.metadata - meta_data.last_played = datetime.datetime.now() - self.rgame.save_metadata() - - def _socket_connected(self): - self.timer.stop() - self.timer.deleteLater() - logger.info(f"Connection established for {self.rgame.app_name} ({self.rgame.app_title})") - if self.on_startup: - logger.info(f"Found {self.rgame.app_name} ({self.rgame.app_title}) running at startup") - - # FIXME run this after startup, widgets do not exist at this time - QTimer.singleShot(1000, lambda: self.game_launched.emit(self.rgame)) - - def _error_occurred(self, _): - if self.on_startup: - self.socket.close() - self.deleteLater() - self._game_finished(-1234) # 1234 is exit code for startup - logger.error(f"{self.rgame.app_name} ({self.rgame.app_title}): {self.socket.errorString()}") - - def _game_finished(self, exit_code: int): - self.game_finished.emit(self.rgame, exit_code) - - def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False): igame = core.get_installed_game(app_name) @@ -282,6 +174,7 @@ class GameUtils(QObject): game_process.game_launched.connect(self.game_launched.emit) self.running_games[rgame.app_name] = game_process + @pyqtSlot(RareGame, int) def game_finished(self, rgame: RareGame, exit_code): if self.running_games.get(rgame.app_name): self.running_games.pop(rgame.app_name)