gnu: Add strongswan.
[jackhill/guix/guix.git] / gnu / build / marionette.scm
CommitLineData
957afcae 1;;; GNU Guix --- Functional package management for GNU
5fa7cc53 2;;; Copyright © 2016, 2017 Ludovic Courtès <ludo@gnu.org>
957afcae
LC
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)
fe933833 24 #:use-module (ice-9 popen)
957afcae
LC
25 #:export (marionette?
26 make-marionette
27 marionette-eval
5fa7cc53 28 wait-for-file
957afcae 29 marionette-control
fe933833 30 marionette-screen-text
f7f292d3 31 wait-for-screen-text
957afcae
LC
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
f25c9ebc
LC
52 (repl %marionette-repl)) ;promise of a port
53
54(define-syntax-rule (marionette-repl marionette)
55 (force (%marionette-repl marionette)))
957afcae
LC
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
59QUIET? 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
90QEMU 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 "-device" "virtio-serial"
101 "-device" "virtconsole,chardev=repl"))
102
ad174705
LC
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
957afcae
LC
110 (let ((monitor (socket AF_UNIX SOCK_STREAM 0))
111 (repl (socket AF_UNIX SOCK_STREAM 0)))
112 (bind monitor (file->sockaddr "monitor"))
113 (listen monitor 1)
114 (bind repl (file->sockaddr "repl"))
115 (listen repl 1)
116
117 (match (primitive-fork)
118 (0
119 (catch #t
120 (lambda ()
121 (close monitor)
122 (close repl)
123 (match command
124 ((program . args)
125 (apply execl program program
126 (append args extra-options)))))
127 (lambda (key . args)
128 (print-exception (current-error-port)
129 (stack-ref (make-stack #t) 1)
130 key args)
131 (primitive-exit 1))))
132 (pid
133 (format #t "QEMU runs as PID ~a~%" pid)
957afcae 134
ad174705 135 (match (accept* monitor)
957afcae
LC
136 ((monitor-conn . _)
137 (display "connected to QEMU's monitor\n")
138 (close-port monitor)
139 (wait-for-monitor-prompt monitor-conn)
140 (display "read QEMU monitor prompt\n")
f25c9ebc
LC
141
142 (marionette (append command extra-options) pid
143 monitor-conn
144
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))))))))))))
957afcae
LC
159
160(define (marionette-eval exp marionette)
161 "Evaluate EXP in MARIONETTE's backdoor REPL. Return the result."
162 (match marionette
f25c9ebc 163 (($ <marionette> command pid monitor (= force repl))
957afcae
LC
164 (write exp repl)
165 (newline repl)
166 (read repl))))
167
13877c34
LC
168(define* (wait-for-file file marionette
169 #:key (timeout 10) (read 'read))
170 "Wait until FILE exists in MARIONETTE; READ its content and return it. If
5fa7cc53 171FILE has not shown up after TIMEOUT seconds, raise an error."
8bd52314
LC
172 (match (marionette-eval
173 `(let loop ((i ,timeout))
174 (cond ((file-exists? ,file)
13877c34 175 (cons 'success (call-with-input-file ,file ,read)))
8bd52314
LC
176 ((> i 0)
177 (sleep 1)
178 (loop (- i 1)))
179 (else
180 'failure)))
181 marionette)
182 (('success . result)
183 result)
184 ('failure
185 (error "file didn't show up" file))))
5fa7cc53 186
957afcae
LC
187(define (marionette-control command marionette)
188 "Run COMMAND in the QEMU monitor of MARIONETTE. COMMAND is a string such as
189\"sendkey ctrl-alt-f1\" or \"screendump foo.ppm\" (info \"(qemu-doc)
190pcsys_monitor\")."
191 (match marionette
192 (($ <marionette> _ _ monitor)
193 (display command monitor)
194 (newline monitor)
195 (wait-for-monitor-prompt monitor))))
196
fe933833
LC
197(define* (marionette-screen-text marionette
198 #:key
199 (ocrad "ocrad"))
200 "Take a screenshot of MARIONETTE, perform optical character
201recognition (OCR), and return the text read from the screen as a string. Do
202this by invoking OCRAD (file name for GNU Ocrad's command)"
203 (define (random-file-name)
204 (string-append "/tmp/marionette-screenshot-"
205 (number->string (random (expt 2 32)) 16)
206 ".ppm"))
207
208 (let ((image (random-file-name)))
209 (dynamic-wind
210 (const #t)
211 (lambda ()
212 (marionette-control (string-append "screendump " image)
213 marionette)
214
215 ;; Tell Ocrad to invert the image colors (make it black on white) and
216 ;; to scale the image up, which significantly improves the quality of
217 ;; the result. In spite of this, be aware that OCR confuses "y" and
218 ;; "V" and sometimes erroneously introduces white space.
219 (let* ((pipe (open-pipe* OPEN_READ ocrad
220 "-i" "-s" "10" image))
221 (text (get-string-all pipe)))
222 (unless (zero? (close-pipe pipe))
223 (error "'ocrad' failed" ocrad))
224 text))
225 (lambda ()
226 (false-if-exception (delete-file image))))))
227
f7f292d3
LC
228(define* (wait-for-screen-text marionette predicate
229 #:key (timeout 30) (ocrad "ocrad"))
230 "Wait for TIMEOUT seconds or until the screen text on MARIONETTE matches
231PREDICATE, whichever comes first. Raise an error when TIMEOUT is exceeded."
232 (define start
233 (car (gettimeofday)))
234
235 (define end
236 (+ start timeout))
237
238 (let loop ()
239 (if (> (car (gettimeofday)) end)
240 (error "'wait-for-screen-text' timeout" predicate)
241 (or (predicate (marionette-screen-text marionette #:ocrad ocrad))
242 (begin
243 (sleep 1)
244 (loop))))))
245
957afcae
LC
246(define %qwerty-us-keystrokes
247 ;; Maps "special" characters to their keystrokes.
248 '((#\newline . "ret")
249 (#\space . "spc")
250 (#\- . "minus")
251 (#\+ . "shift-equal")
252 (#\* . "shift-8")
253 (#\= . "equal")
254 (#\? . "shift-slash")
255 (#\[ . "bracket_left")
256 (#\] . "bracket_right")
257 (#\( . "shift-9")
258 (#\) . "shift-0")
259 (#\/ . "slash")
260 (#\< . "less")
261 (#\> . "shift-less")
262 (#\. . "dot")
263 (#\, . "comma")
264 (#\; . "semicolon")
06b8eae3
LC
265 (#\' . "apostrophe")
266 (#\" . "shift-apostrophe")
267 (#\` . "grave_accent")
957afcae
LC
268 (#\bs . "backspace")
269 (#\tab . "tab")))
270
0a809811
LC
271(define (character->keystroke chr keystrokes)
272 "Return the keystroke for CHR according to the keyboard layout defined by
273KEYSTROKES."
274 (if (char-set-contains? char-set:upper-case chr)
275 (string-append "shift-" (string (char-downcase chr)))
276 (or (assoc-ref keystrokes chr)
277 (string chr))))
278
957afcae
LC
279(define* (string->keystroke-commands str
280 #:optional
281 (keystrokes
282 %qwerty-us-keystrokes))
283 "Return a list of QEMU monitor commands to send the keystrokes corresponding
284to STR. KEYSTROKES is an alist specifying a mapping from characters to
285keystrokes."
286 (string-fold-right (lambda (chr result)
0a809811
LC
287 (cons (string-append
288 "sendkey "
289 (character->keystroke chr keystrokes))
957afcae
LC
290 result))
291 '()
292 str))
293
294(define* (marionette-type str marionette
295 #:key (keystrokes %qwerty-us-keystrokes))
296 "Type STR on MARIONETTE's keyboard, using the KEYSTROKES alist to map characters
297to actual keystrokes."
298 (for-each (cut marionette-control <> marionette)
299 (string->keystroke-commands str keystrokes)))
300
301;;; marionette.scm ends here