VulpesOS/tundra/vm/init.py
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

96 lines
4 KiB
Python

#!/usr/bin/python3
"""Guest PID 1. Start the graphical session and retain serial diagnostics."""
import json
import os
from pathlib import Path
import signal
import subprocess
import time
import traceback
if os.getpid() != 1:
raise SystemExit('This init runs only as PID 1 inside the VM.')
os.environ.update(PATH='/usr/bin', HOME='/root', LANG='C.UTF-8', LD_LIBRARY_PATH='/usr/lib/weston:/usr/lib')
children = []
def run(*args):
subprocess.run(args, check=True)
def start(*args, env=None):
p = subprocess.Popen(args, env=env)
children.append(p)
return p
def shutdown(*unused):
print('TUNDRA_SHUTDOWN', flush=True)
for p in reversed(children):
if p.poll() is None: p.terminate()
time.sleep(2)
os.sync()
# Kernel poweroff; this process cannot run on the host due to the PID1 guard.
import ctypes
ctypes.CDLL(None).reboot(0x4321FEDC)
signal.signal(signal.SIGTERM, shutdown)
try:
Path('/dev/shm').mkdir(exist_ok=True)
run('mount','-t','tmpfs','tmpfs','/dev/shm')
Path('/dev/pts').mkdir(exist_ok=True)
run('mount','-t','devpts','devpts','/dev/pts')
for path in ('/run/user/1000','/home/vulpes','/var/log/tundra'):
Path(path).mkdir(parents=True, exist_ok=True)
os.chown(path,1000,1000)
os.chmod('/run/user/1000',0o700)
# Staged files belong to the builder; the guest grants only product state to its user.
for base, dirs, files in os.walk('/opt/vulpes'):
os.chown(base,1000,1000)
for name in files: os.chown(Path(base)/name,1000,1000)
run('ip','link','set','lo','up')
start('/usr/lib/systemd/systemd-udevd','--resolve-names=never')
# Coldplug must wait for the daemon's control socket, otherwise its initial
# events can be lost and libinput sees neither keyboard nor pointer.
for _ in range(100):
ping = subprocess.run(['udevadm','control','--timeout=1','--ping'],
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
if ping.returncode == 0: break
time.sleep(.1)
else: raise RuntimeError('udevd did not become ready')
run('udevadm','trigger','--action=add')
run('udevadm','settle','--timeout=10')
inputs = []
for device in Path('/dev/input').glob('event*'):
info = subprocess.check_output(['udevadm','info','--query=property',str(device)],text=True)
inputs.append(info)
if not any('ID_INPUT_MOUSE=1' in item for item in inputs):
raise RuntimeError('Virtual pointer missing from udev: '+repr(inputs))
if not any('ID_INPUT_KEYBOARD=1' in item for item in inputs):
raise RuntimeError('Virtual keyboard missing from udev: '+repr(inputs))
print('TUNDRA_INPUT_READY',flush=True)
if 'tundra.usb=1' in Path('/proc/cmdline').read_text():
run('python3','-u','/opt/tundra/usb-test.py')
env = dict(os.environ, SEATD_VTBOUND='0')
start('seatd','-g','video',env=env)
for _ in range(50):
if Path('/run/seatd.sock').exists(): break
time.sleep(.1)
env.update(HOME='/home/vulpes',USER='vulpes',LOGNAME='vulpes',
XDG_RUNTIME_DIR='/run/user/1000',LIBSEAT_BACKEND='seatd',
WAYLAND_DISPLAY='wayland-0',MOZ_ENABLE_WAYLAND='1',GDK_BACKEND='wayland',
XDG_SESSION_TYPE='wayland',LIBGL_ALWAYS_SOFTWARE='1')
print('TUNDRA_GRAPHICAL_START', flush=True)
user = ['setpriv','--reuid=1000','--regid=1000','--groups=44,992']
session = start(*user,'dbus-run-session','--','python3','-u','/opt/tundra/session.py',env=env)
while session.poll() is None: time.sleep(1)
print('TUNDRA_SESSION_EXIT',session.returncode,flush=True)
except Exception:
traceback.print_exc()
print('TUNDRA_BOOT_FAILED',flush=True)
finally:
for name in ('weston.log','gecko.log','server.log','test.json','usb.log','usb-test.json'):
p = Path('/var/log/tundra')/name
if p.exists():
print('TUNDRA_LOG_BEGIN', name, flush=True)
print(p.read_text(errors='replace')[-18000:],flush=True)
print('TUNDRA_LOG_END', name, flush=True)
shutdown()
while True: time.sleep(1)