mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
feat(launcher): hide the League client UI while a match runs
Add an opt-in alternative to closing LeagueClientUx: hide the managed LeagueClientUx and Riot Client X11 windows while the game process runs and show them again when it exits. The client stays open; the game window is never touched. Detection is attributed to the launcher prefix through __mldr_sockpath and fails closed when proc, ownership or window state is unreadable. The helper requires X11/Xwayland, restores the window on end, signal or error, and is mutually exclusive with RCLOL_CLOSE_CLIENT_IN_GAME=1. Document the flag, compliance scope and limits. A local 30-second test on an i5-3470/RX 550 reduced summed client/CEF CPU from 120.7% to 93.4%; this is a CPU-bound-host measurement, not a universal FPS claim. No client binaries, CEF, assets or anti-cheat are modified.
This commit is contained in:
1 parent
a4da79011f
commit
415aa7a949
7 files changed
+537
No files matched your search
@@ -31,3 +31,4 @@ jobs:
|
|||||||
- run: python -m unittest discover -s tests -p 'test_metal_device_info.py'
|
- run: python -m unittest discover -s tests -p 'test_metal_device_info.py'
|
||||||
- run: python -m unittest discover -s tests -p 'test_exec_guest.py'
|
- run: python -m unittest discover -s tests -p 'test_exec_guest.py'
|
||||||
- run: python -m unittest discover -s tests -p 'test_close_league_client_in_game.py'
|
- run: python -m unittest discover -s tests -p 'test_close_league_client_in_game.py'
|
||||||
|
- run: python -m unittest discover -s tests -p 'test_hide_league_client_in_game.py'
|
||||||
@@ -109,6 +109,14 @@ First installation and future Riot updates can still encounter compatibility gap
|
|||||||
|
|
||||||
For another region, set `RCLOL_REGION` and `RCLOL_LOCALE` before launch. For example `RCLOL_REGION=NA RCLOL_LOCALE=en_US ./scripts/run.sh`. Linux display selection follows `$DISPLAY`; the legacy launcher fallback is `:1`.
|
For another region, set `RCLOL_REGION` and `RCLOL_LOCALE` before launch. For example `RCLOL_REGION=NA RCLOL_LOCALE=en_US ./scripts/run.sh`. Linux display selection follows `$DISPLAY`; the legacy launcher fallback is `:1`.
|
||||||
|
|
||||||
|
If you want to keep the client open during matches, opt in to hiding its window instead of closing it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
RCLOL_HIDE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper detects the match, hides the `LeagueClientUx`/`Riot Client` X11 windows, and restores them when the game ends. It does not close the client, patch CEF or touch the game window. It is off by default, requires X11/Xwayland, and `RCLOL_CLOSE_CLIENT_IN_GAME=1` takes precedence. See [troubleshooting](docs/TROUBLESHOOTING.md#keep-the-client-open-but-hide-it-during-a-match) for the measured CPU effect and caveats.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Start with standalone regression checks, then play a match manually. See [testing](docs/TESTING.md). Do not attach a debugger, query client APIs, inject diagnostic hooks or automate client/game input. This project ships no client-side automation, inspection or profiling helpers.
|
Start with standalone regression checks, then play a match manually. See [testing](docs/TESTING.md). Do not attach a debugger, query client APIs, inject diagnostic hooks or automate client/game input. This project ships no client-side automation, inspection or profiling helpers.
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ opts in to Hyprland-only recovery of a default-prefix Riot session with no
|
|||||||
visible window; it is off by default. See [troubleshooting](TROUBLESHOOTING.md#riot-exits-and-league-reports-that-it-lost-its-connection)
|
visible window; it is off by default. See [troubleshooting](TROUBLESHOOTING.md#riot-exits-and-league-reports-that-it-lost-its-connection)
|
||||||
for the observed failures, recovery guards and NSURLSession limitations.
|
for the observed failures, recovery guards and NSURLSession limitations.
|
||||||
|
|
||||||
|
`RCLOL_HIDE_CLIENT_IN_GAME=1` opts in to hiding the managed `LeagueClientUx`
|
||||||
|
and `Riot Client` X11 windows while a match runs, then showing them when the
|
||||||
|
game process ends. It keeps the client open and is mutually exclusive with
|
||||||
|
`RCLOL_CLOSE_CLIENT_IN_GAME=1` (close takes precedence). It is off by default,
|
||||||
|
requires X11/Xwayland, and does not patch client binaries, CEF or the game. See
|
||||||
|
[the troubleshooting measurement](TROUBLESHOOTING.md#keep-the-client-open-but-hide-it-during-a-match)
|
||||||
|
for the measured CPU effect and limitations.
|
||||||
|
|
||||||
`RCLOL_FILTER_HOST_ENV=1` opts in to removing host desktop-session variables before
|
`RCLOL_FILTER_HOST_ENV=1` opts in to removing host desktop-session variables before
|
||||||
the guest starts: `WAYLAND_DISPLAY`, `XDG_SESSION_*`, `XDG_CURRENT_DESKTOP`,
|
the guest starts: `WAYLAND_DISPLAY`, `XDG_SESSION_*`, `XDG_CURRENT_DESKTOP`,
|
||||||
GNOME/KDE/GDM, systemd, SSH agent, terminal, toolkit (`QT_*`, `GTK_*`, `GDK_*`),
|
GNOME/KDE/GDM, systemd, SSH agent, terminal, toolkit (`QT_*`, `GTK_*`, `GDK_*`),
|
||||||
|
|||||||
@@ -192,6 +192,55 @@ observation, without waiting for a match:
|
|||||||
python3 tools/close-league-client-in-game.py --once --dry-run
|
python3 tools/close-league-client-in-game.py --once --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Keep the client open but hide it during a match
|
||||||
|
|
||||||
|
`LeagueClientUx` can consume a noticeable amount of CPU while the client stays
|
||||||
|
open during a match. An opt-in alternative to closing it hides the managed
|
||||||
|
client window instead:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
RCLOL_HIDE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The launcher detects when a match starts, unmaps the `LeagueClientUx` and
|
||||||
|
`Riot Client` X11 windows, and maps them again when the game process ends. The
|
||||||
|
client is not closed, the League backend and Riot client keep running, and the
|
||||||
|
game window is never hidden. `RCLOL_CLOSE_CLIENT_IN_GAME=1` takes precedence
|
||||||
|
when both flags are set.
|
||||||
|
|
||||||
|
The helper is off by default and window-management only: it does not patch
|
||||||
|
client binaries, CEF, anti-cheat or DRM. It attributes the game to this prefix
|
||||||
|
through the loader's `__mldr_sockpath`, and fails closed when ownership, proc
|
||||||
|
state, environment or window information cannot be read. It requires an X11 or
|
||||||
|
Xwayland desktop; on a native Wayland window manager without Xwayland it cannot
|
||||||
|
find the windows.
|
||||||
|
|
||||||
|
On a CPU-bound host the effect appears after CEF has been hidden for a few
|
||||||
|
seconds. In a local 30-second test on an i5-3470 with a Radeon RX 550, hiding
|
||||||
|
`LeagueClientUx` reduced the summed CPU of the matched client/CEF processes from
|
||||||
|
120.7% to 93.4% (about 23%, or 27 percentage points, of one core); showing the
|
||||||
|
window returned it to 119.9%. This is not a universal FPS or percentage claim:
|
||||||
|
a game that is GPU-bound, a client with no active UI work, or a different
|
||||||
|
desktop may show less benefit. The hidden window can also need a moment to
|
||||||
|
repaint after it is shown.
|
||||||
|
|
||||||
|
`RCLOL_CLOSE_CLIENT_IN_GAME=1` remains the more aggressive option when in-client
|
||||||
|
chat during the match is not needed. Configure one of the two, not both:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Keep the client open, hide its windows while playing
|
||||||
|
RCLOL_HIDE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||||
|
|
||||||
|
# Or close the client UI while playing
|
||||||
|
RCLOL_CLOSE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Diagnose the match detector without changing any window:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/hide-league-client-in-game.py --once
|
||||||
|
```
|
||||||
|
|
||||||
### Riot reports NO INTERNET FOUND while the host is online
|
### Riot reports NO INTERNET FOUND while the host is online
|
||||||
|
|
||||||
Older base runtimes ship Google DNS servers (`8.8.8.8` and `8.8.4.4`), which
|
Older base runtimes ship Google DNS servers (`8.8.8.8` and `8.8.4.4`), which
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
"""Exercise the in-match client window hide with fake /proc entries and X11."""
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
'hide_in_game', ROOT / 'tools/hide-league-client-in-game.py')
|
||||||
|
hide_in_game = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(hide_in_game)
|
||||||
|
|
||||||
|
GAME = '/Applications/League of Legends.app/Contents/LoL/LeagueofLegends'
|
||||||
|
MAIN = '/Applications/League of Legends.app/Contents/LoL/LeagueClientUx.app/Contents/MacOS/LeagueClientUx'
|
||||||
|
RIOT = '/Applications/Riot Client.app/Contents/MacOS/RiotClientServices'
|
||||||
|
|
||||||
|
|
||||||
|
class FakeX11:
|
||||||
|
def __init__(self):
|
||||||
|
self.unmapped = []
|
||||||
|
self.shown = []
|
||||||
|
|
||||||
|
def unmap(self, window):
|
||||||
|
self.unmapped.append(window)
|
||||||
|
|
||||||
|
def show(self, window):
|
||||||
|
self.shown.append(window)
|
||||||
|
|
||||||
|
|
||||||
|
class HideClientInGameTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.root = Path(self.temp.name)
|
||||||
|
self.proc = self.root / 'proc'
|
||||||
|
self.proc.mkdir()
|
||||||
|
self.prefix = self.root / '.darling'
|
||||||
|
self.env_patch = patch.dict(os.environ, {'DPREFIX': str(self.prefix)})
|
||||||
|
self.env_patch.start()
|
||||||
|
self.addCleanup(self.env_patch.stop)
|
||||||
|
|
||||||
|
def environment(self, prefix):
|
||||||
|
return b'%s=%s\0' % (
|
||||||
|
hide_in_game.SOCK_VARIABLE,
|
||||||
|
os.fsencode(prefix / hide_in_game.SOCK_NAME),
|
||||||
|
)
|
||||||
|
|
||||||
|
def process(self, pid, executable=GAME, prefix='own', environ=None, args=('--flag',)):
|
||||||
|
folder = self.proc / str(pid)
|
||||||
|
folder.mkdir()
|
||||||
|
(folder / 'cmdline').write_bytes(
|
||||||
|
executable.encode() + b'\0' +
|
||||||
|
b''.join(arg.encode() + b'\0' for arg in args))
|
||||||
|
if environ is None:
|
||||||
|
environ = self.environment(self.prefix if prefix == 'own' else prefix)
|
||||||
|
(folder / 'environ').write_bytes(environ)
|
||||||
|
(folder / 'stat').write_text(f'{pid} (game) S')
|
||||||
|
return folder
|
||||||
|
|
||||||
|
def test_game_running_for_own_prefix(self):
|
||||||
|
self.process(123)
|
||||||
|
self.assertTrue(hide_in_game.game_running(self.prefix, self.proc))
|
||||||
|
|
||||||
|
def test_no_game_is_false(self):
|
||||||
|
self.process(123, executable=MAIN)
|
||||||
|
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
||||||
|
|
||||||
|
def test_game_of_another_prefix_is_false(self):
|
||||||
|
self.process(123, prefix=self.root / 'other')
|
||||||
|
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
||||||
|
|
||||||
|
def test_game_without_socket_is_false(self):
|
||||||
|
self.process(123, environ=b'')
|
||||||
|
self.assertFalse(hide_in_game.game_running(self.prefix, self.proc))
|
||||||
|
|
||||||
|
def test_unreadable_environment_is_unknown(self):
|
||||||
|
folder = self.process(123)
|
||||||
|
original = Path.read_bytes
|
||||||
|
|
||||||
|
def read(path):
|
||||||
|
if path.name == 'environ':
|
||||||
|
raise PermissionError()
|
||||||
|
return original(path)
|
||||||
|
|
||||||
|
with patch.object(Path, 'read_bytes', read):
|
||||||
|
self.assertIsNone(hide_in_game.game_running(self.prefix, self.proc))
|
||||||
|
|
||||||
|
def test_target_windows_ignores_game_class(self):
|
||||||
|
with patch.object(hide_in_game, 'xprop_root',
|
||||||
|
return_value=['0x10', '0x20', '0x30']), \
|
||||||
|
patch.object(hide_in_game, 'window_class',
|
||||||
|
side_effect=[
|
||||||
|
'wm_class(string) = "leagueclientux"',
|
||||||
|
'wm_class(string) = "leagueoflegends"',
|
||||||
|
'wm_class(string) = "riot client"',
|
||||||
|
]):
|
||||||
|
self.assertEqual(hide_in_game.target_windows(), [0x10, 0x30])
|
||||||
|
|
||||||
|
def test_hides_then_shows_on_match_end(self):
|
||||||
|
states = iter([True, True, False])
|
||||||
|
x11 = FakeX11()
|
||||||
|
output = io.StringIO()
|
||||||
|
watcher = hide_in_game.Watcher(
|
||||||
|
self.prefix, x11,
|
||||||
|
window_source=lambda: [0xabc],
|
||||||
|
state_source=lambda _prefix: next(states),
|
||||||
|
sleep=lambda _seconds: None,
|
||||||
|
out=output,
|
||||||
|
)
|
||||||
|
watcher.run(max_steps=3)
|
||||||
|
self.assertEqual(x11.unmapped, [0xabc])
|
||||||
|
self.assertEqual(x11.shown, [0xabc])
|
||||||
|
self.assertIn('hidden 0xabc', output.getvalue())
|
||||||
|
self.assertIn('the match ended', output.getvalue())
|
||||||
|
self.assertIn('shown 0xabc', output.getvalue())
|
||||||
|
|
||||||
|
def test_unreadable_state_restores_the_window(self):
|
||||||
|
states = iter([True, None])
|
||||||
|
x11 = FakeX11()
|
||||||
|
output = io.StringIO()
|
||||||
|
watcher = hide_in_game.Watcher(
|
||||||
|
self.prefix, x11,
|
||||||
|
window_source=lambda: [0xdef],
|
||||||
|
state_source=lambda _prefix: next(states),
|
||||||
|
sleep=lambda _seconds: None,
|
||||||
|
out=output,
|
||||||
|
)
|
||||||
|
watcher.run(max_steps=2)
|
||||||
|
self.assertEqual(x11.unmapped, [0xdef])
|
||||||
|
self.assertEqual(x11.shown, [0xdef])
|
||||||
|
self.assertIn('unreadable /proc state', output.getvalue())
|
||||||
|
|
||||||
|
def test_no_game_does_not_touch_windows(self):
|
||||||
|
x11 = FakeX11()
|
||||||
|
watcher = hide_in_game.Watcher(
|
||||||
|
self.prefix, x11,
|
||||||
|
window_source=lambda: [0xabc],
|
||||||
|
state_source=lambda _prefix: False,
|
||||||
|
sleep=lambda _seconds: None,
|
||||||
|
out=io.StringIO(),
|
||||||
|
)
|
||||||
|
watcher.run(max_steps=1)
|
||||||
|
self.assertEqual(x11.unmapped, [])
|
||||||
|
self.assertEqual(x11.shown, [])
|
||||||
|
|
||||||
|
def test_main_is_disabled_by_default(self):
|
||||||
|
with patch.dict(os.environ, {
|
||||||
|
'RCLOL_HIDE_CLIENT_IN_GAME': '',
|
||||||
|
'RCLOL_CLOSE_CLIENT_IN_GAME': '',
|
||||||
|
}):
|
||||||
|
self.assertEqual(hide_in_game.main([]), 0)
|
||||||
|
|
||||||
|
def test_close_mode_takes_precedence(self):
|
||||||
|
with patch.dict(os.environ, {
|
||||||
|
'RCLOL_HIDE_CLIENT_IN_GAME': '1',
|
||||||
|
'RCLOL_CLOSE_CLIENT_IN_GAME': '1',
|
||||||
|
}):
|
||||||
|
self.assertEqual(hide_in_game.main([]), 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Executable
+295
@@ -0,0 +1,295 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Opt-in: hide the League client UI while a match is running.
|
||||||
|
|
||||||
|
`RCLOL_HIDE_CLIENT_IN_GAME=1` is an alternative to closing the client UI with
|
||||||
|
`RCLOL_CLOSE_CLIENT_IN_GAME=1`. It hides the managed X11 windows for
|
||||||
|
`LeagueClientUx` and `Riot Client`, and shows them again when the game process
|
||||||
|
exits. The Riot client, League backend, CEF helpers and game keep running.
|
||||||
|
|
||||||
|
This is launcher/window management only. It does not patch client binaries,
|
||||||
|
CEF, anti-cheat or DRM, and it never hides the game window. It is off by
|
||||||
|
default, fails closed, and is intended for X11/Xwayland desktops.
|
||||||
|
|
||||||
|
The game is attributed to this prefix through the loader's
|
||||||
|
`__mldr_sockpath`, because guest processes do not carry `DPREFIX`. If proc,
|
||||||
|
ownership or window information cannot be read, no window is touched.
|
||||||
|
|
||||||
|
Diagnose one observation with:
|
||||||
|
|
||||||
|
python3 tools/hide-league-client-in-game.py --once
|
||||||
|
|
||||||
|
Enable it for a launcher run with:
|
||||||
|
|
||||||
|
RCLOL_HIDE_CLIENT_IN_GAME=1 ./scripts/run.sh
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
GAME_NAME = 'LeagueofLegends'
|
||||||
|
SOCK_VARIABLE = b'__mldr_sockpath'
|
||||||
|
SOCK_NAME = '.darlingserver.sock'
|
||||||
|
POLL_SECONDS = 2.0
|
||||||
|
TARGET_CLASSES = ('leagueclientux', 'riot client')
|
||||||
|
|
||||||
|
|
||||||
|
def target_prefix():
|
||||||
|
"""The prefix this launcher owns, or None when it cannot be determined."""
|
||||||
|
value = os.environ.get('DPREFIX', str(Path.home() / '.darling'))
|
||||||
|
prefix = Path(value).expanduser()
|
||||||
|
return prefix if prefix.is_absolute() else None
|
||||||
|
|
||||||
|
|
||||||
|
def process_state(proc):
|
||||||
|
try:
|
||||||
|
return (proc / 'stat').read_text().rsplit(')', 1)[1].split()[0]
|
||||||
|
except (OSError, IndexError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def command_names(proc):
|
||||||
|
try:
|
||||||
|
raw = (proc / 'cmdline').read_bytes()
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
os.path.basename(part.decode(errors='replace'))
|
||||||
|
for part in raw.split(b'\0')
|
||||||
|
if part
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def game_running(prefix, proc_root=Path('/proc')):
|
||||||
|
"""True/False for a game of this prefix; None when /proc is unreliable.
|
||||||
|
|
||||||
|
The game executable is matched by its exact file name. The process must
|
||||||
|
carry the loader socket of this prefix, so a game from another install is
|
||||||
|
never considered. Any unreadable candidate makes the result None (fail
|
||||||
|
closed).
|
||||||
|
"""
|
||||||
|
owner = (prefix / SOCK_NAME).parent.resolve()
|
||||||
|
try:
|
||||||
|
entries = list(proc_root.iterdir())
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
for proc in entries:
|
||||||
|
if not proc.name.isdigit() or process_state(proc) in ('Z', None):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if proc.stat().st_uid != os.getuid():
|
||||||
|
continue
|
||||||
|
if GAME_NAME not in command_names(proc):
|
||||||
|
continue
|
||||||
|
environment = dict(
|
||||||
|
item.split(b'=', 1)
|
||||||
|
for item in (proc / 'environ').read_bytes().split(b'\0')
|
||||||
|
if b'=' in item
|
||||||
|
)
|
||||||
|
socket = environment.get(SOCK_VARIABLE)
|
||||||
|
if socket is None:
|
||||||
|
continue
|
||||||
|
if Path(os.fsdecode(socket)).parent.resolve() != owner:
|
||||||
|
continue
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
if proc.exists():
|
||||||
|
return None
|
||||||
|
continue
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return None
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def xprop_root():
|
||||||
|
try:
|
||||||
|
listing = subprocess.check_output(
|
||||||
|
['xprop', '-root', '_NET_CLIENT_LIST'], text=True, timeout=5
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return []
|
||||||
|
return re.findall(r'0x[0-9a-fA-F]+', listing)
|
||||||
|
|
||||||
|
|
||||||
|
def window_class(window):
|
||||||
|
try:
|
||||||
|
prop = subprocess.check_output(
|
||||||
|
['xprop', '-id', window, 'WM_CLASS'], text=True, timeout=5
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return ''
|
||||||
|
return prop.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def target_windows():
|
||||||
|
"""Managed client windows only; the game window is never selected."""
|
||||||
|
found = []
|
||||||
|
for window in xprop_root():
|
||||||
|
classes = window_class(window)
|
||||||
|
if any(name in classes for name in TARGET_CLASSES):
|
||||||
|
found.append(int(window, 16))
|
||||||
|
return sorted(set(found))
|
||||||
|
|
||||||
|
|
||||||
|
class X11:
|
||||||
|
def __init__(self):
|
||||||
|
self.lib = ctypes.CDLL('libX11.so.6')
|
||||||
|
self.lib.XOpenDisplay.restype = ctypes.c_void_p
|
||||||
|
self.lib.XOpenDisplay.argtypes = [ctypes.c_char_p]
|
||||||
|
self.lib.XUnmapWindow.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
|
||||||
|
self.lib.XMapRaised.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
|
||||||
|
self.lib.XSync.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
||||||
|
self.lib.XCloseDisplay.argtypes = [ctypes.c_void_p]
|
||||||
|
error_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
||||||
|
self._error_handler = error_type(lambda _display, _event: 0)
|
||||||
|
self.lib.XSetErrorHandler.argtypes = [error_type]
|
||||||
|
self.lib.XSetErrorHandler.restype = ctypes.c_void_p
|
||||||
|
self.lib.XSetErrorHandler(self._error_handler)
|
||||||
|
self.display = self.lib.XOpenDisplay(None)
|
||||||
|
if not self.display:
|
||||||
|
raise RuntimeError('cannot open DISPLAY')
|
||||||
|
|
||||||
|
def unmap(self, window):
|
||||||
|
self.lib.XUnmapWindow(self.display, window)
|
||||||
|
self.lib.XSync(self.display, 0)
|
||||||
|
|
||||||
|
def show(self, window):
|
||||||
|
self.lib.XMapRaised(self.display, window)
|
||||||
|
self.lib.XSync(self.display, 0)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.lib.XCloseDisplay(self.display)
|
||||||
|
|
||||||
|
|
||||||
|
class Watcher:
|
||||||
|
"""Hide while a match is running, restore on end, uncertainty or exit."""
|
||||||
|
|
||||||
|
def __init__(self, prefix, x11, window_source=target_windows,
|
||||||
|
state_source=game_running, sleep=time.sleep, out=sys.stdout):
|
||||||
|
self.prefix = prefix
|
||||||
|
self.x11 = x11
|
||||||
|
self.window_source = window_source
|
||||||
|
self.state_source = state_source
|
||||||
|
self.sleep = sleep
|
||||||
|
self.out = out
|
||||||
|
self.hidden = set()
|
||||||
|
self.reported_no_window = False
|
||||||
|
|
||||||
|
def _show_all(self, reason):
|
||||||
|
if not self.hidden:
|
||||||
|
return
|
||||||
|
print(f'hide-league-client-in-game: {reason}; showing the client', file=self.out, flush=True)
|
||||||
|
for window in sorted(self.hidden):
|
||||||
|
try:
|
||||||
|
self.x11.show(window)
|
||||||
|
print(f'hide-league-client-in-game: shown 0x{window:x}', file=self.out, flush=True)
|
||||||
|
except Exception as error:
|
||||||
|
print(f'hide-league-client-in-game: cannot show 0x{window:x}: {error}',
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
self.hidden.clear()
|
||||||
|
|
||||||
|
def _hide_new(self):
|
||||||
|
windows = self.window_source()
|
||||||
|
if not windows and not self.hidden and not self.reported_no_window:
|
||||||
|
print('hide-league-client-in-game: no managed client window; not hiding',
|
||||||
|
file=self.out, flush=True)
|
||||||
|
self.reported_no_window = True
|
||||||
|
for window in windows:
|
||||||
|
if window in self.hidden:
|
||||||
|
continue
|
||||||
|
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:
|
||||||
|
print(f'hide-league-client-in-game: cannot hide 0x{window:x}: {error}',
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
def run(self, max_steps=None):
|
||||||
|
steps = 0
|
||||||
|
while True:
|
||||||
|
state = self.state_source(self.prefix)
|
||||||
|
if state is None:
|
||||||
|
self._show_all('unreadable /proc state')
|
||||||
|
elif state:
|
||||||
|
if not self.hidden and self.reported_no_window:
|
||||||
|
self.reported_no_window = False
|
||||||
|
self._hide_new()
|
||||||
|
else:
|
||||||
|
self._show_all('the match ended')
|
||||||
|
self.reported_no_window = False
|
||||||
|
steps += 1
|
||||||
|
if max_steps is not None and steps >= max_steps:
|
||||||
|
return 0
|
||||||
|
self.sleep(POLL_SECONDS)
|
||||||
|
|
||||||
|
def restore(self):
|
||||||
|
self._show_all('exiting')
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
parser.add_argument('--detach', action='store_true',
|
||||||
|
help='leave the launcher session so a terminal hang-up cannot end it')
|
||||||
|
parser.add_argument('--once', action='store_true',
|
||||||
|
help='one game observation and exit (no window changes)')
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if os.environ.get('RCLOL_CLOSE_CLIENT_IN_GAME') == '1':
|
||||||
|
print('hide-league-client-in-game: ignored; RCLOL_CLOSE_CLIENT_IN_GAME=1 takes precedence',
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
return 0
|
||||||
|
if os.environ.get('RCLOL_HIDE_CLIENT_IN_GAME') != '1':
|
||||||
|
print('hide-league-client-in-game: disabled; set RCLOL_HIDE_CLIENT_IN_GAME=1',
|
||||||
|
flush=True)
|
||||||
|
return 0
|
||||||
|
prefix = target_prefix()
|
||||||
|
if prefix is None:
|
||||||
|
print('hide-league-client-in-game: unusable DPREFIX: no action',
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
return 1
|
||||||
|
if args.once:
|
||||||
|
state = game_running(prefix)
|
||||||
|
print(f'hide-league-client-in-game: match={"unreadable" if state is None else "running" if state else "absent"}',
|
||||||
|
flush=True)
|
||||||
|
return 0
|
||||||
|
if args.detach:
|
||||||
|
try:
|
||||||
|
os.setsid()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
x11 = X11()
|
||||||
|
except Exception as error:
|
||||||
|
print(f'hide-league-client-in-game: cannot open X11: {error}',
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
watcher = Watcher(prefix, x11)
|
||||||
|
|
||||||
|
def stop(_signum, _frame):
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, stop)
|
||||||
|
signal.signal(signal.SIGINT, stop)
|
||||||
|
print('hide-league-client-in-game: watching %s for a running match; '
|
||||||
|
'opt out with RCLOL_HIDE_CLIENT_IN_GAME=0' % prefix, flush=True)
|
||||||
|
try:
|
||||||
|
watcher.run()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
watcher.restore()
|
||||||
|
x11.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -19,6 +19,17 @@ fi
|
|||||||
if [ "${RCLOL_CLOSE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
if [ "${RCLOL_CLOSE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||||
python3 "$runner_dir/close-league-client-in-game.py" --detach &
|
python3 "$runner_dir/close-league-client-in-game.py" --detach &
|
||||||
fi
|
fi
|
||||||
|
# Opt-in alternative for users who keep the client open during a match: hide
|
||||||
|
# the managed client windows instead of closing the UI. It never touches the
|
||||||
|
# League backend, the Riot client or the game window; it restores the window
|
||||||
|
# when the game process ends. Close and hide are mutually exclusive.
|
||||||
|
if [ "${RCLOL_HIDE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||||
|
if [ "${RCLOL_CLOSE_CLIENT_IN_GAME:-0}" = 1 ]; then
|
||||||
|
echo 'RCLOL_HIDE_CLIENT_IN_GAME ignored: RCLOL_CLOSE_CLIENT_IN_GAME=1 takes precedence' >&2
|
||||||
|
else
|
||||||
|
python3 "$runner_dir/hide-league-client-in-game.py" --detach &
|
||||||
|
fi
|
||||||
|
fi
|
||||||
# The bundled downloader rejects IPv6 CONNECT_TO addresses. Only Riot CDN
|
# The bundled downloader rejects IPv6 CONNECT_TO addresses. Only Riot CDN
|
||||||
# AAAA lookups are suppressed; set this to 0 to retain the upstream resolver.
|
# AAAA lookups are suppressed; set this to 0 to retain the upstream resolver.
|
||||||
export RCLOL_CDN_IPV4_ONLY="${RCLOL_CDN_IPV4_ONLY:-1}"
|
export RCLOL_CDN_IPV4_ONLY="${RCLOL_CDN_IPV4_ONLY:-1}"
|
||||||
|
|||||||
Reference in new issue
Block a user