import fcntl import os from pathlib import Path import subprocess import tempfile import time import unittest BIN = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin' class DesktopSteamLaunchTest(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) self.session = self.root / 'session' self.session.mkdir() self.env = dict(os.environ, HOME=str(self.root), XDG_DATA_HOME=str(self.root / 'data'), XDG_CACHE_HOME=str(self.root / 'cache'), BL_DESKTOP='1', BL_LAUNCH_DIR=str(self.session)) supervisor = self.root / 'supervisor' supervisor.write_text('#!/bin/bash\nprintf "%s\\n" "$BL_INSIDE" "$BL_STEAM_UI" "$DISPLAY" "$@"\n') supervisor.chmod(0o755) x64 = self.root / 'steam-x64' x64.write_text('#!/bin/bash\nprintf "%s\\n" x64 "$@"\n') x64.chmod(0o755) self.launcher = self.root / 'launch' self.launcher.write_text((BIN / 'droiddeck-steam-launch').read_text().replace( '/usr/local/bin/droiddeck-session', str(supervisor)).replace( '/usr/local/bin/droiddeck-steam-x64', str(x64))) def launch(self, *args): return subprocess.run(['bash', str(self.launcher), *args], env=self.env, text=True, capture_output=True) def test_desktop_keeps_display_and_does_not_request_session_switch(self): self.env['DISPLAY'] = ':7' result = self.launch('desktop') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines(), ['1', 'desktop', ':7', 'steam', 'steam://open/library']) self.assertFalse((self.session / 'steam-launch').exists()) def test_desktop_games_present_without_dri3(self): (self.root / 'supervisor').write_text('#!/bin/bash\nprintf "%s\\n" "$MESA_VK_WSI_DEBUG"\n') self.env.pop('MESA_VK_WSI_DEBUG', None) self.assertEqual(self.launch('desktop', '945360').stdout.splitlines(), ['sw']) self.env['MESA_VK_WSI_DEBUG'] = 'noshm' self.assertEqual(self.launch('desktop', '945360').stdout.splitlines(), ['noshm,sw']) self.env['MESA_VK_WSI_DEBUG'] = 'sw,noshm' self.assertEqual(self.launch('desktop', '945360').stdout.splitlines(), ['sw,noshm']) def test_desktop_game_url_goes_to_client(self): result = self.launch('desktop', '501590') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines()[-1], 'steam://rungameid/501590') self.assertFalse((self.session / 'steam-launch').exists()) def test_exit_without_a_running_client_starts_nothing(self): result = self.launch('desktop', 'steam://exit') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout, '') self.assertFalse((self.session / 'steam-launch').exists()) def test_shim_turns_shutdown_into_exit_url(self): launch = self.root / 'record-launch' launch.write_text('#!/bin/bash\nprintf "%s\\n" "$@"\n') launch.chmod(0o755) shim = self.root / 'shim' shim.write_text((BIN / 'droiddeck-steam-shim').read_text().replace( '/usr/local/bin/droiddeck-steam-launch', str(launch))) result = subprocess.run(['bash', str(shim), '-shutdown'], env=self.env, text=True, capture_output=True) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines(), ['desktop', 'steam://exit']) def test_second_click_forwards_without_another_supervisor(self): lock = self.root / 'cache/droiddeck/steam-desktop.lock' lock.parent.mkdir(parents=True) steam = self.root / 'data/Steam/steamrtarm64/steam' steam.parent.mkdir(parents=True) steam.write_text('#!/bin/bash\nprintf "%s\\n" "$@"\n') steam.chmod(0o755) (self.root / '.steam').mkdir() os.mkfifo(self.root / '.steam/steam.pipe') (self.root / '.steam/steam.pid').write_text(str(os.getpid())) with lock.open('w') as handle: fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) result = self.launch('desktop', 'steam://open/settings') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines(), ['-nobigpicture', '-no-cef-sandbox', '-cef-ozone-platform=x11', '-cef-disable-gpu', 'steam://open/settings']) self.assertFalse((self.session / 'steam-launch').exists()) def _running_client(self): steam = self.root / 'data/Steam/steamrtarm64/steam' steam.parent.mkdir(parents=True) steam.write_text('#!/bin/bash\nprintf "%s\\n" "$@"\n') steam.chmod(0o755) (self.root / '.steam').mkdir() os.mkfifo(self.root / '.steam/steam.pipe') (self.root / '.steam/steam.pid').write_text(str(os.getpid())) def test_bottom_screen_desktop_hands_games_to_the_running_client(self): self._running_client() self.env['BL_BOTTOM_DESKTOP'] = '1' result = self.launch('desktop', '501590') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines(), ['-nobigpicture', '-no-cef-sandbox', '-cef-ozone-platform=x11', '-cef-disable-gpu', 'steam://rungameid/501590']) self.assertFalse((self.root / 'cache/droiddeck/steam-desktop.lock').exists()) def test_bottom_screen_desktop_starts_its_own_client_when_none_runs(self): self.env.update(BL_BOTTOM_DESKTOP='1', DISPLAY=':7') result = self.launch('desktop') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines(), ['1', 'desktop', ':7', 'steam', 'steam://open/library']) def test_second_click_forwards_to_the_x86_64_client_in_its_own_home(self): lock = self.root / 'cache/droiddeck/steam-desktop.lock' lock.parent.mkdir(parents=True) x64_home = self.root / 'x64' (x64_home / '.steam').mkdir(parents=True) os.mkfifo(x64_home / '.steam/steam.pipe') (x64_home / '.steam/steam.pid').write_text(str(os.getpid())) self.env.update(BL_STEAM_CLIENT_ARCH='x86_64', BL_X64_HOME=str(x64_home)) with lock.open('w') as handle: fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) result = self.launch('desktop', '501590') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines(), ['x64', '--exec', '-nobigpicture', '-no-cef-sandbox', '-cef-ozone-platform=x11', '-cef-disable-gpu', 'steam://rungameid/501590']) def test_second_click_waits_for_steam_ipc_during_startup(self): lock = self.root / 'cache/droiddeck/steam-desktop.lock' lock.parent.mkdir(parents=True) steam = self.root / 'data/Steam/steamrtarm64/steam' steam.parent.mkdir(parents=True) marker = self.root / 'forwarded' steam.write_text(f'#!/bin/bash\ntouch "{marker}"\n') steam.chmod(0o755) with lock.open('w') as handle: fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) process = subprocess.Popen(['bash', str(self.launcher), 'desktop'], env=self.env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) try: time.sleep(0.7) self.assertIsNone(process.poll()) self.assertFalse(marker.exists()) (self.root / '.steam').mkdir() (self.root / '.steam/steam.pid').write_text(str(os.getpid())) os.mkfifo(self.root / '.steam/steam.pipe') _, stderr = process.communicate(timeout=3) self.assertEqual(process.returncode, 0, stderr) self.assertTrue(marker.exists()) finally: if process.poll() is None: process.kill() process.communicate() def test_big_picture_requests_explicit_handoff(self): result = self.launch('bigpicture') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual((self.session / 'steam-launch').read_text(), 'ui=bigpicture\nurl=\n') def test_outside_desktop_preserves_session_request(self): self.env.pop('BL_DESKTOP') result = self.launch('desktop', 'steam://open/settings') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual((self.session / 'steam-launch').read_text(), 'ui=desktop\nurl=steam://open/settings\n') def test_invalid_target_cannot_trigger_a_handoff(self): self.assertEqual(self.launch('desktop', 'bad').returncode, 64) self.assertFalse((self.session / 'steam-launch').exists()) def test_cef_software_is_scoped_to_the_kde_client(self): source = (BIN / 'droiddeck-session').read_text() setup = source[source.index(' _bl_ui=(-gamepadui)'):source.index(' if [ "${BL_STEAM_UI:-}" != desktop ]; then')] for desktop, ui, software in [('1', 'desktop', True), ('1', 'bigpicture', False), ('', 'desktop', False)]: with self.subTest(desktop=desktop, ui=ui): env = dict(self.env, BL_DESKTOP=desktop, BL_STEAM_UI=ui) result = subprocess.check_output(['bash', '-c', setup + '\nprintf "%s\\n" "${_bl_cef[@]}"'], env=env, text=True) self.assertEqual('-cef-disable-gpu' in result, software) self.assertEqual('-cef-use-angle=vulkan' in result, not software)