github.com/shaardie/u-root@v4.0.1-0.20190127173353-f24a1c26aa2e+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 provides a Go API for starting QEMU VMs.
     6  //
     7  // qemu is mainly suitable for running QEMU-based integration tests.
     8  //
     9  // The environment variable `UROOT_QEMU` overrides the path to QEMU and the
    10  // first few arguments (defaults to "qemu"). For example, I use:
    11  //
    12  //     UROOT_QEMU='qemu-system-x86_64 -L . -m 4096 -enable-kvm'
    13  //
    14  // For CI, this environment variable is set in `.circleci/images/integration/Dockerfile`.
    15  package qemu
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"regexp"
    22  	"strings"
    23  	"time"
    24  
    25  	expect "github.com/google/goexpect"
    26  )
    27  
    28  // DefaultTimeout for `Expect` and `ExpectRE` functions.
    29  var DefaultTimeout = 7 * time.Second
    30  
    31  // TimeoutMultiplier increases all timeouts proportionally. Useful when running
    32  // QEMU on a slow machine.
    33  var TimeoutMultiplier = 2.0
    34  
    35  // Options are VM start-up parameters.
    36  type Options struct {
    37  	// QEMUPath is the path to the QEMU binary to invoke.
    38  	//
    39  	// If left unspecified, the UROOT_QEMU env var will be used.
    40  	// If the env var is unspecified, "qemu" is the default.
    41  	QEMUPath string
    42  
    43  	// Path to the kernel to boot.
    44  	Kernel string
    45  
    46  	// Path to the initramfs.
    47  	Initramfs string
    48  
    49  	// Extra kernel command-line arguments.
    50  	KernelArgs string
    51  
    52  	// Where to send serial output.
    53  	SerialOutput io.WriteCloser
    54  
    55  	// Timeout is the expect timeout.
    56  	Timeout time.Duration
    57  
    58  	// Devices are devices to expose to the QEMU VM.
    59  	Devices []Device
    60  }
    61  
    62  // Start starts a QEMU VM.
    63  func (o *Options) Start() (*VM, error) {
    64  	cmdline := o.Cmdline()
    65  
    66  	gExpect, ch, err := expect.SpawnWithArgs(cmdline, -1,
    67  		expect.Tee(o.SerialOutput),
    68  		expect.CheckDuration(2*time.Millisecond))
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	return &VM{
    73  		Options: o,
    74  		errCh:   ch,
    75  		cmdline: cmdline,
    76  		gExpect: gExpect,
    77  	}, nil
    78  }
    79  
    80  // cmdline returns the command line arguments used to start QEMU. These
    81  // arguments are derived from the given QEMU struct.
    82  func (o *Options) Cmdline() []string {
    83  	var args []string
    84  	if len(o.QEMUPath) > 0 {
    85  		args = append(args, o.QEMUPath)
    86  	} else {
    87  		// Read first few arguments for env.
    88  		env := os.Getenv("UROOT_QEMU")
    89  		if env == "" {
    90  			env = "qemu" // default
    91  		}
    92  		args = append(args, strings.Fields(env)...)
    93  	}
    94  
    95  	// Disable graphics because we are using serial.
    96  	args = append(args, "-nographic")
    97  
    98  	// Arguments passed to the kernel:
    99  	//
   100  	// - earlyprintk=ttyS0: print very early debug messages to the serial
   101  	// - console=ttyS0: /dev/console points to /dev/ttyS0 (the serial port)
   102  	// - o.KernelArgs: extra, optional kernel arguments
   103  	if len(o.Kernel) != 0 {
   104  		args = append(args, "-kernel", o.Kernel)
   105  		args = append(args, "-append", o.KernelArgs)
   106  	}
   107  	if len(o.Initramfs) != 0 {
   108  		args = append(args, "-initrd", o.Initramfs)
   109  	}
   110  
   111  	for _, dev := range o.Devices {
   112  		if dev != nil {
   113  			if c := dev.Cmdline(); c != nil {
   114  				args = append(args, c...)
   115  			}
   116  		}
   117  	}
   118  	return args
   119  }
   120  
   121  // VM is a running QEMU virtual machine.
   122  type VM struct {
   123  	Options *Options
   124  	cmdline []string
   125  	errCh   <-chan error
   126  	gExpect *expect.GExpect
   127  }
   128  
   129  // Wait waits for the VM to exit.
   130  func (v *VM) Wait() error {
   131  	return <-v.errCh
   132  }
   133  
   134  // Cmdline is the command-line the VM was started with.
   135  func (v *VM) Cmdline() []string {
   136  	// Maybe return a copy?
   137  	return v.cmdline
   138  }
   139  
   140  // CmdlineQuoted quotes any of QEMU's command line arguments containing a space
   141  // so it is easy to copy-n-paste into a shell for debugging.
   142  func (v *VM) CmdlineQuoted() string {
   143  	args := make([]string, len(v.cmdline))
   144  	for i, arg := range v.cmdline {
   145  		if strings.ContainsAny(arg, " \t\n") {
   146  			args[i] = fmt.Sprintf("'%s'", arg)
   147  		} else {
   148  			args[i] = arg
   149  		}
   150  	}
   151  	return strings.Join(args, " ")
   152  }
   153  
   154  // Close stops QEMU.
   155  func (v *VM) Close() {
   156  	v.gExpect.Close()
   157  	v.gExpect = nil
   158  }
   159  
   160  // Send sends a string to QEMU's serial.
   161  func (v *VM) Send(in string) {
   162  	v.gExpect.Send(in)
   163  }
   164  
   165  func (v *VM) TimeoutOr() time.Duration {
   166  	if v.Options.Timeout == 0 {
   167  		return DefaultTimeout
   168  	}
   169  	return v.Options.Timeout
   170  }
   171  
   172  // Expect returns an error if the given string is not found in vEMU's serial
   173  // output within `DefaultTimeout`.
   174  func (v *VM) Expect(search string) error {
   175  	return v.ExpectTimeout(search, v.TimeoutOr())
   176  }
   177  
   178  // ExpectTimeout returns an error if the given string is not found in vEMU's serial
   179  // output within the given timeout.
   180  func (v *VM) ExpectTimeout(search string, timeout time.Duration) error {
   181  	_, err := v.ExpectRETimeout(regexp.MustCompile(regexp.QuoteMeta(search)), timeout)
   182  	return err
   183  }
   184  
   185  // ExpectRE returns an error if the given regular expression is not found in
   186  // vEMU's serial output within `DefaultTimeout`. The matched string is
   187  // returned.
   188  func (v *VM) ExpectRE(pattern *regexp.Regexp) (string, error) {
   189  	return v.ExpectRETimeout(pattern, v.TimeoutOr())
   190  }
   191  
   192  // ExpectRETimeout returns an error if the given regular expression is not
   193  // found in vEMU's serial output within the given timeout. The matched string
   194  // is returned.
   195  func (v *VM) ExpectRETimeout(pattern *regexp.Regexp, timeout time.Duration) (string, error) {
   196  	scaled := time.Duration(float64(timeout) * TimeoutMultiplier)
   197  	str, _, err := v.gExpect.Expect(pattern, scaled)
   198  	return str, err
   199  }