blob: 4fa143b5ae0c5180b73f1c25c0709e5b4c62aa1c [file] [log] [blame]
// 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 ffxutil provides support for running ffx commands.
package ffxutil
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"time"
"go.fuchsia.dev/fuchsia/tools/build"
"go.fuchsia.dev/fuchsia/tools/lib/ffxutil/constants"
"go.fuchsia.dev/fuchsia/tools/lib/jsonutil"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"go.fuchsia.dev/fuchsia/tools/lib/subprocess"
)
const (
// The name of the snapshot zip file that gets outputted by `ffx target snapshot`.
// Keep in sync with //src/developer/ffx/plugins/target/snapshot/src/lib.rs.
snapshotZipName = "snapshot.zip"
// The JSON Pointer (https://tools.ietf.org/html/rfc6901) to log level in the config.
logLevelJsonPointer = "/log/level"
)
type LogLevel string
const (
Off LogLevel = "Off"
Error LogLevel = "Error"
Warn LogLevel = "Warn"
Info LogLevel = "Info"
Debug LogLevel = "Debug"
Trace LogLevel = "Trace"
)
func runCommand(
ctx context.Context,
runner *subprocess.Runner,
stdout, stderr io.Writer,
args ...string,
) error {
return runner.RunWithStdin(ctx, args, stdout, stderr, nil)
}
// FFXInstance takes in a path to the ffx tool and runs ffx commands with the provided config.
type FFXInstance struct {
ffxPath string
// Config represents the config associated with this ffx instance.
Config *FFXConfig
// ConfigPath is the path to the ffx config.
ConfigPath string
runner *subprocess.Runner
stdout io.Writer
stderr io.Writer
target string
}
// NewFFXInstance creates an isolated FFXInstance.
func NewFFXInstance(
ffxPath string,
dir string,
env []string,
target, sshKey string,
outputDir string,
) (*FFXInstance, error) {
if ffxPath == "" {
return nil, nil
}
config := newIsolatedFFXConfig(outputDir)
config.SetJsonPointer("/target/default", target)
sshKey, err := filepath.Abs(sshKey)
if err != nil {
config.Close()
return nil, err
}
config.SetJsonPointer("/ssh/priv", []string{sshKey})
config.Set("test", map[string]bool{
"experimental_structured_output": true,
"experimental_json_input": true,
})
configPath := filepath.Join(outputDir, "ffx_config.json")
if err := config.ToFile(configPath); err != nil {
config.Close()
return nil, err
}
env = append(env, config.Env()...)
ffx := FFXInstanceWithConfig(ffxPath, dir, env, target, configPath)
if ffx != nil {
ffx.Config = config
}
return ffx, nil
}
func FFXInstanceWithConfig(
ffxPath, dir string,
env []string,
target, configPath string,
) *FFXInstance {
if ffxPath == "" {
return nil
}
return &FFXInstance{
ffxPath: ffxPath,
ConfigPath: configPath,
runner: &subprocess.Runner{Dir: dir, Env: append(os.Environ(), env...)},
stdout: os.Stdout,
stderr: os.Stderr,
target: target,
}
}
func (f *FFXInstance) SetTarget(target string) {
f.target = target
}
// SetStdoutStderr sets the stdout and stderr for the ffx commands to write to.
func (f *FFXInstance) SetStdoutStderr(stdout, stderr io.Writer) {
f.stdout = stdout
f.stderr = stderr
}
// SetLogLevel sets the log-level in the ffx instance's associated config.
func (f *FFXInstance) SetLogLevel(level LogLevel) error {
return f.SetConfigJsonPointer(logLevelJsonPointer, string(level))
}
// SetConfigJsonPointer sets a field in the ffx instance's associated config and
// rewrites the config file.
func (f *FFXInstance) SetConfigJsonPointer(pointer string, value interface{}) error {
if f.Config == nil {
config, err := configFromFile(f.ConfigPath)
if err != nil {
return err
}
f.Config = config
}
f.Config.SetJsonPointer(pointer, value)
return f.Config.ToFile(f.ConfigPath)
}
// Run runs ffx with the associated config and provided args.
func (f *FFXInstance) Run(ctx context.Context, args ...string) error {
args = append([]string{f.ffxPath, "--config", f.ConfigPath}, args...)
if err := runCommand(ctx, f.runner, f.stdout, f.stderr, args...); err != nil {
return fmt.Errorf("%s: %w", constants.CommandFailedMsg, err)
}
return nil
}
// RunWithTarget runs ffx with the associated target.
func (f *FFXInstance) RunWithTarget(ctx context.Context, args ...string) error {
args = append([]string{"--target", f.target}, args...)
return f.Run(ctx, args...)
}
// Stop stops the daemon.
func (f *FFXInstance) Stop() error {
// Use a new context for Stop() to give it time to complete.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := f.Run(ctx, "daemon", "stop")
if f.Config != nil {
if configErr := f.Config.Close(); err == nil {
err = configErr
}
}
// Wait to see that the daemon is stopped before exiting this function
// because the ffx command returns after it has initiated the shutdown,
// but there may be some delay where the daemon can continue to output
// logs before it's completely shut down.
// TODO(fxbug.dev/92296): Remove this workaround when ffx can ensure that
// no more logs are written once the command returns.
for i := 0; i < 3; i++ {
var b bytes.Buffer
if err := runCommand(ctx, f.runner, &b, io.Discard, "pgrep", "ffx"); err != nil {
logger.Debugf(ctx, "failed to run \"pgrep ffx\": %s", err)
continue
}
if len(b.Bytes()) == 0 {
break
}
logger.Debugf(ctx, "ffx daemon hasn't completed shutdown. Checking again after 1 second")
time.Sleep(time.Second)
}
return err
}
// BootloaderBoot RAM boots the target.
func (f *FFXInstance) BootloaderBoot(ctx context.Context, zbi, vbmeta, slot string) error {
var args []string
if zbi != "" {
args = append(args, "--zbi", zbi)
}
if vbmeta != "" {
args = append(args, "--vbmeta", vbmeta)
}
if slot != "" {
args = append(args, "--slot", slot)
}
return f.RunWithTarget(ctx, append([]string{"target", "bootloader", "boot"}, args...)...)
}
// Flash flashes the target.
func (f *FFXInstance) Flash(ctx context.Context, manifest, sshKey string) error {
return f.RunWithTarget(ctx, "target", "flash", "--authorized-keys", sshKey, manifest)
}
// List lists all available targets.
func (f *FFXInstance) List(ctx context.Context, args ...string) error {
return f.Run(ctx, append([]string{"target", "list"}, args...)...)
}
// TargetWait waits until the target becomes available.
func (f *FFXInstance) TargetWait(ctx context.Context) error {
return f.RunWithTarget(ctx, "target", "wait")
}
// Test runs a test suite.
func (f *FFXInstance) Test(
ctx context.Context,
testList build.TestList,
outDir string,
args ...string,
) (*TestRunResult, error) {
// Write the test def to a file and store in the outDir to upload with the test outputs.
if err := os.MkdirAll(outDir, os.ModePerm); err != nil {
return nil, err
}
testFile := filepath.Join(outDir, "test-list.json")
if err := jsonutil.WriteToFile(testFile, testList); err != nil {
return nil, err
}
// Create a new subdirectory within outDir to pass to --output-directory which is expected to be
// empty.
testOutputDir := filepath.Join(outDir, "test-outputs")
f.RunWithTarget(
ctx,
append(
[]string{
"test",
"run",
"--continue-on-timeout",
"--test-file",
testFile,
"--output-directory",
testOutputDir,
},
args...)...)
return GetRunResult(testOutputDir)
}
// Snapshot takes a snapshot of the target's state and saves it to outDir/snapshotFilename.
func (f *FFXInstance) Snapshot(ctx context.Context, outDir string, snapshotFilename string) error {
err := f.RunWithTarget(ctx, "target", "snapshot", "--dir", outDir)
if err != nil {
return err
}
if snapshotFilename != "" && snapshotFilename != snapshotZipName {
return os.Rename(
filepath.Join(outDir, snapshotZipName),
filepath.Join(outDir, snapshotFilename),
)
}
return nil
}
// GetConfig shows the ffx config.
func (f *FFXInstance) GetConfig(ctx context.Context) error {
return f.Run(ctx, "config", "get")
}