marionette.scm 13 KB

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