marionette.scm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2016-2022 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2018 Chris Marusich <cmmarusich@gmail.com>
  4. ;;; Copyright © 2022 Maxim Cournoyer <maxim.cournoyer@gmail.com>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (gnu build marionette)
  21. #:use-module (srfi srfi-9)
  22. #:use-module (srfi srfi-26)
  23. #:use-module (srfi srfi-64)
  24. #:use-module (srfi srfi-71)
  25. #:use-module (rnrs io ports)
  26. #:use-module (ice-9 match)
  27. #:use-module (ice-9 popen)
  28. #:use-module (ice-9 regex)
  29. #:export (marionette?
  30. marionette-pid
  31. make-marionette
  32. marionette-eval
  33. wait-for-file
  34. wait-for-tcp-port
  35. wait-for-unix-socket
  36. marionette-control
  37. wait-for-screen-text
  38. %qwerty-us-keystrokes
  39. marionette-type
  40. system-test-runner
  41. qemu-command))
  42. ;;; Commentary:
  43. ;;;
  44. ;;; Instrumentation tools for QEMU virtual machines (VMs). A "marionette" is
  45. ;;; essentially a VM (a QEMU instance) with its monitor connected to a
  46. ;;; Unix-domain socket, and with a REPL inside the guest listening on a
  47. ;;; virtual console, which is itself connected to the host via a Unix-domain
  48. ;;; socket--these are the marionette's strings, connecting it to the almighty
  49. ;;; puppeteer.
  50. ;;;
  51. ;;; Code:
  52. (define-record-type <marionette>
  53. (marionette command pid monitor repl)
  54. marionette?
  55. (command marionette-command) ;list of strings
  56. (pid marionette-pid) ;integer
  57. (monitor marionette-monitor) ;port
  58. (repl %marionette-repl)) ;promise of a port
  59. (define-syntax-rule (marionette-repl marionette)
  60. (force (%marionette-repl marionette)))
  61. (define* (wait-for-monitor-prompt port #:key (quiet? #t))
  62. "Read from PORT until we have seen all of QEMU's monitor prompt. When
  63. QUIET? is false, the monitor's output is written to the current output port."
  64. (define full-prompt
  65. (string->list "(qemu) "))
  66. (let loop ((prompt full-prompt)
  67. (matches '())
  68. (prefix '()))
  69. (match prompt
  70. (()
  71. ;; It's useful to set QUIET? so we don't display the echo of our own
  72. ;; commands.
  73. (unless quiet?
  74. (for-each (lambda (line)
  75. (format #t "qemu monitor: ~a~%" line))
  76. (string-tokenize (list->string (reverse prefix))
  77. (char-set-complement (char-set #\newline))))))
  78. ((chr rest ...)
  79. (let ((read (read-char port)))
  80. (cond ((eqv? read chr)
  81. (loop rest (cons read matches) prefix))
  82. ((eof-object? read)
  83. (error "EOF while waiting for QEMU monitor prompt"
  84. (list->string (reverse prefix))))
  85. (else
  86. (loop full-prompt
  87. '()
  88. (cons read (append matches prefix))))))))))
  89. (define* (make-marionette command
  90. #:key (socket-directory "/tmp") (timeout 20))
  91. "Return a QEMU marionette--i.e., a virtual machine with open connections to the
  92. QEMU monitor and to the guest's backdoor REPL."
  93. (define (file->sockaddr file)
  94. (make-socket-address AF_UNIX
  95. (string-append socket-directory "/" file)))
  96. (define extra-options
  97. (list "-nographic"
  98. "-monitor" (string-append "unix:" socket-directory "/monitor")
  99. "-chardev" (string-append "socket,id=repl,path=" socket-directory
  100. "/repl")
  101. "-chardev" (string-append "socket,id=qga,server=on,wait=off,path="
  102. socket-directory "/qemu-ga")
  103. ;; See
  104. ;; <http://www.linux-kvm.org/page/VMchannel_Requirements#Invocation>.
  105. "-device" "virtio-serial"
  106. "-device" "virtserialport,chardev=repl,name=org.gnu.guix.port.0"
  107. "-device" "virtserialport,chardev=qga,name=org.qemu.guest_agent.0"))
  108. (define (accept* port)
  109. (match (select (list port) '() (list port) timeout)
  110. (((port) () ())
  111. (accept port))
  112. (_
  113. (error "timeout in 'accept'" port))))
  114. (let ((monitor (socket AF_UNIX SOCK_STREAM 0))
  115. (repl (socket AF_UNIX SOCK_STREAM 0)))
  116. (bind monitor (file->sockaddr "monitor"))
  117. (listen monitor 1)
  118. (bind repl (file->sockaddr "repl"))
  119. (listen repl 1)
  120. (match (primitive-fork)
  121. (0
  122. (catch #t
  123. (lambda ()
  124. (close monitor)
  125. (close repl)
  126. (match command
  127. ((program . args)
  128. (apply execl program program
  129. (append args extra-options)))))
  130. (lambda (key . args)
  131. (print-exception (current-error-port)
  132. (stack-ref (make-stack #t) 1)
  133. key args)
  134. (primitive-exit 1))))
  135. (pid
  136. (format #t "QEMU runs as PID ~a~%" pid)
  137. (match (accept* monitor)
  138. ((monitor-conn . _)
  139. (display "connected to QEMU's monitor\n")
  140. (close-port monitor)
  141. (wait-for-monitor-prompt monitor-conn)
  142. (display "read QEMU monitor prompt\n")
  143. (marionette (append command extra-options) pid
  144. monitor-conn
  145. ;; The following 'accept' call connects immediately, but
  146. ;; we don't know whether the guest has connected until
  147. ;; we actually receive the 'ready' message.
  148. (match (accept* repl)
  149. ((repl-conn . addr)
  150. (display "connected to guest REPL\n")
  151. (close-port repl)
  152. ;; Delay reception of the 'ready' message so that the
  153. ;; caller can already send monitor commands.
  154. (delay
  155. (match (read repl-conn)
  156. ('ready
  157. (display "marionette is ready\n")
  158. repl-conn))))))))))))
  159. (define (marionette-eval exp marionette)
  160. "Evaluate EXP in MARIONETTE's backdoor REPL. Return the result."
  161. (match marionette
  162. (($ <marionette> command pid monitor (= force repl))
  163. (write exp repl)
  164. (newline repl)
  165. (with-exception-handler
  166. (lambda (exn)
  167. (simple-format
  168. (current-error-port)
  169. "error reading marionette response: ~A
  170. remaining response: ~A\n"
  171. exn
  172. (get-line repl))
  173. (raise-exception exn))
  174. (lambda ()
  175. (read repl))
  176. #:unwind? #t))))
  177. (define* (wait-for-file file marionette
  178. #:key (timeout 10) (read 'read))
  179. "Wait until FILE exists in MARIONETTE; READ its content and return it. If
  180. FILE has not shown up after TIMEOUT seconds, raise an error."
  181. (match (marionette-eval
  182. `(let loop ((i ,timeout))
  183. (cond ((file-exists? ,file)
  184. (cons 'success
  185. (let ((content
  186. (call-with-input-file ,file ,read)))
  187. (if (eof-object? content)
  188. ;; #<eof> can't be read, so convert to the
  189. ;; empty string
  190. ""
  191. content))))
  192. ((> i 0)
  193. (sleep 1)
  194. (loop (- i 1)))
  195. (else
  196. 'failure)))
  197. marionette)
  198. (('success . result)
  199. result)
  200. ('failure
  201. (error "file didn't show up" file))))
  202. (define* (wait-for-tcp-port port marionette
  203. #:key
  204. (timeout 20)
  205. (address `(make-socket-address AF_INET
  206. INADDR_LOOPBACK
  207. ,port)))
  208. "Wait for up to TIMEOUT seconds for PORT to accept connections in
  209. MARIONETTE. ADDRESS must be an expression that returns a socket address,
  210. typically a call to 'make-socket-address'. Raise an error on failure."
  211. ;; Note: The 'connect' loop has to run within the guest because, when we
  212. ;; forward ports to the host, connecting to the host never raises
  213. ;; ECONNREFUSED.
  214. (match (marionette-eval
  215. `(let* ((address ,address)
  216. (sock (socket (sockaddr:fam address) SOCK_STREAM 0)))
  217. (let loop ((i 0))
  218. (catch 'system-error
  219. (lambda ()
  220. (connect sock address)
  221. (close-port sock)
  222. 'success)
  223. (lambda args
  224. (if (< i ,timeout)
  225. (begin
  226. (sleep 1)
  227. (loop (+ 1 i)))
  228. (list 'failure address))))))
  229. marionette)
  230. ('success #t)
  231. (('failure address)
  232. (error "nobody's listening on port"
  233. (list (inet-ntop (sockaddr:fam address) (sockaddr:addr address))
  234. (sockaddr:port address))))))
  235. (define* (wait-for-unix-socket file-name marionette
  236. #:key (timeout 20))
  237. "Wait for up to TIMEOUT seconds for FILE-NAME, a Unix domain socket, to
  238. accept connections in MARIONETTE. Raise an error on failure."
  239. (match (marionette-eval
  240. `(begin
  241. (let ((sock (socket PF_UNIX SOCK_STREAM 0)))
  242. (let loop ((i 0))
  243. (catch 'system-error
  244. (lambda ()
  245. (connect sock AF_UNIX ,file-name)
  246. (close-port sock)
  247. 'success)
  248. (lambda args
  249. (if (< i ,timeout)
  250. (begin
  251. (sleep 1)
  252. (loop (+ 1 i)))
  253. 'failure))))))
  254. marionette)
  255. ('success #t)
  256. ('failure
  257. (error "nobody's listening on unix domain socket" file-name))))
  258. (define (marionette-control command marionette)
  259. "Run COMMAND in the QEMU monitor of MARIONETTE. COMMAND is a string such as
  260. \"sendkey ctrl-alt-f1\" or \"screendump foo.ppm\" (info \"(QEMU) QEMU
  261. Monitor\")."
  262. (match marionette
  263. (($ <marionette> _ _ monitor)
  264. (display command monitor)
  265. (newline monitor)
  266. ;; The "quit" command terminates QEMU immediately, with no output.
  267. (unless (string=? command "quit") (wait-for-monitor-prompt monitor)))))
  268. (define* (invoke-ocrad-ocr image #:key (ocrad "ocrad"))
  269. "Invoke the OCRAD command on image, and return the recognized text."
  270. (let* ((pipe (open-pipe* OPEN_READ ocrad "-i" "-s" "10" image))
  271. (text (get-string-all pipe)))
  272. (unless (zero? (close-pipe pipe))
  273. (error "'ocrad' failed" ocrad))
  274. text))
  275. (define* (invoke-tesseract-ocr image #:key (tesseract "tesseract"))
  276. "Invoke the TESSERACT command on IMAGE, and return the recognized text."
  277. (let* ((output-basename (tmpnam))
  278. (output-basename* (string-append output-basename ".txt")))
  279. (dynamic-wind
  280. (const #t)
  281. (lambda ()
  282. (let ((exit-val (status:exit-val
  283. (system* tesseract image output-basename))))
  284. (unless (zero? exit-val)
  285. (error "'tesseract' failed" tesseract))
  286. (call-with-input-file output-basename* get-string-all)))
  287. (lambda ()
  288. (false-if-exception (delete-file output-basename))
  289. (false-if-exception (delete-file output-basename*))))))
  290. (define* (marionette-screen-text marionette #:key (ocr "ocrad"))
  291. "Take a screenshot of MARIONETTE, perform optical character
  292. recognition (OCR), and return the text read from the screen as a string, along
  293. the screen dump image used. Do this by invoking OCR, which should be the file
  294. name of GNU Ocrad's@command{ocrad} or Tesseract OCR's @command{tesseract}
  295. command. The screen dump image returned as the second value should be deleted
  296. if it is not needed."
  297. (define image (string-append (tmpnam) ".ppm"))
  298. ;; Use the QEMU Monitor to save an image of the screen to the host.
  299. (marionette-control (string-append "screendump " image) marionette)
  300. ;; Process it via the OCR.
  301. (cond
  302. ((string-contains ocr "ocrad")
  303. (values (invoke-ocrad-ocr image #:ocrad ocr) image))
  304. ((string-contains ocr "tesseract")
  305. (values (invoke-tesseract-ocr image #:tesseract ocr) image))
  306. (else (error "unsupported ocr command"))))
  307. (define* (wait-for-screen-text marionette predicate
  308. #:key
  309. (ocr "ocrad")
  310. (timeout 30)
  311. pre-action
  312. post-action)
  313. "Wait for TIMEOUT seconds or until the screen text on MARIONETTE matches
  314. PREDICATE, whichever comes first. Raise an error when TIMEOUT is exceeded.
  315. The error contains the recognized text along the preserved file name of the
  316. screen dump, which is relative to the current working directory. If
  317. PRE-ACTION is provided, it should be a thunk to call before each OCR attempt.
  318. Likewise for POST-ACTION, except it runs at the end of a successful OCR."
  319. (define start
  320. (car (gettimeofday)))
  321. (define end
  322. (+ start timeout))
  323. (let loop ((last-text #f)
  324. (last-screendump #f))
  325. (if (> (car (gettimeofday)) end)
  326. (let ((screendump-backup (string-drop last-screendump 5)))
  327. ;; Move the file from /tmp/fileXXXXXX.pmm to the current working
  328. ;; directory, so that it is preserved in the test derivation output.
  329. (copy-file last-screendump screendump-backup)
  330. (delete-file last-screendump)
  331. (error "'wait-for-screen-text' timeout"
  332. 'ocr-text: last-text
  333. 'screendump: screendump-backup))
  334. (let* ((_ (and (procedure? pre-action) (pre-action)))
  335. (text screendump (marionette-screen-text marionette #:ocr ocr))
  336. (_ (and (procedure? post-action) (post-action)))
  337. (result (predicate text)))
  338. (cond (result
  339. (delete-file screendump)
  340. result)
  341. (else
  342. (sleep 1)
  343. (loop text screendump)))))))
  344. (define %qwerty-us-keystrokes
  345. ;; Maps "special" characters to their keystrokes.
  346. '((#\newline . "ret")
  347. (#\space . "spc")
  348. (#\- . "minus")
  349. (#\+ . "shift-equal")
  350. (#\* . "shift-8")
  351. (#\= . "equal")
  352. (#\? . "shift-slash")
  353. (#\[ . "bracket_left")
  354. (#\] . "bracket_right")
  355. (#\{ . "shift-bracket_left")
  356. (#\} . "shift-bracket_right")
  357. (#\( . "shift-9")
  358. (#\) . "shift-0")
  359. (#\/ . "slash")
  360. (#\< . "shift-comma")
  361. (#\> . "shift-dot")
  362. (#\. . "dot")
  363. (#\, . "comma")
  364. (#\: . "shift-semicolon")
  365. (#\; . "semicolon")
  366. (#\' . "apostrophe")
  367. (#\! . "shift-1")
  368. (#\" . "shift-apostrophe")
  369. (#\` . "grave_accent")
  370. (#\bs . "backspace")
  371. (#\tab . "tab")))
  372. (define (character->keystroke chr keystrokes)
  373. "Return the keystroke for CHR according to the keyboard layout defined by
  374. KEYSTROKES."
  375. (if (char-set-contains? char-set:upper-case chr)
  376. (string-append "shift-" (string (char-downcase chr)))
  377. (or (assoc-ref keystrokes chr)
  378. (string chr))))
  379. (define* (string->keystroke-commands str
  380. #:optional
  381. (keystrokes
  382. %qwerty-us-keystrokes))
  383. "Return a list of QEMU monitor commands to send the keystrokes corresponding
  384. to STR. KEYSTROKES is an alist specifying a mapping from characters to
  385. keystrokes."
  386. (string-fold-right (lambda (chr result)
  387. (cons (string-append
  388. "sendkey "
  389. (character->keystroke chr keystrokes))
  390. result))
  391. '()
  392. str))
  393. (define* (marionette-type str marionette
  394. #:key (keystrokes %qwerty-us-keystrokes))
  395. "Type STR on MARIONETTE's keyboard, using the KEYSTROKES alist to map characters
  396. to actual keystrokes."
  397. (for-each (cut marionette-control <> marionette)
  398. (string->keystroke-commands str keystrokes)))
  399. ;;;
  400. ;;; Test helper.
  401. ;;;
  402. (define* (system-test-runner #:optional log-directory)
  403. "Return a SRFI-64 test runner that calls 'exit' upon 'test-end'. When
  404. LOG-DIRECTORY is specified, create log file within it."
  405. (let ((runner (test-runner-simple)))
  406. ;; Log to a file under LOG-DIRECTORY.
  407. (test-runner-on-group-begin! runner
  408. (let ((on-begin (test-runner-on-group-begin runner)))
  409. (lambda (runner suite-name count)
  410. (when log-directory
  411. (catch 'system-error
  412. (lambda ()
  413. (mkdir log-directory))
  414. (lambda args
  415. (unless (= (system-error-errno args) EEXIST)
  416. (apply throw args))))
  417. (set! test-log-to-file
  418. (string-append log-directory "/" suite-name ".log")))
  419. (on-begin runner suite-name count))))
  420. ;; The default behavior on 'test-end' is to only write a line if the test
  421. ;; failed. Arrange to also write a line on success.
  422. (test-runner-on-test-end! runner
  423. (let ((on-end (test-runner-on-test-end runner)))
  424. (lambda (runner)
  425. (let* ((kind (test-result-ref runner 'result-kind))
  426. (results (test-result-alist runner))
  427. (test-name (assq-ref results 'test-name)))
  428. (unless (memq kind '(fail xpass))
  429. (format (current-output-port) "~a: ~a~%"
  430. (string-upcase (symbol->string kind))
  431. test-name)))
  432. (on-end runner))))
  433. ;; On 'test-end', display test results and exit with zero if and only if
  434. ;; there were no test failures.
  435. (test-runner-on-final! runner
  436. (lambda (runner)
  437. (let ((success? (= (test-runner-fail-count runner) 0)))
  438. (test-on-final-simple runner)
  439. (when (not success?)
  440. (let* ((log-port (test-runner-aux-value runner))
  441. (log-file (port-filename log-port)))
  442. (format (current-error-port)
  443. "\nTests failed, dumping log file '~a'.\n\n"
  444. log-file)
  445. ;; At this point LOG-PORT is not closed yet; flush it.
  446. (force-output log-port)
  447. ;; Brute force to avoid dependency on (guix build utils) for
  448. ;; 'dump-port'.
  449. (let ((content (call-with-input-file log-file
  450. get-bytevector-all)))
  451. (put-bytevector (current-error-port) content))))
  452. (exit success?))))
  453. runner))
  454. (define* (qemu-command #:optional (system %host-type))
  455. "Return the default name of the QEMU command for SYSTEM."
  456. (let ((cpu (substring system 0
  457. (string-index system #\-))))
  458. (string-append "qemu-system-"
  459. (cond
  460. ((string-match "^i[3456]86$" cpu) "i386")
  461. ((string-match "armhf" cpu) "arm")
  462. (else cpu)))))
  463. ;;; marionette.scm ends here