diff --git a/.gitignore b/.gitignore index b5aa625d..dbd7a529 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,9 @@ __pycache__ /AppDir/ /System Volume Information/ /test_files/ +# Nuitka build artifacts /rare.build +/rare.dist /rare.bin +/rare.cmd +/rare.exe diff --git a/optional_requirements.txt b/optional_requirements.txt deleted file mode 100644 index 0f29f291..00000000 --- a/optional_requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pywebview[gtk]; platform_system == "Linux" -pywebview[cef]; platform_system == "Windows" -pypresence - diff --git a/rare/__main__.py b/rare/__main__.py index 84e1fc87..d782c122 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -114,6 +114,7 @@ if __name__ == "__main__": # ) # insert source directory - sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute())) + if "__compiled__" not in globals(): + sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute())) main() diff --git a/rare/app.py b/rare/app.py index ef48f817..cee3e06f 100644 --- a/rare/app.py +++ b/rare/app.py @@ -1,21 +1,17 @@ import logging import os -import platform import shutil import sys -import time import traceback from argparse import Namespace from datetime import datetime from typing import Optional -import legendary import requests.exceptions -from PyQt5.QtCore import QThreadPool, QTimer, QT_VERSION_STR, PYQT_VERSION_STR +from PyQt5.QtCore import QThreadPool, QTimer from PyQt5.QtWidgets import QApplication, QMessageBox from requests import HTTPError -import rare from rare.components.dialogs.launch_dialog import LaunchDialog from rare.components.main_window import MainWindow from rare.shared import ( @@ -27,7 +23,6 @@ from rare.shared.rare_core import RareCore from rare.utils import legendary_utils, config_helper, paths from rare.widgets.rare_app import RareApp - logger = logging.getLogger("Rare") @@ -52,49 +47,8 @@ def excepthook(exc_type, exc_value, exc_tb): class App(RareApp): def __init__(self, args: Namespace): - super(App, self).__init__(args) - - start_time = time.strftime("%y-%m-%d--%H-%M") # year-month-day-hour-minute - file_name = os.path.join(paths.log_dir(), f"Rare_{start_time}.log") - - for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) - - file_handler = logging.FileHandler(filename=file_name, encoding="utf-8") - file_handler.setFormatter(fmt=logging.Formatter("[%(name)s] %(levelname)s: %(message)s")) - - # configure logging - if args.debug: - logging.basicConfig( - format="[%(name)s] %(levelname)s: %(message)s", - level=logging.DEBUG, - stream=sys.stderr, - ) - file_handler.setLevel(logging.DEBUG) - logging.root.addHandler(file_handler) - logging.getLogger().setLevel(level=logging.DEBUG) - # keep requests, asyncio and pillow quiet - logging.getLogger("requests").setLevel(logging.WARNING) - logging.getLogger("urllib3").setLevel(logging.WARNING) - logging.getLogger("asyncio").setLevel(logging.WARNING) - logger.info( - f"Launching Rare version {rare.__version__} Codename: {rare.code_name}\n" - f" - Using Legendary {legendary.__version__} Codename: {legendary.__codename__} as backend\n" - f" - Operating System: {platform.system()}, Python version: {platform.python_version()}\n" - f" - Running {sys.executable} {' '.join(sys.argv)}\n" - f" - Qt version: {QT_VERSION_STR}, PyQt version: {PYQT_VERSION_STR}" - ) - else: - logging.basicConfig( - format="[%(name)s] %(levelname)s: %(message)s", - level=logging.INFO, - stream=sys.stderr, - ) - file_handler.setLevel(logging.INFO) - logging.root.addHandler(file_handler) - logger.info(f"Launching Rare version {rare.__version__}") - logger.info(f"Operating System: {platform.system()}") - + log_file = "Rare_{0}.log" + super(App, self).__init__(args, log_file) self.rare_core = RareCore(args=args) self.args = ArgumentsSingleton() self.signals = GlobalSignalsSingleton() diff --git a/rare/components/main_window.py b/rare/components/main_window.py index 60759be6..5f4f9e92 100644 --- a/rare/components/main_window.py +++ b/rare/components/main_window.py @@ -19,7 +19,7 @@ class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent=parent) - self.setAttribute(Qt.WA_DeleteOnClose) + self.setAttribute(Qt.WA_DeleteOnClose, True) self.core = LegendaryCoreSingleton() self.signals = GlobalSignalsSingleton() self.args = ArgumentsSingleton() diff --git a/rare/game_launch_helper/__init__.py b/rare/game_launch_helper/__init__.py index ac97e272..b1778952 100644 --- a/rare/game_launch_helper/__init__.py +++ b/rare/game_launch_helper/__init__.py @@ -17,6 +17,8 @@ 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") + class PreLaunchThread(QRunnable): class Signals(QObject): @@ -65,7 +67,8 @@ class GameProcessApp(RareApp): success: bool = True def __init__(self, args: Namespace): - super(GameProcessApp, self).__init__(args) + log_file = f"Rare_Launcher_{args.app_name}" + "_{0}.log" + super(GameProcessApp, self).__init__(args, log_file) self.game_process = QProcess() self.app_name = args.app_name self.logger = getLogger(self.app_name) @@ -98,7 +101,7 @@ class GameProcessApp(RareApp): ) ) self.game_process.readyReadStandardError.connect( - lambda: self.console.log( + lambda: self.console.error( self.game_process.readAllStandardError().data().decode("utf-8", "ignore") ) ) @@ -132,6 +135,8 @@ class GameProcessApp(RareApp): def game_finished(self, exit_code): self.logger.info("game finished") + if self.console: + self.console.on_process_exit(self.core.get_game(self.app_name).app_title, exit_code) self.send_message( FinishedModel( action=Actions.finished, @@ -171,6 +176,8 @@ class GameProcessApp(RareApp): def error_occurred(self, error_str: str): self.logger.warning(error_str) + if self.console: + self.console.on_process_exit(self.core.get_game(self.app_name).app_title, error_str) self.send_message(ErrorModel( error_string=error_str, app_name=self.app_name, action=Actions.error) @@ -206,10 +213,6 @@ class GameProcessApp(RareApp): def start_game(args: Namespace): args = InitArgs.from_argparse(args) - logging.basicConfig( - format="[%(name)s] %(levelname)s: %(message)s", - level=logging.INFO, - ) app = GameProcessApp(args) app.setQuitOnLastWindowClosed(True) diff --git a/rare/game_launch_helper/console.py b/rare/game_launch_helper/console.py index 1046ff6c..bc9d9691 100644 --- a/rare/game_launch_helper/console.py +++ b/rare/game_launch_helper/console.py @@ -1,7 +1,8 @@ import platform +from typing import Union -from PyQt5.QtCore import QProcessEnvironment, pyqtSignal, QSize -from PyQt5.QtGui import QTextCursor, QFont, QCursor +from PyQt5.QtCore import QProcessEnvironment, pyqtSignal, QSize, Qt +from PyQt5.QtGui import QTextCursor, QFont, QCursor, QCloseEvent from PyQt5.QtWidgets import ( QPlainTextEdit, QDialog, @@ -23,6 +24,7 @@ class Console(QDialog): def __init__(self, parent=None): super(Console, self).__init__(parent=parent) + self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowTitle("Rare - Console") self.setGeometry(0, 0, 640, 480) layout = QVBoxLayout() @@ -46,15 +48,15 @@ class Console(QDialog): button_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed)) - self.terminate_button = QPushButton(self.tr("Terminate")) - self.terminate_button.setVisible(platform.system() == "Windows") - button_layout.addWidget(self.terminate_button) - self.terminate_button.clicked.connect(lambda: self.term.emit()) - - self.kill_button = QPushButton(self.tr("Kill")) - self.kill_button.setVisible(platform.system() == "Windows") - button_layout.addWidget(self.kill_button) - self.kill_button.clicked.connect(lambda: self.kill.emit()) + # self.terminate_button = QPushButton(self.tr("Terminate")) + # self.terminate_button.setVisible(platform.system() == "Windows") + # button_layout.addWidget(self.terminate_button) + # self.terminate_button.clicked.connect(lambda: self.term.emit()) + # + # self.kill_button = QPushButton(self.tr("Kill")) + # self.kill_button.setVisible(platform.system() == "Windows") + # button_layout.addWidget(self.kill_button) + # self.kill_button.clicked.connect(lambda: self.kill.emit()) layout.addLayout(button_layout) @@ -63,6 +65,8 @@ class Console(QDialog): self.env_variables = ConsoleEnv(self) self.env_variables.hide() + self.accept_close = False + def show(self) -> None: super(Console, self).show() self.center_window() @@ -113,11 +117,26 @@ class Console(QDialog): def error(self, text, end: str = "\n"): self.console.error(text + end) + def on_process_exit(self, app_title: str, status: Union[int, str]): + self.error( + self.tr("Application \"{}\" finished with \"{}\"").format(app_title, status) + ) + self.accept_close = True + + def closeEvent(self, a0: QCloseEvent) -> None: + if self.accept_close: + super(Console, self).closeEvent(a0) + a0.accept() + else: + self.showMinimized() + a0.ignore() + class ConsoleEnv(QDialog): def __init__(self, parent=None): super(ConsoleEnv, self).__init__(parent=parent) + self.setAttribute(Qt.WA_DeleteOnClose, False) self.ui = Ui_ConsoleEnv() self.ui.setupUi(self) @@ -133,10 +152,13 @@ class ConsoleEnv(QDialog): class ConsoleEdit(QPlainTextEdit): + def __init__(self, parent=None): super(ConsoleEdit, self).__init__(parent=parent) self.setReadOnly(True) - self.setFont(QFont("monospace")) + font = QFont("Monospace") + font.setStyleHint(QFont.Monospace) + self.setFont(font) self._cursor_output = self.textCursor() def log(self, text): diff --git a/rare/game_launch_helper/lgd_helper.py b/rare/game_launch_helper/lgd_helper.py index 7e82960c..d6c40e4c 100644 --- a/rare/game_launch_helper/lgd_helper.py +++ b/rare/game_launch_helper/lgd_helper.py @@ -21,6 +21,7 @@ class GameArgsError(Exception): @dataclass class InitArgs: app_name: str + debug: bool = False offline: bool = False skip_version_check: bool = False wine_prefix: str = "" @@ -30,10 +31,11 @@ class InitArgs: def from_argparse(cls, args): return cls( app_name=args.app_name, + debug=args.debug, offline=args.offline, skip_version_check=args.skip_update_check, wine_bin=args.wine_bin, - wine_prefix=args.wine_pfx + wine_prefix=args.wine_pfx, ) diff --git a/rare/lgndr/core.py b/rare/lgndr/core.py index 5b4b32ac..c0f11efd 100644 --- a/rare/lgndr/core.py +++ b/rare/lgndr/core.py @@ -1,3 +1,4 @@ +from hashlib import sha1 from multiprocessing import Queue # On Windows the monkeypatching of `run_real` below doesn't work like on Linux @@ -27,6 +28,30 @@ class LegendaryCore(LegendaryCoreReal): # def get_installed_game(self, app_name, skip_sync=True) -> InstalledGame: # return super(LegendaryCore, self).get_installed_game(app_name, skip_sync) + # FIXME: delete this when legendary merges https://github.com/derrod/legendary/pull/477 + def get_cdn_manifest(self, game, platform='Windows', disable_https=False): + manifest_urls, base_urls, manifest_hash = self.get_cdn_urls(game, platform) + + if disable_https: + manifest_urls = [url.replace('https://', 'http://') for url in manifest_urls] + + manifest_bytes = None + for url in manifest_urls: + self.log.debug(f'Trying to download manifest from {url} ...') + r = self.egs.unauth_session.get(url) + if r.ok: + manifest_bytes = r.content + break + else: + self.log.warning(f'Unable to download manifest from {url}, trying next one ...') + if not manifest_bytes: + raise ValueError('Unable to get manifest data from any CDN URL') + + if sha1(manifest_bytes).hexdigest() != manifest_hash: + raise ValueError('Manifest sha hash mismatch!') + + return manifest_bytes, base_urls + def prepare_download(self, game: Game, base_game: Game = None, base_path: str = '', status_q: Queue = None, max_shm: int = 0, max_workers: int = 0, force: bool = False, disable_patching: bool = False, diff --git a/rare/utils/misc.py b/rare/utils/misc.py index 15c563be..44920744 100644 --- a/rare/utils/misc.py +++ b/rare/utils/misc.py @@ -189,6 +189,7 @@ def get_rare_executable() -> List[str]: else: executable = [sys.executable] + executable[0] = os.path.abspath(executable[0]) return executable @@ -222,7 +223,7 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link= f"Icon={os.path.join(resources_path, 'images', 'Rare.png')}\n" f"Exec={executable}\n" "Terminal=false\n" - "StartupWMClass=rare\n" + "StartupWMClass=Rare\n" ) else: with open(os.path.join(path, f"{igame.title}.desktop"), "w") as desktop_file: @@ -234,7 +235,7 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link= f"Icon={icon}.png\n" f"Exec={executable} launch {app_name}\n" "Terminal=false\n" - "StartupWMClass=rare-game\n" + "StartupWMClass=Rare\n" ) os.chmod(os.path.join(path, f"{igame.title}.desktop"), 0o755) diff --git a/rare/widgets/rare_app.py b/rare/widgets/rare_app.py index 7d1bed82..86097096 100644 --- a/rare/widgets/rare_app.py +++ b/rare/widgets/rare_app.py @@ -1,24 +1,24 @@ +import logging import os +import platform import sys +import time from argparse import Namespace -from logging import getLogger -from PyQt5.QtCore import Qt, QSettings, QTranslator +import legendary +from PyQt5.QtCore import Qt, QSettings, QTranslator, QT_VERSION_STR, PYQT_VERSION_STR from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication -# noinspection PyUnresolvedReferences -from legendary.core import LegendaryCore - import rare.resources.resources from rare.utils import paths from rare.utils.misc import set_color_pallete, set_style_sheet class RareApp(QApplication): - logger = getLogger("RareApp") + logger = logging.getLogger("RareApp") - def __init__(self, args: Namespace): + def __init__(self, args: Namespace, log_file: str): super(RareApp, self).__init__(sys.argv) self.setQuitOnLastWindowClosed(False) if hasattr(Qt, "AA_UseHighDpiPixmaps"): @@ -26,7 +26,53 @@ class RareApp(QApplication): self.setApplicationName("Rare") self.setOrganizationName("Rare") + + # Create directories after QStandardPaths has been initialized paths.create_dirs() + + # Clean any existing logging handlers from library imports + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + + start_time = time.strftime("%y-%m-%d--%H-%M") # year-month-day-hour-minute + file_handler = logging.FileHandler( + filename=os.path.join(paths.log_dir(), log_file.format(start_time)), + encoding="utf-8", + ) + file_handler.setFormatter(fmt=logging.Formatter("[%(name)s] %(levelname)s: %(message)s")) + + # Set up common logging channel to stderr + if args.debug: + logging.basicConfig( + format="[%(name)s] %(levelname)s: %(message)s", + level=logging.DEBUG, + stream=sys.stderr, + ) + file_handler.setLevel(logging.DEBUG) + logging.root.addHandler(file_handler) + logging.getLogger().setLevel(level=logging.DEBUG) + # keep requests, asyncio and pillow quiet + logging.getLogger("requests").setLevel(logging.WARNING) + logging.getLogger("urllib3").setLevel(logging.WARNING) + logging.getLogger("asyncio").setLevel(logging.WARNING) + self.logger.info( + f"Launching Rare version {rare.__version__} Codename: {rare.code_name}\n" + f" - Using Legendary {legendary.__version__} Codename: {legendary.__codename__} as backend\n" + f" - Operating System: {platform.system()}, Python version: {platform.python_version()}\n" + f" - Running {sys.executable} {' '.join(sys.argv)}\n" + f" - Qt version: {QT_VERSION_STR}, PyQt version: {PYQT_VERSION_STR}" + ) + else: + logging.basicConfig( + format="[%(name)s] %(levelname)s: %(message)s", + level=logging.INFO, + stream=sys.stderr, + ) + file_handler.setLevel(logging.DEBUG) + logging.root.addHandler(file_handler) + self.logger.info(f"Launching Rare version {rare.__version__}") + self.logger.info(f"Operating System: {platform.system()}") + self.settings = QSettings() # Translator diff --git a/requirements-full.txt b/requirements-full.txt new file mode 100644 index 00000000..7ee86ac0 --- /dev/null +++ b/requirements-full.txt @@ -0,0 +1,10 @@ +typing_extensions +requests +PyQt5 +QtAwesome +setuptools +legendary-gl +pywin32; platform_system == "Windows" +pywebview[qt]; platform_system == "Linux" +pywebview[cef]; platform_system == "Windows" +pypresence diff --git a/requirements-presence.txt b/requirements-presence.txt new file mode 100644 index 00000000..1d33e60b --- /dev/null +++ b/requirements-presence.txt @@ -0,0 +1,2 @@ +pypresence + diff --git a/requirements-webview.txt b/requirements-webview.txt new file mode 100644 index 00000000..1ee0e8e6 --- /dev/null +++ b/requirements-webview.txt @@ -0,0 +1,3 @@ +pywebview[qt]; platform_system == "Linux" +pywebview[cef]; platform_system == "Windows" +