From a074da17a4055352fea94afbd5a15c53d0946653 Mon Sep 17 00:00:00 2001 From: Dmitry Vyukov Date: Thu, 8 Dec 2016 20:54:09 +0100 Subject: vm/adb: support BeagleBone console BeagleBone console requires some special tty-ism to work. Fortunately, this code also works with Suzy-Q. --- vm/adb/adb.go | 73 +++++++++++++---------------------------------------- vm/adb/console.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ vm/gce/gce.go | 30 ++++++---------------- vm/merger.go | 9 ++++++- vm/merger_test.go | 8 ++++-- vm/qemu/qemu.go | 17 +++++-------- 6 files changed, 119 insertions(+), 93 deletions(-) create mode 100644 vm/adb/console.go diff --git a/vm/adb/adb.go b/vm/adb/adb.go index f16a01e0e..77f7489c9 100644 --- a/vm/adb/adb.go +++ b/vm/adb/adb.go @@ -47,7 +47,7 @@ func ctor(cfg *vm.Config) (vm.Instance, error) { return nil, err } var err error - if inst.console, err = findConsole(inst.cfg.Device); err != nil { + if inst.console, err = findConsole(inst.cfg.Bin, inst.cfg.Device); err != nil { return nil, err } if err := inst.checkBatteryLevel(); err != nil { @@ -83,7 +83,7 @@ var ( // while Suzy-Q console uses the same USB calbe as adb. // The overall idea is as follows. We use 'adb shell' to write a unique string onto console, // then we read from all console devices and see on what console the unique string appears. -func findConsole(dev string) (string, error) { +func findConsole(adb, dev string) (string, error) { consoleCacheMu.Lock() defer consoleCacheMu.Unlock() if con := devToConsole[dev]; con != "" { @@ -103,20 +103,17 @@ func findConsole(dev string) (string, error) { out := new([]byte) output[con] = out go func(con string) { - cmd := exec.Command("cat", con) - stdout, err := cmd.StdoutPipe() + tty, err := openConsole(con) if err != nil { errors <- err + return } - if cmd.Start() != nil { - errors <- err - } + defer tty.Close() go func() { <-done - cmd.Process.Kill() + tty.Close() }() - *out, _ = ioutil.ReadAll(stdout) - cmd.Wait() + *out, _ = ioutil.ReadAll(tty) errors <- nil }(con) } @@ -125,7 +122,7 @@ func findConsole(dev string) (string, error) { } time.Sleep(500 * time.Millisecond) unique := fmt.Sprintf(">>>%v<<<", dev) - cmd := exec.Command("adb", "-s", dev, "shell", "echo", "\"", unique, "\"", ">", "/dev/kmsg") + cmd := exec.Command(adb, "-s", dev, "shell", "echo", "\"", unique, "\"", ">", "/dev/kmsg") if out, err := cmd.CombinedOutput(); err != nil { return "", fmt.Errorf("failed to run adb shell: %v\n%s", err, out) } @@ -329,34 +326,14 @@ func (inst *instance) Copy(hostSrc string) (string, error) { } func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command string) (<-chan []byte, <-chan error, error) { - catRpipe, catWpipe, err := vm.LongPipe() + tty, err := openConsole(inst.console) if err != nil { return nil, nil, err } - cat := exec.Command("cat", inst.console) - cat.Stdout = catWpipe - cat.Stderr = catWpipe - if err := cat.Start(); err != nil { - catRpipe.Close() - catWpipe.Close() - return nil, nil, fmt.Errorf("failed to start cat %v: %v", inst.console, err) - - } - catWpipe.Close() - catDone := make(chan error, 1) - go func() { - err := cat.Wait() - if inst.cfg.Debug { - Logf(0, "cat exited: %v", err) - } - catDone <- fmt.Errorf("cat exited: %v", err) - }() - adbRpipe, adbWpipe, err := vm.LongPipe() if err != nil { - cat.Process.Kill() - catRpipe.Close() + tty.Close() return nil, nil, err } if inst.cfg.Debug { @@ -366,29 +343,20 @@ func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command strin adb.Stdout = adbWpipe adb.Stderr = adbWpipe if err := adb.Start(); err != nil { - cat.Process.Kill() - catRpipe.Close() + tty.Close() adbRpipe.Close() adbWpipe.Close() return nil, nil, fmt.Errorf("failed to start adb: %v", err) } adbWpipe.Close() - adbDone := make(chan error, 1) - go func() { - err := adb.Wait() - if inst.cfg.Debug { - Logf(0, "adb exited: %v", err) - } - adbDone <- fmt.Errorf("adb exited: %v", err) - }() var tee io.Writer if inst.cfg.Debug { tee = os.Stdout } merger := vm.NewOutputMerger(tee) - merger.Add(catRpipe) - merger.Add(adbRpipe) + merger.Add("console", tty) + merger.Add("adb", adbRpipe) errc := make(chan error, 1) signal := func(err error) { @@ -402,27 +370,20 @@ func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command strin select { case <-time.After(timeout): signal(vm.TimeoutErr) - cat.Process.Kill() - adb.Process.Kill() case <-stop: signal(vm.TimeoutErr) - cat.Process.Kill() - adb.Process.Kill() case <-inst.closed: if inst.cfg.Debug { Logf(0, "instance closed") } signal(fmt.Errorf("instance closed")) - cat.Process.Kill() - adb.Process.Kill() - case err := <-catDone: - signal(err) - adb.Process.Kill() - case err := <-adbDone: + case err := <-merger.Err: signal(err) - cat.Process.Kill() } + tty.Close() + adb.Process.Kill() merger.Wait() + adb.Wait() }() return merger.Output, errc, nil } diff --git a/vm/adb/console.go b/vm/adb/console.go new file mode 100644 index 000000000..e55e019ae --- /dev/null +++ b/vm/adb/console.go @@ -0,0 +1,75 @@ +// Copyright 2016 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package adb + +import ( + "fmt" + "io" + "sync" + "syscall" + "unsafe" + + . "golang.org/x/sys/unix" +) + +// Tested on Suzy-Q and BeagleBone. +func openConsole(con string) (rc io.ReadCloser, err error) { + fd, err := syscall.Open(con, syscall.O_RDONLY|syscall.O_NOCTTY|syscall.O_SYNC, 0) + if err != nil { + return nil, fmt.Errorf("failed to open console file: %v", err) + } + defer func() { + if fd != -1 { + syscall.Close(fd) + } + }() + var term Termios + if _, _, errno := syscall.Syscall(SYS_IOCTL, uintptr(fd), TCGETS2, uintptr(unsafe.Pointer(&term))); errno != 0 { + return nil, fmt.Errorf("failed to get console termios: %v", errno) + } + // no parity bit, only need 1 stop bit, no hardware flowcontrol + term.Cflag &^= CBAUD | CSIZE | PARENB | CSTOPB | CRTSCTS + // ignore modem controls + term.Cflag |= B115200 | CS8 | CLOCAL | CREAD + // setup for non-canonical mode + term.Iflag &^= IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON + term.Lflag &^= ECHO | ECHONL | ICANON | ISIG | IEXTEN + term.Oflag &^= OPOST + term.Cc[VMIN] = 0 + term.Cc[VTIME] = 10 // 1 second timeout + if _, _, errno := syscall.Syscall(SYS_IOCTL, uintptr(fd), TCSETS2, uintptr(unsafe.Pointer(&term))); errno != 0 { + return nil, fmt.Errorf("failed to get console termios: %v", errno) + } + tmp := fd + fd = -1 + return &tty{fd: tmp}, nil +} + +type tty struct { + mu sync.Mutex + fd int +} + +func (t *tty) Read(buf []byte) (int, error) { + t.mu.Lock() + defer t.mu.Unlock() + if t.fd == -1 { + return 0, io.EOF + } + n, err := syscall.Read(t.fd, buf) + if n < 0 { + n = 0 + } + return n, err +} + +func (t *tty) Close() error { + t.mu.Lock() + defer t.mu.Unlock() + if t.fd != -1 { + syscall.Close(t.fd) + t.fd = -1 + } + return nil +} diff --git a/vm/gce/gce.go b/vm/gce/gce.go index 03ceb10dd..edce6c38e 100644 --- a/vm/gce/gce.go +++ b/vm/gce/gce.go @@ -170,11 +170,6 @@ func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command strin } conWpipe.Close() - conDone := make(chan error, 1) - go func() { - err := con.Wait() - conDone <- fmt.Errorf("console connection closed: %v", err) - }() sshRpipe, sshWpipe, err := vm.LongPipe() if err != nil { @@ -197,15 +192,10 @@ func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command strin return nil, nil, fmt.Errorf("failed to connect to instance: %v", err) } sshWpipe.Close() - sshDone := make(chan error, 1) - go func() { - err := ssh.Wait() - sshDone <- fmt.Errorf("ssh exited: %v", err) - }() merger := vm.NewOutputMerger(nil) - merger.Add(conRpipe) - merger.Add(sshRpipe) + merger.Add("console", conRpipe) + merger.Add("ssh", sshRpipe) errc := make(chan error, 1) signal := func(err error) { @@ -219,20 +209,11 @@ func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command strin select { case <-time.After(timeout): signal(vm.TimeoutErr) - con.Process.Kill() - ssh.Process.Kill() case <-stop: signal(vm.TimeoutErr) - con.Process.Kill() - ssh.Process.Kill() case <-inst.closed: signal(fmt.Errorf("instance closed")) - con.Process.Kill() - ssh.Process.Kill() - case err := <-conDone: - signal(err) - ssh.Process.Kill() - case err := <-sshDone: + case err := <-merger.Err: // Check if the instance was terminated due to preemption or host maintenance. time.Sleep(5 * time.Second) // just to avoid any GCE races if !GCE.IsInstanceRunning(inst.name) { @@ -240,9 +221,12 @@ func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command strin err = vm.TimeoutErr } signal(err) - con.Process.Kill() } + con.Process.Kill() + ssh.Process.Kill() merger.Wait() + con.Wait() + ssh.Wait() }() return merger.Output, errc, nil } diff --git a/vm/merger.go b/vm/merger.go index 2effd0f45..c3b5c16d0 100644 --- a/vm/merger.go +++ b/vm/merger.go @@ -5,12 +5,14 @@ package vm import ( "bytes" + "fmt" "io" "sync" ) type OutputMerger struct { Output chan []byte + Err chan error tee io.Writer wg sync.WaitGroup } @@ -18,6 +20,7 @@ type OutputMerger struct { func NewOutputMerger(tee io.Writer) *OutputMerger { return &OutputMerger{ Output: make(chan []byte, 1000), + Err: make(chan error, 1), tee: tee, } } @@ -27,7 +30,7 @@ func (merger *OutputMerger) Wait() { close(merger.Output) } -func (merger *OutputMerger) Add(r io.ReadCloser) { +func (merger *OutputMerger) Add(name string, r io.ReadCloser) { merger.wg.Add(1) go func() { var pending []byte @@ -61,6 +64,10 @@ func (merger *OutputMerger) Add(r io.ReadCloser) { } } r.Close() + select { + case merger.Err <- fmt.Errorf("failed to read from %v: %v", name, err): + default: + } merger.wg.Done() return } diff --git a/vm/merger_test.go b/vm/merger_test.go index 1f0b0ba87..35da35284 100644 --- a/vm/merger_test.go +++ b/vm/merger_test.go @@ -18,14 +18,14 @@ func TestMerger(t *testing.T) { t.Fatal(err) } defer wp1.Close() - merger.Add(rp1) + merger.Add("pipe1", rp1) rp2, wp2, err := LongPipe() if err != nil { t.Fatal(err) } defer wp2.Close() - merger.Add(rp2) + merger.Add("pipe2", rp2) wp1.Write([]byte("111")) select { @@ -59,6 +59,10 @@ func TestMerger(t *testing.T) { t.Fatalf("bad line: '%s', want '%s'", got, want) } + if err := <-merger.Err; err == nil || err.Error() != "failed to read from pipe1: EOF" { + t.Fatalf("merger did not produce io.EOF: %v", err) + } + wp2.Close() got = string(<-merger.Output) if want := "777\n"; got != want { diff --git a/vm/qemu/qemu.go b/vm/qemu/qemu.go index 6c2ac3b89..386e96b9a 100644 --- a/vm/qemu/qemu.go +++ b/vm/qemu/qemu.go @@ -222,7 +222,7 @@ func (inst *instance) Boot() error { tee = os.Stdout } inst.merger = vm.NewOutputMerger(tee) - inst.merger.Add(inst.rpipe) + inst.merger.Add("qemu", inst.rpipe) inst.rpipe = nil var bootOutput []byte @@ -321,7 +321,7 @@ func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command strin if err != nil { return nil, nil, err } - inst.merger.Add(rpipe) + inst.merger.Add("ssh", rpipe) args := append(inst.sshArgs("-p"), "root@localhost", command) if inst.cfg.Debug { @@ -343,22 +343,17 @@ func (inst *instance) Run(timeout time.Duration, stop <-chan bool, command strin } } - done := make(chan bool) go func() { select { case <-time.After(timeout): signal(vm.TimeoutErr) - cmd.Process.Kill() case <-stop: signal(vm.TimeoutErr) - cmd.Process.Kill() - case <-done: + case err := <-inst.merger.Err: + signal(err) } - }() - go func() { - err := cmd.Wait() - close(done) - signal(err) + cmd.Process.Kill() + cmd.Wait() }() return inst.merger.Output, errc, nil } -- cgit mrf-deployment