Merge "Add memory size to VM config."
diff --git a/authfs/tests/Android.bp b/authfs/tests/Android.bp
index bacb890..8061c56 100644
--- a/authfs/tests/Android.bp
+++ b/authfs/tests/Android.bp
@@ -10,6 +10,12 @@
"compatibility-tradefed",
"compatibility-host-util",
],
+ static_libs: [
+ "VirtualizationTestHelper",
+ ],
test_suites: ["general-tests"],
- data: [":authfs_test_files"],
+ data: [
+ ":authfs_test_files",
+ ":MicrodroidTestApp.signed",
+ ],
}
diff --git a/authfs/tests/AndroidTest.xml b/authfs/tests/AndroidTest.xml
index 485e392..8f940f6 100644
--- a/authfs/tests/AndroidTest.xml
+++ b/authfs/tests/AndroidTest.xml
@@ -15,10 +15,13 @@
-->
<configuration description="Config for authfs tests">
- <!-- Since Android does not support user namespace, we need root to access /dev/fuse and also
- to set up the mount. -->
+ <!-- Need root to start virtualizationservice -->
<target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+ <!-- virtualizationservice doesn't have access to shell_data_file. Instead of giving it
+ a test-only permission, run it without selinux -->
+ <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
+
<!-- Basic checks that the device has all the prerequisites. -->
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="throw-if-cmd-fail" value="true" />
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 43d1210..426b333 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -18,19 +18,16 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import android.platform.test.annotations.RootPermissionTest;
+import android.virt.test.VirtualizationTestCaseBase;
import com.android.compatibility.common.util.PollingCheck;
import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
import org.junit.After;
import org.junit.Before;
@@ -40,18 +37,22 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+// TODO move to Virtualization/tests/hostside/
@RootPermissionTest
@RunWith(DeviceJUnit4ClassRunner.class)
-public final class AuthFsHostTest extends BaseHostJUnit4Test {
+public final class AuthFsHostTest extends VirtualizationTestCaseBase {
- /** Test directory where data are located */
+ /** Test directory on Android where data are located */
private static final String TEST_DIR = "/data/local/tmp/authfs";
- /** Mount point of authfs during the test */
- private static final String MOUNT_DIR = "/data/local/tmp/authfs/mnt";
+ /** Mount point of authfs on Microdroid during the test */
+ private static final String MOUNT_DIR = "/data/local/tmp";
+ /** Path to fd_server on Android */
private static final String FD_SERVER_BIN = "/apex/com.android.virt/bin/fd_server";
- private static final String AUTHFS_BIN = "/apex/com.android.virt/bin/authfs";
+
+ /** Path to authfs on Microdroid */
+ private static final String AUTHFS_BIN = "/system/bin/authfs";
/** Plenty of time for authfs to get ready */
private static final int AUTHFS_INIT_TIMEOUT_MS = 1500;
@@ -59,68 +60,65 @@
/** FUSE's magic from statfs(2) */
private static final String FUSE_SUPER_MAGIC_HEX = "65735546";
- private ITestDevice mDevice;
private ExecutorService mThreadPool;
+ private String mCid;
@Before
- public void setUp() {
- mDevice = getDevice();
+ public void setUp() throws DeviceNotAvailableException {
+ testIfDeviceIsCapable();
+
+ cleanUpTestFiles();
+
+ prepareVirtualizationTestSetup();
+
mThreadPool = Executors.newCachedThreadPool();
+
+ // For each test case, boot and adb connect to a new Microdroid
+ final String apkName = "MicrodroidTestApp.apk";
+ final String packageName = "com.android.microdroid.test";
+ final String configPath = "assets/vm_config.json"; // path inside the APK
+ mCid = startMicrodroid(apkName, packageName, configPath, /* debug */ false);
+ adbConnectToMicrodroid(mCid);
+
+ // Root because authfs (started from shell in this test) currently require root to open
+ // /dev/fuse and mount the FUSE.
+ rootMicrodroid();
}
@After
public void tearDown() throws DeviceNotAvailableException {
- mDevice.executeShellV2Command("killall authfs fd_server");
- mDevice.executeShellV2Command("umount " + MOUNT_DIR);
- mDevice.executeShellV2Command("rm -f " + TEST_DIR + "/output");
+ if (mCid != null) {
+ shutdownMicrodroid(mCid);
+ mCid = null;
+ }
+
+ tryRunOnAndroid("killall fd_server");
+ cleanUpTestFiles();
+ cleanUpVirtualizationTestSetup();
}
- @Test
- public void testReadWithFsverityVerification_LocalFile()
- throws DeviceNotAvailableException, InterruptedException {
- // Setup
- runAuthFsInBackground(
- "--local-ro-file-unverified 3:input.4m"
- + " --local-ro-file 4:input.4m:input.4m.merkle_dump:input.4m.fsv_sig:cert.der"
- + " --local-ro-file 5:input.4k1:input.4k1.merkle_dump:input.4k1.fsv_sig:cert.der"
- + " --local-ro-file 6:input.4k:input.4k.merkle_dump:input.4k.fsv_sig:cert.der"
- );
-
- // Action
- String actualHashUnverified4m = computeFileHashInGuest(MOUNT_DIR + "/3");
- String actualHash4m = computeFileHashInGuest(MOUNT_DIR + "/4");
- String actualHash4k1 = computeFileHashInGuest(MOUNT_DIR + "/5");
- String actualHash4k = computeFileHashInGuest(MOUNT_DIR + "/6");
-
- // Verify
- String expectedHash4m = computeFileHash(TEST_DIR + "/input.4m");
- String expectedHash4k1 = computeFileHash(TEST_DIR + "/input.4k1");
- String expectedHash4k = computeFileHash(TEST_DIR + "/input.4k");
-
- assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4m, actualHashUnverified4m);
- assertEquals("Inconsistent hash from /authfs/4: ", expectedHash4m, actualHash4m);
- assertEquals("Inconsistent hash from /authfs/5: ", expectedHash4k1, actualHash4k1);
- assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4k, actualHash4k);
+ private void cleanUpTestFiles() throws DeviceNotAvailableException {
+ tryRunOnAndroid("rm -f " + TEST_DIR + "/output");
}
@Test
public void testReadWithFsverityVerification_RemoteFile()
throws DeviceNotAvailableException, InterruptedException {
// Setup
- runFdServerInBackground(
+ runFdServerOnAndroid(
"3<input.4m 4<input.4m.merkle_dump 5<input.4m.fsv_sig 6<input.4m",
- "--ro-fds 3:4:5 --ro-fds 6"
- );
- runAuthFsInBackground(
+ "--ro-fds 3:4:5 --ro-fds 6 --rpc-binder");
+
+ runAuthFsOnMicrodroid(
"--remote-ro-file-unverified 10:6:4194304 --remote-ro-file 11:3:4194304:cert.der"
- );
+ + " --cid 2");
// Action
- String actualHashUnverified4m = computeFileHashInGuest(MOUNT_DIR + "/10");
- String actualHash4m = computeFileHashInGuest(MOUNT_DIR + "/11");
+ String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
+ String actualHash4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
// Verify
- String expectedHash4m = computeFileHash(TEST_DIR + "/input.4m");
+ String expectedHash4m = computeFileHashOnAndroid(TEST_DIR + "/input.4m");
assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4m, actualHashUnverified4m);
assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4m, actualHash4m);
@@ -132,22 +130,20 @@
public void testReadWithFsverityVerification_RemoteSmallerFile()
throws DeviceNotAvailableException, InterruptedException {
// Setup
- runFdServerInBackground(
+ runFdServerOnAndroid(
"3<input.4k 4<input.4k.merkle_dump 5<input.4k.fsv_sig"
- + " 6<input.4k1 7<input.4k1.merkle_dump 8<input.4k1.fsv_sig",
- "--ro-fds 3:4:5 --ro-fds 6:7:8"
- );
- runAuthFsInBackground(
- "--remote-ro-file 10:3:4096:cert.der --remote-ro-file 11:6:4097:cert.der"
- );
+ + " 6<input.4k1 7<input.4k1.merkle_dump 8<input.4k1.fsv_sig",
+ "--ro-fds 3:4:5 --ro-fds 6:7:8 --rpc-binder");
+ runAuthFsOnMicrodroid(
+ "--remote-ro-file 10:3:4096:cert.der --remote-ro-file 11:6:4097:cert.der --cid 2");
// Action
- String actualHash4k = computeFileHashInGuest(MOUNT_DIR + "/10");
- String actualHash4k1 = computeFileHashInGuest(MOUNT_DIR + "/11");
+ String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
+ String actualHash4k1 = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
// Verify
- String expectedHash4k = computeFileHash(TEST_DIR + "/input.4k");
- String expectedHash4k1 = computeFileHash(TEST_DIR + "/input.4k1");
+ String expectedHash4k = computeFileHashOnAndroid(TEST_DIR + "/input.4k");
+ String expectedHash4k1 = computeFileHashOnAndroid(TEST_DIR + "/input.4k1");
assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4k, actualHash4k);
assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4k1, actualHash4k1);
@@ -157,31 +153,30 @@
public void testReadWithFsverityVerification_TamperedMerkleTree()
throws DeviceNotAvailableException, InterruptedException {
// Setup
- runFdServerInBackground(
+ runFdServerOnAndroid(
"3<input.4m 4<input.4m.merkle_dump.bad 5<input.4m.fsv_sig",
- "--ro-fds 3:4:5"
- );
- runAuthFsInBackground("--remote-ro-file 10:3:4096:cert.der");
+ "--ro-fds 3:4:5 --rpc-binder");
+ runAuthFsOnMicrodroid("--remote-ro-file 10:3:4096:cert.der --cid 2");
// Verify
- assertFalse(copyFileInGuest(MOUNT_DIR + "/10", "/dev/null"));
+ assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/10", "/dev/null"));
}
@Test
public void testWriteThroughCorrectly()
throws DeviceNotAvailableException, InterruptedException {
// Setup
- runFdServerInBackground("3<>output", "--rw-fds 3");
- runAuthFsInBackground("--remote-new-rw-file 20:3");
+ runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+ runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
// Action
- String srcPath = "/system/bin/linker";
+ String srcPath = "/system/bin/linker64";
String destPath = MOUNT_DIR + "/20";
String backendPath = TEST_DIR + "/output";
- assertTrue(copyFileInGuest(srcPath, destPath));
+ assertTrue(copyFileOnMicrodroid(srcPath, destPath));
// Verify
- String expectedHash = computeFileHashInGuest(srcPath);
+ String expectedHash = computeFileHashOnMicrodroid(srcPath);
expectBackingFileConsistency(destPath, backendPath, expectedHash);
}
@@ -189,60 +184,59 @@
public void testWriteFailedIfDetectsTampering()
throws DeviceNotAvailableException, InterruptedException {
// Setup
- runFdServerInBackground("3<>output", "--rw-fds 3");
- runAuthFsInBackground("--remote-new-rw-file 20:3");
+ runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+ runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
- String srcPath = "/system/bin/linker";
+ String srcPath = "/system/bin/linker64";
String destPath = MOUNT_DIR + "/20";
String backendPath = TEST_DIR + "/output";
- assertTrue(copyFileInGuest(srcPath, destPath));
+ assertTrue(copyFileOnMicrodroid(srcPath, destPath));
// Action
// Tampering with the first 2 4K block of the backing file.
- expectRemoteCommandToSucceed("dd if=/dev/zero of=" + backendPath + " bs=1 count=8192");
+ runOnAndroid("dd if=/dev/zero of=" + backendPath + " bs=1 count=8192");
// Verify
// Write to a block partially requires a read back to calculate the new hash. It should fail
// when the content is inconsistent to the known hash. Use direct I/O to avoid simply
// writing to the filesystem cache.
- expectRemoteCommandToFail("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 direct");
+ assertEquals(
+ tryRunOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 direct"),
+ null);
// A full 4K write does not require to read back, so write can succeed even if the backing
// block has already been tampered.
- expectRemoteCommandToSucceed(
- "dd if=/dev/zero of=" + destPath + " bs=1 count=4096 skip=4096");
+ runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=4096 skip=4096");
// Otherwise, a partial write with correct backing file should still succeed.
- expectRemoteCommandToSucceed(
- "dd if=/dev/zero of=" + destPath + " bs=1 count=1024 skip=8192");
+ runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 skip=8192");
}
@Test
public void testFileResize() throws DeviceNotAvailableException, InterruptedException {
// Setup
- runFdServerInBackground("3<>output", "--rw-fds 3");
- runAuthFsInBackground("--remote-new-rw-file 20:3");
+ runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+ runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
String outputPath = MOUNT_DIR + "/20";
String backendPath = TEST_DIR + "/output";
// Action & Verify
- expectRemoteCommandToSucceed(
- "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=10000 of=" + outputPath);
- assertEquals(getFileSizeInBytes(outputPath), 10000);
+ runOnMicrodroid("yes $'\\x01' | tr -d '\\n' | dd bs=1 count=10000 of=" + outputPath);
+ assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 10000);
expectBackingFileConsistency(
outputPath,
backendPath,
"684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
- resizeFile(outputPath, 15000);
- assertEquals(getFileSizeInBytes(outputPath), 15000);
+ resizeFileOnMicrodroid(outputPath, 15000);
+ assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 15000);
expectBackingFileConsistency(
outputPath,
backendPath,
"567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
- resizeFile(outputPath, 5000);
- assertEquals(getFileSizeInBytes(outputPath), 5000);
+ resizeFileOnMicrodroid(outputPath, 5000);
+ assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 5000);
expectBackingFileConsistency(
outputPath,
backendPath,
@@ -252,30 +246,16 @@
private void expectBackingFileConsistency(
String authFsPath, String backendPath, String expectedHash)
throws DeviceNotAvailableException {
- String hashOnAuthFs = computeFileHashInGuest(authFsPath);
+ String hashOnAuthFs = computeFileHashOnMicrodroid(authFsPath);
assertEquals("File hash is different to expectation", expectedHash, hashOnAuthFs);
- String hashOfBackingFile = computeFileHash(backendPath);
+ String hashOfBackingFile = computeFileHashOnAndroid(backendPath);
assertEquals(
"Inconsistent file hash on the backend storage", hashOnAuthFs, hashOfBackingFile);
}
- // TODO(b/178874539): This does not really run in the guest VM. Send the shell command to the
- // guest VM when authfs works across VM boundary.
- private String computeFileHashInGuest(String path) throws DeviceNotAvailableException {
- return computeFileHash(path);
- }
-
- private boolean copyFileInGuest(String src, String dest) throws DeviceNotAvailableException {
- // TODO(b/182576497): cp returns error because close(2) returns ENOSYS in the current authfs
- // implementation. We should probably fix that since programs can expect close(2) return 0.
- String cmd = "cat " + src + " > " + dest;
- CommandResult result = mDevice.executeShellV2Command(cmd);
- return result.getStatus() == CommandStatus.SUCCESS;
- }
-
- private String computeFileHash(String path) throws DeviceNotAvailableException {
- String result = expectRemoteCommandToSucceed("sha256sum " + path);
+ private String computeFileHashOnMicrodroid(String path) {
+ String result = runOnMicrodroid("sha256sum " + path);
String[] tokens = result.split("\\s");
if (tokens.length > 0) {
return tokens[0];
@@ -285,18 +265,46 @@
}
}
- private void resizeFile(String path, long size) throws DeviceNotAvailableException {
- expectRemoteCommandToSucceed("truncate -c -s " + size + " " + path);
+ private boolean copyFileOnMicrodroid(String src, String dest)
+ throws DeviceNotAvailableException {
+ // TODO(b/182576497): cp returns error because close(2) returns ENOSYS in the current authfs
+ // implementation. We should probably fix that since programs can expect close(2) return 0.
+ String cmd = "cat " + src + " > " + dest;
+ return tryRunOnMicrodroid(cmd) != null;
}
- private long getFileSizeInBytes(String path) throws DeviceNotAvailableException {
- return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + path));
- }
-
- private void throwDowncastedException(Exception e) throws DeviceNotAvailableException {
- if (e instanceof DeviceNotAvailableException) {
- throw (DeviceNotAvailableException) e;
+ private String computeFileHashOnAndroid(String path) throws DeviceNotAvailableException {
+ String result = runOnAndroid("sha256sum " + path);
+ String[] tokens = result.split("\\s");
+ if (tokens.length > 0) {
+ return tokens[0];
} else {
+ CLog.e("Unrecognized output by sha256sum: " + result);
+ return "";
+ }
+ }
+
+ private void resizeFileOnMicrodroid(String path, long size) {
+ runOnMicrodroid("truncate -c -s " + size + " " + path);
+ }
+
+ private long getFileSizeInBytesOnMicrodroid(String path) {
+ return Long.parseLong(runOnMicrodroid("stat -c '%s' " + path));
+ }
+
+ private void runAuthFsOnMicrodroid(String flags) {
+ String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags;
+
+ mThreadPool.submit(
+ () -> {
+ CLog.i("Starting authfs");
+ CommandResult result = runOnMicrodroidForResult(cmd);
+ CLog.w("authfs has stopped: " + result);
+ });
+ try {
+ PollingCheck.waitFor(
+ AUTHFS_INIT_TIMEOUT_MS, () -> isMicrodroidDirectoryOnFuse(MOUNT_DIR));
+ } catch (Exception e) {
// Convert the broad Exception into an unchecked exception to avoid polluting all other
// methods. waitFor throws Exception because the callback, Callable#call(), has a
// signature to throw an Exception.
@@ -304,56 +312,25 @@
}
}
- private void runAuthFsInBackground(String flags) throws DeviceNotAvailableException {
- String cmd = "cd " + TEST_DIR + " && " + AUTHFS_BIN + " " + MOUNT_DIR + " " + flags;
-
- mThreadPool.submit(() -> {
- try {
- CLog.i("Starting authfs");
- expectRemoteCommandToSucceed(cmd);
- } catch (DeviceNotAvailableException e) {
- CLog.e("Error running authfs", e);
- throw new RuntimeException(e);
- }
- });
- try {
- PollingCheck.waitFor(AUTHFS_INIT_TIMEOUT_MS, () -> isRemoteDirectoryOnFuse(MOUNT_DIR));
- } catch (Exception e) {
- throwDowncastedException(e);
- }
- }
-
- private void runFdServerInBackground(String execParamsForOpeningFds, String flags)
+ private void runFdServerOnAndroid(String execParamsForOpeningFds, String flags)
throws DeviceNotAvailableException {
String cmd = "cd " + TEST_DIR + " && exec " + execParamsForOpeningFds + " " + FD_SERVER_BIN
+ " " + flags;
- mThreadPool.submit(() -> {
- try {
- CLog.i("Starting fd_server");
- expectRemoteCommandToSucceed(cmd);
- } catch (DeviceNotAvailableException e) {
- CLog.e("Error running fd_server", e);
- throw new RuntimeException(e);
- }
- });
+ mThreadPool.submit(
+ () -> {
+ try {
+ CLog.i("Starting fd_server");
+ CommandResult result = getDevice().executeShellV2Command(cmd);
+ CLog.w("fd_server has stopped: " + result);
+ } catch (DeviceNotAvailableException e) {
+ CLog.e("Error running fd_server", e);
+ throw new RuntimeException(e);
+ }
+ });
}
- private boolean isRemoteDirectoryOnFuse(String path) throws DeviceNotAvailableException {
- String fs_type = expectRemoteCommandToSucceed("stat -f -c '%t' " + path);
+ private boolean isMicrodroidDirectoryOnFuse(String path) {
+ String fs_type = tryRunOnMicrodroid("stat -f -c '%t' " + path);
return FUSE_SUPER_MAGIC_HEX.equals(fs_type);
}
-
- private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException {
- CommandResult result = mDevice.executeShellV2Command(cmd);
- assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS,
- result.getStatus());
- CLog.d("Stdout: " + result.getStdout());
- return result.getStdout().trim();
- }
-
- private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException {
- CommandResult result = mDevice.executeShellV2Command(cmd);
- assertNotEquals("Unexpected success from `" + cmd + "`: " + result.getStdout(),
- result.getStatus(), CommandStatus.SUCCESS);
- }
}
diff --git a/demo/Android.bp b/demo/Android.bp
new file mode 100644
index 0000000..77049de
--- /dev/null
+++ b/demo/Android.bp
@@ -0,0 +1,21 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "MicrodroidDemoApp",
+ srcs: ["java/**/*.java"],
+ resource_dirs: ["res"],
+ static_libs: [
+ "androidx-constraintlayout_constraintlayout",
+ "androidx.appcompat_appcompat",
+ "com.google.android.material_material",
+ ],
+ libs: [
+ "android.system.virtualmachine",
+ ],
+ jni_libs: ["MicrodroidTestNativeLib"],
+ platform_apis: true,
+ use_embedded_native_libs: true,
+ v4_signature: true,
+}
diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml
new file mode 100644
index 0000000..ae4f734
--- /dev/null
+++ b/demo/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.microdroid.demo">
+
+ <application
+ android:label="MicrodroidDemo"
+ android:theme="@style/Theme.MicrodroidDemo">
+ <uses-library android:name="android.system.virtualmachine" android:required="true" />
+ <activity android:name=".MainActivity" android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/demo/assets/vm_config.json b/demo/assets/vm_config.json
new file mode 100644
index 0000000..b814394
--- /dev/null
+++ b/demo/assets/vm_config.json
@@ -0,0 +1,13 @@
+{
+ "os": {
+ "name": "microdroid"
+ },
+ "task": {
+ "type": "microdroid_launcher",
+ "command": "MicrodroidTestNativeLib.so",
+ "args": [
+ "hello",
+ "microdroid"
+ ]
+ }
+}
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
new file mode 100644
index 0000000..6373b55
--- /dev/null
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2021 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.demo;
+
+import android.app.Application;
+import android.os.Bundle;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * This app is to demonstrate the use of APIs in the android.system.virtualmachine library.
+ * Currently, this app starts a virtual machine running Microdroid and shows the console output from
+ * the virtual machine to the UI.
+ */
+public class MainActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ TextView consoleView = (TextView) findViewById(R.id.consoleOutput);
+ Button runStopButton = (Button) findViewById(R.id.runStopButton);
+ ScrollView scrollView = (ScrollView) findViewById(R.id.scrollview);
+
+ // When the console model is updated, append the new line to the text view.
+ VirtualMachineModel model = new ViewModelProvider(this).get(VirtualMachineModel.class);
+ model.getConsoleOutput()
+ .observeForever(
+ new Observer<String>() {
+ @Override
+ public void onChanged(String line) {
+ consoleView.append(line + "\n");
+ scrollView.fullScroll(View.FOCUS_DOWN);
+ }
+ });
+
+ // When the VM status is updated, change the label of the button
+ model.getStatus()
+ .observeForever(
+ new Observer<VirtualMachine.Status>() {
+ @Override
+ public void onChanged(VirtualMachine.Status status) {
+ if (status == VirtualMachine.Status.RUNNING) {
+ runStopButton.setText("Stop");
+ } else {
+ runStopButton.setText("Run");
+ consoleView.setText("");
+ }
+ }
+ });
+
+ // When the button is clicked, run or stop the VM
+ runStopButton.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ if (model.getStatus().getValue() == VirtualMachine.Status.RUNNING) {
+ model.stop();
+ } else {
+ CheckBox debugModeCheckBox = (CheckBox) findViewById(R.id.debugMode);
+ final boolean debug = debugModeCheckBox.isChecked();
+ model.run(debug);
+ }
+ }
+ });
+ }
+
+ /** Models a virtual machine and console output from it. */
+ public static class VirtualMachineModel extends AndroidViewModel {
+ private VirtualMachine mVirtualMachine;
+ private final MutableLiveData<String> mConsoleOutput = new MutableLiveData<>();
+ private final MutableLiveData<VirtualMachine.Status> mStatus = new MutableLiveData<>();
+
+ public VirtualMachineModel(Application app) {
+ super(app);
+ mStatus.setValue(VirtualMachine.Status.DELETED);
+ }
+
+ /** Runs a VM */
+ public void run(boolean debug) {
+ // Create a VM and run it.
+ // TODO(jiyong): remove the call to idsigPath
+ try {
+ VirtualMachineConfig.Builder builder =
+ new VirtualMachineConfig.Builder(getApplication(), "assets/vm_config.json")
+ .idsigPath("/data/local/tmp/virt/MicrodroidDemoApp.apk.idsig")
+ .debugMode(debug);
+ VirtualMachineConfig config = builder.build();
+ VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
+ mVirtualMachine = vmm.create("demo_vm", config);
+ mVirtualMachine.run();
+ mStatus.postValue(mVirtualMachine.getStatus());
+ } catch (VirtualMachineException e) {
+ throw new RuntimeException(e);
+ }
+
+ // Read console output from the VM in the background
+ ExecutorService executorService = Executors.newFixedThreadPool(1);
+ executorService.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ BufferedReader reader =
+ new BufferedReader(
+ new InputStreamReader(
+ mVirtualMachine.getConsoleOutputStream()));
+ while (true) {
+ String line = reader.readLine();
+ mConsoleOutput.postValue(line);
+ }
+ } catch (IOException | VirtualMachineException e) {
+ // Consume
+ }
+ }
+ });
+ }
+
+ /** Stops the running VM */
+ public void stop() {
+ try {
+ mVirtualMachine.stop();
+ } catch (VirtualMachineException e) {
+ // Consume
+ }
+ mVirtualMachine = null;
+ mStatus.postValue(VirtualMachine.Status.STOPPED);
+ }
+
+ /** Returns the console output from the VM */
+ public LiveData<String> getConsoleOutput() {
+ return mConsoleOutput;
+ }
+
+ /** Returns the status of the VM */
+ public LiveData<VirtualMachine.Status> getStatus() {
+ return mStatus;
+ }
+ }
+}
diff --git a/demo/res/layout/activity_main.xml b/demo/res/layout/activity_main.xml
new file mode 100644
index 0000000..cd30f35
--- /dev/null
+++ b/demo/res/layout/activity_main.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#FFC107"
+ android:scrollbars="horizontal|vertical"
+ android:textAlignment="textStart"
+ tools:context=".MainActivity">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/runStopButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Run" />
+
+ <CheckBox
+ android:id="@+id/debugMode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Debug mode" />
+ </LinearLayout>
+
+ <ScrollView
+ android:id="@+id/scrollview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/consoleOutput"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#FFEB3B"
+ android:fontFamily="monospace"
+ android:textColor="#000000" />
+ </ScrollView>
+ </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/demo/res/values/colors.xml b/demo/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/demo/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/demo/res/values/themes.xml b/demo/res/values/themes.xml
new file mode 100644
index 0000000..16b8ab3
--- /dev/null
+++ b/demo/res/values/themes.xml
@@ -0,0 +1,16 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.MicrodroidDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
+ <!-- Primary brand color. -->
+ <item name="colorPrimary">@color/purple_500</item>
+ <item name="colorPrimaryVariant">@color/purple_700</item>
+ <item name="colorOnPrimary">@color/white</item>
+ <!-- Secondary brand color. -->
+ <item name="colorSecondary">@color/teal_200</item>
+ <item name="colorSecondaryVariant">@color/teal_700</item>
+ <item name="colorOnSecondary">@color/black</item>
+ <!-- Status bar color. -->
+ <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 504bc03..8089d85 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -16,5 +16,267 @@
package android.system.virtualmachine;
-/** @hide */
-public class VirtualMachine {}
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.virtualizationservice.IVirtualMachine;
+import android.system.virtualizationservice.IVirtualizationService;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Optional;
+
+/**
+ * A handle to the virtual machine. The virtual machine is local to the app which created the
+ * virtual machine.
+ *
+ * @hide
+ */
+public class VirtualMachine {
+ /** Name of the directory under the files directory where all VMs created for the app exist. */
+ private static final String VM_DIR = "vm";
+
+ /** Name of the persisted config file for a VM. */
+ private static final String CONFIG_FILE = "config.xml";
+
+ /** Name of the instance image file for a VM. (Not implemented) */
+ private static final String INSTANCE_IMAGE_FILE = "instance.img";
+
+ /** Name of the virtualization service. */
+ private static final String SERVICE_NAME = "android.system.virtualizationservice";
+
+ /** Status of a virtual machine */
+ public enum Status {
+ /** The virtual machine has just been created, or {@link #stop()} was called on it. */
+ STOPPED,
+ /** The virtual machine is running. */
+ RUNNING,
+ /**
+ * The virtual machine is deleted. This is a irreversable state. Once a virtual machine is
+ * deleted, it can never be undone, which means all its secrets are permanently lost.
+ */
+ DELETED,
+ }
+
+ /** The package which owns this VM. */
+ private final String mPackageName;
+
+ /** Name of this VM within the package. The name should be unique in the package. */
+ private final String mName;
+
+ /**
+ * Path to the config file for this VM. The config file is where the configuration is persisted.
+ */
+ private final File mConfigFilePath;
+
+ /** Path to the instance image file for this VM. (Not implemented) */
+ private final File mInstanceFilePath;
+
+ /** The configuration that is currently associated with this VM. */
+ private VirtualMachineConfig mConfig;
+
+ /** Handle to the "running" VM. */
+ private IVirtualMachine mVirtualMachine;
+
+ private ParcelFileDescriptor mConsoleReader;
+ private ParcelFileDescriptor mConsoleWriter;
+
+ private VirtualMachine(Context context, String name, VirtualMachineConfig config) {
+ mPackageName = context.getPackageName();
+ mName = name;
+ mConfig = config;
+
+ final File vmRoot = new File(context.getFilesDir(), VM_DIR);
+ final File thisVmDir = new File(vmRoot, mName);
+ mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
+ mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
+ }
+
+ /**
+ * Creates a virtual machine with the given name and config. Once a virtual machine is created
+ * it is persisted until it is deleted by calling {@link #delete()}. The created virtual machine
+ * is in {@link #STOPPED} state. To run the VM, call {@link #run()}.
+ */
+ /* package */ static VirtualMachine create(
+ Context context, String name, VirtualMachineConfig config)
+ throws VirtualMachineException {
+ // TODO(jiyong): trigger an error if the VM having 'name' already exists.
+ VirtualMachine vm = new VirtualMachine(context, name, config);
+
+ try {
+ final File vmRoot = vm.mConfigFilePath.getParentFile();
+ Files.createDirectories(vmRoot.toPath());
+
+ FileOutputStream output = new FileOutputStream(vm.mConfigFilePath);
+ vm.mConfig.serialize(output);
+ output.close();
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ }
+
+ // TODO(jiyong): create the instance image file
+ return vm;
+ }
+
+ /** Loads a virtual machine that is already created before. */
+ /* package */ static VirtualMachine load(Context context, String name)
+ throws VirtualMachineException {
+ // TODO(jiyong): return null if the VM having the 'name' doesn't exist.
+ VirtualMachine vm = new VirtualMachine(context, name, /* config */ null);
+
+ try {
+ FileInputStream input = new FileInputStream(vm.mConfigFilePath);
+ VirtualMachineConfig config = VirtualMachineConfig.from(input);
+ input.close();
+ vm.mConfig = config;
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ }
+
+ return vm;
+ }
+
+ /**
+ * Returns the name of this virtual machine. The name is unique in the package and can't be
+ * changed.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the currently selected config of this virtual machine. There can be multiple virtual
+ * machines sharing the same config. Even in that case, the virtual machines are completely
+ * isolated from each other; one cannot share its secret to another virtual machine even if they
+ * share the same config. It is also possible that a virtual machine can switch its config,
+ * which can be done by calling {@link #setConfig(VirtualMachineCOnfig)}.
+ */
+ public VirtualMachineConfig getConfig() {
+ return mConfig;
+ }
+
+ /** Returns the current status of this virtual machine. */
+ public Status getStatus() throws VirtualMachineException {
+ try {
+ if (mVirtualMachine != null && mVirtualMachine.isRunning()) {
+ return Status.RUNNING;
+ }
+ } catch (RemoteException e) {
+ throw new VirtualMachineException(e);
+ }
+ if (!mConfigFilePath.exists()) {
+ return Status.DELETED;
+ }
+ return Status.STOPPED;
+ }
+
+ /**
+ * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
+ * actually started running or the OS has booted there. Such events can be notified by
+ * registering a callback object (not implemented currently).
+ */
+ public void run() throws VirtualMachineException {
+ if (getStatus() != Status.STOPPED) {
+ throw new VirtualMachineException(this + " is not in stopped state");
+ }
+ IVirtualizationService service =
+ IVirtualizationService.Stub.asInterface(ServiceManager.getService(SERVICE_NAME));
+
+ try {
+ if (mConsoleReader == null && mConsoleWriter == null) {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mConsoleReader = pipe[0];
+ mConsoleWriter = pipe[1];
+ }
+ mVirtualMachine =
+ service.startVm(
+ android.system.virtualizationservice.VirtualMachineConfig.appConfig(
+ getConfig().toParcel()),
+ mConsoleWriter);
+ } catch (IOException e) {
+ throw new VirtualMachineException(e);
+ } catch (RemoteException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+
+ /** Returns the stream object representing the console output from the virtual machine. */
+ public InputStream getConsoleOutputStream() throws VirtualMachineException {
+ if (mConsoleReader == null) {
+ throw new VirtualMachineException("Console output not available");
+ }
+ return new FileInputStream(mConsoleReader.getFileDescriptor());
+ }
+
+ /**
+ * Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real
+ * computer; the machine halts immediately. Software running on the virtual machine is not
+ * notified with the event. A stopped virtual machine can be re-started by calling {@link
+ * #run()}.
+ */
+ public void stop() throws VirtualMachineException {
+ // Dropping the IVirtualMachine handle stops the VM
+ mVirtualMachine = null;
+ }
+
+ /**
+ * Deletes this virtual machine. Deleting a virtual machine means deleting any persisted data
+ * associated with it including the per-VM secret. This is an irreversable action. A virtual
+ * machine once deleted can never be restored. A new virtual machine created with the same name
+ * and the same config is different from an already deleted virtual machine.
+ */
+ public void delete() throws VirtualMachineException {
+ if (getStatus() != Status.STOPPED) {
+ throw new VirtualMachineException("Virtual machine is not stopped");
+ }
+ final File vmRootDir = mConfigFilePath.getParentFile();
+ mConfigFilePath.delete();
+ mInstanceFilePath.delete();
+ vmRootDir.delete();
+ }
+
+ /** Returns the CID of this virtual machine, if it is running. */
+ public Optional<Integer> getCid() throws VirtualMachineException {
+ if (getStatus() != Status.RUNNING) {
+ return Optional.empty();
+ }
+ try {
+ return Optional.of(mVirtualMachine.getCid());
+ } catch (RemoteException e) {
+ throw new VirtualMachineException(e);
+ }
+ }
+
+ /**
+ * Changes the config of this virtual machine to a new one. This can be used to adjust things
+ * like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the
+ * application to run on the virtual machine, etc.) However, changing a config might make the
+ * virtual machine un-bootable if the new config is not compatible with the existing one. For
+ * example, if the signer of the app payload in the new config is different from that of the old
+ * config, the virtual machine won't boot. To prevent such cases, this method returns exception
+ * when an incompatible config is attempted.
+ *
+ * @return the old config
+ */
+ public VirtualMachineConfig setConfig(VirtualMachineConfig newConfig)
+ throws VirtualMachineException {
+ // TODO(jiyong): implement this
+ throw new VirtualMachineException("Not implemented");
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("VirtualMachine(");
+ sb.append("name:" + getName() + ", ");
+ sb.append("config:" + getConfig().getPayloadConfigPath() + ", ");
+ sb.append("package: " + mPackageName);
+ sb.append(")");
+ return sb.toString();
+ }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
new file mode 100644
index 0000000..b5f04a2
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2021 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 android.system.virtualmachine;
+
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.system.virtualizationservice.VirtualMachineAppConfig;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Represents a configuration of a virtual machine. A configuration consists of hardware
+ * configurations like the number of CPUs and the size of RAM, and software configurations like the
+ * OS and application to run on the virtual machine.
+ *
+ * @hide
+ */
+public final class VirtualMachineConfig {
+ // These defines the schema of the config file persisted on disk.
+ private static final int VERSION = 1;
+ private static final String KEY_VERSION = "version";
+ private static final String KEY_APKPATH = "apkPath";
+ private static final String KEY_IDSIGPATH = "idsigPath";
+ private static final String KEY_PAYLOADCONFIGPATH = "payloadConfigPath";
+ private static final String KEY_DEBUGMODE = "debugMode";
+
+ // Paths to the APK and its idsig file of this application.
+ private final String mApkPath;
+ private final String mIdsigPath;
+ private final boolean mDebugMode;
+
+ /**
+ * Path within the APK to the payload config file that defines software aspects of this config.
+ */
+ private final String mPayloadConfigPath;
+
+ // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc.
+
+ private VirtualMachineConfig(
+ String apkPath, String idsigPath, String payloadConfigPath, boolean debugMode) {
+ mApkPath = apkPath;
+ mIdsigPath = idsigPath;
+ mPayloadConfigPath = payloadConfigPath;
+ mDebugMode = debugMode;
+ }
+
+ /** Loads a config from a stream, for example a file. */
+ /* package */ static VirtualMachineConfig from(InputStream input)
+ throws IOException, VirtualMachineException {
+ PersistableBundle b = PersistableBundle.readFromStream(input);
+ final int version = b.getInt(KEY_VERSION);
+ if (version > VERSION) {
+ throw new VirtualMachineException("Version too high");
+ }
+ final String apkPath = b.getString(KEY_APKPATH);
+ if (apkPath == null) {
+ throw new VirtualMachineException("No apkPath");
+ }
+ final String idsigPath = b.getString(KEY_IDSIGPATH);
+ if (idsigPath == null) {
+ throw new VirtualMachineException("No idsigPath");
+ }
+ final String payloadConfigPath = b.getString(KEY_PAYLOADCONFIGPATH);
+ if (payloadConfigPath == null) {
+ throw new VirtualMachineException("No payloadConfigPath");
+ }
+ final boolean debugMode = b.getBoolean(KEY_DEBUGMODE);
+ return new VirtualMachineConfig(apkPath, idsigPath, payloadConfigPath, debugMode);
+ }
+
+ /** Persists this config to a stream, for example a file. */
+ /* package */ void serialize(OutputStream output) throws IOException {
+ PersistableBundle b = new PersistableBundle();
+ b.putInt(KEY_VERSION, VERSION);
+ b.putString(KEY_APKPATH, mApkPath);
+ b.putString(KEY_IDSIGPATH, mIdsigPath);
+ b.putString(KEY_PAYLOADCONFIGPATH, mPayloadConfigPath);
+ b.putBoolean(KEY_DEBUGMODE, mDebugMode);
+ b.writeToStream(output);
+ }
+
+ /** Returns the path to the payload config within the owning application. */
+ public String getPayloadConfigPath() {
+ return mPayloadConfigPath;
+ }
+
+ /**
+ * Converts this config object into a parcel. Used when creating a VM via the virtualization
+ * service. Notice that the files are not passed as paths, but as file descriptors because the
+ * service doesn't accept paths as it might not have permission to open app-owned files and that
+ * could be abused to run a VM with software that the calling application doesn't own.
+ */
+ /* package */ VirtualMachineAppConfig toParcel() throws FileNotFoundException {
+ VirtualMachineAppConfig parcel = new VirtualMachineAppConfig();
+ parcel.apk = ParcelFileDescriptor.open(new File(mApkPath), MODE_READ_ONLY);
+ parcel.idsig = ParcelFileDescriptor.open(new File(mIdsigPath), MODE_READ_ONLY);
+ parcel.configPath = mPayloadConfigPath;
+ parcel.debug = mDebugMode;
+ return parcel;
+ }
+
+ /** A builder used to create a {@link VirtualMachineConfig}. */
+ public static class Builder {
+ private Context mContext;
+ private String mPayloadConfigPath;
+ private boolean mDebugMode;
+ private String mIdsigPath; // TODO(jiyong): remove this
+ // TODO(jiyong): add more items like # of cpu, size of ram, debuggability, etc.
+
+ /** Creates a builder for the given context (APK), and the payload config file in APK. */
+ public Builder(Context context, String payloadConfigPath) {
+ mContext = context;
+ mPayloadConfigPath = payloadConfigPath;
+ mDebugMode = false;
+ }
+
+ /** Enables or disables the debug mode */
+ public Builder debugMode(boolean enableOrDisable) {
+ mDebugMode = enableOrDisable;
+ return this;
+ }
+
+ // TODO(jiyong): remove this. Apps shouldn't need to set the path to the idsig file. It
+ // should be automatically found or created on demand.
+ /** Set the path to the idsig file for the current application. */
+ public Builder idsigPath(String idsigPath) {
+ mIdsigPath = idsigPath;
+ return this;
+ }
+
+ /** Builds an immutable {@link VirtualMachineConfig} */
+ public VirtualMachineConfig build() {
+ final String apkPath = mContext.getPackageCodePath();
+ return new VirtualMachineConfig(apkPath, mIdsigPath, mPayloadConfigPath, mDebugMode);
+ }
+ }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineException.java b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
new file mode 100644
index 0000000..d6aeab3
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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 android.system.virtualmachine;
+
+/** @hide */
+public class VirtualMachineException extends Exception {
+ public VirtualMachineException() {
+ super();
+ }
+
+ public VirtualMachineException(String message) {
+ super(message);
+ }
+
+ public VirtualMachineException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public VirtualMachineException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
new file mode 100644
index 0000000..dfa4f0b
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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 android.system.virtualmachine;
+
+import android.content.Context;
+
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Manages {@link VirtualMachine} objects created for an application.
+ *
+ * @hide
+ */
+public class VirtualMachineManager {
+ private final Context mContext;
+
+ private VirtualMachineManager(Context context) {
+ mContext = context;
+ }
+
+ static Map<Context, WeakReference<VirtualMachineManager>> sInstances = new WeakHashMap<>();
+
+ /** Returns the per-context instance. */
+ public static VirtualMachineManager getInstance(Context context) {
+ synchronized (sInstances) {
+ VirtualMachineManager vmm =
+ sInstances.containsKey(context) ? sInstances.get(context).get() : null;
+ if (vmm == null) {
+ vmm = new VirtualMachineManager(context);
+ sInstances.put(context, new WeakReference(vmm));
+ }
+ return vmm;
+ }
+ }
+
+ /** A lock used to synchronize the creation of virtual machines */
+ private static final Object sCreateLock = new Object();
+
+ /**
+ * Creates a new {@link VirtualMachine} with the given name and config. Creating a virtual
+ * machine with the same name as an existing virtual machine is an error. The existing virtual
+ * machine has to be deleted before its name can be reused. Every call to this methods creates a
+ * new (and different) virtual machine even if the name and the config are the same as the
+ * deleted one.
+ */
+ public VirtualMachine create(String name, VirtualMachineConfig config)
+ throws VirtualMachineException {
+ synchronized (sCreateLock) {
+ return VirtualMachine.create(mContext, name, config);
+ }
+ }
+
+ /**
+ * Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
+ * such virtual machine.
+ */
+ public VirtualMachine get(String name) throws VirtualMachineException {
+ return VirtualMachine.load(mContext, name);
+ }
+
+ /** Returns an existing {@link VirtualMachine} if it exists, or create a new one. */
+ public VirtualMachine getOrCreate(String name, VirtualMachineConfig config)
+ throws VirtualMachineException {
+ VirtualMachine vm;
+ synchronized (sCreateLock) {
+ vm = get(name);
+ if (vm == null) {
+ return create(name, config);
+ }
+ }
+
+ if (vm.getConfig().equals(config)) {
+ return vm;
+ } else {
+ throw new VirtualMachineException("Incompatible config");
+ }
+ }
+}
diff --git a/microdroid/README.md b/microdroid/README.md
index d51c8d0..96a2ef9 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -11,7 +11,7 @@
supported. Note that we currently don't support user builds; only userdebug
builds are supported.
-The only remaining requirment is that `com.android.virt` APEX has to be
+The only remaining requirement is that `com.android.virt` APEX has to be
pre-installed. To do this, add the following line in your product makefile.
```make
diff --git a/microdroid/payload/README.md b/microdroid/payload/README.md
index b76eead..35502c1 100644
--- a/microdroid/payload/README.md
+++ b/microdroid/payload/README.md
@@ -7,7 +7,7 @@
Payload disk has 1 + N(number of APEX/APK payloads) partitions.
-The first partition is a "metadata" partition which describes other partitions.
+The first partition is a "payload-metadata" partition which describes other partitions.
And APEXes and an APK are following as separate partitions.
For now, the order of partitions are important.
diff --git a/microdroid/payload/metadata/Android.bp b/microdroid/payload/metadata/Android.bp
index d3ec625..e4c7692 100644
--- a/microdroid/payload/metadata/Android.bp
+++ b/microdroid/payload/metadata/Android.bp
@@ -10,6 +10,7 @@
prefer_rlib: true,
edition: "2018",
rustlibs: [
+ "libanyhow",
"libmicrodroid_metadata_proto_rust",
"libprotobuf",
],
diff --git a/microdroid/payload/metadata/src/lib.rs b/microdroid/payload/metadata/src/lib.rs
index 9c97411..eb9d90d 100644
--- a/microdroid/payload/metadata/src/lib.rs
+++ b/microdroid/payload/metadata/src/lib.rs
@@ -18,15 +18,15 @@
//! 4 bytes : size(N) in big endian
//! N bytes : protobuf message for Metadata
+use anyhow::Result;
use protobuf::Message;
-use std::io;
use std::io::Read;
use std::io::Write;
pub use microdroid_metadata::metadata::{ApexPayload, ApkPayload, Metadata};
/// Reads a metadata from a reader
-pub fn read_metadata<T: Read>(mut r: T) -> io::Result<Metadata> {
+pub fn read_metadata<T: Read>(mut r: T) -> Result<Metadata> {
let mut buf = [0u8; 4];
r.read_exact(&mut buf)?;
let size = i32::from_be_bytes(buf);
@@ -34,9 +34,10 @@
}
/// Writes a metadata to a writer
-pub fn write_metadata<T: Write>(metadata: &Metadata, mut w: T) -> io::Result<()> {
+pub fn write_metadata<T: Write>(metadata: &Metadata, mut w: T) -> Result<()> {
let mut buf = Vec::new();
metadata.write_to_writer(&mut buf)?;
w.write_all(&(buf.len() as i32).to_be_bytes())?;
- w.write_all(&buf)
+ w.write_all(&buf)?;
+ Ok(())
}
diff --git a/microdroid/payload/mk_payload.cc b/microdroid/payload/mk_payload.cc
index c31dcff..b27683c 100644
--- a/microdroid/payload/mk_payload.cc
+++ b/microdroid/payload/mk_payload.cc
@@ -302,7 +302,7 @@
// put metadata at the first partition
partitions.push_back(MultipleImagePartition{
- .label = "metadata",
+ .label = "payload-metadata",
.image_file_paths = {metadata_file},
.type = kLinuxFilesystem,
.read_only = true,
diff --git a/microdroid/sepolicy/system/private/apkdmverity.te b/microdroid/sepolicy/system/private/apkdmverity.te
index c6160be..8974a1d 100644
--- a/microdroid/sepolicy/system/private/apkdmverity.te
+++ b/microdroid/sepolicy/system/private/apkdmverity.te
@@ -6,7 +6,7 @@
# allow domain transition from init
init_daemon_domain(apkdmverity)
-# apkdmverity accesses /dev/block/by-name/metadata which points to
+# apkdmverity accesses "payload metadata disk" which points to
# a /dev/vd* block device file.
allow apkdmverity block_device:dir r_dir_perms;
allow apkdmverity block_device:lnk_file r_file_perms;
diff --git a/microdroid/sepolicy/system/private/microdroid_manager.te b/microdroid/sepolicy/system/private/microdroid_manager.te
index 81a6839..53c63ae 100644
--- a/microdroid/sepolicy/system/private/microdroid_manager.te
+++ b/microdroid/sepolicy/system/private/microdroid_manager.te
@@ -6,8 +6,7 @@
# allow domain transition from init
init_daemon_domain(microdroid_manager)
-# microdroid_manager accesses /dev/block/by-name/metadata which points to
-# a /dev/vd* block device file.
+# microdroid_manager accesses a virtual disk block device to read VM payload
allow microdroid_manager block_device:dir r_dir_perms;
allow microdroid_manager block_device:lnk_file r_file_perms;
allow microdroid_manager vd_device:blk_file r_file_perms;
diff --git a/microdroid_manager/src/metadata.rs b/microdroid_manager/src/metadata.rs
index 81d9cc4..86a9e3e 100644
--- a/microdroid_manager/src/metadata.rs
+++ b/microdroid_manager/src/metadata.rs
@@ -12,17 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-//! Payload metadata from /dev/block/by-name/metadata
+//! Payload metadata from /dev/block/by-name/payload-metadata
+use anyhow::Result;
use log::info;
use microdroid_metadata::{read_metadata, Metadata};
use std::fs::File;
-use std::io;
-const METADATA_PATH: &str = "/dev/block/by-name/metadata";
+const PAYLOAD_METADATA_PATH: &str = "/dev/block/by-name/payload-metadata";
-/// loads payload metadata from /dev/block/by-name/metadata
-pub fn load() -> io::Result<Metadata> {
+/// loads payload metadata from /dev/block/by-name/paylaod-metadata
+pub fn load() -> Result<Metadata> {
info!("loading payload metadata...");
- read_metadata(File::open(METADATA_PATH)?)
+ read_metadata(File::open(PAYLOAD_METADATA_PATH)?)
}
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index 5821309..6d43760 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -23,12 +23,15 @@
import static org.junit.Assume.assumeThat;
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.RunUtil;
import java.io.File;
+import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -45,7 +48,9 @@
// Set the maximum timeout value big enough.
private static final long MICRODROID_BOOT_TIMEOUT_MINUTES = 5;
- public void prepareVirtualizationTestSetup() throws Exception {
+ private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
+
+ public void prepareVirtualizationTestSetup() throws DeviceNotAvailableException {
// kill stale crosvm processes
tryRunOnAndroid("killall", "crosvm");
@@ -57,7 +62,7 @@
tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
}
- public void cleanUpVirtualizationTestSetup() throws Exception {
+ public void cleanUpVirtualizationTestSetup() throws DeviceNotAvailableException {
// disconnect from microdroid
tryRunOnHost("adb", "disconnect", MICRODROID_SERIAL);
@@ -67,7 +72,7 @@
tryRunOnAndroid("stop", "virtualizationservice");
}
- public void testIfDeviceIsCapable() throws Exception {
+ public void testIfDeviceIsCapable() throws DeviceNotAvailableException {
// Checks the preconditions to run microdroid. If the condition is not satisfied
// don't run the test (instead of failing)
skipIfFail("ls /dev/kvm");
@@ -96,7 +101,7 @@
}
// Run a shell command on Android. the default timeout is 2 min by tradefed
- private String runOnAndroid(String... cmd) throws Exception {
+ public String runOnAndroid(String... cmd) throws DeviceNotAvailableException {
CommandResult result = getDevice().executeShellV2Command(join(cmd));
if (result.getStatus() != CommandStatus.SUCCESS) {
fail(join(cmd) + " has failed: " + result);
@@ -104,13 +109,19 @@
return result.getStdout().trim();
}
- // Same as runOnAndroid, but failure is not an error
- private String tryRunOnAndroid(String... cmd) throws Exception {
+ // Same as runOnAndroid, but returns null on error.
+ public String tryRunOnAndroid(String... cmd) throws DeviceNotAvailableException {
CommandResult result = getDevice().executeShellV2Command(join(cmd));
- return result.getStdout().trim();
+ if (result.getStatus() == CommandStatus.SUCCESS) {
+ return result.getStdout().trim();
+ } else {
+ CLog.d(join(cmd) + " has failed (but ok): " + result);
+ return null;
+ }
}
- private String runOnAndroidWithTimeout(long timeoutMillis, String... cmd) throws Exception {
+ private String runOnAndroidWithTimeout(long timeoutMillis, String... cmd)
+ throws DeviceNotAvailableException {
CommandResult result =
getDevice()
.executeShellV2Command(
@@ -125,26 +136,46 @@
// Run a shell command on Microdroid
public String runOnMicrodroid(String... cmd) {
- final long timeout = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
- CommandResult result =
- RunUtil.getDefault()
- .runTimedCmd(timeout, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
+ CommandResult result = runOnMicrodroidForResult(cmd);
if (result.getStatus() != CommandStatus.SUCCESS) {
fail(join(cmd) + " has failed: " + result);
}
return result.getStdout().trim();
}
+ // Same as runOnMicrodroid, but returns null on error.
+ public String tryRunOnMicrodroid(String... cmd) {
+ CommandResult result = runOnMicrodroidForResult(cmd);
+ if (result.getStatus() == CommandStatus.SUCCESS) {
+ return result.getStdout().trim();
+ } else {
+ CLog.d(join(cmd) + " has failed (but ok): " + result);
+ return null;
+ }
+ }
+
+ public CommandResult runOnMicrodroidForResult(String... cmd) {
+ final long timeout = 30000; // 30 sec. Microdroid is extremely slow on GCE-on-CF.
+ return RunUtil.getDefault()
+ .runTimedCmd(timeout, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
+ }
+
private String join(String... strs) {
return String.join(" ", Arrays.asList(strs));
}
- public File findTestFile(String name) throws Exception {
- return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+ public File findTestFile(String name) {
+ try {
+ return (new CompatibilityBuildHelper(getBuild())).getTestFile(name);
+ } catch (FileNotFoundException e) {
+ fail("Missing test file: " + name);
+ return null;
+ }
}
- public String startMicrodroid(String apkName, String packageName, String configPath)
- throws Exception {
+ public String startMicrodroid(
+ String apkName, String packageName, String configPath, boolean debug)
+ throws DeviceNotAvailableException {
// Install APK
File apkFile = findTestFile(apkName);
getDevice().installPackage(apkFile, /* reinstall */ true);
@@ -162,6 +193,7 @@
getDevice().pushFile(idsigOnHost, apkIdsigPath);
final String logPath = TEST_ROOT + "log.txt";
+ final String debugFlag = debug ? "--debug " : "";
// Run the VM
runOnAndroid("start", "virtualizationservice");
@@ -171,6 +203,7 @@
"run-app",
"--daemonize",
"--log " + logPath,
+ debugFlag,
apkPath,
apkIdsigPath,
configPath);
@@ -200,16 +233,38 @@
return matcher.group(1);
}
- public void shutdownMicrodroid(String cid) throws Exception {
+ public void shutdownMicrodroid(String cid) throws DeviceNotAvailableException {
// Shutdown microdroid
runOnAndroid(VIRT_APEX + "bin/vm", "stop", cid);
+
+ // TODO(192660485): Figure out why shutting down the VM disconnects adb on cuttlefish
+ // temporarily. Without this wait, the rest of `runOnAndroid/skipIfFail` fails due to the
+ // connection loss, and results in assumption error exception for the rest of the tests.
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ public void rootMicrodroid() throws DeviceNotAvailableException {
+ runOnHost("adb", "-s", MICRODROID_SERIAL, "root");
+
+ // TODO(192660959): Figure out the root cause and remove the sleep. For unknown reason,
+ // even though `adb root` actually wait-for-disconnect then wait-for-device, the next
+ // `adb -s $MICRODROID_SERIAL shell ...` often fails with "adb: device offline".
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
}
// Establish an adb connection to microdroid by letting Android forward the connection to
// microdroid. Wait until the connection is established and microdroid is booted.
- public void adbConnectToMicrodroid(String cid, long timeoutMinutes) throws Exception {
+ public void adbConnectToMicrodroid(String cid) throws DeviceNotAvailableException {
long start = System.currentTimeMillis();
- long timeoutMillis = timeoutMinutes * 60 * 1000;
+ long timeoutMillis = MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
long elapsed = 0;
final String serial = getDevice().getSerialNumber();
@@ -247,7 +302,7 @@
assertThat(runOnMicrodroid("getprop", "ro.hardware"), is("microdroid"));
}
- private void skipIfFail(String command) throws Exception {
+ private void skipIfFail(String command) throws DeviceNotAvailableException {
CommandResult result = getDevice().executeShellV2Command(command);
assumeThat(result.getStatus(), is(CommandStatus.SUCCESS));
}
diff --git a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
index 0b82d43..02fb7e5 100644
--- a/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
+++ b/tests/hostside/java/android/virt/test/MicrodroidTestCase.java
@@ -29,15 +29,14 @@
@RunWith(DeviceJUnit4ClassRunner.class)
public class MicrodroidTestCase extends VirtualizationTestCaseBase {
- private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
private static final String APK_NAME = "MicrodroidTestApp.apk";
private static final String PACKAGE_NAME = "com.android.microdroid.test";
@Test
public void testMicrodroidBoots() throws Exception {
final String configPath = "assets/vm_config.json"; // path inside the APK
- final String cid = startMicrodroid(APK_NAME, PACKAGE_NAME, configPath);
- adbConnectToMicrodroid(cid, MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES);
+ final String cid = startMicrodroid(APK_NAME, PACKAGE_NAME, configPath, /* debug */ false);
+ adbConnectToMicrodroid(cid);
// Test writing to /data partition
runOnMicrodroid("echo MicrodroidTest > /data/local/tmp/test.txt");
@@ -77,6 +76,18 @@
shutdownMicrodroid(cid);
}
+ @Test
+ public void testDebugMode() throws Exception {
+ final String configPath = "assets/vm_config.json"; // path inside the APK
+ final boolean debug = true;
+ final String cid = startMicrodroid(APK_NAME, PACKAGE_NAME, configPath, debug);
+ adbConnectToMicrodroid(cid);
+
+ assertThat(runOnMicrodroid("getenforce"), is("Permissive"));
+
+ shutdownMicrodroid(cid);
+ }
+
@Before
public void setUp() throws Exception {
testIfDeviceIsCapable();
diff --git a/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java b/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
index b25869b..f73772e 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
@@ -17,8 +17,35 @@
import android.app.Activity;
import android.os.Bundle;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
public class TestActivity extends Activity {
+
@Override
- public void onCreate(Bundle savedInstanceState) {}
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ VirtualMachine vm1 = createAndRunVirtualMachine("vm1");
+ VirtualMachine vm2 = createAndRunVirtualMachine("vm2");
+ }
+
+ private VirtualMachine createAndRunVirtualMachine(String name) {
+ VirtualMachine vm;
+ try {
+ VirtualMachineConfig config =
+ new VirtualMachineConfig.Builder(this, "assets/vm_config.json")
+ .idsigPath("/data/local/tmp/virt/MicrodroidTestApp.apk.idsig")
+ .build();
+
+ VirtualMachineManager vmm = VirtualMachineManager.getInstance(this);
+ vm = vmm.create(name, config);
+ vm.run();
+ } catch (VirtualMachineException e) {
+ throw new RuntimeException(e);
+ }
+ return vm;
+ }
}
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index c209f10..0b8f2e5 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -29,6 +29,7 @@
"liblog_rust",
"libmicrodroid_metadata",
"libmicrodroid_payload_config",
+ "libonce_cell",
"libprotobuf",
"libprotos",
"libregex",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index a3311f2..f7cb339 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -5,8 +5,8 @@
aidl_interface {
name: "android.system.virtualizationservice",
srcs: ["**/*.aidl"],
- // This is never accessed directly. Apps are expected to use this indirectly via the java wrapper
- // android.system.virtualmachine.
+ // This is never accessed directly. Apps are expected to use this indirectly via the Java
+ // wrapper android.system.virtualmachine.
unstable: true,
backend: {
java: {
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index c0b2e11..bc19109 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -16,7 +16,7 @@
use crate::composite::make_composite_image;
use crate::crosvm::{CrosvmConfig, DiskFile, VmInstance};
-use crate::payload::{make_payload_disk, ApexInfoList};
+use crate::payload::make_payload_disk;
use crate::{Cid, FIRST_GUEST_CID};
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::IVirtualizationService;
@@ -63,19 +63,9 @@
["com.android.adbd", "com.android.i18n", "com.android.os.statsd", "com.android.sdkext"];
/// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
-#[derive(Debug)]
+#[derive(Debug, Default)]
pub struct VirtualizationService {
state: Mutex<State>,
- apex_info_list: ApexInfoList,
-}
-
-impl VirtualizationService {
- pub fn new() -> Result<VirtualizationService> {
- Ok(VirtualizationService {
- state: Default::default(),
- apex_info_list: ApexInfoList::load()?,
- })
- }
}
impl Interface for VirtualizationService {}
@@ -120,15 +110,13 @@
let config = match config {
VirtualMachineConfig::AppConfig(config) => BorrowedOrOwned::Owned(
- load_app_config(&self.apex_info_list, config, &temporary_directory).map_err(
- |e| {
- error!("Failed to load app config from {}: {}", &config.configPath, e);
- new_binder_exception(
- ExceptionCode::SERVICE_SPECIFIC,
- format!("Failed to load app config from {}: {}", &config.configPath, e),
- )
- },
- )?,
+ load_app_config(config, &temporary_directory).map_err(|e| {
+ error!("Failed to load app config from {}: {}", &config.configPath, e);
+ new_binder_exception(
+ ExceptionCode::SERVICE_SPECIFIC,
+ format!("Failed to load app config from {}: {}", &config.configPath, e),
+ )
+ })?,
),
VirtualMachineConfig::RawConfig(config) => BorrowedOrOwned::Borrowed(config),
};
@@ -300,7 +288,6 @@
}
fn load_app_config(
- apex_info_list: &ApexInfoList,
config: &VirtualMachineAppConfig,
temporary_directory: &Path,
) -> Result<VirtualMachineRawConfig> {
@@ -330,7 +317,6 @@
apexes.dedup_by(|a, b| a.name == b.name);
vm_config.disks.push(make_payload_disk(
- apex_info_list,
format!("/proc/self/fd/{}", apk_file.as_raw_fd()).into(),
format!("/proc/self/fd/{}", idsig_file.as_raw_fd()).into(),
config_path,
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 3f2e99c..658203b 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -39,7 +39,7 @@
android_logger::Config::default().with_tag(LOG_TAG).with_min_level(Level::Trace),
);
- let service = VirtualizationService::new().unwrap();
+ let service = VirtualizationService::default();
let service = BnVirtualizationService::new_binder(
service,
BinderFeatures { set_requesting_sid: true, ..BinderFeatures::default() },
diff --git a/virtualizationservice/src/payload.rs b/virtualizationservice/src/payload.rs
index d3dc289..76c55de 100644
--- a/virtualizationservice/src/payload.rs
+++ b/virtualizationservice/src/payload.rs
@@ -16,9 +16,10 @@
use crate::composite::align_to_partition_size;
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, bail, Result};
use microdroid_metadata::{ApexPayload, ApkPayload, Metadata};
use microdroid_payload_config::ApexConfig;
+use once_cell::sync::OnceCell;
use regex::Regex;
use std::fs;
use std::fs::OpenOptions;
@@ -29,7 +30,7 @@
/// Represents the list of APEXes
#[derive(Debug)]
-pub struct ApexInfoList {
+struct ApexInfoList {
list: Vec<ApexInfo>,
}
@@ -41,30 +42,38 @@
impl ApexInfoList {
/// Loads ApexInfoList
- pub fn load() -> Result<ApexInfoList> {
- // TODO(b/191601801): look up /apex/apex-info-list.xml instead of apexservice
- // Each APEX prints the line:
- // Module: <...> Version: <...> VersionName: <...> Path: <...> IsActive: <...> IsFactory: <...>
- // We only care about "Module:" and "Path:" tagged values for now.
- let info_pattern = Regex::new(r"^Module: (?P<name>[^ ]*) .* Path: (?P<path>[^ ]*) .*$")?;
- let output = Command::new("cmd")
- .arg("-w")
- .arg("apexservice")
- .arg("getActivePackages")
- .output()
- .expect("failed to execute apexservice cmd");
- let list = BufReader::new(output.stdout.as_slice())
- .lines()
- .map(|line| -> Result<ApexInfo> {
- let line = line?;
- let captures =
- info_pattern.captures(&line).ok_or_else(|| anyhow!("can't parse: {}", line))?;
- let name = captures.name("name").unwrap();
- let path = captures.name("path").unwrap();
- Ok(ApexInfo { name: name.as_str().to_owned(), path: path.as_str().into() })
- })
- .collect::<Result<Vec<ApexInfo>>>()?;
- Ok(ApexInfoList { list })
+ fn load() -> Result<&'static ApexInfoList> {
+ static INSTANCE: OnceCell<ApexInfoList> = OnceCell::new();
+ INSTANCE.get_or_try_init(|| {
+ // TODO(b/191601801): look up /apex/apex-info-list.xml instead of apexservice
+ // Each APEX prints the line:
+ // Module: <...> Version: <...> VersionName: <...> Path: <...> IsActive: <...> IsFactory: <...>
+ // We only care about "Module:" and "Path:" tagged values for now.
+ let info_pattern =
+ Regex::new(r"^Module: (?P<name>[^ ]*) .* Path: (?P<path>[^ ]*) .*$")?;
+ let output = Command::new("cmd")
+ .arg("-w")
+ .arg("apexservice")
+ .arg("getActivePackages")
+ .output()
+ .expect("failed to execute apexservice cmd");
+ let list = BufReader::new(output.stdout.as_slice())
+ .lines()
+ .map(|line| -> Result<ApexInfo> {
+ let line = line?;
+ let captures = info_pattern
+ .captures(&line)
+ .ok_or_else(|| anyhow!("can't parse: {}", line))?;
+ let name = captures.name("name").unwrap();
+ let path = captures.name("path").unwrap();
+ Ok(ApexInfo { name: name.as_str().to_owned(), path: path.as_str().into() })
+ })
+ .collect::<Result<Vec<ApexInfo>>>()?;
+ if list.is_empty() {
+ bail!("failed to load apex info: empty");
+ }
+ Ok(ApexInfoList { list })
+ })
}
fn get_path_for(&self, apex_name: &str) -> Result<PathBuf> {
@@ -120,7 +129,6 @@
/// microdroid-apk: [apk, zero filler]
/// microdroid-apk-idsig: idsig
pub fn make_payload_disk(
- apex_info_list: &ApexInfoList,
apk_file: PathBuf,
idsig_file: PathBuf,
config_path: &str,
@@ -150,11 +158,12 @@
// put metadata at the first partition
let mut partitions = vec![Partition {
- label: "metadata".to_owned(),
+ label: "payload-metadata".to_owned(),
paths: vec![metadata_path],
writable: false,
}];
+ let apex_info_list = ApexInfoList::load()?;
let mut filler_count = 0;
for (i, apex) in apexes.iter().enumerate() {
partitions.push(make_partition(