From e98af8b15951a4b63021e9648e4eddb2abf42f7c Mon Sep 17 00:00:00 2001 From: Jan200101 Date: Mon, 15 Nov 2021 11:30:31 +0100 Subject: [PATCH] Implement Linux support - disable building on Linux (TODO) - add winreg compatibility layer to allow reusing existing code without having to write an new system - replace ctypes usage available python replacement - gate windows logic behind os.name == "nt" checks (TODO compare how this behaves on cygwin and msys) - update hl2 linux executable name to use the script instead - replace hardcoded usage of "*.exe" with indexing of the executables list - --- .gitignore | 2 ++ TF2 Rich Presence/build.py | 15 ++++++--------- TF2 Rich Presence/configs.py | 14 +++++++++++--- TF2 Rich Presence/gui.py | 6 +++++- TF2 Rich Presence/localization.py | 3 +-- TF2 Rich Presence/logger.py | 10 +++++++--- TF2 Rich Presence/main.py | 10 +++++++--- TF2 Rich Presence/processes.py | 32 +++++++++++++++++-------------- TF2 Rich Presence/settings.py | 5 ++++- TF2 Rich Presence/utils.py | 8 ++++++-- requirements.txt | 3 ++- 11 files changed, 69 insertions(+), 39 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8d35cb32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.pyc diff --git a/TF2 Rich Presence/build.py b/TF2 Rich Presence/build.py index 94e65518..6f4b6a8b 100644 --- a/TF2 Rich Presence/build.py +++ b/TF2 Rich Presence/build.py @@ -277,10 +277,7 @@ def main(version_num='v2.1'): python_target = os.path.abspath(Path(f'{new_build_folder_name}/resources/{interpreter_name}')) print(f"Copying from {python_source}\n\tto {python_target}: ", end='') assert os.path.isdir(python_source) and not os.path.isdir(python_target) - if sys.platform == 'win32': - subprocess.run(f'xcopy \"{python_source}\" \"{python_target}\\\" /E /Q') - else: - raise SyntaxError("Whatever the Linux/MacOS equivalent of xcopy is") + subprocess.run(f'xcopy \"{python_source}\" \"{python_target}\\\" /E /Q') elif os.path.isfile(python_source_zip): python_target = os.path.abspath(Path(f'{new_build_folder_name}/resources')) print(f"Extracting from {python_source_zip}\n\tto {python_target}") @@ -474,10 +471,7 @@ def copy_dir(source, target): pass print(f"Copying from {source} to {target}: ", end='') - if sys.platform == 'win32': - subprocess.run(f'xcopy \"{source}\" \"{target}{os.path.sep}\" /E /Q') - else: - raise SyntaxError("Whatever the Linux/MacOS equivalent of xcopy is") + subprocess.run(f'xcopy \"{source}\" \"{target}{os.path.sep}\" /E /Q') # log all prints to a file @@ -499,4 +493,7 @@ def finish(self): if __name__ == '__main__': - main() + if os.name == 'nt': + main() + else: + print("Build is not available on non Windows systems") diff --git a/TF2 Rich Presence/configs.py b/TF2 Rich Presence/configs.py index 87602fde..5c5b2f66 100644 --- a/TF2 Rich Presence/configs.py +++ b/TF2 Rich Presence/configs.py @@ -3,7 +3,10 @@ # cython: language_level=3 import os -import winreg +try: + import winreg +except ImportError: + import unixreg as winreg from typing import Callable, List, Optional, Tuple, Union import vdf @@ -136,7 +139,7 @@ def steam_config_file(self, exe_location: str, require_condebug: bool) -> Option # given Steam's install, find a TF2 install def find_tf2_exe(self, steam_location: str) -> Optional[str]: - extend_path: Callable[[str], str] = lambda path: os.path.join(path, 'steamapps', 'common', 'Team Fortress 2', 'hl2.exe') + extend_path: Callable[[str], str] = lambda path: os.path.join(path, 'steamapps', 'common', 'Team Fortress 2', 'hl2.' + ('exe' if os.name == "nt" else 'sh')) default_path: str = extend_path(steam_location) if is_tf2_install(self.log, default_path): @@ -198,7 +201,12 @@ def is_tf2_install(log: logger.Log, exe_location: str) -> bool: # Steam seems to update this often enough def get_steam_username() -> str: key: winreg.HKEYType = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"SOFTWARE\\Valve\\Steam\\") - username: str = winreg.QueryValueEx(key, 'LastGameNameUsed')[0] + username: str + try: + username = winreg.QueryValueEx(key, 'LastGameNameUsed')[0] + except FileNotFoundError: + # TODO Jan: implement username detection for Linux systems + username = "" key.Close() return username diff --git a/TF2 Rich Presence/gui.py b/TF2 Rich Presence/gui.py index 86a038a9..9aae4eb4 100644 --- a/TF2 Rich Presence/gui.py +++ b/TF2 Rich Presence/gui.py @@ -397,9 +397,13 @@ def launch_tf2(self): self.launch_tf2_button['text'] = self.loc.text("Launching...") self.safe_update() + kwargs = {} + if os.name == "nt": + kwargs["creationflags"] = 0x08000000 + if self.tf2_launch_cmd: self.launched_tf2_with_button = True - subprocess.Popen(f'"{self.tf2_launch_cmd[0]}" {self.tf2_launch_cmd[1]} -game tf -steam -secure -condebug -conclearlog', creationflags=0x08000000) + subprocess.Popen(list(self.tf2_launch_cmd) + ["-game", "tf", "-steam", "-secure", "-condebug", "-conclearlog"], **kwargs) else: messagebox.showerror(self.loc.text("TF2 Rich Presence"), self.loc.text("Couldn't find a Team Fortress 2 installation.")) self.launch_tf2_button['state'] = 'normal' diff --git a/TF2 Rich Presence/localization.py b/TF2 Rich Presence/localization.py index 892c8ecc..7a503202 100644 --- a/TF2 Rich Presence/localization.py +++ b/TF2 Rich Presence/localization.py @@ -2,7 +2,6 @@ # https://github.com/Kataiser/tf2-rich-presence/blob/master/LICENSE # cython: language_level=3 -import ctypes import functools import json import locale @@ -119,7 +118,7 @@ def detect_system_language(log: logger.Log): if not db['has_asked_language']: language_codes: dict = {read_localization_files()[lang]['code']: lang for lang in langs[1:]} - system_locale: str = locale.windows_locale[ctypes.windll.kernel32.GetUserDefaultUILanguage()] + system_locale: str = locale.getlocale()[0] system_language_code: str = system_locale.split('_')[0] is_brazilian_port: bool = system_locale == 'pt_BR' diff --git a/TF2 Rich Presence/logger.py b/TF2 Rich Presence/logger.py index 6537020a..6368cef3 100644 --- a/TF2 Rich Presence/logger.py +++ b/TF2 Rich Presence/logger.py @@ -22,6 +22,11 @@ import utils +if os.name == "nt": + APPDATA = os.getenv('APPDATA') +else: + APPDATA = os.path.join(os.getenv("HOME"), ".config") + # TODO: replace this whole thing with a real logger class Log: def __init__(self, path: Optional[str] = None): @@ -30,11 +35,10 @@ def __init__(self, path: Optional[str] = None): self.logs_path: str = 'logs' created_logs_dir: bool = False else: - self.logs_path = os.path.join(os.getenv('APPDATA'), 'TF2 Rich Presence', 'logs') + self.logs_path = os.path.join(APPDATA, 'TF2 Rich Presence', 'logs') if not os.path.isdir(self.logs_path): - os.mkdir(os.path.join(os.getenv('APPDATA'), 'TF2 Rich Presence')) - os.mkdir(self.logs_path) + os.makedirs(self.logs_path) time.sleep(0.1) # ensure it gets created created_logs_dir = True else: diff --git a/TF2 Rich Presence/main.py b/TF2 Rich Presence/main.py index 732d744b..98989682 100644 --- a/TF2 Rich Presence/main.py +++ b/TF2 Rich Presence/main.py @@ -339,8 +339,9 @@ def loop_body(self): if not self.has_set_process_priority: self_process: psutil.Process = psutil.Process() priorities_before: tuple = (self_process.nice(), self_process.ionice()) - self_process.nice(psutil.BELOW_NORMAL_PRIORITY_CLASS) - self_process.ionice(psutil.IOPRIO_LOW) + if os.name == "nt": + self_process.nice(psutil.BELOW_NORMAL_PRIORITY_CLASS) + self_process.ionice(psutil.IOPRIO_LOW) priorities_after: tuple = (self_process.nice(), self_process.ionice()) self.log.debug(f"Set process priorities from {priorities_before} to {priorities_after}") self.has_set_process_priority = True @@ -478,4 +479,7 @@ def find_tf2_exe(self, *args, **kwargs) -> str: if __name__ == '__main__': - launch() + try: + launch() + except KeyboardInterrupt: + pass diff --git a/TF2 Rich Presence/processes.py b/TF2 Rich Presence/processes.py index 125b9431..4a656c08 100644 --- a/TF2 Rich Presence/processes.py +++ b/TF2 Rich Presence/processes.py @@ -23,7 +23,7 @@ def __init__(self, log: logger.Log): self.used_tasklist: bool = False self.tf2_without_condebug: bool = False self.parsed_tasklist: Dict[str, int] = {} - self.executables: Dict[str, list] = {'posix': ['hl2_linux', 'steam', 'Discord'], + self.executables: Dict[str, list] = {'posix': ['hl2.sh', 'steam', 'Discord'], 'nt': ['hl2.exe', 'steam.exe', 'discord'], 'order': ['TF2', 'Steam', 'Discord']} self.process_data: Dict[str, dict] = {'TF2': {'running': False, 'pid': None, 'path': None, 'time': None}, @@ -65,9 +65,9 @@ def scan_windows(self): if len(self.parsed_tasklist) == 3: self.all_pids_cached = True - self.process_data['TF2']['pid'] = self.parsed_tasklist['hl2.exe'] if 'hl2.exe' in self.parsed_tasklist else None - self.process_data['Steam']['pid'] = self.parsed_tasklist['steam.exe'] if 'steam.exe' in self.parsed_tasklist else None - self.process_data['Discord']['pid'] = self.parsed_tasklist['discord'] if 'discord' in self.parsed_tasklist else None + self.process_data['TF2']['pid'] = self.parsed_tasklist[self.executables[os.name][0]] if self.executables[os.name][0] in self.parsed_tasklist else None + self.process_data['Steam']['pid'] = self.parsed_tasklist[self.executables[os.name][1]] if self.executables[os.name][1] in self.parsed_tasklist else None + self.process_data['Discord']['pid'] = self.parsed_tasklist[self.executables[os.name][2]] if self.executables[os.name][2] in self.parsed_tasklist else None self.get_all_extended_info() else: @@ -88,6 +88,7 @@ def scan_posix(self): except psutil.NoSuchProcess: pass + self.get_all_extended_info() # get only the needed info (exe path and process start time) for each, and then apply it to self.p_data @@ -124,7 +125,7 @@ def get_process_info(self, process: Union[str, int], return_data: Tuple[str, ... try: process: psutil.Process = psutil.Process(pid=pid) - running: bool = [name for name in self.executables[os.name] if name in process.name().lower()] != [] + running: bool = [name for name in self.executables[os.name] if name in process.name()] != [] p_info['running'] = running if not running: @@ -138,7 +139,10 @@ def get_process_info(self, process: Union[str, int], return_data: Tuple[str, ... p_info['path'] = os.path.dirname(process.cwd()) + '/Steam' else: cmdline: List[str] = process.cmdline() - p_info['path'] = os.path.dirname(cmdline[0]) + try: + p_info['path'] = os.path.dirname(cmdline[0]) + except IndexError: + pass else: cmdline = process.cmdline() p_info['path'] = os.path.dirname(cmdline[0]) @@ -195,29 +199,29 @@ def parse_tasklist(self): for process_line in processes: process: list = process_line.split() - for ref_name in ('hl2.exe', 'Steam.exe', 'steam.exe', 'Discord'): + for ref_name in self.executables[os.name]: if ref_name in process[0]: try: self.parsed_tasklist[ref_name.lower()] = int(process[1]) except ValueError: self.log.error(f"Couldn't parse PID from process {process}") - self.process_data['TF2']['running'] = 'hl2.exe' in self.parsed_tasklist - self.process_data['Steam']['running'] = 'steam.exe' in self.parsed_tasklist - self.process_data['Discord']['running'] = 'discord' in self.parsed_tasklist + self.process_data['TF2']['running'] = self.executables[os.name][0] in self.parsed_tasklist + self.process_data['Steam']['running'] = self.executables[os.name][1] in self.parsed_tasklist + self.process_data['Discord']['running'] = self.executables[os.name][2] in self.parsed_tasklist # don't detect gmod (or any other program named hl2.exe) if self.process_data['TF2']['running']: - if not self.hl2_exe_is_tf2(self.parsed_tasklist['hl2.exe']): - self.log.debug(f"Found running non-TF2 hl2.exe with PID {self.parsed_tasklist['hl2.exe']}") + if not self.hl2_exe_is_tf2(self.parsed_tasklist[self.executables[os.name][0]]): + self.log.debug(f"Found running non-TF2 hl2.exe with PID {self.parsed_tasklist[self.executables[os.name][0]]}") self.process_data['TF2'] = copy.deepcopy(self.p_data_default['TF2']) - del self.parsed_tasklist['hl2.exe'] + del self.parsed_tasklist[self.executables[os.name][0]] # makes sure a process's path is a TF2 install, not some other game @functools.cache def hl2_exe_is_tf2(self, hl2_exe_pid: int) -> bool: hl2_exe_dir: str = self.get_process_info(hl2_exe_pid, ('path',))['path'] - return configs.is_tf2_install(self.log, os.path.join(hl2_exe_dir, 'hl2.exe')) + return configs.is_tf2_install(self.log, os.path.join(hl2_exe_dir, self.executables[os.name][0])) if __name__ == '__main__': diff --git a/TF2 Rich Presence/settings.py b/TF2 Rich Presence/settings.py index ddb053f1..a71e5745 100644 --- a/TF2 Rich Presence/settings.py +++ b/TF2 Rich Presence/settings.py @@ -4,7 +4,10 @@ import functools import json -import winreg +try: + import winreg +except ImportError: + import unixreg as winreg from typing import Optional, Union import logger diff --git a/TF2 Rich Presence/utils.py b/TF2 Rich Presence/utils.py index dba1cccc..1f7bb8b2 100644 --- a/TF2 Rich Presence/utils.py +++ b/TF2 Rich Presence/utils.py @@ -9,6 +9,10 @@ import os from typing import Dict, Optional, Union +if os.name == "nt": + APPDATA = os.getenv('APPDATA') +else: + APPDATA = os.path.join(os.getenv("HOME"), ".config") # read from or write to DB.json (intentionally uncached) def access_db(write: dict = None, pass_permission_error: bool = True) -> Optional[Dict[str, Union[bool, list, str]]]: @@ -50,8 +54,8 @@ def access_db(write: dict = None, pass_permission_error: bool = True) -> Optiona @functools.cache def db_json_path() -> str: - if os.path.isdir(os.path.join(os.getenv('APPDATA'), 'TF2 Rich Presence')): - return os.path.join(os.getenv('APPDATA'), 'TF2 Rich Presence', 'DB.json') + if os.path.isdir(os.path.join(APPDATA, 'TF2 Rich Presence')): + return os.path.join(APPDATA, 'TF2 Rich Presence', 'DB.json') else: return 'DB.json' diff --git a/requirements.txt b/requirements.txt index bb39ab2f..3dde288b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,5 @@ requests-futures==1.0.0 requests==2.26.0 sentry_sdk==1.4.3 urllib3==1.26.7 -vdf==3.4 \ No newline at end of file +vdf==3.4 +unixreg; sys_platform != 'win32' \ No newline at end of file