Reland "[botanist] Expose a socket to proxy serial I/O"

This is a reland of Ice609af336852143fa9bc4e129c7310c6bba51da

Original change's description:
> [botanist] Expose a socket to proxy serial I/O
>
> Bug: 41930
> Bug: 41377
>
> Test: CQ consumes botanist from source.
>
> Change-Id: Ice609af336852143fa9bc4e129c7310c6bba51da

TBR=phosek@google.com,garymm@google.com,joshuaseaton@google.com

Change-Id: I827a1377d8ae045b192a657aa4062369b043b607
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: 41930, 41377
diff --git a/tools/botanist/botanist.go b/tools/botanist/botanist.go
new file mode 100644
index 0000000..016eea1c
--- /dev/null
+++ b/tools/botanist/botanist.go
@@ -0,0 +1,14 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can
+// found in the LICENSE file.
+
+package botanist
+
+const (
+	// SerialLogBufferSize gives the amount of data to buffer when doing serial
+	// I/O.
+	// The size of the serial log of an entire test run is on the order of 1MB;
+	// accordingly, we allow a buffer size of an order of magnitude less for
+	// conservative estimate.
+	SerialLogBufferSize = (1000 * 1000) / 10
+)
diff --git a/tools/botanist/cmd/run.go b/tools/botanist/cmd/run.go
index eb54c46..13f784f 100644
--- a/tools/botanist/cmd/run.go
+++ b/tools/botanist/cmd/run.go
@@ -5,6 +5,8 @@
 
 import (
 	"context"
+	"crypto/rand"
+	"encoding/hex"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -12,16 +14,19 @@
 	"io/ioutil"
 	"net"
 	"os"
+	"path/filepath"
 	"sync"
 	"time"
 
 	"go.fuchsia.dev/fuchsia/tools/bootserver/lib"
+	"go.fuchsia.dev/fuchsia/tools/botanist/lib"
 	"go.fuchsia.dev/fuchsia/tools/botanist/target"
 	"go.fuchsia.dev/fuchsia/tools/lib/command"
 	"go.fuchsia.dev/fuchsia/tools/lib/logger"
 	"go.fuchsia.dev/fuchsia/tools/lib/runner"
 	"go.fuchsia.dev/fuchsia/tools/lib/syslog"
 	"go.fuchsia.dev/fuchsia/tools/net/sshutil"
+	"go.fuchsia.dev/fuchsia/tools/serial"
 
 	"github.com/google/subcommands"
 )
@@ -170,22 +175,40 @@
 
 	errs := make(chan error)
 
-	serial := t0.Serial()
-	if serial != nil && r.serialLogFile != "" {
+	var socketPath string
+	if t0.Serial() != nil {
 		// Modify the zirconArgs passed to the kernel on boot to enable serial on x64.
 		// arm64 devices should already be enabling kernel.serial at compile time.
 		r.zirconArgs = append(r.zirconArgs, "kernel.serial=legacy")
 		// Force serial output to the console instead of buffering it.
 		r.zirconArgs = append(r.zirconArgs, "kernel.bypass-debuglog=true")
 
-		serialLog, err := os.Create(r.serialLogFile)
+		sOpts := serial.ServerOptions{
+			WriteBufferSize: botanist.SerialLogBufferSize,
+		}
+		if r.serialLogFile != "" {
+			serialLog, err := os.Create(r.serialLogFile)
+			if err != nil {
+				return err
+			}
+			defer serialLog.Close()
+			sOpts.AuxiliaryOutput = serialLog
+		}
+
+		s := serial.NewServer(t0.Serial(), sOpts)
+		socketPath = createSocketPath()
+		addr := &net.UnixAddr{Name: socketPath, Net: "unix"}
+		l, err := net.ListenUnix("unix", addr)
 		if err != nil {
 			return err
 		}
-		defer serialLog.Close()
+		defer l.Close()
+		ctx, cancel := context.WithCancel(ctx)
+		defer cancel()
 		go func() {
-			if _, err := io.Copy(serialLog, serial); err != nil {
-				errs <- fmt.Errorf("failed to write serial log: %v", err)
+			if err := s.Run(ctx, l); err != nil {
+				errs <- err
+				return
 			}
 		}()
 	}
@@ -227,7 +250,7 @@
 	}
 	go func() {
 		wg.Wait()
-		errs <- r.runAgainstTarget(ctx, t0, args)
+		errs <- r.runAgainstTarget(ctx, t0, args, socketPath)
 	}()
 
 	select {
@@ -238,9 +261,10 @@
 	return nil
 }
 
-func (r *RunCommand) runAgainstTarget(ctx context.Context, t Target, args []string) error {
+func (r *RunCommand) runAgainstTarget(ctx context.Context, t Target, args []string, socketPath string) error {
 	subprocessEnv := map[string]string{
-		"FUCHSIA_NODENAME": t.Nodename(),
+		"FUCHSIA_NODENAME":      t.Nodename(),
+		"FUCHSIA_SERIAL_SOCKET": socketPath,
 	}
 
 	// If |netboot| is true, then we assume that fuchsia is not provisioned
@@ -321,6 +345,13 @@
 	return subcommands.ExitSuccess
 }
 
+func createSocketPath() string {
+	// We randomly construct a socket path that is highly improbable to collide with anything.
+	randBytes := make([]byte, 16)
+	rand.Read(randBytes)
+	return filepath.Join(os.TempDir(), "serial"+hex.EncodeToString(randBytes)+".sock")
+}
+
 func deriveTarget(ctx context.Context, obj []byte, opts target.Options) (Target, error) {
 	type typed struct {
 		Type string `json:"type"`