mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 17:04:35 +01:00
115 lines
4.2 KiB
Python
115 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Launch a built Linux AppImage on Xvfb and check its bundled server.
|
|
|
|
This checks the packaged app without a headset or a desktop login session.
|
|
The profile and display are temporary, and every process started by this check
|
|
is stopped before it exits.
|
|
"""
|
|
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
|
|
def server_port(profile):
|
|
for log in (profile / "config").rglob("server.log"):
|
|
ports = re.findall(r"--port (\d+)", log.read_text(errors="replace"))
|
|
if ports:
|
|
return int(ports[-1])
|
|
return None
|
|
|
|
|
|
def port_closed(port):
|
|
try:
|
|
with socket.create_connection(("127.0.0.1", port), timeout=1):
|
|
return False
|
|
except OSError:
|
|
return True
|
|
|
|
|
|
def check(appimage):
|
|
if not appimage.is_file() or not os.access(appimage, os.X_OK):
|
|
raise RuntimeError(f"AppImage is missing or cannot run: {appimage}")
|
|
if not shutil.which("xvfb-run"):
|
|
raise RuntimeError("xvfb-run is required for the AppImage smoke check")
|
|
digest = hashlib.sha256(appimage.read_bytes()).hexdigest()
|
|
with tempfile.TemporaryDirectory(prefix="frame-appimage-smoke-") as directory:
|
|
profile = Path(directory)
|
|
(profile / "tmp").mkdir()
|
|
env = dict(os.environ)
|
|
env.update(
|
|
APPIMAGE_EXTRACT_AND_RUN="1", ELECTRON_DISABLE_SANDBOX="1",
|
|
TMPDIR=str(profile / "tmp"),
|
|
XDG_CONFIG_HOME=str(profile / "config"),
|
|
XDG_CACHE_HOME=str(profile / "cache"),
|
|
XDG_DATA_HOME=str(profile / "data"),
|
|
FRAME_ALIAS="frame-control-appimage-smoke-no-headset",
|
|
FRAME_CONTROL_TELEMETRY="0", FRAME_CONTROL_NO_UPDATE_CHECK="1",
|
|
DO_NOT_TRACK="1",
|
|
)
|
|
output = profile / "app.log"
|
|
port = None
|
|
passed = False
|
|
with output.open("w") as log:
|
|
proc = subprocess.Popen(
|
|
["xvfb-run", "-a", "-s", "-screen 0 1280x800x24 -nolisten tcp",
|
|
str(appimage), "--no-sandbox"],
|
|
cwd=profile, env=env, stdout=log, stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
)
|
|
try:
|
|
deadline = time.monotonic() + 60
|
|
while time.monotonic() < deadline:
|
|
if proc.poll() is not None:
|
|
raise RuntimeError(f"AppImage exited {proc.returncode} before its server answered")
|
|
port = server_port(profile)
|
|
if port is not None:
|
|
try:
|
|
with urllib.request.urlopen(f"http://127.0.0.1:{port}/", timeout=2) as response:
|
|
if response.status == 200 and response.headers.get("Server", "").startswith("FrameControl/1"):
|
|
passed = True
|
|
break
|
|
except (OSError, urllib.error.URLError):
|
|
pass
|
|
time.sleep(0.5)
|
|
if not passed:
|
|
raise RuntimeError("AppImage server did not answer within 60 seconds")
|
|
except BaseException:
|
|
log.flush()
|
|
print(output.read_text(errors="replace")[-1500:], file=sys.stderr)
|
|
raise
|
|
finally:
|
|
try:
|
|
os.killpg(proc.pid, signal.SIGTERM)
|
|
except ProcessLookupError:
|
|
pass
|
|
try:
|
|
proc.wait(timeout=15)
|
|
except subprocess.TimeoutExpired:
|
|
os.killpg(proc.pid, signal.SIGKILL)
|
|
proc.wait(timeout=10)
|
|
if port is not None:
|
|
for _ in range(20):
|
|
if port_closed(port):
|
|
break
|
|
time.sleep(0.5)
|
|
else:
|
|
raise RuntimeError(f"AppImage server stayed on port {port} after shutdown")
|
|
print(f"AppImage SHA-256 {digest}: HTTP 200, FrameControl/1, server stopped")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 2:
|
|
raise SystemExit("usage: check-appimage-launch.py path/to/AppImage")
|
|
check(Path(sys.argv[1]).resolve())
|