| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- #!/usr/bin/env python3
- """Record the QEMU VGA console through QMP screendump.
- Captures at a fixed target rate, writes JPEG frames plus index.csv, and sends
- space keys during the initial boot so the Windows ISO's "press any key" prompt
- does not fall through to an empty disk.
- """
- import argparse
- import csv
- import datetime as dt
- import os
- import shutil
- import signal
- import subprocess
- import sys
- import time
- from qmp import QMP
- def log(message):
- stamp = dt.datetime.now().strftime("%H:%M:%S")
- print("[%s] %s" % (stamp, message), flush=True)
- def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("--qmp", required=True)
- parser.add_argument("--out", required=True)
- parser.add_argument("--interval", type=float, default=0.125)
- parser.add_argument("--quality", type=int, default=82)
- parser.add_argument("--tmp", default="/dev/shm/pcl-poc-frame.ppm")
- parser.add_argument("--boot-keys", action="store_true")
- parser.add_argument("--timeout", type=float, default=3600.0)
- args = parser.parse_args()
- os.makedirs(args.out, exist_ok=True)
- convert = shutil.which("convert")
- if not convert:
- log("ImageMagick convert not found")
- return 2
- qmp = None
- deadline = time.time() + 30.0
- while time.time() < deadline:
- try:
- qmp = QMP(args.qmp)
- log("QMP connected")
- break
- except Exception as exc:
- log("waiting for QMP: %s" % exc)
- time.sleep(0.2)
- if qmp is None:
- log("could not connect to QMP")
- return 2
- index_path = os.path.join(args.out, "index.csv")
- index = open(index_path, "w", newline="")
- writer = csv.writer(index)
- writer.writerow(["frame", "epoch_ms", "iso"])
- index.flush()
- stopped = {"flag": False}
- def stop(_signum, _frame):
- stopped["flag"] = True
- signal.signal(signal.SIGTERM, stop)
- signal.signal(signal.SIGINT, stop)
- start = time.time()
- next_at = start
- frame = 0
- key_at = [2.5, 3.5, 4.5, 5.5, 6.5] if args.boot_keys else []
- key_sent = set()
- failures = 0
- log("recording to %s (interval %.3fs)" % (args.out, args.interval))
- try:
- while not stopped["flag"] and time.time() - start < args.timeout:
- now = time.time()
- for when in key_at:
- if now - start >= when and when not in key_sent:
- key_sent.add(when)
- try:
- qmp.sendkey("spc")
- log("sent boot key at %.1fs" % (now - start))
- except Exception as exc:
- log("sendkey failed: %s" % exc)
- if now < next_at:
- time.sleep(next_at - now)
- target = os.path.join(args.out, "frame-%06d.jpg" % frame)
- try:
- response = qmp.screendump(args.tmp)
- if "error" in response:
- raise RuntimeError(response["error"])
- subprocess.run(
- [convert, args.tmp, "-quality", str(args.quality), target],
- check=True,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- env=dict(os.environ, MAGICK_THREAD_LIMIT="1"),
- )
- stamp = time.time()
- writer.writerow(
- [
- frame,
- int(stamp * 1000),
- dt.datetime.fromtimestamp(stamp).isoformat(timespec="milliseconds"),
- ]
- )
- index.flush()
- frame += 1
- failures = 0
- except EOFError:
- log("QMP closed; guest likely shut down")
- break
- except Exception as exc:
- failures += 1
- if failures <= 3 or failures % 50 == 0:
- log("capture error (%d): %s" % (failures, exc))
- time.sleep(0.2)
- next_at += args.interval
- if next_at < time.time() - args.interval:
- next_at = time.time() + args.interval
- finally:
- index.close()
- qmp.close()
- log("stopped after %d frames, %.1fs" % (frame, time.time() - start))
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|