1 ;;; GNU Guix --- Functional package management for GNU
2 ;;; Copyright © 2016 Ludovic Courtès <ludo@gnu.org>
4 ;;; This file is part of GNU Guix.
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.
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.
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/>.
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)
33 ;;; Instrumentation tools for QEMU virtual machines (VMs). A "marionette" is
34 ;;; essentially a VM (a QEMU instance) with its monitor connected to a
35 ;;; Unix-domain socket, and with a REPL inside the guest listening on a
36 ;;; virtual console, which is itself connected to the host via a Unix-domain
37 ;;; socket--these are the marionette's strings, connecting it to the almighty
42 (define-record-type <marionette>
43 (marionette command pid monitor repl)
45 (command marionette-command) ;list of strings
46 (pid marionette-pid) ;integer
47 (monitor marionette-monitor) ;port
48 (repl %marionette-repl)) ;promise of a port
50 (define-syntax-rule (marionette-repl marionette)
51 (force (%marionette-repl marionette)))
53 (define* (wait-for-monitor-prompt port #:key (quiet? #t))
54 "Read from PORT until we have seen all of QEMU's monitor prompt. When
55 QUIET? is false, the monitor's output is written to the current output port."
57 (string->list "(qemu) "))
59 (let loop ((prompt full-prompt)
64 ;; It's useful to set QUIET? so we don't display the echo of our own
67 (for-each (lambda (line)
68 (format #t "qemu monitor: ~a~%" line))
69 (string-tokenize (list->string (reverse prefix))
70 (char-set-complement (char-set #\newline))))))
72 (let ((read (read-char port)))
73 (cond ((eqv? read chr)
74 (loop rest (cons read matches) prefix))
76 (error "EOF while waiting for QEMU monitor prompt"
77 (list->string (reverse prefix))))
81 (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)))
93 "-monitor" (string-append "unix:" socket-directory "/monitor")
94 "-chardev" (string-append "socket,id=repl,path=" socket-directory
96 "-device" "virtio-serial"
97 "-device" "virtconsole,chardev=repl"))
99 (define (accept* port)
100 (match (select (list port) '() (list port) timeout)
104 (error "timeout in 'accept'" port))))
106 (let ((monitor (socket AF_UNIX SOCK_STREAM 0))
107 (repl (socket AF_UNIX SOCK_STREAM 0)))
108 (bind monitor (file->sockaddr "monitor"))
110 (bind repl (file->sockaddr "repl"))
113 (match (primitive-fork)
121 (apply execl program program
122 (append args extra-options)))))
124 (print-exception (current-error-port)
125 (stack-ref (make-stack #t) 1)
127 (primitive-exit 1))))
129 (format #t "QEMU runs as PID ~a~%" pid)
131 (match (accept* monitor)
133 (display "connected to QEMU's monitor\n")
135 (wait-for-monitor-prompt monitor-conn)
136 (display "read QEMU monitor prompt\n")
138 (marionette (append command extra-options) pid
141 ;; The following 'accept' call connects immediately, but
142 ;; we don't know whether the guest has connected until
143 ;; we actually receive the 'ready' message.
144 (match (accept* repl)
146 (display "connected to guest REPL\n")
148 ;; Delay reception of the 'ready' message so that the
149 ;; caller can already send monitor commands.
151 (match (read repl-conn)
153 (display "marionette is ready\n")
154 repl-conn))))))))))))
156 (define (marionette-eval exp marionette)
157 "Evaluate EXP in MARIONETTE's backdoor REPL. Return the result."
159 (($ <marionette> command pid monitor (= force repl))
164 (define (marionette-control command marionette)
165 "Run COMMAND in the QEMU monitor of MARIONETTE. COMMAND is a string such as
166 \"sendkey ctrl-alt-f1\" or \"screendump foo.ppm\" (info \"(qemu-doc)
169 (($ <marionette> _ _ monitor)
170 (display command monitor)
172 (wait-for-monitor-prompt monitor))))
174 (define %qwerty-us-keystrokes
175 ;; Maps "special" characters to their keystrokes.
176 '((#\newline . "ret")
179 (#\+ . "shift-equal")
182 (#\? . "shift-slash")
183 (#\[ . "bracket_left")
184 (#\] . "bracket_right")
196 (define* (string->keystroke-commands str
199 %qwerty-us-keystrokes))
200 "Return a list of QEMU monitor commands to send the keystrokes corresponding
201 to STR. KEYSTROKES is an alist specifying a mapping from characters to
203 (string-fold-right (lambda (chr result)
204 (cons (string-append "sendkey "
205 (or (assoc-ref keystrokes chr)
211 (define* (marionette-type str marionette
212 #:key (keystrokes %qwerty-us-keystrokes))
213 "Type STR on MARIONETTE's keyboard, using the KEYSTROKES alist to map characters
214 to actual keystrokes."
215 (for-each (cut marionette-control <> marionette)
216 (string->keystroke-commands str keystrokes)))
218 ;;; marionette.scm ends here