mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
fix(launcher): fail closed on automatic mode edge cases
- restrict preference reads to the lol-user-experience section; - distinguish a failed close-preference read from a missing key; - reject xprop's 'not found' text as a WM_CLASS value; - track a window before unmap so an interrupting signal still restores it; - report the tricks-mode opt-out in the hide helper's startup message; - cover the new cases with preference, window and interruption tests.
This commit is contained in:
1 parent
8de6bceeae
commit
9bcd41e12f
4 files changed
+107
-21
No files matched your search
@@ -51,12 +51,30 @@ class ClientInGameTricksTests(unittest.TestCase):
|
||||
|
||||
def test_last_duplicate_value_wins_and_quotes_are_accepted(self):
|
||||
self.path.write_text(
|
||||
'potatoModeEnabled: false\n'
|
||||
'closeLeagueClientDuringGame: false\n'
|
||||
'potatoModeEnabled: "true"\n'
|
||||
'closeLeagueClientDuringGame: "true"\n')
|
||||
'lol-user-experience:\n'
|
||||
' data:\n'
|
||||
' potatoModeEnabled: false\n'
|
||||
' closeLeagueClientDuringGame: false\n'
|
||||
' potatoModeEnabled: "true"\n'
|
||||
' closeLeagueClientDuringGame: "true"\n')
|
||||
self.assertEqual(tricks.mode(self.path), 'close')
|
||||
|
||||
def test_keys_outside_the_named_section_are_ignored(self):
|
||||
self.path.write_text(
|
||||
'lol-user-experience:\n'
|
||||
' data:\n'
|
||||
' potatoModeEnabled: true\n'
|
||||
'other-section:\n'
|
||||
' potatoModeEnabled: true\n'
|
||||
' closeLeagueClientDuringGame: true\n')
|
||||
self.assertEqual(tricks.mode(self.path), 'hide')
|
||||
|
||||
def test_second_read_failure_fails_closed(self):
|
||||
self.write(True, False)
|
||||
text = self.path.read_text()
|
||||
with patch.object(Path, 'read_text', side_effect=[text, OSError()]):
|
||||
self.assertEqual(tricks.mode(self.path), 'none')
|
||||
|
||||
def test_main_prints_mode(self):
|
||||
self.write(True, False)
|
||||
output = io.StringIO()
|
||||
|
||||
@@ -156,6 +156,49 @@ class HideClientInGameTests(unittest.TestCase):
|
||||
self.assertEqual(hide_in_game.target_windows(self.prefix, cache), [0x10])
|
||||
self.assertEqual(classes.call_count, 1)
|
||||
|
||||
def test_missing_wm_class_is_not_cached_as_a_class(self):
|
||||
with patch.object(hide_in_game, 'xprop_value',
|
||||
return_value='WM_CLASS: not found.'):
|
||||
self.assertEqual(hide_in_game.window_class('0x10'), '')
|
||||
|
||||
def test_unmap_failure_removes_the_window_from_hidden(self):
|
||||
class FailingX11(FakeX11):
|
||||
def unmap(self, window):
|
||||
raise RuntimeError('unmap failed')
|
||||
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, FailingX11(),
|
||||
window_source=lambda _prefix, _cache: [0xabc],
|
||||
state_source=lambda _prefix: True,
|
||||
sleep=lambda _seconds: None,
|
||||
out=io.StringIO(),
|
||||
)
|
||||
watcher._hide_new()
|
||||
self.assertEqual(watcher.hidden, set())
|
||||
|
||||
def test_interrupted_unmap_keeps_the_window_tracked(self):
|
||||
shown = []
|
||||
|
||||
class InterruptingX11:
|
||||
def unmap(self, window):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
def show(self, window):
|
||||
shown.append(window)
|
||||
|
||||
watcher = hide_in_game.Watcher(
|
||||
self.prefix, InterruptingX11(),
|
||||
window_source=lambda _prefix, _cache: [0xabc],
|
||||
state_source=lambda _prefix: True,
|
||||
sleep=lambda _seconds: None,
|
||||
out=io.StringIO(),
|
||||
)
|
||||
with self.assertRaises(KeyboardInterrupt):
|
||||
watcher._hide_new()
|
||||
self.assertEqual(watcher.hidden, {0xabc})
|
||||
watcher.restore()
|
||||
self.assertEqual(shown, [0xabc])
|
||||
|
||||
def test_hides_then_shows_on_match_end(self):
|
||||
states = iter([True, True, False])
|
||||
x11 = FakeX11()
|
||||
|
||||
@@ -7,32 +7,45 @@ The public launcher switch is RCLOL_CLIENT_IN_GAME_TRICKS=1. It reads
|
||||
- potatoModeEnabled must be true (client optimizations enabled);
|
||||
- closeLeagueClientDuringGame true -> close the client UI while a match runs;
|
||||
- closeLeagueClientDuringGame false -> hide the client windows instead;
|
||||
- missing/unreadable preferences -> no action (none).
|
||||
- missing preferences or an unreadable file -> no action (none).
|
||||
|
||||
This helper only selects a mode. It does not change preferences or files.
|
||||
Only the named `lol-user-experience` section is considered, so identical keys
|
||||
in another section cannot select a helper. This helper only selects a mode; it
|
||||
does not change preferences or files.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SECTION = 'lol-user-experience'
|
||||
CLOSE_KEY = 'closeLeagueClientDuringGame'
|
||||
OPTIMIZATION_KEY = 'potatoModeEnabled'
|
||||
TRUE_VALUES = {'true', 'yes', '1'}
|
||||
FALSE_VALUES = {'false', 'no', '0'}
|
||||
|
||||
|
||||
def read_bool(path, key):
|
||||
"""Return True/False for the last value, or None when absent/unreadable."""
|
||||
try:
|
||||
lines = Path(path).read_text().splitlines()
|
||||
except (OSError, UnicodeError):
|
||||
return None
|
||||
def read_bool(path, key, section=SECTION):
|
||||
"""Return True/False for the last key in section, or None when absent.
|
||||
|
||||
OSError/UnicodeError is raised for an unreadable file so callers can fail
|
||||
closed instead of treating a read race as a missing preference.
|
||||
"""
|
||||
lines = Path(path).read_text().splitlines()
|
||||
in_section = False
|
||||
value = None
|
||||
for line in lines:
|
||||
match = re.match(r'^\s*' + re.escape(key) + r':\s*(\S+)', line)
|
||||
if match:
|
||||
value = match.group(1).strip('"\'').lower()
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith('#'):
|
||||
continue
|
||||
if not line.startswith((' ', '\t')):
|
||||
if stripped == section + ':':
|
||||
in_section = True
|
||||
elif in_section:
|
||||
break
|
||||
continue
|
||||
if not in_section or not stripped.startswith(key + ':'):
|
||||
continue
|
||||
value = stripped.split(':', 1)[1].strip().strip('"\'').lower()
|
||||
if value in TRUE_VALUES:
|
||||
return True
|
||||
if value in FALSE_VALUES:
|
||||
@@ -41,9 +54,17 @@ def read_bool(path, key):
|
||||
|
||||
|
||||
def mode(path):
|
||||
if read_bool(path, OPTIMIZATION_KEY) is not True:
|
||||
try:
|
||||
optimization = read_bool(path, OPTIMIZATION_KEY)
|
||||
except (OSError, UnicodeError):
|
||||
return 'none'
|
||||
return 'close' if read_bool(path, CLOSE_KEY) is True else 'hide'
|
||||
if optimization is not True:
|
||||
return 'none'
|
||||
try:
|
||||
close = read_bool(path, CLOSE_KEY)
|
||||
except (OSError, UnicodeError):
|
||||
return 'none'
|
||||
return 'close' if close is True else 'hide'
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
||||
@@ -129,7 +129,8 @@ def xprop_value(window, prop):
|
||||
|
||||
|
||||
def window_class(window):
|
||||
return xprop_value(window, 'WM_CLASS').lower()
|
||||
value = xprop_value(window, 'WM_CLASS').lower()
|
||||
return value if '=' in value else ''
|
||||
|
||||
|
||||
def window_pid(window):
|
||||
@@ -295,11 +296,12 @@ class Watcher:
|
||||
for window in windows:
|
||||
if window in self.hidden:
|
||||
continue
|
||||
self.hidden.add(window)
|
||||
try:
|
||||
self.x11.unmap(window)
|
||||
self.hidden.add(window)
|
||||
print(f'hide-league-client-in-game: hidden 0x{window:x}', file=self.out, flush=True)
|
||||
except Exception as error:
|
||||
self.hidden.discard(window)
|
||||
print(f'hide-league-client-in-game: cannot hide 0x{window:x}: {error}',
|
||||
file=sys.stderr, flush=True)
|
||||
|
||||
@@ -372,8 +374,10 @@ def main(argv=None):
|
||||
|
||||
for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
||||
signal.signal(signum, stop)
|
||||
optout = ('RCLOL_CLIENT_IN_GAME_TRICKS=0' if tricks
|
||||
else 'RCLOL_HIDE_CLIENT_IN_GAME=0')
|
||||
print('hide-league-client-in-game: watching %s for a running match; '
|
||||
'opt out with RCLOL_HIDE_CLIENT_IN_GAME=0' % prefix, flush=True)
|
||||
'opt out with %s' % (prefix, optout), flush=True)
|
||||
try:
|
||||
watcher.run()
|
||||
except KeyboardInterrupt:
|
||||
|
||||
Reference in new issue
Block a user