Merge "Revert^2 "Add microfuchsia apex that runs in AVF on bootup"" into main
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..a246e08
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,3 @@
+package {
+ default_team: "trendy_team_android_kvm",
+}
diff --git a/README.md b/README.md
index fc4d389..4a10c89 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,8 @@
AVF components:
* [pVM firmware](guest/pvmfw/README.md)
* [Android Boot Loader (ABL)](docs/abl.md)
-* [Microdroid](microdroid/README.md)
-* [Microdroid kernel](microdroid/kernel/README.md)
+* [Microdroid](build/microdroid/README.md)
+* [Microdroid kernel](guest/kernel/README.md)
* [Microdroid payload](libs/libmicrodroid_payload_metadata/README.md)
* [vmbase](libs/libvmbase/README.md)
* [Encrypted Storage](guest/encryptedstore/README.md)
diff --git a/TEST_MAPPING b/TEST_MAPPING
index fc88a59..2112125 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -144,7 +144,7 @@
"path": "packages/modules/Virtualization/android/vm"
},
{
- "path": "packages/modules/Virtualization/libs/libvmbase"
+ "path": "packages/modules/Virtualization/tests/vmbase_example"
},
{
"path": "packages/modules/Virtualization/guest/zipfuse"
diff --git a/android/FerrochromeApp/AndroidManifest.xml b/android/FerrochromeApp/AndroidManifest.xml
index d640c4a..f6d3f6a 100644
--- a/android/FerrochromeApp/AndroidManifest.xml
+++ b/android/FerrochromeApp/AndroidManifest.xml
@@ -12,6 +12,9 @@
<intent>
<action android:name="android.virtualization.VM_LAUNCHER" />
</intent>
+ <intent>
+ <action android:name="android.virtualization.FERROCHROME_DOWNLOADER" />
+ </intent>
</queries>
<application
android:label="Ferrochrome">
diff --git a/android/FerrochromeApp/custom_vm_setup.sh b/android/FerrochromeApp/custom_vm_setup.sh
index a5480ff..4dce0c7 100644
--- a/android/FerrochromeApp/custom_vm_setup.sh
+++ b/android/FerrochromeApp/custom_vm_setup.sh
@@ -7,13 +7,13 @@
}
function install() {
- user=$(cmd user get-main-user)
- src_dir=/data/media/${user}/ferrochrome/
+ src_dir=$(getprop debug.custom_vm_setup.path)
+ src_dir=${src_dir/#\/storage\/emulated\//\/data\/media\/}
dst_dir=/data/local/tmp/
cat $(find ${src_dir} -name "images.tar.gz*" | sort) | tar xz -C ${dst_dir}
- cp -u ${src_dir}vm_config.json ${dst_dir}
- chmod 666 ${dst_dir}*
+ cp -u ${src_dir}/vm_config.json ${dst_dir}
+ chmod 666 ${dst_dir}/*
# increase the size of state.img to the multiple of 4096
num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
@@ -21,8 +21,8 @@
additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
- rm ${src_dir}images.tar.gz*
- rm ${src_dir}vm_config.json
+ rm ${src_dir}/images.tar.gz*
+ rm ${src_dir}/vm_config.json
}
setprop debug.custom_vm_setup.done false
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
index 2df5cab..dba0078 100644
--- a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -16,14 +16,17 @@
package com.android.virtualization.ferrochrome;
+import android.annotation.WorkerThread;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Environment;
import android.os.SystemProperties;
+import android.text.TextUtils;
import android.util.Log;
import android.view.WindowManager;
import android.widget.TextView;
@@ -43,12 +46,20 @@
public class FerrochromeActivity extends Activity {
private static final String TAG = FerrochromeActivity.class.getName();
private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
+ private static final String ACTION_FERROCHROME_DOWNLOAD =
+ "android.virtualization.FERROCHROME_DOWNLOADER";
+ private static final String EXTRA_FERROCHROME_DEST_DIR = "dest_dir";
+ private static final String EXTRA_FERROCHROME_UPDATE_NEEDED = "update_needed";
private static final Path DEST_DIR =
Path.of(Environment.getExternalStorageDirectory().getPath(), "ferrochrome");
+ private static final String ASSET_DIR = "ferrochrome";
private static final Path VERSION_FILE = Path.of(DEST_DIR.toString(), "version");
private static final int REQUEST_CODE_VMLAUNCHER = 1;
+ private static final int REQUEST_CODE_FERROCHROME_DOWNLOADER = 2;
+
+ private ResolvedActivity mVmLauncher;
ExecutorService executorService = Executors.newSingleThreadExecutor();
@@ -66,25 +77,28 @@
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// Find VM Launcher
- Intent intent = new Intent(ACTION_VM_LAUNCHER).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
- PackageManager pm = getPackageManager();
- List<ResolveInfo> resolveInfos =
- pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
- if (resolveInfos == null || resolveInfos.size() != 1) {
+ mVmLauncher = ResolvedActivity.resolve(getPackageManager(), ACTION_VM_LAUNCHER);
+ if (mVmLauncher == null) {
updateStatus("Failed to resolve VM Launcher");
return;
}
// Clean up the existing vm launcher process if there is
ActivityManager am = getSystemService(ActivityManager.class);
- am.killBackgroundProcesses(resolveInfos.get(0).activityInfo.packageName);
+ am.killBackgroundProcesses(mVmLauncher.activityInfo.packageName);
executorService.execute(
() -> {
- if (updateImageIfNeeded()) {
- updateStatus("Starting Ferrochrome...");
- runOnUiThread(
- () -> startActivityForResult(intent, REQUEST_CODE_VMLAUNCHER));
+ if (hasLocalAssets()) {
+ if (updateImageIfNeeded()) {
+ updateStatus("Starting Ferrochrome...");
+ runOnUiThread(
+ () ->
+ startActivityForResult(
+ mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER));
+ }
+ } else {
+ tryLaunchDownloader();
}
});
}
@@ -93,9 +107,55 @@
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_VMLAUNCHER) {
finishAndRemoveTask();
+ } else if (requestCode == REQUEST_CODE_FERROCHROME_DOWNLOADER) {
+ String destDir = data.getStringExtra(EXTRA_FERROCHROME_DEST_DIR);
+ boolean updateNeeded =
+ data.getBooleanExtra(EXTRA_FERROCHROME_UPDATE_NEEDED, /* default= */ true);
+
+ if (resultCode != RESULT_OK || TextUtils.isEmpty(destDir)) {
+ Log.w(
+ TAG,
+ "Ferrochrome downloader returned error, code="
+ + resultCode
+ + ", dest="
+ + destDir);
+ updateStatus("User didn't accepted ferrochrome download..");
+ return;
+ }
+
+ Log.w(TAG, "Ferrochrome downloader returned OK");
+
+ if (!updateNeeded) {
+ updateStatus("Starting Ferrochrome...");
+ startActivityForResult(mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER);
+ }
+
+ executorService.execute(
+ () -> {
+ if (!extractImages(destDir)) {
+ updateStatus("Images from downloader looks bad..");
+ return;
+ }
+ updateStatus("Starting Ferrochrome...");
+ runOnUiThread(
+ () ->
+ startActivityForResult(
+ mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER));
+ });
}
}
+ @WorkerThread
+ private boolean hasLocalAssets() {
+ try {
+ String[] files = getAssets().list(ASSET_DIR);
+ return files != null && files.length > 0;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @WorkerThread
private boolean updateImageIfNeeded() {
if (!isUpdateNeeded()) {
Log.d(TAG, "No update needed.");
@@ -107,13 +167,8 @@
Files.createDirectory(DEST_DIR);
}
- String[] files = getAssets().list("ferrochrome");
- if (files == null || files.length == 0) {
- updateStatus("ChromeOS image not found. Please go/try-ferrochrome");
- return false;
- }
-
updateStatus("Copying images...");
+ String[] files = getAssets().list("ferrochrome");
for (String file : files) {
updateStatus(file);
Path dst = Path.of(DEST_DIR.toString(), file);
@@ -126,7 +181,38 @@
}
updateStatus("Done.");
+ return extractImages(DEST_DIR.toAbsolutePath().toString());
+ }
+
+ @WorkerThread
+ private void tryLaunchDownloader() {
+ // TODO(jaewan): Add safeguard to check whether ferrochrome downloader is valid.
+ Log.w(TAG, "No built-in assets found. Try again with ferrochrome downloader");
+
+ ResolvedActivity downloader =
+ ResolvedActivity.resolve(getPackageManager(), ACTION_FERROCHROME_DOWNLOAD);
+ if (downloader == null) {
+ Log.d(TAG, "Ferrochrome downloader doesn't exist");
+ updateStatus("ChromeOS image not found. Please go/try-ferrochrome");
+ return;
+ }
+ String pkgName = downloader.activityInfo.packageName;
+ Log.d(TAG, "Resolved Ferrochrome Downloader, pkgName=" + pkgName);
+ updateStatus("Launching Ferrochrome downloader for update");
+
+ // onActivityResult() will handle downloader result.
+ startActivityForResult(downloader.intent, REQUEST_CODE_FERROCHROME_DOWNLOADER);
+ }
+
+ @WorkerThread
+ private boolean extractImages(String destDir) {
updateStatus("Extracting images...");
+
+ if (TextUtils.isEmpty(destDir)) {
+ throw new RuntimeException("Internal error: destDir shouldn't be null");
+ }
+
+ SystemProperties.set("debug.custom_vm_setup.path", destDir);
SystemProperties.set("debug.custom_vm_setup.done", "false");
SystemProperties.set("debug.custom_vm_setup.start", "true");
while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
@@ -143,6 +229,7 @@
return true;
}
+ @WorkerThread
private boolean isUpdateNeeded() {
Path[] pathsToCheck = {DEST_DIR, VERSION_FILE};
for (Path p : pathsToCheck) {
@@ -188,4 +275,33 @@
statusView.append(line + "\n");
});
}
+
+ private static final class ResolvedActivity {
+ public final ActivityInfo activityInfo;
+ public final Intent intent;
+
+ private ResolvedActivity(ActivityInfo activityInfo, Intent intent) {
+ this.activityInfo = activityInfo;
+ this.intent = intent;
+ }
+
+ /* synthetic access */
+ static ResolvedActivity resolve(PackageManager pm, String action) {
+ Intent intent = new Intent(action).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ List<ResolveInfo> resolveInfos =
+ pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfos == null || resolveInfos.size() != 1) {
+ Log.w(
+ TAG,
+ "Failed to resolve activity, action="
+ + action
+ + ", resolved="
+ + resolveInfos);
+ return null;
+ }
+ ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
+ intent.setClassName(activityInfo.packageName, activityInfo.name);
+ return new ResolvedActivity(activityInfo, intent);
+ }
+ }
}
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
index c32d017..433e89c 100644
--- a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
+++ b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
@@ -30,8 +30,8 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- boolean isRoot = isTaskRoot();
finish();
+
if (!Intent.ACTION_SEND.equals(getIntent().getAction())) {
return;
}
@@ -49,16 +49,6 @@
return;
}
Log.i(TAG, "Sending " + scheme + " URL to VM");
- if (isRoot) {
- Log.w(
- TAG,
- "Cannot open URL without starting "
- + FerrochromeActivity.class.getSimpleName()
- + " first, starting it now");
- startActivity(
- new Intent(this, FerrochromeActivity.class).setAction(Intent.ACTION_MAIN));
- return;
- }
startActivity(
new Intent(ACTION_VM_OPEN_URL)
.setFlags(
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
new file mode 100644
index 0000000..f5f39e3
--- /dev/null
+++ b/android/TerminalApp/Android.bp
@@ -0,0 +1,17 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "VmTerminalApp",
+ srcs: ["java/**/*.java"],
+ resource_dirs: ["res"],
+ static_libs: [
+ "vm_launcher_lib",
+ ],
+ sdk_version: "system_current",
+ product_specific: true,
+ optimize: {
+ shrink_resources: true,
+ },
+}
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
new file mode 100644
index 0000000..07e6147
--- /dev/null
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.virtualization.terminal" >
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"/>
+
+ <application
+ android:label="VmTerminalApp"
+ android:usesCleartextTraffic="true">
+ <activity android:name=".MainActivity"
+ android:screenOrientation="landscape"
+ android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
+ 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/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
new file mode 100644
index 0000000..e6e56d9
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 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.virtualization.terminal;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.TextView;
+
+import com.android.virtualization.vmlauncher.VmLauncherServices;
+
+public class MainActivity extends Activity implements VmLauncherServices.VmLauncherServiceCallback {
+ private static final String TAG = "VmTerminalApp";
+ private String mVmIpAddr;
+ private WebView mWebView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ VmLauncherServices.startVmLauncherService(this, this);
+
+ setContentView(R.layout.activity_headless);
+ mWebView = (WebView) findViewById(R.id.webview);
+ mWebView.getSettings().setDatabaseEnabled(true);
+ mWebView.getSettings().setDomStorageEnabled(true);
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.setWebChromeClient(new WebChromeClient());
+ mWebView.setWebViewClient(
+ new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ view.loadUrl(url);
+ return true;
+ }
+ });
+ }
+
+ private void gotoURL(String url) {
+ runOnUiThread(() -> mWebView.loadUrl(url));
+ }
+
+ public void onVmStart() {
+ Log.i(TAG, "onVmStart()");
+ }
+
+ public void onVmStop() {
+ Log.i(TAG, "onVmStop()");
+ finish();
+ }
+
+ public void onVmError() {
+ Log.i(TAG, "onVmError()");
+ finish();
+ }
+
+ public void onIpAddrAvailable(String ipAddr) {
+ mVmIpAddr = ipAddr;
+ ((TextView) findViewById(R.id.ip_addr_textview)).setText(mVmIpAddr);
+
+ // TODO(b/359523803): Use AVF API to be notified when shell is ready instead of using dealy
+ new Handler(Looper.getMainLooper())
+ .postDelayed(() -> gotoURL("http://" + mVmIpAddr + ":7681"), 2000);
+ }
+}
diff --git a/android/TerminalApp/res/layout/activity_headless.xml b/android/TerminalApp/res/layout/activity_headless.xml
new file mode 100644
index 0000000..2a640f3
--- /dev/null
+++ b/android/TerminalApp/res/layout/activity_headless.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout 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:orientation="vertical"
+ tools:context=".MainActivity">
+ <TextView
+ android:id="@+id/ip_addr_textview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <WebView
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginBottom="5dp" />
+
+</LinearLayout>
diff --git a/android/VmLauncherApp/AndroidManifest.xml b/android/VmLauncherApp/AndroidManifest.xml
index 67b7a45..583fce7 100644
--- a/android/VmLauncherApp/AndroidManifest.xml
+++ b/android/VmLauncherApp/AndroidManifest.xml
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-feature android:name="android.software.virtualization_framework" android:required="true" />
<permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
@@ -26,6 +28,21 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <service
+ android:name=".VmLauncherService"
+ android:enabled="true"
+ android:exported="true"
+ android:permission="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
+ android:foregroundServiceType="specialUse">
+ <property
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="Run VM instances" />
+ <intent-filter>
+ <action android:name="android.virtualization.START_VM_LAUNCHER_SERVICE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </service>
+
</application>
</manifest>
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
new file mode 100644
index 0000000..def464e
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+
+/** Provide methods to synchronize clipboard across Android and VM. */
+class ClipboardHandler {
+ private static final String TAG = MainActivity.TAG;
+ private final ClipboardManager mClipboardManager;
+ private final VmAgent mVmAgent;
+
+ ClipboardHandler(Context context, VmAgent vmAgent) {
+ mClipboardManager = context.getSystemService(ClipboardManager.class);
+ mVmAgent = vmAgent;
+ }
+
+ private VmAgent.Connection getConnection() throws InterruptedException {
+ return mVmAgent.connect();
+ }
+
+ /** Read a text clip from Android's clipboard and send it to VM. */
+ void writeClipboardToVm() {
+ if (!mClipboardManager.hasPrimaryClip()) {
+ return;
+ }
+
+ ClipData clip = mClipboardManager.getPrimaryClip();
+ String text = clip.getItemAt(0).getText().toString();
+ // TODO: remove this trailing null character. The size is already encoded in the header.
+ text = text + '\0';
+ // TODO: use UTF-8 encoding
+ byte[] data = text.getBytes();
+
+ try {
+ getConnection().sendData(VmAgent.WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, data);
+ } catch (InterruptedException | RuntimeException e) {
+ Log.e(TAG, "Failed to write clipboard data to VM", e);
+ }
+ }
+
+ /** Read a text clip from VM and paste it to Android's clipboard. */
+ void readClipboardFromVm() {
+ VmAgent.Data data;
+ try {
+ data = getConnection().sendAndReceive(VmAgent.READ_CLIPBOARD_FROM_VM, null);
+ } catch (InterruptedException | RuntimeException e) {
+ Log.e(TAG, "Failed to read clipboard data from VM", e);
+ return;
+ }
+
+ switch (data.type) {
+ case VmAgent.WRITE_CLIPBOARD_TYPE_EMPTY:
+ Log.d(TAG, "clipboard data from VM is empty");
+ break;
+ case VmAgent.WRITE_CLIPBOARD_TYPE_TEXT_PLAIN:
+ String text = new String(data.data, StandardCharsets.UTF_8);
+ ClipData clip = ClipData.newPlainText(null, text);
+ mClipboardManager.setPrimaryClip(clip);
+ break;
+ default:
+ Log.e(TAG, "Unknown clipboard response type: " + data.type);
+ }
+ }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmConfigJson.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java
similarity index 98%
rename from android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmConfigJson.java
rename to android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java
index 8116743..6d39b46 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmConfigJson.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java
@@ -36,10 +36,10 @@
import java.util.Arrays;
/** This class and its inner classes model vm_config.json. */
-class VmConfigJson {
+class ConfigJson {
private static final boolean DEBUG = true;
- private VmConfigJson() {}
+ private ConfigJson() {}
@SerializedName("protected")
private boolean isProtected;
@@ -64,9 +64,9 @@
private GpuJson gpu;
/** Parses JSON file at jsonPath */
- static VmConfigJson from(String jsonPath) {
+ static ConfigJson from(String jsonPath) {
try (FileReader r = new FileReader(jsonPath)) {
- return new Gson().fromJson(r, VmConfigJson.class);
+ return new Gson().fromJson(r, ConfigJson.class);
} catch (Exception e) {
throw new RuntimeException("Failed to parse " + jsonPath, e);
}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/DisplayProvider.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/DisplayProvider.java
new file mode 100644
index 0000000..6eba709
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/DisplayProvider.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.crosvm.ICrosvmAndroidDisplayService;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import libcore.io.IoBridge;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/** Presents Android-side surface where VM can use as a display */
+class DisplayProvider {
+ private static final String TAG = MainActivity.TAG;
+ private final SurfaceView mMainView;
+ private final SurfaceView mCursorView;
+ private final IVirtualizationServiceInternal mVirtService;
+ private CursorHandler mCursorHandler;
+
+ DisplayProvider(SurfaceView mainView, SurfaceView cursorView) {
+ mMainView = mainView;
+ mCursorView = cursorView;
+
+ mMainView.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT);
+ mMainView.getHolder().addCallback(new Callback(Callback.SurfaceKind.MAIN));
+
+ mCursorView.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT);
+ mCursorView.getHolder().addCallback(new Callback(Callback.SurfaceKind.CURSOR));
+ mCursorView.getHolder().setFormat(PixelFormat.RGBA_8888);
+ // TODO: do we need this z-order?
+ mCursorView.setZOrderMediaOverlay(true);
+
+ IBinder b = ServiceManager.waitForService("android.system.virtualizationservice");
+ mVirtService = IVirtualizationServiceInternal.Stub.asInterface(b);
+ try {
+ // To ensure that the previous display service is removed.
+ mVirtService.clearDisplayService();
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failed to clear prior display service", e);
+ }
+ }
+
+ void notifyDisplayIsGoingToInvisible() {
+ // When the display is going to be invisible (by putting in the background), save the frame
+ // of the main surface so that we can re-draw it next time the display becomes visible. This
+ // is to save the duration of time where nothing is drawn by VM.
+ try {
+ getDisplayService().saveFrameForSurface(false /* forCursor */);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failed to save frame for the main surface", e);
+ }
+ }
+
+ private synchronized ICrosvmAndroidDisplayService getDisplayService() {
+ try {
+ IBinder b = mVirtService.waitDisplayService();
+ return ICrosvmAndroidDisplayService.Stub.asInterface(b);
+ } catch (Exception e) {
+ throw new RuntimeException("Error while getting display service", e);
+ }
+ }
+
+ private class Callback implements SurfaceHolder.Callback {
+ enum SurfaceKind {
+ MAIN,
+ CURSOR
+ }
+
+ private final SurfaceKind mSurfaceKind;
+
+ Callback(SurfaceKind kind) {
+ mSurfaceKind = kind;
+ }
+
+ private boolean isForCursor() {
+ return mSurfaceKind == SurfaceKind.CURSOR;
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ try {
+ getDisplayService().setSurface(holder.getSurface(), isForCursor());
+ } catch (Exception e) {
+ // TODO: don't consume this exception silently. For some unknown reason, setSurface
+ // call above throws IllegalArgumentException and that fails the surface
+ // configuration.
+ Log.e(TAG, "Failed to present surface " + mSurfaceKind + " to VM", e);
+ }
+
+ try {
+ switch (mSurfaceKind) {
+ case MAIN:
+ getDisplayService().drawSavedFrameForSurface(isForCursor());
+ break;
+ case CURSOR:
+ ParcelFileDescriptor stream = createNewCursorStream();
+ getDisplayService().setCursorStream(stream);
+ break;
+ }
+ } catch (Exception e) {
+ // TODO: don't consume exceptions here too
+ Log.e(TAG, "Failed to configure surface " + mSurfaceKind, e);
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ // TODO: support resizeable display. We could actually change the display size that the
+ // VM sees, or keep the size and render it by fitting it in the new surface.
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ try {
+ getDisplayService().removeSurface(isForCursor());
+ } catch (RemoteException e) {
+ throw new RuntimeException("Error while destroying surface for " + mSurfaceKind, e);
+ }
+ }
+ }
+
+ private ParcelFileDescriptor createNewCursorStream() {
+ if (mCursorHandler != null) {
+ mCursorHandler.interrupt();
+ }
+ ParcelFileDescriptor[] pfds;
+ try {
+ pfds = ParcelFileDescriptor.createSocketPair();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to create socketpair for cursor stream", e);
+ }
+ mCursorHandler = new CursorHandler(pfds[0]);
+ mCursorHandler.start();
+ return pfds[1];
+ }
+
+ /**
+ * Thread reading cursor coordinate from a stream, and updating the position of the cursor
+ * surface accordingly.
+ */
+ private class CursorHandler extends Thread {
+ private final ParcelFileDescriptor mStream;
+ private final SurfaceControl mCursor;
+ private final SurfaceControl.Transaction mTransaction;
+
+ CursorHandler(ParcelFileDescriptor stream) {
+ mStream = stream;
+ mCursor = DisplayProvider.this.mCursorView.getSurfaceControl();
+ mTransaction = new SurfaceControl.Transaction();
+
+ SurfaceControl main = DisplayProvider.this.mMainView.getSurfaceControl();
+ mTransaction.reparent(mCursor, main).apply();
+ }
+
+ @Override
+ public void run() {
+ try {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ while (true) {
+ if (Thread.interrupted()) {
+ Log.d(TAG, "CursorHandler thread interrupted!");
+ return;
+ }
+ byteBuffer.clear();
+ int bytes =
+ IoBridge.read(
+ mStream.getFileDescriptor(),
+ byteBuffer.array(),
+ 0,
+ byteBuffer.array().length);
+ if (bytes == -1) {
+ Log.e(TAG, "cannot read from cursor stream, stop the handler");
+ return;
+ }
+ float x = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+ float y = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+ mTransaction.setPosition(mCursor, x, y).apply();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "failed to run CursorHandler", e);
+ }
+ }
+ }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/InputForwarder.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/InputForwarder.java
new file mode 100644
index 0000000..1be362b
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/InputForwarder.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.View;
+
+/** Forwards input events (touch, mouse, ...) from Android to VM */
+class InputForwarder {
+ private static final String TAG = MainActivity.TAG;
+ private final Context mContext;
+ private final VirtualMachine mVirtualMachine;
+ private InputManager.InputDeviceListener mInputDeviceListener;
+
+ private boolean isTabletMode = false;
+
+ InputForwarder(
+ Context context,
+ VirtualMachine vm,
+ View touchReceiver,
+ View mouseReceiver,
+ View keyReceiver) {
+ mContext = context;
+ mVirtualMachine = vm;
+
+ VirtualMachineCustomImageConfig config = vm.getConfig().getCustomImageConfig();
+ if (config.useTouch()) {
+ setupTouchReceiver(touchReceiver);
+ }
+ if (config.useMouse() || config.useTrackpad()) {
+ setupMouseReceiver(mouseReceiver);
+ }
+ if (config.useKeyboard()) {
+ setupKeyReceiver(keyReceiver);
+ }
+ if (config.useSwitches()) {
+ // Any view's handler is fine.
+ setupTabletModeHandler(touchReceiver.getHandler());
+ }
+ }
+
+ void cleanUp() {
+ if (mInputDeviceListener != null) {
+ InputManager im = mContext.getSystemService(InputManager.class);
+ im.unregisterInputDeviceListener(mInputDeviceListener);
+ mInputDeviceListener = null;
+ }
+ }
+
+ private void setupTouchReceiver(View receiver) {
+ receiver.setOnTouchListener(
+ (v, event) -> {
+ return mVirtualMachine.sendMultiTouchEvent(event);
+ });
+ }
+
+ private void setupMouseReceiver(View receiver) {
+ receiver.requestUnbufferedDispatch(InputDevice.SOURCE_ANY);
+ receiver.setOnCapturedPointerListener(
+ (v, event) -> {
+ int eventSource = event.getSource();
+ if ((eventSource & InputDevice.SOURCE_CLASS_POSITION) != 0) {
+ return mVirtualMachine.sendTrackpadEvent(event);
+ }
+ return mVirtualMachine.sendMouseEvent(event);
+ });
+ }
+
+ private void setupKeyReceiver(View receiver) {
+ receiver.setOnKeyListener(
+ (v, code, event) -> {
+ // TODO: this is guest-os specific. It shouldn't be handled here.
+ if (isVolumeKey(code)) {
+ return false;
+ }
+ return mVirtualMachine.sendKeyEvent(event);
+ });
+ }
+
+ private static boolean isVolumeKey(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_VOLUME_UP
+ || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE;
+ }
+
+ private void setupTabletModeHandler(Handler handler) {
+ InputManager im = mContext.getSystemService(InputManager.class);
+ mInputDeviceListener =
+ new InputManager.InputDeviceListener() {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ setTabletModeConditionally();
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ setTabletModeConditionally();
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ setTabletModeConditionally();
+ }
+ };
+ im.registerInputDeviceListener(mInputDeviceListener, handler);
+ }
+
+ private static boolean hasPhysicalKeyboard() {
+ for (int id : InputDevice.getDeviceIds()) {
+ InputDevice d = InputDevice.getDevice(id);
+ if (!d.isVirtual() && d.isEnabled() && d.isFullKeyboard()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void setTabletModeConditionally() {
+ boolean tabletModeNeeded = !hasPhysicalKeyboard();
+ if (tabletModeNeeded != isTabletMode) {
+ String mode = tabletModeNeeded ? "tablet mode" : "desktop mode";
+ Log.d(TAG, "switching to " + mode);
+ isTabletMode = tabletModeNeeded;
+ mVirtualMachine.sendTabletModeEvent(tabletModeNeeded);
+ }
+ }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java
new file mode 100644
index 0000000..e1cb285
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import libcore.io.Streams;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Forwards VM's console output to a file on the Android side, and VM's log output to Android logd.
+ */
+class Logger {
+ private Logger() {}
+
+ static void setup(VirtualMachine vm, Path path, ExecutorService executor) {
+ if (vm.getConfig().getDebugLevel() != VirtualMachineConfig.DEBUG_LEVEL_FULL) {
+ return;
+ }
+
+ try {
+ InputStream console = vm.getConsoleOutput();
+ OutputStream file = Files.newOutputStream(path, StandardOpenOption.CREATE);
+ executor.submit(() -> Streams.copy(console, new LineBufferedOutputStream(file)));
+
+ InputStream log = vm.getLogOutput();
+ executor.submit(() -> writeToLogd(log, vm.getName()));
+ } catch (VirtualMachineException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean writeToLogd(InputStream input, String vmName) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+ String line;
+ while ((line = reader.readLine()) != null && !Thread.interrupted()) {
+ Log.d(vmName, line);
+ }
+ // TODO: find out why javac complains when the return type of this method is void. It
+ // (incorrectly?) thinks that IOException should be caught inside the lambda.
+ return true;
+ }
+
+ private static class LineBufferedOutputStream extends BufferedOutputStream {
+ LineBufferedOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(byte[] buf, int off, int len) throws IOException {
+ super.write(buf, off, len);
+ for (int i = 0; i < len; ++i) {
+ if (buf[off + i] == '\n') {
+ flush();
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
index 160140a..fb75533 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -20,54 +20,27 @@
import android.Manifest.permission;
import android.app.Activity;
-import android.content.ClipData;
-import android.content.ClipboardManager;
import android.content.Intent;
-import android.crosvm.ICrosvmAndroidDisplayService;
-import android.graphics.PixelFormat;
-import android.hardware.input.InputManager;
import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
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 android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.SurfaceControl;
-import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
+import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
-import android.view.WindowManager;
-import libcore.io.IoBridge;
-
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-public class MainActivity extends Activity implements InputManager.InputDeviceListener {
- private static final String TAG = "VmLauncherApp";
- private static final String VM_NAME = "my_custom_vm";
+public class MainActivity extends Activity {
+ static final String TAG = "VmLauncherApp";
+ // TODO: this path should be from outside of this activity
+ private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
- private static final boolean DEBUG = true;
private static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 101;
private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
@@ -75,565 +48,138 @@
private ExecutorService mExecutorService;
private VirtualMachine mVirtualMachine;
- private CursorHandler mCursorHandler;
- private ClipboardManager mClipboardManager;
-
-
- private static boolean isVolumeKey(int keyCode) {
- return keyCode == KeyEvent.KEYCODE_VOLUME_UP
- || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
- || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE;
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (mVirtualMachine == null) {
- return false;
- }
- return !isVolumeKey(keyCode) && mVirtualMachine.sendKeyEvent(event);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (mVirtualMachine == null) {
- return false;
- }
- return !isVolumeKey(keyCode) && mVirtualMachine.sendKeyEvent(event);
- }
-
- private void registerInputDeviceListener() {
- InputManager inputManager = getSystemService(InputManager.class);
- if (inputManager == null) {
- Log.e(TAG, "failed to registerInputDeviceListener because InputManager is null");
- return;
- }
- inputManager.registerInputDeviceListener(this, null);
- }
-
- private void unregisterInputDeviceListener() {
- InputManager inputManager = getSystemService(InputManager.class);
- if (inputManager == null) {
- Log.e(TAG, "failed to unregisterInputDeviceListener because InputManager is null");
- return;
- }
- inputManager.unregisterInputDeviceListener(this);
- }
-
- private void setTabletModeConditionally() {
- if (mVirtualMachine == null) {
- Log.e(TAG, "failed to setTabletModeConditionally because VirtualMachine is null");
- return;
- }
- for (int id : InputDevice.getDeviceIds()) {
- InputDevice d = InputDevice.getDevice(id);
- if (!d.isVirtual() && d.isEnabled() && d.isFullKeyboard()) {
- Log.d(TAG, "the device has a physical keyboard, turn off tablet mode");
- mVirtualMachine.sendTabletModeEvent(false);
- return;
- }
- }
- mVirtualMachine.sendTabletModeEvent(true);
- Log.d(TAG, "the device doesn't have a physical keyboard, turn on tablet mode");
- }
-
- @Override
- public void onInputDeviceAdded(int deviceId) {
- setTabletModeConditionally();
- }
-
- @Override
- public void onInputDeviceRemoved(int deviceId) {
- setTabletModeConditionally();
- }
-
- @Override
- public void onInputDeviceChanged(int deviceId) {
- setTabletModeConditionally();
- }
+ private InputForwarder mInputForwarder;
+ private DisplayProvider mDisplayProvider;
+ private VmAgent mVmAgent;
+ private ClipboardHandler mClipboardHandler;
+ private OpenUrlHandler mOpenUrlHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- String action = getIntent().getAction();
- if (!ACTION_VM_LAUNCHER.equals(action)) {
- finish();
- Log.e(TAG, "onCreate unsupported intent action: " + action);
- return;
- }
+ Log.d(TAG, "onCreate intent: " + getIntent());
checkAndRequestRecordAudioPermission();
mExecutorService = Executors.newCachedThreadPool();
+
+ ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+ VirtualMachineConfig config = json.toConfig(this);
+
+ Runner runner;
try {
- // To ensure that the previous display service is removed.
- IVirtualizationServiceInternal.Stub.asInterface(
- ServiceManager.waitForService("android.system.virtualizationservice"))
- .clearDisplayService();
- } catch (RemoteException e) {
- Log.d(TAG, "failed to clearDisplayService");
- }
- getWindow().setDecorFitsSystemWindows(false);
- setContentView(R.layout.activity_main);
- VirtualMachineCallback callback =
- new VirtualMachineCallback() {
- // store reference to ExecutorService to avoid race condition
- private final ExecutorService mService = mExecutorService;
-
- @Override
- public void onPayloadStarted(VirtualMachine vm) {
- // This event is only from Microdroid-based VM. Custom VM shouldn't emit
- // this.
- }
-
- @Override
- public void onPayloadReady(VirtualMachine vm) {
- // This event is only from Microdroid-based VM. Custom VM shouldn't emit
- // this.
- }
-
- @Override
- public void onPayloadFinished(VirtualMachine vm, int exitCode) {
- // This event is only from Microdroid-based VM. Custom VM shouldn't emit
- // this.
- }
-
- @Override
- public void onError(VirtualMachine vm, int errorCode, String message) {
- Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
- setResult(RESULT_CANCELED);
- finish();
- }
-
- @Override
- public void onStopped(VirtualMachine vm, int reason) {
- Log.d(TAG, "VM stopped. Reason: " + reason);
- setResult(RESULT_OK);
- finish();
- }
- };
-
- try {
- VirtualMachineConfig config =
- VmConfigJson.from("/data/local/tmp/vm_config.json").toConfig(this);
- VirtualMachineManager vmm =
- getApplication().getSystemService(VirtualMachineManager.class);
- if (vmm == null) {
- Log.e(TAG, "vmm is null");
- return;
- }
- mVirtualMachine = vmm.getOrCreate(VM_NAME, config);
- try {
- mVirtualMachine.setConfig(config);
- } catch (VirtualMachineException e) {
- vmm.delete(VM_NAME);
- mVirtualMachine = vmm.create(VM_NAME, config);
- Log.e(TAG, "error for setting VM config", e);
- }
-
- Log.d(TAG, "vm start");
- mVirtualMachine.run();
- mVirtualMachine.setCallback(Executors.newSingleThreadExecutor(), callback);
- if (DEBUG) {
- InputStream console = mVirtualMachine.getConsoleOutput();
- InputStream log = mVirtualMachine.getLogOutput();
- OutputStream consoleLogFile =
- new LineBufferedOutputStream(
- getApplicationContext().openFileOutput("console.log", 0));
- mExecutorService.execute(new CopyStreamTask("console", console, consoleLogFile));
- mExecutorService.execute(new Reader("log", log));
- }
- } catch (VirtualMachineException | IOException e) {
+ runner = Runner.create(this, config);
+ } catch (VirtualMachineException e) {
throw new RuntimeException(e);
}
-
- SurfaceView surfaceView = findViewById(R.id.surface_view);
- SurfaceView cursorSurfaceView = findViewById(R.id.cursor_surface_view);
- cursorSurfaceView.setZOrderMediaOverlay(true);
- View backgroundTouchView = findViewById(R.id.background_touch_view);
- backgroundTouchView.setOnTouchListener(
- (v, event) -> {
- if (mVirtualMachine == null) {
- return false;
- }
- return mVirtualMachine.sendMultiTouchEvent(event);
- });
- surfaceView.requestUnbufferedDispatch(InputDevice.SOURCE_ANY);
- surfaceView.setOnCapturedPointerListener(
- (v, event) -> {
- if (mVirtualMachine == null) {
- return false;
- }
- int eventSource = event.getSource();
- if ((eventSource & InputDevice.SOURCE_CLASS_POSITION) != 0) {
- return mVirtualMachine.sendTrackpadEvent(event);
- }
- return mVirtualMachine.sendMouseEvent(event);
- });
- surfaceView
- .getHolder()
- .addCallback(
- // TODO(b/331708504): it should be handled in AVF framework.
- new SurfaceHolder.Callback() {
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- Log.d(
- TAG,
- "surface size: "
- + holder.getSurfaceFrame().flattenToString());
- Log.d(
- TAG,
- "ICrosvmAndroidDisplayService.setSurface("
- + holder.getSurface()
- + ")");
- runWithDisplayService(
- s ->
- s.setSurface(
- holder.getSurface(),
- false /* forCursor */));
- // TODO execute the above and the below togther with the same call
- // to runWithDisplayService. Currently this doesn't work because
- // setSurface somtimes trigger an exception and as a result
- // drawSavedFrameForSurface is skipped.
- runWithDisplayService(
- s -> s.drawSavedFrameForSurface(false /* forCursor */));
- }
-
- @Override
- public void surfaceChanged(
- SurfaceHolder holder, int format, int width, int height) {
- Log.d(
- TAG,
- "surface changed, width: " + width + ", height: " + height);
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
- runWithDisplayService(
- (service) -> service.removeSurface(false /* forCursor */));
- }
+ mVirtualMachine = runner.getVm();
+ runner.getExitStatus()
+ .thenAcceptAsync(
+ success -> {
+ setResult(success ? RESULT_OK : RESULT_CANCELED);
+ finish();
});
- cursorSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
- cursorSurfaceView
- .getHolder()
- .addCallback(
- new SurfaceHolder.Callback() {
- @Override
- public void surfaceCreated(SurfaceHolder holder) {
- try {
- ParcelFileDescriptor[] pfds =
- ParcelFileDescriptor.createSocketPair();
- if (mCursorHandler != null) {
- mCursorHandler.interrupt();
- }
- mCursorHandler =
- new CursorHandler(
- surfaceView.getSurfaceControl(),
- cursorSurfaceView.getSurfaceControl(),
- pfds[0]);
- mCursorHandler.start();
- runWithDisplayService(
- (service) -> service.setCursorStream(pfds[1]));
- } catch (Exception e) {
- Log.d(TAG, "failed to run cursor stream handler", e);
- }
- Log.d(
- TAG,
- "ICrosvmAndroidDisplayService.setSurface("
- + holder.getSurface()
- + ")");
- runWithDisplayService(
- (service) ->
- service.setSurface(
- holder.getSurface(), true /* forCursor */));
- }
- @Override
- public void surfaceChanged(
- SurfaceHolder holder, int format, int width, int height) {
- Log.d(
- TAG,
- "cursor surface changed, width: "
- + width
- + ", height: "
- + height);
- }
+ // Setup UI
+ setContentView(R.layout.activity_main);
+ SurfaceView mainView = findViewById(R.id.surface_view);
+ SurfaceView cursorView = findViewById(R.id.cursor_surface_view);
+ View touchView = findViewById(R.id.background_touch_view);
+ makeFullscreen();
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
- Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
- runWithDisplayService(
- (service) -> service.removeSurface(true /* forCursor */));
- }
- });
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ // Connect the views to the VM
+ mInputForwarder = new InputForwarder(this, mVirtualMachine, touchView, mainView, mainView);
+ mDisplayProvider = new DisplayProvider(mainView, cursorView);
- // Fullscreen:
- WindowInsetsController windowInsetsController = surfaceView.getWindowInsetsController();
- windowInsetsController.setSystemBarsBehavior(
+ Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
+ Logger.setup(mVirtualMachine, logPath, mExecutorService);
+
+ mVmAgent = new VmAgent(mVirtualMachine);
+ mClipboardHandler = new ClipboardHandler(this, mVmAgent);
+ mOpenUrlHandler = new OpenUrlHandler(mVmAgent);
+ handleIntent(getIntent());
+ }
+
+ private void makeFullscreen() {
+ Window w = getWindow();
+ w.setDecorFitsSystemWindows(false);
+ WindowInsetsController insetsCtrl = w.getInsetsController();
+ insetsCtrl.hide(WindowInsets.Type.systemBars());
+ insetsCtrl.setSystemBarsBehavior(
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
- windowInsetsController.hide(WindowInsets.Type.systemBars());
- registerInputDeviceListener();
}
@Override
protected void onResume() {
super.onResume();
- setTabletModeConditionally();
+ mInputForwarder.setTabletModeConditionally();
}
@Override
protected void onPause() {
super.onPause();
- runWithDisplayService(s -> s.saveFrameForSurface(false /* forCursor */));
+ mDisplayProvider.notifyDisplayIsGoingToInvisible();
}
@Override
protected void onStop() {
super.onStop();
- if (mVirtualMachine != null) {
- try {
- mVirtualMachine.sendLidEvent(/* close */ true);
- mVirtualMachine.suspend();
- } catch (VirtualMachineException e) {
- Log.e(TAG, "Failed to suspend VM" + e);
- }
+ try {
+ mVirtualMachine.suspend();
+ } catch (VirtualMachineException e) {
+ Log.e(TAG, "Failed to suspend VM" + e);
}
}
@Override
protected void onRestart() {
super.onRestart();
- if (mVirtualMachine != null) {
- try {
- mVirtualMachine.resume();
- mVirtualMachine.sendLidEvent(/* close */ false);
- } catch (VirtualMachineException e) {
- Log.e(TAG, "Failed to resume VM" + e);
- }
+ try {
+ mVirtualMachine.resume();
+ } catch (VirtualMachineException e) {
+ Log.e(TAG, "Failed to resume VM" + e);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
- if (mExecutorService != null) {
- mExecutorService.shutdownNow();
- }
- unregisterInputDeviceListener();
+ mExecutorService.shutdownNow();
+ mInputForwarder.cleanUp();
+ mOpenUrlHandler.shutdown();
Log.d(TAG, "destroyed");
}
- private static final int DATA_SHARING_SERVICE_PORT = 3580;
- private static final byte READ_CLIPBOARD_FROM_VM = 0;
- private static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
- private static final byte WRITE_CLIPBOARD_TYPE_TEXT_PLAIN = 2;
- private static final byte OPEN_URL = 3;
-
- private ClipboardManager getClipboardManager() {
- if (mClipboardManager == null) {
- mClipboardManager = getSystemService(ClipboardManager.class);
- }
- return mClipboardManager;
- }
-
- // Construct header for the clipboard data.
- // Byte 0: Data type
- // Byte 1-3: Padding alignment & Reserved for other use cases in the future
- // Byte 4-7: Data size of the payload
- private byte[] constructClipboardHeader(byte type, int dataSize) {
- ByteBuffer header = ByteBuffer.allocate(8);
- header.clear();
- header.order(ByteOrder.LITTLE_ENDIAN);
- header.put(0, type);
- header.putInt(4, dataSize);
- return header.array();
- }
-
- private ParcelFileDescriptor connectDataSharingService() throws VirtualMachineException {
- // TODO(349702313): Consider when clipboard sharing server is started to run in VM.
- return mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT);
- }
-
- private void writeClipboardToVm() {
- Log.d(TAG, "running writeClipboardToVm");
- try (ParcelFileDescriptor pfd = connectDataSharingService()) {
- ClipboardManager clipboardManager = getClipboardManager();
- if (!clipboardManager.hasPrimaryClip()) {
- Log.d(TAG, "host device has no clipboard data");
- return;
- }
- ClipData clip = clipboardManager.getPrimaryClip();
- String text = clip.getItemAt(0).getText().toString();
-
- byte[] header =
- constructClipboardHeader(
- WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, text.getBytes().length + 1);
- try (OutputStream stream = new FileOutputStream(pfd.getFileDescriptor())) {
- stream.write(header);
- stream.write(text.getBytes());
- stream.write('\0');
- Log.d(TAG, "successfully wrote clipboard data to the VM");
- } catch (IOException e) {
- Log.e(TAG, "failed to write clipboard data to the VM", e);
- }
- } catch (Exception e) {
- Log.e(TAG, "error on writeClipboardToVm", e);
- }
- }
-
- private byte[] readExactly(InputStream stream, int len) throws IOException {
- byte[] buf = stream.readNBytes(len);
- if (buf.length != len) {
- throw new IOException("Cannot read enough bytes");
- }
- return buf;
- }
-
- private void readClipboardFromVm() {
- Log.d(TAG, "running readClipboardFromVm");
- try (ParcelFileDescriptor pfd = connectDataSharingService()) {
- byte[] request = constructClipboardHeader(READ_CLIPBOARD_FROM_VM, 0);
- try (OutputStream output = new FileOutputStream(pfd.getFileDescriptor())) {
- output.write(request);
- Log.d(TAG, "successfully send request to the VM for reading clipboard");
- } catch (IOException e) {
- Log.e(TAG, "failed to send request to the VM for reading clipboard");
- throw e;
- }
-
- try (InputStream input = new FileInputStream(pfd.getFileDescriptor())) {
- ByteBuffer header = ByteBuffer.wrap(readExactly(input, 8));
- header.order(ByteOrder.LITTLE_ENDIAN);
- switch (header.get(0)) {
- case WRITE_CLIPBOARD_TYPE_EMPTY:
- Log.d(TAG, "clipboard data in VM is empty");
- break;
- case WRITE_CLIPBOARD_TYPE_TEXT_PLAIN:
- int dataSize = header.getInt(4);
- String text_data =
- new String(readExactly(input, dataSize), StandardCharsets.UTF_8);
- getClipboardManager()
- .setPrimaryClip(ClipData.newPlainText(null, text_data));
- Log.d(TAG, "successfully received clipboard data from VM");
- break;
- default:
- Log.e(TAG, "unknown clipboard response type");
- break;
- }
- } catch (IOException e) {
- Log.e(TAG, "failed to receive clipboard content from VM");
- throw e;
- }
- } catch (Exception e) {
- Log.e(TAG, "error on readClipboardFromVm", e);
- }
- }
-
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
+
+ // TODO: explain why we have to do this on every focus change
if (hasFocus) {
- SurfaceView surfaceView = findViewById(R.id.surface_view);
- Log.d(TAG, "requestPointerCapture()");
- surfaceView.requestPointerCapture();
+ SurfaceView mainView = findViewById(R.id.surface_view);
+ mainView.requestPointerCapture();
}
- if (mVirtualMachine != null) {
- if (hasFocus) {
- mExecutorService.execute(() -> writeClipboardToVm());
- } else {
- mExecutorService.execute(() -> readClipboardFromVm());
- }
- }
+
+ // TODO: remove executor here. Let clipboard handler handle this.
+ mExecutorService.execute(
+ () -> {
+ if (hasFocus) {
+ mClipboardHandler.writeClipboardToVm();
+ } else {
+ mClipboardHandler.readClipboardFromVm();
+ }
+ });
}
@Override
protected void onNewIntent(Intent intent) {
- String action = intent.getAction();
- if (!ACTION_VM_OPEN_URL.equals(action)) {
- Log.e(TAG, "onNewIntent unsupported intent action: " + action);
- return;
- }
- Log.d(TAG, "onNewIntent intent action: " + action);
- String text = intent.getStringExtra(Intent.EXTRA_TEXT);
- if (text != null) {
- mExecutorService.execute(
- () -> {
- byte[] data = text.getBytes();
- try (ParcelFileDescriptor pfd = connectDataSharingService();
- OutputStream stream =
- new FileOutputStream(pfd.getFileDescriptor())) {
- stream.write(constructClipboardHeader(OPEN_URL, data.length));
- stream.write(data);
- Log.d(TAG, "Successfully sent URL to the VM");
- } catch (IOException | VirtualMachineException e) {
- Log.e(TAG, "Failed to send URL to the VM", e);
- }
- });
- }
+ Log.d(TAG, "onNewIntent intent: " + intent);
+ handleIntent(intent);
}
- @FunctionalInterface
- public interface RemoteExceptionCheckedFunction<T> {
- void apply(T t) throws RemoteException;
- }
-
- private void runWithDisplayService(
- RemoteExceptionCheckedFunction<ICrosvmAndroidDisplayService> func) {
- IVirtualizationServiceInternal vs =
- IVirtualizationServiceInternal.Stub.asInterface(
- ServiceManager.waitForService("android.system.virtualizationservice"));
- try {
- Log.d(TAG, "wait for the display service");
- ICrosvmAndroidDisplayService service =
- ICrosvmAndroidDisplayService.Stub.asInterface(vs.waitDisplayService());
- assert service != null;
- func.apply(service);
- Log.d(TAG, "display service runs successfully");
- } catch (Exception e) {
- Log.d(TAG, "error on running display service", e);
- }
- }
-
- static class CursorHandler extends Thread {
- private final SurfaceControl mCursor;
- private final ParcelFileDescriptor mStream;
- private final SurfaceControl.Transaction mTransaction;
-
- CursorHandler(SurfaceControl main, SurfaceControl cursor, ParcelFileDescriptor stream) {
- mCursor = cursor;
- mStream = stream;
- mTransaction = new SurfaceControl.Transaction();
-
- mTransaction.reparent(cursor, main).apply();
- }
-
- @Override
- public void run() {
- Log.d(TAG, "running CursorHandler");
- try {
- ByteBuffer byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */);
- byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
- while (true) {
- if (Thread.interrupted()) {
- Log.d(TAG, "interrupted: exiting CursorHandler");
- return;
- }
- byteBuffer.clear();
- int bytes =
- IoBridge.read(
- mStream.getFileDescriptor(),
- byteBuffer.array(),
- 0,
- byteBuffer.array().length);
- if (bytes == -1) {
- Log.e(TAG, "cannot read from cursor stream, stop the handler");
- return;
- }
- float x = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
- float y = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
- mTransaction.setPosition(mCursor, x, y).apply();
- }
- } catch (IOException e) {
- Log.e(TAG, "failed to run CursorHandler", e);
+ private void handleIntent(Intent intent) {
+ if (ACTION_VM_OPEN_URL.equals(intent.getAction())) {
+ String url = intent.getStringExtra(Intent.EXTRA_TEXT);
+ if (url != null) {
+ mOpenUrlHandler.sendUrlToVm(url);
}
}
}
@@ -645,73 +191,4 @@
new String[] {permission.RECORD_AUDIO}, RECORD_AUDIO_PERMISSION_REQUEST_CODE);
}
}
-
- /** Reads data from an input stream and posts it to the output data */
- static class Reader implements Runnable {
- private final String mName;
- private final InputStream mStream;
-
- Reader(String name, InputStream stream) {
- mName = name;
- mStream = stream;
- }
-
- @Override
- public void run() {
- try {
- BufferedReader reader = new BufferedReader(new InputStreamReader(mStream));
- String line;
- while ((line = reader.readLine()) != null && !Thread.interrupted()) {
- Log.d(TAG, mName + ": " + line);
- }
- } catch (IOException e) {
- Log.e(TAG, "Exception while posting " + mName + " output: " + e.getMessage());
- }
- }
- }
-
- private static class CopyStreamTask implements Runnable {
- private final String mName;
- private final InputStream mIn;
- private final OutputStream mOut;
-
- CopyStreamTask(String name, InputStream in, OutputStream out) {
- mName = name;
- mIn = in;
- mOut = out;
- }
-
- @Override
- public void run() {
- try {
- byte[] buffer = new byte[2048];
- while (!Thread.interrupted()) {
- int len = mIn.read(buffer);
- if (len < 0) {
- break;
- }
- mOut.write(buffer, 0, len);
- }
- } catch (Exception e) {
- Log.e(TAG, "Exception while posting " + mName, e);
- }
- }
- }
-
- private static class LineBufferedOutputStream extends BufferedOutputStream {
- LineBufferedOutputStream(OutputStream out) {
- super(out);
- }
-
- @Override
- public void write(byte[] buf, int off, int len) throws IOException {
- super.write(buf, off, len);
- for (int i = 0; i < len; ++i) {
- if (buf[off + i] == '\n') {
- flush();
- break;
- }
- }
- }
- }
}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java
new file mode 100644
index 0000000..fb0c6bf
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+class OpenUrlHandler {
+ private static final String TAG = MainActivity.TAG;
+
+ private final VmAgent mVmAgent;
+ private final ExecutorService mExecutorService;
+
+ OpenUrlHandler(VmAgent vmAgent) {
+ mVmAgent = vmAgent;
+ mExecutorService = Executors.newSingleThreadExecutor();
+ }
+
+ void shutdown() {
+ mExecutorService.shutdownNow();
+ }
+
+ void sendUrlToVm(String url) {
+ mExecutorService.execute(
+ () -> {
+ try {
+ mVmAgent.connect().sendData(VmAgent.OPEN_URL, url.getBytes());
+ Log.d(TAG, "Successfully sent URL to the VM");
+ } catch (InterruptedException | RuntimeException e) {
+ Log.e(TAG, "Failed to send URL to the VM", e);
+ }
+ });
+ }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
new file mode 100644
index 0000000..a5f58fe
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ForkJoinPool;
+
+/** Utility class for creating a VM and waiting for it to finish. */
+class Runner {
+ private static final String TAG = MainActivity.TAG;
+ private final VirtualMachine mVirtualMachine;
+ private final Callback mCallback;
+
+ private Runner(VirtualMachine vm, Callback cb) {
+ mVirtualMachine = vm;
+ mCallback = cb;
+ }
+
+ /** Create a virtual machine of the given config, under the given context. */
+ static Runner create(Context context, VirtualMachineConfig config)
+ throws VirtualMachineException {
+ // context may already be the app context, but calling this again is not harmful.
+ // See b/359439878 on why vmm should be obtained from the app context.
+ Context appContext = context.getApplicationContext();
+ VirtualMachineManager vmm = appContext.getSystemService(VirtualMachineManager.class);
+ VirtualMachineCustomImageConfig customConfig = config.getCustomImageConfig();
+ if (customConfig == null) {
+ throw new RuntimeException("CustomImageConfig is missing");
+ }
+
+ String name = customConfig.getName();
+ if (name == null || name.isEmpty()) {
+ throw new RuntimeException("Virtual machine's name is missing in the config");
+ }
+
+ VirtualMachine vm = vmm.getOrCreate(name, config);
+ try {
+ vm.setConfig(config);
+ } catch (VirtualMachineException e) {
+ vmm.delete(name);
+ vm = vmm.create(name, config);
+ Log.w(TAG, "Re-creating virtual machine (" + name + ")", e);
+ }
+
+ Callback cb = new Callback();
+ vm.setCallback(ForkJoinPool.commonPool(), cb);
+ vm.run();
+ return new Runner(vm, cb);
+ }
+
+ /** Give access to the underlying VirtualMachine object. */
+ VirtualMachine getVm() {
+ return mVirtualMachine;
+ }
+
+ /** Get future about VM's exit status. */
+ CompletableFuture<Boolean> getExitStatus() {
+ return mCallback.mFinishedSuccessfully;
+ }
+
+ private static class Callback implements VirtualMachineCallback {
+ final CompletableFuture<Boolean> mFinishedSuccessfully = new CompletableFuture<>();
+
+ @Override
+ public void onPayloadStarted(VirtualMachine vm) {
+ // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+ }
+
+ @Override
+ public void onPayloadReady(VirtualMachine vm) {
+ // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+ }
+
+ @Override
+ public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+ // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+ }
+
+ @Override
+ public void onError(VirtualMachine vm, int errorCode, String message) {
+ Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
+ mFinishedSuccessfully.complete(false);
+ }
+
+ @Override
+ public void onStopped(VirtualMachine vm, int reason) {
+ Log.d(TAG, "VM stopped. Reason: " + reason);
+ mFinishedSuccessfully.complete(true);
+ }
+ }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
new file mode 100644
index 0000000..af1d298
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import libcore.io.Streams;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Agent running in the VM. This class provides connection to the agent and ways to communicate with
+ * it.
+ */
+class VmAgent {
+ private static final String TAG = MainActivity.TAG;
+ private static final int DATA_SHARING_SERVICE_PORT = 3580;
+ private static final int HEADER_SIZE = 8; // size of the header
+ private static final int SIZE_OFFSET = 4; // offset of the size field in the header
+ private static final long RETRY_INTERVAL_MS = 1_000;
+
+ static final byte READ_CLIPBOARD_FROM_VM = 0;
+ static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
+ static final byte WRITE_CLIPBOARD_TYPE_TEXT_PLAIN = 2;
+ static final byte OPEN_URL = 3;
+
+ private final VirtualMachine mVirtualMachine;
+
+ VmAgent(VirtualMachine vm) {
+ mVirtualMachine = vm;
+ }
+
+ /**
+ * Connects to the agent and returns the established communication channel. This can block.
+ *
+ * @throws InterruptedException If the current thread was interrupted
+ */
+ Connection connect() throws InterruptedException {
+ boolean shouldLog = true;
+ while (true) {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ try {
+ return new Connection(mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT));
+ } catch (VirtualMachineException e) {
+ if (shouldLog) {
+ shouldLog = false;
+ Log.d(TAG, "Still waiting for VM agent to start", e);
+ }
+ }
+ SystemClock.sleep(RETRY_INTERVAL_MS);
+ }
+ }
+
+ static class Data {
+ final int type;
+ final byte[] data;
+
+ Data(int type, byte[] data) {
+ this.type = type;
+ this.data = data;
+ }
+ }
+
+ /** Represents a connection to the agent */
+ class Connection {
+ private final ParcelFileDescriptor mConn;
+
+ private Connection(ParcelFileDescriptor conn) {
+ mConn = conn;
+ }
+
+ /** Send data of a given type. This can block. */
+ void sendData(byte type, byte[] data) {
+ // Byte 0: Data type
+ // Byte 1-3: Padding alignment & Reserved for other use cases in the future
+ // Byte 4-7: Data size of the payload
+ ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE);
+ header.clear();
+ header.order(ByteOrder.LITTLE_ENDIAN);
+ header.put(0, type);
+ int dataSize = data == null ? 0 : data.length;
+ header.putInt(SIZE_OFFSET, dataSize);
+
+ try (OutputStream out = new FileOutputStream(mConn.getFileDescriptor())) {
+ out.write(header.array());
+ if (data != null) {
+ out.write(data);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to send message of type: " + type, e);
+ }
+ }
+
+ /** Read data from agent. This can block. */
+ Data readData() {
+ ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE);
+ header.clear();
+ header.order(ByteOrder.LITTLE_ENDIAN);
+ byte[] data;
+
+ try (InputStream in = new FileInputStream(mConn.getFileDescriptor())) {
+ Streams.readFully(in, header.array());
+ byte type = header.get(0);
+ int dataSize = header.getInt(SIZE_OFFSET);
+ data = new byte[dataSize];
+ Streams.readFully(in, data);
+ return new Data(type, data);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read data", e);
+ }
+ }
+
+ /** Convenient method for sending data and then reading response for it. This can block. */
+ Data sendAndReceive(byte type, byte[] data) {
+ sendData(type, data);
+ return readData();
+ }
+ }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
new file mode 100644
index 0000000..ec98f4c
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.ResultReceiver;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class VmLauncherService extends Service {
+ private static final String TAG = "VmLauncherService";
+ // TODO: this path should be from outside of this service
+ private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
+
+ private static final int RESULT_START = 0;
+ private static final int RESULT_STOP = 1;
+ private static final int RESULT_ERROR = 2;
+ private static final int RESULT_IPADDR = 3;
+ private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+ private ExecutorService mExecutorService;
+ private VirtualMachine mVirtualMachine;
+ private ResultReceiver mResultReceiver;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private void startForeground() {
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ NotificationChannel notificationChannel =
+ new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
+ notificationManager.createNotificationChannel(notificationChannel);
+ startForeground(
+ this.hashCode(),
+ new Notification.Builder(this, TAG)
+ .setChannelId(TAG)
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setContentText("A VM " + mVirtualMachine.getName() + " is running")
+ .build());
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ mExecutorService = Executors.newCachedThreadPool();
+
+ ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+ VirtualMachineConfig config = json.toConfig(this);
+
+ Runner runner;
+ try {
+ runner = Runner.create(this, config);
+ } catch (VirtualMachineException e) {
+ throw new RuntimeException(e);
+ }
+ mVirtualMachine = runner.getVm();
+ mResultReceiver =
+ intent.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver.class);
+
+ runner.getExitStatus()
+ .thenAcceptAsync(
+ success -> {
+ if (mResultReceiver != null) {
+ mResultReceiver.send(success ? RESULT_STOP : RESULT_ERROR, null);
+ }
+ if (!success) {
+ stopSelf();
+ }
+ });
+ Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
+ Logger.setup(mVirtualMachine, logPath, mExecutorService);
+
+ startForeground();
+
+ mResultReceiver.send(RESULT_START, null);
+ if (config.getCustomImageConfig().useNetwork()) {
+ Handler handler = new Handler(Looper.getMainLooper());
+ gatherIpAddrFromVm(handler);
+ }
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mExecutorService.shutdownNow();
+ }
+
+ // TODO(b/359523803): Use AVF API to get ip addr when it exists
+ private void gatherIpAddrFromVm(Handler handler) {
+ handler.postDelayed(
+ () -> {
+ int INTERNAL_VSOCK_SERVER_PORT = 1024;
+ try (ParcelFileDescriptor pfd =
+ mVirtualMachine.connectVsock(INTERNAL_VSOCK_SERVER_PORT)) {
+ try (BufferedReader input =
+ new BufferedReader(
+ new InputStreamReader(
+ new FileInputStream(pfd.getFileDescriptor())))) {
+ String vmIpAddr = input.readLine().strip();
+ Bundle b = new Bundle();
+ b.putString(KEY_VM_IP_ADDR, vmIpAddr);
+ mResultReceiver.send(RESULT_IPADDR, b);
+ return;
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ }
+ } catch (Exception e) {
+ Log.e(TAG, e.toString());
+ }
+ gatherIpAddrFromVm(handler);
+ },
+ 1000);
+ }
+}
diff --git a/android/VmLauncherApp/proguard.flags b/android/VmLauncherApp/proguard.flags
index 5e05ecf..13ec24e 100644
--- a/android/VmLauncherApp/proguard.flags
+++ b/android/VmLauncherApp/proguard.flags
@@ -1,7 +1,7 @@
# Keep the no-args constructor of the deserialized class
--keepclassmembers class com.android.virtualization.vmlauncher.VmConfigJson {
+-keepclassmembers class com.android.virtualization.vmlauncher.ConfigJson {
<init>();
}
--keepclassmembers class com.android.virtualization.vmlauncher.VmConfigJson$* {
+-keepclassmembers class com.android.virtualization.vmlauncher.ConfigJson$* {
<init>();
}
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index f1bfd8c..144524f 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -411,9 +411,9 @@
let state = &mut *self.state.lock().unwrap();
let console_out_fd =
- clone_or_prepare_logger_fd(&debug_config, console_out_fd, format!("Console({})", cid))?;
+ clone_or_prepare_logger_fd(console_out_fd, format!("Console({})", cid))?;
let console_in_fd = console_in_fd.map(clone_file).transpose()?;
- let log_fd = clone_or_prepare_logger_fd(&debug_config, log_fd, format!("Log({})", cid))?;
+ let log_fd = clone_or_prepare_logger_fd(log_fd, format!("Log({})", cid))?;
// Counter to generate unique IDs for temporary image files.
let mut next_temporary_image_id = 0;
@@ -1563,7 +1563,6 @@
}
fn clone_or_prepare_logger_fd(
- debug_config: &DebugConfig,
fd: Option<&ParcelFileDescriptor>,
tag: String,
) -> Result<Option<File>, Status> {
@@ -1571,10 +1570,6 @@
return Ok(Some(clone_file(fd)?));
}
- if !debug_config.should_prepare_console_output() {
- return Ok(None);
- };
-
let (read_fd, write_fd) =
pipe().context("Failed to create pipe").or_service_specific_exception(-1)?;
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index f9fbd16..37618c7 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -25,6 +25,7 @@
use log::{debug, error, info};
use semver::{Version, VersionReq};
use nix::{fcntl::OFlag, unistd::pipe2, unistd::Uid, unistd::User};
+use nix::unistd::dup;
use regex::{Captures, Regex};
use rustutils::system_properties;
use shared_child::SharedChild;
@@ -35,7 +36,8 @@
use std::io::{self, Read};
use std::mem;
use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
+use std::os::fd::FromRawFd;
+use std::os::unix::io::{AsRawFd, OwnedFd};
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
@@ -872,26 +874,6 @@
}
}
-fn append_platform_devices(
- command: &mut Command,
- preserved_fds: &mut Vec<RawFd>,
- config: &CrosvmConfig,
-) -> Result<(), Error> {
- if config.vfio_devices.is_empty() {
- return Ok(());
- }
-
- let Some(dtbo) = &config.dtbo else {
- bail!("VFIO devices assigned but no DTBO available");
- };
- command.arg(format!("--device-tree-overlay={},filter", add_preserved_fd(preserved_fds, dtbo)));
-
- for device in &config.vfio_devices {
- command.arg(vfio_argument_for_platform_device(device)?);
- }
- Ok(())
-}
-
/// Starts an instance of `crosvm` to manage a new VM.
fn run_vm(
config: CrosvmConfig,
@@ -986,7 +968,7 @@
}
// Keep track of what file descriptors should be mapped to the crosvm process.
- let mut preserved_fds = config.indirect_files.iter().map(|file| file.as_raw_fd()).collect();
+ let mut preserved_fds = config.indirect_files.into_iter().map(|f| f.into()).collect();
// Setup the serial devices.
// 1. uart device: used as the output device by bootloaders and as early console by linux
@@ -997,15 +979,14 @@
//
// When [console|log]_fd is not specified, the devices are attached to sink, which means what's
// written there is discarded.
- let console_out_arg = format_serial_out_arg(&mut preserved_fds, &config.console_out_fd);
+ let console_out_arg = format_serial_out_arg(&mut preserved_fds, config.console_out_fd);
let console_in_arg = config
.console_in_fd
- .as_ref()
.map(|fd| format!(",input={}", add_preserved_fd(&mut preserved_fds, fd)))
.unwrap_or_default();
- let log_arg = format_serial_out_arg(&mut preserved_fds, &config.log_fd);
- let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
- let ramdump_arg = format_serial_out_arg(&mut preserved_fds, &config.ramdump);
+ let log_arg = format_serial_out_arg(&mut preserved_fds, config.log_fd);
+ let failure_serial_path = add_preserved_fd(&mut preserved_fds, failure_pipe_write);
+ let ramdump_arg = format_serial_out_arg(&mut preserved_fds, config.ramdump);
let console_input_device = config.console_input_device.as_deref().unwrap_or(CONSOLE_HVC0);
match console_input_device {
CONSOLE_HVC0 | CONSOLE_TTYS0 => {}
@@ -1035,11 +1016,11 @@
// /dev/hvc2
command.arg(format!("--serial={},hardware=virtio-console,num=3", &log_arg));
- if let Some(bootloader) = &config.bootloader {
+ if let Some(bootloader) = config.bootloader {
command.arg("--bios").arg(add_preserved_fd(&mut preserved_fds, bootloader));
}
- if let Some(initrd) = &config.initrd {
+ if let Some(initrd) = config.initrd {
command.arg("--initrd").arg(add_preserved_fd(&mut preserved_fds, initrd));
}
@@ -1047,25 +1028,30 @@
command.arg("--params").arg(params);
}
- for disk in &config.disks {
+ for disk in config.disks {
command.arg("--block").arg(format!(
"path={},ro={}",
- add_preserved_fd(&mut preserved_fds, &disk.image),
+ add_preserved_fd(&mut preserved_fds, disk.image),
!disk.writable,
));
}
- if let Some(kernel) = &config.kernel {
+ if let Some(kernel) = config.kernel {
command.arg(add_preserved_fd(&mut preserved_fds, kernel));
}
- let control_server_socket = UnixSeqpacketListener::bind(crosvm_control_socket_path)
+ let control_sock = UnixSeqpacketListener::bind(crosvm_control_socket_path)
.context("failed to create control server")?;
- command
- .arg("--socket")
- .arg(add_preserved_fd(&mut preserved_fds, &control_server_socket.as_raw_descriptor()));
+ command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, {
+ let dup_fd = dup(control_sock.as_raw_descriptor())?;
+ // SAFETY: UnixSeqpacketListener doesn't provide a way to convert it into a RawFd or
+ // OwnedFd. In order to provide a OwnedFd for add_preserved_fd, dup the control socket
+ // and create a OwnedFd from the duped fd. This is fine as the original fd is still
+ // closed when control_socket is dropped.
+ unsafe { OwnedFd::from_raw_fd(dup_fd) }
+ }));
- if let Some(dt_overlay) = &config.device_tree_overlay {
+ if let Some(dt_overlay) = config.device_tree_overlay {
command.arg("--device-tree-overlay").arg(add_preserved_fd(&mut preserved_fds, dt_overlay));
}
@@ -1116,15 +1102,15 @@
}
if cfg!(network) {
- if let Some(tap) = &config.tap {
- let tap_fd = tap.as_raw_fd();
- preserved_fds.push(tap_fd);
- command.arg("--net").arg(format!("tap-fd={}", tap_fd));
+ if let Some(tap) = config.tap {
+ command
+ .arg("--net")
+ .arg(format!("tap-fd={}", add_preserved_fd(&mut preserved_fds, tap)));
}
}
if cfg!(paravirtualized_devices) {
- for input_device_option in config.input_device_options.iter() {
+ for input_device_option in config.input_device_options.into_iter() {
command.arg("--input");
command.arg(match input_device_option {
InputDeviceOption::EvDev(file) => {
@@ -1172,7 +1158,19 @@
command.arg("--boost-uclamp");
}
- append_platform_devices(&mut command, &mut preserved_fds, &config)?;
+ if !config.vfio_devices.is_empty() {
+ if let Some(dtbo) = config.dtbo {
+ command.arg(format!(
+ "--device-tree-overlay={},filter",
+ add_preserved_fd(&mut preserved_fds, dtbo)
+ ));
+ } else {
+ bail!("VFIO devices assigned but no DTBO available");
+ }
+ };
+ for device in config.vfio_devices {
+ command.arg(vfio_argument_for_platform_device(&device)?);
+ }
debug!("Preserving FDs {:?}", preserved_fds);
command.preserved_fds(preserved_fds);
@@ -1242,15 +1240,16 @@
/// Adds the file descriptor for `file` to `preserved_fds`, and returns a string of the form
/// "/proc/self/fd/N" where N is the file descriptor.
-fn add_preserved_fd(preserved_fds: &mut Vec<RawFd>, file: &dyn AsRawFd) -> String {
- let fd = file.as_raw_fd();
+fn add_preserved_fd<F: Into<OwnedFd>>(preserved_fds: &mut Vec<OwnedFd>, file: F) -> String {
+ let fd = file.into();
+ let raw_fd = fd.as_raw_fd();
preserved_fds.push(fd);
- format!("/proc/self/fd/{}", fd)
+ format!("/proc/self/fd/{}", raw_fd)
}
/// Adds the file descriptor for `file` (if any) to `preserved_fds`, and returns the appropriate
/// string for a crosvm `--serial` flag. If `file` is none, creates a dummy sink device.
-fn format_serial_out_arg(preserved_fds: &mut Vec<RawFd>, file: &Option<File>) -> String {
+fn format_serial_out_arg(preserved_fds: &mut Vec<OwnedFd>, file: Option<File>) -> String {
if let Some(file) = file {
format!("type=file,path={}", add_preserved_fd(preserved_fds, file))
} else {
diff --git a/android/vm/src/run.rs b/android/vm/src/run.rs
index cb15802..b3743ae 100644
--- a/android/vm/src/run.rs
+++ b/android/vm/src/run.rs
@@ -36,7 +36,7 @@
use std::fs::File;
use std::io;
use std::io::{Read, Write};
-use std::os::unix::io::{AsRawFd, FromRawFd};
+use std::os::fd::AsFd;
use std::path::{Path, PathBuf};
use vmclient::{ErrorCode, VmInstance};
use vmconfig::{get_debug_level, open_parcel_file, VmConfig};
@@ -365,16 +365,6 @@
}
/// Safely duplicate the file descriptor.
-fn duplicate_fd<T: AsRawFd>(file: T) -> io::Result<File> {
- let fd = file.as_raw_fd();
- // SAFETY: This just duplicates a file descriptor which we know to be valid, and we check for an
- // an error.
- let dup_fd = unsafe { libc::dup(fd) };
- if dup_fd < 0 {
- Err(io::Error::last_os_error())
- } else {
- // SAFETY: We have just duplicated the file descriptor so we own it, and `from_raw_fd` takes
- // ownership of it.
- Ok(unsafe { File::from_raw_fd(dup_fd) })
- }
+fn duplicate_fd<T: AsFd>(file: T) -> io::Result<File> {
+ Ok(file.as_fd().try_clone_to_owned()?.into())
}
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index e4b374f..6a1cc00 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -53,7 +53,7 @@
"name": "debian",
"disks": [
{
- "image": "/data/local/tmp/debian.img
+ "image": "/data/local/tmp/debian.img",
"partitions": [],
"writable": true
}
@@ -306,15 +306,15 @@
```
To see console logs only, check
-`/data/data/com{,.google}.android.virtualization.vmlauncher/files/console.log`
+`/data/data/com{,.google}.android.virtualization.vmlauncher/files/${vm_name}.log`
For HSUM enabled devices,
-`/data/user/${current_user_id}/com{,.google}.android.virtualization.vmlauncher/files/console.log`
+`/data/user/${current_user_id}/com{,.google}.android.virtualization.vmlauncher/files/${vm_name}.log`
You can monitor console out as follows
```shell
-$ adb shell 'su root tail +0 -F /data/user/$(am get-current-user)/com{,.google}.android.virtualization.vmlauncher/files/console.log'
+$ adb shell 'su root tail +0 -F /data/user/$(am get-current-user)/com{,.google}.android.virtualization.vmlauncher/files/${vm_name}.log'
```
For ChromiumOS, you can enter to the console via SSH connection. Check your IP
diff --git a/docs/device_trees.md b/docs/device_trees.md
new file mode 100644
index 0000000..003e7be
--- /dev/null
+++ b/docs/device_trees.md
@@ -0,0 +1,211 @@
+# Device Trees in AVF
+
+This document aims to provide a centralized overview of the way the Android
+Virtualization Framework (AVF) composes and validates the device tree (DT)
+received by protected guest kernels, such as [Microdroid].
+
+[Microdroid]: ../guest/microdroid/README.md
+
+## Context
+
+As of Android 15, AVF only supports protected virtual machines (pVMs) on
+AArch64. On this architecture, the Linux kernel and many other embedded projects
+have adopted the [device tree format][dtspec] as the way to describe the
+platform to the software. This includes so-called "[platform devices]" (which are
+non-discoverable MMIO-based devices), CPUs (number, characteristics, ...),
+memory (address and size), and more.
+
+With virtualization, it is common for the virtual machine manager (VMM, e.g.
+crosvm or QEMU), typically a host userspace process, to generate the DT as it
+configures the virtual platform. In the case of AVF, the threat model prevents
+the guest from trusting the host and therefore the DT must be validated by a
+trusted entity. To avoid adding extra logic in the highly-privileged hypervisor,
+AVF relies on [pvmfw], a small piece of code that runs in the context of the
+guest (but before the guest kernel), loaded by the hypervisor, which validates
+the untrusted device tree. If any anomaly is detected, pvmfw aborts the boot of
+the guest. As a result, the guest kernel can trust the DT it receives.
+
+The DT sanitized by pvmfw is received by guests following the [Linux boot
+protocol][booting.txt] and includes both virtual and physical devices, which are
+hardly distinguishable from the guest's perspective (although the context could
+provide information helping to identify the nature of the device e.g. a
+virtio-blk device is likely to be virtual while a platform accelerator would be
+physical). The guest is not expected to treat physical devices differently from
+virtual devices and this distinction is therefore not relevant.
+
+```
+┌────────┐ ┌───────┐ valid ┌───────┐
+│ crosvm ├──{input DT}──►│ pvmfw ├───────{guest DT}──►│ guest │
+└────────┘ └───┬───┘ └───────┘
+ │ invalid
+ └───────────► SYSTEM RESET
+```
+
+[dtspec]: https://www.devicetree.org/specifications
+[platform devices]: https://docs.kernel.org/driver-api/driver-model/platform.html
+[pvmfw]: ../guest/pvmfw/README.md
+[booting.txt]: https://www.kernel.org/doc/Documentation/arm64/booting.txt
+
+## Device Tree Generation (Host-side)
+
+crosvm describes the virtual platform to the guest by generating a DT
+enumerating the memory region, virtual CPUs, virtual devices, and other
+properties (e.g. ramdisk, cmdline, ...). For physical devices (assigned using
+VFIO), it generates simple nodes describing the fundamental properties it
+configures for the devices i.e. `<reg>`, `<interrupts>`, `<iommus>`
+(respectively referring to IPA ranges, vIRQs, and pvIOMMUs).
+
+It is possible for the caller of crosvm to pass more DT properties or nodes to
+the guest by providing device tree overlays (DTBO) to crosvm. These overlays get
+applied after the DT describing the configured platform has been generated, the
+final result getting passed to the guest.
+
+For physical devices, crosvm supports applying a "filtered" subset of the DTBO
+received, where subnodes are only kept if they have a label corresponding to an
+assigned VFIO device. This allows the caller to always pass the same overlay,
+irrespective of which physical devices are being assigned, greatly simplifying
+the logic of the caller. This makes it possible for crosvm to support complex
+nodes for physical devices without including device-specific logic as any extra
+property (e.g. `<compatible>`) will be passed through the overlay and added to
+the final DT in a generic way. This _vm DTBO_ is read from an AVB-verified
+partition (see `ro.boot.hypervisor.vm_dtbo_idx`).
+
+Otherwise, if the `filter` option is not used, crosvm applies the overlay fully.
+This can be used to supplement the guest DT with nodes and properties which are
+not tied to particular assigned physical devices or emulated virtual devices. In
+particular, `virtualizationservice` currently makes use of it to pass
+AVF-specific properties.
+
+```
+ ┌─►{DTBO,filter}─┐
+┌─────────┐ │ │ ┌────────┐
+│ virtmgr ├─┼────►{DTBO}─────┼─►│ crosvm ├───►{guest DT}───► ...
+└─────────┘ │ │ └────────┘
+ └─►{VFIO sysfs}──┘
+```
+
+## Device Tree Sanitization
+
+pvmfw intercepts the boot sequence of the guest and locates the DT generated by
+the VMM through the VMM-guest ABI. A design goal of pvmfw is to have as little
+side-effect as possible on the guest so that the VMM can keep the illusion that
+it configured and booted the guest directly and the guest does not need to rely
+or expect pvmfw to have performed any noticeable work (a noteworthy exception
+being the memory region describing the [DICE chain]). As a result, both VMM and
+guest can mostly use the same logic between protected and non-protected VMs
+(where pvmfw does not run) and keep the simpler VMM-guest execution model they
+are used to. In the context of pvmfw and DT validation, the final DT passed by
+crosvm to the guest is typically referred to as the _input DT_.
+
+```
+┌────────┐ ┌───────┐ ┌───────┐
+│ crosvm ├───►{input DT}───►│ pvmfw │───►{guest DT}───►│ guest │
+└────────┘ └───────┘ └───────┘
+ ▲ ▲
+ ┌─────┐ ┌─►{VM DTBO}──────┘ │
+ │ ABL ├──┤ │
+ └─────┘ └─►{ref. DT}──────────┘
+```
+
+[DICE chain]: ../guest/pvmfw/README.md#virtual-platform-dice-chain-handover
+
+### Virtual Platform
+
+The DT sanitization policy in pvmfw matches the virtual platform defined by
+crosvm and its implementation is therefore tightly coupled with it (this is one
+reason why AVF expects pvmfw and the VMM to be updated in sync). It covers
+fundamental properties of the platform (e.g. location of main memory,
+properties of CPUs, layout of the interrupt controller, ...) and the properties
+of (sometimes optional) virtual devices supported by crosvm and used by AVF
+guests.
+
+### Physical Devices
+
+To support device assignment, pvmfw needs to be able to validate physical
+platform-specific device properties. To achieve this in a platform-agnostic way,
+pvmfw receives a DT overlay (called the _VM DTBO_) from the Android Bootloader
+(ABL), containing a description of all the assignable devices. By detecting
+which devices have been assigned using platform-specific reserved DT labels, it
+can validate the properties of the physical devices through [generic logic].
+pvmfw also verifies with the hypervisor that the guest addresses from the DT
+have been properly mapped to the expected physical addresses of the devices; see
+[_Getting started with device assignment_][da.md].
+
+Note that, as pvmfw runs within the context of an individual pVM, it cannot
+detect abuses by the host of device assignment across guests (e.g.
+simultaneously assigning the same device to multiple guests), and it is the
+responsibility of the hypervisor to enforce this isolation. AVF also relies on
+the hypervisor to clear the state of the device on donation and (most
+importantly) on return to the host so that pvmfw does not need to access the
+assigned devices.
+
+[generic logic]: ../guest/pvmfw/src/device_assignment.rs
+[da.md]: ../docs/device_assignment.md
+
+### Extra Properties (Security-Sensitive)
+
+Some AVF use-cases require passing platform-specific inputs to protected guests.
+If these are security-sensitive, they must also be validated before being used
+by the guest. In most cases, the DT property is platform-agnostic (and supported
+by the generic guest) but its value is platform-specific. The _reference DT_ is
+an [input of pvmfw][pvmfw-config] (received from the loader) and used to
+validate DT entries which are:
+
+- security-sensitive: the host should not be able to tamper with these values
+- not confidential: the property is visible to the host (as it generates it)
+- Same across VMs: the property (if present) must be same across all instances
+- possibly optional: pvmfw does not abort the boot if the entry is missing
+
+[pvmfw-config]: ../guest/pvmfw/README.md#configuration-data-format
+
+### Extra Properties (Host-Generated)
+
+Finally, to allow the host to generate values that vary between guests (and
+which therefore can't be described using one the previous mechanisms), pvmfw
+treats the subtree of the input DT at path `/avf/untrusted` differently: it only
+performs minimal sanitization on it, allowing the host to pass arbitrary,
+unsanitized DT entries. Therefore, this subtree must be used with extra
+validation by guests e.g. only accessed by path (where the name, "`untrusted`",
+acts as a reminder), with no assumptions about the presence or correctness of
+nodes or properties, without expecting properties to be well-formed, ...
+
+In particular, pvmfw prevents other nodes from linking to this subtree
+(`<phandle>` is rejected) and limits the risk of guests unexpectedly parsing it
+other than by path (`<compatible>` is also rejected) but guests must not support
+non-standard ways of binding against nodes by property as they would then be
+vulnerable to attacks from a malicious host.
+
+### Implementation details
+
+DT sanitization is currently implemented in pvmfw by parsing the input DT into
+temporary data structures and pruning a built-in device tree (called the
+_platform DT_; see [platform.dts]) accordingly. For device assignment, it prunes
+the received VM DTBO to only keep the devices that have actually been assigned
+(as the overlay contains all assignable devices of the platform).
+
+[platform.dts]: ../guest/pvmfw/platform.dts
+
+## DT for guests
+
+### AVF-specific properties and nodes
+
+For Microdroid and other AVF guests, some special DT entries are defined:
+
+- the `/chosen/avf,new-instance` flag, set when pvmfw triggered the generation
+ of a new set of CDIs (see DICE) _i.e._ the pVM instance was booted for the
+ first time. This should be used by the next stages to synchronise the
+ generation of new CDIs and detect a malicious host attempting to force only
+ one stage to do so. This property becomes obsolete (and might not be set) when
+ [deferred rollback protection] is used by the guest kernel;
+
+- the `/chosen/avf,strict-boot` flag, always set for protected VMs and can be
+ used by guests to enable extra validation;
+
+- the `/avf/untrusted/defer-rollback-protection` flag controls [deferred
+ rollback protection] on devices and for guests which support it;
+
+- the host-allocated `/avf/untrusted/instance-id` is used to assign a unique
+ identifier to the VM instance & is used for differentiating VM secrets as well
+ as by guest OS to index external storage such as Secretkeeper.
+
+[deferred rollback protection]: ../docs/updatable_vm.md#deferring-rollback-protection
diff --git a/guest/pvmfw/README.md b/guest/pvmfw/README.md
index cc5ae71..4712d77 100644
--- a/guest/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -405,32 +405,25 @@
### Handover ABI
After verifying the guest kernel, pvmfw boots it using the Linux ABI described
-above. It uses the device tree to pass the following:
+above. It uses the device tree to pass [AVF-specific properties][dt.md] and the
+DICE chain:
-- a reserved memory node containing the produced DICE chain:
-
- ```
- / {
- reserved-memory {
- #address-cells = <0x02>;
- #size-cells = <0x02>;
- ranges;
- dice {
- compatible = "google,open-dice";
- no-map;
- reg = <0x0 0x7fe0000>, <0x0 0x1000>;
- };
+```
+/ {
+ reserved-memory {
+ #address-cells = <0x02>;
+ #size-cells = <0x02>;
+ ranges;
+ dice {
+ compatible = "google,open-dice";
+ no-map;
+ reg = <0x0 0x7fe0000>, <0x0 0x1000>;
};
};
- ```
+};
+```
-- the `/chosen/avf,new-instance` flag, set when pvmfw generated a new secret
- (_i.e._ the pVM instance was booted for the first time). This should be used
- by the next stages to ensure that an attacker isn't trying to force new
- secrets to be generated by one stage, in isolation;
-
-- the `/chosen/avf,strict-boot` flag, always set and can be used by guests to
- enable extra validation
+[dt.md]: ../docs/device_trees.md#avf_specific-properties-and-nodes
### Guest Image Signing
diff --git a/guest/pvmfw/image.ld b/guest/pvmfw/image.ld
index 18bb3ba..fb26806 100644
--- a/guest/pvmfw/image.ld
+++ b/guest/pvmfw/image.ld
@@ -18,5 +18,4 @@
{
image : ORIGIN = 0x7fc00000, LENGTH = 2M
writable_data : ORIGIN = 0x7fe00000, LENGTH = 2M
- dtb_region : ORIGIN = 0x80000000, LENGTH = 2M
}
diff --git a/guest/pvmfw/src/dice.rs b/guest/pvmfw/src/dice.rs
index 8be73a4..470711f 100644
--- a/guest/pvmfw/src/dice.rs
+++ b/guest/pvmfw/src/dice.rs
@@ -36,8 +36,10 @@
#[derive(Debug)]
pub enum Error {
/// Error in CBOR operations
+ #[allow(dead_code)]
CborError(ciborium::value::Error),
/// Error in DICE operations
+ #[allow(dead_code)]
DiceError(diced_open_dice::DiceError),
}
diff --git a/guest/rialto/idmap.S b/guest/rialto/idmap.S
index 7281d9b..eb4d823 100644
--- a/guest/rialto/idmap.S
+++ b/guest/rialto/idmap.S
@@ -28,9 +28,8 @@
.set .PAGE_SIZE, .SZ_4K
.set .ORIGIN_ADDR, 2 * .SZ_1G
-.set .DTB_ADDR, .ORIGIN_ADDR + (0 * .SZ_2M)
-.set .TEXT_ADDR, .ORIGIN_ADDR + (1 * .SZ_2M)
-.set .DATA_ADDR, .ORIGIN_ADDR + (2 * .SZ_2M)
+.set .TEXT_ADDR, .ORIGIN_ADDR + (0 * .SZ_2M)
+.set .DATA_ADDR, .ORIGIN_ADDR + (1 * .SZ_2M)
.set .L_TT_TYPE_BLOCK, 0x1
.set .L_TT_TYPE_PAGE, 0x3
@@ -60,7 +59,7 @@
.balign .PAGE_SIZE, 0 // unmapped
/* level 2 */
-0: .quad .L_BLOCK_RO | .DTB_ADDR // DT provided by VMM
+0:
.quad .L_BLOCK_MEM_XIP | .TEXT_ADDR // 2 MiB of DRAM containing image
.quad .L_BLOCK_MEM | .DATA_ADDR // 2 MiB of writable DRAM
.balign .PAGE_SIZE, 0 // unmapped
diff --git a/guest/rialto/image.ld b/guest/rialto/image.ld
index 368acbb..3bf910c 100644
--- a/guest/rialto/image.ld
+++ b/guest/rialto/image.ld
@@ -16,7 +16,6 @@
MEMORY
{
- dtb_region : ORIGIN = 0x80000000, LENGTH = 2M
- image : ORIGIN = 0x80200000, LENGTH = 2M
- writable_data : ORIGIN = 0x80400000, LENGTH = 2M
+ image : ORIGIN = 0x80000000, LENGTH = 2M
+ writable_data : ORIGIN = 0x80200000, LENGTH = 2M
}
diff --git a/guest/rialto/src/main.rs b/guest/rialto/src/main.rs
index 930f4e8..a98ec25 100644
--- a/guest/rialto/src/main.rs
+++ b/guest/rialto/src/main.rs
@@ -47,6 +47,7 @@
use vmbase::{
configure_heap,
fdt::SwiotlbInfo,
+ generate_image_header,
hyp::{get_mem_sharer, get_mmio_guard},
layout::{self, crosvm, UART_PAGE_ADDR},
main,
@@ -232,5 +233,6 @@
}
}
+generate_image_header!();
main!(main);
configure_heap!(SIZE_128KB * 2);
diff --git a/guest/rialto/tests/test.rs b/guest/rialto/tests/test.rs
index cf5630f..7c0d9dc 100644
--- a/guest/rialto/tests/test.rs
+++ b/guest/rialto/tests/test.rs
@@ -34,7 +34,7 @@
use service_vm_fake_chain::client_vm::{
fake_client_vm_dice_artifacts, fake_sub_components, SubComponent,
};
-use service_vm_manager::ServiceVm;
+use service_vm_manager::{ServiceVm, VM_MEMORY_MB};
use std::fs;
use std::fs::File;
use std::panic;
@@ -59,7 +59,7 @@
// The test is skipped if the feature flag |dice_changes| is not enabled, because when
// the flag is off, the DICE chain is truncated in the pvmfw, and the service VM cannot
// verify the chain due to the missing entries in the chain.
- check_processing_requests(VmType::ProtectedVm)
+ check_processing_requests(VmType::ProtectedVm, None)
} else {
warn!("pVMs are not supported on device, skipping test");
Ok(())
@@ -68,11 +68,18 @@
#[test]
fn process_requests_in_non_protected_vm() -> Result<()> {
- check_processing_requests(VmType::NonProtectedVm)
+ check_processing_requests(VmType::NonProtectedVm, None)
}
-fn check_processing_requests(vm_type: VmType) -> Result<()> {
- let mut vm = start_service_vm(vm_type)?;
+#[ignore] // TODO(b/360077974): Figure out why this is flaky.
+#[test]
+fn process_requests_in_non_protected_vm_with_extra_ram() -> Result<()> {
+ const MEMORY_MB: i32 = 300;
+ check_processing_requests(VmType::NonProtectedVm, Some(MEMORY_MB))
+}
+
+fn check_processing_requests(vm_type: VmType, vm_memory_mb: Option<i32>) -> Result<()> {
+ let mut vm = start_service_vm(vm_type, vm_memory_mb)?;
check_processing_reverse_request(&mut vm)?;
let key_pair = check_processing_generating_key_pair_request(&mut vm)?;
@@ -285,7 +292,7 @@
Ok(())
}
-fn start_service_vm(vm_type: VmType) -> Result<ServiceVm> {
+fn start_service_vm(vm_type: VmType, vm_memory_mb: Option<i32>) -> Result<ServiceVm> {
android_logger::init_once(
android_logger::Config::default()
.with_tag("rialto")
@@ -297,19 +304,20 @@
}));
// We need to start the thread pool for Binder to work properly, especially link_to_death.
ProcessState::start_thread_pool();
- ServiceVm::start_vm(vm_instance(vm_type)?, vm_type)
+ ServiceVm::start_vm(vm_instance(vm_type, vm_memory_mb)?, vm_type)
}
-fn vm_instance(vm_type: VmType) -> Result<VmInstance> {
+fn vm_instance(vm_type: VmType, vm_memory_mb: Option<i32>) -> Result<VmInstance> {
match vm_type {
VmType::ProtectedVm => {
+ assert!(vm_memory_mb.is_none());
service_vm_manager::protected_vm_instance(PathBuf::from(INSTANCE_IMG_PATH))
}
- VmType::NonProtectedVm => nonprotected_vm_instance(),
+ VmType::NonProtectedVm => nonprotected_vm_instance(vm_memory_mb.unwrap_or(VM_MEMORY_MB)),
}
}
-fn nonprotected_vm_instance() -> Result<VmInstance> {
+fn nonprotected_vm_instance(memory_mib: i32) -> Result<VmInstance> {
let rialto = File::open(UNSIGNED_RIALTO_PATH).context("Failed to open Rialto kernel binary")?;
// Do not use `#allocateInstanceId` to generate the instance ID because the method
// also adds an instance ID to the database it manages.
@@ -317,10 +325,10 @@
let mut instance_id = [0u8; 64];
rand_bytes(&mut instance_id).unwrap();
let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
- name: String::from("Non protected rialto"),
- bootloader: Some(ParcelFileDescriptor::new(rialto)),
+ name: format!("Non protected rialto ({memory_mib}MiB)"),
+ kernel: Some(ParcelFileDescriptor::new(rialto)),
protectedVm: false,
- memoryMib: 300,
+ memoryMib: memory_mib,
platformVersion: "~1.0".to_string(),
instanceId: instance_id,
..Default::default()
diff --git a/guest/vmbase_example/Android.bp b/guest/vmbase_example/Android.bp
new file mode 100644
index 0000000..ff7bd83
--- /dev/null
+++ b/guest/vmbase_example/Android.bp
@@ -0,0 +1,114 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_ffi_static {
+ name: "libvmbase_example",
+ defaults: ["vmbase_ffi_defaults"],
+ crate_name: "vmbase_example",
+ srcs: ["src/main.rs"],
+ rustlibs: [
+ "libaarch64_paging",
+ "libcstr",
+ "libdiced_open_dice_nostd",
+ "libfdtpci",
+ "liblibfdt",
+ "liblog_rust_nostd",
+ "libvirtio_drivers",
+ "libvmbase",
+ ],
+}
+
+genrule {
+ name: "vmbase_image.ld.S.mm",
+ // Soong won't let us use cc_object to preprocess *.ld.S files because it
+ // can't resist feeding any and all *.S files to the assembler, which fails
+ // because linker scripts typically aren't valid assembly. Also, cc_object
+ // rejects inputs that don't end in one of .{s,S,c,cpp,cc,cxx,mm}. So keep
+ // the proper extension (.ld.S) for the file in VCS and use this convoluted
+ // extra step to please Soong by pretending that our linker script is in
+ // fact some Object C++ code, which fortunately it doesn't try to compile.
+ srcs: ["image.ld.S"],
+ out: ["image.ld.S.mm"],
+ cmd: "cp $(in) $(out)",
+ visibility: ["//visibility:private"],
+}
+
+cc_defaults {
+ name: "vmbase_example_ld_defaults",
+ defaults: ["vmbase_cc_defaults"],
+ cflags: [
+ "-E",
+ "-P",
+ "-xassembler-with-cpp", // allow C preprocessor directives
+ ],
+ srcs: [":vmbase_image.ld.S.mm"],
+ visibility: ["//visibility:private"],
+}
+
+cc_object {
+ name: "vmbase_example_bios.ld",
+ defaults: ["vmbase_example_ld_defaults"],
+ cflags: ["-DVMBASE_EXAMPLE_IS_BIOS"],
+}
+
+cc_object {
+ name: "vmbase_example_kernel.ld",
+ defaults: ["vmbase_example_ld_defaults"],
+ cflags: ["-DVMBASE_EXAMPLE_IS_KERNEL"],
+}
+
+cc_defaults {
+ name: "vmbase_example_elf_defaults",
+ defaults: ["vmbase_elf_defaults"],
+ srcs: [
+ "idmap.S",
+ ],
+ static_libs: [
+ "libvmbase_example",
+ ],
+}
+
+cc_binary {
+ name: "vmbase_example_bios",
+ defaults: ["vmbase_example_elf_defaults"],
+ asflags: ["-DVMBASE_EXAMPLE_IS_BIOS"],
+ linker_scripts: [
+ ":vmbase_example_bios.ld",
+ ":vmbase_sections",
+ ],
+}
+
+cc_binary {
+ name: "vmbase_example_kernel",
+ defaults: ["vmbase_example_elf_defaults"],
+ asflags: ["-DVMBASE_EXAMPLE_IS_KERNEL"],
+ linker_scripts: [
+ ":vmbase_example_kernel.ld",
+ ":vmbase_sections",
+ ],
+}
+
+raw_binary {
+ name: "vmbase_example_bios_bin",
+ stem: "vmbase_example_bios.bin",
+ src: ":vmbase_example_bios",
+ enabled: false,
+ target: {
+ android_arm64: {
+ enabled: true,
+ },
+ },
+}
+
+raw_binary {
+ name: "vmbase_example_kernel_bin",
+ stem: "vmbase_example_kernel.bin",
+ src: ":vmbase_example_kernel",
+ enabled: false,
+ target: {
+ android_arm64: {
+ enabled: true,
+ },
+ },
+}
diff --git a/libs/libvmbase/example/idmap.S b/guest/vmbase_example/idmap.S
similarity index 82%
rename from libs/libvmbase/example/idmap.S
rename to guest/vmbase_example/idmap.S
index 71a6ade..881850c 100644
--- a/libs/libvmbase/example/idmap.S
+++ b/guest/vmbase_example/idmap.S
@@ -43,8 +43,16 @@
.quad .L_TT_TYPE_TABLE + 0f // up to 1 GiB of DRAM
.fill 509, 8, 0x0 // 509 GiB of remaining VA space
- /* level 2 */
-0: .quad .L_BLOCK_MEM | 0x80000000 // DT provided by VMM
+0: /* level 2 */
+#if defined(VMBASE_EXAMPLE_IS_BIOS)
+ .quad 0 // 2 MiB not mapped (DT)
.quad .L_BLOCK_MEM_XIP | 0x80200000 // 2 MiB of DRAM containing image
.quad .L_BLOCK_MEM | 0x80400000 // 2 MiB of writable DRAM
.fill 509, 8, 0x0
+#elif defined(VMBASE_EXAMPLE_IS_KERNEL)
+ .quad .L_BLOCK_MEM_XIP | 0x80000000 // 2 MiB of DRAM containing image
+ .quad .L_BLOCK_MEM | 0x80200000 // 2 MiB of writable DRAM
+ .fill 510, 8, 0x0
+#else
+#error "Unexpected vmbase_example mode: failed to generate idmap"
+#endif
diff --git a/libs/libvmbase/example/image.ld b/guest/vmbase_example/image.ld.S
similarity index 73%
rename from libs/libvmbase/example/image.ld
rename to guest/vmbase_example/image.ld.S
index 368acbb..a5cd965 100644
--- a/libs/libvmbase/example/image.ld
+++ b/guest/vmbase_example/image.ld.S
@@ -16,7 +16,13 @@
MEMORY
{
- dtb_region : ORIGIN = 0x80000000, LENGTH = 2M
+#if defined(VMBASE_EXAMPLE_IS_BIOS)
image : ORIGIN = 0x80200000, LENGTH = 2M
writable_data : ORIGIN = 0x80400000, LENGTH = 2M
+#elif defined(VMBASE_EXAMPLE_IS_KERNEL)
+ image : ORIGIN = 0x80000000, LENGTH = 2M
+ writable_data : ORIGIN = 0x80200000, LENGTH = 2M
+#else
+#error "Unexpected vmbase_example mode: failed to generate image layout"
+#endif
}
diff --git a/libs/libvmbase/example/src/exceptions.rs b/guest/vmbase_example/src/exceptions.rs
similarity index 100%
rename from libs/libvmbase/example/src/exceptions.rs
rename to guest/vmbase_example/src/exceptions.rs
diff --git a/libs/libvmbase/example/src/layout.rs b/guest/vmbase_example/src/layout.rs
similarity index 90%
rename from libs/libvmbase/example/src/layout.rs
rename to guest/vmbase_example/src/layout.rs
index fc578bc..50ecb7e 100644
--- a/libs/libvmbase/example/src/layout.rs
+++ b/guest/vmbase_example/src/layout.rs
@@ -17,20 +17,17 @@
use aarch64_paging::paging::{MemoryRegion, VirtualAddress};
use core::ops::Range;
use log::info;
-use vmbase::layout;
+use vmbase::{layout, memory::PAGE_SIZE};
/// The first 1 GiB of memory are used for MMIO.
pub const DEVICE_REGION: MemoryRegion = MemoryRegion::new(0, 0x40000000);
/// Writable data region for the stack.
pub fn boot_stack_range() -> Range<VirtualAddress> {
- const PAGE_SIZE: usize = 4 << 10;
layout::stack_range(40 * PAGE_SIZE)
}
pub fn print_addresses() {
- let dtb = layout::dtb_range();
- info!("dtb: {}..{} ({} bytes)", dtb.start, dtb.end, dtb.end - dtb.start);
let text = layout::text_range();
info!("text: {}..{} ({} bytes)", text.start, text.end, text.end - text.start);
let rodata = layout::rodata_range();
diff --git a/libs/libvmbase/example/src/main.rs b/guest/vmbase_example/src/main.rs
similarity index 87%
rename from libs/libvmbase/example/src/main.rs
rename to guest/vmbase_example/src/main.rs
index da82b17..7a3f427 100644
--- a/libs/libvmbase/example/src/main.rs
+++ b/guest/vmbase_example/src/main.rs
@@ -25,38 +25,37 @@
use crate::layout::{boot_stack_range, print_addresses, DEVICE_REGION};
use crate::pci::{check_pci, get_bar_region};
-use aarch64_paging::paging::MemoryRegion;
+use aarch64_paging::paging::VirtualAddress;
use aarch64_paging::MapError;
use alloc::{vec, vec::Vec};
+use core::mem;
use core::ptr::addr_of_mut;
use cstr::cstr;
use fdtpci::PciInfo;
use libfdt::Fdt;
use log::{debug, error, info, trace, warn, LevelFilter};
use vmbase::{
- bionic, configure_heap,
- layout::{dtb_range, rodata_range, scratch_range, text_range},
+ bionic, configure_heap, generate_image_header,
+ layout::{crosvm::FDT_MAX_SIZE, rodata_range, scratch_range, text_range},
linker, logger, main,
memory::{PageTable, SIZE_64KB},
+ util::RangeExt as _,
};
static INITIALISED_DATA: [u32; 4] = [1, 2, 3, 4];
static mut ZEROED_DATA: [u32; 10] = [0; 10];
static mut MUTABLE_DATA: [u32; 4] = [1, 2, 3, 4];
+generate_image_header!();
main!(main);
configure_heap!(SIZE_64KB);
-fn init_page_table(pci_bar_range: &MemoryRegion) -> Result<(), MapError> {
- let mut page_table = PageTable::default();
-
+fn init_page_table(page_table: &mut PageTable) -> Result<(), MapError> {
page_table.map_device(&DEVICE_REGION)?;
page_table.map_code(&text_range().into())?;
page_table.map_rodata(&rodata_range().into())?;
page_table.map_data(&scratch_range().into())?;
page_table.map_data(&boot_stack_range().into())?;
- page_table.map_rodata(&dtb_range().into())?;
- page_table.map_device(pci_bar_range)?;
info!("Activating IdMap...");
// SAFETY: page_table duplicates the static mappings for everything that the Rust code is
@@ -76,15 +75,18 @@
info!("Hello world");
info!("x0={:#018x}, x1={:#018x}, x2={:#018x}, x3={:#018x}", arg0, arg1, arg2, arg3);
print_addresses();
- assert_eq!(arg0, dtb_range().start.0 as u64);
check_data();
check_stack_guard();
+ let mut page_table = PageTable::default();
+ init_page_table(&mut page_table).unwrap();
+
info!("Checking FDT...");
- let fdt = dtb_range();
- let fdt_size = fdt.end.0 - fdt.start.0;
+ let fdt_addr = usize::try_from(arg0).unwrap();
// SAFETY: The DTB range is valid, writable memory, and we don't construct any aliases to it.
- let fdt = unsafe { core::slice::from_raw_parts_mut(fdt.start.0 as *mut u8, fdt_size) };
+ let fdt = unsafe { core::slice::from_raw_parts_mut(fdt_addr as *mut u8, FDT_MAX_SIZE) };
+ let fdt_region = (VirtualAddress(fdt_addr)..VirtualAddress(fdt_addr + fdt.len())).into();
+ page_table.map_data(&fdt_region).unwrap();
let fdt = Fdt::from_mut_slice(fdt).unwrap();
info!("FDT passed verification.");
check_fdt(fdt);
@@ -96,7 +98,13 @@
check_alloc();
- init_page_table(&get_bar_region(&pci_info)).unwrap();
+ let bar_region = get_bar_region(&pci_info);
+ if bar_region.is_within(&DEVICE_REGION) {
+ // Avoid a MapError::BreakBeforeMakeViolation.
+ info!("BAR region is within already mapped device region: skipping page table ops.");
+ } else {
+ page_table.map_device(&bar_region).unwrap();
+ }
check_data();
check_dice();
@@ -106,6 +114,10 @@
check_pci(&mut pci_root);
emit_suppressed_log();
+
+ info!("De-activating IdMap...");
+ mem::drop(page_table); // Release PageTable and switch back to idmap.S
+ info!("De-activated.");
}
fn check_stack_guard() {
diff --git a/libs/libvmbase/example/src/pci.rs b/guest/vmbase_example/src/pci.rs
similarity index 100%
rename from libs/libvmbase/example/src/pci.rs
rename to guest/vmbase_example/src/pci.rs
diff --git a/libs/dice/open_dice/Android.bp b/libs/dice/open_dice/Android.bp
index 62504d1..efe350f 100644
--- a/libs/dice/open_dice/Android.bp
+++ b/libs/dice/open_dice/Android.bp
@@ -92,6 +92,9 @@
"--ctypes-prefix=core::ffi",
"--raw-line=#![no_std]",
],
+ dylib: {
+ enabled: false,
+ },
no_stdlibs: true,
prefer_rlib: true,
stdlibs: [
diff --git a/libs/libfdt/Android.bp b/libs/libfdt/Android.bp
index 7dc9e64..b2e7b2b 100644
--- a/libs/libfdt/Android.bp
+++ b/libs/libfdt/Android.bp
@@ -16,6 +16,9 @@
"--raw-line=#![no_std]",
"--ctypes-prefix=core::ffi",
],
+ dylib: {
+ enabled: false,
+ },
static_libs: [
"libfdt",
],
diff --git a/libs/libservice_vm_manager/src/lib.rs b/libs/libservice_vm_manager/src/lib.rs
index 78ed85b..d3d86e9 100644
--- a/libs/libservice_vm_manager/src/lib.rs
+++ b/libs/libservice_vm_manager/src/lib.rs
@@ -37,12 +37,14 @@
use vmclient::{DeathReason, VmInstance};
use vsock::{VsockListener, VsockStream, VMADDR_CID_HOST};
+/// Size of virtual memory allocated to the Service VM.
+pub const VM_MEMORY_MB: i32 = 6;
+
const VIRT_DATA_DIR: &str = "/data/misc/apexdata/com.android.virt";
const RIALTO_PATH: &str = "/apex/com.android.virt/etc/rialto.bin";
const INSTANCE_IMG_NAME: &str = "service_vm_instance.img";
const INSTANCE_ID_FILENAME: &str = "service_vm_instance_id";
const INSTANCE_IMG_SIZE_BYTES: i64 = 1 << 20; // 1MB
-const MEMORY_MB: i32 = 300;
const WRITE_BUFFER_CAPACITY: usize = 512;
const READ_TIMEOUT: Duration = Duration::from_secs(10);
const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
@@ -227,11 +229,11 @@
let instance_id = get_or_allocate_instance_id(service.as_ref(), instance_id_file)?;
let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
name: String::from("Service VM"),
- bootloader: Some(ParcelFileDescriptor::new(rialto)),
+ kernel: Some(ParcelFileDescriptor::new(rialto)),
disks: vec![DiskImage { image: None, partitions: writable_partitions, writable: true }],
instanceId: instance_id,
protectedVm: true,
- memoryMib: MEMORY_MB,
+ memoryMib: VM_MEMORY_MB,
cpuTopology: CpuTopology::ONE_CPU,
platformVersion: "~1.0".to_string(),
gdbPort: 0, // No gdb
diff --git a/libs/libvmbase/example/Android.bp b/libs/libvmbase/example/Android.bp
deleted file mode 100644
index fe9de44..0000000
--- a/libs/libvmbase/example/Android.bp
+++ /dev/null
@@ -1,74 +0,0 @@
-package {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_ffi_static {
- name: "libvmbase_example",
- defaults: ["vmbase_ffi_defaults"],
- crate_name: "vmbase_example",
- srcs: ["src/main.rs"],
- rustlibs: [
- "libaarch64_paging",
- "libcstr",
- "libdiced_open_dice_nostd",
- "libfdtpci",
- "liblibfdt",
- "liblog_rust_nostd",
- "libvirtio_drivers",
- "libvmbase",
- ],
-}
-
-cc_binary {
- name: "vmbase_example",
- defaults: ["vmbase_elf_defaults"],
- srcs: [
- "idmap.S",
- ],
- static_libs: [
- "libvmbase_example",
- ],
- linker_scripts: [
- "image.ld",
- ":vmbase_sections",
- ],
-}
-
-raw_binary {
- name: "vmbase_example_bin",
- stem: "vmbase_example.bin",
- src: ":vmbase_example",
- enabled: false,
- target: {
- android_arm64: {
- enabled: true,
- },
- },
-}
-
-rust_test {
- name: "vmbase_example.integration_test",
- crate_name: "vmbase_example_test",
- srcs: ["tests/test.rs"],
- prefer_rlib: true,
- edition: "2021",
- rustlibs: [
- "android.system.virtualizationservice-rust",
- "libandroid_logger",
- "libanyhow",
- "liblibc",
- "liblog_rust",
- "libnix",
- "libvmclient",
- ],
- data: [
- ":vmbase_example_bin",
- ],
- test_suites: ["general-tests"],
- enabled: false,
- target: {
- android_arm64: {
- enabled: true,
- },
- },
-}
diff --git a/libs/libvmbase/sections.ld b/libs/libvmbase/sections.ld
index c7ef0ec..7d464bc 100644
--- a/libs/libvmbase/sections.ld
+++ b/libs/libvmbase/sections.ld
@@ -29,17 +29,13 @@
SECTIONS
{
- .dtb (NOLOAD) : {
- dtb_begin = .;
- . += LENGTH(dtb_region);
- dtb_end = .;
- } >dtb_region
-
/*
* Collect together the code. This is page aligned so it can be mapped
* as executable-only.
*/
.text : ALIGN(4096) {
+ KEEP(*(.init.head));
+ *(.init.head)
text_begin = .;
*(.init.entry)
*(.init.*)
diff --git a/libs/libvmbase/src/entry.rs b/libs/libvmbase/src/entry.rs
index ad633ed..99f28fc 100644
--- a/libs/libvmbase/src/entry.rs
+++ b/libs/libvmbase/src/entry.rs
@@ -18,7 +18,7 @@
bionic, console, heap, hyp,
layout::{UART_ADDRESSES, UART_PAGE_ADDR},
logger,
- memory::{SIZE_16KB, SIZE_4KB},
+ memory::{PAGE_SIZE, SIZE_16KB, SIZE_4KB},
power::{reboot, shutdown},
rand,
};
@@ -129,3 +129,37 @@
}
};
}
+
+/// Prepends a Linux kernel header to the generated binary image.
+///
+/// See https://docs.kernel.org/arch/arm64/booting.html
+/// ```
+#[macro_export]
+macro_rules! generate_image_header {
+ () => {
+ #[cfg(not(target_endian = "little"))]
+ compile_error!("Image header uses wrong endianness: bootloaders expect LE!");
+
+ core::arch::global_asm!(
+ // This section gets linked at the start of the image.
+ ".section .init.head, \"ax\"",
+ // This prevents the macro from being called more than once.
+ ".global image_header",
+ "image_header:",
+ // Linux uses a special NOP to be ELF-compatible; we're not.
+ "nop", // code0
+ "b entry", // code1
+ ".quad 0", // text_offset
+ ".quad bin_end - image_header", // image_size
+ ".quad (1 << 1)", // flags (PAGE_SIZE=4KiB)
+ ".quad 0", // res2
+ ".quad 0", // res3
+ ".quad 0", // res4
+ ".ascii \"ARM\x64\"", // magic
+ ".long 0", // res5
+ );
+ };
+}
+
+// If this fails, the image header flags are out-of-sync with PAGE_SIZE!
+static_assertions::const_assert_eq!(PAGE_SIZE, SIZE_4KB);
diff --git a/libs/libvmbase/src/layout.rs b/libs/libvmbase/src/layout.rs
index 5ac435f..adcb2fa 100644
--- a/libs/libvmbase/src/layout.rs
+++ b/libs/libvmbase/src/layout.rs
@@ -60,11 +60,6 @@
}};
}
-/// Memory reserved for the DTB.
-pub fn dtb_range() -> Range<VirtualAddress> {
- linker_region!(dtb_begin, dtb_end)
-}
-
/// Executable code.
pub fn text_range() -> Range<VirtualAddress> {
linker_region!(text_begin, text_end)
diff --git a/libs/libvmbase/src/util.rs b/libs/libvmbase/src/util.rs
index 8c230a1..e52ac8e 100644
--- a/libs/libvmbase/src/util.rs
+++ b/libs/libvmbase/src/util.rs
@@ -14,6 +14,7 @@
//! Utility functions.
+use aarch64_paging::paging::MemoryRegion;
use core::ops::Range;
/// Flatten [[T; N]] into &[T]
@@ -91,3 +92,13 @@
self.start < other.end && other.start < self.end
}
}
+
+impl RangeExt for MemoryRegion {
+ fn is_within(&self, other: &Self) -> bool {
+ self.start() >= other.start() && self.end() <= other.end()
+ }
+
+ fn overlaps(&self, other: &Self) -> bool {
+ self.start() < other.end() && other.start() < self.end()
+ }
+}
diff --git a/libs/libvmclient/src/lib.rs b/libs/libvmclient/src/lib.rs
index 88072a7..fe86504 100644
--- a/libs/libvmclient/src/lib.rs
+++ b/libs/libvmclient/src/lib.rs
@@ -45,6 +45,7 @@
use shared_child::SharedChild;
use std::io::{self, Read};
use std::process::Command;
+use std::process::Stdio;
use std::{
fmt::{self, Debug, Formatter},
fs::File,
@@ -90,16 +91,16 @@
let (client_fd, server_fd) = posix_socketpair()?;
let mut command = Command::new(VIRTMGR_PATH);
+ command.stdin(Stdio::null());
+ command.stdout(Stdio::null());
+ command.stderr(Stdio::null());
+ // Can't use BorrowedFd as it doesn't implement Display
command.arg("--rpc-server-fd").arg(format!("{}", server_fd.as_raw_fd()));
command.arg("--ready-fd").arg(format!("{}", ready_fd.as_raw_fd()));
- command.preserved_fds(vec![server_fd.as_raw_fd(), ready_fd.as_raw_fd()]);
+ command.preserved_fds(vec![server_fd, ready_fd]);
SharedChild::spawn(&mut command)?;
- // Drop FDs that belong to virtmgr.
- drop(server_fd);
- drop(ready_fd);
-
// Wait for the child to signal that the RpcBinder server is ready
// by closing its end of the pipe.
let _ignored = File::from(wait_fd).read(&mut [0]);
diff --git a/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
new file mode 100644
index 0000000..8591c8d
--- /dev/null
+++ b/libs/vm_launcher_lib/Android.bp
@@ -0,0 +1,13 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "vm_launcher_lib",
+ srcs: ["java/**/*.java"],
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.virt",
+ ],
+ sdk_version: "system_current",
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
new file mode 100644
index 0000000..c5bc5fb
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import java.util.List;
+
+public class VmLauncherServices {
+ private static final String TAG = "VmLauncherServices";
+
+ private static final String ACTION_START_VM_LAUNCHER_SERVICE =
+ "android.virtualization.START_VM_LAUNCHER_SERVICE";
+
+ private static final int RESULT_START = 0;
+ private static final int RESULT_STOP = 1;
+ private static final int RESULT_ERROR = 2;
+ private static final int RESULT_IPADDR = 3;
+ private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+ private static Intent buildVmLauncherServiceIntent(Context context) {
+ Intent i = new Intent();
+ i.setAction(ACTION_START_VM_LAUNCHER_SERVICE);
+
+ Intent intent = new Intent(ACTION_START_VM_LAUNCHER_SERVICE);
+ PackageManager pm = context.getPackageManager();
+ List<ResolveInfo> resolveInfos =
+ pm.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfos == null || resolveInfos.size() != 1) {
+ Log.e(TAG, "cannot find a service to handle ACTION_START_VM_LAUNCHER_SERVICE");
+ return null;
+ }
+ String packageName = resolveInfos.get(0).serviceInfo.packageName;
+
+ i.setPackage(packageName);
+ return i;
+ }
+
+ public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback) {
+ Intent i = buildVmLauncherServiceIntent(context);
+ if (i == null) {
+ return;
+ }
+ ResultReceiver resultReceiver =
+ new ResultReceiver(new Handler(Looper.myLooper())) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (callback == null) {
+ return;
+ }
+ switch (resultCode) {
+ case RESULT_START:
+ callback.onVmStart();
+ return;
+ case RESULT_STOP:
+ callback.onVmStop();
+ return;
+ case RESULT_ERROR:
+ callback.onVmError();
+ return;
+ case RESULT_IPADDR:
+ callback.onIpAddrAvailable(resultData.getString(KEY_VM_IP_ADDR));
+ return;
+ }
+ }
+ };
+ i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver));
+ context.startForegroundService(i);
+ }
+
+ public interface VmLauncherServiceCallback {
+ void onVmStart();
+
+ void onVmStop();
+
+ void onVmError();
+
+ void onIpAddrAvailable(String ipAddr);
+ }
+
+ private static ResultReceiver getResultReceiverForIntent(ResultReceiver r) {
+ Parcel parcel = Parcel.obtain();
+ r.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ r = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return r;
+ }
+}
diff --git a/tests/authfs/common/src/open_then_run.rs b/tests/authfs/common/src/open_then_run.rs
index e5e33eb..a9004b0 100644
--- a/tests/authfs/common/src/open_then_run.rs
+++ b/tests/authfs/common/src/open_then_run.rs
@@ -24,7 +24,7 @@
use log::{debug, error};
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
-use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
+use std::os::unix::io::{OwnedFd, RawFd};
use std::process::Command;
// `PseudoRawFd` is just an integer and not necessarily backed by a real FD. It is used to denote
@@ -38,8 +38,8 @@
}
impl OwnedFdMapping {
- fn as_fd_mapping(&self) -> FdMapping {
- FdMapping { parent_fd: self.owned_fd.as_raw_fd(), child_fd: self.target_fd }
+ fn into_fd_mapping(self) -> FdMapping {
+ FdMapping { parent_fd: self.owned_fd, child_fd: self.target_fd }
}
}
@@ -148,9 +148,9 @@
// Set up FD mappings in the child process.
let mut fd_mappings = Vec::new();
- fd_mappings.extend(args.ro_file_fds.iter().map(OwnedFdMapping::as_fd_mapping));
- fd_mappings.extend(args.rw_file_fds.iter().map(OwnedFdMapping::as_fd_mapping));
- fd_mappings.extend(args.dir_fds.iter().map(OwnedFdMapping::as_fd_mapping));
+ fd_mappings.extend(args.ro_file_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
+ fd_mappings.extend(args.rw_file_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
+ fd_mappings.extend(args.dir_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
command.fd_mappings(fd_mappings)?;
debug!("Spawning {:?}", command);
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index ec1a553..0e59a01 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -268,6 +268,7 @@
/* fullDebug */ false,
(builder) -> builder.setCpuTopology(CPU_TOPOLOGY_ONE_CPU));
}
+
@Test
public void testMicrodroidHostCpuTopologyBootTime()
throws VirtualMachineException, InterruptedException, IOException {
@@ -280,10 +281,7 @@
@Test
public void testMicrodroidDebugBootTime()
throws VirtualMachineException, InterruptedException, IOException {
- runBootTimeTest(
- "test_vm_boot_time_debug",
- /* fullDebug */ true,
- (builder) -> builder);
+ runBootTimeTest("test_vm_boot_time_debug", /* fullDebug */ true, (builder) -> builder);
}
private void testMicrodroidDebugBootTime_withVendorBase(File vendorDiskImage) throws Exception {
@@ -366,12 +364,12 @@
@Test
public void testVirtioBlkSeqReadRate() throws Exception {
- testVirtioBlkReadRate(/*isRand=*/ false);
+ testVirtioBlkReadRate(/* isRand= */ false);
}
@Test
public void testVirtioBlkRandReadRate() throws Exception {
- testVirtioBlkReadRate(/*isRand=*/ true);
+ testVirtioBlkReadRate(/* isRand= */ true);
}
private void testVirtioBlkReadRate(boolean isRand) throws Exception {
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index 4a61016..e2956f2 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -69,6 +69,7 @@
/** Boot time test related variables */
private static final int REINSTALL_APEX_RETRY_INTERVAL_MS = 5 * 1000;
+
private static final int REINSTALL_APEX_TIMEOUT_SEC = 15;
private static final int COMPILE_STAGED_APEX_RETRY_INTERVAL_MS = 10 * 1000;
private static final int COMPILE_STAGED_APEX_TIMEOUT_SEC = 540;
@@ -122,17 +123,18 @@
@Test
public void testNoLongHypSections() throws Exception {
- String[] hypEvents = {
- "hyp_enter", "hyp_exit"
- };
+ String[] hypEvents = {"hyp_enter", "hyp_exit"};
- assumeTrue("Skip without hypervisor tracing",
- KvmHypTracer.isSupported(getDevice(), hypEvents));
+ assumeTrue(
+ "Skip without hypervisor tracing",
+ KvmHypTracer.isSupported(getDevice(), hypEvents));
KvmHypTracer tracer = new KvmHypTracer(getDevice(), hypEvents);
String result = tracer.run(COMPOSD_CMD_BIN + " test-compile");
assertWithMessage("Failed to test compilation VM.")
- .that(result).ignoringCase().contains("all ok");
+ .that(result)
+ .ignoringCase()
+ .contains("all ok");
SimpleStats stats = tracer.getDurationStats();
reportMetric(stats.getData(), "hyp_sections", "s");
@@ -141,32 +143,37 @@
@Test
public void testPsciMemProtect() throws Exception {
- String[] hypEvents = {
- "psci_mem_protect"
- };
+ String[] hypEvents = {"psci_mem_protect"};
- assumeTrue("Skip without hypervisor tracing",
- KvmHypTracer.isSupported(getDevice(), hypEvents));
+ assumeTrue(
+ "Skip without hypervisor tracing",
+ KvmHypTracer.isSupported(getDevice(), hypEvents));
KvmHypTracer tracer = new KvmHypTracer(getDevice(), hypEvents);
/* We need to wait for crosvm to die so all the VM pages are reclaimed */
String result = tracer.run(COMPOSD_CMD_BIN + " test-compile && killall -w crosvm || true");
assertWithMessage("Failed to test compilation VM.")
- .that(result).ignoringCase().contains("all ok");
+ .that(result)
+ .ignoringCase()
+ .contains("all ok");
List<Integer> values = tracer.getPsciMemProtect();
assertWithMessage("PSCI MEM_PROTECT events not recorded")
- .that(values.size()).isGreaterThan(2);
+ .that(values.size())
+ .isGreaterThan(2);
assertWithMessage("PSCI MEM_PROTECT counter not starting from 0")
- .that(values.get(0)).isEqualTo(0);
+ .that(values.get(0))
+ .isEqualTo(0);
assertWithMessage("PSCI MEM_PROTECT counter not ending with 0")
- .that(values.get(values.size() - 1)).isEqualTo(0);
+ .that(values.get(values.size() - 1))
+ .isEqualTo(0);
assertWithMessage("PSCI MEM_PROTECT counter didn't increment")
- .that(Collections.max(values)).isGreaterThan(0);
+ .that(Collections.max(values))
+ .isGreaterThan(0);
}
@Test
@@ -182,9 +189,7 @@
@Test
public void testSettingsAppStartupTime() throws Exception {
- String[] launchIntentPackages = {
- "com.android.settings"
- };
+ String[] launchIntentPackages = {"com.android.settings"};
String launchIntentPackage = findSupportedPackage(launchIntentPackages);
assume().withMessage("No supported settings package").that(launchIntentPackage).isNotNull();
appStartupHelper(launchIntentPackage);
@@ -193,28 +198,34 @@
private void appStartupHelper(String launchIntentPackage) throws Exception {
assumeTrue(
"Skip on non-protected VMs",
- ((TestDevice) getDevice()).supportsMicrodroid(/*protectedVm=*/ true));
+ ((TestDevice) getDevice()).supportsMicrodroid(/* protectedVm= */ true));
StartupTimeMetricCollection mCollection =
new StartupTimeMetricCollection(getPackageName(launchIntentPackage), ROUND_COUNT);
getAppStartupTime(launchIntentPackage, mCollection);
- reportMetric(mCollection.mAppBeforeVmRunTotalTime,
+ reportMetric(
+ mCollection.mAppBeforeVmRunTotalTime,
"app_startup/" + mCollection.getPkgName() + "/total_time/before_vm",
"ms");
- reportMetric(mCollection.mAppBeforeVmRunWaitTime,
+ reportMetric(
+ mCollection.mAppBeforeVmRunWaitTime,
"app_startup/" + mCollection.getPkgName() + "/wait_time/before_vm",
"ms");
- reportMetric(mCollection.mAppDuringVmRunTotalTime,
+ reportMetric(
+ mCollection.mAppDuringVmRunTotalTime,
"app_startup/" + mCollection.getPkgName() + "/total_time/during_vm",
"ms");
- reportMetric(mCollection.mAppDuringVmRunWaitTime,
+ reportMetric(
+ mCollection.mAppDuringVmRunWaitTime,
"app_startup/" + mCollection.getPkgName() + "/wait_time/during_vm",
"ms");
- reportMetric(mCollection.mAppAfterVmRunTotalTime,
+ reportMetric(
+ mCollection.mAppAfterVmRunTotalTime,
"app_startup/" + mCollection.getPkgName() + "/total_time/after_vm",
"ms");
- reportMetric(mCollection.mAppAfterVmRunWaitTime,
+ reportMetric(
+ mCollection.mAppAfterVmRunWaitTime,
"app_startup/" + mCollection.getPkgName() + "/wait_time/after_vm",
"ms");
}
@@ -234,8 +245,9 @@
for (String pkgName : pkgNameList) {
String appPkg = getPackageName(pkgName);
- String hasPackage = android.run("pm list package | grep -w " + appPkg + " 1> /dev/null"
- + "; echo $?");
+ String hasPackage =
+ android.run(
+ "pm list package | grep -w " + appPkg + " 1> /dev/null" + "; echo $?");
assertNotNull(hasPackage);
if (hasPackage.equals("0")) {
@@ -390,8 +402,8 @@
}
}
- private int getFreeMemoryInfoMb(CommandRunner android) throws DeviceNotAvailableException,
- IllegalArgumentException {
+ private int getFreeMemoryInfoMb(CommandRunner android)
+ throws DeviceNotAvailableException, IllegalArgumentException {
int freeMemory = 0;
String content = android.runForResult("cat /proc/meminfo").getStdout().trim();
String[] lines = content.split("[\r\n]+");
@@ -410,8 +422,8 @@
throws DeviceNotAvailableException, InterruptedException {
android.run("input keyevent", "KEYCODE_WAKEUP");
Thread.sleep(500);
- final String ret = android.runForResult("dumpsys nfc | grep 'mScreenState='")
- .getStdout().trim();
+ final String ret =
+ android.runForResult("dumpsys nfc | grep 'mScreenState='").getStdout().trim();
if (ret != null && ret.contains("ON_LOCKED")) {
android.run("input keyevent", "KEYCODE_MENU");
}
@@ -429,8 +441,9 @@
String[] bootKeyVal = bootLoaderPhase.split(":");
String key = String.format("%s%s", BOOTLOADER_PREFIX, bootKeyVal[0]);
- bootloaderTime.computeIfAbsent(key,
- k -> new ArrayList<>()).add(Double.parseDouble(bootKeyVal[1]));
+ bootloaderTime
+ .computeIfAbsent(key, k -> new ArrayList<>())
+ .add(Double.parseDouble(bootKeyVal[1]));
// SW is the time spent on the warning screen. So ignore it in
// final boot time calculation.
if (BOOTLOADER_PHASE_SW.equalsIgnoreCase(bootKeyVal[0])) {
@@ -438,8 +451,9 @@
}
bootLoaderTotalTime += Double.parseDouble(bootKeyVal[1]);
}
- bootloaderTime.computeIfAbsent(BOOTLOADER_TIME,
- k -> new ArrayList<>()).add(bootLoaderTotalTime);
+ bootloaderTime
+ .computeIfAbsent(BOOTLOADER_TIME, k -> new ArrayList<>())
+ .add(bootLoaderTotalTime);
}
}
@@ -518,7 +532,9 @@
android.runWithTimeout(
3 * 60 * 1000, COMPOSD_CMD_BIN + " staged-apex-compile");
assertWithMessage("Failed to compile staged APEX. Reason: " + result)
- .that(result).ignoringCase().contains("all ok");
+ .that(result)
+ .ignoringCase()
+ .contains("all ok");
CLog.i("Success to compile staged APEX. Result: " + result);
@@ -546,22 +562,23 @@
try {
CommandRunner android = new CommandRunner(getDevice());
- String packagesOutput =
- android.run("pm list packages -f --apex-only");
+ String packagesOutput = android.run("pm list packages -f --apex-only");
- Pattern p = Pattern.compile(
- "package:(.*)=(com(?:\\.google)?\\.android\\.art)$", Pattern.MULTILINE);
+ Pattern p =
+ Pattern.compile(
+ "package:(.*)=(com(?:\\.google)?\\.android\\.art)$",
+ Pattern.MULTILINE);
Matcher m = p.matcher(packagesOutput);
assertWithMessage("ART module not found. Packages are:\n" + packagesOutput)
- .that(m.find())
- .isTrue();
+ .that(m.find())
+ .isTrue();
String artApexPath = m.group(1);
- CommandResult result = android.runForResult(
- "pm install --apex " + artApexPath);
+ CommandResult result = android.runForResult("pm install --apex " + artApexPath);
assertWithMessage("Failed to install APEX. Reason: " + result)
- .that(result.getExitCode()).isEqualTo(0);
+ .that(result.getExitCode())
+ .isEqualTo(0);
CLog.i("Success to install APEX. Result: " + result);
diff --git a/tests/ferrochrome/AndroidTest.xml b/tests/ferrochrome/AndroidTest.xml
index 9eaaed3..6c975be 100644
--- a/tests/ferrochrome/AndroidTest.xml
+++ b/tests/ferrochrome/AndroidTest.xml
@@ -35,13 +35,19 @@
<option name="run-command" value="mkdir /data/local/tmp" />
<option name="teardown-command" value="pkill vmlauncher" />
<option name="teardown-command" value="rm /data/local/tmp/chromiumos_base_image.bin" />
+ <option name="teardown-command" value="rm -rf /data/local/tmp/ferrochrome_screenshots" />
</target_preparer>
- <test class="com.android.tradefed.testtype.binary.ExecutableHostTest" >
+ <test class="com.android.tradefed.testtype.binary.ExecutableHostTest">
<option name="binary" value="ferrochrome-tests" />
<option name="relative-path-execution" value="true" />
<option name="runtime-hint" value="10m" />
<option name="per-binary-timeout" value="20m" />
</test>
+
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/data/local/tmp/ferrochrome_screenshots" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
</configuration>
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
index 683b82e..c68e8a9 100755
--- a/tests/ferrochrome/ferrochrome.sh
+++ b/tests/ferrochrome/ferrochrome.sh
@@ -21,12 +21,13 @@
FECR_GS_URL="https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public"
FECR_DEFAULT_VERSION="R128-15958.0.0"
+FECR_DEFAULT_SCREENSHOT_DIR="/data/local/tmp/ferrochrome_screenshots" # Hardcoded at AndroidTest.xml
FECR_TEST_IMAGE="chromiumos_test_image"
FECR_BASE_IMAGE="chromiumos_base_image"
FECR_DEVICE_DIR="/data/local/tmp"
FECR_IMAGE_VM_CONFIG_JSON="chromiumos_base_image.bin" # hardcoded at vm_config.json
FECR_CONFIG_PATH="/data/local/tmp/vm_config.json" # hardcoded at VmLauncherApp
-FECR_CONSOLE_LOG_PATH="/data/data/\${pkg_name}/files/console.log"
+FECR_CONSOLE_LOG_PATH="files/cros.log" # log file name is ${vm_name}.log
FECR_TEST_IMAGE_BOOT_COMPLETED_LOG="Have fun and send patches!"
FECR_BASE_IMAGE_BOOT_COMPLETED_LOG="Chrome started, our work is done, exiting"
FECR_BOOT_TIMEOUT="300" # 5 minutes (300 seconds)
@@ -74,6 +75,7 @@
fecr_verbose=""
fecr_image="${FECR_DEFAULT_IMAGE}"
fecr_boot_completed_log="${FECR_DEFAULT_BOOT_COMPLETED_LOG}"
+fecr_screenshot_dir="${FECR_DEFAULT_SCREENSHOT_DIR}"
# Parse parameters
while (( "${#}" )); do
@@ -153,18 +155,20 @@
adb shell svc power stayon true
adb shell wm dismiss-keyguard
+echo "Granting runtime permissions to ensure VmLauncher is focused"
+adb shell pm grant ${pkg_name} android.permission.RECORD_AUDIO
+
echo "Starting ferrochrome"
adb shell am start-activity -a ${ACTION_NAME} > /dev/null
-if [[ $(adb shell getprop ro.fw.mu.headless_system_user) == "true" ]]; then
- current_user=$(adb shell am get-current-user)
- log_path="/data/user/${current_user}/${pkg_name}/files/console.log"
-else
- log_path="/data/data/${pkg_name}/files/console.log"
-fi
+# HSUM aware log path
+current_user=$(adb shell am get-current-user)
+log_path="/data/user/${current_user}/${pkg_name}/${FECR_CONSOLE_LOG_PATH}"
fecr_start_time=${EPOCHSECONDS}
+adb shell mkdir -p "${fecr_screenshot_dir}"
while [[ $((EPOCHSECONDS - fecr_start_time)) -lt ${FECR_BOOT_TIMEOUT} ]]; do
+ adb shell screencap -p "${fecr_screenshot_dir}/screenshot-${EPOCHSECONDS}.png"
adb shell grep -soF \""${fecr_boot_completed_log}"\" "${log_path}" && exit 0 || true
sleep 10
done
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java b/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
index dd68d6a..8f93d1e 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
@@ -28,8 +28,8 @@
public static String getMetricPrefix(String debugTag) {
return "avf_perf"
- + ((debugTag != null && !debugTag.isEmpty()) ? "[" + debugTag + "]" : "")
- + "/";
+ + ((debugTag != null && !debugTag.isEmpty()) ? "[" + debugTag + "]" : "")
+ + "/";
}
public MetricsProcessor(String prefix) {
@@ -41,8 +41,8 @@
* a {@link Map} with the corresponding keys equal to [mPrefix + name +
* _[min|max|average|stdev]_ + unit].
*/
- public Map<String, Double> computeStats(List<? extends Number> metrics, String name,
- String unit) {
+ public Map<String, Double> computeStats(
+ List<? extends Number> metrics, String name, String unit) {
List<Double> values = new ArrayList<>(metrics.size());
for (Number metric : metrics) {
values.add(metric.doubleValue());
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java b/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
index e058674..c4aba81 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
@@ -69,13 +69,13 @@
}
/** Gets global memory metrics key and values mapping */
- public static Map<String, Long> getProcessMemoryMap(
- Function<String, String> shellExecutor) throws IOException {
+ public static Map<String, Long> getProcessMemoryMap(Function<String, String> shellExecutor)
+ throws IOException {
// The input file of parseMemoryInfo need a header string as the key of output entries.
// /proc/meminfo doesn't have this line so add one as the key.
String header = "device memory info\n";
- List<SMapEntry> entries = parseMemoryInfo(header
- + shellExecutor.apply("cat /proc/meminfo"));
+ List<SMapEntry> entries =
+ parseMemoryInfo(header + shellExecutor.apply("cat /proc/meminfo"));
if (entries.size() != 1) {
throw new RuntimeException(
"expected one entry in /proc/meminfo, got " + entries.size());
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 8169376..135d947 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -108,15 +108,15 @@
protected final void grantPermission(String permission) {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
UiAutomation uiAutomation = instrumentation.getUiAutomation();
- uiAutomation.grantRuntimePermission(instrumentation.getContext().getPackageName(),
- permission);
+ uiAutomation.grantRuntimePermission(
+ instrumentation.getContext().getPackageName(), permission);
}
protected final void revokePermission(String permission) {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
UiAutomation uiAutomation = instrumentation.getUiAutomation();
- uiAutomation.revokeRuntimePermission(instrumentation.getContext().getPackageName(),
- permission);
+ uiAutomation.revokeRuntimePermission(
+ instrumentation.getContext().getPackageName(), permission);
}
protected final void setMaxPerformanceTaskProfile() throws IOException {
@@ -233,12 +233,11 @@
}
protected void assumeVsrCompliant() {
- boolean featureCheck = mCtx.getPackageManager().hasSystemFeature(FEATURE_WATCH) ||
- mCtx.getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE) ||
- mCtx.getPackageManager().hasSystemFeature(FEATURE_LEANBACK);
- assume().withMessage("This device is not VSR compliant")
- .that(featureCheck)
- .isFalse();
+ boolean featureCheck =
+ mCtx.getPackageManager().hasSystemFeature(FEATURE_WATCH)
+ || mCtx.getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE)
+ || mCtx.getPackageManager().hasSystemFeature(FEATURE_LEANBACK);
+ assume().withMessage("This device is not VSR compliant").that(featureCheck).isFalse();
}
protected boolean isGsi() {
@@ -256,8 +255,9 @@
// Cuttlefish/Goldfish on Arm 64 doesn't and cannot support any form of virtualization,
// so there's no point running any of these tests.
- assume().withMessage("Virtualization not supported on Arm64 Cuttlefish/Goldfish."
- + " b/341889915")
+ assume().withMessage(
+ "Virtualization not supported on Arm64 Cuttlefish/Goldfish."
+ + " b/341889915")
.that(isCuttlefishArm64() || isGoldfishArm64())
.isFalse();
}
@@ -288,7 +288,8 @@
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")
+ if (log.contains("microdroid_manager")
+ && log.contains("executing main task")
&& !mPayloadStartedNanoTime.isPresent()) {
mPayloadStartedNanoTime = OptionalLong.of(System.nanoTime());
}
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/CommandResultSubject.java b/tests/hostside/helper/java/com/android/microdroid/test/host/CommandResultSubject.java
index 2e9d078..1d292eb 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/CommandResultSubject.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/CommandResultSubject.java
@@ -25,9 +25,7 @@
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
-/**
- * A <a href="https://github.com/google/truth">Truth</a> subject for {@link CommandResult}.
- */
+/** A <a href="https://github.com/google/truth">Truth</a> subject for {@link CommandResult}. */
public class CommandResultSubject extends Subject {
private final CommandResult mActual;
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java b/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
index 5c72358..3814cdd 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
@@ -17,22 +17,23 @@
package com.android.microdroid.test.host;
import static com.google.common.truth.Truth.assertWithMessage;
+
import static org.junit.Assert.assertNotNull;
-import com.android.microdroid.test.host.CommandRunner;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.SimpleStats;
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
-import java.io.BufferedReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+
import javax.annotation.Nonnull;
class KvmHypEvent {
@@ -42,16 +43,14 @@
public final String args;
public final boolean valid;
- private static final Pattern LOST_EVENT_PATTERN = Pattern.compile(
- "^CPU:[0-9]* \\[LOST ([0-9]*) EVENTS\\]");
+ private static final Pattern LOST_EVENT_PATTERN =
+ Pattern.compile("^CPU:[0-9]* \\[LOST ([0-9]*) EVENTS\\]");
public KvmHypEvent(String str) {
Matcher matcher = LOST_EVENT_PATTERN.matcher(str);
- if (matcher.find())
- throw new OutOfMemoryError("Lost " + matcher.group(1) + " events");
+ if (matcher.find()) throw new OutOfMemoryError("Lost " + matcher.group(1) + " events");
- Pattern pattern = Pattern.compile(
- "^\\[([0-9]*)\\][ \t]*([0-9]*\\.[0-9]*): (\\S+) (.*)");
+ Pattern pattern = Pattern.compile("^\\[([0-9]*)\\][ \t]*([0-9]*\\.[0-9]*): (\\S+) (.*)");
matcher = pattern.matcher(str);
if (!matcher.find()) {
@@ -72,8 +71,7 @@
}
public String toString() {
- return String.format(
- "[%03d]\t%f: %s %s", cpu, timestamp, name, args);
+ return String.format("[%03d]\t%f: %s %s", cpu, timestamp, name, args);
}
}
@@ -99,16 +97,16 @@
}
public static boolean isSupported(ITestDevice device, String[] events) throws Exception {
- for (String event: events) {
- if (!device.doesFileExist(HYP_TRACING_ROOT + eventDir(event) + "/enable"))
- return false;
+ for (String event : events) {
+ if (!device.doesFileExist(HYP_TRACING_ROOT + eventDir(event) + "/enable")) return false;
}
return true;
}
public KvmHypTracer(@Nonnull ITestDevice device, String[] events) throws Exception {
assertWithMessage("Hypervisor events " + String.join(",", events) + " not supported")
- .that(isSupported(device, events)).isTrue();
+ .that(isSupported(device, events))
+ .isTrue();
mDevice = device;
mRunner = new CommandRunner(mDevice);
@@ -123,8 +121,7 @@
setNode("tracing_on", 0);
mRunner.run("echo 0 | tee " + HYP_TRACING_ROOT + "events/*/*/enable");
setNode("buffer_size_kb", DEFAULT_BUF_SIZE_KB);
- for (String event: mHypEvents)
- setNode(eventDir(event) + "/enable", 1);
+ for (String event : mHypEvents) setNode(eventDir(event) + "/enable", 1);
setNode("trace", 0);
/* Cat each per-cpu trace_pipe in its own tmp file in the background */
@@ -147,8 +144,10 @@
/* Wait for cat to finish reading the pipe interface before killing it */
for (int i = 0; i < mNrCpus; i++) {
- cmd += "while $(test '$(ps -o S -p $CPU" + i
- + "_TRACE_PIPE_PID | tail -n 1)' = 'R'); do sleep 1; done;";
+ cmd +=
+ "while $(test '$(ps -o S -p $CPU"
+ + i
+ + "_TRACE_PIPE_PID | tail -n 1)' = 'R'); do sleep 1; done;";
cmd += "kill -9 $CPU" + i + "_TRACE_PIPE_PID;";
}
cmd += "wait";
@@ -164,7 +163,7 @@
mRunner.run("rm -f " + cmd_script);
- for (String t: trace_pipes) {
+ for (String t : trace_pipes) {
File trace = mDevice.pullFile(t);
assertNotNull(trace);
mTraces.add(trace);
@@ -190,12 +189,10 @@
KvmHypEvent event;
String l;
- if ((l = br.readLine()) == null)
- return null;
+ if ((l = br.readLine()) == null) return null;
event = new KvmHypEvent(l);
- if (!event.valid)
- return null;
+ if (!event.valid) return null;
return event;
}
@@ -205,9 +202,10 @@
SimpleStats stats = new SimpleStats();
assertWithMessage("KvmHypTracer() is missing events " + String.join(",", reqEvents))
- .that(hasEvents(reqEvents)).isTrue();
+ .that(hasEvents(reqEvents))
+ .isTrue();
- for (File trace: mTraces) {
+ for (File trace : mTraces) {
BufferedReader br = new BufferedReader(new FileReader(trace));
double last = 0.0, hyp_enter = 0.0;
String prev_event = "";
@@ -219,20 +217,18 @@
throw new ParseException("Incorrect CPU number: " + cpu, 0);
double cur = hypEvent.timestamp;
- if (cur < last)
- throw new ParseException("Time must not go backward: " + cur, 0);
+ if (cur < last) throw new ParseException("Time must not go backward: " + cur, 0);
last = cur;
String event = hypEvent.name;
if (event.equals(prev_event)) {
- throw new ParseException("Hyp event found twice in a row: " +
- trace + " - " + hypEvent, 0);
+ throw new ParseException(
+ "Hyp event found twice in a row: " + trace + " - " + hypEvent, 0);
}
switch (event) {
case "hyp_exit":
- if (prev_event.equals("hyp_enter"))
- stats.add(cur - hyp_enter);
+ if (prev_event.equals("hyp_enter")) stats.add(cur - hyp_enter);
break;
case "hyp_enter":
hyp_enter = cur;
@@ -252,7 +248,8 @@
List<Integer> psciMemProtect = new ArrayList<>();
assertWithMessage("KvmHypTracer() is missing events " + String.join(",", reqEvents))
- .that(hasEvents(reqEvents)).isTrue();
+ .that(hasEvents(reqEvents))
+ .isTrue();
BufferedReader[] brs = new BufferedReader[mTraces.size()];
KvmHypEvent[] next = new KvmHypEvent[mTraces.size()];
@@ -266,22 +263,20 @@
double oldest = Double.MAX_VALUE;
int oldestIdx = -1;
- for (int i = 0; i < mTraces.size(); i ++) {
+ for (int i = 0; i < mTraces.size(); i++) {
if ((next[i] != null) && (next[i].timestamp < oldest)) {
oldest = next[i].timestamp;
oldestIdx = i;
}
}
- if (oldestIdx < 0)
- break;
+ if (oldestIdx < 0) break;
- Pattern pattern = Pattern.compile(
- "count=([0-9]*) was=([0-9]*)");
+ Pattern pattern = Pattern.compile("count=([0-9]*) was=([0-9]*)");
Matcher matcher = pattern.matcher(next[oldestIdx].args);
if (!matcher.find()) {
- throw new ParseException("Unexpected psci_mem_protect event: " +
- next[oldestIdx], 0);
+ throw new ParseException(
+ "Unexpected psci_mem_protect event: " + next[oldestIdx], 0);
}
int count = Integer.parseInt(matcher.group(1));
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/LogArchiver.java b/tests/hostside/helper/java/com/android/microdroid/test/host/LogArchiver.java
index 96ab543..ed753d0 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/LogArchiver.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/LogArchiver.java
@@ -27,15 +27,17 @@
/** A helper class for archiving device log files to the host's tradefed output directory. */
public abstract class LogArchiver {
- /** Copy device log (then delete) to a tradefed output directory on the host.
+ /**
+ * Copy device log (then delete) to a tradefed output directory on the host.
*
* @param logs A {@link TestLogData} that needs to be owned by the actual test case.
* @param device The device to pull the log file from.
* @param remotePath The path on the device.
* @param localName Local file name to be copied to.
*/
- public static void archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath,
- String localName) throws DeviceNotAvailableException {
+ public static void archiveLogThenDelete(
+ TestLogData logs, ITestDevice device, String remotePath, String localName)
+ throws DeviceNotAvailableException {
File logFile = device.pullFile(remotePath);
if (logFile != null) {
logs.addTestLog(localName, LogDataType.TEXT, new FileInputStreamSource(logFile));
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index cd90fbe..974a58c 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -67,8 +67,11 @@
protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000;
private static final long MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS = 500;
protected static final int MICRODROID_ADB_CONNECT_MAX_ATTEMPTS =
- (int) (MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000
- / MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS);
+ (int)
+ (MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES
+ * 60
+ * 1000
+ / MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS);
protected static final Set<String> SUPPORTED_GKI_VERSIONS =
Collections.unmodifiableSet(new HashSet(Arrays.asList("android15-6.6")));
@@ -148,8 +151,9 @@
isGsi && vendorApiLevel < 202404);
}
- public static void archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath,
- String localName) throws DeviceNotAvailableException {
+ public static void archiveLogThenDelete(
+ TestLogData logs, ITestDevice device, String remotePath, String localName)
+ throws DeviceNotAvailableException {
LogArchiver.archiveLogThenDelete(logs, device, remotePath, localName);
}
@@ -167,6 +171,7 @@
CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd);
return result.getStdout().trim();
}
+
private static String join(String... strs) {
return String.join(" ", Arrays.asList(strs));
}
@@ -197,8 +202,7 @@
throw new AssertionError("Failed to find test file " + name + " for module " + moduleName);
}
- public String getPathForPackage(String packageName)
- throws DeviceNotAvailableException {
+ public String getPathForPackage(String packageName) throws DeviceNotAvailableException {
return getPathForPackage(getDevice(), packageName);
}
@@ -210,7 +214,8 @@
CommandRunner android = new CommandRunner(device);
String pathLine = android.run("pm", "path", packageName);
assertWithMessage("Package " + packageName + " not found")
- .that(pathLine).startsWith("package:");
+ .that(pathLine)
+ .startsWith("package:");
return pathLine.substring("package:".length());
}
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 80d1fc6..0f7be20 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -19,7 +19,6 @@
import static com.android.microdroid.test.host.CommandResultSubject.command_results;
import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
-import com.android.tradefed.device.DeviceRuntimeException;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -45,6 +44,7 @@
import com.android.os.AtomsProto;
import com.android.os.StatsLog;
import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceRuntimeException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.TestDevice;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
@@ -79,13 +79,13 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import java.util.Objects;
@RunWith(DeviceJUnit4Parameterized.class)
@UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
@@ -490,6 +490,7 @@
.cpuTopology("match_host")
.protectedVm(true)
.gki(mGki)
+ .name("protected_vm_runs_pvmfw")
.build(getAndroidDevice());
// Assert
@@ -785,6 +786,7 @@
.cpuTopology("match_host")
.protectedVm(mProtectedVm)
.gki(mGki)
+ .name("test_telemetry_pushed_atoms")
.build(device);
microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
device.shutdownMicrodroid(microdroid);
@@ -816,7 +818,8 @@
assertThat(atomVmCreationRequested.getIsProtected()).isEqualTo(mProtectedVm);
assertThat(atomVmCreationRequested.getCreationSucceeded()).isTrue();
assertThat(atomVmCreationRequested.getBinderExceptionCode()).isEqualTo(0);
- assertThat(atomVmCreationRequested.getVmIdentifier()).isEqualTo("VmRunApp");
+ assertThat(atomVmCreationRequested.getVmIdentifier())
+ .isEqualTo("test_telemetry_pushed_atoms");
assertThat(atomVmCreationRequested.getConfigType())
.isEqualTo(AtomsProto.VmCreationRequested.ConfigType.VIRTUAL_MACHINE_APP_CONFIG);
assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(getDeviceNumCpus(device));
@@ -826,11 +829,11 @@
// Check VmBooted atom
AtomsProto.VmBooted atomVmBooted = data.get(1).getAtom().getVmBooted();
- assertThat(atomVmBooted.getVmIdentifier()).isEqualTo("VmRunApp");
+ assertThat(atomVmBooted.getVmIdentifier()).isEqualTo("test_telemetry_pushed_atoms");
// Check VmExited atom
AtomsProto.VmExited atomVmExited = data.get(2).getAtom().getVmExited();
- assertThat(atomVmExited.getVmIdentifier()).isEqualTo("VmRunApp");
+ assertThat(atomVmExited.getVmIdentifier()).isEqualTo("test_telemetry_pushed_atoms");
assertThat(atomVmExited.getDeathReason()).isEqualTo(AtomsProto.VmExited.DeathReason.KILLED);
assertThat(atomVmExited.getExitSignal()).isEqualTo(9);
// In CPU & memory related fields, check whether positive values are collected or not.
@@ -927,6 +930,7 @@
.memoryMib(minMemorySize())
.cpuTopology("match_host")
.protectedVm(mProtectedVm)
+ .name("test_microdroid_boots")
.gki(mGki));
}
@@ -940,6 +944,7 @@
.cpuTopology("match_host")
.protectedVm(mProtectedVm)
.gki(mGki)
+ .name("test_microdroid_ram_usage")
.build(getAndroidDevice());
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
mMicrodroidDevice.enableAdbRoot();
@@ -1205,6 +1210,7 @@
.protectedVm(mProtectedVm)
.gki(mGki)
.hugePages(true)
+ .name("test_huge_pages")
.build(getAndroidDevice());
mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
diff --git a/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java b/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java
index a77ba40..5ae5186 100644
--- a/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java
+++ b/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java
@@ -27,8 +27,8 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.util.Objects;
import java.nio.ByteBuffer;
+import java.util.Objects;
/** pvmfw.bin with custom config payloads on host. */
public final class Pvmfw {
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
index 2a6ab2d..7efbbc7 100644
--- a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
+++ b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
@@ -30,8 +30,8 @@
import com.android.tradefed.device.DeviceRuntimeException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
import org.junit.After;
import org.junit.Before;
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 1465e73..7089b33 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -335,6 +335,7 @@
testResults.assertNoException();
assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
}
+
@Test
@CddTest(requirements = {"9.17/C-1-1"})
public void autoCloseVm() throws Exception {
@@ -737,7 +738,6 @@
VirtualMachineConfig.Builder otherOsBuilder =
newBaselineBuilder().setOs("microdroid_gki-android14-6.1");
assertConfigCompatible(microdroidOsConfig, otherOsBuilder).isFalse();
-
}
private VirtualMachineConfig.Builder newBaselineBuilder() {
@@ -870,11 +870,12 @@
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- "9.17/C-1-2",
- "9.17/C-1-4",
- })
+ @CddTest(
+ requirements = {
+ "9.17/C-1-1",
+ "9.17/C-1-2",
+ "9.17/C-1-4",
+ })
public void createVmWithConfigRequiresPermission() throws Exception {
assumeSupportedDevice();
revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
@@ -890,14 +891,16 @@
SecurityException e =
assertThrows(
SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
- assertThat(e).hasMessageThat()
+ assertThat(e)
+ .hasMessageThat()
.contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- })
+ @CddTest(
+ requirements = {
+ "9.17/C-1-1",
+ })
public void deleteVm() throws Exception {
assumeSupportedDevice();
@@ -954,9 +957,10 @@
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- })
+ @CddTest(
+ requirements = {
+ "9.17/C-1-1",
+ })
public void validApkPathIsAccepted() throws Exception {
assumeSupportedDevice();
@@ -989,10 +993,7 @@
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- "9.17/C-2-1"
- })
+ @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void extraApk() throws Exception {
assumeSupportedDevice();
@@ -1044,7 +1045,7 @@
@Test
public void bootFailsWhenLowMem() throws Exception {
- for (int memMib : new int[]{ 10, 20, 40 }) {
+ for (int memMib : new int[] {10, 20, 40}) {
VirtualMachineConfig lowMemConfig =
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
.setMemoryBytes(memMib)
@@ -1061,8 +1062,9 @@
onPayloadReadyExecuted.complete(true);
super.onPayloadReady(vm);
}
+
@Override
- public void onStopped(VirtualMachine vm, int reason) {
+ public void onStopped(VirtualMachine vm, int reason) {
onStoppedExecuted.complete(true);
super.onStopped(vm, reason);
}
@@ -1210,10 +1212,7 @@
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- "9.17/C-2-7"
- })
+ @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
public void instancesOfSameVmHaveDifferentCdis() throws Exception {
assumeSupportedDevice();
// TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
@@ -1240,10 +1239,7 @@
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- "9.17/C-2-7"
- })
+ @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
public void sameInstanceKeepsSameCdis() throws Exception {
assumeSupportedDevice();
assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
@@ -1298,7 +1294,7 @@
// then pvmfw, vm_entry (Microdroid kernel) and Microdroid payload entries.
// Before Android V we did not require that vendor code contain any DICE entries
// preceding pvmfw, so the minimum is one less.
- int minDiceChainSize = getVendorApiLevel() >= 202404 ? 5 : 4;
+ int minDiceChainSize = getVendorApiLevel() > 202404 ? 5 : 4;
assertThat(diceChainSize).isAtLeast(minDiceChainSize);
} else {
// pvmfw truncates the DICE chain it gets, so we expect exactly entries for
@@ -1339,10 +1335,7 @@
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- "9.17/C-1-2"
- })
+ @CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2"})
public void accessToCdisIsRestricted() throws Exception {
assumeSupportedDevice();
@@ -1399,8 +1392,7 @@
private void assertThatPartitionIsMissing(UUID partitionUuid) throws Exception {
RandomAccessFile instanceFile = prepareInstanceImage("test_vm_integrity");
- assertThat(findPartitionDataOffset(instanceFile, partitionUuid).isPresent())
- .isFalse();
+ assertThat(findPartitionDataOffset(instanceFile, partitionUuid).isPresent()).isFalse();
}
// Flips a bit of given partition, and then see if boot fails.
@@ -1420,10 +1412,7 @@
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- "9.17/C-2-7"
- })
+ @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
public void bootFailsWhenMicrodroidDataIsCompromised() throws Exception {
// If Updatable VM is supported => No instance.img required
assumeNoUpdatableVmSupport();
@@ -1431,10 +1420,7 @@
}
@Test
- @CddTest(requirements = {
- "9.17/C-1-1",
- "9.17/C-2-7"
- })
+ @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
public void bootFailsWhenPvmFwDataIsCompromised() throws Exception {
// If Updatable VM is supported => No instance.img required
assumeNoUpdatableVmSupport();
@@ -1456,8 +1442,8 @@
BootResult bootResult = tryBootVmWithConfig(config, "test_vm_invalid_config");
assertThat(bootResult.payloadStarted).isFalse();
- assertThat(bootResult.deathReason).isEqualTo(
- VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG);
+ assertThat(bootResult.deathReason)
+ .isEqualTo(VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG);
}
@Test
@@ -2143,7 +2129,6 @@
IVmShareTestService service = connection.waitForService();
assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
-
try {
ITestService testServiceProxy = transferAndStartVm(service, vmDesc, "vm_to_share");
@@ -2627,16 +2612,15 @@
}
private long minMemoryRequired() {
- assertThat(Build.SUPPORTED_ABIS).isNotEmpty();
- String primaryAbi = Build.SUPPORTED_ABIS[0];
- switch (primaryAbi) {
- case "x86_64":
- return MIN_MEM_X86_64;
- case "arm64-v8a":
- case "arm64-v8a-hwasan":
- return MIN_MEM_ARM64;
- }
- throw new AssertionError("Unsupported ABI: " + primaryAbi);
+ assertThat(Build.SUPPORTED_ABIS).isNotEmpty();
+ String primaryAbi = Build.SUPPORTED_ABIS[0];
+ switch (primaryAbi) {
+ case "x86_64":
+ return MIN_MEM_X86_64;
+ case "arm64-v8a":
+ case "arm64-v8a-hwasan":
+ return MIN_MEM_ARM64;
+ }
+ throw new AssertionError("Unsupported ABI: " + primaryAbi);
}
-
}
diff --git a/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
index 1772e6b..27e26e5 100644
--- a/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
+++ b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
@@ -16,18 +16,19 @@
package com.android.microdroid.test;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
import android.system.virtualmachine.VirtualMachineConfig;
import com.android.compatibility.common.util.CddTest;
import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
import org.junit.Before;
-import org.junit.runners.Parameterized;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
/**
* Test that the android.permission.MANAGE_VIRTUAL_MACHINE is enforced and that an app cannot launch
diff --git a/tests/vmbase_example/Android.bp b/tests/vmbase_example/Android.bp
new file mode 100644
index 0000000..4c1aa30
--- /dev/null
+++ b/tests/vmbase_example/Android.bp
@@ -0,0 +1,31 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_test {
+ name: "vmbase_example.integration_test",
+ crate_name: "vmbase_example_test",
+ srcs: ["src/main.rs"],
+ prefer_rlib: true,
+ edition: "2021",
+ rustlibs: [
+ "android.system.virtualizationservice-rust",
+ "libandroid_logger",
+ "libanyhow",
+ "liblibc",
+ "liblog_rust",
+ "libnix",
+ "libvmclient",
+ ],
+ data: [
+ ":vmbase_example_bios_bin",
+ ":vmbase_example_kernel_bin",
+ ],
+ test_suites: ["general-tests"],
+ enabled: false,
+ target: {
+ android_arm64: {
+ enabled: true,
+ },
+ },
+}
diff --git a/libs/libvmbase/example/tests/test.rs b/tests/vmbase_example/src/main.rs
similarity index 86%
rename from libs/libvmbase/example/tests/test.rs
rename to tests/vmbase_example/src/main.rs
index 8f9fafc..e0563b7 100644
--- a/libs/libvmbase/example/tests/test.rs
+++ b/tests/vmbase_example/src/main.rs
@@ -31,13 +31,27 @@
};
use vmclient::{DeathReason, VmInstance};
-const VMBASE_EXAMPLE_PATH: &str = "vmbase_example.bin";
+const VMBASE_EXAMPLE_KERNEL_PATH: &str = "vmbase_example_kernel.bin";
+const VMBASE_EXAMPLE_BIOS_PATH: &str = "vmbase_example_bios.bin";
const TEST_DISK_IMAGE_PATH: &str = "test_disk.img";
const EMPTY_DISK_IMAGE_PATH: &str = "empty_disk.img";
-/// Runs the vmbase_example VM as an unprotected VM via VirtualizationService.
+/// Runs the vmbase_example VM as an unprotected VM kernel via VirtualizationService.
#[test]
-fn test_run_example_vm() -> Result<(), Error> {
+fn test_run_example_kernel_vm() -> Result<(), Error> {
+ run_test(Some(open_payload(VMBASE_EXAMPLE_KERNEL_PATH)?), None)
+}
+
+/// Runs the vmbase_example VM as an unprotected VM BIOS via VirtualizationService.
+#[test]
+fn test_run_example_bios_vm() -> Result<(), Error> {
+ run_test(None, Some(open_payload(VMBASE_EXAMPLE_BIOS_PATH)?))
+}
+
+fn run_test(
+ kernel: Option<ParcelFileDescriptor>,
+ bootloader: Option<ParcelFileDescriptor>,
+) -> Result<(), Error> {
android_logger::init_once(
android_logger::Config::default()
.with_tag("vmbase")
@@ -56,12 +70,6 @@
vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
let service = virtmgr.connect().context("Failed to connect to VirtualizationService")?;
- // Start example VM.
- let bootloader = ParcelFileDescriptor::new(
- File::open(VMBASE_EXAMPLE_PATH)
- .with_context(|| format!("Failed to open VM image {}", VMBASE_EXAMPLE_PATH))?,
- );
-
// Make file for test disk image.
let mut test_image = File::options()
.create(true)
@@ -91,10 +99,10 @@
let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
name: String::from("VmBaseTest"),
- kernel: None,
+ kernel,
initrd: None,
params: None,
- bootloader: Some(bootloader),
+ bootloader,
disks: vec![disk_image, empty_disk_image],
protectedVm: false,
memoryMib: 300,
@@ -142,6 +150,11 @@
Ok((reader_fd.into(), writer_fd.into()))
}
+fn open_payload(path: &str) -> Result<ParcelFileDescriptor, Error> {
+ let file = File::open(path).with_context(|| format!("Failed to open VM image {path}"))?;
+ Ok(ParcelFileDescriptor::new(file))
+}
+
struct VmLogProcessor {
reader: Option<File>,
expected: VecDeque<String>,
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index 109486c..9f606e5 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -28,8 +28,8 @@
import android.util.Log;
import com.android.microdroid.test.vmshare.IVmShareTestService;
-import com.android.microdroid.testservice.ITestService;
import com.android.microdroid.testservice.IAppCallback;
+import com.android.microdroid.testservice.ITestService;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;