diff --git a/rare/__init__.py b/rare/__init__.py index bf4033ab..55431f1d 100644 --- a/rare/__init__.py +++ b/rare/__init__.py @@ -1,2 +1,10 @@ __version__ = "1.10.7" __codename__ = "Garlic Crab" + +# For PyCharm profiler +if __name__ == "__main__": + import sys + from argparse import Namespace + from rare import client + status = client.start(Namespace(debug=True, silent=False, offline=False, test_start=False)) + sys.exit(status) diff --git a/rare/__main__.py b/rare/__main__.py index d60a50c8..73c789a8 100755 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -1,114 +1,10 @@ -#!/usr/bin/env python3 -import multiprocessing import os import pathlib import sys -from argparse import ArgumentParser - - -def main(): - # fix cx_freeze - multiprocessing.freeze_support() - - # insert legendary for installed via pip/setup.py submodule to path - # if not __name__ == "__main__": - # sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary")) - - # CLI Options - parser = ArgumentParser() - parser.add_argument( - "-V", "--version", action="store_true", help="Shows version and exits" - ) - parser.add_argument( - "-S", - "--silent", - action="store_true", - help="Launch Rare in background. Open it from System Tray Icon", - ) - parser.add_argument("--debug", action="store_true", help="Launch in debug mode") - parser.add_argument( - "--offline", action="store_true", help="Launch Rare in offline mode" - ) - parser.add_argument( - "--test-start", action="store_true", help="Quit immediately after launch" - ) - - parser.add_argument( - "--desktop-shortcut", - action="store_true", - dest="desktop_shortcut", - help="Use this, if there is no link on desktop to start Rare", - ) - parser.add_argument( - "--startmenu-shortcut", - action="store_true", - dest="startmenu_shortcut", - help="Use this, if there is no link in start menu to launch Rare", - ) - subparsers = parser.add_subparsers(title="Commands", dest="subparser") - - launch_minimal_parser = subparsers.add_parser("start", aliases=["launch"]) - launch_minimal_parser.add_argument("app_name", help="AppName of the game to launch", - metavar="", action="store") - launch_minimal_parser.add_argument("--dry-run", help="Print arguments and exit", action="store_true") - launch_minimal_parser.add_argument("--offline", help="Launch game offline", - action="store_true") - launch_minimal_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='', - default=os.environ.get('LGDRY_WINE_BINARY', None), - help='Set WINE binary to use to launch the app') - launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='', - default=os.environ.get('LGDRY_WINE_PREFIX', None), - help='Set WINE prefix to use') - launch_minimal_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves", - action="store_true") - launch_minimal_parser.add_argument("--skip-update-check", help="Do not check for updates", - action="store_true") - - args = parser.parse_args() - - if args.desktop_shortcut or args.startmenu_shortcut: - from rare.utils.paths import create_desktop_link - - if args.desktop_shortcut: - create_desktop_link(app_name="rare_shortcut", link_type="desktop") - - if args.startmenu_shortcut: - create_desktop_link(app_name="rare_shortcut", link_type="start_menu") - - print("Link created") - return - - if args.version: - from rare import __version__, code_name - print(f"Rare {__version__} Codename: {code_name}") - return - - if args.subparser == "start" or args.subparser == "launch": - from rare import game_launch_helper as helper - helper.start_game(args) - return - - from rare.utils import singleton - - try: - # this object only allows one instance per machine - - me = singleton.SingleInstance() - except singleton.SingleInstanceException: - print("Rare is already running") - from rare.utils.paths import lock_file - - with open(lock_file(), "w") as file: - file.write("show") - file.close() - return - - from rare.app import start - - start(args) - if __name__ == "__main__": + from rare.main import main + # run from source # insert raw legendary submodule # sys.path.insert( @@ -130,4 +26,4 @@ if __name__ == "__main__": if sys.stderr is None: sys.stderr = open(os.devnull, 'w') - main() + sys.exit(main()) diff --git a/rare/app.py b/rare/app.py deleted file mode 100644 index 9d23b8f9..00000000 --- a/rare/app.py +++ /dev/null @@ -1,132 +0,0 @@ -import logging -import os -import shutil -import sys -import traceback -from argparse import Namespace -from datetime import datetime, timezone -from typing import Optional - -import requests.exceptions -from PyQt5.QtCore import QThreadPool, QTimer, pyqtSlot, Qt -from PyQt5.QtWidgets import QApplication, QMessageBox -from requests import HTTPError - -from rare.components.dialogs.launch_dialog import LaunchDialog -from rare.components.main_window import MainWindow -from rare.shared import RareCore -from rare.utils import config_helper, paths -from rare.widgets.rare_app import RareApp, RareAppException - -logger = logging.getLogger("Rare") - - -class RareException(RareAppException): - def __init__(self, parent=None): - super(RareException, self).__init__(parent=parent) - - def _handler(self, exc_type, exc_value, exc_tb) -> bool: - if exc_type == HTTPError: - try: - if RareCore.instance() is not None: - if RareCore.instance().core().login(): - return True - raise ValueError - except Exception as e: - logger.fatal(str(e)) - QMessageBox.warning(None, "Error", self.tr("Failed to login")) - QApplication.exit(1) - return False - - -class Rare(RareApp): - def __init__(self, args: Namespace): - log_file = "Rare_{0}.log" - super(Rare, self).__init__(args, log_file) - self._hook.deleteLater() - self._hook = RareException(self) - self.rcore = RareCore(args=args) - self.args = RareCore.instance().args() - self.signals = RareCore.instance().signals() - self.core = RareCore.instance().core() - - config_helper.init_config_handler(self.core) - - lang = self.settings.value("language", self.core.language_code, type=str) - self.load_translator(lang) - - # set Application name for settings - self.main_window: Optional[MainWindow] = None - self.launch_dialog: Optional[LaunchDialog] = None - self.timer: Optional[QTimer] = None - - # launch app - self.launch_dialog = LaunchDialog(parent=None) - self.launch_dialog.quit_app.connect(self.launch_dialog.close) - self.launch_dialog.quit_app.connect(lambda x: sys.exit(x)) - self.launch_dialog.start_app.connect(self.start_app) - self.launch_dialog.start_app.connect(self.launch_dialog.close) - - self.launch_dialog.login() - - def poke_timer(self): - dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]).replace(tzinfo=timezone.utc) - dt_now = datetime.utcnow().replace(tzinfo=timezone.utc) - td = abs(dt_exp - dt_now) - self.timer.start(int(td.total_seconds() - 60) * 1000) - logger.info(f"Renewed session expires at {self.core.lgd.userdata['expires_at']}") - - def re_login(self): - logger.info("Session expires shortly. Renew session") - try: - self.core.login() - except requests.exceptions.ConnectionError: - self.timer.start(3000) # try again if no connection - return - self.poke_timer() - - def start_app(self): - self.timer = QTimer() - self.timer.timeout.connect(self.re_login) - self.poke_timer() - - self.main_window = MainWindow() - self.main_window.exit_app.connect(self.on_exit_app) - - if not self.args.silent: - self.main_window.show() - - if self.args.test_start: - self.main_window.close() - self.main_window = None - self.on_exit_app(0) - - @pyqtSlot() - @pyqtSlot(int) - def on_exit_app(self, exit_code=0): - threadpool = QThreadPool.globalInstance() - threadpool.waitForDone() - if self.timer is not None: - self.timer.stop() - self.timer.deleteLater() - self.timer = None - self.rcore.deleteLater() - del self.rcore - self.processEvents() - shutil.rmtree(paths.tmp_dir()) - os.makedirs(paths.tmp_dir()) - - self.exit(exit_code) - - -def start(args): - while True: - QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) - QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) - app = Rare(args) - exit_code = app.exec_() - # if not restart - # restart app - del app - if exit_code != -133742: - break diff --git a/rare/components/__init__.py b/rare/components/__init__.py index e69de29b..f5af6d2d 100644 --- a/rare/components/__init__.py +++ b/rare/components/__init__.py @@ -0,0 +1,128 @@ +import os +import shutil +import sys +from argparse import Namespace +from datetime import datetime, timezone +from typing import Optional + +import requests.exceptions +from PyQt5.QtCore import QThreadPool, QTimer, pyqtSlot, Qt +from PyQt5.QtWidgets import QApplication, QMessageBox +from requests import HTTPError + +from rare.components.dialogs.launch_dialog import LaunchDialog +from rare.components.main_window import MainWindow +from rare.shared import RareCore +from rare.utils import config_helper, paths +from rare.widgets.rare_app import RareApp, RareAppException + + +class RareException(RareAppException): + def __init__(self, parent=None): + super(RareException, self).__init__(parent=parent) + + def _handler(self, exc_type, exc_value, exc_tb) -> bool: + if exc_type == HTTPError: + try: + if RareCore.instance() is not None: + if RareCore.instance().core().login(): + return True + raise ValueError + except Exception as e: + self.logger.fatal(str(e)) + QMessageBox.warning(None, "Error", self.tr("Failed to login")) + QApplication.exit(1) + return False + + +class Rare(RareApp): + def __init__(self, args: Namespace): + super(Rare, self).__init__(args, f"{type(self).__name__}_{{0}}.log") + self._hook.deleteLater() + self._hook = RareException(self) + self.rcore = RareCore(args=args) + self.args = RareCore.instance().args() + self.signals = RareCore.instance().signals() + self.core = RareCore.instance().core() + + config_helper.init_config_handler(self.core) + + lang = self.settings.value("language", self.core.language_code, type=str) + self.load_translator(lang) + + # set Application name for settings + self.main_window: Optional[MainWindow] = None + self.launch_dialog: Optional[LaunchDialog] = None + self.timer: Optional[QTimer] = None + + # launch app + self.launch_dialog = LaunchDialog(parent=None) + self.launch_dialog.quit_app.connect(self.launch_dialog.close) + self.launch_dialog.quit_app.connect(lambda x: sys.exit(x)) + self.launch_dialog.start_app.connect(self.start_app) + self.launch_dialog.start_app.connect(self.launch_dialog.close) + + self.launch_dialog.login() + + def poke_timer(self): + dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]).replace(tzinfo=timezone.utc) + dt_now = datetime.utcnow().replace(tzinfo=timezone.utc) + td = abs(dt_exp - dt_now) + self.timer.start(int(td.total_seconds() - 60) * 1000) + self.logger.info(f"Renewed session expires at {self.core.lgd.userdata['expires_at']}") + + def re_login(self): + self.logger.info("Session expires shortly. Renew session") + try: + self.core.login() + except requests.exceptions.ConnectionError: + self.timer.start(3000) # try again if no connection + return + self.poke_timer() + + def start_app(self): + self.timer = QTimer() + self.timer.timeout.connect(self.re_login) + self.poke_timer() + + self.main_window = MainWindow() + self.main_window.exit_app.connect(self.on_exit_app) + + if not self.args.silent: + self.main_window.show() + + if self.args.test_start: + self.main_window.close() + self.main_window = None + self.on_exit_app(0) + + @pyqtSlot() + @pyqtSlot(int) + def on_exit_app(self, exit_code=0): + threadpool = QThreadPool.globalInstance() + threadpool.waitForDone() + if self.timer is not None: + self.timer.stop() + self.timer.deleteLater() + self.timer = None + self.rcore.deleteLater() + del self.rcore + self.processEvents() + shutil.rmtree(paths.tmp_dir()) + os.makedirs(paths.tmp_dir()) + + self.exit(exit_code) + + +def start(args) -> int: + while True: + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + app = Rare(args) + exit_code = app.exec_() + # if not restart + # restart app + del app + if exit_code != -133742: + break + return exit_code diff --git a/rare/game_launch_helper/__init__.py b/rare/launcher/__init__.py similarity index 95% rename from rare/game_launch_helper/__init__.py rename to rare/launcher/__init__.py index 45c7f4d5..dae563cc 100644 --- a/rare/game_launch_helper/__init__.py +++ b/rare/launcher/__init__.py @@ -1,14 +1,12 @@ import json -import logging import platform import subprocess -import sys import time import traceback from argparse import Namespace from logging import getLogger from signal import signal, SIGINT, SIGTERM, strsignal -from typing import Union, Optional +from typing import Optional from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool, QSettings, Qt, pyqtSlot from PyQt5.QtGui import QDesktopServices @@ -24,8 +22,6 @@ from rare.widgets.rare_app import RareApp, RareAppException from .console import Console from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError -logger = logging.getLogger("RareLauncher") - DETACHED_APP_NAMES = [ "0a2d9f6403244d12969e11da6713137b" ] @@ -40,6 +36,7 @@ class PreLaunchThread(QRunnable): def __init__(self, core: LegendaryCore, args: InitArgs, rgame: RareGameSlim, sync_action=None): super(PreLaunchThread, self).__init__() + self.logger = getLogger(type(self).__name__) self.core = core self.signals = self.Signals() self.args = args @@ -47,13 +44,13 @@ class PreLaunchThread(QRunnable): self.sync_action = sync_action def run(self) -> None: - logger.info(f"Sync action: {self.sync_action}") + self.logger.info(f"Sync action: {self.sync_action}") if self.sync_action == CloudSaveDialog.UPLOAD: self.rgame.upload_saves(False) elif self.sync_action == CloudSaveDialog.DOWNLOAD: self.rgame.download_saves(False) else: - logger.info("No sync action") + self.logger.info("No sync action") args = self.prepare_launch(self.args) if not args: @@ -121,11 +118,9 @@ class RareLauncher(RareApp): exit_app = pyqtSignal() def __init__(self, args: InitArgs): - log_file = f"Rare_Launcher_{args.app_name}" + "_{0}.log" - super(RareLauncher, self).__init__(args, log_file) + super(RareLauncher, self).__init__(args, f"{type(self).__name__}_{args.app_name}_{{0}}.log") self._hook.deleteLater() self._hook = RareLauncherException(self, args, self) - self.logger = getLogger(f"Launcher_{args.app_name}") self.success: bool = True self.no_sync_on_exit = False @@ -280,7 +275,7 @@ class RareLauncher(RareApp): self.stop() return if self.args.dry_run: - logger.info("Dry run activated") + self.logger.info("Dry run activated") if self.console: self.console.log(f"{args.executable} {' '.join(args.args)}") self.console.log(f"Do not start {self.rgame.app_name}") @@ -338,7 +333,7 @@ class RareLauncher(RareApp): args.offline = True if not args.offline and self.rgame.auto_sync_saves: - logger.info("Start sync worker") + self.logger.info("Start sync worker") worker = SyncCheckWorker(self.core, self.rgame) worker.signals.error_occurred.connect(self.error_occurred) worker.signals.sync_state_ready.connect(self.sync_ready) @@ -355,7 +350,7 @@ class RareLauncher(RareApp): self.game_process.finished.disconnect() self.game_process.errorOccurred.disconnect() except TypeError as e: - logger.error(f"Failed to disconnect signals: {e}") + self.logger.error(f"Failed to disconnect signals: {e}") self.logger.info("Stopping server") try: self.server.close() @@ -369,7 +364,7 @@ class RareLauncher(RareApp): self.console.on_process_exit(self.rgame.app_name, 0) -def start_game(args: Namespace): +def launch(args: Namespace): args = InitArgs.from_argparse(args) QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) @@ -381,7 +376,7 @@ def start_game(args: Namespace): # This prevents ghost QLocalSockets, which block the name, which makes it unable to start # No handling for SIGKILL def sighandler(s, frame): - logger.info(f"{strsignal(s)} received. Stopping") + app.logger.info(f"{strsignal(s)} received. Stopping") app.stop() app.exit(1) signal(SIGINT, sighandler) @@ -392,4 +387,4 @@ def start_game(args: Namespace): app.start(args) # app.exit_app.connect(lambda: app.exit(0)) - sys.exit(app.exec_()) + return app.exec_() diff --git a/rare/game_launch_helper/console.py b/rare/launcher/console.py similarity index 100% rename from rare/game_launch_helper/console.py rename to rare/launcher/console.py diff --git a/rare/game_launch_helper/lgd_helper.py b/rare/launcher/lgd_helper.py similarity index 100% rename from rare/game_launch_helper/lgd_helper.py rename to rare/launcher/lgd_helper.py diff --git a/rare/main.py b/rare/main.py new file mode 100644 index 00000000..7d1c5dd1 --- /dev/null +++ b/rare/main.py @@ -0,0 +1,103 @@ +import multiprocessing +import os +from argparse import ArgumentParser + + +def main() -> int: + # fix cx_freeze + multiprocessing.freeze_support() + + # insert legendary for installed via pip/setup.py submodule to path + # if not __name__ == "__main__": + # sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary")) + + # CLI Options + parser = ArgumentParser() + parser.add_argument( + "-V", "--version", action="store_true", help="Shows version and exits" + ) + parser.add_argument( + "-S", + "--silent", + action="store_true", + help="Launch Rare in background. Open it from System Tray Icon", + ) + parser.add_argument("--debug", action="store_true", help="Launch in debug mode") + parser.add_argument( + "--offline", action="store_true", help="Launch Rare in offline mode" + ) + parser.add_argument( + "--test-start", action="store_true", help="Quit immediately after launch" + ) + + parser.add_argument( + "--desktop-shortcut", + action="store_true", + dest="desktop_shortcut", + help="Use this, if there is no link on desktop to start Rare", + ) + parser.add_argument( + "--startmenu-shortcut", + action="store_true", + dest="startmenu_shortcut", + help="Use this, if there is no link in start menu to launch Rare", + ) + subparsers = parser.add_subparsers(title="Commands", dest="subparser") + + launch_minimal_parser = subparsers.add_parser("start", aliases=["launch"]) + launch_minimal_parser.add_argument("app_name", help="AppName of the game to launch", + metavar="", action="store") + launch_minimal_parser.add_argument("--dry-run", help="Print arguments and exit", action="store_true") + launch_minimal_parser.add_argument("--offline", help="Launch game offline", + action="store_true") + launch_minimal_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='', + default=os.environ.get('LGDRY_WINE_BINARY', None), + help='Set WINE binary to use to launch the app') + launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='', + default=os.environ.get('LGDRY_WINE_PREFIX', None), + help='Set WINE prefix to use') + launch_minimal_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves", + action="store_true") + launch_minimal_parser.add_argument("--skip-update-check", help="Do not check for updates", + action="store_true") + + args = parser.parse_args() + + if args.desktop_shortcut or args.startmenu_shortcut: + from rare.utils.paths import create_desktop_link + + if args.desktop_shortcut: + create_desktop_link(app_name="rare_shortcut", link_type="desktop") + + if args.startmenu_shortcut: + create_desktop_link(app_name="rare_shortcut", link_type="start_menu") + + print("Link created") + return 0 + + if args.version: + from rare import __version__, __codename__ + print(f"Rare {__version__} Codename: {__codename__}") + return 0 + + if args.subparser == "start" or args.subparser == "launch": + from rare.launcher import launch + return launch(args) + + from rare.utils import singleton + + try: + # this object only allows one instance per machine + + me = singleton.SingleInstance() + except singleton.SingleInstanceException: + print("Rare is already running") + from rare.utils.paths import lock_file + + with open(lock_file(), "w") as file: + file.write("show") + file.close() + return -1 + + from rare.components import start + return start(args) diff --git a/rare/models/game.py b/rare/models/game.py index ea702810..3d54eeef 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -550,7 +550,7 @@ class RareGame(RareGameSlim): cmd_line = get_rare_executable() executable, args = cmd_line[0], cmd_line[1:] - args.extend(["start", self.app_name]) + args.extend(["launch", self.app_name]) if offline: args.append("--offline") if skip_update_check: diff --git a/rare/utils/paths.py b/rare/utils/paths.py index 5685e92f..a6593623 100644 --- a/rare/utils/paths.py +++ b/rare/utils/paths.py @@ -129,6 +129,7 @@ def desktop_link_path(link_name: str, link_type: str) -> Path: def get_rare_executable() -> List[str]: + logger.debug(f"Trying to find executable: {sys.executable}, {sys.argv}") # lk: detect if nuitka if "__compiled__" in globals(): executable = [sys.executable] @@ -141,7 +142,7 @@ def get_rare_executable() -> List[str]: if sys.executable == os.path.abspath(sys.argv[0]): executable = [sys.executable] else: - executable = [os.path.abspath(sys.argv[0])] + executable = [sys.executable, os.path.abspath(sys.argv[0])] elif platform.system() == "Windows": executable = [sys.executable] diff --git a/rare/widgets/rare_app.py b/rare/widgets/rare_app.py index e64aead5..7a731958 100644 --- a/rare/widgets/rare_app.py +++ b/rare/widgets/rare_app.py @@ -20,8 +20,8 @@ class RareAppException(QObject): exception = pyqtSignal(object, object, object) def __init__(self, parent=None): - self.logger = logging.getLogger(type(self).__name__) super(RareAppException, self).__init__(parent=parent) + self.logger = logging.getLogger(type(self).__name__) sys.excepthook = self._excepthook self.exception.connect(self._on_exception) @@ -37,14 +37,19 @@ class RareAppException(QObject): if self._handler(exc_type, exc_value, exc_tb): return self.logger.fatal(message) - QMessageBox.warning(None, exc_type.__name__, message) - QApplication.exit(1) + action = QMessageBox.warning( + None, exc_type.__name__, message, + buttons=QMessageBox.Ignore | QMessageBox.Close, + defaultButton=QMessageBox.Ignore + ) + if action == QMessageBox.RejectRole: + QApplication.exit(1) class RareApp(QApplication): def __init__(self, args: Namespace, log_file: str): - self.logger = logging.getLogger(type(self).__name__) super(RareApp, self).__init__(sys.argv) + self.logger = logging.getLogger(type(self).__name__) self._hook = RareAppException(self) self.setQuitOnLastWindowClosed(False) self.setAttribute(Qt.AA_DontUseNativeDialogs, True) diff --git a/setup.py b/setup.py index 7a769de8..4fff0459 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,10 @@ setuptools.setup( ], include_package_data=True, python_requires=">=3.9", - entry_points=dict(console_scripts=["rare=rare.__main__:main"]), + entry_points={ + # 'console_scripts': ["rare = rare.main:main"], + 'gui_scripts': ["rare = rare.main:main"], + }, install_requires=requirements, extras_require=optional_reqs, )