| // Copyright 2023 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 ffx |
| |
| import ( |
| "bytes" |
| "cmp" |
| "context" |
| "encoding/json" |
| "fmt" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "slices" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| ) |
| |
| var _ FFXToolImpl = (*ffxDaemon)(nil) |
| |
| type ffxDaemon struct { |
| ffxToolPath string |
| runDir RunDir |
| supportsPackageBlob *bool |
| supportsDirect bool |
| } |
| |
| var directSupportCache sync.Map |
| |
| func newFfxDaemon(ctx context.Context, ffxToolPath string, runDir RunDir) (*ffxDaemon, error) { |
| if _, err := os.Stat(ffxToolPath); err != nil { |
| return nil, fmt.Errorf("error accessing %v: %w", ffxToolPath, err) |
| } |
| |
| // Check if the ffx binary supports the --direct flag, which allows bypassing |
| // the daemon for certain commands to avoid sync issues in tests that frequently |
| // reboot the target. |
| var supportsDirect bool |
| if val, ok := directSupportCache.Load(ffxToolPath); ok { |
| supportsDirect = val.(bool) |
| } else { |
| supportsDirect = false |
| cmd := exec.CommandContext(ctx, ffxToolPath, "--help") |
| output, err := cmd.Output() |
| if err == nil { |
| if strings.Contains(string(output), "--direct") { |
| supportsDirect = true |
| } |
| } else { |
| logger.Warningf(ctx, "failed to run ffx --help to check for --direct: %v", err) |
| } |
| directSupportCache.Store(ffxToolPath, supportsDirect) |
| } |
| |
| return &ffxDaemon{ |
| ffxToolPath: ffxToolPath, |
| runDir: runDir, |
| supportsPackageBlob: nil, |
| supportsDirect: supportsDirect, |
| }, nil |
| } |
| |
| func (f *ffxDaemon) RunDir() RunDir { |
| return f.runDir |
| } |
| |
| func (f *ffxDaemon) ClearRunDir() { |
| os.RemoveAll(f.RunDir().path) |
| } |
| |
| func (f *ffxDaemon) appendDirectFlag(args []string) []string { |
| if f.supportsDirect { |
| return append(args, "--direct") |
| } |
| return args |
| } |
| |
| func (f *ffxDaemon) StopDaemon(ctx context.Context) error { |
| // TODO(https://fxbug.dev/415899721): We put a time |
| // limit because the command fails when run inside an nsjail. |
| // Remove when bug is fixed. |
| args := []string{"daemon", "stop", "-t", "4000"} |
| _, err := f.runFFXCmd(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) TargetList(ctx context.Context) ([]TargetEntry, error) { |
| args := f.appendDirectFlag([]string{}) |
| args = append(args, "--machine", "json", "target", "list") |
| |
| stdout, err := f.runFFXCmd(ctx, args...) |
| if err != nil { |
| return []TargetEntry{}, fmt.Errorf("ffx target list failed: %w", err) |
| } |
| |
| if len(stdout) == 0 { |
| return []TargetEntry{}, nil |
| } |
| |
| var entries []TargetEntry |
| if err := json.Unmarshal(stdout, &entries); err != nil { |
| return []TargetEntry{}, err |
| } |
| |
| return entries, nil |
| } |
| |
| func (f *ffxDaemon) GetDisambiguatedTarget(ctx context.Context) (TargetEntry, error) { |
| targets, err := f.TargetList(ctx) |
| if err != nil { |
| return TargetEntry{}, err |
| } |
| |
| if len(targets) == 1 { |
| return targets[0], nil |
| } |
| |
| for _, v := range targets { |
| if v.IsDefault { |
| return v, nil |
| } |
| } |
| |
| return slices.MinFunc(targets, func(a, b TargetEntry) int { |
| return cmp.Compare(a.NodeName, b.NodeName) |
| }), nil |
| } |
| |
| func (f *ffxDaemon) TargetListForNode(ctx context.Context, nodeName string) ([]TargetEntry, error) { |
| entries, err := f.TargetList(ctx) |
| if err != nil { |
| return []TargetEntry{}, err |
| } |
| |
| var matchingTargets []TargetEntry |
| |
| for _, target := range entries { |
| if target.NodeName == nodeName { |
| matchingTargets = append(matchingTargets, target) |
| } |
| } |
| |
| return matchingTargets, nil |
| } |
| |
| func (f *ffxDaemon) WaitForTarget(ctx context.Context, address string) (TargetEntry, error) { |
| for attempt := 0; attempt < 10; attempt++ { |
| entries, err := f.TargetList(ctx) |
| if err != nil { |
| return TargetEntry{}, fmt.Errorf("failed to get target list: %w", err) |
| } |
| |
| for _, target := range entries { |
| for _, addr := range target.Addresses { |
| if addr.Type == "Ip" && addr.IP == address { |
| return target, nil |
| } |
| } |
| } |
| time.Sleep(5 * time.Second) |
| } |
| |
| return TargetEntry{}, fmt.Errorf("no target found for address %v", address) |
| } |
| |
| func (f *ffxDaemon) TargetGetSshAddress(ctx context.Context, target string) (string, error) { |
| args := f.appendDirectFlag([]string{}) |
| args = append(args, "--target", target, "target", "list", "--format", "addresses", "--no-probe", "--no-usb") |
| |
| stdout, err := f.runFFXCmd(ctx, args...) |
| if err != nil { |
| return "", fmt.Errorf("ffx target list --format addresses failed: %w", err) |
| } |
| |
| return strings.TrimSpace(string(stdout)), nil |
| } |
| |
| func (f *ffxDaemon) SupportsZedbootDiscovery(ctx context.Context) (bool, error) { |
| // Check if ffx is configured to resolve devices in zedboot. |
| args := []string{ |
| "config", |
| "get", |
| "discovery.zedboot.enabled", |
| } |
| |
| stdout, err := f.runFFXCmd(ctx, args...) |
| if err != nil { |
| // `ffx config get` exits with 2 if variable is undefined. |
| if exiterr, ok := err.(*exec.ExitError); ok { |
| if exiterr.ExitCode() == 2 { |
| return false, nil |
| } |
| } |
| |
| return false, fmt.Errorf("ffx config get failed: %w", err) |
| } |
| |
| // FIXME(https://fxbug.dev/42060660): Unfortunately we need to parse the raw string to see if it's true. |
| if string(stdout) == "true\n" { |
| return true, nil |
| } |
| |
| return false, nil |
| } |
| |
| func (f *ffxDaemon) TargetAdd(ctx context.Context, target string) error { |
| args := []string{"target", "add", "--nowait", target} |
| _, err := f.runFFXCmd(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) TargetGetSshTime(ctx context.Context, target string) (time.Duration, error) { |
| args := []string{ |
| "--target", |
| target, |
| "target", |
| "get-time", |
| } |
| |
| t0 := time.Now() |
| stdout, err := f.runFFXCmd(ctx, args...) |
| t1 := time.Now() |
| |
| if err != nil { |
| return 0, fmt.Errorf("ffx target get-time failed: %w", err) |
| } |
| |
| t, err := strconv.Atoi(strings.TrimSpace(string(stdout))) |
| if err != nil { |
| return 0, fmt.Errorf("failed to parse ffx target-get-time output: %w", err) |
| } |
| |
| // Estimate the latency as half the time to execute the command. |
| latency := t1.Sub(t0) / 2 |
| |
| // The output is in nanoseconds. |
| monotonicTime := (time.Duration(t) * time.Nanosecond) - latency |
| |
| return monotonicTime, nil |
| } |
| |
| func (f *ffxDaemon) TargetUpdateChannelSet(ctx context.Context, target string, channel string) error { |
| args := []string{ |
| "--target", |
| target, |
| "target", |
| "update", |
| "channel", |
| "set", |
| channel, |
| } |
| |
| _, err := f.runFFXCmd(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) TargetUpdateCheckNowMonitor(ctx context.Context, target string) ([]byte, error) { |
| args := []string{ |
| "--target", |
| target, |
| "target", |
| "update", |
| "check-now", |
| "--monitor", |
| } |
| |
| return f.runFFXCmd(ctx, args...) |
| } |
| |
| func (f *ffxDaemon) TargetUpdateForceInstallNoReboot(ctx context.Context, target string, url string) error { |
| args := f.appendDirectFlag([]string{}) |
| args = append(args, "--target", target, "target", "update", "force-install", url, "--reboot", "false") |
| |
| _, err := f.runFFXCmd(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) Flasher() *Flasher { |
| return newFlasher(f) |
| } |
| |
| func (f *ffxDaemon) runFFXCmd(ctx context.Context, args ...string) ([]byte, error) { |
| path, err := exec.LookPath(f.ffxToolPath) |
| if err != nil { |
| return []byte{}, err |
| } |
| |
| // prepend a config flag for finding subtools that are compiled separately |
| // in the same directory as ffx itself. |
| args = append( |
| []string{ |
| "--log-level", "trace", |
| "--isolate-dir", f.runDir.path, |
| "--config", fmt.Sprintf("ffx.subtool-search-paths=%s", filepath.Dir(path)), |
| }, |
| args..., |
| ) |
| |
| logger.Infof(ctx, "running: %s %q", path, args) |
| cmd := exec.CommandContext(ctx, path, args...) |
| var stdoutBuf bytes.Buffer |
| cmd.Stdout = &stdoutBuf |
| cmd.Stderr = os.Stderr |
| |
| cmdRet := cmd.Run() |
| |
| stdout := stdoutBuf.Bytes() |
| if len(stdout) != 0 { |
| logger.Infof(ctx, "%s", string(stdout)) |
| } |
| |
| if cmdRet == nil { |
| logger.Infof(ctx, "finished running %s %q", path, args) |
| } else { |
| logger.Infof(ctx, "running %s %q failed with: %v", path, args, cmdRet) |
| } |
| return stdout, cmdRet |
| } |
| |
| func (f *ffxDaemon) RunAndGetOutput(ctx context.Context, args ...string) (string, error) { |
| stdout, err := f.runFFXCmd(ctx, args...) |
| return string(stdout), err |
| } |
| |
| func (f *ffxDaemon) Run(ctx context.Context, args ...string) error { |
| _, err := f.RunAndGetOutput(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) RepositoryCreate(ctx context.Context, repoDir, keysDir string) error { |
| args := []string{ |
| "--config", "ffx_repository=true", |
| "repository", |
| "create", |
| "--keys", keysDir, |
| repoDir, |
| } |
| |
| _, err := f.runFFXCmd(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) RepositoryPublish(ctx context.Context, repoDir string, packageManifests []string, additionalArgs ...string) error { |
| args := []string{ |
| "repository", |
| "publish", |
| } |
| |
| for _, manifest := range packageManifests { |
| args = append(args, "--package", manifest) |
| } |
| |
| args = append(args, additionalArgs...) |
| args = append(args, repoDir) |
| |
| _, err := f.runFFXCmd(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) SupportsPackageBlob(ctx context.Context) bool { |
| if f.supportsPackageBlob == nil { |
| _, err := f.runFFXCmd(ctx, "package", "blob", "--help") |
| supportsPackageBlob := err == nil |
| f.supportsPackageBlob = &supportsPackageBlob |
| } |
| return *f.supportsPackageBlob |
| } |
| |
| func (f *ffxDaemon) DecompressBlobs(ctx context.Context, deliveryBlobs []string, outDir string) error { |
| args := []string{ |
| "package", |
| "blob", |
| "decompress", |
| "--output", outDir, |
| } |
| |
| args = append(args, deliveryBlobs...) |
| |
| _, err := f.runFFXCmd(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) RegisterPackageRepository(ctx context.Context, repo_url string) error { |
| args := []string{ |
| "target", |
| "repository", |
| "register", |
| "--json-uri", |
| repo_url, |
| } |
| |
| _, err := f.runFFXCmd(ctx, args...) |
| return err |
| } |
| |
| func (f *ffxDaemon) TargetGetLastRebootReason(ctx context.Context, target string) (string, error) { |
| args := f.appendDirectFlag([]string{}) |
| args = append(args, "--target", target, "--machine", "json", "target", "show") |
| |
| stdout, err := f.runFFXCmd(ctx, args...) |
| if err != nil { |
| return "", fmt.Errorf("ffx target show failed: %w", err) |
| } |
| |
| var showInfo struct { |
| Target struct { |
| LastRebootReason *string `json:"last_reboot_reason"` |
| } `json:"target"` |
| } |
| |
| if err := json.Unmarshal(stdout, &showInfo); err != nil { |
| return "", fmt.Errorf("failed to unmarshal ffx target show output: %w", err) |
| } |
| |
| if showInfo.Target.LastRebootReason != nil { |
| return *showInfo.Target.LastRebootReason, nil |
| } |
| |
| return "", fmt.Errorf("no last reboot reason found in ffx target show output") |
| } |