github.com/mem/u-root@v2.0.1-0.20181004165302-9b18b4636a33+incompatible/pkg/qemu/qemu.go (about)

     1  // Copyright 2018 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package qemu is suitable for running QEMU-based integration tests.
     6  //
     7  // The environment variable `UROOT_QEMU` overrides the path to QEMU and the
     8  // first few arguments (defaults to "qemu"). For example, I use:
     9  //
    10  //     UROOT_QEMU='qemu-system-x86_64 -L . -m 4096 -enable-kvm'
    11  //
    12  // For CI, this environment variable is set in `.circleci/images/integration/Dockerfile`.
    13  package qemu
    14  
    15  import (
    16  	"errors"
    17  	"io"
    18  	"os"
    19  	"regexp"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/google/goexpect"
    24  )
    25  
    26  // DefaultTimeout for `Expect` and `ExpectRE` functions.
    27  var DefaultTimeout = 7 * time.Second
    28  
    29  // TimeoutMultiplier increases all timeouts proportionally. Useful when running
    30  // QEMU on a slow machine.
    31  var TimeoutMultiplier = 2.0
    32  
    33  // QEMU is filled and pass to `Start()`.
    34  type QEMU struct {
    35  	// Path to the bzImage kernel
    36  	Kernel string
    37  
    38  	// Path to the initramfs
    39  	InitRAMFS string
    40  
    41  	// Extra kernel arguments
    42  	KernelArgs string
    43  
    44  	// Extra QEMU arguments
    45  	ExtraArgs []string
    46  
    47  	// Where to send serial output.
    48  	SerialOutput io.WriteCloser
    49  
    50  	gExpect *expect.GExpect
    51  }
    52  
    53  // CmdLine returns the command line arguments used to start QEMU. These
    54  // arguments are derived from the given QEMU struct.
    55  func (q *QEMU) CmdLine() []string {
    56  	// Read first few arguments for env.
    57  	env := os.Getenv("UROOT_QEMU")
    58  	if env == "" {
    59  		env = "qemu" // default
    60  	}
    61  	args := strings.Fields(env)
    62  
    63  	// Disable graphics because we are using serial.
    64  	args = append(args, "-nographic")
    65  
    66  	// Arguments passed to the kernel:
    67  	//
    68  	// - earlyprintk=ttyS0: print very early debug messages to the serial
    69  	// - console=ttyS0: /dev/console points to /dev/ttyS0 (the serial port)
    70  	// - q.KernelArgs: extra, optional kernel arguments
    71  	if q.Kernel != "" {
    72  		args = append(args, "-kernel", q.Kernel)
    73  		args = append(args, "-append", "console=ttyS0 earlyprintk=ttyS0")
    74  		if q.KernelArgs != "" {
    75  			args[len(args)-1] += " " + q.KernelArgs
    76  		}
    77  	}
    78  	if q.InitRAMFS != "" {
    79  		args = append(args, "-initrd", q.InitRAMFS)
    80  	}
    81  
    82  	return append(args, q.ExtraArgs...)
    83  }
    84  
    85  // CmdLineQuoted quotes any of QEMU's command line arguments containing a space
    86  // so it is easy to copy-n-paste into a shell for debugging.
    87  func (q *QEMU) CmdLineQuoted() (cmdline string) {
    88  	args := q.CmdLine()
    89  	for i, v := range q.CmdLine() {
    90  		if strings.ContainsAny(v, " \t\n") {
    91  			args[i] = "'" + v + "'"
    92  		}
    93  	}
    94  	return strings.Join(args, " ")
    95  }
    96  
    97  // Start QEMU.
    98  func (q *QEMU) Start() error {
    99  	if q.gExpect != nil {
   100  		return errors.New("QEMU already started")
   101  	}
   102  	var err error
   103  	q.gExpect, _, err = expect.SpawnWithArgs(q.CmdLine(), -1,
   104  		expect.Tee(q.SerialOutput),
   105  		expect.CheckDuration(2*time.Millisecond))
   106  	return err
   107  }
   108  
   109  // Close stops QEMU.
   110  func (q *QEMU) Close() {
   111  	q.gExpect.Close()
   112  	q.gExpect = nil
   113  }
   114  
   115  // Send sends a string to QEMU's serial.
   116  func (q *QEMU) Send(in string) {
   117  	q.gExpect.Send(in)
   118  }
   119  
   120  // Expect returns an error if the given string is not found in QEMU's serial
   121  // output within `DefaultTimeout`.
   122  func (q *QEMU) Expect(search string) error {
   123  	return q.ExpectTimeout(search, DefaultTimeout)
   124  }
   125  
   126  // ExpectTimeout returns an error if the given string is not found in QEMU's serial
   127  // output within the given timeout.
   128  func (q *QEMU) ExpectTimeout(search string, timeout time.Duration) error {
   129  	_, err := q.ExpectRETimeout(regexp.MustCompile(regexp.QuoteMeta(search)), timeout)
   130  	return err
   131  }
   132  
   133  // ExpectRE returns an error if the given regular expression is not found in
   134  // QEMU's serial output within `DefaultTimeout`. The matched string is
   135  // returned.
   136  func (q *QEMU) ExpectRE(pattern *regexp.Regexp) (string, error) {
   137  	return q.ExpectRETimeout(pattern, DefaultTimeout)
   138  }
   139  
   140  // ExpectRETimeout returns an error if the given regular expression is not
   141  // found in QEMU's serial output within the given timeout. The matched string
   142  // is returned.
   143  func (q *QEMU) ExpectRETimeout(pattern *regexp.Regexp, timeout time.Duration) (string, error) {
   144  	scaled := time.Duration(float64(timeout) * TimeoutMultiplier)
   145  	str, _, err := q.gExpect.Expect(pattern, scaled)
   146  	return str, err
   147  }