mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-10-28 21:25:06 +00:00
200 lines
9.0 KiB
Python
200 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Install real Windows programs with their own installers, then start them, under Raven and under Wine, and compare.
|
|
|
|
Each case runs its installer silently in a fresh prefix of each runtime, on a headless labwc of its own (Wine through its Wayland driver),
|
|
checks that the program's file is where the installer puts it on Windows, starts the program and waits for its main window. What each
|
|
runtime achieved, how long it took and a screenshot of the window go into a table, results.md, which the script writes again on every run.
|
|
A case passes for Raven when Raven installs and starts the program and does at least as well as Wine: no step Wine completes may fail,
|
|
nor take more than twice as long.
|
|
|
|
usage: native-app-matrix.py RAVEN APPS_DIR EVIDENCE_DIR [CASE...]
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import os
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
sys.dont_write_bytecode = True
|
|
import importlib.util
|
|
_spec = importlib.util.spec_from_file_location("native_redist", Path(__file__).with_name("native-redist.py"))
|
|
native_redist = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(native_redist)
|
|
|
|
WINESERVER = next((str(path) for path in (Path("/usr/lib/wine/wineserver"), Path("/usr/lib/x86_64-linux-gnu/wine/wineserver"))
|
|
if path.exists()), "wineserver")
|
|
|
|
# The case, the installer and how it runs silently (an MSI goes through msiexec), the program it installs, and the words the title of
|
|
# its main window holds; None for a program whose title is the folder it shows.
|
|
CASES = (
|
|
("7zip", "7z2409-x64.exe", ["/S"], r"C:\Program Files\7-Zip\7zFM.exe", None),
|
|
("7zip-msi", "7z2409-x64.msi", ["/qn"], r"C:\Program Files\7-Zip\7zFM.exe", None),
|
|
("notepadpp", "npp.8.7.1.Installer.x64.exe", ["/S"], r"C:\Program Files\Notepad++\notepad++.exe", "Notepad++"),
|
|
("vlc", "vlc-3.0.21-win64.exe", ["/S", "/L=1033"], r"C:\Program Files\VideoLAN\VLC\vlc.exe", "VLC media player"),
|
|
("audacity", "audacity-win-3.6.4-64bit.exe", ["/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"], r"C:\Program Files\Audacity\Audacity.exe",
|
|
"Audacity"),
|
|
("keepass", "KeePass-2.57-Setup.exe", ["/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"],
|
|
r"C:\Program Files\KeePass Password Safe 2\KeePass.exe", "KeePass"),
|
|
("irfanview", "iview467_x64_setup.exe", ["/silent", r"/folder=C:\Program Files\IrfanView"], r"C:\Program Files\IrfanView\i_view64.exe",
|
|
"IrfanView"),
|
|
)
|
|
|
|
|
|
def host_path(prefix, windows):
|
|
"""Where a C: path of a prefix lies on the host."""
|
|
relative = windows[3:].replace("\\", "/")
|
|
return prefix / "drive_c" / relative
|
|
|
|
|
|
def processes_with(marker):
|
|
found = []
|
|
for entry in Path("/proc").iterdir():
|
|
if entry.name.isdigit():
|
|
try:
|
|
if marker.encode() in (entry / "environ").read_bytes():
|
|
found.append(int(entry.name))
|
|
except OSError:
|
|
pass
|
|
return found
|
|
|
|
|
|
def stop(marker):
|
|
for pid in processes_with(marker):
|
|
try:
|
|
os.kill(pid, signal.SIGKILL)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def window_titles():
|
|
probe = subprocess.run(["wlrctl", "toplevel", "list"], env=native_redist.environment(), capture_output=True, text=True, timeout=10)
|
|
return probe.stdout if probe.returncode == 0 else ""
|
|
|
|
|
|
class Runtime:
|
|
def __init__(self, name, raven, work):
|
|
self.name, self.raven, self.prefix = name, raven, work / f"{name}-prefix"
|
|
self.marker = f"RAVEN_MATRIX_RUN={name}-{os.getpid()}"
|
|
|
|
def environment(self):
|
|
env = native_redist.environment() | {self.marker.split("=")[0]: self.marker.split("=")[1]}
|
|
if self.name == "wine":
|
|
env |= {"WINEPREFIX": str(self.prefix), "WINEDEBUG": "-all", "WINEDLLOVERRIDES": "mscoree=;mshtml=", "HOME": str(self.prefix.parent)}
|
|
else:
|
|
env |= {"RAVEN_PREFIX": str(self.prefix)}
|
|
return env
|
|
|
|
def command(self, program, arguments, wait):
|
|
if self.name == "wine":
|
|
return ["wine", str(program), *arguments]
|
|
command = [str(self.raven), "--run-process", "--execution=native", "--timeout-ms=900000", "--desktop=wayland", str(program)]
|
|
return command + (["--", *arguments] if arguments else [])
|
|
|
|
def fresh(self):
|
|
shutil.rmtree(self.prefix, ignore_errors=True)
|
|
if self.name == "wine":
|
|
env = self.environment()
|
|
subprocess.run(["wineboot", "-i"], env=env, capture_output=True, timeout=300)
|
|
subprocess.run(["wine", "reg", "add", "HKCU\\Software\\Wine\\Drivers", "/v", "Graphics", "/d", "wayland", "/f"], env=env,
|
|
capture_output=True, timeout=120)
|
|
subprocess.run([WINESERVER, "-w"], env=env, timeout=120)
|
|
|
|
def finish(self):
|
|
stop(self.marker)
|
|
if self.name == "wine":
|
|
subprocess.run([WINESERVER, "-k"], env=self.environment(), capture_output=True, timeout=60)
|
|
|
|
|
|
def install(runtime, installer, arguments, log):
|
|
"""Runs the installer to its end; the seconds it took, or None when it failed or never ended."""
|
|
if installer.suffix == ".msi":
|
|
program = "msiexec" if runtime.name == "wine" else runtime.raven.parent / "msiexec.exe"
|
|
arguments = ["/i", "Z:" + str(installer).replace("/", "\\"), *arguments]
|
|
else:
|
|
program = installer
|
|
start = time.time()
|
|
with log.open("wb") as output:
|
|
process = subprocess.Popen(runtime.command(program, arguments, True), env=runtime.environment(), stdout=output, stderr=output)
|
|
try:
|
|
process.wait(timeout=900)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
return None
|
|
if runtime.name == "wine":
|
|
subprocess.run([WINESERVER, "-w"], env=runtime.environment(), timeout=300)
|
|
return time.time() - start
|
|
|
|
|
|
def start(runtime, program, title, picture, log):
|
|
"""Starts the program and waits for a window whose title holds `title`; the seconds that took, or None."""
|
|
began = time.time()
|
|
with log.open("wb") as output:
|
|
subprocess.Popen(runtime.command(program, [], False), env=runtime.environment(), stdout=output, stderr=output)
|
|
deadline = began + 120
|
|
while time.time() < deadline:
|
|
listed = window_titles()
|
|
if (title.lower() in listed.lower()) if title else listed.strip():
|
|
waited = time.time() - began
|
|
time.sleep(2)
|
|
subprocess.run(["grim", str(picture)], env=native_redist.environment(), timeout=30)
|
|
return waited
|
|
time.sleep(0.5)
|
|
return None
|
|
|
|
|
|
def seconds(value):
|
|
return "failed" if value is None else f"{value:.1f} s"
|
|
|
|
|
|
def main():
|
|
raven, apps, evidence = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3])
|
|
wanted = set(sys.argv[4:])
|
|
evidence.mkdir(parents=True, exist_ok=True)
|
|
log = (evidence / "compositor.log").open("wb")
|
|
compositor = native_redist.start_compositor(log)
|
|
rows, failures = [], []
|
|
try:
|
|
for name, installer, arguments, program, title in CASES:
|
|
if wanted and name not in wanted:
|
|
continue
|
|
results = {}
|
|
for runtime in (Runtime("raven", raven, evidence / name), Runtime("wine", raven, evidence / name)):
|
|
(evidence / name).mkdir(parents=True, exist_ok=True)
|
|
runtime.fresh()
|
|
took = install(runtime, apps / installer, arguments, evidence / name / f"{runtime.name}-install.log")
|
|
installed = took is not None and host_path(runtime.prefix, program).exists()
|
|
shown = None
|
|
if installed:
|
|
target = program if runtime.name == "wine" else host_path(runtime.prefix, program)
|
|
shown = start(runtime, target, title, evidence / name / f"{runtime.name}.png", evidence / name / f"{runtime.name}-start.log")
|
|
runtime.finish()
|
|
results[runtime.name] = (took if installed else None, shown)
|
|
raven_install, raven_start = results["raven"]
|
|
wine_install, wine_start = results["wine"]
|
|
behind = (raven_install is None or raven_start is None or
|
|
(wine_install is not None and raven_install > 2 * wine_install + 5) or
|
|
(wine_start is not None and raven_start > 2 * wine_start + 5))
|
|
if behind:
|
|
failures.append(name)
|
|
rows.append(f"| {name} | {seconds(raven_install)} | {seconds(wine_install)} | {seconds(raven_start)} | {seconds(wine_start)} | "
|
|
f"{'no' if behind else 'yes'} |")
|
|
finally:
|
|
if compositor:
|
|
compositor.terminate()
|
|
compositor.wait(timeout=10)
|
|
table = ["| Case | Raven install | Wine install | Raven start | Wine start | Raven at least as good |",
|
|
"|---|---|---|---|---|---|", *rows]
|
|
(evidence / "results.md").write_text("\n".join(table) + "\n")
|
|
print("\n".join(table))
|
|
if failures:
|
|
print("FAIL: " + ", ".join(failures))
|
|
sys.exit(1)
|
|
print("PASS: every case installed and started, at least as well as under Wine")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|