blob: a07731e6fbd84fb0609764422858670e055f5ea4 [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.microdroid.test.device;
import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assume.assumeNoException;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.os.SystemProperties;
import android.system.virtualmachine.VirtualMachine;
import android.system.virtualmachine.VirtualMachineCallback;
import android.system.virtualmachine.VirtualMachineConfig;
import android.system.virtualmachine.VirtualMachineException;
import android.system.virtualmachine.VirtualMachineManager;
import android.util.Log;
import androidx.annotation.CallSuper;
import androidx.test.core.app.ApplicationProvider;
import com.android.virt.VirtualizationTestHelper;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public abstract class MicrodroidDeviceTestBase {
public static boolean isCuttlefish() {
return VirtualizationTestHelper.isCuttlefish(SystemProperties.get("ro.product.name"));
}
// TODO(b/220920264): remove Inner class; this is a hack to hide virt APEX types
protected static class Inner {
private final boolean mProtectedVm;
private final Context mContext;
private final VirtualMachineManager mVmm;
public Inner(Context context, boolean protectedVm, VirtualMachineManager vmm) {
mProtectedVm = protectedVm;
mVmm = vmm;
mContext = context;
}
public VirtualMachineManager getVirtualMachineManager() {
return mVmm;
}
public Context getContext() {
return mContext;
}
/** Create a new VirtualMachineConfig.Builder with the parameterized protection mode. */
public VirtualMachineConfig.Builder newVmConfigBuilder(String payloadConfigPath) {
return new VirtualMachineConfig.Builder(mContext, payloadConfigPath)
.protectedVm(mProtectedVm);
}
/**
* Creates a new virtual machine, potentially removing an existing virtual machine with
* given name.
*/
public VirtualMachine forceCreateNewVirtualMachine(String name, VirtualMachineConfig config)
throws VirtualMachineException {
VirtualMachine existingVm = mVmm.get(name);
if (existingVm != null) {
existingVm.delete();
}
return mVmm.create(name, config);
}
}
protected Inner mInner;
protected Context getContext() {
return mInner.getContext();
}
public void prepareTestSetup(boolean protectedVm) {
// In case when the virt APEX doesn't exist on the device, classes in the
// android.system.virtualmachine package can't be loaded. Therefore, before using the
// classes, check the existence of a class in the package and skip this test if not exist.
try {
Class.forName("android.system.virtualmachine.VirtualMachineManager");
} catch (ClassNotFoundException e) {
assumeNoException(e);
return;
}
if (protectedVm) {
assume().withMessage("Skip where protected VMs aren't supported")
.that(hypervisor_protected_vm_supported())
.isTrue();
} else {
assume().withMessage("Skip where VMs aren't supported")
.that(hypervisor_vm_supported())
.isTrue();
}
Context context = ApplicationProvider.getApplicationContext();
mInner = new Inner(context, protectedVm, VirtualMachineManager.getInstance(context));
}
// These are inlined from android.sysprop.HypervisorProperties which isn't @SystemApi.
// TODO(b/243642678): Move to using a proper Java API for this.
private boolean hypervisor_vm_supported() {
return SystemProperties.getBoolean("ro.boot.hypervisor.vm.supported", false);
}
private boolean hypervisor_protected_vm_supported() {
return SystemProperties.getBoolean("ro.boot.hypervisor.protected_vm.supported", false);
}
public abstract static class VmEventListener implements VirtualMachineCallback {
private ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
private OptionalLong mVcpuStartedNanoTime = OptionalLong.empty();
private OptionalLong mKernelStartedNanoTime = OptionalLong.empty();
private OptionalLong mInitStartedNanoTime = OptionalLong.empty();
private OptionalLong mPayloadStartedNanoTime = OptionalLong.empty();
private void processBootEvents(String log) {
if (!mVcpuStartedNanoTime.isPresent()) {
mVcpuStartedNanoTime = OptionalLong.of(System.nanoTime());
}
if (log.contains("Starting kernel") && !mKernelStartedNanoTime.isPresent()) {
mKernelStartedNanoTime = OptionalLong.of(System.nanoTime());
}
if (log.contains("Run /init as init process") && !mInitStartedNanoTime.isPresent()) {
mInitStartedNanoTime = OptionalLong.of(System.nanoTime());
}
if (log.contains("microdroid_manager") && log.contains("executing main task")
&& !mPayloadStartedNanoTime.isPresent()) {
mPayloadStartedNanoTime = OptionalLong.of(System.nanoTime());
}
}
private void logVmOutputAndMonitorBootEvents(String tag,
InputStream vmOutputStream,
String name,
boolean monitorEvents) {
new Thread(
() -> {
try {
BufferedReader reader =
new BufferedReader(new InputStreamReader(vmOutputStream));
String line;
while ((line = reader.readLine()) != null
&& !Thread.interrupted()) {
if (monitorEvents) processBootEvents(line);
Log.i(tag, name + ": " + line);
}
} catch (Exception e) {
Log.w(tag, name, e);
}
}).start();
}
private void logVmOutputAndMonitorBootEvents(String tag,
InputStream vmOutputStream,
String name) {
logVmOutputAndMonitorBootEvents(tag, vmOutputStream, name, true);
}
/** Copy output from the VM to logcat. This is helpful when things go wrong. */
protected void logVmOutput(String tag, InputStream vmOutputStream, String name) {
logVmOutputAndMonitorBootEvents(tag, vmOutputStream, name, false);
}
public void runToFinish(String logTag, VirtualMachine vm)
throws VirtualMachineException, InterruptedException {
vm.setCallback(mExecutorService, this);
vm.run();
logVmOutputAndMonitorBootEvents(logTag, vm.getConsoleOutputStream(), "Console");
logVmOutput(logTag, vm.getLogOutputStream(), "Log");
mExecutorService.awaitTermination(300, TimeUnit.SECONDS);
}
public OptionalLong getVcpuStartedNanoTime() {
return mVcpuStartedNanoTime;
}
public OptionalLong getKernelStartedNanoTime() {
return mKernelStartedNanoTime;
}
public OptionalLong getInitStartedNanoTime() {
return mInitStartedNanoTime;
}
public OptionalLong getPayloadStartedNanoTime() {
return mPayloadStartedNanoTime;
}
protected void forceStop(VirtualMachine vm) {
try {
vm.stop();
} catch (VirtualMachineException e) {
throw new RuntimeException(e);
}
}
@Override
public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {}
@Override
public void onPayloadReady(VirtualMachine vm) {}
@Override
public void onPayloadFinished(VirtualMachine vm, int exitCode) {}
@Override
public void onError(VirtualMachine vm, int errorCode, String message) {}
@Override
@CallSuper
public void onDied(VirtualMachine vm, int reason) {
vm.clearCallback();
mExecutorService.shutdown();
}
@Override
public void onRamdump(VirtualMachine vm, ParcelFileDescriptor ramdump) {}
}
public static class BootResult {
public final boolean payloadStarted;
public final int deathReason;
public final long apiCallNanoTime;
public final long endToEndNanoTime;
public final OptionalLong vcpuStartedNanoTime;
public final OptionalLong kernelStartedNanoTime;
public final OptionalLong initStartedNanoTime;
public final OptionalLong payloadStartedNanoTime;
BootResult(boolean payloadStarted,
int deathReason,
long apiCallNanoTime,
long endToEndNanoTime,
OptionalLong vcpuStartedNanoTime,
OptionalLong kernelStartedNanoTime,
OptionalLong initStartedNanoTime,
OptionalLong payloadStartedNanoTime) {
this.apiCallNanoTime = apiCallNanoTime;
this.payloadStarted = payloadStarted;
this.deathReason = deathReason;
this.endToEndNanoTime = endToEndNanoTime;
this.vcpuStartedNanoTime = vcpuStartedNanoTime;
this.kernelStartedNanoTime = kernelStartedNanoTime;
this.initStartedNanoTime = initStartedNanoTime;
this.payloadStartedNanoTime = payloadStartedNanoTime;
}
private long getVcpuStartedNanoTime() {
return vcpuStartedNanoTime.getAsLong();
}
private long getKernelStartedNanoTime() {
return kernelStartedNanoTime.getAsLong();
}
private long getInitStartedNanoTime() {
return initStartedNanoTime.getAsLong();
}
private long getPayloadStartedNanoTime() {
return payloadStartedNanoTime.getAsLong();
}
public long getVMStartingElapsedNanoTime() {
return getVcpuStartedNanoTime() - apiCallNanoTime;
}
public long getBootloaderElapsedNanoTime() {
return getKernelStartedNanoTime() - getVcpuStartedNanoTime();
}
public long getKernelElapsedNanoTime() {
return getInitStartedNanoTime() - getKernelStartedNanoTime();
}
public long getUserspaceElapsedNanoTime() {
return getPayloadStartedNanoTime() - getInitStartedNanoTime();
}
}
public BootResult tryBootVm(String logTag, String vmName)
throws VirtualMachineException, InterruptedException {
VirtualMachine vm = mInner.getVirtualMachineManager().get(vmName);
final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>();
final CompletableFuture<Integer> deathReason = new CompletableFuture<>();
final CompletableFuture<Long> endTime = new CompletableFuture<>();
VmEventListener listener =
new VmEventListener() {
@Override
public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
endTime.complete(System.nanoTime());
payloadStarted.complete(true);
forceStop(vm);
}
@Override
public void onDied(VirtualMachine vm, int reason) {
deathReason.complete(reason);
super.onDied(vm, reason);
}
};
long apiCallNanoTime = System.nanoTime();
listener.runToFinish(logTag, vm);
return new BootResult(
payloadStarted.getNow(false),
deathReason.getNow(VirtualMachineCallback.DEATH_REASON_INFRASTRUCTURE_ERROR),
apiCallNanoTime,
endTime.getNow(apiCallNanoTime) - apiCallNanoTime,
listener.getVcpuStartedNanoTime(),
listener.getKernelStartedNanoTime(),
listener.getInitStartedNanoTime(),
listener.getPayloadStartedNanoTime());
}
}