1
0

record.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. #!/usr/bin/env python3
  2. """Record the QEMU VGA console through QMP screendump.
  3. Captures at a fixed target rate, writes JPEG frames plus index.csv, and sends
  4. space keys during the initial boot so the Windows ISO's "press any key" prompt
  5. does not fall through to an empty disk.
  6. """
  7. import argparse
  8. import csv
  9. import datetime as dt
  10. import os
  11. import shutil
  12. import signal
  13. import subprocess
  14. import sys
  15. import time
  16. from qmp import QMP
  17. def log(message):
  18. stamp = dt.datetime.now().strftime("%H:%M:%S")
  19. print("[%s] %s" % (stamp, message), flush=True)
  20. def main():
  21. parser = argparse.ArgumentParser()
  22. parser.add_argument("--qmp", required=True)
  23. parser.add_argument("--out", required=True)
  24. parser.add_argument("--interval", type=float, default=0.125)
  25. parser.add_argument("--quality", type=int, default=82)
  26. parser.add_argument("--tmp", default="/dev/shm/pcl-poc-frame.ppm")
  27. parser.add_argument("--boot-keys", action="store_true")
  28. parser.add_argument("--timeout", type=float, default=3600.0)
  29. args = parser.parse_args()
  30. os.makedirs(args.out, exist_ok=True)
  31. convert = shutil.which("convert")
  32. if not convert:
  33. log("ImageMagick convert not found")
  34. return 2
  35. qmp = None
  36. deadline = time.time() + 30.0
  37. while time.time() < deadline:
  38. try:
  39. qmp = QMP(args.qmp)
  40. log("QMP connected")
  41. break
  42. except Exception as exc:
  43. log("waiting for QMP: %s" % exc)
  44. time.sleep(0.2)
  45. if qmp is None:
  46. log("could not connect to QMP")
  47. return 2
  48. index_path = os.path.join(args.out, "index.csv")
  49. index = open(index_path, "w", newline="")
  50. writer = csv.writer(index)
  51. writer.writerow(["frame", "epoch_ms", "iso"])
  52. index.flush()
  53. stopped = {"flag": False}
  54. def stop(_signum, _frame):
  55. stopped["flag"] = True
  56. signal.signal(signal.SIGTERM, stop)
  57. signal.signal(signal.SIGINT, stop)
  58. start = time.time()
  59. next_at = start
  60. frame = 0
  61. key_at = [2.5, 3.5, 4.5, 5.5, 6.5] if args.boot_keys else []
  62. key_sent = set()
  63. failures = 0
  64. log("recording to %s (interval %.3fs)" % (args.out, args.interval))
  65. try:
  66. while not stopped["flag"] and time.time() - start < args.timeout:
  67. now = time.time()
  68. for when in key_at:
  69. if now - start >= when and when not in key_sent:
  70. key_sent.add(when)
  71. try:
  72. qmp.sendkey("spc")
  73. log("sent boot key at %.1fs" % (now - start))
  74. except Exception as exc:
  75. log("sendkey failed: %s" % exc)
  76. if now < next_at:
  77. time.sleep(next_at - now)
  78. target = os.path.join(args.out, "frame-%06d.jpg" % frame)
  79. try:
  80. response = qmp.screendump(args.tmp)
  81. if "error" in response:
  82. raise RuntimeError(response["error"])
  83. subprocess.run(
  84. [convert, args.tmp, "-quality", str(args.quality), target],
  85. check=True,
  86. stdout=subprocess.DEVNULL,
  87. stderr=subprocess.DEVNULL,
  88. env=dict(os.environ, MAGICK_THREAD_LIMIT="1"),
  89. )
  90. stamp = time.time()
  91. writer.writerow(
  92. [
  93. frame,
  94. int(stamp * 1000),
  95. dt.datetime.fromtimestamp(stamp).isoformat(timespec="milliseconds"),
  96. ]
  97. )
  98. index.flush()
  99. frame += 1
  100. failures = 0
  101. except EOFError:
  102. log("QMP closed; guest likely shut down")
  103. break
  104. except Exception as exc:
  105. failures += 1
  106. if failures <= 3 or failures % 50 == 0:
  107. log("capture error (%d): %s" % (failures, exc))
  108. time.sleep(0.2)
  109. next_at += args.interval
  110. if next_at < time.time() - args.interval:
  111. next_at = time.time() + args.interval
  112. finally:
  113. index.close()
  114. qmp.close()
  115. log("stopped after %d frames, %.1fs" % (frame, time.time() - start))
  116. return 0
  117. if __name__ == "__main__":
  118. sys.exit(main())