112 lines
5.1 KiB
Python
112 lines
5.1 KiB
Python
|
|
"""Real GUI service fault/restart acceptance, inside a disposable Computer container.
|
||
|
|
|
||
|
|
Run via scripts/test-runtime-gui.py; never targets an existing Computer.
|
||
|
|
"""
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
import signal
|
||
|
|
import resource
|
||
|
|
import subprocess
|
||
|
|
import tempfile
|
||
|
|
import time
|
||
|
|
|
||
|
|
source = Path('/fixture/runtime_probe.py')
|
||
|
|
def probe(slot):
|
||
|
|
# Match the production helper's limits and clean environment, so a healthy
|
||
|
|
# service cannot pass only because the acceptance process has more resources.
|
||
|
|
def limits():
|
||
|
|
resource.setrlimit(resource.RLIMIT_AS, (256 * 1024 * 1024,) * 2)
|
||
|
|
resource.setrlimit(resource.RLIMIT_CPU, (20, 20))
|
||
|
|
resource.setrlimit(resource.RLIMIT_NOFILE, (64, 64))
|
||
|
|
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
||
|
|
result = subprocess.run(['python3', '-I', str(source), json.dumps(slot)],
|
||
|
|
env={'PATH': '/usr/local/bin:/usr/bin:/bin',
|
||
|
|
'HOME': '/home/lazyboy', 'LANG': 'C.UTF-8'},
|
||
|
|
capture_output=True, timeout=8, preexec_fn=limits, check=True)
|
||
|
|
assert len(result.stdout) <= 4096
|
||
|
|
return json.loads(result.stdout)
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
assert os.getuid() == 1000
|
||
|
|
processes = {}
|
||
|
|
with tempfile.TemporaryDirectory(prefix='runtime-gui-') as directory:
|
||
|
|
root = Path(directory)
|
||
|
|
env = dict(os.environ, DISPLAY=':8', HOME=directory,
|
||
|
|
DBUS_SESSION_BUS_ADDRESS=f'unix:path={root / "bus"}')
|
||
|
|
commands = {
|
||
|
|
'bus': ['dbus-daemon', '--session', '--nofork', f'--address=unix:path={root / "bus"}'],
|
||
|
|
'x': ['Xvfb', ':8', '-screen', '0', '1024x768x24', '-ac', '-nolisten', 'tcp'],
|
||
|
|
'wm': ['xfwm4', '--replace', '--compositor=off'],
|
||
|
|
'browser': ['chromium', '--no-sandbox', '--disable-dev-shm-usage',
|
||
|
|
'--no-first-run', '--disable-background-networking',
|
||
|
|
'--remote-debugging-port=9229', f'--user-data-dir={root / "profile"}', 'about:blank'],
|
||
|
|
'rfb': ['x11vnc', '-display', ':8', '-forever', '-shared', '-nopw',
|
||
|
|
'-listen', '127.0.0.1', '-rfbport', '5907'],
|
||
|
|
'viewer': ['websockify', '--web=/usr/share/novnc', '127.0.0.1:6087', '127.0.0.1:5907'],
|
||
|
|
}
|
||
|
|
|
||
|
|
def start(name):
|
||
|
|
with (root / f'{name}.log').open('ab') as log:
|
||
|
|
processes[name] = subprocess.Popen(commands[name], env=env, stdout=log,
|
||
|
|
stderr=subprocess.STDOUT, start_new_session=True)
|
||
|
|
|
||
|
|
def stop(name):
|
||
|
|
process = processes.pop(name)
|
||
|
|
try:
|
||
|
|
os.killpg(process.pid, signal.SIGTERM)
|
||
|
|
process.wait(timeout=5)
|
||
|
|
except ProcessLookupError:
|
||
|
|
pass
|
||
|
|
except subprocess.TimeoutExpired:
|
||
|
|
os.killpg(process.pid, signal.SIGKILL)
|
||
|
|
process.wait(timeout=5)
|
||
|
|
|
||
|
|
def expect(label, desktop, browser, viewer):
|
||
|
|
wanted = dict(version=1, slot=7, runner=True, desktop=desktop, browser=browser, viewer=viewer)
|
||
|
|
deadline = time.monotonic() + 20
|
||
|
|
while True:
|
||
|
|
actual = probe(7)
|
||
|
|
if actual == wanted:
|
||
|
|
print(json.dumps({'case': label, 'observation': actual}), flush=True)
|
||
|
|
return
|
||
|
|
if time.monotonic() >= deadline:
|
||
|
|
logs = {p.name: p.read_text(errors='replace')[-5000:] for p in root.glob('*.log')}
|
||
|
|
raise AssertionError((label, wanted, actual, logs))
|
||
|
|
time.sleep(.2)
|
||
|
|
|
||
|
|
try:
|
||
|
|
assert probe(None) == dict(version=1, slot=None, runner=True, desktop=None, browser=None, viewer=None)
|
||
|
|
expect('cold', False, False, False)
|
||
|
|
start('x')
|
||
|
|
deadline = time.monotonic() + 10
|
||
|
|
while subprocess.run(['xdpyinfo', '-display', ':8'], stdout=subprocess.DEVNULL,
|
||
|
|
stderr=subprocess.DEVNULL).returncode:
|
||
|
|
assert time.monotonic() < deadline, 'Xvfb startup timeout'
|
||
|
|
time.sleep(.1)
|
||
|
|
start('bus')
|
||
|
|
deadline = time.monotonic() + 5
|
||
|
|
while not (root / 'bus').exists():
|
||
|
|
assert time.monotonic() < deadline, 'session bus startup timeout'
|
||
|
|
time.sleep(.1)
|
||
|
|
for name in ['wm', 'browser', 'rfb', 'viewer']:
|
||
|
|
start(name)
|
||
|
|
expect('all-real-services', True, True, True)
|
||
|
|
for service, expected in [('browser', (True, False, True)),
|
||
|
|
('viewer', (True, True, False)),
|
||
|
|
('rfb', (True, True, False)),
|
||
|
|
('wm', (False, True, True))]:
|
||
|
|
stop(service)
|
||
|
|
expect(f'{service}-stopped', *expected)
|
||
|
|
start(service)
|
||
|
|
expect(f'{service}-restarted', True, True, True)
|
||
|
|
assert probe(None)['desktop'] is None, 'unassigned probe must not borrow the live screen'
|
||
|
|
finally:
|
||
|
|
for name in list(reversed(processes)):
|
||
|
|
stop(name)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|