mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-10-26 21:17:40 +00:00
140 lines
6.7 KiB
Python
140 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Install, check, reinstall and remove a Visual C++ redistributable through Raven on an isolated headless compositor.
|
|
|
|
The bundle runs unmodified from a fresh prefix: first with its progress window (/passive), whose screenshot is kept, then again
|
|
silently over the installed copy, then removed. After each step the bundle's own log, the files in the system directory and a program
|
|
running in the prefix say what Windows would show.
|
|
|
|
usage: native-redist.py RAVEN VC_REDIST.EXE CHECKER.EXE SYSTEM_FOLDER EVIDENCE_DIR
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from PIL import Image
|
|
|
|
RUNTIME = Path(f"/run/user/{os.getuid()}/raven-labwc-runtime")
|
|
|
|
|
|
def environment():
|
|
return {"PATH": "/usr/bin:/bin", "XDG_RUNTIME_DIR": str(RUNTIME), "WAYLAND_DISPLAY": "wayland-0"}
|
|
|
|
|
|
def start_compositor(log):
|
|
"""Start a headless labwc that never touches the user's own session."""
|
|
if (RUNTIME / "wayland-0").exists():
|
|
probe = subprocess.run(["wlrctl", "toplevel", "list"], env=environment(), capture_output=True, timeout=10)
|
|
if probe.returncode == 0:
|
|
return None
|
|
(RUNTIME / "wayland-0").unlink(missing_ok=True)
|
|
(RUNTIME / "wayland-0.lock").unlink(missing_ok=True)
|
|
RUNTIME.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
env = environment() | {"WLR_BACKENDS": "headless", "WLR_RENDERER": "pixman", "WLR_HEADLESS_OUTPUTS": "1"}
|
|
process = subprocess.Popen(["labwc"], env=env, stdout=log, stderr=log)
|
|
for _ in range(50):
|
|
if (RUNTIME / "wayland-0").exists():
|
|
time.sleep(0.5)
|
|
return process
|
|
time.sleep(0.1)
|
|
raise RuntimeError("isolated compositor did not start")
|
|
|
|
|
|
def visible_area(path):
|
|
"""How much of the screen is not the compositor's black background."""
|
|
image = Image.open(path).convert("L").point(lambda value: 255 if value > 8 else 0)
|
|
return sum(image.histogram()[255:])
|
|
|
|
|
|
def run(raven, prefix, program, arguments, output, watch=None, timeout=600):
|
|
"""Runs a program in the prefix; with watch set, the screen is captured every second and the fullest capture kept there."""
|
|
env = environment() | {"RAVEN_PREFIX": str(prefix)}
|
|
if "RAVEN_THEME" in os.environ:
|
|
env["RAVEN_THEME"] = os.environ["RAVEN_THEME"]
|
|
with output.open("wb") as log:
|
|
process = subprocess.Popen([str(raven), "--run-process", "--execution=native", f"--timeout-ms={timeout * 1000}", "--desktop=wayland",
|
|
str(program), "--", *arguments], env=env, stdout=log, stderr=log)
|
|
best = 0
|
|
deadline = time.time() + timeout + 30
|
|
while process.poll() is None:
|
|
if time.time() > deadline:
|
|
process.kill()
|
|
raise RuntimeError(f"{program.name} {' '.join(arguments)} did not finish")
|
|
time.sleep(1)
|
|
if watch:
|
|
capture = watch.with_suffix(".capture.png")
|
|
if subprocess.run(["grim", str(capture)], env=environment(), timeout=20).returncode == 0:
|
|
area = visible_area(capture)
|
|
if area > best:
|
|
best = area
|
|
shutil.copy(capture, watch)
|
|
capture.unlink(missing_ok=True)
|
|
return process.returncode
|
|
|
|
|
|
def bundle_log(prefix, evidence, step):
|
|
"""The bundle's main log of the last run, copied into the evidence; its packages' logs go along."""
|
|
temp = next((prefix / "drive_c" / "Users").glob("*/AppData/Local/Temp"))
|
|
logs = sorted(temp.glob("dd_vcredist_*.log"), key=lambda path: path.stat().st_mtime)
|
|
main = [log for log in logs if "_0" not in log.stem.split("_", 3)[-1]]
|
|
if not main:
|
|
raise RuntimeError(f"{step}: the bundle wrote no log")
|
|
for log in logs:
|
|
shutil.copy(log, evidence / f"{step}-{log.name}")
|
|
return main[-1].read_text(encoding="utf-8", errors="replace")
|
|
|
|
|
|
def main():
|
|
raven, installer, checker, folder, evidence = map(Path, sys.argv[1:6])
|
|
evidence.mkdir(parents=True, exist_ok=True)
|
|
prefix = evidence / "prefix"
|
|
shutil.rmtree(prefix, ignore_errors=True)
|
|
system = prefix / "drive_c" / "windows" / str(folder)
|
|
log = (evidence / "compositor.log").open("wb")
|
|
compositor = start_compositor(log)
|
|
try:
|
|
# Install with the progress window, from a prefix that has never seen the runtime.
|
|
code = run(raven, prefix, installer, ["/install", "/passive", "/norestart"], evidence / "install.log", evidence / "progress.png")
|
|
if code != 0:
|
|
raise RuntimeError(f"install ended with status {code}")
|
|
text = bundle_log(prefix, evidence, "install")
|
|
if "Apply complete, result: 0x0" not in text or "Exit code: 0x0" not in text:
|
|
raise RuntimeError("the bundle log does not report a complete install")
|
|
if not (evidence / "progress.png").exists() or visible_area(evidence / "progress.png") < 20000:
|
|
raise RuntimeError("the progress window never showed")
|
|
for name in ("vcruntime140.dll", "msvcp140.dll"):
|
|
if not (system / name).exists():
|
|
raise RuntimeError(f"{name} is not in {folder}")
|
|
code = run(raven, prefix, checker, [], evidence / "check-installed.log", timeout=60)
|
|
if code != 0 or b"Native entry returned 0 " not in (evidence / "check-installed.log").read_bytes():
|
|
raise RuntimeError(f"the installed runtime is not what programs see (status {code})")
|
|
|
|
# Installing again over the installed copy succeeds and leaves it in place.
|
|
code = run(raven, prefix, installer, ["/install", "/quiet", "/norestart"], evidence / "reinstall.log")
|
|
if code not in (0, 3010):
|
|
raise RuntimeError(f"reinstall ended with status {code}")
|
|
bundle_log(prefix, evidence, "reinstall")
|
|
if not (system / "vcruntime140.dll").exists():
|
|
raise RuntimeError("reinstalling removed the runtime")
|
|
|
|
# Removal takes the files and the runtime key away.
|
|
code = run(raven, prefix, installer, ["/uninstall", "/quiet", "/norestart"], evidence / "uninstall.log")
|
|
if code not in (0, 3010):
|
|
raise RuntimeError(f"uninstall ended with status {code}")
|
|
bundle_log(prefix, evidence, "uninstall")
|
|
code = run(raven, prefix, checker, ["--removed"], evidence / "check-removed.log", timeout=60)
|
|
if code != 0 or b"Native entry returned 0 " not in (evidence / "check-removed.log").read_bytes():
|
|
raise RuntimeError(f"the runtime is still installed after removal (status {code})")
|
|
finally:
|
|
if compositor:
|
|
compositor.terminate()
|
|
compositor.wait(timeout=10)
|
|
print("PASS: installed with its progress window, checked, reinstalled and removed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|