marionette.scm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2016, 2017 Ludovic Courtès <ludo@gnu.org>
  3. ;;;
  4. ;;; This file is part of GNU Guix.
  5. ;;;
  6. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  7. ;;; under the terms of the GNU General Public License as published by
  8. ;;; the Free Software Foundation; either version 3 of the License, or (at
  9. ;;; your option) any later version.
  10. ;;;
  11. ;;; GNU Guix is distributed in the hope that it will be useful, but
  12. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;;; GNU General Public License for more details.
  15. ;;;
  16. ;;; You should have received a copy of the GNU General Public License
  17. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  18. (define-module (gnu build marionette)
  19. #:use-module (srfi srfi-9)
  20. #:use-module (srfi srfi-26)
  21. #:use-module (rnrs io ports)
  22. #:use-module (ice-9 match)
  23. #:use-module (ice-9 popen)
  24. #:export (marionette?
  25. make-marionette
  26. marionette-eval
  27. wait-for-file
  28. marionette-control
  29. marionette-screen-text
  30. wait-for-screen-text
  31. %qwerty-us-keystrokes
  32. marionette-type))
  33. ;;; Commentary:
  34. ;;;
  35. ;;; Instrumentation tools for QEMU virtual machines (VMs). A "marionette" is
  36. ;;; essentially a VM (a QEMU instance) with its monitor connected to a
  37. ;;; Unix-domain socket, and with a REPL inside the guest listening on a
  38. ;;; virtual console, which is itself connected to the host via a Unix-domain
  39. ;;; socket--these are the marionette's strings, connecting it to the almighty
  40. ;;; puppeteer.
  41. ;;;
  42. ;;; Code:
  43. (define-record-type <marionette>
  44. (marionette command pid monitor repl)
  45. marionette?
  46. (command marionette-command) ;list of strings
  47. (pid marionette-pid) ;integer
  48. (monitor marionette-monitor) ;port
  49. (repl %marionette-repl)) ;promise of a port
  50. (define-syntax-rule (marionette-repl marionette)
  51. (force (%marionette-repl marionette)))
  52. (define* (wait-for-monitor-prompt port #:key (quiet? #t))
  53. "Read from PORT until we have seen all of QEMU's monitor prompt. When
  54. QUIET? is false, the monitor's output is written to the current output port."
  55. (define full-prompt
  56. (string->list "(qemu) "))
  57. (let loop ((prompt full-prompt)
  58. (matches '())
  59. (prefix '()))
  60. (match prompt
  61. (()
  62. ;; It's useful to set QUIET? so we don't display the echo of our own
  63. ;; commands.
  64. (unless quiet?
  65. (for-each (lambda (line)
  66. (format #t "qemu monitor: ~a~%" line))
  67. (string-tokenize (list->string (reverse prefix))
  68. (char-set-complement (char-set #\newline))))))
  69. ((chr rest ...)
  70. (let ((read (read-char port)))
  71. (cond ((eqv? read chr)
  72. (loop rest (cons read matches) prefix))
  73. ((eof-object? read)
  74. (error "EOF while waiting for QEMU monitor prompt"
  75. (list->string (reverse prefix))))
  76. (else
  77. (loop full-prompt
  78. '()
  79. (cons read (append matches prefix))))))))))
  80. (define* (make-marionette command
  81. #:key (socket-directory "/tmp") (timeout 20))
  82. "Return a QEMU marionette--i.e., a virtual machine with open connections to the
  83. QEMU monitor and to the guest's backdoor REPL."
  84. (define (file->sockaddr file)
  85. (make-socket-address AF_UNIX
  86. (string-append socket-directory "/" file)))
  87. (define extra-options
  88. (list "-nographic"
  89. "-monitor" (string-append "unix:" socket-directory "/monitor")
  90. "-chardev" (string-append "socket,id=repl,path=" socket-directory
  91. "/repl")
  92. "-device" "virtio-serial"
  93. "-device" "virtconsole,chardev=repl"))
  94. (define (accept* port)
  95. (match (select (list port) '() (list port) timeout)
  96. (((port) () ())
  97. (accept port))
  98. (_
  99. (error "timeout in 'accept'" port))))
  100. (let ((monitor (socket AF_UNIX SOCK_STREAM 0))
  101. (repl (socket AF_UNIX SOCK_STREAM 0)))
  102. (bind monitor (file->sockaddr "monitor"))
  103. (listen monitor 1)
  104. (bind repl (file->sockaddr "repl"))
  105. (listen repl 1)
  106. (match (primitive-fork)
  107. (0
  108. (catch #t
  109. (lambda ()
  110. (close monitor)
  111. (close repl)
  112. (match command
  113. ((program . args)
  114. (apply execl program program
  115. (append args extra-options)))))
  116. (lambda (key . args)
  117. (print-exception (current-error-port)
  118. (stack-ref (make-stack #t) 1)
  119. key args)
  120. (primitive-exit 1))))
  121. (pid
  122. (format #t "QEMU runs as PID ~a~%" pid)
  123. (match (accept* monitor)
  124. ((monitor-conn . _)
  125. (display "connected to QEMU's monitor\n")
  126. (close-port monitor)
  127. (wait-for-monitor-prompt monitor-conn)
  128. (display "read QEMU monitor prompt\n")
  129. (marionette (append command extra-options) pid
  130. monitor-conn
  131. ;; The following 'accept' call connects immediately, but
  132. ;; we don't know whether the guest has connected until
  133. ;; we actually receive the 'ready' message.
  134. (match (accept* repl)
  135. ((repl-conn . addr)
  136. (display "connected to guest REPL\n")
  137. (close-port repl)
  138. ;; Delay reception of the 'ready' message so that the
  139. ;; caller can already send monitor commands.
  140. (delay
  141. (match (read repl-conn)
  142. ('ready
  143. (display "marionette is ready\n")
  144. repl-conn))))))))))))
  145. (define (marionette-eval exp marionette)
  146. "Evaluate EXP in MARIONETTE's backdoor REPL. Return the result."
  147. (match marionette
  148. (($ <marionette> command pid monitor (= force repl))
  149. (write exp repl)
  150. (newline repl)
  151. (read repl))))
  152. (define* (wait-for-file file marionette
  153. #:key (timeout 10) (read 'read))
  154. "Wait until FILE exists in MARIONETTE; READ its content and return it. If
  155. FILE has not shown up after TIMEOUT seconds, raise an error."
  156. (match (marionette-eval
  157. `(let loop ((i ,timeout))
  158. (cond ((file-exists? ,file)
  159. (cons 'success (call-with-input-file ,file ,read)))
  160. ((> i 0)
  161. (sleep 1)
  162. (loop (- i 1)))
  163. (else
  164. 'failure)))
  165. marionette)
  166. (('success . result)
  167. result)
  168. ('failure
  169. (error "file didn't show up" file))))
  170. (define (marionette-control command marionette)
  171. "Run COMMAND in the QEMU monitor of MARIONETTE. COMMAND is a string such as
  172. \"sendkey ctrl-alt-f1\" or \"screendump foo.ppm\" (info \"(qemu-doc)
  173. pcsys_monitor\")."
  174. (match marionette
  175. (($ <marionette> _ _ monitor)
  176. (display command monitor)
  177. (newline monitor)
  178. (wait-for-monitor-prompt monitor))))
  179. (define* (marionette-screen-text marionette
  180. #:key
  181. (ocrad "ocrad"))
  182. "Take a screenshot of MARIONETTE, perform optical character
  183. recognition (OCR), and return the text read from the screen as a string. Do
  184. this by invoking OCRAD (file name for GNU Ocrad's command)"
  185. (define (random-file-name)
  186. (string-append "/tmp/marionette-screenshot-"
  187. (number->string (random (expt 2 32)) 16)
  188. ".ppm"))
  189. (let ((image (random-file-name)))
  190. (dynamic-wind
  191. (const #t)
  192. (lambda ()
  193. (marionette-control (string-append "screendump " image)
  194. marionette)
  195. ;; Tell Ocrad to invert the image colors (make it black on white) and
  196. ;; to scale the image up, which significantly improves the quality of
  197. ;; the result. In spite of this, be aware that OCR confuses "y" and
  198. ;; "V" and sometimes erroneously introduces white space.
  199. (let* ((pipe (open-pipe* OPEN_READ ocrad
  200. "-i" "-s" "10" image))
  201. (text (get-string-all pipe)))
  202. (unless (zero? (close-pipe pipe))
  203. (error "'ocrad' failed" ocrad))
  204. text))
  205. (lambda ()
  206. (false-if-exception (delete-file image))))))
  207. (define* (wait-for-screen-text marionette predicate
  208. #:key (timeout 30) (ocrad "ocrad"))
  209. "Wait for TIMEOUT seconds or until the screen text on MARIONETTE matches
  210. PREDICATE, whichever comes first. Raise an error when TIMEOUT is exceeded."
  211. (define start
  212. (car (gettimeofday)))
  213. (define end
  214. (+ start timeout))
  215. (let loop ()
  216. (if (> (car (gettimeofday)) end)
  217. (error "'wait-for-screen-text' timeout" predicate)
  218. (or (predicate (marionette-screen-text marionette #:ocrad ocrad))
  219. (begin
  220. (sleep 1)
  221. (loop))))))
  222. (define %qwerty-us-keystrokes
  223. ;; Maps "special" characters to their keystrokes.
  224. '((#\newline . "ret")
  225. (#\space . "spc")
  226. (#\- . "minus")
  227. (#\+ . "shift-equal")
  228. (#\* . "shift-8")
  229. (#\= . "equal")
  230. (#\? . "shift-slash")
  231. (#\[ . "bracket_left")
  232. (#\] . "bracket_right")
  233. (#\( . "shift-9")
  234. (#\) . "shift-0")
  235. (#\/ . "slash")
  236. (#\< . "less")
  237. (#\> . "shift-less")
  238. (#\. . "dot")
  239. (#\, . "comma")
  240. (#\; . "semicolon")
  241. (#\' . "apostrophe")
  242. (#\" . "shift-apostrophe")
  243. (#\` . "grave_accent")
  244. (#\bs . "backspace")
  245. (#\tab . "tab")))
  246. (define (character->keystroke chr keystrokes)
  247. "Return the keystroke for CHR according to the keyboard layout defined by
  248. KEYSTROKES."
  249. (if (char-set-contains? char-set:upper-case chr)
  250. (string-append "shift-" (string (char-downcase chr)))
  251. (or (assoc-ref keystrokes chr)
  252. (string chr))))
  253. (define* (string->keystroke-commands str
  254. #:optional
  255. (keystrokes
  256. %qwerty-us-keystrokes))
  257. "Return a list of QEMU monitor commands to send the keystrokes corresponding
  258. to STR. KEYSTROKES is an alist specifying a mapping from characters to
  259. keystrokes."
  260. (string-fold-right (lambda (chr result)
  261. (cons (string-append
  262. "sendkey "
  263. (character->keystroke chr keystrokes))
  264. result))
  265. '()
  266. str))
  267. (define* (marionette-type str marionette
  268. #:key (keystrokes %qwerty-us-keystrokes))
  269. "Type STR on MARIONETTE's keyboard, using the KEYSTROKES alist to map characters
  270. to actual keystrokes."
  271. (for-each (cut marionette-control <> marionette)
  272. (string->keystroke-commands str keystrokes)))
  273. ;;; marionette.scm ends here