#!/usr/bin/env python3 import sys import os import re import shutil import subprocess import logging import enum import argparse import json import shlex import datetime import pathlib import socket logging.basicConfig(format='%(message)s', level=logging.DEBUG) logger = logging.getLogger(__name__) DEVKIT_TOOL_FOLDER = os.path.expanduser('~/devkit-game') STEAM_EXTRA_ARGS_FILE = os.path.expanduser('~/.config/systemd/user/steam.service.d/extra_args.conf') WIRELESS_DISABLE_POWER_MANAGEMENT = '/usr/bin/steamos-polkit-helpers/steamos-disable-wireless-power-management' # Must match in gui2.py class SteamStatus(enum.Enum): NOT_RUNNING = 0 ERROR = 1 OS = 2 OS_DEV = 3 SIDELOADED = 4 @classmethod def from_string(cls, status_str): if not status_str: return cls.ERROR try: if '.' in status_str: name = status_str.split('.')[-1] else: name = status_str return cls[name] except KeyError: return cls.ERROR @property def description(self): DESCRIPTIONS = { SteamStatus.NOT_RUNNING: 'not running', SteamStatus.OS: 'OS client', SteamStatus.OS_DEV: 'OS client dev mode', SteamStatus.SIDELOADED: 'sideloaded client', SteamStatus.ERROR: 'error', } return DESCRIPTIONS[self] class SteamConfig(enum.Enum): ERROR = 1 # Matching the SteamStatus numeric values OS = 2 OS_DEV = 3 SIDELOADED = 4 @property def description(self): DESCRIPTIONS = { SteamConfig.OS: 'OS client', SteamConfig.OS_DEV: 'OS client dev mode', SteamConfig.SIDELOADED: 'sideloaded client', SteamConfig.ERROR: 'error', } return DESCRIPTIONS[self] SESSION_NAMES = ['gamescope', 'plasma-x11', 'plasma-x11-persistent', 'plasma-wayland', 'plasma-wayland-persistent'] class SessionConfig(enum.IntEnum): # note: matches SESSION_NAMES indexes GAMESCOPE = 0 PLASMA_X11 = 1 PLASMA_X11_PERSISTENT = 2 PLASMA_WAYLAND = 3 PLASMA_WAYLAND_PERSISTENT = 4 ERROR = 5 def cef_debugging(): '''Only sane way to check is to look for the listening port.''' ret = subprocess.run('/usr/bin/ss -l -t -n -p | grep steamwebhelper | grep 8080 > /dev/null', shell=True) return ( ret.returncode == 0 ) def steam_process_get_path_and_args(): ret = subprocess.run(['pgrep', '-a', '-x', 'steam'], capture_output=True, text=True) if ret.returncode != 0: return None # Proton may run a dummy 'steam' process that confused previous implementations of this logic # look for a process who's real filename is 'steam' for l in ret.stdout.splitlines(): try: pid = int(l.split(' ')[0]) except: continue rp = os.path.realpath(f'/proc/{pid}/exe') if os.path.basename(rp) == 'steam': try: with open(f'/proc/{pid}/cmdline', 'rb') as f: cmdline = f.read() argv = [a.decode('utf-8', errors='replace') for a in cmdline.split(b'\x00') if a] if len(argv) >= 2: path = argv[0] args = argv[1:] # strip -srt-logger-opened: injected by steam.sh at runtime args = [a for a in args if a != '-srt-logger-opened'] return (path, args) return (argv[0], []) except Exception as e: logger.warning(f'Failed to read /proc/{pid}/cmdline: {e}') return None def steam_process_get_path(): try: (path, _) = steam_process_get_path_and_args() except: return None return path def steam_process_get_args(): try: (_, args) = steam_process_get_path_and_args() except: return '' return args def steam_status(): '''What is the status of the Steam client on the system?''' s = steam_process_get_path() if s is None: return SteamStatus.NOT_RUNNING if s.find('.local/share/Steam/') != -1: if os.path.exists(os.path.expanduser('~/devkit-game/devkit-steam')): return SteamStatus.OS_DEV return SteamStatus.OS if s.find('devkit-game/steam/') != -1 or s.find('devkit-game/steamdeckard/') != -1: return SteamStatus.SIDELOADED logger.warning(f'could not interpret pgrep result to determine steam client status: {s!r}') return SteamStatus.ERROR def steam_configuration(): '''How is the Steam client configured to run?''' devkit_steam_trampoline_path = os.path.join(DEVKIT_TOOL_FOLDER, 'devkit-steam') if not os.path.exists(devkit_steam_trampoline_path): return SteamConfig.OS t = open(devkit_steam_trampoline_path, 'rt').read() if t.find('SteamStatus.OS_DEV') != -1: return SteamConfig.OS_DEV if t.find('SteamStatus.SIDELOADED') != -1: return SteamConfig.SIDELOADED logger.warning(f'could not determine what {devkit_steam_trampoline_path} means to do') return SteamConfig.ERROR def osclient_branch(is_deckard): '''Which branch is the default Steam 'OS client' configured to use?''' # makes more sense to return strings here beta_path = os.path.expanduser('~/.steam/steam/package/beta') if not os.path.exists(beta_path): return 'default' # not sure that's valid actually - would be the desktop client, which will only run in desktop mode .. t = open(beta_path, 'rt').readline().strip('\n') try: p = 'steamdeck_(.*)' if re.match(p, t): branch = re.split(p, t)[1] return branch # internal builds p = 'steampal_(.*)_.*' if re.match(p, t): branch = re.split(p, t)[1] return branch if is_deckard: p = 'linux_arm64_(.*)_.*' if re.match(p, t): branch = re.split(p, t)[1] return branch raise Exception('no match') except: # noqa: E722 logger.warning(f'could not determine the OS client branch config: {t!r}') return 'error' def osclient_version(conf): '''Which version is the Steam 'OS client'?''' if conf.is_deckard: # old Steam client was using linuxarm64/, which is now reserved for the SDK binaries for folder in ('linuxarm64', 'steamrtarm64'): fn = os.path.expanduser(f'~/.steam/steam/{folder}/builddate.txt') if os.path.exists(fn): return open(fn, 'rt').read() return 'Unknown - no builddate.txt' beta_path = os.path.expanduser('~/.steam/steam/package/beta') if not os.path.exists(beta_path): logger.warning(f'not found: {beta_path}') return None t = open(beta_path, 'rt').readline().strip('\n') manifest = os.path.expanduser(f'~/.steam/steam/package/steam_client_{t}_ubuntu12.manifest') if not os.path.exists(manifest): logger.warning(f'not found: {manifest}') return None try: version = int(re.search('"version".*"(.*)"', open(manifest,'rt').read()).group(1)) return version except: logger.warning(f'could not parse version out of {manifest}') return None def session_config(): '''What is the graphics session configuration?''' # RESTART_SESSION writes this file conf_file = '/etc/sddm.conf.d/zz-steamos-autologin.conf' if not os.path.exists(conf_file): # fallback to the OS default conf_file = '/etc/sddm.conf.d/steamos.conf' if os.path.exists(conf_file): s = open(conf_file, 'rt').read() if s.find('plasmawayland.desktop') != -1: return SessionConfig.PLASMA_WAYLAND_PERSISTENT if s.find('plasma.desktop') != -1: return SessionConfig.PLASMA_X11_PERSISTENT if s.find('gamescope-wayland.desktop') != -1: return SessionConfig.GAMESCOPE if s.find('plasma-steamos-oneshot.desktop') != -1: return SessionConfig.PLASMA_X11 if s.find('plasma-steamos-wayland-oneshot.desktop') != -1: return SessionConfig.PLASMA_WAYLAND else: # if the conf file doesn't exist we are likely in the default config # check for a running gamescope for sanity if subprocess.call('pgrep -a -x gamescope', shell=True, stdout=subprocess.DEVNULL) == 0: return SessionConfig.GAMESCOPE # couldn't figure it out, halp return SessionConfig.ERROR def session_select_command(): if shutil.which('holo-session-select'): return 'holo-session-select' return 'steamos-session-select' def get_os_info(): os_info = {} try: for k, v in [ s.split('=') for s in open('/etc/os-release').read().split('\n') if len(s) > 0 ]: os_info[k] = v.strip('"') except Exception as e: logger.error(e) logger.error('Failed to parse OS release file') return os_info def steam_default_args(conf): if conf.is_deckard: # Frame currently uses a different setup return [] try: if os.path.exists('/usr/lib/steamos/steam-launcher'): output = subprocess.check_output('cat /usr/lib/steamos/steam-launcher | grep ^steamargs=', shell=True, universal_newlines=True) ret = [ v.strip('"') for v in re.findall('\".*?\"', output) ] return ret except: logger.warning('Failed to obtain steam default arguments from /usr/lib/steamos/steam-launcher') # Legacy SteamOS try: output = subprocess.check_output('cat /usr/bin/gamescope-session | grep ^steamargs', shell=True, universal_newlines=True) ret = [ v.strip('"') for v in re.findall('\".*?\"', output) ] except: logger.warning('Failed to obtain steam default arguments from /usr/bin/gamescope-session') # Hardcoded fallback return ['-steamos3', '-steampal', '-steamdeck', '-gamepadui'] def frame_osclient_extra_args(conf, steam_status): if not conf.is_deckard or steam_status != SteamStatus.OS: return None if os.path.exists(STEAM_EXTRA_ARGS_FILE): try: content = open(STEAM_EXTRA_ARGS_FILE, 'rt').read() match = re.search(r'Environment="STEAM_EXTRA_ARGS=(.*)"', content) if match: return match.group(1).replace('\\"', '"') except Exception as e: logger.warning(f'Failed to parse steam extra args: {e}') return None def user_password_is_set(): ret = subprocess.run('passwd', stdin=subprocess.DEVNULL, shell=True, capture_output=True, universal_newlines=True) logger.debug(repr(ret)) return (ret.stderr.find('Current password:') != -1) def steam_launch_flags(): '''Pull various steam flags that affect title execution.''' ret = {} if not 'XDG_RUNTIME_DIR' in os.environ: logger.warning('XDK_RUNTIME_DIR is not set') return ret env_folder = os.path.join(os.environ['XDG_RUNTIME_DIR'], 'steam/env') if not os.path.isdir(env_folder): return ret for fn in os.listdir(env_folder): filepath = os.path.join(env_folder, fn) content = open(filepath, 'rt').read() # Check if this is a declaration file with key=value pairs if content.count('\n') > 1 or '=' in content: # Parse key=value format with comments for line in content.splitlines(): line = line.strip() # Skip comments and empty lines if not line or line.startswith('#'): continue # Parse key=value pairs if '=' in line: key, value = line.split('=', 1) ret[key.strip()] = value.strip() else: # Legacy format: filename is the key, file content is the value ret[fn] = content.strip('\n') return ret def renderdoc_replay_server_running(): ret = subprocess.run(['pgrep', '-x', 'renderdoccmd'], capture_output=True) return ret.returncode == 0 if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--verbose', required=False, action='store_true') parser.add_argument('--json', required=False, action='store_true') conf = parser.parse_args() if conf.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) os_info = get_os_info() assert os_info is not None conf.is_deckard = os_info.get('VARIANT_ID', None) == 'vr' os_name = os_info.get('PRETTY_NAME', None) os_version = os_info.get('BUILD_ID', None) _steam_launch_flags = steam_launch_flags() if not conf.is_deckard: # this bit of cargo cult is Steam Deck only try: # disable wireless power management for devkit usage: less latency on commands subprocess.check_call(WIRELESS_DISABLE_POWER_MANAGEMENT) except subprocess.CalledProcessError as e: logger.warning(e) session_config = session_config() # enum -> human readable session_status = SESSION_NAMES[session_config] if session_config != SessionConfig.ERROR else 'error' steam_status = steam_status() cef_debugging_enabled = False if steam_status != SteamStatus.NOT_RUNNING: cef_debugging_enabled = cef_debugging() steam_configuration = steam_configuration() osclient_branch = osclient_branch(conf.is_deckard) osclient_version = osclient_version(conf) steam_status_description = steam_status.description if steam_status in (SteamStatus.OS, SteamStatus.OS_DEV) : steam_status_description += f', on branch {osclient_branch!r}' if osclient_version is not None: if conf.is_deckard: # we get builddate.txt steam_status_description += f', {osclient_version}' else: utc_date_string = datetime.datetime.fromtimestamp(osclient_version, datetime.UTC).isoformat() steam_status_description += f', version {osclient_version} {utc_date_string}' has_side_loaded_client = os.path.exists( os.path.join( DEVKIT_TOOL_FOLDER, 'steam' ) ) _user_password_is_set = user_password_is_set() _renderdoc_replay_server_running = renderdoc_replay_server_running() _renderdoc_layer_enabled = _steam_launch_flags.get('ENABLE_VULKAN_RENDERDOC_CAPTURE', '0') == '1' _hostname = socket.gethostname() if conf.json: ret = { 'is_deckard': conf.is_deckard, 'hostname': _hostname, 'os_name': os_name, 'os_version': os_version, 'os_info': os_info, 'session_status': session_status, 'session_options': SESSION_NAMES, 'session_select': session_select_command(), 'steam_status': str(steam_status), 'cef_debugging_enabled': cef_debugging_enabled, 'steam_status_description': steam_status_description, 'steam_configuration': str(steam_configuration), 'steam_osclient_branch': osclient_branch, 'steam_osclient_version': osclient_version, 'has_side_loaded_client': has_side_loaded_client, 'steam_default_args': steam_default_args(conf), 'steam_current_args': steam_process_get_args(), 'frame_osclient_extra_args': frame_osclient_extra_args(conf, steam_status), 'user_password_is_set': _user_password_is_set, 'steam_launch_flags': _steam_launch_flags, 'renderdoc_layer_enabled': _renderdoc_layer_enabled, 'renderdoc_replay_server_running': _renderdoc_replay_server_running, } json.dump(ret, sys.stdout, sort_keys=True, indent=4) else: logger.info(f'Hostname : {_hostname}') logger.info(f'OS : {os_name}') logger.info(f'OS version : {os_version}') logger.info(f'Session mode is : {session_status}') logger.info(f'Session select command : {session_select_command()}') logger.info(f'Steam client status : {steam_status_description}') logger.info(f'Steam client args : {steam_process_get_args()!r}') logger.info(f"Steam extra args (Frame) : {frame_osclient_extra_args(conf, steam_status)!r}") logger.info(f"Steam CEF debug : {'enabled' if cef_debugging_enabled else 'disabled'}") logger.info(f'Steam client config : {steam_configuration.description}') logger.info(f'Steam OS client branch : {osclient_branch}') logger.info(f'Steam OS client version : {osclient_version}') logger.info(f"Sideloaded client : {'available' if has_side_loaded_client else 'not installed'}") logger.info(f'OS client arguments : {steam_default_args(conf)!r}') logger.info(f"User password is set : {'yes' if _user_password_is_set else 'no'}") logger.info(f"Steam launch flags : {_steam_launch_flags}") logger.info(f"RenderDoc layer enabled : {'yes' if _renderdoc_layer_enabled else 'no'}") logger.info(f"RenderDoc replay running : {'yes' if _renderdoc_replay_server_running else 'no'}")