106 lines
5.1 KiB
Python
106 lines
5.1 KiB
Python
|
|
"""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()
|