marionette.scm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2016, 2017, 2018, 2019, 2020, 2021 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 (srfi srfi-64)
  23. #:use-module (rnrs io ports)
  24. #:use-module (ice-9 match)
  25. #:use-module (ice-9 popen)
  26. #:use-module (ice-9 regex)
  27. #:export (marionette?
  28. make-marionette
  29. marionette-eval
  30. wait-for-file
  31. wait-for-tcp-port
  32. wait-for-unix-socket
  33. marionette-control
  34. marionette-screen-text
  35. wait-for-screen-text
  36. %qwerty-us-keystrokes
  37. marionette-type
  38. system-test-runner
  39. qemu-command))
  40. ;;; Commentary:
  41. ;;;
  42. ;;; Instrumentation tools for QEMU virtual machines (VMs). A "marionette" is
  43. ;;; essentially a VM (a QEMU instance) with its monitor connected to a
  44. ;;; Unix-domain socket, and with a REPL inside the guest listening on a
  45. ;;; virtual console, which is itself connected to the host via a Unix-domain
  46. ;;; socket--these are the marionette's strings, connecting it to the almighty
  47. ;;; puppeteer.
  48. ;;;
  49. ;;; Code:
  50. (define-record-type <marionette>
  51. (marionette command pid monitor repl)
  52. marionette?
  53. (command marionette-command) ;list of strings
  54. (pid marionette-pid) ;integer
  55. (monitor marionette-monitor) ;port
  56. (repl %marionette-repl)) ;promise of a port
  57. (define-syntax-rule (marionette-repl marionette)
  58. (force (%marionette-repl marionette)))
  59. (define* (wait-for-monitor-prompt port #:key (quiet? #t))
  60. "Read from PORT until we have seen all of QEMU's monitor prompt. When
  61. QUIET? is false, the monitor's output is written to the current output port."
  62. (define full-prompt
  63. (string->list "(qemu) "))
  64. (let loop ((prompt full-prompt)
  65. (matches '())
  66. (prefix '()))
  67. (match prompt
  68. (()
  69. ;; It's useful to set QUIET? so we don't display the echo of our own
  70. ;; commands.
  71. (unless quiet?
  72. (for-each (lambda (line)
  73. (format #t "qemu monitor: ~a~%" line))
  74. (string-tokenize (list->string (reverse prefix))
  75. (char-set-complement (char-set #\newline))))))
  76. ((chr rest ...)
  77. (let ((read (read-char port)))
  78. (cond ((eqv? read chr)
  79. (loop rest (cons read matches) prefix))
  80. ((eof-object? read)
  81. (error "EOF while waiting for QEMU monitor prompt"
  82. (list->string (reverse prefix))))
  83. (else
  84. (loop full-prompt
  85. '()
  86. (cons read (append matches prefix))))))))))
  87. (define* (make-marionette command
  88. #:key (socket-directory "/tmp") (timeout 20))
  89. "Return a QEMU marionette--i.e., a virtual machine with open connections to the
  90. QEMU monitor and to the guest's backdoor REPL."
  91. (define (file->sockaddr file)
  92. (make-socket-address AF_UNIX
  93. (string-append socket-directory "/" file)))
  94. (define extra-options
  95. (list "-nographic"
  96. "-monitor" (string-append "unix:" socket-directory "/monitor")
  97. "-chardev" (string-append "socket,id=repl,path=" socket-directory
  98. "/repl")
  99. ;; See
  100. ;; <http://www.linux-kvm.org/page/VMchannel_Requirements#Invocation>.
  101. "-device" "virtio-serial"
  102. "-device" "virtserialport,chardev=repl,name=org.gnu.guix.port.0"))
  103. (define (accept* port)
  104. (match (select (list port) '() (list port) timeout)
  105. (((port) () ())
  106. (accept port))
  107. (_
  108. (error "timeout in 'accept'" port))))
  109. (let ((monitor (socket AF_UNIX SOCK_STREAM 0))
  110. (repl (socket AF_UNIX SOCK_STREAM 0)))
  111. (bind monitor (file->sockaddr "monitor"))
  112. (listen monitor 1)
  113. (bind repl (file->sockaddr "repl"))
  114. (listen repl 1)
  115. (match (primitive-fork)
  116. (0
  117. (catch #t
  118. (lambda ()
  119. (close monitor)
  120. (close repl)
  121. (match command
  122. ((program . args)
  123. (apply execl program program
  124. (append args extra-options)))))
  125. (lambda (key . args)
  126. (print-exception (current-error-port)
  127. (stack-ref (make-stack #t) 1)
  128. key args)
  129. (primitive-exit 1))))
  130. (pid
  131. (format #t "QEMU runs as PID ~a~%" pid)
  132. (match (accept* monitor)
  133. ((monitor-conn . _)
  134. (display "connected to QEMU's monitor\n")
  135. (close-port monitor)
  136. (wait-for-monitor-prompt monitor-conn)
  137. (display "read QEMU monitor prompt\n")
  138. (marionette (append command extra-options) pid
  139. monitor-conn
  140. ;; The following 'accept' call connects immediately, but
  141. ;; we don't know whether the guest has connected until
  142. ;; we actually receive the 'ready' message.
  143. (match (accept* repl)
  144. ((repl-conn . addr)
  145. (display "connected to guest REPL\n")
  146. (close-port repl)
  147. ;; Delay reception of the 'ready' message so that the
  148. ;; caller can already send monitor commands.
  149. (delay
  150. (match (read repl-conn)
  151. ('ready
  152. (display "marionette is ready\n")
  153. repl-conn))))))))))))
  154. (define (marionette-eval exp marionette)
  155. "Evaluate EXP in MARIONETTE's backdoor REPL. Return the result."
  156. (match marionette
  157. (($ <marionette> command pid monitor (= force repl))
  158. (write exp repl)
  159. (newline repl)
  160. (read repl))))
  161. (define* (wait-for-file file marionette
  162. #:key (timeout 10) (read 'read))
  163. "Wait until FILE exists in MARIONETTE; READ its content and return it. If
  164. FILE has not shown up after TIMEOUT seconds, raise an error."
  165. (match (marionette-eval
  166. `(let loop ((i ,timeout))
  167. (cond ((file-exists? ,file)
  168. (cons 'success (call-with-input-file ,file ,read)))
  169. ((> i 0)
  170. (sleep 1)
  171. (loop (- i 1)))
  172. (else
  173. 'failure)))
  174. marionette)
  175. (('success . result)
  176. result)
  177. ('failure
  178. (error "file didn't show up" file))))
  179. (define* (wait-for-tcp-port port marionette
  180. #:key (timeout 20))
  181. "Wait for up to TIMEOUT seconds for PORT to accept connections in
  182. MARIONETTE. Raise an error on failure."
  183. ;; Note: The 'connect' loop has to run within the guest because, when we
  184. ;; forward ports to the host, connecting to the host never raises
  185. ;; ECONNREFUSED.
  186. (match (marionette-eval
  187. `(begin
  188. (let ((sock (socket PF_INET SOCK_STREAM 0)))
  189. (let loop ((i 0))
  190. (catch 'system-error
  191. (lambda ()
  192. (connect sock AF_INET INADDR_LOOPBACK ,port)
  193. (close-port sock)
  194. 'success)
  195. (lambda args
  196. (if (< i ,timeout)
  197. (begin
  198. (sleep 1)
  199. (loop (+ 1 i)))
  200. 'failure))))))
  201. marionette)
  202. ('success #t)
  203. ('failure
  204. (error "nobody's listening on port" port))))
  205. (define* (wait-for-unix-socket file-name marionette
  206. #:key (timeout 20))
  207. "Wait for up to TIMEOUT seconds for FILE-NAME, a Unix domain socket, to
  208. accept connections in MARIONETTE. Raise an error on failure."
  209. (match (marionette-eval
  210. `(begin
  211. (let ((sock (socket PF_UNIX SOCK_STREAM 0)))
  212. (let loop ((i 0))
  213. (catch 'system-error
  214. (lambda ()
  215. (connect sock AF_UNIX ,file-name)
  216. (close-port sock)
  217. 'success)
  218. (lambda args
  219. (if (< i ,timeout)
  220. (begin
  221. (sleep 1)
  222. (loop (+ 1 i)))
  223. 'failure))))))
  224. marionette)
  225. ('success #t)
  226. ('failure
  227. (error "nobody's listening on unix domain socket" file-name))))
  228. (define (marionette-control command marionette)
  229. "Run COMMAND in the QEMU monitor of MARIONETTE. COMMAND is a string such as
  230. \"sendkey ctrl-alt-f1\" or \"screendump foo.ppm\" (info \"(qemu-doc)
  231. pcsys_monitor\")."
  232. (match marionette
  233. (($ <marionette> _ _ monitor)
  234. (display command monitor)
  235. (newline monitor)
  236. ;; The "quit" command terminates QEMU immediately, with no output.
  237. (unless (string=? command "quit") (wait-for-monitor-prompt monitor)))))
  238. (define* (marionette-screen-text marionette
  239. #:key
  240. (ocrad "ocrad"))
  241. "Take a screenshot of MARIONETTE, perform optical character
  242. recognition (OCR), and return the text read from the screen as a string. Do
  243. this by invoking OCRAD (file name for GNU Ocrad's command)"
  244. (define (random-file-name)
  245. (string-append "/tmp/marionette-screenshot-"
  246. (number->string (random (expt 2 32)) 16)
  247. ".ppm"))
  248. (let ((image (random-file-name)))
  249. (dynamic-wind
  250. (const #t)
  251. (lambda ()
  252. (marionette-control (string-append "screendump " image)
  253. marionette)
  254. ;; Tell Ocrad to invert the image colors (make it black on white) and
  255. ;; to scale the image up, which significantly improves the quality of
  256. ;; the result. In spite of this, be aware that OCR confuses "y" and
  257. ;; "V" and sometimes erroneously introduces white space.
  258. (let* ((pipe (open-pipe* OPEN_READ ocrad
  259. "-i" "-s" "10" image))
  260. (text (get-string-all pipe)))
  261. (unless (zero? (close-pipe pipe))
  262. (error "'ocrad' failed" ocrad))
  263. text))
  264. (lambda ()
  265. (false-if-exception (delete-file image))))))
  266. (define* (wait-for-screen-text marionette predicate
  267. #:key (timeout 30) (ocrad "ocrad"))
  268. "Wait for TIMEOUT seconds or until the screen text on MARIONETTE matches
  269. PREDICATE, whichever comes first. Raise an error when TIMEOUT is exceeded."
  270. (define start
  271. (car (gettimeofday)))
  272. (define end
  273. (+ start timeout))
  274. (let loop ()
  275. (if (> (car (gettimeofday)) end)
  276. (error "'wait-for-screen-text' timeout" predicate)
  277. (or (predicate (marionette-screen-text marionette #:ocrad ocrad))
  278. (begin
  279. (sleep 1)
  280. (loop))))))
  281. (define %qwerty-us-keystrokes
  282. ;; Maps "special" characters to their keystrokes.
  283. '((#\newline . "ret")
  284. (#\space . "spc")
  285. (#\- . "minus")
  286. (#\+ . "shift-equal")
  287. (#\* . "shift-8")
  288. (#\= . "equal")
  289. (#\? . "shift-slash")
  290. (#\[ . "bracket_left")
  291. (#\] . "bracket_right")
  292. (#\{ . "shift-bracket_left")
  293. (#\} . "shift-bracket_right")
  294. (#\( . "shift-9")
  295. (#\) . "shift-0")
  296. (#\/ . "slash")
  297. (#\< . "shift-comma")
  298. (#\> . "shift-dot")
  299. (#\. . "dot")
  300. (#\, . "comma")
  301. (#\; . "semicolon")
  302. (#\' . "apostrophe")
  303. (#\" . "shift-apostrophe")
  304. (#\` . "grave_accent")
  305. (#\bs . "backspace")
  306. (#\tab . "tab")))
  307. (define (character->keystroke chr keystrokes)
  308. "Return the keystroke for CHR according to the keyboard layout defined by
  309. KEYSTROKES."
  310. (if (char-set-contains? char-set:upper-case chr)
  311. (string-append "shift-" (string (char-downcase chr)))
  312. (or (assoc-ref keystrokes chr)
  313. (string chr))))
  314. (define* (string->keystroke-commands str
  315. #:optional
  316. (keystrokes
  317. %qwerty-us-keystrokes))
  318. "Return a list of QEMU monitor commands to send the keystrokes corresponding
  319. to STR. KEYSTROKES is an alist specifying a mapping from characters to
  320. keystrokes."
  321. (string-fold-right (lambda (chr result)
  322. (cons (string-append
  323. "sendkey "
  324. (character->keystroke chr keystrokes))
  325. result))
  326. '()
  327. str))
  328. (define* (marionette-type str marionette
  329. #:key (keystrokes %qwerty-us-keystrokes))
  330. "Type STR on MARIONETTE's keyboard, using the KEYSTROKES alist to map characters
  331. to actual keystrokes."
  332. (for-each (cut marionette-control <> marionette)
  333. (string->keystroke-commands str keystrokes)))
  334. ;;;
  335. ;;; Test helper.
  336. ;;;
  337. (define* (system-test-runner #:optional log-directory)
  338. "Return a SRFI-64 test runner that calls 'exit' upon 'test-end'. When
  339. LOG-DIRECTORY is specified, create log file within it."
  340. (let ((runner (test-runner-simple)))
  341. ;; Log to a file under LOG-DIRECTORY.
  342. (test-runner-on-group-begin! runner
  343. (let ((on-begin (test-runner-on-group-begin runner)))
  344. (lambda (runner suite-name count)
  345. (when log-directory
  346. (catch 'system-error
  347. (lambda ()
  348. (mkdir log-directory))
  349. (lambda args
  350. (unless (= (system-error-errno args) EEXIST)
  351. (apply throw args))))
  352. (set! test-log-to-file
  353. (string-append log-directory "/" suite-name ".log")))
  354. (on-begin runner suite-name count))))
  355. ;; The default behavior on 'test-end' is to only write a line if the test
  356. ;; failed. Arrange to also write a line on success.
  357. (test-runner-on-test-end! runner
  358. (let ((on-end (test-runner-on-test-end runner)))
  359. (lambda (runner)
  360. (let* ((kind (test-result-ref runner 'result-kind))
  361. (results (test-result-alist runner))
  362. (test-name (assq-ref results 'test-name)))
  363. (unless (memq kind '(fail xpass))
  364. (format (current-output-port) "~a: ~a~%"
  365. (string-upcase (symbol->string kind))
  366. test-name)))
  367. (on-end runner))))
  368. ;; On 'test-end', display test results and exit with zero if and only if
  369. ;; there were no test failures.
  370. (test-runner-on-final! runner
  371. (lambda (runner)
  372. (let ((success? (= (test-runner-fail-count runner) 0)))
  373. (test-on-final-simple runner)
  374. (when (not success?)
  375. (let* ((log-port (test-runner-aux-value runner))
  376. (log-file (port-filename log-port)))
  377. (format (current-error-port)
  378. "\nTests failed, dumping log file '~a'.\n\n"
  379. log-file)
  380. ;; At this point LOG-PORT is not closed yet; flush it.
  381. (force-output log-port)
  382. ;; Brute force to avoid dependency on (guix build utils) for
  383. ;; 'dump-port'.
  384. (let ((content (call-with-input-file log-file
  385. get-bytevector-all)))
  386. (put-bytevector (current-error-port) content))))
  387. (exit success?))))
  388. runner))
  389. (define* (qemu-command #:optional (system %host-type))
  390. "Return the default name of the QEMU command for SYSTEM."
  391. (let ((cpu (substring system 0
  392. (string-index system #\-))))
  393. (string-append "qemu-system-"
  394. (cond
  395. ((string-match "^i[3456]86$" cpu) "i386")
  396. ((string-match "armhf" cpu) "arm")
  397. (else cpu)))))
  398. ;;; marionette.scm ends here