blob: 851f57b7c168412d33562b61207318fad8425b99 [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 croshealthd
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/shutil"
"chromiumos/tast/testing"
)
// List of cros_healthd diagnostic routines
const (
RoutineBatteryCapacity string = "battery_capacity"
RoutineBatteryHealth = "battery_health"
RoutineURandom = "urandom"
RoutineSmartctlCheck = "smartctl_check"
RoutineACPower = "ac_power"
RoutineCPUCache = "cpu_cache"
RoutineCPUStress = "cpu_stress"
RoutineFloatingPointAccurary = "floating_point_accuracy"
RoutineNVMEWearLevel = "nvme_wear_level"
RoutineNVMESelfTest = "nvme_self_test"
RoutineDiskRead = "disk_read"
RoutinePrimeSearch = "prime_search"
RoutineBatteryDischarge = "battery_discharge"
RoutineBatteryCharge = "battery_charge"
RoutineMemory = "memory"
RoutineLanConnectivity = "lan_connectivity"
RoutineSignalStrength = "signal_strength"
RoutineGatewayCanBePinged = "gateway_can_be_pinged"
RoutineHasSecureWifiConnection = "has_secure_wifi_connection"
RoutineDNSResolverPresent = "dns_resolver_present"
RoutineDNSLatency = "dns_latency"
RoutineDNSResolution = "dns_resolution"
RoutineCaptivePortal = "captive_portal"
RoutineHTTPFirewall = "http_firewall"
RoutineHTTPSFirewall = "https_firewall"
RoutineHTTPSLatency = "https_latency"
)
// List of possible routine statuses
const (
StatusReady string = "Ready"
StatusRunning = "Running"
StatusWaiting = "Waiting"
StatusPassed = "Passed"
StatusFailed = "Failed"
StatusError = "Error"
StatusCancelled = "Cancelled"
StatusFailedToStart = "Failed to start"
StatusRemoved = "Removed"
StatusCancelling = "Cancelling"
StatusUnsupported = "Unsupported"
StatusNotRun = "Not run"
)
// RoutineResult contains the the progress of the routine as a percentage and
// the routine status.
type RoutineResult struct {
Progress int
Status string
}
// RoutineParams are different configuration options for running a diagnostic
// routine.
type RoutineParams struct {
Routine string // The name of the routine to run
Cancel bool // Boolean flag to cancel the routine
}
// RunDiagRoutine runs the specified routine based on `params`. Returns a
// RoutineResult on success or an error.
func RunDiagRoutine(ctx context.Context, params RoutineParams) (*RoutineResult, error) {
diagParams := []string{"--action=run_routine", fmt.Sprintf("--routine=%s", params.Routine)}
if params.Cancel {
diagParams = append(diagParams, "--force_cancel_at_percent=5")
}
output, err := runDiag(ctx, diagParams)
if err != nil {
return nil, err
}
return parseOutput(ctx, output)
}
// GetDiagRoutines returns a list of valid routines for the device on success,
// or an error.
func GetDiagRoutines(ctx context.Context) ([]string, error) {
output, err := runDiag(ctx, []string{"--action=get_routines"})
if err != nil {
return []string{}, err
}
re := regexp.MustCompile(`Available routine: (.*)`)
var routines []string
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
match := re.FindStringSubmatch(line)
if match != nil {
routines = append(routines, match[1])
}
}
return routines, nil
}
// runDiag is a helper function that runs the cros_healthd diag command and
// returns the raw stdout on success, or an error.
func runDiag(ctx context.Context, args []string) (string, error) {
args = append([]string{"diag"}, args...)
cmd := testexec.CommandContext(ctx, "cros-health-tool", args...)
testing.ContextLogf(ctx, "Running %q", shutil.EscapeSlice(cmd.Args))
out, err := cmd.Output()
if err != nil {
cmd.DumpLog(ctx)
return "", errors.Wrapf(err, "failed to run %q", shutil.EscapeSlice(cmd.Args))
}
return string(out), nil
}
// parseOutput is a helper function that takes the `raw` output from running a
// diagnostic routine and returns a RoutineResult on success, or an error.
func parseOutput(ctx context.Context, raw string) (*RoutineResult, error) {
status := ""
progress := 0
re := regexp.MustCompile(`([^:]+): (.*)`)
testing.ContextLog(ctx, raw)
for _, line := range strings.Split(strings.TrimSpace(raw), "\n") {
match := re.FindStringSubmatch(line)
if match == nil {
continue
}
key := match[1]
value := match[2]
switch key {
case "Status":
status = value
case "Progress":
// Look for just the last progress value. Diag prints a single
// line for the progress, which may contain carriage returns.
// The line will be formatted as follows, where # is any int:
// #\rProgress: #\rProgress: #\rProgress: # ... \rProgress: #
// Slicing value after the last space should give us the final
// progress percent.
percent := value[strings.LastIndex(value, " ")+1:]
i, err := strconv.Atoi(percent)
if err != nil {
testing.ContextLogf(ctx, "Failed to parse progress status: %q", value)
return nil, errors.Wrapf(err, "Unable to parse %q value %q as int: %v", key, percent, err)
}
progress = i
}
}
return &RoutineResult{progress, status}, nil
}