blob: 5a10dd975cf14a406b9c0ce1e05da38ae7752f35 [file] [edit]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package androidtools
import (
"context"
"strings"
"time"
"go.chromium.org/chromiumos/config/go/test/api"
"go.chromium.org/luci/common/errors"
"go.chromium.org/infra/cros/recovery/internal/components/cft"
"go.chromium.org/infra/cros/recovery/internal/env"
"go.chromium.org/infra/cros/recovery/internal/log"
"go.chromium.org/infra/cros/recovery/tlw"
)
// ADBRun executes a generic adb command.
//
// This function can run any adb command. For specific common commands like shell, root,
// connect, disconnect, more specific wrapper functions are available (e.g., ADBShell, ADBRoot).
// It abstracts the client creation and error handling.
//
// Args:
//
// ctx: Context for the operation.
// dut: The device under test.
// timeout: The maximum time to allow for the command execution.
// command: The adb command to execute (e.g., "devices", "reboot").
// args: Additional arguments for the adb command.
//
// Returns:
//
// Response: The result of the command execution.
// error: An error if the command failed to execute or the exit code was non-zero.
func ADBRun(ctx context.Context, dut *tlw.Dut, timeout time.Duration, command string, args ...string) (Response, error) {
client, err := adbClient(ctx, dut)
if err != nil {
return emptyResponse(), errors.WrapIf(err, "adb run")
}
res, err := adbRun(ctx, client, timeout, command, args...)
if err != nil {
return res, errors.WrapIf(err, "adb run")
}
if res.GetExitCode() != 0 {
return res, errors.Fmt("adb run: failed with exitcode: %d", res.GetExitCode())
}
return res, nil
}
// ADBRoot attempts to restart the adb daemon with root permissions on the device.
//
// Equivalent to running "adb -s <serial> root".
//
// Args:
//
// ctx: Context for the operation.
// dut: The device under test.
// timeout: The maximum time to allow for the command execution.
//
// Returns:
//
// Response: The result of the command execution.
// error: An error if the command failed to execute or the exit code was non-zero.
func ADBRoot(ctx context.Context, dut *tlw.Dut, timeout time.Duration) (Response, error) {
client, err := adbClient(ctx, dut)
if err != nil {
return emptyResponse(), errors.WrapIf(err, "adb root")
}
args := []string{ADBSerial(dut), rootCommand}
res, err := adbRun(ctx, client, timeout, serialParam, args...)
if err != nil {
return res, errors.WrapIf(err, "adb root")
}
if res.GetExitCode() != 0 {
return res, errors.Fmt("adb root: failed with exitcode: %d", res.GetExitCode())
}
return res, nil
}
// ADBConnect connects to the device, usually over TCP/IP.
//
// Equivalent to running "adb connect <serial>".
//
// Args:
//
// ctx: Context for the operation.
// dut: The device under test.
// timeout: The maximum time to allow for the command execution.
//
// Returns:
//
// Response: The result of the command execution.
// error: An error if the command failed to execute or the exit code was non-zero.
func ADBConnect(ctx context.Context, dut *tlw.Dut, timeout time.Duration) (Response, error) {
client, err := adbClient(ctx, dut)
if err != nil {
return emptyResponse(), errors.WrapIf(err, "adb connect")
}
args := []string{ADBSerial(dut)}
res, err := adbRun(ctx, client, timeout, connectCommand, args...)
if err != nil {
return res, errors.WrapIf(err, "adb connect")
}
if res.GetExitCode() != 0 {
return res, errors.Fmt("adb connect: failed with exitcode: %d", res.GetExitCode())
}
return res, nil
}
// ADBDisconnect disconnects from the device, usually over TCP/IP.
//
// Equivalent to running "adb disconnect <serial>".
//
// Args:
//
// ctx: Context for the operation.
// dut: The device under test.
// timeout: The maximum time to allow for the command execution.
//
// Returns:
//
// Response: The result of the command execution.
// error: An error if the command failed to execute or the exit code was non-zero.
func ADBDisconnect(ctx context.Context, dut *tlw.Dut, timeout time.Duration) (Response, error) {
client, err := adbClient(ctx, dut)
if err != nil {
return emptyResponse(), errors.WrapIf(err, "adb disconnect")
}
args := []string{ADBSerial(dut)}
res, err := adbRun(ctx, client, timeout, disconnectCommand, args...)
if err != nil {
return res, errors.WrapIf(err, "adb disconnect")
}
if res.GetExitCode() != 0 {
return res, errors.Fmt("adb disconnect: failed with exitcode: %d", res.GetExitCode())
}
return res, nil
}
// ADBIsConnected checks if the device is currently listed in the output of "adb devices".
//
// Args:
//
// ctx: Context for the operation.
// dut: The device under test.
// timeout: The maximum time to allow for the "adb devices" command.
//
// Returns:
//
// bool: True if the device is listed, false otherwise.
func ADBIsConnected(ctx context.Context, dut *tlw.Dut, timeout time.Duration) bool {
res, err := adbDevices(ctx, dut, timeout)
if err != nil {
log.Debugf(ctx, "Failed to read adb devices, assume device isn't listed yet: %s", err)
return false
}
deviceName := ADBSerial(dut)
if out := string(res.GetStdout()); out != "" && strings.Contains(out, deviceName) {
log.Debugf(ctx, "Device is listed.")
return true
}
log.Debugf(ctx, "Device isn't listed yet: %s", err)
return false
}
// adbDevices runs the "adb devices" command.
func adbDevices(ctx context.Context, dut *tlw.Dut, timeout time.Duration) (Response, error) {
client, err := adbClient(ctx, dut)
if err != nil {
return emptyResponse(), errors.WrapIf(err, "adb devices")
}
res, err := adbRun(ctx, client, timeout, devicesCommand)
if err != nil {
return res, errors.WrapIf(err, "adb devices")
}
if res.GetExitCode() != 0 {
return res, errors.Fmt("adb devices: failed with exitcode: %d", res.GetExitCode())
}
return res, nil
}
// ADBShell executes a shell command on the device via adb.
//
// Equivalent to running "adb -s <serial> shell <command> <args...>".
//
// Args:
//
// ctx: Context for the operation.
// dut: The device under test.
// timeout: The maximum time to allow for the command execution.
// command: The shell command to execute on the device.
// args: Additional arguments for the shell command.
//
// Returns:
//
// Response: The result of the command execution.
// error: An error if the command failed to execute or the exit code was non-zero.
func ADBShell(ctx context.Context, dut *tlw.Dut, timeout time.Duration, command string, args ...string) (Response, error) {
client, err := adbClient(ctx, dut)
if err != nil {
return emptyResponse(), errors.WrapIf(err, "adb shell")
}
// Example adb -s device1:5555 shell xyz
// Where `-s` is the command and rest will be args.
newArgs := []string{ADBSerial(dut), shellCommand, command}
newArgs = append(newArgs, args...)
res, err := adbRun(ctx, client, timeout, serialParam, newArgs...)
if err != nil {
return res, errors.WrapIf(err, "adb shell")
}
if res.GetExitCode() != 0 {
return res, errors.Fmt("exec adb command: failed with exitcode: %d", res.GetExitCode())
}
return res, nil
}
// adbRun runs raw command by base-adb service and return result.
//
// Error is provided only if the service fails to execute command or timeout.
// Command execution is always represented as exec-code in response.
func adbRun(ctx context.Context, client api.ToolsServiceClient, timeout time.Duration, command string, args ...string) (Response, error) {
if command == "" {
return nil, errors.Fmt("adb run: command is empty")
}
fullCmd := "adb " + command
if len(args) > 0 {
fullCmd += " " + strings.Join(args, " ")
}
log.Debugf(ctx, "Prepare to run adb command: %q", fullCmd)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var err error
var response Response
if client != nil {
response, err = client.ADBRun(ctx, &api.ADBRunRequest{
Command: command,
Params: args,
})
} else {
response, err = localRun(ctx, env.ADBPath(), command, args...)
}
if response != nil {
printResponse(ctx, response)
}
if err != nil {
err = errors.Fmt("adb run: failed execute command %q, finished with error: %w", fullCmd, err)
}
return response, errors.WrapIf(err, "adb run: command %q", fullCmd)
}
func adbClient(ctx context.Context, dut *tlw.Dut) (api.ToolsServiceClient, error) {
if UsesContainer(ctx) {
return cft.ToolClientFromScope(ctx, dut)
}
// For local test we do not provide a client.
return nil, nil
}
// ADBDeviceState returns the state of the device.
//
// Returns one of: "device", "offline", "bootloader", "unauthorized", "unknown".
func ADBDeviceState(ctx context.Context, dut *tlw.Dut, timeout time.Duration) (string, error) {
client, err := adbClient(ctx, dut)
if err != nil {
return "", errors.WrapIf(err, "adb get-state")
}
args := []string{ADBSerial(dut), "get-state"}
res, err := adbRun(ctx, client, timeout, serialParam, args...)
if err != nil {
return "", errors.WrapIf(err, "adb get-state")
}
return parseGetStateOutput(res.GetStdout(), res.GetExitCode(), res.GetStderr())
}
func parseGetStateOutput(stdout []byte, exitCode int32, stderr []byte) (string, error) {
if exitCode != 0 {
stderrStr := string(stderr)
if strings.Contains(stderrStr, "not found") {
return "unknown", nil
}
return "", errors.Fmt("adb get-state: failed with exitcode: %d, stderr: %s", exitCode, stderrStr)
}
return strings.TrimSpace(string(stdout)), nil
}
// ADBDeviceVisible checks if the device is visible via ADB.
// It returns nil if the device is visible (state is not offline or unknown),
// and an error otherwise.
func ADBDeviceVisible(ctx context.Context, dut *tlw.Dut, timeout time.Duration) error {
state, err := ADBDeviceState(ctx, dut, timeout)
if err != nil {
return errors.Fmt("adb device visible: %w", err)
}
log.Debugf(ctx, "ADB device state: %q", state)
if state == "offline" || state == "unknown" {
return errors.Reason("adb device visible: device is in %q state", state)
}
return nil
}