blob: 0fd9b3226f872ccd3725334526aaff3bc200c0b7 [file] [log] [blame]
// Copyright 2021 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 memoryuser
import (
"bufio"
"context"
"fmt"
"path"
"strings"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/local/memory"
"chromiumos/tast/local/vm"
"chromiumos/tast/testing"
)
// CrostiniLifecycleUnit launches a process in Crostini which allocates memory.
type CrostiniLifecycleUnit struct {
cont *vm.Container
id int
allocateMiB int64
ratio float64
limit memory.Limit
cmd *testexec.Cmd
}
// Run starts the CrostiniLifecycleUnit process, and uses it to allocate memory.
func (t *CrostiniLifecycleUnit) Run(ctx context.Context) error {
if t.cmd != nil {
return errors.New("lifecycle already running")
}
containerPath, err := containerCrostiniLifecyclePath(ctx, t.cont)
if err != nil {
return errors.Wrap(err, "failed to get crostini lifecycle container path")
}
t.cmd = t.cont.Command(
ctx,
containerPath,
"0",
"1000",
fmt.Sprint(t.allocateMiB),
fmt.Sprint(t.ratio),
)
stdoutPipe, err := t.cmd.StdoutPipe()
if err != nil {
return errors.Wrap(err, "failed to get lifecycle stdout")
}
if err := t.cmd.Start(); err != nil {
return errors.Wrap(err, "failed to start Crostini lifecycle")
}
// Call Wait so that t.cmd.ProcessState is set as soon as it terminates, so
// StillAlive can know if it's still alive.
go func() {
t.cmd.Wait()
}()
// Make sure the output is as expected, and wait until we are done
// allocating.
stdout := bufio.NewReader(stdoutPipe)
if statusString, err := stdout.ReadString('\n'); err != nil {
return errors.Wrap(err, "failed to read status from Crostini lifecycle")
} else if !strings.HasPrefix(statusString, "allocating ") {
return errors.Errorf("failed to read status line, exptected \"allocating ...\", got %q", statusString)
}
if doneString, err := stdout.ReadString('\n'); err != nil {
return errors.Wrap(err, "failed to read done from Crostini lifecycle")
} else if doneString != "done\n" {
return errors.Errorf("failed to read done line, exptected \"done\\n\", got %q", doneString)
}
return nil
}
// Close kills the CrostiniLifecycleUnit process.
func (t *CrostiniLifecycleUnit) Close(ctx context.Context) error {
if t.cmd == nil {
return nil
}
if err := t.cmd.Kill(); err != nil {
return errors.Wrap(err, "failed to kill Crostini lifecycle unit")
}
if err := t.cmd.Wait(testexec.DumpLogOnError); err != nil {
return errors.Wrap(err, "failed to wait for Crostini lifecycle unit after kill")
}
return nil
}
// StillAlive returns false if the process has exited, or if it never started.
func (t *CrostiniLifecycleUnit) StillAlive(ctx context.Context) bool {
if t.cmd == nil {
return false
}
// ProcessState is set within the Wait() call in the goroutine started just
// after we started the lifecycle process.
return t.cmd.ProcessState == nil
}
// NewCrostiniLifecycleUnit creates a helper to allocate memory inside Crostini.
// id - A debug ID for logging.
// allocateBytes - mow much memory to allocate.
// ratio - the compression ratio of allocated memory.
// limit - if not nil, wait for Limit after allocation.
func NewCrostiniLifecycleUnit(cont *vm.Container, id int, allocateMiB int64, ratio float64, limit memory.Limit) *CrostiniLifecycleUnit {
var cmd *testexec.Cmd
return &CrostiniLifecycleUnit{cont, id, allocateMiB, ratio, limit, cmd}
}
// FillCrostiniMemory installs, launches, and allocates in crostini lifecycle
// processes until one is killed, filling up memory in Crostini.
func FillCrostiniMemory(ctx context.Context, cont *vm.Container, unitMiB int64, ratio float64) (func(context.Context) error, error) {
var units []*CrostiniLifecycleUnit
cleanup := func(ctx context.Context) error {
var res error
for _, unit := range units {
if err := unit.Close(ctx); err != nil {
testing.ContextLogf(ctx, "Failed to close CrostiniLifecycleUnit: %s", err)
if res == nil {
res = err
}
}
}
if err := UninstallCrostiniLifecycle(ctx, cont); err != nil {
testing.ContextLog(ctx, "Failed to clean up CrostiniLifecycleUnit: ", err)
if res == nil {
res = err
}
}
return res
}
if err := InstallCrostiniLifecycle(ctx, cont); err != nil {
return cleanup, err
}
for i := 0; ; i++ {
unit := NewCrostiniLifecycleUnit(cont, i, unitMiB, ratio, nil)
units = append(units, unit)
if err := unit.Run(ctx); err != nil {
return cleanup, errors.Wrapf(err, "failed to run CrostiniLifecycleUnit %d", unit.id)
}
for _, unit := range units {
if !unit.StillAlive(ctx) {
testing.ContextLogf(ctx, "FillChromeOSMemory started %d units of %d MiB before first kill", len(units), unitMiB)
return cleanup, nil
}
}
}
}
// CrostiniLifecycleTask wraps CrostiniLifecycleTask to conform to the
// MemoryTask and KillableTask interfaces.
type CrostiniLifecycleTask struct{ CrostiniLifecycleUnit }
// CrostiniLifecycleTask is a MemoryTask.
var _ MemoryTask = (*CrostiniLifecycleTask)(nil)
// CrostiniLifecycleTask is a KillableTask.
var _ KillableTask = (*CrostiniLifecycleTask)(nil)
// Run starts the CrostiniLifecycleUnit process, and uses it to allocate memory.
func (t *CrostiniLifecycleTask) Run(ctx context.Context, testEnv *TestEnv) error {
return t.CrostiniLifecycleUnit.Run(ctx)
}
// Close kills the CrostiniLifecycleUnit process.
func (t *CrostiniLifecycleTask) Close(ctx context.Context, testEnv *TestEnv) {
t.CrostiniLifecycleUnit.Close(ctx)
}
// StillAlive returns false if the process has exited, or if it never started.
func (t *CrostiniLifecycleTask) StillAlive(ctx context.Context, testEnv *TestEnv) bool {
return t.CrostiniLifecycleUnit.StillAlive(ctx)
}
// String returns a friendly name for the task.
func (t *CrostiniLifecycleTask) String() string {
return fmt.Sprintf("Crostini Lifecycle %d", t.id)
}
// NeedVM returns false because, while we do need a Crostini VM, we don't want a
// new one created ust for this MemoryTask.
func (t *CrostiniLifecycleTask) NeedVM() bool {
return false
}
// NewCrostiniLifecycleTask creates a helper to allocate memory inside Crostini.
// id - A debug ID for logging.
// allocateBytes - mow much memory to allocate.
// ratio - the compression ratio of allocated memory.
// limit - if not nil, wait for Limit after allocation.
func NewCrostiniLifecycleTask(cont *vm.Container, id int, allocateMiB int64, ratio float64, limit memory.Limit) *CrostiniLifecycleTask {
return &CrostiniLifecycleTask{*NewCrostiniLifecycleUnit(cont, id, allocateMiB, ratio, limit)}
}
// NB: this name should be under 15 characters so that killall is able to be
// specific.
const crostiniLifecycleName = "lifecycle"
const crostiniLifecyclePath = "/usr/local/libexec/tast/helpers/local/cros/multivm.Lifecycle.crostini"
func containerCrostiniLifecyclePath(ctx context.Context, cont *vm.Container) (string, error) {
username, err := cont.GetUsername(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to get Crostini user name")
}
return path.Join("/home", username, crostiniLifecycleName), nil
}
// InstallCrostiniLifecycle installs the binary needed to run
// CrostiniLifecycleTask.
func InstallCrostiniLifecycle(ctx context.Context, cont *vm.Container) error {
containerPath, err := containerCrostiniLifecyclePath(ctx, cont)
if err != nil {
return errors.Wrap(err, "failed to get crostini lifecycle container path")
}
if err := cont.PushFile(ctx, crostiniLifecyclePath, containerPath); err != nil {
return errors.Wrap(err, "failed to push lifecycle binary to Crostini")
}
if err := cont.Command(ctx, "chmod", "755", containerPath).Run(); err != nil {
return errors.Wrap(err, "failed to make crostini lifecycle binary executable")
}
return nil
}
// UninstallCrostiniLifecycle deletes the binary used to run
// CrostiniLifecycleTask, and kills any processes that might still be running.
func UninstallCrostiniLifecycle(ctx context.Context, cont *vm.Container) error {
if err := cont.Command(ctx, "killall", crostiniLifecycleName).Run(testexec.DumpLogOnError); err != nil {
// Don't return an error, because killall can fail if no processes were running.
testing.ContextLog(ctx, "Failed to kill running crostini lifecycle units: ", err)
}
containerPath, err := containerCrostiniLifecyclePath(ctx, cont)
if err != nil {
return errors.Wrap(err, "failed to get crostini lifecycle container path")
}
if err := cont.Command(ctx, "rm", containerPath).Run(testexec.DumpLogOnError); err != nil {
return errors.Wrapf(err, "failed to rm %q", containerPath)
}
return nil
}