blob: 4f94b7b0192c7ed94ac9ff203b9991daffa37e07 [file] [log] [blame]
/*
* Copyright (C) 2020 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.targetprep;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.host.IHostOptions;
import com.android.tradefed.host.IHostOptions.PermitLimitType;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.ZipUtil2;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* A target preparer that flash the device with android common kernel generic image. Please see
* https://source.android.com/devices/architecture/kernel/android-common for details.
*/
@OptionClass(alias = "gki-device-flash-preparer")
public class GkiDeviceFlashPreparer extends BaseTargetPreparer {
private static final String MKBOOTIMG = "mkbootimg";
private static final String OTATOOLS_ZIP = "otatools.zip";
private static final String KERNEL_IMAGE = "Image.gz";
// Wait time for device state to stablize in millisecond
private static final int STATE_STABLIZATION_WAIT_TIME = 60000;
@Option(
name = "device-boot-time",
description = "max time to wait for device to boot. Set as 5 minutes by default",
isTimeVal = true)
private long mDeviceBootTime = 5 * 60 * 1000;
@Option(
name = "gki-boot-image-name",
description = "The file name in BuildInfo that provides GKI boot image.")
private String mGkiBootImageName = "gki_boot.img";
@Option(
name = "ramdisk-image-name",
description = "The file name in BuildInfo that provides ramdisk image.")
private String mRamdiskImageName = "ramdisk.img";
@Option(
name = "vendor-boot-image-name",
description = "The file name in BuildInfo that provides vendor boot image.")
private String mVendorBootImageName = "vendor_boot.img";
@Option(
name = "dtbo-image-name",
description = "The file name in BuildInfo that provides dtbo image.")
private String mDtboImageName = "dtbo.img";
@Option(
name = "boot-image-file-name",
description =
"The boot image file name to search for if gki-boot-image-name in "
+ "BuildInfo is a zip file or directory, for example boot-5.4-gz.img.")
private String mBootImageFileName = "boot(.*).img";
@Option(
name = "vendor-boot-image-file-name",
description =
"The vendor boot image file name to search for if vendor-boot-image-name in "
+ "BuildInfo is a zip file or directory, for example vendor_boot.img.")
private String mVendorBootImageFileName = "vendor_boot.img";
@Option(
name = "dtbo-image-file-name",
description =
"The dtbo image file name to search for if dtbo-image-name in "
+ "BuildInfo is a zip file or directory, for example dtbo.img.")
private String mDtboImageFileName = "dtbo.img";
@Option(
name = "post-reboot-device-into-user-space",
description = "whether to boot the device in user space after flash.")
private boolean mPostRebootDeviceIntoUserSpace = true;
@Option(
name = "wipe-device-after-gki-flash",
description = "Whether to wipe device after GKI boot image flash.")
private boolean mShouldWipeDevice = true;
@Option(
name = "boot-header-version",
description = "The version of the boot.img header. Set to 4 by default.")
private int mBootHeaderVersion = 4;
private File mBootImg = null;
/** {@inheritDoc} */
@Override
public void setUp(TestInformation testInfo)
throws TargetSetupError, BuildError, DeviceNotAvailableException {
ITestDevice device = testInfo.getDevice();
IBuildInfo buildInfo = testInfo.getBuildInfo();
File tmpDir = null;
try {
tmpDir = FileUtil.createTempDir("gki_preparer");
validateGkiBootImg(device, buildInfo, tmpDir);
flashGki(device, buildInfo, tmpDir);
} catch (IOException ioe) {
throw new TargetSetupError(ioe.getMessage(), ioe, device.getDeviceDescriptor());
} finally {
FileUtil.recursiveDelete(tmpDir);
}
if (!mPostRebootDeviceIntoUserSpace) {
return;
}
// Wait some time after flashing the image.
getRunUtil().sleep(STATE_STABLIZATION_WAIT_TIME);
device.rebootUntilOnline();
if (device.enableAdbRoot()) {
device.setDate(null);
}
try {
device.setRecoveryMode(RecoveryMode.AVAILABLE);
device.waitForDeviceAvailable(mDeviceBootTime);
} catch (DeviceUnresponsiveException e) {
// assume this is a build problem
throw new DeviceFailedToBootError(
String.format(
"Device %s did not become available after flashing GKI. Exception: %s",
device.getSerialNumber(), e),
device.getDeviceDescriptor(),
DeviceErrorIdentifier.ERROR_AFTER_FLASHING);
}
device.postBootSetup();
CLog.i("Device update completed on %s", device.getDeviceDescriptor());
}
/**
* Get a reference to the {@link IHostOptions}
*
* @return the {@link IHostOptions} to use
*/
@VisibleForTesting
protected IHostOptions getHostOptions() {
return GlobalConfiguration.getInstance().getHostOptions();
}
/**
* Get the {@link IRunUtil} instance to use.
*
* @return the {@link IRunUtil} to use
*/
@VisibleForTesting
protected IRunUtil getRunUtil() {
return RunUtil.getDefault();
}
/**
* Flash GKI images.
*
* @param device the {@link ITestDevice}
* @param buildInfo the {@link IBuildInfo} the build info
* @param tmpDir the temporary directory {@link File}
* @throws TargetSetupError, DeviceNotAvailableException, IOException
*/
private void flashGki(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
throws TargetSetupError, DeviceNotAvailableException {
device.rebootIntoBootloader();
long start = System.currentTimeMillis();
getHostOptions().takePermit(PermitLimitType.CONCURRENT_FLASHER);
CLog.v(
"Flashing permit obtained after %ds",
TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
// Don't allow interruptions during flashing operations.
getRunUtil().allowInterrupt(false);
try {
if (buildInfo.getFile(mVendorBootImageName) != null) {
File vendorBootImg =
getRequestedFile(
device,
mVendorBootImageFileName,
buildInfo.getFile(mVendorBootImageName),
tmpDir);
executeFastbootCmd(device, "flash", "vendor_boot", vendorBootImg.getAbsolutePath());
}
if (buildInfo.getFile(mDtboImageName) != null) {
File dtboImg =
getRequestedFile(
device,
mDtboImageFileName,
buildInfo.getFile(mDtboImageName),
tmpDir);
executeFastbootCmd(device, "flash", "dtbo", dtboImg.getAbsolutePath());
}
executeFastbootCmd(device, "flash", "boot", mBootImg.getAbsolutePath());
if (mShouldWipeDevice) {
executeFastbootCmd(device, "-w");
}
} finally {
getHostOptions().returnPermit(PermitLimitType.CONCURRENT_FLASHER);
// Allow interruption at the end no matter what.
getRunUtil().allowInterrupt(true);
CLog.v(
"Flashing permit returned after %ds",
TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
}
}
/**
* Validate GKI boot image is expected. (Obsoleted. Please call with tmpDir provided)
*
* @param device the {@link ITestDevice}
* @param buildInfo the {@link IBuildInfo} the build info
* @throws TargetSetupError if there is no valid gki boot.img
*/
public void validateGkiBootImg(ITestDevice device, IBuildInfo buildInfo)
throws TargetSetupError {
throw new TargetSetupError(
"Obsoleted. Please use validateGkiBootImg(ITestDevice, IBuildInfo, File)",
device.getDeviceDescriptor());
}
/**
* Validate GKI boot image is expected. Throw exception if there is no valid boot.img.
*
* @param device the {@link ITestDevice}
* @param buildInfo the {@link IBuildInfo} the build info
* @param tmpDir the temporary directory {@link File}
* @throws TargetSetupError if there is no valid gki boot.img
*/
@VisibleForTesting
protected void validateGkiBootImg(ITestDevice device, IBuildInfo buildInfo, File tmpDir)
throws TargetSetupError {
if (buildInfo.getFile(mGkiBootImageName) != null && mBootImageFileName != null) {
mBootImg =
getRequestedFile(
device,
mBootImageFileName,
buildInfo.getFile(mGkiBootImageName),
tmpDir);
return;
}
if (buildInfo.getFile(KERNEL_IMAGE) == null) {
throw new TargetSetupError(
KERNEL_IMAGE + " is not provided. Can not generate GKI boot.img.",
device.getDeviceDescriptor());
}
if (buildInfo.getFile(mRamdiskImageName) == null) {
throw new TargetSetupError(
mRamdiskImageName + " is not provided. Can not generate GKI boot.img.",
device.getDeviceDescriptor());
}
if (buildInfo.getFile(OTATOOLS_ZIP) == null) {
throw new TargetSetupError(
OTATOOLS_ZIP + " is not provided. Can not generate GKI boot.img.",
device.getDeviceDescriptor());
}
try {
File mkbootimg =
getRequestedFile(device, MKBOOTIMG, buildInfo.getFile(OTATOOLS_ZIP), tmpDir);
mBootImg = FileUtil.createTempFile("boot", ".img", tmpDir);
String cmd =
String.format(
"%s --kernel %s --header_version %d --base 0x00000000 "
+ "--pagesize 4096 --ramdisk %s -o %s",
mkbootimg.getAbsolutePath(),
buildInfo.getFile(KERNEL_IMAGE),
mBootHeaderVersion,
buildInfo.getFile(mRamdiskImageName),
mBootImg.getAbsolutePath());
executeHostCommand(device, cmd);
CLog.i("The GKI boot.img is of size %d", mBootImg.length());
if (mBootImg.length() == 0) {
throw new TargetSetupError(
"The mkbootimg tool didn't generate a valid boot.img.",
device.getDeviceDescriptor());
}
} catch (IOException e) {
throw new TargetSetupError(
"Fail to generate GKI boot.img.", e, device.getDeviceDescriptor());
}
}
/**
* Helper method to execute host command.
*
* @param device the {@link ITestDevice}
* @param command the command string
* @throws TargetSetupError, DeviceNotAvailableException
*/
private void executeHostCommand(ITestDevice device, final String command)
throws TargetSetupError {
final CommandResult result = getRunUtil().runTimedCmd(300000L, command.split("\\s+"));
switch (result.getStatus()) {
case SUCCESS:
CLog.i(
"Command %s finished successfully, stdout = [%s].",
command, result.getStdout());
break;
case FAILED:
throw new TargetSetupError(
String.format(
"Command %s failed, stdout = [%s], stderr = [%s].",
command, result.getStdout(), result.getStderr()),
device.getDeviceDescriptor());
case TIMED_OUT:
throw new TargetSetupError(
String.format("Command %s timed out.", command),
device.getDeviceDescriptor());
case EXCEPTION:
throw new TargetSetupError(
String.format("Exception occurred when running command %s.", command),
device.getDeviceDescriptor());
}
}
/**
* Get the requested file from the source file (zip or folder) by requested file name.
*
* <p>The provided source file can be a zip file. The method will unzip it to tempary directory
* and find the requested file by the provided file name.
*
* <p>The provided source file can be a file folder. The method will find the requestd file by
* the provided file name.
*
* @param device the {@link ITestDevice}
* @param requestedFileName the requeste file name String
* @param sourceFile the source file
* @return the file that is specified by the requested file name
* @throws TargetSetupError
*/
private File getRequestedFile(
ITestDevice device, String requestedFileName, File sourceFile, File tmpDir)
throws TargetSetupError {
File requestedFile = null;
if (sourceFile.getName().endsWith(".zip")) {
try {
File destDir =
FileUtil.createTempDir(FileUtil.getBaseName(sourceFile.getName()), tmpDir);
ZipUtil2.extractZip(sourceFile, destDir);
requestedFile = FileUtil.findFile(destDir, requestedFileName);
} catch (IOException e) {
throw new TargetSetupError(
String.format("Fail to get %s from %s", requestedFileName, sourceFile),
e,
device.getDeviceDescriptor());
}
} else if (sourceFile.isDirectory()) {
requestedFile = FileUtil.findFile(sourceFile, requestedFileName);
} else {
requestedFile = sourceFile;
}
if (requestedFile == null || !requestedFile.exists()) {
throw new TargetSetupError(
String.format(
"Requested file with file_name %s does not exist in provided %s.",
requestedFileName, sourceFile),
device.getDeviceDescriptor());
}
return requestedFile;
}
/**
* Helper method to execute a fastboot command.
*
* @param device the {@link ITestDevice} to execute command on
* @param cmdArgs the arguments to provide to fastboot
* @return String the stderr output from command if non-empty. Otherwise returns the stdout Some
* fastboot commands are weird in that they dump output to stderr on success case
* @throws DeviceNotAvailableException if device is not available
* @throws TargetSetupError if fastboot command fails
*/
private String executeFastbootCmd(ITestDevice device, String... cmdArgs)
throws DeviceNotAvailableException, TargetSetupError {
CLog.i(
"Execute fastboot command %s on %s",
Arrays.toString(cmdArgs), device.getSerialNumber());
CommandResult result = device.executeLongFastbootCommand(cmdArgs);
CLog.v("fastboot stdout: " + result.getStdout());
CLog.v("fastboot stderr: " + result.getStderr());
CommandStatus cmdStatus = result.getStatus();
// fastboot command line output is in stderr even for successful run
if (result.getStderr().contains("FAILED")) {
// if output contains "FAILED", just override to failure
cmdStatus = CommandStatus.FAILED;
}
if (cmdStatus != CommandStatus.SUCCESS) {
throw new TargetSetupError(
String.format(
"fastboot command %s failed in device %s. stdout: %s, stderr: %s",
Arrays.toString(cmdArgs),
device.getSerialNumber(),
result.getStdout(),
result.getStderr()),
device.getDeviceDescriptor());
}
if (result.getStderr().length() > 0) {
return result.getStderr();
} else {
return result.getStdout();
}
}
}