mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
fix(launcher): remove orphaned client-in-game helpers
A launcher can exit while its long-lived close/hide helper remains. The next run then starts another watcher, so several copies watch the same prefix. Add an opt-in RCLOL_CLEAN_STALE_HELPERS=1 preflight that removes only helper processes of this prefix reparented to init/systemd. Helpers of a live launcher, other prefixes, the game, Riot/League/CEF, darlingserver and the runtime are preserved. Unreadable or relative process data leaves a process alone. Validated across repeated launcher starts: the cleanup removed four, then one, then one orphaned watcher; a subsequent dry run reported no orphaned helpers. Zombie reaping after partial recovery is tracked separately in #124.
This commit is contained in:
1 parent
c8dd3886fc
commit
0a04d4e16c
7 files changed
+346
No files matched your search
@@ -0,0 +1,110 @@
|
||||
"""Exercise orphan-helper cleanup with fake /proc entries and mocked signals."""
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'clean_stale_helpers', ROOT / 'tools/clean-stale-helpers.py')
|
||||
clean_stale_helpers = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(clean_stale_helpers)
|
||||
|
||||
HELPER = '/repo/tools/close-league-client-in-game.py'
|
||||
HIDE_HELPER = '/repo/tools/hide-league-client-in-game.py'
|
||||
|
||||
|
||||
class CleanStaleHelpersTests(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.other = self.root / 'other-prefix'
|
||||
|
||||
def parent(self, pid, comm):
|
||||
folder = self.proc / str(pid)
|
||||
folder.mkdir(exist_ok=True)
|
||||
(folder / 'comm').write_text(comm + '\n')
|
||||
|
||||
def process(self, pid, script=HELPER, parent_pid=500, parent_comm='systemd',
|
||||
prefix='own', argv0='/usr/bin/python3', state='S',
|
||||
environment=True, arguments=('--detach',)):
|
||||
folder = self.proc / str(pid)
|
||||
folder.mkdir()
|
||||
argv = [argv0, script, *arguments]
|
||||
(folder / 'cmdline').write_bytes(
|
||||
b'\0'.join(arg.encode() for arg in argv) + b'\0')
|
||||
if parent_comm is not None:
|
||||
self.parent(parent_pid, parent_comm)
|
||||
fields = [state, str(parent_pid)] + ['0'] * 17 + ['100']
|
||||
(folder / 'stat').write_text(f'{pid} (python3) ' + ' '.join(fields) + '\n')
|
||||
if environment:
|
||||
prefix = self.prefix if prefix == 'own' else prefix
|
||||
(folder / 'environ').write_bytes(
|
||||
b'DPREFIX=' + os.fsencode(prefix) + b'\0')
|
||||
else:
|
||||
(folder / 'environ').write_bytes(b'UNRELATED=1\0')
|
||||
return folder
|
||||
|
||||
def run_clean(self, dry_run=False):
|
||||
killed = []
|
||||
|
||||
def kill(pid, signum):
|
||||
killed.append((pid, signum))
|
||||
|
||||
out = io.StringIO()
|
||||
result = clean_stale_helpers.clean(
|
||||
self.prefix, proc_root=self.proc, kill=kill,
|
||||
sleep=lambda _seconds: None, out=out, dry_run=dry_run)
|
||||
return result, killed, out.getvalue()
|
||||
|
||||
def test_removes_only_orphaned_helpers_of_this_prefix(self):
|
||||
self.process(100, HELPER)
|
||||
self.process(101, HIDE_HELPER)
|
||||
self.process(200, HELPER, parent_pid=600, parent_comm='darling')
|
||||
self.process(201, HELPER, prefix=self.other)
|
||||
self.process(202, HELPER, argv0='/usr/bin/vim')
|
||||
self.process(203, HELPER, state='Z')
|
||||
self.process(204, HELPER, environment=False)
|
||||
self.process(205, HELPER, parent_pid=700, parent_comm=None)
|
||||
result, killed, output = self.run_clean()
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(sorted(killed), sorted([
|
||||
(100, signal.SIGTERM), (100, signal.SIGKILL),
|
||||
(101, signal.SIGTERM), (101, signal.SIGKILL),
|
||||
]))
|
||||
self.assertIn('removing 2 orphaned helper(s): 100,101', output)
|
||||
|
||||
def test_dry_run_reports_without_signalling(self):
|
||||
self.process(100)
|
||||
result, killed, output = self.run_clean(dry_run=True)
|
||||
self.assertEqual((result, killed), (0, []))
|
||||
self.assertIn('would remove 1 orphaned helper(s): 100', output)
|
||||
|
||||
def test_no_orphans_is_a_noop(self):
|
||||
self.process(200, parent_pid=600, parent_comm='darling')
|
||||
result, killed, output = self.run_clean()
|
||||
self.assertEqual((result, killed), (0, []))
|
||||
self.assertIn('no orphaned helpers', output)
|
||||
|
||||
def test_relative_helper_prefix_is_preserved(self):
|
||||
self.process(100, prefix='relative-prefix')
|
||||
result, killed, output = self.run_clean()
|
||||
self.assertEqual((result, killed), (0, []))
|
||||
self.assertIn('no orphaned helpers', output)
|
||||
|
||||
def test_launcher_runs_cleanup_before_starting_a_helper(self):
|
||||
source = (ROOT / 'tools/run-riot-client.sh').read_text()
|
||||
self.assertIn('RCLOL_CLEAN_STALE_HELPERS', source)
|
||||
self.assertLess(source.index('clean-stale-helpers.py'),
|
||||
source.index('close-league-client-in-game.py'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user