summaryrefslogtreecommitdiff
path: root/gnu/build
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2016-05-04 23:31:08 +0200
committerLudovic Courtès <ludo@gnu.org>2016-05-04 23:35:55 +0200
commit957afcae3cded622f4260385f69b40dbdcaade9f (patch)
tree55ec9609a5fb2ffd5704121fb3e2f82cfc3876a6 /gnu/build
parentb2fef041fcfbb63d7901c25647373aeda56b026e (diff)
Add (gnu tests) and (gnu build marionette).
* gnu/build/marionette.scm, gnu/tests.scm: New files. * gnu/local.mk (GNU_SYSTEM_MODULES): Add them. * gnu/system/vm.scm (common-qemu-options): Remove '-serial stdio'.
Diffstat (limited to 'gnu/build')
-rw-r--r--gnu/build/marionette.scm206
1 files changed, 206 insertions, 0 deletions
diff --git a/gnu/build/marionette.scm b/gnu/build/marionette.scm
new file mode 100644
index 0000000000..9399c55313
--- /dev/null
+++ b/gnu/build/marionette.scm
@@ -0,0 +1,206 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2016 Ludovic Courtès <ludo@gnu.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu build marionette)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-26)
+ #:use-module (rnrs io ports)
+ #:use-module (ice-9 match)
+ #:export (marionette?
+ make-marionette
+ marionette-eval
+ marionette-control
+ %qwerty-us-keystrokes
+ marionette-type))
+
+;;; Commentary:
+;;;
+;;; Instrumentation tools for QEMU virtual machines (VMs). A "marionette" is
+;;; essentially a VM (a QEMU instance) with its monitor connected to a
+;;; Unix-domain socket, and with a REPL inside the guest listening on a
+;;; virtual console, which is itself connected to the host via a Unix-domain
+;;; socket--these are the marionette's strings, connecting it to the almighty
+;;; puppeteer.
+;;;
+;;; Code:
+
+(define-record-type <marionette>
+ (marionette command pid monitor repl)
+ marionette?
+ (command marionette-command) ;list of strings
+ (pid marionette-pid) ;integer
+ (monitor marionette-monitor) ;port
+ (repl marionette-repl)) ;port
+
+(define* (wait-for-monitor-prompt port #:key (quiet? #t))
+ "Read from PORT until we have seen all of QEMU's monitor prompt. When
+QUIET? is false, the monitor's output is written to the current output port."
+ (define full-prompt
+ (string->list "(qemu) "))
+
+ (let loop ((prompt full-prompt)
+ (matches '())
+ (prefix '()))
+ (match prompt
+ (()
+ ;; It's useful to set QUIET? so we don't display the echo of our own
+ ;; commands.
+ (unless quiet?
+ (for-each (lambda (line)
+ (format #t "qemu monitor: ~a~%" line))
+ (string-tokenize (list->string (reverse prefix))
+ (char-set-complement (char-set #\newline))))))
+ ((chr rest ...)
+ (let ((read (read-char port)))
+ (cond ((eqv? read chr)
+ (loop rest (cons read matches) prefix))
+ ((eof-object? read)
+ (error "EOF while waiting for QEMU monitor prompt"
+ (list->string (reverse prefix))))
+ (else
+ (loop full-prompt
+ '()
+ (cons read (append matches prefix))))))))))
+
+(define* (make-marionette command
+ #:key (socket-directory "/tmp") (timeout 20))
+ "Return a QEMU marionette--i.e., a virtual machine with open connections to the
+QEMU monitor and to the guest's backdoor REPL."
+ (define (file->sockaddr file)
+ (make-socket-address AF_UNIX
+ (string-append socket-directory "/" file)))
+
+ (define extra-options
+ (list "-nographic"
+ "-monitor" (string-append "unix:" socket-directory "/monitor")
+ "-chardev" (string-append "socket,id=repl,path=" socket-directory
+ "/repl")
+ "-device" "virtio-serial"
+ "-device" "virtconsole,chardev=repl"))
+
+ (let ((monitor (socket AF_UNIX SOCK_STREAM 0))
+ (repl (socket AF_UNIX SOCK_STREAM 0)))
+ (bind monitor (file->sockaddr "monitor"))
+ (listen monitor 1)
+ (bind repl (file->sockaddr "repl"))
+ (listen repl 1)
+
+ (match (primitive-fork)
+ (0
+ (catch #t
+ (lambda ()
+ (close monitor)
+ (close repl)
+ (match command
+ ((program . args)
+ (apply execl program program
+ (append args extra-options)))))
+ (lambda (key . args)
+ (print-exception (current-error-port)
+ (stack-ref (make-stack #t) 1)
+ key args)
+ (primitive-exit 1))))
+ (pid
+ (format #t "QEMU runs as PID ~a~%" pid)
+ (sigaction SIGALRM
+ (lambda (signum)
+ (display "time is up!\n") ;FIXME: break
+ #t))
+ (alarm timeout)
+
+ (match (accept monitor)
+ ((monitor-conn . _)
+ (display "connected to QEMU's monitor\n")
+ (close-port monitor)
+ (wait-for-monitor-prompt monitor-conn)
+ (display "read QEMU monitor prompt\n")
+ (match (accept repl)
+ ((repl-conn . addr)
+ (display "connected to guest REPL\n")
+ (close-port repl)
+ (match (read repl-conn)
+ ('ready
+ (alarm 0)
+ (sigaction SIGALRM SIG_DFL)
+ (display "marionette is ready\n")
+ (marionette (append command extra-options) pid
+ monitor-conn repl-conn)))))))))))
+
+(define (marionette-eval exp marionette)
+ "Evaluate EXP in MARIONETTE's backdoor REPL. Return the result."
+ (match marionette
+ (($ <marionette> command pid monitor repl)
+ (write exp repl)
+ (newline repl)
+ (read repl))))
+
+(define (marionette-control command marionette)
+ "Run COMMAND in the QEMU monitor of MARIONETTE. COMMAND is a string such as
+\"sendkey ctrl-alt-f1\" or \"screendump foo.ppm\" (info \"(qemu-doc)
+pcsys_monitor\")."
+ (match marionette
+ (($ <marionette> _ _ monitor)
+ (display command monitor)
+ (newline monitor)
+ (wait-for-monitor-prompt monitor))))
+
+(define %qwerty-us-keystrokes
+ ;; Maps "special" characters to their keystrokes.
+ '((#\newline . "ret")
+ (#\space . "spc")
+ (#\- . "minus")
+ (#\+ . "shift-equal")
+ (#\* . "shift-8")
+ (#\= . "equal")
+ (#\? . "shift-slash")
+ (#\[ . "bracket_left")
+ (#\] . "bracket_right")
+ (#\( . "shift-9")
+ (#\) . "shift-0")
+ (#\/ . "slash")
+ (#\< . "less")
+ (#\> . "shift-less")
+ (#\. . "dot")
+ (#\, . "comma")
+ (#\; . "semicolon")
+ (#\bs . "backspace")
+ (#\tab . "tab")))
+
+(define* (string->keystroke-commands str
+ #:optional
+ (keystrokes
+ %qwerty-us-keystrokes))
+ "Return a list of QEMU monitor commands to send the keystrokes corresponding
+to STR. KEYSTROKES is an alist specifying a mapping from characters to
+keystrokes."
+ (string-fold-right (lambda (chr result)
+ (cons (string-append "sendkey "
+ (or (assoc-ref keystrokes chr)
+ (string chr)))
+ result))
+ '()
+ str))
+
+(define* (marionette-type str marionette
+ #:key (keystrokes %qwerty-us-keystrokes))
+ "Type STR on MARIONETTE's keyboard, using the KEYSTROKES alist to map characters
+to actual keystrokes."
+ (for-each (cut marionette-control <> marionette)
+ (string->keystroke-commands str keystrokes)))
+
+;;; marionette.scm ends here