blob: 9414ae736368430c7a30ebb7b5dacf661880fe58 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.device.cloud;
import com.android.annotations.VisibleForTesting;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.GCSFileDownloader;
import com.android.tradefed.util.Pair;
import java.io.File;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** Utility to interact with Oxygen service. */
public class OxygenUtil {
private GCSFileDownloader mDownloader;
// TODO: Support more type of log data types
private static final Map<Pattern, LogDataType> REMOTE_LOG_NAME_PATTERN_TO_TYPE_MAP =
Stream.of(
new AbstractMap.SimpleEntry<>(
Pattern.compile("^logcat.*"), LogDataType.LOGCAT),
new AbstractMap.SimpleEntry<>(
Pattern.compile(".*kernel.*"), LogDataType.KERNEL_LOG),
new AbstractMap.SimpleEntry<>(
Pattern.compile(".*bugreport.*zip"), LogDataType.BUGREPORTZ),
new AbstractMap.SimpleEntry<>(
Pattern.compile(".*bugreport.*txt"), LogDataType.BUGREPORT),
new AbstractMap.SimpleEntry<>(
Pattern.compile(".*tombstones-zip.*zip"),
LogDataType.TOMBSTONEZ))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
private static final Map<Pattern, Pair<Pattern, String>>
REMOTE_LOG_NAME_PATTERN_TO_ERROR_SIGNATURE_MAP =
Stream.of(
new AbstractMap.SimpleEntry<>(
Pattern.compile("^launcher\\.log.*"),
Pair.create(
Pattern.compile(".*Address already in use.*"),
"launch_cvd_port_collision")),
new AbstractMap.SimpleEntry<>(
Pattern.compile("^launcher\\.log.*"),
Pair.create(
Pattern.compile(".*vcpu hw run failure: 0x7.*"),
"crosvm_vcpu_hw_run_failure_7")),
new AbstractMap.SimpleEntry<>(
Pattern.compile("^launcher\\.log.*"),
Pair.create(
Pattern.compile(
".*Unable to connect to vsock"
+ " server.*"),
"unable_to_connect_to_vsock_server")))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
/** Default constructor of OxygenUtil */
public OxygenUtil() {
mDownloader = new GCSFileDownloader(true);
}
/**
* Constructor of OxygenUtil
*
* @param downloader {@link GCSFileDownloader} to download file from GCS.
*/
@VisibleForTesting
OxygenUtil(GCSFileDownloader downloader) {
mDownloader = downloader;
}
/**
* Download error logs from GCS when Oxygen failed to launch a virtual device.
*
* @param error TargetSetupError raised when leasing device through Oxygen service.
* @param logger The {@link ITestLogger} where to log the file
*/
public void downloadLaunchFailureLogs(TargetSetupError error, ITestLogger logger) {
String errorMessage = error.getMessage();
if (error.getCause() != null) {
// Also include the message from the internal cause.
errorMessage = String.format("%s %s", errorMessage, error.getCause().getMessage());
}
CLog.d("Downloading device launch failure logs based on error message: %s", errorMessage);
Pattern pattern = Pattern.compile(".*/storage/browser/(.*)\\?&project=.*", Pattern.DOTALL);
Matcher matcher = pattern.matcher(errorMessage);
if (!matcher.find()) {
CLog.d("Error message doesn't contain expected GCS link.");
return;
}
String remoteFilePath = "gs://" + matcher.group(1);
File localDir;
try {
localDir = mDownloader.downloadFile(remoteFilePath);
Set<String> files = FileUtil.findFiles(localDir, ".*");
for (String f : files) {
File file = new File(f);
if (file.isDirectory()) {
continue;
}
CLog.d("Logging %s", f);
try (FileInputStreamSource data = new FileInputStreamSource(file)) {
String logFileName =
"oxygen_"
+ localDir.toPath()
.relativize(file.toPath())
.toString()
.replace(File.separatorChar, '_');
LogDataType logDataType = getDefaultLogType(logFileName);
if (logDataType == LogDataType.UNKNOWN) {
// Default log type to be CUTTLEFISH_LOG to avoid compression.
logDataType = LogDataType.CUTTLEFISH_LOG;
}
logger.testLog(logFileName, logDataType, data);
}
}
} catch (Exception e) {
CLog.e("Failed to download Oxygen log from %s", remoteFilePath);
CLog.e(e);
}
}
/**
* Determine a log file's log data type based on its name.
*
* @param logFileName The remote log file's name.
* @return A {@link LogDataType} which the log file associates with. Will return the type
* UNKNOWN if unable to determine the log data type based on its name.
*/
public static LogDataType getDefaultLogType(String logFileName) {
for (Map.Entry<Pattern, LogDataType> entry :
REMOTE_LOG_NAME_PATTERN_TO_TYPE_MAP.entrySet()) {
Matcher matcher = entry.getKey().matcher(logFileName);
if (matcher.find()) {
return entry.getValue();
}
}
CLog.d(
String.format(
"Unable to determine log type of the remote log file %s, log type is"
+ " UNKNOWN",
logFileName));
return LogDataType.UNKNOWN;
}
/**
* Collect error signatures from logs.
*
* @param logDir directory of logs pulled from remote host.
*/
public static List<String> collectErrorSignatures(File logDir) {
CLog.d("Collect error signature from logs under: %s.", logDir);
List<String> signatures = new ArrayList<>();
try {
Set<String> files = FileUtil.findFiles(logDir, ".*");
for (String f : files) {
File file = new File(f);
if (file.isDirectory()) {
continue;
}
String fileName = file.getName();
List<Pair<Pattern, String>> pairs = new ArrayList<>();
for (Map.Entry<Pattern, Pair<Pattern, String>> entry :
REMOTE_LOG_NAME_PATTERN_TO_ERROR_SIGNATURE_MAP.entrySet()) {
Matcher matcher = entry.getKey().matcher(fileName);
if (matcher.find()) {
pairs.add(entry.getValue());
}
}
if (pairs.size() == 0) {
continue;
}
try (Scanner scanner = new Scanner(file)) {
List<Pair<Pattern, String>> pairsToRemove = new ArrayList<>();
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
for (Pair<Pattern, String> pair : pairs) {
if (pair.first.matcher(line).find()) {
pairsToRemove.add(pair);
signatures.add(pair.second);
}
}
if (pairsToRemove.size() > 0) {
pairs.removeAll(pairsToRemove);
if (pairs.size() == 0) {
break;
}
}
}
}
}
} catch (Exception e) {
CLog.e("Failed to collect error signature.");
CLog.e(e);
}
Collections.sort(signatures);
return signatures;
}
/**
* Collect device launcher metrics from vdl_stdout.
*
* @param logDir directory of logs pulled from remote host.
*/
public static long[] collectDeviceLaunchMetrics(File logDir) {
CLog.d("Collect device launcher metrics from logs under: %s.", logDir);
long[] metrics = {-1, -1};
try {
Set<String> files = FileUtil.findFiles(logDir, "^vdl_stdout\\.txt.*");
if (files.size() == 0) {
CLog.d("There is no vdl_stdout.txt found.");
return metrics;
}
File vdlStdout = new File(files.iterator().next());
double cuttlefishCommon = 0;
double launchDevice = 0;
double mainstart = 0;
Pattern cuttlefishCommonPatteren =
Pattern.compile(".*\\|\\s*(\\d+\\.\\d+)\\s*\\|\\sCuttlefishCommon");
Pattern launchDevicePatteren =
Pattern.compile(".*\\|\\s*(\\d+\\.\\d+)\\s*\\|\\sLaunchDevice");
Pattern mainstartPatteren =
Pattern.compile(".*\\|\\s*(\\d+\\.\\d+)\\s*\\|\\sCuttlefishLauncherMainstart");
try (Scanner scanner = new Scanner(vdlStdout)) {
boolean metricsPending = false;
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if (!metricsPending) {
if (line.indexOf("launch_cvd exited") != -1) {
metricsPending = true;
} else {
continue;
}
}
Matcher matcher;
if (cuttlefishCommon == 0) {
matcher = cuttlefishCommonPatteren.matcher(line);
if (matcher.find()) {
cuttlefishCommon = Double.parseDouble(matcher.group(1));
}
}
if (launchDevice == 0) {
matcher = launchDevicePatteren.matcher(line);
if (matcher.find()) {
launchDevice = Double.parseDouble(matcher.group(1));
}
}
if (mainstart == 0) {
matcher = mainstartPatteren.matcher(line);
if (matcher.find()) {
mainstart = Double.parseDouble(matcher.group(1));
}
}
}
}
if (mainstart > 0) {
metrics[0] = (long) ((mainstart - launchDevice - cuttlefishCommon) * 1000);
metrics[1] = (long) (launchDevice * 1000);
}
} catch (Exception e) {
CLog.e("Failed to parse device launch time from vdl_stdout.txt.");
CLog.e(e);
}
return metrics;
}
}