lazyBoy/tests/screen-recovery.test.py

106 lines
5.1 KiB
Python
Raw Permalink Normal View History

2026-09-11 02:47:43 +00:00
"""Production lazyboy-screen service repair in an isolated Computer."""
import json
from concurrent.futures import ThreadPoolExecutor
import os
from pathlib import Path
import signal
import shlex
import subprocess
import time
root = Path('/tmp/lazyboy')
def ensure(slot=7):
result = subprocess.run(['bash', '/fixture/lazyboy-screen', 'ensure', str(slot), f'/home/lazyboy/.browser-profiles/recovery-{slot}'],
capture_output=True, text=True, timeout=60)
if result.returncode:
logs = {p.name: p.read_text(errors='replace')[-3000:] for p in root.glob('screen-8*.log')}
raise AssertionError((result.returncode, result.stdout, result.stderr, logs))
assert json.loads(result.stdout)['ok'] is True
def pids(slot=7):
return {name: int((root / f'screen-{slot + 1}-{name}.pid').read_text())
for name in ['wm', 'desktop', 'panel', 'cua', 'xvfb', 'dbus', 'browser']}
def main():
assert os.getuid() == 1000
with ThreadPoolExecutor(max_workers=2) as workers:
pending = [workers.submit(ensure, slot) for slot in [6, 7]]
for result in pending:
result.result()
peer = pids(6)
peer_bus = (root / 'screen-7.dbus').read_text()
initial = pids()
bus = (root / 'screen-8.dbus').read_text()
ensure()
assert pids() == initial, 'warm ensure must reuse existing services'
for name in ['wm', 'panel', 'desktop', 'cua', 'xvfb', 'dbus', 'browser']:
before = pids()
os.kill(before[name], signal.SIGTERM)
deadline = time.monotonic() + 10
while True:
try:
state = Path(f'/proc/{before[name]}/stat').read_text().rsplit(')', 1)[1].split()[0]
except FileNotFoundError:
break
if state == 'Z':
break
assert time.monotonic() < deadline, f'{name} did not stop'
time.sleep(.1)
with ThreadPoolExecutor(max_workers=2) as workers:
pending = [workers.submit(ensure) for _ in range(2)]
for result in pending:
result.result()
after = pids()
assert after[name] != before[name], (name, before, after)
if name not in ('xvfb', 'dbus'):
for other in before.keys() - {name}:
assert after[other] == before[other], (name, other, before, after)
if name == 'dbus':
assert (root / 'screen-8.dbus').read_text() != bus
bus = (root / 'screen-8.dbus').read_text()
else:
assert (root / 'screen-8.dbus').read_text() == bus
deadline = time.monotonic() + 15
while True:
probe = subprocess.run(['python3', '-I', '/fixture/runtime_probe.py', '7'],
capture_output=True, text=True, check=True, timeout=8)
observation = json.loads(probe.stdout)
if all(observation[component] is True for component in ['desktop', 'viewer', 'browser']):
break
assert time.monotonic() < deadline, observation
time.sleep(.1)
assert pids(6) == peer, (name, 'other display services changed')
assert (root / 'screen-7.dbus').read_text() == peer_bus
peer_probe = subprocess.run(['python3', '-I', '/fixture/runtime_probe.py', '6'],
capture_output=True, text=True, check=True, timeout=8)
peer_observation = json.loads(peer_probe.stdout)
assert all(peer_observation[c] is True for c in ['desktop', 'browser', 'viewer']), peer_observation
for slot in [6, 7]:
browser_pid = pids(slot)['browser']
# This Chromium build rewrites argv into a single process-title string.
# Fixture paths contain no spaces; normalize that representation.
raw = Path(f'/proc/{browser_pid}/cmdline').read_bytes().replace(b'\0', b' ').decode()
argv = shlex.split(raw)
assert f'--user-data-dir=/home/lazyboy/.browser-profiles/recovery-{slot}' in argv, (slot, browser_pid, argv)
assert f'--remote-debugging-port={9222 + slot}' in argv, (slot, browser_pid, argv)
env = dict(os.environ, DISPLAY=':8', CUA_DRIVER_RS_HOME='/tmp/lazyboy/cua-home-8',
DBUS_SESSION_BUS_ADDRESS=bus.strip())
for tool, payload, extra in [
('start_session', {'session': 'recovery-fixture'}, []),
('get_desktop_state', {'session': 'recovery-fixture', 'screenshot_out_file': '/tmp/recovery.png'},
['--screenshot-out-file', '/tmp/recovery.png']),
]:
Path('/tmp/recovery.png').unlink(missing_ok=True)
result = subprocess.run(['cua-driver', 'call', '--socket', '/tmp/lazyboy/cua-8.sock', *extra, tool],
input=json.dumps(payload), capture_output=True, text=True, env=env, timeout=15)
assert result.returncode == 0, (name, tool, result.stdout, result.stderr)
assert Path('/tmp/recovery.png').read_bytes().startswith(b'\x89PNG\r\n\x1a\n')
print(json.dumps({'repaired': name, 'observation': observation, 'cuaScreenshot': True, 'peerUnchanged': True}), flush=True)
if __name__ == '__main__':
main()