Files
darling-lol/tools/stop-darling-runtime.py
T

126 lines
5.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Wait for Darling's old server to exit before rebuilding its namespace."""
import os
import signal
from pathlib import Path
import subprocess
import sys
import time
prefix = Path(os.environ.get('DPREFIX', str(Path.home() / '.darling')))
try:
old_pid = int((prefix / '.init.pid').read_text().strip())
except (OSError, ValueError):
old_pid = 0
def live(pid):
if pid <= 0:
return False
try:
state = next(line.split()[1] for line in (Path('/proc') / str(pid) / 'status').read_text().splitlines() if line.startswith('State:'))
return state != 'Z'
except (FileNotFoundError, ProcessLookupError, StopIteration):
# A process can exit after status is opened but before it is read.
return False
def process_identity(pid):
try:
return (Path('/proc') / str(pid) / 'stat').read_text().rsplit(')', 1)[1].split()[19]
except (OSError, IndexError):
return None
def riot_descendants(server_pid):
# The host server and its guest init have different PID namespaces. Follow
# ancestry while the server is alive, rather than comparing namespace IDs.
pending = [server_pid]
seen = set()
result = {}
while pending:
pid = pending.pop()
if pid in seen or pid <= 0:
continue
seen.add(pid)
try:
for children in (Path('/proc') / str(pid) / 'task').glob('*/children'):
pending.extend(int(child) for child in children.read_text().split())
mappings = (Path('/proc') / str(pid) / 'maps').read_text()
if ('Electron Framework.framework/Versions/A/Electron Framework' in mappings or
'/Applications/Riot Client.app/' in mappings):
result[pid] = process_identity(pid)
except (OSError, ValueError):
continue
return result
def orphaned_riot():
# A dead server marker cannot enumerate its former descendants. Recover
# only same-user Riot mappings in this prefix with no live server ancestor.
result = {}
riot_path = str(prefix / 'Applications/Riot Client.app') + '/'
league_path = str(prefix / 'Applications/League of Legends.app') + '/'
for entry in Path('/proc').iterdir():
if not entry.name.isdigit():
continue
try:
if entry.stat().st_uid != os.getuid():
continue
mappings = (entry / 'maps').read_text()
if riot_path not in mappings and league_path not in mappings:
continue
pid = int(entry.name)
ancestor = pid
seen = set()
has_server = False
while ancestor > 1 and ancestor not in seen:
seen.add(ancestor)
proc = Path('/proc') / str(ancestor)
if proc.joinpath('comm').read_text().strip() == 'darlingserver' and live(ancestor):
has_server = True
break
fields = proc.joinpath('stat').read_text().rsplit(')', 1)[1].split()
ancestor = int(fields[1])
if not has_server:
result[pid] = process_identity(pid)
except (OSError, ValueError, IndexError):
continue
return result
owned_riot = riot_descendants(old_pid)
owned_riot.update(orphaned_riot())
subprocess.run(['darling', 'shutdown'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
deadline = time.monotonic() + 5
while live(old_pid) and time.monotonic() < deadline:
time.sleep(.1)
if live(old_pid):
raise SystemExit(f'Darling server {old_pid} has not exited; runtime remains untouched')
# Shutdown can leave Electron helpers or League's startup window reparented
# and consuming CPU/GPU time.
# Stop the observed descendants and verified serverless Riot processes in this
# prefix, guarding against PID reuse. Leave other live Darling sessions alone.
for pid, identity in owned_riot.items():
if identity is not None and process_identity(pid) == identity and live(pid):
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
pass
deadline = time.monotonic() + 3
while any(process_identity(pid) == identity and live(pid)
for pid, identity in owned_riot.items()) and time.monotonic() < deadline:
time.sleep(.1)
remaining = {pid: identity for pid, identity in owned_riot.items()
if process_identity(pid) == identity and live(pid)}
# These helpers have no live server and ignored graceful termination. They
# cannot complete guest RPCs after shutdown. Kill only the previously observed
# Riot descendants, with the same PID/start-time guard used above.
for pid, identity in remaining.items():
if process_identity(pid) == identity and live(pid):
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
deadline = time.monotonic() + 5
while any(process_identity(pid) == identity and live(pid)
for pid, identity in remaining.items()) and time.monotonic() < deadline:
time.sleep(.1)
if any(process_identity(pid) == identity and live(pid) for pid, identity in remaining.items()):
raise SystemExit('Old Riot helpers remain alive; runtime remains untouched')
subprocess.run([sys.executable, str(Path(__file__).with_name('ensure-darling-runtime.py'))], check=True)