blob: 7f9bdc3a4024549828372c06416a553dca6405f2 [file] [edit]
// 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 device
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/artifacts"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/build"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/ffx"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/packages"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/paver"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/sl4f"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"go.fuchsia.dev/fuchsia/tools/lib/retry"
"go.fuchsia.dev/fuchsia/tools/net/sshutil"
"golang.org/x/crypto/ssh"
)
const rebootCheckPath = "/tmp/ota_test_should_reboot"
// Client manages the connection to the device.
type Client struct {
resolverMode string
nodeName string
host string
sshPort int
ffx *ffx.FFXTool
sshClient *sshutil.Client
initialMonotonicTime time.Time
workaroundBrokenTimeSkip bool
bootCounter *uint32
repoPort int
flashRetrySleep time.Duration
}
// AddrResolver wrapper for sshutil
type addrResolver struct {
client *Client
}
func (a *addrResolver) Resolve(ctx context.Context) (net.Addr, error) {
resolver, err := a.client.getResolver(ctx)
if err != nil {
return nil, err
}
addr, err := resolver.ResolveSshAddress(ctx)
if err != nil {
return nil, err
}
return net.ResolveTCPAddr("tcp", addr)
}
// NewClient creates a new Client.
func NewClient(
ctx context.Context,
repoPort int,
resolverMode string,
nodeName string,
host string,
sshPort int,
privateKey ssh.Signer,
sshConnectBackoff retry.Backoff,
workaroundBrokenTimeSkip bool,
serialConn *SerialConn,
ffxTool *ffx.FFXTool,
) (*Client, error) {
sshConfig, err := newSSHConfig(privateKey)
if err != nil {
return nil, err
}
c := &Client{
resolverMode: resolverMode,
nodeName: nodeName,
host: host,
sshPort: sshPort,
ffx: ffxTool,
workaroundBrokenTimeSkip: workaroundBrokenTimeSkip,
repoPort: repoPort,
flashRetrySleep: 12 * time.Second,
}
sshClient, err := sshutil.NewClient(
ctx,
&addrResolver{
client: c,
},
sshConfig,
sshConnectBackoff,
)
if err != nil {
return nil, err
}
c.sshClient = sshClient
bootCounter := new(uint32)
if serialConn != nil {
go func() {
for {
line, err := serialConn.ReadLine()
if err != nil {
logger.Errorf(ctx, "failed to read from serial: %v", err)
break
}
if strings.HasSuffix(line, "Welcome to Zircon\n") {
atomic.AddUint32(bootCounter, 1)
}
}
}()
}
c.bootCounter = bootCounter
if err := c.postConnectSetup(ctx, ffxTool); err != nil {
c.Close()
return nil, err
}
return c, nil
}
// SetFFXTool sets the active ffx tool used by the client and its dynamic resolver.
func (c *Client) SetFFXTool(t *ffx.FFXTool) {
c.ffx = t
}
// getResolver creates a short-lived resolver based on the current client state.
func (c *Client) getResolver(ctx context.Context) (DeviceResolver, error) {
switch c.resolverMode {
case "constant":
return NewConstantHostResolver(ctx, c.nodeName, c.host, c.sshPort), nil
case "ffx", "mdns": // Both use ffx. ffx is preferred over a raw mDNS resolver
// because it uses mDNS but is more general (e.g. supports USB).
return NewFfxResolver(ctx, c.ffx, c.nodeName, c.host)
default:
return nil, fmt.Errorf("invalid resolver mode: %s", c.resolverMode)
}
}
// Name returns the node name of the device.
func (c *Client) Name() string {
return c.nodeName
}
// Construct a new `ssh.ClientConfig` for a given key file, or return an error if
// the key is invalid.
func newSSHConfig(privateKey ssh.Signer) (*ssh.ClientConfig, error) {
config := &ssh.ClientConfig{
User: "fuchsia",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(privateKey),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 30 * time.Second,
}
return config, nil
}
// Close the Client connection
func (c *Client) Close() {
c.sshClient.Close()
}
// Run all setup steps after we've connected to a device.
func (c *Client) postConnectSetup(
ctx context.Context,
ffxTool *ffx.FFXTool,
) error {
// TODO(https://fxbug.dev/42154680): The device might drop connections
// early after boot when the RTC is updated, which typically happens
// about 10 seconds after boot. To avoid this, if we find that we
// connected before 15s, we'll disconnect, sleep, then connect again.
if c.workaroundBrokenTimeSkip {
logger.Infof(ctx, "Sleeping 15s in case https://fxbug.dev/42154685 causes a spurious disconnection")
time.Sleep(15 * time.Second)
if err := c.sshClient.Reconnect(ctx); err != nil {
return err
}
}
c.setInitialMonotonicTime(ctx, ffxTool)
return nil
}
func (c *Client) Reconnect(ctx context.Context, ffxTool *ffx.FFXTool) error {
if err := c.sshClient.Reconnect(ctx); err != nil {
return err
}
return c.postConnectSetup(ctx, ffxTool)
}
func (c *Client) getTargetSpecifier(ctx context.Context, ffxTool *ffx.FFXTool) (string, error) {
if ffxTool == nil {
return "", fmt.Errorf("ffx is nil")
}
var target string
nodeName := c.Name()
// Prioritize finding an address or valid specifier using ffxTool.
// This handles devices known only to ffx (like Pontis or user emulators).
if nodeName != "" {
if resolver, err := NewFfxResolver(ctx, ffxTool, nodeName, ""); err == nil {
if addr, err := resolver.ResolveSshAddress(ctx); err == nil {
target = addr
logger.Infof(ctx, "getTargetSpecifier: resolved target %q using FfxResolver", target)
}
}
}
// If that fails, fall back to standard resolver.
if target == "" {
if resolver, err := c.getResolver(ctx); err == nil {
if addr, err := resolver.ResolveSshAddress(ctx); err == nil {
target = addr
logger.Infof(ctx, "getTargetSpecifier: resolved target %q using standard resolver", target)
}
}
}
// Fall back to node name if still empty.
if target == "" {
target = nodeName
logger.Infof(ctx, "getTargetSpecifier: resolved target %q using node name", target)
}
if target == "" {
target = ffxTool.GetTarget()
logger.Infof(ctx, "getTargetSpecifier: resolved target %q using ffxTool default target", target)
}
// Clean up IPv6 brackets if needed.
if target != "" {
if host, _, err := net.SplitHostPort(target); err == nil {
target = host
}
if strings.Contains(target, ":") && !strings.HasPrefix(target, "[") {
target = fmt.Sprintf("[%s]", target)
}
}
// Verify it is a valid target specifier for strict mode!
if target != "" {
// Strip brackets and scope ID to validate if the target is an IP address.
ipStr := strings.Trim(target, "[]")
if idx := strings.Index(ipStr, "%"); idx != -1 {
ipStr = ipStr[:idx]
}
if net.ParseIP(ipStr) == nil &&
!strings.HasPrefix(target, "usb:") && !strings.HasPrefix(target, "vsock:") && !strings.HasPrefix(target, "id:") {
return "", fmt.Errorf("cannot resolve a valid target specifier for strict mode (got node name %q). Ffx strict mode requires an IP address or a valid prefix (\"id:<serial-number>\", \"usb:cid\", \"vsock:cid\").", target)
}
} else {
return "", fmt.Errorf("cannot resolve a target specifier. Ffx strict mode requires an IP address or a valid prefix (\"id:<serial-number>\", \"usb:cid\", \"vsock:cid\").")
}
return target, nil
}
func (c *Client) setInitialMonotonicTime(
ctx context.Context,
ffxTool *ffx.FFXTool,
) {
targetSpecifier, err := c.getTargetSpecifier(ctx, ffxTool)
if err != nil {
logger.Warningf(ctx, "failed to get target specifier for setInitialMonotonicTime: %v", err)
return
}
args := []string{"--target", targetSpecifier}
args = append(args, "--machine", "raw", "target", "get-time")
t0 := time.Now()
stdout, err := ffxTool.RunAndGetOutput(ctx, args...)
t1 := time.Now()
if err == nil {
stdoutStr := strings.TrimSpace(stdout)
t, errParse := strconv.ParseInt(stdoutStr, 10, 64)
if errParse == nil {
monotonicTime := time.Duration(t) * time.Nanosecond
latency := t1.Sub(t0) / 2
c.initialMonotonicTime = t1.Add(-(monotonicTime + latency))
return
}
}
logger.Warningf(ctx, "failed to get time with ffx: %v", err)
logger.Warningf(ctx, "resetting time to zero")
c.initialMonotonicTime = time.Time{}
}
func (c *Client) getEstimatedMonotonicTime() time.Duration {
if c.initialMonotonicTime.IsZero() {
return 0
}
return time.Since(c.initialMonotonicTime)
}
// Run a command to completion on the remote device and write STDOUT and STDERR
// to the passed in io.Writers.
func (c *Client) Run(ctx context.Context, command []string, stdout io.Writer, stderr io.Writer) error {
return c.sshClient.Run(ctx, command, stdout, stderr)
}
// DisconnectionListener returns a channel that is closed when the client is
// disconnected.
func (c *Client) DisconnectionListener() <-chan struct{} {
return c.sshClient.DisconnectionListener()
}
func (c *Client) GetSSHConnection(ctx context.Context) (string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := []string{"PATH=''", "echo", "$SSH_CONNECTION"}
if err := c.Run(ctx, cmd, &stdout, &stderr); err != nil {
return "", fmt.Errorf("failed to read SSH_CONNECTION: %w: %s", err, string(stderr.Bytes()))
}
return strings.Split(string(stdout.Bytes()), " ")[0], nil
}
func (c *Client) GetSystemImageMerkle(ctx context.Context) (build.MerkleRoot, error) {
const systemImageMeta = "/system/meta"
merkleBytes, err := c.ReadRemotePath(ctx, systemImageMeta)
if err != nil {
return build.MerkleRoot{}, err
}
return build.DecodeMerkleRoot([]byte(strings.TrimSpace(string(merkleBytes))))
}
// Reboot asks the device to reboot. It waits until the device reconnects
// before returning.
func (c *Client) Reboot(ctx context.Context, ffxTool *ffx.FFXTool) error {
logger.Infof(ctx, "rebooting")
return c.ExpectReboot(ctx, ffxTool, func() error {
// Run the reboot in the background, which gives us a chance to
// observe us successfully executing the reboot command.
return c.RunReboot(ctx)
})
}
// RunReboot runs the reboot command
func (c *Client) RunReboot(ctx context.Context) error {
cmd := []string{"dm", "reboot", "&", "exit", "0"}
if err := c.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
// If the device rebooted before ssh was able to tell
// us the command ran, it will tell us the session
// exited without passing along an exit code. So,
// ignore that specific error.
var exitErr *ssh.ExitMissingError
if errors.As(err, &exitErr) {
logger.Infof(ctx, "ssh disconnected before returning a status")
} else {
return fmt.Errorf("failed to reboot: %w", err)
}
}
return nil
}
// RebootToBootloader asks the device to reboot into the bootloader. It
// waits until the device disconnects before returning.
func (c *Client) RebootToBootloader(ctx context.Context, ffxTool *ffx.FFXTool) error {
logger.Infof(ctx, "Rebooting to bootloader")
targetSpecifier, err := c.getTargetSpecifier(ctx, ffxTool)
if err != nil {
return err
}
return c.ExpectDisconnect(ctx, func() error {
return ffxTool.RebootToBootloader(ctx, targetSpecifier)
})
}
// RebootToRecovery asks the device to reboot into the recovery partition. It
// waits until the device disconnects before returning.
func (c *Client) RebootToRecovery(ctx context.Context) error {
logger.Infof(ctx, "Rebooting to recovery")
return c.ExpectDisconnect(ctx, func() error {
// Run the reboot in the background, which gives us a chance to
// observe us successfully executing the reboot command.
cmd := []string{"dm", "reboot-recovery", "&", "exit", "0"}
if err := c.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
// If the device rebooted before ssh was able to tell
// us the command ran, it will tell us the session
// exited without passing along an exit code. So,
// ignore that specific error.
var exitErr *ssh.ExitMissingError
if errors.As(err, &exitErr) {
logger.Infof(ctx, "ssh disconnected before returning a status")
} else {
return fmt.Errorf("failed to reboot into recovery: %w", err)
}
}
return nil
})
}
// Suspend asks the device to suspend. It waits until the device disconnects
// before returning.
func (c *Client) Suspend(ctx context.Context) error {
logger.Infof(ctx, "Suspending")
return c.ExpectDisconnect(ctx, func() error {
// Run the suspend in the background, which gives us a chance to
// observe us successfully executing the suspend command.
cmd := []string{"dm", "suspend", "&", "exit", "0"}
if err := c.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
// If the device suspends before ssh was able to tell
// us the command ran, it will tell us the session
// exited without passing along an exit code. So,
// ignore that specific error.
var exitErr *ssh.ExitMissingError
if errors.As(err, &exitErr) {
logger.Infof(ctx, "ssh disconnected before returning a status")
} else {
return fmt.Errorf("failed to suspend: %w", err)
}
}
return nil
})
}
func (c *Client) ExpectDisconnect(ctx context.Context, f func() error) error {
ch := c.DisconnectionListener()
if err := f(); err != nil {
return err
}
// Wait until we get a signal that we have disconnected
select {
case <-ch:
case <-ctx.Done():
return fmt.Errorf("device did not disconnect: %w", ctx.Err())
}
logger.Infof(ctx, "device disconnected")
return nil
}
// ExpectReboot prepares a device for a reboot, runs a closure `f` that should
// reboot the device, then finally verifies whether a reboot actually took
// place. It does this by writing a unique value to
// `/tmp/ota_test_should_reboot`, then executing the closure. After we
// reconnect, we check if `/tmp/ota_test_should_reboot` exists. If not, exit
// with `nil`. Otherwise, we failed to reboot, or some competing test is also
// trying to reboot the device. Either way, err out.
func (c *Client) ExpectReboot(
ctx context.Context,
ffxTool *ffx.FFXTool,
f func() error,
) error {
// Generate a unique value.
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return fmt.Errorf("failed to generate a unique boot number: %w", err)
}
// Encode the id into hex so we can write it through the shell.
bootID := hex.EncodeToString(b)
// Write the value to the file. Err if the file already exists by setting the
// noclobber setting.
cmd := fmt.Sprintf(
`(
set -C &&
PATH= echo "%s" > "%s"
)`, bootID, rebootCheckPath)
// Delete the stale reboot check file if it exists.
exists, err := c.RemoteFileExists(ctx, rebootCheckPath)
if err == nil && exists {
if err := c.DeleteRemotePath(ctx, rebootCheckPath); err != nil {
logger.Warningf(ctx, "failed to delete stale reboot check file %q: %v", rebootCheckPath, err)
}
}
if err := c.Run(ctx, strings.Fields(cmd), os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("failed to write reboot check file: %w", err)
}
// As a sanity check, make sure the file actually exists and has the correct
// value.
b, err = c.ReadRemotePath(ctx, rebootCheckPath)
if err != nil {
return fmt.Errorf("failed to read reboot check file: %w", err)
}
actual := strings.TrimSpace(string(b))
if actual != bootID {
return fmt.Errorf("reboot check file has wrong value: expected %q, got %q", bootID, actual)
}
// Look up the boot count before we reboot the device.
initialBootCount := *c.bootCounter
ch := c.DisconnectionListener()
if err := f(); err != nil {
return err
}
// Wait until we get a signal that we have disconnected
select {
case <-ch:
case <-ctx.Done():
return fmt.Errorf("device did not disconnect: %w", ctx.Err())
}
logger.Infof(ctx, "device disconnected, waiting for device to boot")
if err := c.Reconnect(ctx, ffxTool); err != nil {
return fmt.Errorf("failed to reconnect: %w", err)
}
// We've reconnected to the device, so count how many times we've rebooted.
afterBootCount := *c.bootCounter
// If we have boot counting enabled (signified by the initial boot
// count not being zero), then check how many times we rebooted. It
// should be 1 more than our initial count. If not, error out.
logger.Infof(ctx, "device appears to have rebooted %d times", afterBootCount-initialBootCount)
if initialBootCount != 0 && initialBootCount+1 != afterBootCount {
return fmt.Errorf("device appears to have rebooted more than once! %d != %d", initialBootCount, afterBootCount)
}
// We reconnected to the device. Check that the reboot check file doesn't exist.
exists, err = c.RemoteFileExists(ctx, rebootCheckPath)
if err != nil {
return fmt.Errorf(`failed to check if %q exists: %w`, rebootCheckPath, err)
}
if exists {
// The reboot file exists. This could have happened because either we
// didn't reboot, or some other test is also trying to reboot the
// device. We can distinguish the two by comparing the file contents
// with the bootID we wrote earlier.
b, err = c.ReadRemotePath(ctx, rebootCheckPath)
if err != nil {
return fmt.Errorf("failed to read reboot check file: %w", err)
}
actual := strings.TrimSpace(string(b))
// If the contents match, then we failed to reboot.
if actual == bootID {
return fmt.Errorf("reboot check file exists after reboot, device did not reboot")
}
return fmt.Errorf(
"reboot check file exists after reboot, and has unexpected value: expected %q, got %q",
bootID,
actual,
)
}
return nil
}
// ValidateStaticPackages checks that all static packages have no missing blobs.
func (c *Client) ValidateStaticPackages(ctx context.Context) error {
logger.Infof(ctx, "validating static packages")
path := "/pkgfs/ctl/validation/missing"
f, err := c.ReadRemotePath(ctx, path)
if err != nil {
return fmt.Errorf("error reading %q: %w", path, err)
}
merkles := strings.TrimSpace(string(f))
if merkles != "" {
return fmt.Errorf("static packages are missing the following blobs:\n%s", merkles)
}
logger.Infof(ctx, "all static package blobs are accounted for")
return nil
}
// ReadRemotePath read a file off the remote device.
func (c *Client) ReadRemotePath(ctx context.Context, path string) ([]byte, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := fmt.Sprintf(
`(
test -e "%s" &&
while IFS='' read f; do
echo "$f";
done < "%s" &&
if [ ! -z "$f" ];
then echo "$f";
fi
)`, path, path)
if err := c.Run(ctx, strings.Fields(cmd), &stdout, &stderr); err != nil {
return nil, fmt.Errorf("failed to read %q: %w: %s", path, err, string(stderr.Bytes()))
}
return stdout.Bytes(), nil
}
// DeleteRemotePath deletes a file off the remote device.
func (c *Client) DeleteRemotePath(ctx context.Context, path string) error {
var stderr bytes.Buffer
cmd := []string{"PATH=''", "rm", path}
if err := c.Run(ctx, cmd, os.Stdout, &stderr); err != nil {
return fmt.Errorf("failed to delete %q: %w: %s", path, err, string(stderr.Bytes()))
}
return nil
}
// RemoteFileExists checks if a file exists on the remote device.
func (c *Client) RemoteFileExists(ctx context.Context, path string) (bool, error) {
var stderr bytes.Buffer
cmd := []string{"PATH=''", "test", "-e", path}
if err := c.Run(ctx, cmd, io.Discard, &stderr); err != nil {
if e, ok := err.(*ssh.ExitError); ok {
if e.ExitStatus() == 1 {
return false, nil
}
}
return false, fmt.Errorf("error reading %q: %w: %s", path, err, string(stderr.Bytes()))
}
return true, nil
}
// RegisterPackageRepository adds the repository as a repository via ffxTool.
// If rewritePackages is not nil, the rewrite rule will only affect the passed packages.
func (c *Client) RegisterPackageRepository(
ctx context.Context,
ffxTool *ffx.FFXTool,
repo *packages.Server,
repoName string,
createRewriteRule bool,
rewritePackages []string,
sshAddr string,
) error {
logger.Infof(ctx, "registering package repository: %s", repo.Dir)
// From ffx's viewpoint, this repo is served on localhost, not via the qemu link local
// scope in the json config file for the target. Hence, we need to rewrite the URL
// to let ffx fetch the config from [::1].
url, err := url.Parse(repo.URL)
if err != nil {
return err
}
ffx_repo_url := fmt.Sprintf("http://[::1]:%v%v", url.Port(), url.Path)
if createRewriteRule {
target := sshAddr
if target == "" {
var err error
target, err = c.getTargetSpecifier(ctx, ffxTool)
if err != nil {
logger.Warningf(ctx, "failed to get target specifier for RegisterPackageRepository: %v", err)
target = c.nodeName
}
}
if err := ffxTool.RegisterPackageRepository(ctx, target, ffx_repo_url); err != nil {
logger.Errorf(ctx,
"%v %v\n%v",
"unable to register package repository via ffx:",
err,
"registering via ffx might be unsupported on the target, falling back to pkgctl.")
cmd := []string{"pkgctl", "repo", "add", "url", "-n", repoName, repo.URL}
if err := c.Run(ctx, cmd, os.Stdout, os.Stderr); err != nil {
return err
}
}
logger.Infof(ctx, "establishing rewriting rule for: %s", repo.URL)
ruleTemplate := `'{"version":"1","content":[
{"host_match":"fuchsia.com","host_replacement":"%[1]v","path_prefix_match":"/","path_prefix_replacement":"/"},
{"host_match":"chromium.org","host_replacement":"%[1]v","path_prefix_match":"/","path_prefix_replacement":"/"}
]}'`
if rewritePackages != nil {
ruleTemplate = `'{"version":"1","content":[`
for i, p := range rewritePackages {
if i > 0 {
ruleTemplate += ","
}
ruleTemplate += `{
"host_match":"fuchsia.com",
"host_replacement":"%[1]v",
"path_prefix_match":"/` + p + `",
"path_prefix_replacement":"/` + p + `"
}, {
"host_match":"fuchsia.com",
"host_replacement":"%[1]v",
"path_prefix_match":"/` + p + `/0",
"path_prefix_replacement":"/` + p + `/0"
}`
}
ruleTemplate += `]}'`
}
cmd := []string{"pkgctl", "rule", "replace", "json", fmt.Sprintf(ruleTemplate, repoName)}
return c.Run(ctx, cmd, os.Stdout, os.Stderr)
} else {
if err := ffxTool.RegisterPackageRepository(ctx, c.nodeName, ffx_repo_url); err != nil {
logger.Errorf(ctx,
"%v %v\n%v",
"unable to register package repository via ffx:",
err,
"registering via ffx might be unsupported on the target, falling back to pkgctl.")
cmd := []string{"pkgctl", "repo", "add", "url", repo.URL}
return c.Run(ctx, cmd, os.Stdout, os.Stderr)
} else {
return nil
}
}
}
func (c *Client) ServePackageRepository(
ctx context.Context,
repo *packages.Repository,
repoName string,
) (*packages.Server, error) {
// Make sure the device doesn't have any broken static packages.
if err := c.ValidateStaticPackages(ctx); err != nil {
return nil, err
}
// Tell the device to connect to our repository.
localHostname, err := c.GetSSHConnection(ctx)
if err != nil {
return nil, err
}
// Serve the repository before the test begins.
server, err := repo.Serve(ctx, localHostname, repoName, c.repoPort)
if err != nil {
return nil, err
}
return server, nil
}
func (c *Client) StartRpcSession(ctx context.Context, ffxTool *ffx.FFXTool, repo *packages.Repository) (*sl4f.Client, error) {
logger.Infof(ctx, "connecting to sl4f")
startTime := time.Now()
// Configure the target to use this repository as "fuchsia-pkg://host_target_testing_sl4f".
repoName := "host-target-testing-sl4f"
repoServer, err := c.ServePackageRepository(ctx, repo, repoName)
if err != nil {
return nil, fmt.Errorf("error serving repo to device: %w", err)
}
defer repoServer.Shutdown(ctx)
resolver, err := c.getResolver(ctx)
if err != nil {
return nil, fmt.Errorf("error getting resolver: %w", err)
}
sshAddr, err := resolver.ResolveSshAddress(ctx)
if err != nil {
return nil, fmt.Errorf("error resolving device host: %w", err)
}
if err := c.RegisterPackageRepository(ctx, ffxTool, repoServer, repoName, true, []string{"sl4f", "start_sl4f"}, sshAddr); err != nil {
return nil, fmt.Errorf("error registering repository with target: %w", err)
}
deviceHostname, _, err := net.SplitHostPort(sshAddr)
if err != nil {
return nil, fmt.Errorf("error parsing ssh address %v: %w", sshAddr, err)
}
rpcClient, err := sl4f.NewClient(ctx, c.sshClient, net.JoinHostPort(deviceHostname, "80"), "fuchsia.com")
if err != nil {
return nil, fmt.Errorf("error creating sl4f client: %w", err)
}
logger.Infof(ctx, "connected to sl4f in %s", time.Now().Sub(startTime))
return rpcClient, nil
}
// Pave paves the device to the specified build. It assumes the device is
// already in recovery, since there are multiple ways to get a device into
// recovery. Does not reconnect to the device.
func (c *Client) Pave(
ctx context.Context,
build artifacts.Build,
sshPublicKey ssh.PublicKey,
) error {
p, err := build.GetPaver(ctx, sshPublicKey)
if err != nil {
return fmt.Errorf("failed to get paver to pave device: %w", err)
}
if err := c.RebootToRecovery(ctx); err != nil {
return fmt.Errorf("failed to reboot to recovery during paving: %w", err)
}
// First, pave the build's zedboot onto the device.
logger.Infof(ctx, "waiting for device to enter zedboot")
resolver, err := c.getResolver(ctx)
if err != nil {
return fmt.Errorf("error getting resolver: %w", err)
}
listeningName, err := resolver.WaitToFindDeviceInNetboot(ctx)
if err != nil {
return fmt.Errorf("failed to wait for device to reboot into zedboot: %w", err)
}
if err = p.PaveWithOptions(ctx, listeningName, paver.Options{Mode: paver.ZedbootOnly}); err != nil {
return fmt.Errorf("device failed to pave: %w", err)
}
// Next, pave the build onto the device.
logger.Infof(ctx, "paved zedboot, waiting for the device to boot into zedboot")
listeningName, err = resolver.WaitToFindDeviceInNetboot(ctx)
if err != nil {
return fmt.Errorf("failed to wait for device to reboot into zedboot: %w", err)
}
if err = p.PaveWithOptions(ctx, listeningName, paver.Options{Mode: paver.SkipZedboot}); err != nil {
return fmt.Errorf("device failed to pave: %w", err)
}
logger.Infof(ctx, "paver completed, waiting for device to boot")
return nil
}
// Flash the device to the specified build. Does not reconnect to the device.
func (c *Client) Flash(
ctx context.Context,
ffxTool *ffx.FFXTool,
build artifacts.Build,
publicKey ssh.PublicKey,
) error {
flasher := ffxTool.Flasher()
flasher.SetDiscoveryTimeout(12000)
flasher.SetRetries(3)
flasher.SetRetrySleep(c.flashRetrySleep)
flasher.SetSSHPublicKey(publicKey)
if productBundleDir, err := build.GetProductBundleDir(ctx); err == nil {
logger.Infof(ctx, "Flashing with the product bundle %s", productBundleDir)
flasher.SetProductBundle(productBundleDir)
} else {
logger.Warningf(ctx, "Failed to download the product bundle, trying to fall back to the flash manifest: %v", err)
manifest, errMan := build.GetFlashManifest(ctx)
if errMan != nil {
return fmt.Errorf("failed to get flash manifest from build: %w", errMan)
}
logger.Infof(ctx, "Flashing with the flash manifest %s", manifest)
flasher.SetManifest(manifest)
}
// Try to get the serial number while the device is still in Product state
// because in strict mode ffx might not be able to resolve the nodename
// when the device is in fastboot state later.
var serialNumber string
entries, err := ffxTool.TargetList(ctx, c.nodeName, 0)
if err != nil {
return fmt.Errorf("failed to list devices in Product state: %w", err)
}
if len(entries) > 0 {
serialNumber = entries[0].Serial
logger.Infof(ctx, "Found serial number %s in Product state", serialNumber)
}
logger.Infof(ctx, "rebooting device to bootloader before flashing in strict mode")
if err := c.RebootToBootloader(ctx, ffxTool); err != nil {
logger.Warningf(ctx, "failed to reboot to bootloader: %v. The device might already be in fastboot.", err)
}
logger.Infof(ctx, "waiting for device to enter fastboot")
resolver, err := c.getResolver(ctx)
if err != nil {
return fmt.Errorf("error getting resolver: %w", err)
}
fastbootTarget, err := resolver.WaitToFindDeviceInFastboot(ctx)
if err != nil {
return fmt.Errorf("device failed to enter fastboot mode: %w", err)
}
// If the target is the nodename, try to find the serial number instead
// because in strict mode ffx needs the serial number for fastboot devices.
if fastbootTarget == c.nodeName {
if serialNumber != "" {
fastbootTarget = serialNumber
logger.Infof(ctx, "Using serial number %s for fastboot target extracted in Product state", fastbootTarget)
} else {
entries, err := ffxTool.TargetList(ctx, "", 12*time.Second)
if err != nil {
return fmt.Errorf("failed to list devices in Fastboot state: %w", err)
}
for _, entry := range entries {
if entry.NodeName == fastbootTarget && entry.TargetState == "Fastboot" {
if entry.Serial != "" {
fastbootTarget = entry.Serial
logger.Infof(ctx, "Found serial number %s for fastboot target", fastbootTarget)
break
}
}
}
}
}
if fastbootTarget != "" {
logger.Infof(ctx, "found fastboot target: %s", fastbootTarget)
flasher.SetTarget(fastbootTarget)
}
_, err = flasher.Flash(ctx)
return err
}
// Forces an install of an update from an url, without requesting a reboot
func (c *Client) ForceInstall(
ctx context.Context,
ffxTool *ffx.FFXTool,
target string,
url string,
) error {
return ffxTool.TargetUpdateForceInstallNoReboot(ctx, target, url)
}
// Monitors the update for the connected client
func (c *Client) MonitorUpdate(
ctx context.Context,
ffxTool *ffx.FFXTool,
target string,
) (string, error) {
stdout, err := ffxTool.TargetUpdateCheckNowMonitor(ctx, target)
return string(stdout), err
}
// Set the update channel for the connected client
func (c *Client) SetUpdateChannel(
ctx context.Context,
ffxTool *ffx.FFXTool,
target string,
channel string,
) error {
targetSpecifier, err := c.getTargetSpecifier(ctx, ffxTool)
if err != nil {
return fmt.Errorf("failed to get target specifier: %w", err)
}
ffxTool.SetTarget(targetSpecifier)
return ffxTool.TargetUpdateChannelSet(ctx, targetSpecifier, channel)
}