Revert "[tools] Move logic for running commands over serial to serial package."
This reverts commit a74fc61e5ab07ff4212df7b8f5a93ee04c9ad6db.
diff --git a/tools/botanist/cmd/run.go b/tools/botanist/cmd/run.go
index 08588f6..f735d52 100644
--- a/tools/botanist/cmd/run.go
+++ b/tools/botanist/cmd/run.go
@@ -338,13 +338,15 @@
if sshAddr.IP == nil {
// Reachable when ResolveIP times out because no error is returned.
// Invoke `threads` over serial if possible to dump process state to logs.
- socket, err := serial.NewSocket(ctx, socketPath)
- if err != nil {
- logger.Errorf(ctx, "newSerialSocket failed: %v", err)
+ // Invokes the command twice to identify hanging processes.
+ if conn, err := net.Dial("unix", socketPath); err != nil {
+ logger.Errorf(ctx, "failed to open serial socket %s to invoke threads: %s", socketPath, err)
} else {
- defer socket.Close()
- if err := serial.RunDiagnostics(ctx, socket); err != nil {
- logger.Errorf(ctx, "failed to run diagnostics over serial socket: %v", err)
+ for i := 0; i < 2; i++ {
+ if _, err := io.WriteString(conn, fmt.Sprintf("\r\nthreads --all-processes\r\n")); err != nil {
+ logger.Errorf(ctx, "failed to send threads over serial socket: %s", err)
+ }
+ time.Sleep(5 * time.Second)
}
}
return fmt.Errorf("%s for %s", constants.FailedToResolveIPErrorMsg, t.Nodename())
diff --git a/tools/serial/BUILD.gn b/tools/serial/BUILD.gn
index b8855f0..5479310 100644
--- a/tools/serial/BUILD.gn
+++ b/tools/serial/BUILD.gn
@@ -10,13 +10,11 @@
"serial.go",
"serial_darwin.go",
"serial_linux.go",
- "serial_test.go",
"server.go",
"server_test.go",
]
deps = [
"//third_party/golibs:golang.org/x/sys",
- "//tools/lib/iomisc",
"//tools/lib/logger",
]
}
@@ -24,9 +22,5 @@
go_test("tests") {
output_name = "serial_tests"
gopackages = [ "go.fuchsia.dev/fuchsia/tools/serial" ]
- deps = [
- ":serial",
- "//third_party/golibs:github.com/google/go-cmp",
- "//third_party/golibs:golang.org/x/sync",
- ]
+ deps = [ ":serial" ]
}
diff --git a/tools/serial/serial.go b/tools/serial/serial.go
index ab9f116..83fb6b5 100644
--- a/tools/serial/serial.go
+++ b/tools/serial/serial.go
@@ -6,35 +6,13 @@
package serial
import (
- "context"
- "fmt"
"io"
- "net"
- "strings"
- "time"
-
- "go.fuchsia.dev/fuchsia/tools/lib/iomisc"
- "go.fuchsia.dev/fuchsia/tools/lib/logger"
)
const (
defaultBaudRate = 115200
-
- // Printed to the serial console when ready to accept user input.
- consoleCursor = "\n$"
)
-var diagnosticCmds = []Command{
- {[]string{"k", "threadload"}, 200 * time.Millisecond}, // Turn on threadload
- {[]string{"k", "threadq"}, 5 * time.Second}, // Turn on threadq and wait 5 sec
- {[]string{"k", "cpu", "sev"}, 5 * time.Second}, // Send a SEV and wait 5 sec
- {[]string{"k", "threadload"}, 200 * time.Millisecond}, // Turn off threadload
- {[]string{"k", "threadq"}, 0}, // Turn off threadq
- // Invoke the threads command twice to identify hanging processes.
- {[]string{"threads", "--all-processes"}, 5 * time.Minute},
- {[]string{"threads", "--all-processes"}, 5 * time.Minute},
-}
-
// Open opens a new serial port using defaults.
func Open(name string) (io.ReadWriteCloser, error) {
return OpenWithOptions(name, defaultBaudRate)
@@ -44,63 +22,3 @@
func OpenWithOptions(name string, baudRate int) (io.ReadWriteCloser, error) {
return open(name, baudRate)
}
-
-// Command contains the command to run over serial and the expected duration
-// to wait for the command to complete.
-type Command struct {
- Cmd []string
- SleepDuration time.Duration
-}
-
-func asSerialCmd(cmd []string) string {
- // The UART kernel driver expects a command to be followed by \r\n.
- // Send a leading \r\n for all commands as there may be characters in the buffer already
- // that we need to clear first.
- return fmt.Sprintf("\r\n%s\r\n", strings.Join(cmd, " "))
-}
-
-// NewSocket opens a connection on the provided `socketPath`.
-func NewSocket(ctx context.Context, socketPath string) (io.ReadWriteCloser, error) {
- if socketPath == "" {
- return nil, fmt.Errorf("serialSocketPath not set")
- }
- socket, err := net.Dial("unix", socketPath)
- if err != nil {
- return nil, fmt.Errorf("failed to open serial socket connection: %v", err)
- }
- // Trigger a new cursor print by sending a newline. This may do nothing if the
- // system was not ready to process input, but in that case it will print a
- // new cursor anyways when it is ready to receive input.
- io.WriteString(socket, asSerialCmd([]string{}))
- // Look for the cursor, which should indicate that the console is ready for input.
- ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
- defer cancel()
- m := iomisc.NewMatchingReader(socket, [][]byte{[]byte(consoleCursor)})
- if _, err = iomisc.ReadUntilMatch(ctx, m); err != nil {
- return nil, fmt.Errorf("failed to find cursor: %v", err)
- }
- return socket, nil
-}
-
-// RunCommands writes the provided commands to the serial socket.
-func RunCommands(ctx context.Context, socket io.ReadWriteCloser, cmds []Command) error {
- for _, cmd := range cmds {
- logger.Debugf(ctx, "running over serial: %v", cmd.Cmd)
-
- if _, err := io.WriteString(socket, asSerialCmd(cmd.Cmd)); err != nil {
- return fmt.Errorf("failed to write to serial socket: %v", err)
- }
-
- if cmd.SleepDuration > 0 {
- logger.Debugf(ctx, "sleeping for %v", cmd.SleepDuration)
- time.Sleep(cmd.SleepDuration)
- }
- }
- return nil
-}
-
-// RunDiagnostics runs a series of diagnostic commands over serial.
-func RunDiagnostics(ctx context.Context, socket io.ReadWriteCloser) error {
- logger.Debugf(ctx, "attempting to run diagnostics over serial")
- return RunCommands(ctx, socket, diagnosticCmds)
-}
diff --git a/tools/serial/serial_test.go b/tools/serial/serial_test.go
deleted file mode 100644
index 64bd414..0000000
--- a/tools/serial/serial_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright 2021 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 serial
-
-import (
- "context"
- "fmt"
- "io"
- "math/rand"
- "net"
- "os"
- "testing"
- "time"
-
- "github.com/google/go-cmp/cmp"
- "go.fuchsia.dev/fuchsia/tools/lib/iomisc"
- "golang.org/x/sync/errgroup"
-)
-
-type fakeSerialServer struct {
- received []byte
- shutdownString string
- socketPath string
- listeningChan chan bool
-}
-
-func (s *fakeSerialServer) Serve() error {
- listener, err := net.Listen("unix", s.socketPath)
- if err != nil {
- s.listeningChan <- false
- return fmt.Errorf("Listen(%s) failed: %v", s.socketPath, err)
- }
- defer listener.Close()
- s.listeningChan <- true
- conn, err := listener.Accept()
- if err != nil {
- return fmt.Errorf("Accept() failed: %v", err)
- }
- defer conn.Close()
- // Signal we're ready to accept input.
- if _, err := conn.Write([]byte(consoleCursor)); err != nil {
- return fmt.Errorf("conn.Write() failed: %v", err)
- }
- reader := iomisc.NewMatchingReader(conn, [][]byte{[]byte(s.shutdownString)})
- for {
- buf := make([]byte, 1024)
- bytesRead, err := reader.Read(buf)
- s.received = append(s.received, buf[:bytesRead]...)
- if err != nil {
- if err == io.EOF {
- return nil
- }
- return fmt.Errorf("conn.Read() failed: %v", err)
- }
- }
-}
-
-func TestRunCommands(t *testing.T) {
- socketPath := fmt.Sprintf("%d.sock", rand.Uint32())
- defer os.Remove(socketPath)
- server := fakeSerialServer{
- shutdownString: "dm shutdown",
- socketPath: socketPath,
- listeningChan: make(chan bool),
- }
- eg := errgroup.Group{}
- eg.Go(server.Serve)
-
- if !<-server.listeningChan {
- t.Fatalf("fakeSerialServer.Serve() returned: %v", eg.Wait())
- }
-
- clientSocket, err := NewSocket(context.Background(), socketPath)
- if err != nil {
- t.Fatalf("NewSocket() failed: %v", err)
- }
- diagnosticCmds = []Command{
- {
- Cmd: []string{"foo"},
- // Ensure we don't waste time sleeping in this test.
- SleepDuration: time.Microsecond,
- },
- {
- Cmd: []string{"bar"},
- // Ensure we don't waste time sleeping in this test.
- SleepDuration: time.Microsecond,
- },
- {
- // This is a hack to ensure the shutdown command gets sent to the serial server.
- // Rather than introduce a new synchronization mechanism, just use the code under test's
- // existing mechanism for sending commands.
- Cmd: []string{server.shutdownString},
- // Ensure we don't waste time sleeping in this test.
- SleepDuration: time.Microsecond,
- },
- }
- // RunDiagnostics calls RunCommands so this covers both functions for testability.
- err = RunDiagnostics(context.Background(), clientSocket)
- if err != nil {
- t.Errorf("RunDiagnostics failed: %v", err)
- }
- if err = eg.Wait(); err != nil {
- t.Errorf("server returned: %v", err)
- }
- if err = clientSocket.Close(); err != nil {
- t.Errorf("clientSocket.Close() returned: %v", err)
- }
- // Verify that each command was seen in the received data, and in the
- // proper order. The first command run was an empty command in NewSocket().
- expectedCommands := asSerialCmd([]string{})
- for _, cmd := range diagnosticCmds {
- expectedCommands += asSerialCmd(cmd.Cmd)
- }
- if diff := cmp.Diff(expectedCommands, string(server.received)); diff != "" {
- t.Errorf("Unexpected server.received (-want +got):\n%s", diff)
- }
-}
diff --git a/tools/testing/testrunner/cmd/tester.go b/tools/testing/testrunner/cmd/tester.go
index 05cf1a9..a47d926 100644
--- a/tools/testing/testrunner/cmd/tester.go
+++ b/tools/testing/testrunner/cmd/tester.go
@@ -25,7 +25,6 @@
"go.fuchsia.dev/fuchsia/tools/lib/retry"
"go.fuchsia.dev/fuchsia/tools/lib/runner"
"go.fuchsia.dev/fuchsia/tools/net/sshutil"
- "go.fuchsia.dev/fuchsia/tools/serial"
"go.fuchsia.dev/fuchsia/tools/testing/runtests"
"go.fuchsia.dev/fuchsia/tools/testing/testrunner/constants"
"golang.org/x/crypto/ssh"
@@ -93,11 +92,6 @@
Close() error
}
-// For testability
-type serialClient interface {
- runDiagnostics(ctx context.Context) error
-}
-
// subprocessTester executes tests in local subprocesses.
type subprocessTester struct {
env []string
@@ -231,22 +225,6 @@
return nil
}
-type serialSocket struct {
- socketPath string
-}
-
-func (s *serialSocket) runDiagnostics(ctx context.Context) error {
- if s.socketPath == "" {
- return fmt.Errorf("serialSocketPath not set")
- }
- socket, err := serial.NewSocket(ctx, s.socketPath)
- if err != nil {
- return fmt.Errorf("newSerialSocket failed: %v", err)
- }
- defer socket.Close()
- return serial.RunDiagnostics(ctx, socket)
-}
-
// fuchsiaSSHTester executes fuchsia tests over an SSH connection.
type fuchsiaSSHTester struct {
client sshClient
@@ -255,7 +233,7 @@
localOutputDir string
perTestTimeout time.Duration
connectionErrorRetryBackoff retry.Backoff
- serialSocket serialClient
+ serialSocketPath string
}
// newFuchsiaSSHTester returns a fuchsiaSSHTester associated to a fuchsia
@@ -297,10 +275,55 @@
localOutputDir: localOutputDir,
perTestTimeout: perTestTimeout,
connectionErrorRetryBackoff: retry.NewConstantBackoff(time.Second),
- serialSocket: &serialSocket{serialSocketPath},
+ serialSocketPath: serialSocketPath,
}, nil
}
+func asSerialCmd(cmd []string) string {
+ // The UART kernel driver expects a command to be followed by \r\n.
+ // Send a leading \r\n for all commands as there may be characters in the buffer already
+ // that we need to clear first.
+ return fmt.Sprintf("\r\n%s\r\n", strings.Join(cmd, " "))
+}
+
+type serialDiagnosticCmd struct {
+ cmd []string
+ sleepDuration time.Duration
+}
+
+var serialDiagnosticCmds = []serialDiagnosticCmd{
+ {[]string{"k", "threadload"}, 200 * time.Millisecond}, // Turn on threadload
+ {[]string{"k", "threadq"}, 5 * time.Second}, // Turn on threadq and wait 5 sec
+ {[]string{"k", "cpu", "sev"}, 5 * time.Second}, // Send a SEV and wait 5 sec
+ {[]string{"k", "threadload"}, 200 * time.Millisecond}, // Turn off threadload
+ {[]string{"k", "threadq"}, 0}, // Turn off threadq
+}
+
+func (t *fuchsiaSSHTester) runSerialDiagnostics(ctx context.Context) error {
+ if t.serialSocketPath == "" {
+ return fmt.Errorf("serialSocketPath not set")
+ }
+ logger.Debugf(ctx, "attempting to run diagnostics over serial")
+ socket, err := newSerialSocket(ctx, t.serialSocketPath)
+ if err != nil {
+ return fmt.Errorf("newSerialSocket failed: %v", err)
+ }
+ defer socket.Close()
+ for _, cmd := range serialDiagnosticCmds {
+ logger.Debugf(ctx, "running over serial: %v", cmd.cmd)
+
+ if _, err := io.WriteString(socket, asSerialCmd(cmd.cmd)); err != nil {
+ return fmt.Errorf("failed to write to serial socket: %v", err)
+ }
+
+ if cmd.sleepDuration > 0 {
+ logger.Debugf(ctx, "sleeping for %v", cmd.sleepDuration)
+ time.Sleep(cmd.sleepDuration)
+ }
+ }
+ return nil
+}
+
func (t *fuchsiaSSHTester) reconnect(ctx context.Context) error {
if err := t.client.Reconnect(ctx); err != nil {
return fmt.Errorf("failed to reestablish SSH connection: %w", err)
@@ -370,7 +393,7 @@
testErr := t.runSSHCommandWithRetry(ctx, command, stdout, stderr)
if sshutil.IsConnectionError(testErr) {
- if err := t.serialSocket.runDiagnostics(ctx); err != nil {
+ if err := t.runSerialDiagnostics(ctx); err != nil {
logger.Warningf(ctx, "failed to run serial diagnostics: %v", err)
}
return nil, testErr
@@ -446,8 +469,25 @@
localOutputDir string
}
+func newSerialSocket(ctx context.Context, path string) (io.ReadWriteCloser, error) {
+ socket, err := net.Dial("unix", path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open serial socket connection: %v", err)
+ }
+ // Trigger a new cursor print by sending a newline. This may do nothing if the
+ // system was not ready to process input, but in that case it will print a
+ // new cursor anyways when it is ready to receive input.
+ io.WriteString(socket, asSerialCmd([]string{}))
+ // Look for the cursor, which should indicate that the console is ready for input.
+ m := iomisc.NewMatchingReader(socket, [][]byte{[]byte(serialConsoleCursor)})
+ if _, err = iomisc.ReadUntilMatch(ctx, m); err != nil {
+ return nil, fmt.Errorf("failed to find cursor: %v", err)
+ }
+ return socket, nil
+}
+
func newFuchsiaSerialTester(ctx context.Context, serialSocketPath string, perTestTimeout time.Duration) (*fuchsiaSerialTester, error) {
- socket, err := serial.NewSocket(ctx, serialSocketPath)
+ socket, err := newSerialSocket(ctx, serialSocketPath)
if err != nil {
return nil, err
}
@@ -479,6 +519,7 @@
if err != nil {
return nil, err
}
+ cmd := asSerialCmd(command)
logger.Debugf(ctx, "starting: %v", command)
// If a single read from the socket includes both the bytes that indicate the test started and the bytes
@@ -487,7 +528,7 @@
lastWrite := &lastWriteSaver{}
startedReader := iomisc.NewMatchingReader(io.TeeReader(t.socket, lastWrite), [][]byte{[]byte(runtests.StartedSignature + test.Name)})
for ctx.Err() == nil {
- if err := serial.RunCommands(ctx, t.socket, []serial.Command{{command, 0}}); err != nil {
+ if _, err := io.WriteString(t.socket, cmd); err != nil {
return nil, fmt.Errorf("failed to write to serial socket: %v", err)
}
startedCtx, cancel := newTestStartedContext(ctx)
diff --git a/tools/testing/testrunner/cmd/tester_test.go b/tools/testing/testrunner/cmd/tester_test.go
index fb1a787..5b33a0b 100644
--- a/tools/testing/testrunner/cmd/tester_test.go
+++ b/tools/testing/testrunner/cmd/tester_test.go
@@ -10,6 +10,7 @@
"fmt"
"io"
"io/ioutil"
+ "math/rand"
"net"
"os"
"path/filepath"
@@ -24,6 +25,7 @@
"go.fuchsia.dev/fuchsia/tools/lib/retry"
"go.fuchsia.dev/fuchsia/tools/net/sshutil"
"go.fuchsia.dev/fuchsia/tools/testing/runtests"
+ "golang.org/x/sync/errgroup"
"github.com/google/go-cmp/cmp"
)
@@ -59,15 +61,6 @@
return err
}
-type fakeSerialClient struct {
- runCalls int
-}
-
-func (c *fakeSerialClient) runDiagnostics(_ context.Context) error {
- c.runCalls++
- return nil
-}
-
type fakeCmdRunner struct {
runErrs []error
runCalls int
@@ -273,12 +266,40 @@
runErrs: c.runErrs,
}
copier := &fakeDataSinkCopier{}
- serialSocket := &fakeSerialClient{}
tester := fuchsiaSSHTester{
client: client,
copier: copier,
connectionErrorRetryBackoff: &retry.ZeroBackoff{},
- serialSocket: serialSocket,
+ }
+ eg := errgroup.Group{}
+ serialServer := fakeSerialServer{
+ received: make([]byte, 1024),
+ shutdownString: "shutdown",
+ socketPath: fmt.Sprintf("%d.sock", rand.Uint32()),
+ listeningChan: make(chan bool),
+ }
+ if c.wantConnErr {
+ serialDiagnosticCmds = []serialDiagnosticCmd{
+ {
+ cmd: []string{"foo"},
+ // Ensure we don't waste time sleeping in this test.
+ sleepDuration: time.Microsecond,
+ },
+ {
+ // This is a hack to ensure the shutdown command gets sent to the serial server.
+ // Rather than introduce a new synchronization mechanism, just use the code under test's
+ // existing mechanism for sending commands.
+ cmd: []string{serialServer.shutdownString},
+ // Ensure we don't waste time sleeping in this test.
+ sleepDuration: time.Microsecond,
+ },
+ }
+ tester.serialSocketPath = serialServer.socketPath
+ defer os.Remove(serialServer.socketPath)
+ eg.Go(serialServer.Serve)
+ if !<-serialServer.listeningChan {
+ t.Fatalf("fakeSerialServer.Serve() returned: %v", eg.Wait())
+ }
}
defer func() {
if err := tester.Close(); err != nil {
@@ -335,11 +356,20 @@
}
if c.wantConnErr {
- if serialSocket.runCalls != 1 {
- t.Errorf("called RunSerialDiagnostics() %d times", serialSocket.runCalls)
+ if err = eg.Wait(); err != nil {
+ t.Errorf("serialServer.Serve() failed: %v", err)
}
- } else if serialSocket.runCalls > 0 {
- t.Errorf("unexpectedly called RunSerialDiagnostics()")
+ // Verify that each command was seen in the received data, and in the
+ // proper order. Ignore the shutdown command we appended at the end
+ searchPos := 0
+ for _, cmd := range serialDiagnosticCmds[:len(serialDiagnosticCmds)-2] {
+ index := bytes.Index(serialServer.received[searchPos:], []byte(asSerialCmd(cmd.cmd)))
+ if index == -1 {
+ t.Errorf("SSHTester did find the command \"%v\" in the proper order, all received: %s", cmd, string(serialServer.received))
+ } else {
+ searchPos = index + 1
+ }
+ }
}
})
}
@@ -422,6 +452,44 @@
}
}
+func TestNewSerialSocket(t *testing.T) {
+ socketPath := fmt.Sprintf("%d.sock", rand.Uint32())
+ defer os.Remove(socketPath)
+ server := fakeSerialServer{
+ shutdownString: "dm shutdown",
+ socketPath: socketPath,
+ listeningChan: make(chan bool),
+ }
+ eg := errgroup.Group{}
+ eg.Go(server.Serve)
+
+ if !<-server.listeningChan {
+ t.Fatalf("fakeSerialServer.Serve() returned: %v", eg.Wait())
+ }
+
+ clientSocket, err := newSerialSocket(context.Background(), socketPath)
+ if err != nil {
+ t.Fatalf("newSerialSocket() failed: %v", err)
+ }
+ bytesWritten, err := clientSocket.Write([]byte(server.shutdownString))
+ if err != nil {
+ t.Errorf("clientSocket.Write() failed: %v", err)
+ }
+ if bytesWritten != len(server.shutdownString) {
+ t.Errorf("clientSocket.Write() wrote %d bytes, wanted %d", bytesWritten, len(server.shutdownString))
+ }
+ if err = eg.Wait(); err != nil {
+ t.Errorf("server returned: %v", err)
+ }
+ if err = clientSocket.Close(); err != nil {
+ t.Errorf("clientSocket.Close() returned: %v", err)
+ }
+ // First newline should be sent by newSerialSocket to trigger a cursor.
+ if diff := cmp.Diff("\r\n\r\n"+server.shutdownString, string(server.received)); diff != "" {
+ t.Errorf("Unexpected server.received (-want +got):\n%s", diff)
+ }
+}
+
// fakeContext conforms to context.Context but lets us control the return
// value of Err().
type fakeContext struct {