blob: 7dce6271d38e152028e275cdd3b94b3e4c5cdd4a [file] [log] [blame]
// Copyright 2020 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 fuzz
import (
"fmt"
"io"
"time"
"github.com/golang/glog"
)
// An Instance is a specific combination of build, connector, and launcher,
// representable by a handle. Most methods of this interface map directly to
// the ClusterFuchsia API.
type Instance interface {
Start() error
Stop() error
ListFuzzers() []string
Get(fuzzerName, targetSrc, hostDst string) error
Put(fuzzerName, hostSrc, targetDst string) error
RunFuzzer(out io.Writer, name, hostArtifactDir string, args ...string) error
GetLogs(out io.Writer) error
PrepareFuzzer(name string) error
Handle() (Handle, error)
Close()
}
// BaseInstance groups the core subobjects, to which most work will be delegated
type BaseInstance struct {
Build Build
Connector Connector
Launcher Launcher
// While we rewrite the handle data each time Handle() is called, we need to keep a
// reference around to avoid recreating new Handles each time
handle Handle
// This will take a default value, and only needs to be overridden for tests
reconnectInterval time.Duration
}
const defaultReconnectInterval = 3 * time.Second
const maxInitialConnectAttempts = 5
// NewInstance creates a fresh instance
func NewInstance() (Instance, error) {
build, err := NewBuild()
if err != nil {
return nil, fmt.Errorf("Error configuring build: %s", err)
}
if err := build.Prepare(); err != nil {
return nil, fmt.Errorf("Error preparing build: %s", err)
}
// TODO(fxbug.dev/47479): should the user be able to choose connector/launcher types?
launcher := NewQemuLauncher(build)
// Note: We can't get a Connector until the Launcher has started
return &BaseInstance{build, nil, launcher, "", defaultReconnectInterval}, nil
}
func loadInstanceFromHandle(handle Handle) (Instance, error) {
// TODO(fxbug.dev/47320): Store build info in the handle too
build, err := NewBuild()
if err != nil {
return nil, fmt.Errorf("Error configuring build: %s", err)
}
connector, err := loadConnectorFromHandle(handle)
if err != nil {
return nil, err
}
launcher, err := loadLauncherFromHandle(build, handle)
if err != nil {
return nil, err
}
return &BaseInstance{build, connector, launcher, handle, defaultReconnectInterval}, nil
}
// Close releases the Instance, but doesn't Stop it
func (i *BaseInstance) Close() {
i.Connector.Close()
}
// Start boots up the instance and waits for connectivity to be established
//
// If Start succeeds, it is up to the caller to clean up by calling Stop later.
// However, if it fails, any resources will have been automatically released.
func (i *BaseInstance) Start() error {
conn, err := i.Launcher.Start()
if err != nil {
return err
}
i.Connector = conn
glog.Infof("Waiting for connectivity...")
first := true
for j := 1; j <= maxInitialConnectAttempts; j++ {
if !first {
// Check if instance still appears to be running; there is no point
// in attempting to connect if it is down.
running, err := i.Launcher.IsRunning()
if err != nil {
i.Launcher.Kill()
return fmt.Errorf("Error checking instance status: %s", err)
}
if !running {
i.Launcher.Kill()
if qemuLauncher, ok := i.Launcher.(*QemuLauncher); ok {
return fmt.Errorf("Instance (PID %d) not running", qemuLauncher.Pid)
}
return fmt.Errorf("Instance not running")
}
glog.Warningf("Retrying in %s...", i.reconnectInterval)
time.Sleep(i.reconnectInterval)
}
first = false
cmd := conn.Command("echo", "hello")
cmd.SetTimeout(5 * time.Second)
// Start() will return an error if we have trouble connecting to the server.
// Note that the underlying connector may have its own additional retry logic.
if err := cmd.Start(); err != nil {
glog.Warningf("Connection failed during attempt %d: %s", j, err)
continue
}
// Wait() will return an error if we have trouble executing the remote
// command. Some example cases would be:
// - Timeout running the command
// - If you forgot to --with-base the devtools:
// error: 2 (/boot/bin/sh: 1: Cannot create child process: -1 (ZX_ERR_INTERNAL):
// failed to resolve fuchsia-pkg://fuchsia.com/ls#bin/ls
if err := cmd.Wait(); err != nil {
glog.Warningf("Wait failed during attempt %d: %s", j, err)
continue
}
glog.Info("Instance is now online.")
if sc, ok := conn.(*SSHConnector); ok {
glog.Infof("Access via: ssh -i %q -p %d %s", sc.Key, sc.Port, sc.Host)
}
return nil
}
i.Launcher.Kill()
return fmt.Errorf("error establishing connectivity to instance")
}
// RunFuzzer runs the named fuzzer on the Instance. If `hostArtifactDir` is
// specified and the run generated any output artifacts, they will be copied to
// that directory. `args` is an optional list of arguments in the form
// `-key=value` that will be passed to libFuzzer.
func (i *BaseInstance) RunFuzzer(out io.Writer, name, hostArtifactDir string, args ...string) error {
fuzzer, err := i.Build.Fuzzer(name)
if err != nil {
return err
}
fuzzer.Parse(args)
artifacts, err := fuzzer.Run(i.Connector, out, hostArtifactDir)
if err != nil {
return err
}
if hostArtifactDir != "" {
for _, art := range artifacts {
if err := i.Get(name, art, hostArtifactDir); err != nil {
return err
}
}
}
return nil
}
// PrepareFuzzer ensures the named fuzzer is ready to be used on the Instance.
// This must be called before running the fuzzer or exchanging any data with
// the fuzzer. If called for a fuzzer that has already been previously
// prepared, it will reset the state of that fuzzer: clearing data directories,
// etc.
//
// This method is explicitly separated from RunFuzzer and others to ensure that
// any caller timeouts being enforced on fuzzer command execution are not
// affected by unrelated setup delays such as package fetching over the network.
func (i *BaseInstance) PrepareFuzzer(name string) error {
fuzzer, err := i.Build.Fuzzer(name)
if err != nil {
return err
}
if err := fuzzer.Prepare(i.Connector); err != nil {
return err
}
return nil
}
// GetLogs writes any system logs for an instance to `out`
func (i *BaseInstance) GetLogs(out io.Writer) error {
return i.Launcher.GetLogs(out)
}
// Get copies files from a fuzzer namespace on the Instance to the host
func (i *BaseInstance) Get(fuzzerName, targetSrc, hostDst string) error {
fuzzer, err := i.Build.Fuzzer(fuzzerName)
if err != nil {
return err
}
return i.Connector.Get(fuzzer.AbsPath(targetSrc), hostDst)
}
// Put copies files from the host to a fuzzer namespace on the Instance
func (i *BaseInstance) Put(fuzzerName, hostSrc, targetDst string) error {
fuzzer, err := i.Build.Fuzzer(fuzzerName)
if err != nil {
return err
}
return i.Connector.Put(hostSrc, fuzzer.AbsPath(targetDst))
}
// Stop shuts down the Instance
func (i *BaseInstance) Stop() error {
if err := i.Launcher.Kill(); err != nil {
return err
}
if i.handle != "" {
i.handle.Release()
}
return nil
}
// Handle returns a Handle representing the Instance
func (i *BaseInstance) Handle() (Handle, error) {
data := HandleData{i.Connector, i.Launcher}
if i.handle == "" {
// Create a new handle from scratch
handle, err := NewHandleWithData(data)
if err != nil {
return "", fmt.Errorf("error constructing instance handle: %s", err)
}
i.handle = handle
return handle, nil
}
// Update an existing handle, but don't release it even if we fail
if err := i.handle.SetData(data); err != nil {
return "", fmt.Errorf("error updating instance handle: %s", err)
}
return i.handle, nil
}
// ListFuzzers lists fuzzers available on the Instance
func (i *BaseInstance) ListFuzzers() []string {
return i.Build.ListFuzzers()
}