gnu: python-pkginfo: Update to 1.4.2.
[jackhill/guix/guix.git] / gnu / build / marionette.scm
1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2016, 2017, 2018 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
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 marionette-control
30 marionette-screen-text
31 wait-for-screen-text
32 %qwerty-us-keystrokes
33 marionette-type))
34
35 ;;; Commentary:
36 ;;;
37 ;;; Instrumentation tools for QEMU virtual machines (VMs). A "marionette" is
38 ;;; essentially a VM (a QEMU instance) with its monitor connected to a
39 ;;; Unix-domain socket, and with a REPL inside the guest listening on a
40 ;;; virtual console, which is itself connected to the host via a Unix-domain
41 ;;; socket--these are the marionette's strings, connecting it to the almighty
42 ;;; puppeteer.
43 ;;;
44 ;;; Code:
45
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
54 (define-syntax-rule (marionette-repl marionette)
55 (force (%marionette-repl marionette)))
56
57 (define* (wait-for-monitor-prompt port #:key (quiet? #t))
58 "Read from PORT until we have seen all of QEMU's monitor prompt. When
59 QUIET? is false, the monitor's output is written to the current output port."
60 (define full-prompt
61 (string->list "(qemu) "))
62
63 (let loop ((prompt full-prompt)
64 (matches '())
65 (prefix '()))
66 (match prompt
67 (()
68 ;; It's useful to set QUIET? so we don't display the echo of our own
69 ;; commands.
70 (unless quiet?
71 (for-each (lambda (line)
72 (format #t "qemu monitor: ~a~%" line))
73 (string-tokenize (list->string (reverse prefix))
74 (char-set-complement (char-set #\newline))))))
75 ((chr rest ...)
76 (let ((read (read-char port)))
77 (cond ((eqv? read chr)
78 (loop rest (cons read matches) prefix))
79 ((eof-object? read)
80 (error "EOF while waiting for QEMU monitor prompt"
81 (list->string (reverse prefix))))
82 (else
83 (loop full-prompt
84 '()
85 (cons read (append matches prefix))))))))))
86
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
95 (define extra-options
96 (list "-nographic"
97 "-monitor" (string-append "unix:" socket-directory "/monitor")
98 "-chardev" (string-append "socket,id=repl,path=" socket-directory
99 "/repl")
100
101 ;; See
102 ;; <http://www.linux-kvm.org/page/VMchannel_Requirements#Invocation>.
103 "-device" "virtio-serial"
104 "-device" "virtserialport,chardev=repl,name=org.gnu.guix.port.0"))
105
106 (define (accept* port)
107 (match (select (list port) '() (list port) timeout)
108 (((port) () ())
109 (accept port))
110 (_
111 (error "timeout in 'accept'" port))))
112
113 (let ((monitor (socket AF_UNIX SOCK_STREAM 0))
114 (repl (socket AF_UNIX SOCK_STREAM 0)))
115 (bind monitor (file->sockaddr "monitor"))
116 (listen monitor 1)
117 (bind repl (file->sockaddr "repl"))
118 (listen repl 1)
119
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
138 (match (accept* monitor)
139 ((monitor-conn . _)
140 (display "connected to QEMU's monitor\n")
141 (close-port monitor)
142 (wait-for-monitor-prompt monitor-conn)
143 (display "read QEMU monitor prompt\n")
144
145 (marionette (append command extra-options) pid
146 monitor-conn
147
148 ;; The following 'accept' call connects immediately, but
149 ;; we don't know whether the guest has connected until
150 ;; we actually receive the 'ready' message.
151 (match (accept* repl)
152 ((repl-conn . addr)
153 (display "connected to guest REPL\n")
154 (close-port repl)
155 ;; Delay reception of the 'ready' message so that the
156 ;; caller can already send monitor commands.
157 (delay
158 (match (read repl-conn)
159 ('ready
160 (display "marionette is ready\n")
161 repl-conn))))))))))))
162
163 (define (marionette-eval exp marionette)
164 "Evaluate EXP in MARIONETTE's backdoor REPL. Return the result."
165 (match marionette
166 (($ <marionette> command pid monitor (= force repl))
167 (write exp repl)
168 (newline repl)
169 (read repl))))
170
171 (define* (wait-for-file file marionette
172 #:key (timeout 10) (read 'read))
173 "Wait until FILE exists in MARIONETTE; READ its content and return it. If
174 FILE has not shown up after TIMEOUT seconds, raise an error."
175 (match (marionette-eval
176 `(let loop ((i ,timeout))
177 (cond ((file-exists? ,file)
178 (cons 'success (call-with-input-file ,file ,read)))
179 ((> i 0)
180 (sleep 1)
181 (loop (- i 1)))
182 (else
183 'failure)))
184 marionette)
185 (('success . result)
186 result)
187 ('failure
188 (error "file didn't show up" file))))
189
190 (define (marionette-control command marionette)
191 "Run COMMAND in the QEMU monitor of MARIONETTE. COMMAND is a string such as
192 \"sendkey ctrl-alt-f1\" or \"screendump foo.ppm\" (info \"(qemu-doc)
193 pcsys_monitor\")."
194 (match marionette
195 (($ <marionette> _ _ monitor)
196 (display command monitor)
197 (newline monitor)
198 (wait-for-monitor-prompt monitor))))
199
200 (define* (marionette-screen-text marionette
201 #:key
202 (ocrad "ocrad"))
203 "Take a screenshot of MARIONETTE, perform optical character
204 recognition (OCR), and return the text read from the screen as a string. Do
205 this by invoking OCRAD (file name for GNU Ocrad's command)"
206 (define (random-file-name)
207 (string-append "/tmp/marionette-screenshot-"
208 (number->string (random (expt 2 32)) 16)
209 ".ppm"))
210
211 (let ((image (random-file-name)))
212 (dynamic-wind
213 (const #t)
214 (lambda ()
215 (marionette-control (string-append "screendump " image)
216 marionette)
217
218 ;; Tell Ocrad to invert the image colors (make it black on white) and
219 ;; to scale the image up, which significantly improves the quality of
220 ;; the result. In spite of this, be aware that OCR confuses "y" and
221 ;; "V" and sometimes erroneously introduces white space.
222 (let* ((pipe (open-pipe* OPEN_READ ocrad
223 "-i" "-s" "10" image))
224 (text (get-string-all pipe)))
225 (unless (zero? (close-pipe pipe))
226 (error "'ocrad' failed" ocrad))
227 text))
228 (lambda ()
229 (false-if-exception (delete-file image))))))
230
231 (define* (wait-for-screen-text marionette predicate
232 #:key (timeout 30) (ocrad "ocrad"))
233 "Wait for TIMEOUT seconds or until the screen text on MARIONETTE matches
234 PREDICATE, whichever comes first. Raise an error when TIMEOUT is exceeded."
235 (define start
236 (car (gettimeofday)))
237
238 (define end
239 (+ start timeout))
240
241 (let loop ()
242 (if (> (car (gettimeofday)) end)
243 (error "'wait-for-screen-text' timeout" predicate)
244 (or (predicate (marionette-screen-text marionette #:ocrad ocrad))
245 (begin
246 (sleep 1)
247 (loop))))))
248
249 (define %qwerty-us-keystrokes
250 ;; Maps "special" characters to their keystrokes.
251 '((#\newline . "ret")
252 (#\space . "spc")
253 (#\- . "minus")
254 (#\+ . "shift-equal")
255 (#\* . "shift-8")
256 (#\= . "equal")
257 (#\? . "shift-slash")
258 (#\[ . "bracket_left")
259 (#\] . "bracket_right")
260 (#\( . "shift-9")
261 (#\) . "shift-0")
262 (#\/ . "slash")
263 (#\< . "less")
264 (#\> . "shift-less")
265 (#\. . "dot")
266 (#\, . "comma")
267 (#\; . "semicolon")
268 (#\' . "apostrophe")
269 (#\" . "shift-apostrophe")
270 (#\` . "grave_accent")
271 (#\bs . "backspace")
272 (#\tab . "tab")))
273
274 (define (character->keystroke chr keystrokes)
275 "Return the keystroke for CHR according to the keyboard layout defined by
276 KEYSTROKES."
277 (if (char-set-contains? char-set:upper-case chr)
278 (string-append "shift-" (string (char-downcase chr)))
279 (or (assoc-ref keystrokes chr)
280 (string chr))))
281
282 (define* (string->keystroke-commands str
283 #:optional
284 (keystrokes
285 %qwerty-us-keystrokes))
286 "Return a list of QEMU monitor commands to send the keystrokes corresponding
287 to STR. KEYSTROKES is an alist specifying a mapping from characters to
288 keystrokes."
289 (string-fold-right (lambda (chr result)
290 (cons (string-append
291 "sendkey "
292 (character->keystroke chr keystrokes))
293 result))
294 '()
295 str))
296
297 (define* (marionette-type str marionette
298 #:key (keystrokes %qwerty-us-keystrokes))
299 "Type STR on MARIONETTE's keyboard, using the KEYSTROKES alist to map characters
300 to actual keystrokes."
301 (for-each (cut marionette-control <> marionette)
302 (string->keystroke-commands str keystrokes)))
303
304 ;;; marionette.scm ends here