github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/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  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	expect "github.com/google/goexpect"
    27  )
    28  
    29  // DefaultTimeout for `Expect` and `ExpectRE` functions.
    30  var DefaultTimeout = 7 * time.Second
    31  
    32  // TimeoutMultiplier increases all timeouts proportionally. Useful when running
    33  // QEMU on a slow machine.
    34  var TimeoutMultiplier = 1.0
    35  
    36  func init() {
    37  	if timeoutMultS := os.Getenv("UROOT_QEMU_TIMEOUT_X"); len(timeoutMultS) > 0 {
    38  		t, err := strconv.ParseFloat(timeoutMultS, 64)
    39  		if err == nil {
    40  			TimeoutMultiplier = t
    41  		}
    42  	}
    43  }
    44  
    45  // Options are VM start-up parameters.
    46  type Options struct {
    47  	// QEMUPath is the path to the QEMU binary to invoke.
    48  	//
    49  	// If left unspecified, the UROOT_QEMU env var will be used.
    50  	// If the env var is unspecified, "qemu" is the default.
    51  	QEMUPath string
    52  
    53  	// Path to the kernel to boot.
    54  	Kernel string
    55  
    56  	// Path to the initramfs.
    57  	Initramfs string
    58  
    59  	// Extra kernel command-line arguments.
    60  	KernelArgs string
    61  
    62  	// Where to send serial output.
    63  	SerialOutput io.WriteCloser
    64  
    65  	// Timeout is the expect timeout.
    66  	Timeout time.Duration
    67  
    68  	// Devices are devices to expose to the QEMU VM.
    69  	Devices []Device
    70  }
    71  
    72  // Start starts a QEMU VM.
    73  func (o *Options) Start() (*VM, error) {
    74  	cmdline, err := o.Cmdline()
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	gExpect, ch, err := expect.SpawnWithArgs(cmdline, -1,
    80  		expect.Tee(o.SerialOutput),
    81  		expect.CheckDuration(2*time.Millisecond))
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	return &VM{
    86  		Options: o,
    87  		errCh:   ch,
    88  		cmdline: cmdline,
    89  		gExpect: gExpect,
    90  	}, nil
    91  }
    92  
    93  // cmdline returns the command line arguments used to start QEMU. These
    94  // arguments are derived from the given QEMU struct.
    95  func (o *Options) Cmdline() ([]string, error) {
    96  	var args []string
    97  	if len(o.QEMUPath) > 0 {
    98  		args = append(args, o.QEMUPath)
    99  	} else {
   100  		// Read first few arguments for env.
   101  		env := os.Getenv("UROOT_QEMU")
   102  		if env == "" {
   103  			env = "qemu" // default
   104  		}
   105  		args = append(args, strings.Fields(env)...)
   106  	}
   107  
   108  	// Disable graphics because we are using serial.
   109  	args = append(args, "-nographic")
   110  
   111  	// Arguments passed to the kernel:
   112  	//
   113  	// - earlyprintk=ttyS0: print very early debug messages to the serial
   114  	// - console=ttyS0: /dev/console points to /dev/ttyS0 (the serial port)
   115  	// - o.KernelArgs: extra, optional kernel arguments
   116  	// - args required by devices
   117  	for _, dev := range o.Devices {
   118  		if dev != nil {
   119  			if a := dev.KArgs(); a != nil {
   120  				o.KernelArgs += " " + strings.Join(a, " ")
   121  			}
   122  		}
   123  	}
   124  	if len(o.Kernel) != 0 {
   125  		args = append(args, "-kernel", o.Kernel)
   126  		if len(o.KernelArgs) != 0 {
   127  			args = append(args, "-append", o.KernelArgs)
   128  		}
   129  	} else if len(o.KernelArgs) != 0 {
   130  		err := fmt.Errorf("kernel args are required but cannot be added due to bootloader")
   131  		return nil, err
   132  	}
   133  	if len(o.Initramfs) != 0 {
   134  		args = append(args, "-initrd", o.Initramfs)
   135  	}
   136  
   137  	for _, dev := range o.Devices {
   138  		if dev != nil {
   139  			if c := dev.Cmdline(); c != nil {
   140  				args = append(args, c...)
   141  			}
   142  		}
   143  	}
   144  	return args, nil
   145  }
   146  
   147  // VM is a running QEMU virtual machine.
   148  type VM struct {
   149  	Options *Options
   150  	cmdline []string
   151  	errCh   <-chan error
   152  	gExpect *expect.GExpect
   153  }
   154  
   155  // Wait waits for the VM to exit.
   156  func (v *VM) Wait() error {
   157  	return <-v.errCh
   158  }
   159  
   160  // Cmdline is the command-line the VM was started with.
   161  func (v *VM) Cmdline() []string {
   162  	// Maybe return a copy?
   163  	return v.cmdline
   164  }
   165  
   166  // CmdlineQuoted quotes any of QEMU's command line arguments containing a space
   167  // so it is easy to copy-n-paste into a shell for debugging.
   168  func (v *VM) CmdlineQuoted() string {
   169  	args := make([]string, len(v.cmdline))
   170  	for i, arg := range v.cmdline {
   171  		if strings.ContainsAny(arg, " \t\n") {
   172  			args[i] = fmt.Sprintf("'%s'", arg)
   173  		} else {
   174  			args[i] = arg
   175  		}
   176  	}
   177  	return strings.Join(args, " ")
   178  }
   179  
   180  // Close stops QEMU.
   181  func (v *VM) Close() {
   182  	v.gExpect.Close()
   183  	v.gExpect = nil
   184  	// Ensure the logs are closed by waiting the complete end
   185  	<-v.errCh
   186  }
   187  
   188  // Send sends a string to QEMU's serial.
   189  func (v *VM) Send(in string) {
   190  	v.gExpect.Send(in)
   191  }
   192  
   193  func (v *VM) TimeoutOr() time.Duration {
   194  	if v.Options.Timeout == 0 {
   195  		return DefaultTimeout
   196  	}
   197  	return v.Options.Timeout
   198  }
   199  
   200  // Expect returns an error if the given string is not found in vEMU's serial
   201  // output within `DefaultTimeout`.
   202  func (v *VM) Expect(search string) error {
   203  	return v.ExpectTimeout(search, v.TimeoutOr())
   204  }
   205  
   206  // ExpectTimeout returns an error if the given string is not found in vEMU's serial
   207  // output within the given timeout.
   208  func (v *VM) ExpectTimeout(search string, timeout time.Duration) error {
   209  	_, err := v.ExpectRETimeout(regexp.MustCompile(regexp.QuoteMeta(search)), timeout)
   210  	return err
   211  }
   212  
   213  // ExpectRE returns an error if the given regular expression is not found in
   214  // vEMU's serial output within `DefaultTimeout`. The matched string is
   215  // returned.
   216  func (v *VM) ExpectRE(pattern *regexp.Regexp) (string, error) {
   217  	return v.ExpectRETimeout(pattern, v.TimeoutOr())
   218  }
   219  
   220  // ExpectRETimeout returns an error if the given regular expression is not
   221  // found in vEMU's serial output within the given timeout. The matched string
   222  // is returned.
   223  func (v *VM) ExpectRETimeout(pattern *regexp.Regexp, timeout time.Duration) (string, error) {
   224  	scaled := time.Duration(float64(timeout) * TimeoutMultiplier)
   225  	str, _, err := v.gExpect.Expect(pattern, scaled)
   226  	return str, err
   227  }