blob: 494a857e173d89d47c54c043134121d68bbd4390 [file] [log] [blame]
// Copyright 2020 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
import (
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"google.golang.org/grpc"
"chromiumos/tast/common/testexec"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/arc/optin"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/disk"
"chromiumos/tast/local/upstart"
arcpb "chromiumos/tast/services/cros/arc"
"chromiumos/tast/testing"
)
// Defines custom error that indicate required flag file is not set to "1".
type flagIsNotSetError struct {
reason string
}
func (e *flagIsNotSetError) Error() string {
return e.reason
}
func init() {
testing.AddService(&testing.Service{
Register: func(srv *grpc.Server, s *testing.ServiceState) {
arcpb.RegisterUreadaheadPackServiceServer(srv, &UreadaheadPackService{s: s})
},
})
}
// UreadaheadPackService implements tast.cros.arc.UreadaheadPackService.
type UreadaheadPackService struct {
s *testing.ServiceState
}
// Generate generates ureadahead pack for requested Chrome login mode for VM or container.
func (c *UreadaheadPackService) Generate(ctx context.Context, request *arcpb.UreadaheadPackRequest) (*arcpb.UreadaheadPackResponse, error) {
const (
ureadaheadDataDir = "/var/lib/ureadahead"
containerPackName = "opt.google.containers.android.rootfs.root.pack"
containerRoot = "/opt/google/containers/android/rootfs/root"
arcvmPackName = "opt.google.vms.android.pack"
arcvmRoot = "/opt/google/vms/android"
sysOpenTrace = "/sys/kernel/debug/tracing/events/fs/do_sys_open"
logName = "ureadahead.log"
ureadaheadTimeout = 30 * time.Second
)
// Create arguments for running ureadahead.
args := []string{
"--verbose",
"--force-trace",
}
// Stop UI to make sure we don't have any pending holds and race condition restarting Chrome.
testing.ContextLog(ctx, "Stopping UI to release all possible locks")
if err := upstart.StopJob(ctx, "ui"); err != nil {
return nil, errors.Wrap(err, "failed to stop ui")
}
defer upstart.EnsureJobRunning(ctx, "ui")
// Check for whether ARCVM is present.
vmEnabled, err := arc.VMEnabled()
if err != nil {
return nil, errors.Wrap(err, "failed to check whether ARCVM is enabled")
}
var packPath string
var arcRoot string
// Part of arguments differ in container and arcvm.
if vmEnabled {
packPath = filepath.Join(ureadaheadDataDir, arcvmPackName)
args = append(args, fmt.Sprintf("--path-prefix-filter=%s", arcvmRoot))
args = append(args, fmt.Sprintf("--pack-file=%s", packPath))
arcRoot = arcvmRoot
} else {
packPath = filepath.Join(ureadaheadDataDir, containerPackName)
args = append(args, fmt.Sprintf("--path-prefix=%s", containerRoot))
arcRoot = containerRoot
}
args = append(args, arcRoot)
out, err := testexec.CommandContext(ctx, "lsof", "+D", arcRoot).CombinedOutput()
if err != nil {
// In case nobody holds file, lsof returns 1.
if exitError, ok := err.(*exec.ExitError); !ok || exitError.ExitCode() != 1 {
return nil, errors.Wrap(err, "failed to verify android root is not locked")
}
}
outStr := string(out)
if outStr != "" {
return nil, errors.Errorf("found locks for %q: %q", arcRoot, outStr)
}
if err := os.Remove(packPath); err != nil && !os.IsNotExist(err) {
return nil, errors.Wrap(err, "failed to clean up existing pack")
}
testing.ContextLog(ctx, "Login Chrome")
chromeArgs := append(arc.DisableSyncFlags(), "--arc-force-show-optin-ui")
if vmEnabled {
chromeArgs = append(chromeArgs, "--arcvm-mount-debugfs", "--arcvm-ureadahead-mode=generate")
}
opts := []chrome.Option{
chrome.ARCSupported(), // This does not start ARC automatically
chrome.RestrictARCCPU(),
chrome.GAIALoginPool(request.Creds),
chrome.ExtraArgs(chromeArgs...)}
cr, err := chrome.New(ctx, opts...)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Chrome")
}
// Shorten the total context by 5 seconds to allow for cleanup.
cleanCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
defer cr.Close(cleanCtx)
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to create test API connection")
}
// Drop caches before starting ureadahead tracing.
if err := disk.DropCaches(ctx); err != nil {
return nil, errors.Wrap(err, "failed to drop caches")
}
testing.ContextLog(ctx, "Start ureadahead tracing")
flags := []string{"/sys/kernel/debug/tracing/tracing_on",
filepath.Join(sysOpenTrace, "enable")}
// Define callback to handle flag.
type flagHandler func(string) error
// Helper that processes all tracked tracing flags.
processFlags := func(fn flagHandler) error {
for _, flag := range flags {
if err := fn(flag); err != nil {
return err
}
}
return nil
}
// Make sure ureadahead flips these flags to confirm it is started.
resetFlag := func(flag string) error {
return ioutil.WriteFile(flag, []byte("0"), 0644)
}
if err := processFlags(resetFlag); err != nil {
return nil, errors.Wrap(err, "failed to reset ureadahead flag")
}
if err := ioutil.WriteFile("/sys/kernel/debug/tracing/trace", []byte(""), 0644); err != nil {
return nil, errors.Wrap(err, "failed to reset tracing buffer")
}
logPath := filepath.Join(ureadaheadDataDir, logName)
log, err := os.Create(logPath)
if err != nil {
return nil, errors.Wrap(err, "failed to create log file")
}
defer log.Close()
cmd := testexec.CommandContext(ctx, "ureadahead", args...)
cmd.Stdout = log
cmd.Stderr = log
if err := cmd.Start(); err != nil {
return nil, errors.Wrap(err, "failed to start ureadahead tracing")
}
// Make sure that content of the flag is set to "1".
enusureFlagSet := func(flag string) error {
content, err := ioutil.ReadFile(flag)
if err != nil {
return err
}
contentStr := strings.TrimSpace(string(content))
// 1 means flag is enabled.
if contentStr != "1" {
return &flagIsNotSetError{
reason: fmt.Sprintf("flag %q=%q is not set to 1", flag, contentStr),
}
}
return nil
}
// Wait ureadahead actually started. All tracked flags must be flipped to "1".
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := processFlags(enusureFlagSet); err != nil {
if _, ok := err.(*flagIsNotSetError); ok {
return err
}
return testing.PollBreak(errors.Wrap(err, "failed to read flag"))
}
return nil
}, &testing.PollOptions{Timeout: ureadaheadTimeout}); err != nil {
return nil, errors.Wrap(err, "failed to ensure ureadahead started")
}
defer func() {
if err := stopUreadaheadTracing(cleanCtx, cmd); err != nil {
testing.ContextLog(cleanCtx, "Failed to stop ureadahead tracing")
}
}()
if vmEnabled {
// In ARCVM we trace system and vendor images. They are mounted as block devices
// and normally they would not appear in tracing open requests.
// Open images explicitly here in order to ensure tracing buffer has it.
images := []string{"system.raw.img", "vendor.raw.img"}
for _, image := range images {
imagePath := filepath.Join(arcvmRoot, image)
file, err := os.Open(imagePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to touch image %q", imagePath)
}
file.Close()
}
}
// Opt in.
testing.ContextLog(ctx, "Waiting for ARC opt-in flow to complete")
if err := optin.Perform(ctx, cr, tconn); err != nil {
return nil, errors.Wrap(err, "failed to perform opt-in")
}
// Make sure tracing was not stopped in between. This verifies that all tracked flags
// are still set to 1. If it not, that indicates that other component altered it while
// ureadahead tracing session was running.
if err := processFlags(enusureFlagSet); err != nil {
return nil, errors.Wrap(err, "failed to ensure flag is set")
}
if err := stopUreadaheadTracing(ctx, cmd); err != nil {
return nil, err
}
if err := testing.Poll(ctx, func(ctx context.Context) error {
_, err := os.Stat(packPath)
return err
}, &testing.PollOptions{Timeout: ureadaheadTimeout}); err != nil {
return nil, errors.Wrap(err, "failed to ensure pack file exists")
}
testing.ContextLog(ctx, "Ureadahead pack was generated")
var vmPackPath string
if vmEnabled {
// Pull and obtain ARCVM pack from guest OS.
vmPackPath, err = getGuestPack(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain ureadahead pack from ARCVM guest OS")
}
}
response := arcpb.UreadaheadPackResponse{
PackPath: packPath,
VmPackPath: vmPackPath,
LogPath: logPath,
}
return &response, nil
}
// stopUreadaheadTracing stops ureadahead tracing by sending interrupt request and waits until it
// stops. If ureadahead tracing is already stopped this returns no error.
func stopUreadaheadTracing(ctx context.Context, cmd *testexec.Cmd) error {
if cmd.ProcessState != nil {
// Already stopped. Do nothing.
return nil
}
testing.ContextLog(ctx, "Sending interrupt signal to ureadahead tracing process")
if err := cmd.Process.Signal(os.Interrupt); err != nil {
return errors.Wrap(err, "failed to send interrupt signal to ureadahead tracing")
}
if err := cmd.Wait(); err != nil {
return errors.Wrap(err, "failed to wait ureadahead tracing done")
}
return nil
}
// getGuestPack pulls ureadahead initial pack for requested Chrome login mode from guest OS.
func getGuestPack(ctx context.Context) (string, error) {
const (
ureadaheadDataDir = "/var/lib/ureadahead"
arcvmPackName = "arcvm.var.lib.ureadahead.pack"
ureadaheadStopTimeout = 50 * time.Second
ureadaheadStopInterval = 5 * time.Second
ureadaheadFileStatTimeout = 90 * time.Second
ureadaheadFileStatInterval = 15 * time.Second
)
packPath := filepath.Join(ureadaheadDataDir, arcvmPackName)
if err := os.Remove(packPath); err != nil && !os.IsNotExist(err) {
return "", errors.Wrapf(err, "failed to clean up %s on the host", packPath)
}
outdir, ok := testing.ContextOutDir(ctx)
if !ok {
return "", errors.New("failed to get name of the output directory")
}
// Connect to ARCVM instance.
a, err := arc.New(ctx, outdir)
if err != nil {
return "", errors.Wrap(err, "failed to connect ARCVM")
}
defer a.Close(ctx)
// Confirm ureadahead_generate service has stopped.
if err := testing.Poll(ctx, func(ctx context.Context) error {
if value, err := a.GetProp(ctx, "init.svc.ureadahead_generate"); err != nil {
return testing.PollBreak(err)
} else if value != "stopped" {
return errors.New("ureadahead is not yet stopped")
}
return nil
}, &testing.PollOptions{
Timeout: ureadaheadStopTimeout,
Interval: ureadaheadStopInterval,
}); err != nil {
return "", errors.Wrap(err, "failed to wait for ureadahead to stop")
}
// Verify ureadahead exited which is triggered by opt-in completion.
if value, err := a.GetProp(ctx, "dev.arc.ureadahead.exit"); err != nil || value != "1" {
return "", errors.Wrap(err, "failed to verify ureadahead to exited")
}
// Check for existence of newly generated pack file on guest side and get size.
var sizeKB int64
srcPath := filepath.Join(ureadaheadDataDir, "pack")
if err := testing.Poll(ctx, func(ctx context.Context) error {
sizeBytes, err := a.FileSize(ctx, srcPath)
if err == nil {
sizeKB = int64(sizeBytes / 1024)
}
return err
}, &testing.PollOptions{
Timeout: ureadaheadFileStatTimeout,
Interval: ureadaheadFileStatInterval,
}); err != nil {
return "", errors.Wrap(err, "failed to ensure pack file exists")
}
testing.ContextLogf(ctx, "Found ureadahead pack file with size %vkB in ARCVM", sizeKB)
if err := a.PullFile(ctx, srcPath, packPath); err != nil {
return "", errors.Wrapf(err, "failed to pull %s from ARCVM", srcPath)
}
return packPath, nil
}