github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+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 "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.PartialMatch(true), 82 expect.CheckDuration(2*time.Millisecond)) 83 if err != nil { 84 return nil, err 85 } 86 return &VM{ 87 Options: o, 88 errCh: ch, 89 cmdline: cmdline, 90 gExpect: gExpect, 91 }, nil 92 } 93 94 // Cmdline returns the command line arguments used to start QEMU. These 95 // arguments are derived from the given QEMU struct. 96 func (o *Options) Cmdline() ([]string, error) { 97 var args []string 98 if len(o.QEMUPath) > 0 { 99 args = append(args, o.QEMUPath) 100 } else { 101 // Read first few arguments for env. 102 env := os.Getenv("UROOT_QEMU") 103 if env == "" { 104 env = "qemu" // default 105 } 106 args = append(args, strings.Fields(env)...) 107 } 108 109 // Disable graphics because we are using serial. 110 args = append(args, "-nographic") 111 112 // Arguments passed to the kernel: 113 // 114 // - earlyprintk=ttyS0: print very early debug messages to the serial 115 // - console=ttyS0: /dev/console points to /dev/ttyS0 (the serial port) 116 // - o.KernelArgs: extra, optional kernel arguments 117 // - args required by devices 118 for _, dev := range o.Devices { 119 if dev != nil { 120 if a := dev.KArgs(); a != nil { 121 o.KernelArgs += " " + strings.Join(a, " ") 122 } 123 } 124 } 125 if len(o.Kernel) != 0 { 126 args = append(args, "-kernel", o.Kernel) 127 if len(o.KernelArgs) != 0 { 128 args = append(args, "-append", o.KernelArgs) 129 } 130 } else if len(o.KernelArgs) != 0 { 131 err := fmt.Errorf("kernel args are required but cannot be added due to bootloader") 132 return nil, err 133 } 134 if len(o.Initramfs) != 0 { 135 args = append(args, "-initrd", o.Initramfs) 136 } 137 138 for _, dev := range o.Devices { 139 if dev != nil { 140 if c := dev.Cmdline(); c != nil { 141 args = append(args, c...) 142 } 143 } 144 } 145 return args, nil 146 } 147 148 // VM is a running QEMU virtual machine. 149 type VM struct { 150 Options *Options 151 cmdline []string 152 errCh <-chan error 153 gExpect *expect.GExpect 154 } 155 156 // Wait waits for the VM to exit. 157 func (v *VM) Wait() error { 158 return <-v.errCh 159 } 160 161 // Cmdline is the command-line the VM was started with. 162 func (v *VM) Cmdline() []string { 163 // Maybe return a copy? 164 return v.cmdline 165 } 166 167 // CmdlineQuoted quotes any of QEMU's command line arguments containing a space 168 // so it is easy to copy-n-paste into a shell for debugging. 169 func (v *VM) CmdlineQuoted() string { 170 args := make([]string, len(v.cmdline)) 171 for i, arg := range v.cmdline { 172 if strings.ContainsAny(arg, " \t\n") { 173 args[i] = fmt.Sprintf("'%s'", arg) 174 } else { 175 args[i] = arg 176 } 177 } 178 return strings.Join(args, " ") 179 } 180 181 // Close stops QEMU. 182 func (v *VM) Close() { 183 v.gExpect.Close() 184 v.gExpect = nil 185 // Ensure the logs are closed by waiting the complete end 186 <-v.errCh 187 } 188 189 // Send sends a string to QEMU's serial. 190 func (v *VM) Send(in string) { 191 v.gExpect.Send(in) 192 } 193 194 func (v *VM) TimeoutOr() time.Duration { 195 if v.Options.Timeout == 0 { 196 return DefaultTimeout 197 } 198 return v.Options.Timeout 199 } 200 201 // Expect returns an error if the given string is not found in vEMU's serial 202 // output within `DefaultTimeout`. 203 func (v *VM) Expect(search string) error { 204 return v.ExpectTimeout(search, v.TimeoutOr()) 205 } 206 207 // ExpectTimeout returns an error if the given string is not found in vEMU's serial 208 // output within the given timeout. 209 func (v *VM) ExpectTimeout(search string, timeout time.Duration) error { 210 _, err := v.ExpectRETimeout(regexp.MustCompile(regexp.QuoteMeta(search)), timeout) 211 return err 212 } 213 214 // ExpectRE returns an error if the given regular expression is not found in 215 // vEMU's serial output within `DefaultTimeout`. The matched string is 216 // returned. 217 func (v *VM) ExpectRE(pattern *regexp.Regexp) (string, error) { 218 return v.ExpectRETimeout(pattern, v.TimeoutOr()) 219 } 220 221 // ExpectRETimeout returns an error if the given regular expression is not 222 // found in vEMU's serial output within the given timeout. The matched string 223 // is returned. 224 func (v *VM) ExpectRETimeout(pattern *regexp.Regexp, timeout time.Duration) (string, error) { 225 scaled := time.Duration(float64(timeout) * TimeoutMultiplier) 226 str, _, err := v.gExpect.Expect(pattern, scaled) 227 return str, err 228 } 229 230 // ExpectBatch matches many regular expressions. 231 func (v *VM) ExpectBatch(batch []expect.Batcher, timeout time.Duration) ([]expect.BatchRes, error) { 232 scaled := time.Duration(float64(timeout) * TimeoutMultiplier) 233 return v.gExpect.ExpectBatch(batch, scaled) 234 }