[serial][botanist] Respect ctx.Cancel().

This refactors the Serial object into its own interface that extends the
normal io.ReadWriteCloser behavior to allow cancellation during
io.Read() so that io.Copy() may be cancelled prematurely on
ctx.Cancel().

Change-Id: I1c3dc325cf3a02b0475a4a878ef251076b1f681c
diff --git a/botanist/serial_device.go b/botanist/serial_device.go
new file mode 100644
index 0000000..9ddc626
--- /dev/null
+++ b/botanist/serial_device.go
@@ -0,0 +1,40 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package botanist
+
+import (
+	"context"
+	"io"
+
+	"fuchsia.googlesource.com/tools/serial"
+)
+
+// SerialDevice is the interface to interacting with a serial device.
+type SerialDevice struct {
+	ctx context.Context
+	io.ReadWriteCloser
+}
+
+// NewSerialDevice returns a new SerialDevice which is usable as io.ReadWriteCloser.
+func NewSerialDevice(ctx context.Context, device string) (*SerialDevice, error) {
+	s, err := serial.Open(device)
+	if err != nil {
+		return nil, err
+	}
+	return &SerialDevice{
+		ctx:             ctx,
+		ReadWriteCloser: s,
+	}, nil
+}
+
+// Read is a thin wrapper around io.Read() which respects Context.cancel() so that we may cancel an in-flight io.Copy().
+func (s *SerialDevice) Read(p []byte) (int, error) {
+	select {
+	case <-s.ctx.Done():
+		return 0, s.ctx.Err()
+	default:
+		return s.ReadWriteCloser.Read(p)
+	}
+}
diff --git a/botanist/target/device.go b/botanist/target/device.go
index 6c14c9b..ced7696 100644
--- a/botanist/target/device.go
+++ b/botanist/target/device.go
@@ -9,7 +9,6 @@
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"net"
 	"time"
@@ -17,10 +16,8 @@
 	"fuchsia.googlesource.com/tools/botanist"
 	"fuchsia.googlesource.com/tools/botanist/power"
 	"fuchsia.googlesource.com/tools/build"
-	"fuchsia.googlesource.com/tools/logger"
 	"fuchsia.googlesource.com/tools/netboot"
 	"fuchsia.googlesource.com/tools/netutil"
-	"fuchsia.googlesource.com/tools/serial"
 
 	"golang.org/x/crypto/ssh"
 )
@@ -74,7 +71,7 @@
 	config  DeviceConfig
 	opts    Options
 	signers []ssh.Signer
-	serial  io.ReadWriteCloser
+	serial  *botanist.SerialDevice
 }
 
 // NewDeviceTarget returns a new device target with a given configuration.
@@ -88,14 +85,11 @@
 	if err != nil {
 		return nil, fmt.Errorf("could not parse out signers from private keys: %v", err)
 	}
-	var s io.ReadWriteCloser
+	var s *botanist.SerialDevice
 	if config.Serial != "" {
-		s, err = serial.Open(config.Serial)
+		s, err = botanist.NewSerialDevice(ctx, config.Serial)
 		if err != nil {
-			// TODO(IN-????): This should be returned as an error, but we don't want to fail any
-			// test runs for misconfigured serial until it is actually required to complete certain
-			// tasks.
-			logger.Errorf(ctx, "unable to open %s: %v", config.Serial, err)
+			return nil, err
 		}
 	}
 	return &DeviceTarget{
@@ -121,7 +115,7 @@
 }
 
 // Serial returns the serial device associated with the target for serial i/o.
-func (t *DeviceTarget) Serial() io.ReadWriteCloser {
+func (t *DeviceTarget) Serial() *botanist.SerialDevice {
 	return t.serial
 }
 
diff --git a/botanist/target/qemu.go b/botanist/target/qemu.go
index a9faf9b..a71e1d9 100644
--- a/botanist/target/qemu.go
+++ b/botanist/target/qemu.go
@@ -15,6 +15,7 @@
 	"os/exec"
 	"path/filepath"
 
+	"fuchsia.googlesource.com/tools/botanist"
 	"fuchsia.googlesource.com/tools/build"
 	"fuchsia.googlesource.com/tools/qemu"
 )
@@ -104,7 +105,7 @@
 }
 
 // Serial returns the serial device associated with the target for serial i/o.
-func (t *QEMUTarget) Serial() io.ReadWriteCloser {
+func (t *QEMUTarget) Serial() *botanist.SerialDevice {
 	return nil
 }
 
diff --git a/cmd/botanist/run.go b/cmd/botanist/run.go
index 60a425d..7c33917 100644
--- a/cmd/botanist/run.go
+++ b/cmd/botanist/run.go
@@ -38,7 +38,7 @@
 	IPv4Addr() (net.IP, error)
 
 	// Serial returns the serial device associated with the target for serial i/o.
-	Serial() io.ReadWriteCloser
+	Serial() *botanist.SerialDevice
 
 	// SSHKey returns the private key corresponding an authorized SSH key of the target.
 	SSHKey() string
@@ -200,8 +200,8 @@
 	}
 
 	opts := target.Options{
-		Netboot:  r.netboot,
-		SSHKey:   r.sshKey,
+		Netboot: r.netboot,
+		SSHKey:  r.sshKey,
 	}
 
 	data, err := ioutil.ReadFile(r.configFile)
@@ -253,9 +253,12 @@
 
 			go func() {
 				_, err := io.Copy(serialLog, t.Serial())
-				if err != nil {
+				if err != nil && err != context.Canceled {
 					logger.Errorf(ctx, "failed to write serial log: %v", err)
 				}
+				if err := serialLog.Sync(); err != nil {
+					logger.Errorf(ctx, "failed to fsync serial log: %v", err)
+				}
 			}()
 			r.zirconArgs = append(r.zirconArgs, "kernel.bypass-debuglog=true")
 		}