blob: 9944d20b0444918d1b0e233ae35d9cf2e5f6391c [file] [log] [blame]
// Copyright 2018 The Chromium OS 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 arc supports interacting with the ARC framework, which is used to run Android applications on Chrome OS.
package arc
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/shirou/gopsutil/process"
"chromiumos/tast/caller"
"chromiumos/tast/common/android/adb"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
localadb "chromiumos/tast/local/android/adb"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/chromeproc"
"chromiumos/tast/local/syslog"
"chromiumos/tast/testing"
"chromiumos/tast/timing"
)
const (
// BootTimeout is the maximum amount of time that ARC is expected to take to boot.
// Tests that call New should declare a timeout that's at least this long.
BootTimeout = 120 * time.Second
// Time Android init process takes to start. It should be smaller than BootTimeout.
androidInitTimeout = 60 * time.Second
intentHelperTimeout = 20 * time.Second
// Time waiting for packages to install, for example enterprise auto install.
waitPackagesTimeout = 10 * time.Minute
logcatName = "logcat.txt"
pstoreCommandPath = "/usr/bin/vm_pstore_dump"
pstoreCommandExitCodeFileNotFound = 2
arcvmConsoleName = "messages-arcvm"
//ARCPath is the path where the container images are installed in the rootfs.
ARCPath = "/opt/google/containers/android"
//ARCVMPath is the pather where the VM images are installed in the rootfs.
ARCVMPath = "/opt/google/vms/android"
)
// DisableSyncFlags is the default flags for disabling ARC content sync and background activities when using GAIA accounts.
// --arc-disable-app-sync - prevents syncing installed apps from the previous sessions.
// --arc-disable-play-auto-install - disables PAI flow that downloads and installs apps in the background.
// --arc-play-store-auto-update=off - prevents Play Store and GMS Core from third party app update and prevents self-updates and downloadable content.
// --arc-disable-locale-sync - don’t propagate locale sync for the account that might cause reconfiguration updates.
// --arc-disable-media-store-maintenance - disables GMS scheduling of media store periodic indexing and corpora maintenance tasks.
func DisableSyncFlags() []string {
return []string{"--arc-disable-app-sync", "--arc-disable-play-auto-install", "--arc-disable-locale-sync", "--arc-play-store-auto-update=off", "--arc-disable-media-store-maintenance"}
}
// InstallType is the type of ARC (Container or VM) available on the device.
type InstallType int
const (
// Container is for the ARC container install.
Container InstallType = iota
// VM is for the ARCVM install.
VM
)
// locked is a flag that makes New and Close fail unconditionally.
var locked = false
// prePackages lists packages containing preconditions that are allowed to call
// Lock and Unlock.
var prePackages = []string{
"chromiumos/tast/local/arc",
"chromiumos/tast/local/multivm",
}
// Lock sets a flag that makes New and Close fail unconditionally.
// Preconditions and fixtures should call this function on setup to prevent
// tests from invalidating an ARC object by a mistake.
func Lock() {
caller.Check(2, prePackages)
locked = true
}
// Unlock resets the flag set by lock.
func Unlock() {
caller.Check(2, prePackages)
locked = false
}
// TODO(b/134144418): Consolidate ARC and ARCVM diverged code once ADB issues are resolved.
// Supported returns true if ARC is supported on the board.
//
// This function must not be used to skip tests entirely; declare the "android_p"
// software dependency instead. A valid use case would be to change the test
// expectation by whether ARC is supported or not (e.g. existence of mount
// points).
func Supported() bool {
_, ok := Type()
return ok
}
// Type detects the type (container or VM) of the ARC installation. As for
// Supported(), it should not be used to skip tests entirely, both fall under
// the "android_p" software dependency. But it could be used to change the
// behaviour of a test (e.g. check that ARCVM is running or not).
func Type() (t InstallType, ok bool) {
if _, err := os.Stat(filepath.Join(ARCPath, "system.raw.img")); err == nil {
return Container, true
}
if _, err := os.Stat(filepath.Join(ARCVMPath, "system.raw.img")); err == nil {
return VM, true
}
return 0, false
}
// ARC holds resources related to an active ARC session. Call Close to release
// those resources.
type ARC struct {
device *adb.Device // ADB device to communicate with ARC
outDir string // directory for log files. This becomes empty after ARC.saveLogFiles().
logcatCmd *testexec.Cmd // process saving Android logs
logcatWriter dynamicWriter // writes output from logcatCmd to logcatFile
logcatFile *os.File // file currently being written to
}
// Close releases testing-related resources associated with ARC.
// ARC itself is not stopped.
func (a *ARC) Close(ctx context.Context) error {
if locked {
panic("Do not call Close while precondition is being used")
}
var errs []error
if err := a.saveLogFiles(ctx); err != nil {
errs = append(errs, err)
}
if err := a.cleanUpLogcatFile(); err != nil {
errs = append(errs, err)
}
if len(errs) != 0 {
return errs[0]
}
return nil
}
// New waits for Android to finish booting.
//
// ARC must be enabled in advance by passing chrome.ARCEnabled or chrome.ARCSupported with
// real user gaia login to chrome.New.
//
// After this function returns successfully, you can assume BOOT_COMPLETED
// intent has been broadcast from Android system, and ADB connection is ready.
// Note that this does not necessarily mean all ARC mojo services are up; call
// WaitIntentHelper() to wait for ArcIntentHelper to be ready, for example.
//
// The returned ARC instance must be closed when the test is finished.
func New(ctx context.Context, outDir string) (*ARC, error) {
ctx, st := timing.Start(ctx, "arc_new")
defer st.End()
if locked {
panic("Cannot create ARC instance while precondition is being used")
}
ctx, cancel := context.WithTimeout(ctx, BootTimeout)
defer cancel()
if err := checkSoftwareDeps(ctx); err != nil {
return nil, err
}
if err := ensureARCEnabled(); err != nil {
return nil, err
}
arc := &ARC{
outDir: outDir,
}
toClose := arc
defer func() {
if toClose != nil {
toClose.Close(ctx)
}
}()
testing.ContextLog(ctx, "Waiting for Android boot")
// Prepare logcat file, it may be empty if early boot fails.
logcatPath := filepath.Join(outDir, logcatName)
if err := arc.setLogcatFile(logcatPath); err != nil {
return nil, errors.Wrap(err, "failed to create logcat output file")
}
if err := WaitAndroidInit(ctx); err != nil {
// Try starting logcat just in case logcat is possible. Android might still be up.
logcatCmd := BootstrapCommand(ctx, "/system/bin/logcat", "-d")
logcatCmd.Stdout = &arc.logcatWriter
testing.ContextLog(ctx, "Forcing collection of logcat at early boot")
if err := logcatCmd.Run(); err != nil {
testing.ContextLog(ctx, "Tried starting logcat anyway but failed: ", err)
}
return nil, diagnose(logcatPath, errors.Wrap(err, "Android failed to boot in very early stage"))
}
// At this point we can start logcat.
logcatCmd, err := startLogcat(ctx, &arc.logcatWriter)
if err != nil {
return nil, errors.Wrap(err, "failed to start logcat")
}
arc.logcatCmd = logcatCmd
// This property is set by the Android system server just before LOCKED_BOOT_COMPLETED is broadcast.
const androidBootProp = "sys.boot_completed"
if err := waitProp(ctx, androidBootProp, "1", reportTiming); err != nil {
return nil, diagnose(logcatPath, errors.Wrapf(err, "%s not set", androidBootProp))
}
var ch chan error
// Android is up. Set up ADB connection in parallel to Android boot since
// ADB local server takes a few seconds to start up.
testing.ContextLog(ctx, "Setting up ADB connection")
ch = make(chan error, 1)
go func() {
ch <- localadb.LaunchServer(ctx)
}()
// This property is set by ArcAppLauncher when it receives BOOT_COMPLETED.
const arcBootProp = "ro.arc.boot_completed"
if err := waitProp(ctx, arcBootProp, "1", reportTiming); err != nil {
return nil, diagnose(logcatPath, errors.Wrapf(err, "%s not set", arcBootProp))
}
// Android has booted.
if err := <-ch; err != nil {
return nil, diagnose(logcatPath, errors.Wrap(err, "failed setting up ADB auth"))
}
// Connect to ADB.
device, err := connectADB(ctx)
if err != nil {
return nil, diagnose(logcatPath, errors.Wrap(err, "failed connecting to ADB"))
}
arc.device = device
// Disable the Play Store package entirely unless it's booted with chrome.ARCSupported().
if enabled, err := isPlayStoreEnabled(); err != nil {
return nil, errors.Wrap(err, "failed to check whether Play Store is enabled")
} else if !enabled {
pkgs, err := arc.InstalledPackages(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get installed packages")
}
if _, found := pkgs["com.android.vending"]; found {
testing.ContextLog(ctx, "Disabling Play Store")
if err := arc.Command(ctx, "pm", "disable-user", "--user", "0", "com.android.vending").Run(); err != nil {
return nil, errors.Wrap(err, "failed to disable Play Store")
}
}
}
toClose = nil
return arc, nil
}
// WaitIntentHelper waits for ArcIntentHelper to get ready.
func (a *ARC) WaitIntentHelper(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, intentHelperTimeout)
defer cancel()
testing.ContextLog(ctx, "Waiting for ArcIntentHelper")
const prop = "ro.arc.intent_helper.ready"
if err := waitProp(ctx, prop, "1", reportTiming); err != nil {
return errors.Wrapf(err, "property %s not set", prop)
}
return nil
}
// androidDeps contains Android-related software features (see testing.Test.SoftwareDeps).
// At least one of them must be declared to call New.
var androidDeps = []string{
"android_vm",
"android_vm_r",
"android_p",
"arc",
}
// checkSoftwareDeps ensures the current test declares Android software dependencies.
func checkSoftwareDeps(ctx context.Context) error {
deps, ok := testing.ContextSoftwareDeps(ctx)
if !ok {
// Test info can be unavailable in unit tests.
return nil
}
for _, dep := range deps {
for _, adep := range androidDeps {
if dep == adep {
return nil
}
}
}
return errors.Errorf("test must declare at least one of Android software dependencies %v", androidDeps)
}
// resetOutDir updates the outDir field of ARC object.
func (a *ARC) resetOutDir(ctx context.Context, outDir string) error {
a.outDir = outDir
if err := a.setLogcatFile(filepath.Join(a.outDir, logcatName)); err != nil {
return err
}
return nil
}
// setLogcatFile creates a new logcat output file at p and opens it as a.logcatFile.
// a.logcatWriter is updated to write to the new file, and explanatory messages are
// written to both the new file and old file (if there was a previous file).
func (a *ARC) setLogcatFile(p string) error {
oldFile := a.logcatFile
var createErr error
a.logcatFile, createErr = os.Create(p)
if createErr == nil && oldFile != nil {
// Make the new file start with a line pointing at the old file.
if rel, err := filepath.Rel(filepath.Dir(a.logcatFile.Name()), oldFile.Name()); err == nil {
fmt.Fprintf(a.logcatFile, "[output continued from %v]\n", rel)
}
}
// If the create failed, we'll just drop the new logs.
a.logcatWriter.setDest(a.logcatFile)
if oldFile != nil {
if a.logcatFile != nil {
// Make the old file end with a line pointing at the new file.
if rel, err := filepath.Rel(filepath.Dir(oldFile.Name()), a.logcatFile.Name()); err == nil {
fmt.Fprintf(oldFile, "[output continued in %v]\n", rel)
}
}
oldFile.Close()
}
return createErr
}
func (a *ARC) cleanUpLogcatFile() error {
if a.logcatCmd != nil {
a.logcatCmd.Kill()
a.logcatCmd.Wait()
}
if a.logcatFile != nil {
a.logcatWriter.setDest(nil)
if err := a.logcatFile.Close(); err != nil {
return err
}
}
return nil
}
// VMEnabled returns true if Chrome OS is running ARCVM.
func VMEnabled() (bool, error) {
installType, ok := Type()
if !ok {
return false, errors.New("failed to get installation type")
}
return installType == VM, nil
}
// ensureARCEnabled makes sure ARC is enabled by a command line flag to Chrome.
func ensureARCEnabled() error {
args, err := getChromeArgs()
if err != nil {
return errors.Wrap(err, "failed getting Chrome args")
}
for _, a := range args {
if a == "--arc-start-mode=always-start-with-no-play-store" || a == "--arc-availability=officially-supported" {
return nil
}
}
return errors.New("ARC is not enabled; pass chrome.ARCEnabled or chrome.ARCSupported to chrome.New")
}
// isPlayStoreEnabled returns true when Play Store is enabled i.e. chrome.ARCSupported is passed.
func isPlayStoreEnabled() (bool, error) {
args, err := getChromeArgs()
if err != nil {
return false, errors.Wrap(err, "failed getting Chrome args")
}
for _, a := range args {
if a == "--arc-start-mode=always-start-with-no-play-store" {
return false, nil
}
}
return true, nil
}
// getChromeArgs returns command line arguments of the Chrome browser process.
func getChromeArgs() ([]string, error) {
pid, err := chromeproc.GetRootPID()
if err != nil {
return nil, err
}
proc, err := process.NewProcess(int32(pid))
if err != nil {
return nil, err
}
return proc.CmdlineSlice()
}
// diagnoseInitfailure extracts significant logs for init failure,
// such as exit message from crosvm.
func diagnoseInitfailure(reader *syslog.Reader, observedErr error) error {
lastMessage := ""
for {
entry, err := reader.Read()
if err != nil {
// End of syslog is reached (io.EOF) or some other error
// happened. Either way, return with last significant
// message if available.
if lastMessage != "" {
return errors.Wrap(observedErr, lastMessage)
}
return observedErr
}
if entry.Program == "crash_reporter" && strings.Contains(entry.Content, "Received crash notification for crosvm") {
return errors.Wrap(observedErr, entry.Content)
}
if strings.HasPrefix(entry.Program, "ARCVM") {
// TODO(b/167944318): try a better message
lastMessage = entry.Content
}
}
}
// WaitAndroidInit waits for Android init process to start.
//
// It is very rare you want to call this function from your test; to wait for
// the Android system to start and become ready, call New instead. A valid use
// case is when you want to interact with Android mini container.
//
// It is fine to call BootstrapCommand after this function successfully returns.
func WaitAndroidInit(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, androidInitTimeout)
defer cancel()
// Start a syslog reader so we can give more useful debug
// information waiting for boot.
reader, err := syslog.NewReader(ctx)
if err != nil {
return errors.Wrap(err, "failed to open syslog reader")
}
defer reader.Close()
// Wait for init or crosvm process to start before checking deeper.
testing.ContextLog(ctx, "Waiting for initial ARC process")
if err := testing.Poll(ctx, func(ctx context.Context) error {
_, err := InitPID()
return err
}, &testing.PollOptions{Interval: time.Second}); err != nil {
return diagnoseInitfailure(reader, errors.Wrap(err, "init/crosvm process did not start up"))
}
// Wait for property set by Android init in early stages. For P, wait for net.tcp.default_init_rwnd.
// For R and later, wait for ro.arc.on_boot which is set while bertha device is on boot.
// TODO(b/185198563): Replace net.tcp.default_init_rwnd with ro.arc.on_boot completely.
isVMEnabled, err := VMEnabled()
if err != nil {
return err
}
var prop = "ro.arc.on_boot"
var value = "1"
if !isVMEnabled {
prop = "net.tcp.default_init_rwnd"
value = "60"
}
if err := waitProp(ctx, prop, value, reportTiming); err != nil {
// Check if init/crosvm is still alive at this point.
if _, err := InitPID(); err != nil {
return diagnoseInitfailure(reader, errors.Wrap(err, "init/crosvm process exited unexpectedly"))
}
return errors.Wrapf(err, "%s property is not set which shows that Android init did not come up", prop)
}
return nil
}
// startLogcat starts a logcat process with its stdout redirected to w.
func startLogcat(ctx context.Context, w io.Writer) (*testexec.Cmd, error) {
// Wait for logd to start by polling logcat.
cmd := BootstrapCommand(ctx, "/system/bin/sh", "-c", "while ! /system/bin/logcat -ds; do sleep 0.1; done")
if err := cmd.Run(testexec.DumpLogOnError); err != nil {
return nil, errors.Wrap(err, "logcat failed")
}
// The logcat process may need to span multiple tests if we're being used by a precondition,
// so use context.Background instead of ctx to make sure it isn't killed prematurely.
cmd = BootstrapCommand(context.Background(), "/system/bin/logcat") // NOLINT: process may need to persist across multiple tests
cmd.Stdout = w
if err := cmd.Start(); err != nil {
return nil, err
}
return cmd, nil
}
// timingMode describes whether timing information should be reported.
type timingMode int
const (
reportTiming timingMode = iota // create a timing stage
noReportTiming // don't create a timing stage
)
// waitProp waits for Android prop name is set to value.
func waitProp(ctx context.Context, name, value string, tm timingMode) error {
if tm == reportTiming {
var st *timing.Stage
ctx, st = timing.Start(ctx, fmt.Sprintf("wait_prop_%s=%s", name, value))
defer st.End()
}
const loop = `while [ "$(/system/bin/getprop "$1")" != "$2" ]; do sleep 0.1; done`
return testing.Poll(ctx, func(ctx context.Context) error {
return BootstrapCommand(ctx, "/system/bin/sh", "-c", loop, "-", name, value).Run()
}, &testing.PollOptions{Interval: time.Second})
}
// APKPath returns the absolute path to a helper APK.
func APKPath(value string) string {
return adb.APKPath(value)
}
// makeList returns a list of keys from map.
func makeList(packages map[string]bool) []string {
var packagesList []string
for pkg := range packages {
packagesList = append(packagesList, pkg)
}
sort.Strings(packagesList)
return packagesList
}
// WaitForPackages waits for Android packages being installed.
func (a *ARC) WaitForPackages(ctx context.Context, packages []string) error {
ctx, st := timing.Start(ctx, "wait_packages")
defer st.End()
ctx, cancel := context.WithTimeout(ctx, waitPackagesTimeout)
defer cancel()
notInstalledPackages := make(map[string]bool)
for _, p := range packages {
notInstalledPackages[p] = true
}
testing.ContextLog(ctx, "Waiting for packages")
return testing.Poll(ctx, func(ctx context.Context) error {
pkgs, err := a.InstalledPackages(ctx)
if err != nil {
// On ARCVM package service may not be running yet. Wait until it's available.
vmEnabled, vmerr := VMEnabled()
if vmerr != nil {
return errors.Wrap(vmerr, "failed to check if VM is enabled")
}
if vmEnabled && strings.Contains(err.Error(), "package service not running") {
return err
}
return testing.PollBreak(err)
}
for p := range pkgs {
if notInstalledPackages[p] {
delete(notInstalledPackages, p)
}
}
if len(notInstalledPackages) != 0 {
return errors.Errorf("%d package(s) are not installed yet: %s",
len(notInstalledPackages),
strings.Join(makeList(notInstalledPackages), ", "))
}
return nil
}, &testing.PollOptions{Interval: time.Second})
}
// State holds the ARC state returned from autotestPrivate.getArcState() call.
//
// Refer to https://chromium.googlesource.com/chromium/src/+/main/chrome/common/extensions/api/autotest_private.idl
// for the mapping of the fields to JavaScript.
type State struct {
// Provisioned indicates whether the ARC is provisioned.
Provisioned bool `json:"provisioned"`
// TOSNeeded indicates whether ARC Terms of Service needs to be shown.
TOSNeeded bool `json:"tosNeeded"`
// PreStartTime is ARC pre-start time (mini-ARC) or 0 if not pre-started.
PreStartTime float64 `json:"preStartTime"`
// StartTime is the ARC start time or 0 if not started.
StartTime float64 `json:"startTime"`
}
// GetState gets the arc state. It is a wrapper for chrome.autotestPrivate.getArcState.
func GetState(ctx context.Context, tconn *chrome.TestConn) (State, error) {
var state State
if err := tconn.Call(ctx, &state, `tast.promisify(chrome.autotestPrivate.getArcState)`); err != nil {
return state, errors.Wrap(err, "failed to run autotestPrivate.getArcState")
}
return state, nil
}
// saveLogFiles writes log files to the a.outDir directory and clears the a.outDir.
func (a *ARC) saveLogFiles(ctx context.Context) error {
if a.outDir == "" {
return nil
}
if _, err := os.Stat(a.outDir); os.IsNotExist(err) {
// Preconditions may call this method after the outDir is removed.
return nil
}
if err := saveARCVMConsole(ctx, filepath.Join(a.outDir, arcvmConsoleName)); err != nil {
return errors.Wrap(err, "failed to save the messages-arcvm")
}
// Reset outDir to avoid saving the same files twice at ARC.Close().
a.outDir = ""
return nil
}
// saveARCVMConsole saves the console output of ARCVM Kernel to the given path using vm_pstore_dump command.
func saveARCVMConsole(ctx context.Context, path string) error {
// Do nothing for containers. The console output is already captured for containers.
isVMEnabled, err := VMEnabled()
if err != nil {
return err
}
if !isVMEnabled {
return nil
}
// TODO(b/153934386): Remove this check when pstore is enabled on ARM.
// The pstore feature is enabled only on x86_64. It's not enabled on some architectures, and this `vm_pstore_dump` command doesn't exist on such architectures.
if _, err := os.Stat(pstoreCommandPath); os.IsNotExist(err) {
testing.ContextLog(ctx, "Saving messages-arcvm file is skipped because vm_pstore_dump command is not found")
return nil
}
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
cmd := testexec.CommandContext(ctx, pstoreCommandPath)
cmd.Stdout = file
var errbuf bytes.Buffer
cmd.Stderr = &errbuf
if err := cmd.Run(); err != nil {
errmsg := errbuf.String()
if cmd.ProcessState.ExitCode() == pstoreCommandExitCodeFileNotFound {
// This failure sometimes happens when ARCVM failed to boot. So we don't make this error.
testing.ContextLogf(ctx, "vm_pstore_dump command failed because the .pstore file doesn't exist: %#v", errmsg)
} else {
return errors.Wrapf(err, "vm_pstore_dump command failed with an unexpected reason: %#v", errmsg)
}
}
return nil
}