Merge "open_then_run: open directory as path fd"
diff --git a/apex/Android.bp b/apex/Android.bp
index 20a863f..983253e 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -59,6 +59,7 @@
"microdroid.json",
"microdroid_uboot_env",
"microdroid_bootloader",
+ "microdroid_bootloader.avbpubkey",
"microdroid_bootconfig_normal",
"microdroid_bootconfig_app_debuggable",
"microdroid_bootconfig_full_debuggable",
diff --git a/apex/sign_virt_apex.py b/apex/sign_virt_apex.py
index 6ecd07e..77f54c4 100644
--- a/apex/sign_virt_apex.py
+++ b/apex/sign_virt_apex.py
@@ -214,11 +214,34 @@
RunCommand(args, cmd)
+def ReplaceBootloaderPubkey(args, key, bootloader, bootloader_pubkey):
+ # read old pubkey before replacement
+ with open(bootloader_pubkey, 'rb') as f:
+ old_pubkey = f.read()
+
+ # replace bootloader pubkey
+ RunCommand(args, ['avbtool', 'extract_public_key', '--key', key, '--output', bootloader_pubkey])
+
+ # read new pubkey
+ with open(bootloader_pubkey, 'rb') as f:
+ new_pubkey = f.read()
+
+ assert len(old_pubkey) == len(new_pubkey)
+
+ # replace pubkey embedded in bootloader
+ with open(bootloader, 'r+b') as bl_f:
+ pos = bl_f.read().find(old_pubkey)
+ assert pos != -1
+ bl_f.seek(pos)
+ bl_f.write(new_pubkey)
+
+
def SignVirtApex(args):
key = args.key
input_dir = args.input_dir
# target files in the Virt APEX
+ bootloader_pubkey = os.path.join(input_dir, 'etc', 'microdroid_bootloader.avbpubkey')
bootloader = os.path.join(input_dir, 'etc', 'microdroid_bootloader')
boot_img = os.path.join(input_dir, 'etc', 'fs', 'microdroid_boot-5.10.img')
vendor_boot_img = os.path.join(
@@ -226,6 +249,10 @@
super_img = os.path.join(input_dir, 'etc', 'fs', 'microdroid_super.img')
vbmeta_img = os.path.join(input_dir, 'etc', 'fs', 'microdroid_vbmeta.img')
+ # Key(pubkey) for bootloader should match with the one used to make VBmeta below
+ # while it's okay to use different keys for other image files.
+ ReplaceBootloaderPubkey(args, key, bootloader, bootloader_pubkey)
+
# re-sign bootloader, boot.img, vendor_boot.img
AddHashFooter(args, key, bootloader)
AddHashFooter(args, key, boot_img)
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 94ded00..af504a1 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -85,6 +85,7 @@
.context("Failed to open config APK idsig file")?;
let idsig_fd = ParcelFileDescriptor::new(idsig_fd);
+ // Console output and the system log output from the VM are redirected to this file.
// TODO: Send this to stdout instead? Or specify None?
let log_fd = File::create(data_dir.join("vm.log")).context("Failed to create log file")?;
let log_fd = ParcelFileDescriptor::new(log_fd);
@@ -100,7 +101,9 @@
..Default::default()
});
- let vm = service.createVm(&config, Some(&log_fd)).context("Failed to create VM")?;
+ let vm = service
+ .createVm(&config, Some(&log_fd), Some(&log_fd))
+ .context("Failed to create VM")?;
let vm_state = Arc::new(VmStateMonitor::default());
let vm_state_clone = Arc::clone(&vm_state);
diff --git a/compos/compos_key_cmd/compos_key_cmd.cpp b/compos/compos_key_cmd/compos_key_cmd.cpp
index 7bf622d..2735f2e 100644
--- a/compos/compos_key_cmd/compos_key_cmd.cpp
+++ b/compos/compos_key_cmd/compos_key_cmd.cpp
@@ -197,6 +197,7 @@
return Error() << "Failed to connect to virtualization service.";
}
+ // Console output and the system log output from the VM are redirected to this file.
ScopedFileDescriptor logFd;
if (mLogFile.empty()) {
logFd.set(dup(STDOUT_FILENO));
@@ -239,7 +240,7 @@
appConfig.memoryMib = 0; // Use default
LOG(INFO) << "Starting VM";
- auto status = service->createVm(config, logFd, &mVm);
+ auto status = service->createVm(config, logFd, logFd, &mVm);
if (!status.isOk()) {
return Error() << status.getDescription();
}
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index bc87c3c..60e50bb 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -64,47 +64,14 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
- TextView consoleView = (TextView) findViewById(R.id.consoleOutput);
- TextView payloadView = (TextView) findViewById(R.id.payloadOutput);
Button runStopButton = (Button) findViewById(R.id.runStopButton);
- ScrollView scrollView = (ScrollView) findViewById(R.id.scrollConsoleOutput);
+ TextView consoleView = (TextView) findViewById(R.id.consoleOutput);
+ TextView logView = (TextView) findViewById(R.id.logOutput);
+ TextView payloadView = (TextView) findViewById(R.id.payloadOutput);
+ ScrollView scrollConsoleView = (ScrollView) findViewById(R.id.scrollConsoleOutput);
+ ScrollView scrollLogView = (ScrollView) findViewById(R.id.scrollLogOutput);
- // When the console output or payload output is updated, append the new line to the
- // corresponding text view.
VirtualMachineModel model = new ViewModelProvider(this).get(VirtualMachineModel.class);
- model.getConsoleOutput()
- .observeForever(
- new Observer<String>() {
- @Override
- public void onChanged(String line) {
- consoleView.append(line + "\n");
- scrollView.fullScroll(View.FOCUS_DOWN);
- }
- });
- model.getPayloadOutput()
- .observeForever(
- new Observer<String>() {
- @Override
- public void onChanged(String line) {
- payloadView.append(line + "\n");
- }
- });
-
- // When the VM status is updated, change the label of the button
- model.getStatus()
- .observeForever(
- new Observer<VirtualMachine.Status>() {
- @Override
- public void onChanged(VirtualMachine.Status status) {
- if (status == VirtualMachine.Status.RUNNING) {
- runStopButton.setText("Stop");
- consoleView.setText("");
- payloadView.setText("");
- } else {
- runStopButton.setText("Run");
- }
- }
- });
// When the button is clicked, run or stop the VM
runStopButton.setOnClickListener(
@@ -119,12 +86,86 @@
}
}
});
+
+ // When the VM status is updated, change the label of the button
+ model.getStatus()
+ .observeForever(
+ new Observer<VirtualMachine.Status>() {
+ @Override
+ public void onChanged(VirtualMachine.Status status) {
+ if (status == VirtualMachine.Status.RUNNING) {
+ runStopButton.setText("Stop");
+ // Clear the outputs from the previous run
+ consoleView.setText("");
+ logView.setText("");
+ payloadView.setText("");
+ } else {
+ runStopButton.setText("Run");
+ }
+ }
+ });
+
+ // When the console, log, or payload output is updated, append the new line to the
+ // corresponding text view.
+ model.getConsoleOutput()
+ .observeForever(
+ new Observer<String>() {
+ @Override
+ public void onChanged(String line) {
+ consoleView.append(line + "\n");
+ scrollConsoleView.fullScroll(View.FOCUS_DOWN);
+ }
+ });
+ model.getLogOutput()
+ .observeForever(
+ new Observer<String>() {
+ @Override
+ public void onChanged(String line) {
+ logView.append(line + "\n");
+ scrollLogView.fullScroll(View.FOCUS_DOWN);
+ }
+ });
+ model.getPayloadOutput()
+ .observeForever(
+ new Observer<String>() {
+ @Override
+ public void onChanged(String line) {
+ payloadView.append(line + "\n");
+ }
+ });
}
- /** Models a virtual machine and console output from it. */
+ /** Reads data from an input stream and posts it to the output data */
+ static class Reader implements Runnable {
+ private final String mName;
+ private final MutableLiveData<String> mOutput;
+ private final InputStream mStream;
+
+ Reader(String name, MutableLiveData<String> output, InputStream stream) {
+ mName = name;
+ mOutput = output;
+ mStream = stream;
+ }
+
+ @Override
+ public void run() {
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(mStream));
+ String line;
+ while ((line = reader.readLine()) != null && !Thread.interrupted()) {
+ mOutput.postValue(line);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Exception while posting " + mName + " output: " + e.getMessage());
+ }
+ }
+ }
+
+ /** Models a virtual machine and outputs from it. */
public static class VirtualMachineModel extends AndroidViewModel {
private VirtualMachine mVirtualMachine;
private final MutableLiveData<String> mConsoleOutput = new MutableLiveData<>();
+ private final MutableLiveData<String> mLogOutput = new MutableLiveData<>();
private final MutableLiveData<String> mPayloadOutput = new MutableLiveData<>();
private final MutableLiveData<VirtualMachine.Status> mStatus = new MutableLiveData<>();
private ExecutorService mExecutorService;
@@ -134,20 +175,11 @@
mStatus.setValue(VirtualMachine.Status.DELETED);
}
- private static void postOutput(MutableLiveData<String> output, InputStream stream)
- throws IOException {
- BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
- String line;
- while ((line = reader.readLine()) != null && !Thread.interrupted()) {
- output.postValue(line);
- }
- }
-
/** Runs a VM */
public void run(boolean debug) {
// Create a VM and run it.
// TODO(jiyong): remove the call to idsigPath
- mExecutorService = Executors.newFixedThreadPool(3);
+ mExecutorService = Executors.newFixedThreadPool(4);
VirtualMachineCallback callback =
new VirtualMachineCallback() {
@@ -162,23 +194,8 @@
return;
}
- mService.execute(
- new Runnable() {
- @Override
- public void run() {
- try {
- postOutput(
- mPayloadOutput,
- new FileInputStream(
- stream.getFileDescriptor()));
- } catch (IOException e) {
- Log.e(
- TAG,
- "IOException while reading payload: "
- + e.getMessage());
- }
- }
- });
+ InputStream input = new FileInputStream(stream.getFileDescriptor());
+ mService.execute(new Reader("payload", mPayloadOutput, input));
}
@Override
@@ -261,29 +278,23 @@
VirtualMachineConfig config = builder.build();
VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
mVirtualMachine = vmm.getOrCreate("demo_vm", config);
+ try {
+ mVirtualMachine.setConfig(config);
+ } catch (VirtualMachineException e) {
+ mVirtualMachine.delete();
+ mVirtualMachine = vmm.create("demo_vm", config);
+ }
mVirtualMachine.run();
mVirtualMachine.setCallback(callback);
mStatus.postValue(mVirtualMachine.getStatus());
+
+ InputStream console = mVirtualMachine.getConsoleOutputStream();
+ InputStream log = mVirtualMachine.getLogOutputStream();
+ mExecutorService.execute(new Reader("console", mConsoleOutput, console));
+ mExecutorService.execute(new Reader("log", mLogOutput, log));
} catch (VirtualMachineException e) {
throw new RuntimeException(e);
}
-
- // Read console output from the VM in the background
- mExecutorService.execute(
- new Runnable() {
- @Override
- public void run() {
- try {
- postOutput(
- mConsoleOutput, mVirtualMachine.getConsoleOutputStream());
- } catch (IOException | VirtualMachineException e) {
- Log.e(
- TAG,
- "Exception while posting console output: "
- + e.getMessage());
- }
- }
- });
}
/** Stops the running VM */
@@ -303,6 +314,11 @@
return mConsoleOutput;
}
+ /** Returns the log output from the VM */
+ public LiveData<String> getLogOutput() {
+ return mLogOutput;
+ }
+
/** Returns the payload output from the VM */
public LiveData<String> getPayloadOutput() {
return mPayloadOutput;
diff --git a/demo/res/layout/activity_main.xml b/demo/res/layout/activity_main.xml
index e100027..f0e35d6 100644
--- a/demo/res/layout/activity_main.xml
+++ b/demo/res/layout/activity_main.xml
@@ -62,17 +62,50 @@
<ScrollView
android:id="@+id/scrollConsoleOutput"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="2">
- <TextView
- android:id="@+id/consoleOutput"
+ <HorizontalScrollView
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="#FFEB3B"
- android:fontFamily="monospace"
- android:textColor="#000000" />
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/consoleOutput"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#FFEB3B"
+ android:fontFamily="monospace"
+ android:textSize="10sp"
+ android:textColor="#000000" />
+ </HorizontalScrollView>
+ </ScrollView>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:text="Log output:" />
+
+ <ScrollView
+ android:id="@+id/scrollLogOutput"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:layout_weight="2">
+
+ <HorizontalScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/logOutput"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#FFEB3B"
+ android:fontFamily="monospace"
+ android:textSize="10sp"
+ android:textColor="#000000" />
+ </HorizontalScrollView>
</ScrollView>
</LinearLayout>
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 2da7ecb..63c9288 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -113,6 +113,9 @@
private @Nullable ParcelFileDescriptor mConsoleReader;
private @Nullable ParcelFileDescriptor mConsoleWriter;
+ private @Nullable ParcelFileDescriptor mLogReader;
+ private @Nullable ParcelFileDescriptor mLogWriter;
+
private final ExecutorService mExecutorService = Executors.newCachedThreadPool();
static {
@@ -297,6 +300,12 @@
mConsoleWriter = pipe[1];
}
+ if (mLogReader == null && mLogWriter == null) {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ mLogReader = pipe[0];
+ mLogWriter = pipe[1];
+ }
+
VirtualMachineAppConfig appConfig = getConfig().toParcel();
// Fill the idsig file by hashing the apk
@@ -310,7 +319,7 @@
android.system.virtualizationservice.VirtualMachineConfig vmConfigParcel =
android.system.virtualizationservice.VirtualMachineConfig.appConfig(appConfig);
- mVirtualMachine = service.createVm(vmConfigParcel, mConsoleWriter);
+ mVirtualMachine = service.createVm(vmConfigParcel, mConsoleWriter, mLogWriter);
mVirtualMachine.registerCallback(
new IVirtualMachineCallback.Stub() {
@Override
@@ -377,6 +386,14 @@
return new FileInputStream(mConsoleReader.getFileDescriptor());
}
+ /** Returns the stream object representing the log output from the virtual machine. */
+ public @NonNull InputStream getLogOutputStream() throws VirtualMachineException {
+ if (mLogReader == null) {
+ throw new VirtualMachineException("Log output not available");
+ }
+ return new FileInputStream(mLogReader.getFileDescriptor());
+ }
+
/**
* Stops this virtual machine. Stopping a virtual machine is like pulling the plug on a real
* computer; the machine halts immediately. Software running on the virtual machine is not
@@ -401,6 +418,7 @@
final File vmRootDir = mConfigFilePath.getParentFile();
mConfigFilePath.delete();
mInstanceFilePath.delete();
+ mIdsigFilePath.delete();
vmRootDir.delete();
}
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 4d7c218..274b7ed 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -379,6 +379,7 @@
// MAX_VBMETA_SIZE=64KB, MAX_FOOTER_SIZE=4KB
avb_hash_footer_kb = "68"
+// TODO(b/193504286) remove this when prebuilt bootloader exposes pubkey as well.
genrule {
name: "microdroid_bootloader_gen",
tools: ["avbtool"],
@@ -405,6 +406,22 @@
}
prebuilt_etc {
+ name: "microdroid_bootloader.avbpubkey",
+ src: ":microdroid_bootloader_pubkey_gen",
+}
+
+genrule {
+ name: "microdroid_bootloader_pubkey_gen",
+ tools: ["avbtool"],
+ srcs: [
+ ":microdroid_crosvm_bootloader",
+ ":avb_testkey_rsa4096",
+ ],
+ out: ["bootloader-pubkey"],
+ cmd: "$(location avbtool) extract_public_key --key $(location :avb_testkey_rsa4096) --output $(out)",
+}
+
+prebuilt_etc {
name: "microdroid_uboot_env",
src: ":microdroid_uboot_env_gen",
arch: {
diff --git a/microdroid/bootconfig.app_debuggable b/microdroid/bootconfig.app_debuggable
index f65d4cd..98d326a 100644
--- a/microdroid/bootconfig.app_debuggable
+++ b/microdroid/bootconfig.app_debuggable
@@ -8,3 +8,7 @@
# ADB is supported but rooting is prohibited.
androidboot.adb.enabled=1
+
+# logd is enabled
+# TODO(b/200914564) Filter only the log from the app
+androidboot.logd.enabled=1
diff --git a/microdroid/bootconfig.full_debuggable b/microdroid/bootconfig.full_debuggable
index 0d0457c..fd8a83e 100644
--- a/microdroid/bootconfig.full_debuggable
+++ b/microdroid/bootconfig.full_debuggable
@@ -9,3 +9,6 @@
# ro.adb.secure is still 0 (see build.prop) which means that adbd is started
# unrooted by default. To root, developer should explicitly execute `adb root`.
androidboot.adb.enabled=1
+
+# logd is enabled
+androidboot.logd.enabled=1
diff --git a/microdroid/bootconfig.normal b/microdroid/bootconfig.normal
index f7cdfc7..9cfb55a 100644
--- a/microdroid/bootconfig.normal
+++ b/microdroid/bootconfig.normal
@@ -6,3 +6,6 @@
# ADB is not enabled.
androidboot.adb.enabled=0
+
+# logd is not enabled
+androidboot.logd.enabled=0
diff --git a/microdroid/bootconfig.x86_64 b/microdroid/bootconfig.x86_64
index 20d64f7..2977ee3 100644
--- a/microdroid/bootconfig.x86_64
+++ b/microdroid/bootconfig.x86_64
@@ -1 +1 @@
-androidboot.boot_devices = pci0000:00/0000:00:02.0,pci0000:00/0000:00:03.0,pci0000:00/0000:00:04.0
+androidboot.boot_devices = pci0000:00/0000:00:03.0,pci0000:00/0000:00:04.0,pci0000:00/0000:00:05.0
diff --git a/microdroid/init.rc b/microdroid/init.rc
index 078b51d..ad551cc 100644
--- a/microdroid/init.rc
+++ b/microdroid/init.rc
@@ -74,9 +74,11 @@
chmod 0664 /dev/cpuset/background/tasks
chmod 0664 /dev/cpuset/system-background/tasks
+on init && property:ro.boot.logd.enabled=1
# Start logd before any other services run to ensure we capture all of their logs.
start logd
+on init
start servicemanager
# TODO(b/185767624): remove hidl after full keymint support
@@ -85,7 +87,7 @@
on init && property:ro.boot.adb.enabled=1
start adbd
-on load_persist_props_action
+on load_persist_props_action && property:ro.boot.logd.enabled=1
start logd
start logd-reinit
@@ -193,6 +195,11 @@
seclabel u:r:shell:s0
setenv HOSTNAME console
+service seriallogging /system/bin/logcat -b all -v threadtime -f /dev/hvc1 *:V
+ disabled
+ user logd
+ group root logd
+
on fs
write /dev/event-log-tags "# content owned by logd
"
diff --git a/microdroid/ueventd.rc b/microdroid/ueventd.rc
index 271e134..85f2f9d 100644
--- a/microdroid/ueventd.rc
+++ b/microdroid/ueventd.rc
@@ -24,3 +24,6 @@
# these should not be world writable
/dev/rtc0 0640 system system
/dev/tty0 0660 root system
+
+# Virtual console for logcat
+/dev/hvc1 0660 logd logd
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index ac62e58..f666294 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -49,6 +49,7 @@
const VMADDR_CID_HOST: u32 = 2;
const APEX_CONFIG_DONE_PROP: &str = "apex_config.done";
+const LOGD_ENABLED_PROP: &str = "ro.boot.logd.enabled";
fn get_vms_rpc_binder() -> Result<Strong<dyn IVirtualMachineService>> {
// SAFETY: AIBinder returned by RpcClient has correct reference count, and the ownership can be
@@ -68,7 +69,10 @@
fn main() {
if let Err(e) = try_main() {
- error!("failed with {:?}", e);
+ error!("Failed with {:?}. Shutting down...", e);
+ if let Err(e) = system_properties::write("sys.powerctl", "shutdown") {
+ error!("failed to shutdown {:?}", e);
+ }
std::process::exit(1);
}
}
@@ -223,6 +227,12 @@
info!("notifying payload started");
service.notifyPayloadStarted()?;
+ // Start logging if enabled
+ // TODO(b/200914564) set filterspec if debug_level is app_only
+ if system_properties::read(LOGD_ENABLED_PROP)? == "1" {
+ system_properties::write("ctl.start", "seriallogging")?;
+ }
+
let exit_status = command.spawn()?.wait()?;
if let Some(code) = exit_status.code() {
info!("notifying payload finished");
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 0b0810f..493fc93 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -2,12 +2,12 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-android_test_helper_app {
+android_test {
name: "MicrodroidTestApp",
+ test_suites: ["device-tests"],
srcs: ["src/java/**/*.java"],
- libs: [
- "android.system.virtualmachine",
- ],
+ static_libs: ["androidx.test.runner"],
+ libs: ["android.system.virtualmachine"],
jni_libs: ["MicrodroidTestNativeLib"],
platform_apis: true,
use_embedded_native_libs: true,
diff --git a/tests/testapk/AndroidManifest.xml b/tests/testapk/AndroidManifest.xml
index 94f49dd..21abeb5 100644
--- a/tests/testapk/AndroidManifest.xml
+++ b/tests/testapk/AndroidManifest.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2021 The Android Open Source Project
+
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -14,13 +15,10 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.microdroid.test">
- <application android:label="Microdroid Test">
+ <application>
<uses-library android:name="android.system.virtualmachine" android:required="true" />
- <activity android:name="TestActivity" android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
</application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.microdroid.test"
+ android:label="Microdroid Test" />
</manifest>
diff --git a/tests/testapk/AndroidTest.xml b/tests/testapk/AndroidTest.xml
new file mode 100644
index 0000000..25b1001
--- /dev/null
+++ b/tests/testapk/AndroidTest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs sample instrumentation test.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="MicrodroidTestApp.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.microdroid.test" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
new file mode 100644
index 0000000..5e465d5
--- /dev/null
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.microdroid.test;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MicrodroidTests {
+ @Test
+ public void testNothing() {
+ assertTrue(true);
+ }
+}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java b/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
deleted file mode 100644
index ad34ca4..0000000
--- a/tests/testapk/src/java/com/android/microdroid/test/TestActivity.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.microdroid.test;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.system.virtualmachine.VirtualMachine;
-import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineException;
-import android.system.virtualmachine.VirtualMachineManager;
-
-public class TestActivity extends Activity {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- VirtualMachine vm1 = createAndRunVirtualMachine("vm1");
- VirtualMachine vm2 = createAndRunVirtualMachine("vm2");
- }
-
- private VirtualMachine createAndRunVirtualMachine(String name) {
- VirtualMachine vm;
- try {
- VirtualMachineConfig config =
- new VirtualMachineConfig.Builder(this, "assets/vm_config.json")
- .build();
-
- VirtualMachineManager vmm = VirtualMachineManager.getInstance(this);
- vm = vmm.create(name, config);
- vm.run();
- } catch (VirtualMachineException e) {
- throw new RuntimeException(e);
- }
- return vm;
- }
-}
diff --git a/tests/vsock_test.cc b/tests/vsock_test.cc
index 480d05a..0b863a9 100644
--- a/tests/vsock_test.cc
+++ b/tests/vsock_test.cc
@@ -85,7 +85,7 @@
VirtualMachineConfig config(std::move(raw_config));
sp<IVirtualMachine> vm;
- status = virtualization_service->createVm(config, std::nullopt, &vm);
+ status = virtualization_service->createVm(config, std::nullopt, std::nullopt, &vm);
ASSERT_TRUE(status.isOk()) << "Error creating VM: " << status;
int32_t cid;
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index 8be7331..e417ec4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -23,10 +23,13 @@
interface IVirtualizationService {
/**
* Create the VM with the given config file, and return a handle to it ready to start it. If
- * `logFd` is provided then console logs from the VM will be sent to it.
+ * `consoleFd` is provided then console output from the VM will be sent to it. If `osLogFd` is
+ * provided then the OS-level logs will be sent to it. `osLogFd` is supported only when the OS
+ * running in the VM has the logging system. In case of Microdroid, the logging system is logd.
*/
- IVirtualMachine createVm(
- in VirtualMachineConfig config, in @nullable ParcelFileDescriptor logFd);
+ IVirtualMachine createVm(in VirtualMachineConfig config,
+ in @nullable ParcelFileDescriptor consoleFd,
+ in @nullable ParcelFileDescriptor osLogFd);
/**
* Initialise an empty partition image of the given size to be used as a writable partition.
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 2f901b4..5d64684 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -122,10 +122,12 @@
fn createVm(
&self,
config: &VirtualMachineConfig,
+ console_fd: Option<&ParcelFileDescriptor>,
log_fd: Option<&ParcelFileDescriptor>,
) -> binder::Result<Strong<dyn IVirtualMachine>> {
check_manage_access()?;
let state = &mut *self.state.lock().unwrap();
+ let mut console_fd = console_fd.map(clone_file).transpose()?;
let mut log_fd = log_fd.map(clone_file).transpose()?;
let requester_uid = ThreadState::get_calling_uid();
let requester_sid = get_calling_sid()?;
@@ -160,6 +162,9 @@
// doesn't understand the bootconfig parameters.
if let VirtualMachineConfig::AppConfig(config) = config {
if config.debugLevel != DebugLevel::FULL {
+ console_fd = None;
+ }
+ if config.debugLevel == DebugLevel::NONE {
log_fd = None;
}
}
@@ -212,6 +217,7 @@
params: config.params.to_owned(),
protected: config.protectedVm,
memory_mib: config.memoryMib.try_into().ok().and_then(NonZeroU32::new),
+ console_fd,
log_fd,
indirect_files,
};
@@ -250,7 +256,13 @@
)
})?;
let image = clone_file(image_fd)?;
-
+ // initialize the file. Any data in the file will be erased.
+ image.set_len(0).map_err(|e| {
+ new_binder_exception(
+ ExceptionCode::SERVICE_SPECIFIC,
+ format!("Failed to reset a file: {}", e),
+ )
+ })?;
let mut part = QcowFile::new(image, size).map_err(|e| {
new_binder_exception(
ExceptionCode::SERVICE_SPECIFIC,
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 8a5a7dd..08be052 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -45,6 +45,7 @@
pub params: Option<String>,
pub protected: bool,
pub memory_mib: Option<NonZeroU32>,
+ pub console_fd: Option<File>,
pub log_fd: Option<File>,
pub indirect_files: Vec<File>,
}
@@ -180,8 +181,8 @@
/// `self.vm_state` to avoid holding the lock on `vm_state` while it is running.
fn monitor(&self, child: Arc<SharedChild>) {
match child.wait() {
- Err(e) => error!("Error waiting for crosvm instance to die: {}", e),
- Ok(status) => info!("crosvm exited with status {}", status),
+ Err(e) => error!("Error waiting for crosvm({}) instance to die: {}", child.id(), e),
+ Ok(status) => info!("crosvm({}) exited with status {}", child.id(), status),
}
let mut vm_state = self.vm_state.lock().unwrap();
@@ -219,9 +220,11 @@
pub fn kill(&self) {
let vm_state = &*self.vm_state.lock().unwrap();
if let VmState::Running { child } = vm_state {
+ let id = child.id();
+ debug!("Killing crosvm({})", id);
// TODO: Talk to crosvm to shutdown cleanly.
if let Err(e) = child.kill() {
- error!("Error killing crosvm instance: {}", e);
+ error!("Error killing crosvm({}) instance: {}", id, e);
}
}
}
@@ -243,28 +246,35 @@
command.arg("--mem").arg(memory_mib.to_string());
}
+ // 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();
+
// Setup the serial devices.
// 1. uart device: used as the output device by bootloaders and as early console by linux
// 2. virtio-console device: used as the console device
+ // 3. virtio-console device: used as the logcat output
//
- // When log_fd is not specified, the devices are attached to sink, which means what's written
- // there is discarded.
- //
+ // When [console|log]_fd is not specified, the devices are attached to sink, which means what's
+ // written there is discarded.
+ let mut format_serial_arg = |fd: &Option<File>| {
+ let path = fd.as_ref().map(|fd| add_preserved_fd(&mut preserved_fds, fd));
+ let type_arg = path.as_ref().map_or("type=sink", |_| "type=file");
+ let path_arg = path.as_ref().map_or(String::new(), |path| format!(",path={}", path));
+ format!("{}{}", type_arg, path_arg)
+ };
+ let console_arg = format_serial_arg(&config.console_fd);
+ let log_arg = format_serial_arg(&config.log_fd);
+
// Warning: Adding more serial devices requires you to shift the PCI device ID of the boot
// disks in bootconfig.x86_64. This is because x86 crosvm puts serial devices and the block
// devices in the same PCI bus and serial devices comes before the block devices. Arm crosvm
// doesn't have the issue.
- let backend = if let Some(log_fd) = config.log_fd {
- command.stdout(log_fd);
- "stdout"
- } else {
- "sink"
- };
- command.arg(format!("--serial=type={},hardware=serial", backend));
- command.arg(format!("--serial=type={},hardware=virtio-console", backend));
-
- // 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();
+ // /dev/ttyS0
+ command.arg(format!("--serial={},hardware=serial", &console_arg));
+ // /dev/hvc0
+ command.arg(format!("--serial={},hardware=virtio-console,num=1", &console_arg));
+ // /dev/hvc1
+ command.arg(format!("--serial={},hardware=virtio-console,num=2", &log_arg));
if let Some(bootloader) = &config.bootloader {
command.arg("--bios").arg(add_preserved_fd(&mut preserved_fds, bootloader));
@@ -293,6 +303,7 @@
info!("Running {:?}", command);
let result = SharedChild::spawn(&mut command)?;
+ debug!("Spawned crosvm({}).", result.id());
Ok(result)
}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 7e2a925..87bcda7 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -57,12 +57,16 @@
#[structopt(short, long)]
daemonize: bool,
+ /// Path to file for VM console output.
+ #[structopt(long)]
+ console: Option<PathBuf>,
+
/// Path to file for VM log output.
- #[structopt(short, long)]
+ #[structopt(long)]
log: Option<PathBuf>,
/// Debug level of the VM. Supported values: "none" (default), "app_only", and "full".
- #[structopt(short, long, default_value = "none", parse(try_from_str=parse_debug_level))]
+ #[structopt(long, default_value = "none", parse(try_from_str=parse_debug_level))]
debug: DebugLevel,
/// Memory size (in MiB) of the VM. If unspecified, defaults to the value of `memory_mib`
@@ -80,9 +84,9 @@
#[structopt(short, long)]
daemonize: bool,
- /// Path to file for VM log output.
- #[structopt(short, long)]
- log: Option<PathBuf>,
+ /// Path to file for VM console output.
+ #[structopt(long)]
+ console: Option<PathBuf>,
},
/// Stop a virtual machine running in the background
Stop {
@@ -134,7 +138,7 @@
.context("Failed to find VirtualizationService")?;
match opt {
- Opt::RunApp { apk, idsig, instance, config_path, daemonize, log, debug, mem } => {
+ Opt::RunApp { apk, idsig, instance, config_path, daemonize, console, log, debug, mem } => {
command_run_app(
service,
&apk,
@@ -142,13 +146,14 @@
&instance,
&config_path,
daemonize,
+ console.as_deref(),
log.as_deref(),
debug,
mem,
)
}
- Opt::Run { config, daemonize, log } => {
- command_run(service, &config, daemonize, log.as_deref(), /* mem */ None)
+ Opt::Run { config, daemonize, console } => {
+ command_run(service, &config, daemonize, console.as_deref(), /* mem */ None)
}
Opt::Stop { cid } => command_stop(service, cid),
Opt::List => command_list(service),
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 2d771fc..15775cb 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -44,6 +44,7 @@
instance: &Path,
config_path: &str,
daemonize: bool,
+ console_path: Option<&Path>,
log_path: Option<&Path>,
debug_level: DebugLevel,
mem: Option<u32>,
@@ -76,7 +77,14 @@
debugLevel: debug_level,
memoryMib: mem.unwrap_or(0) as i32, // 0 means use the VM default
});
- run(service, &config, &format!("{:?}!{:?}", apk, config_path), daemonize, log_path)
+ run(
+ service,
+ &config,
+ &format!("{:?}!{:?}", apk, config_path),
+ daemonize,
+ console_path,
+ log_path,
+ )
}
/// Run a VM from the given configuration file.
@@ -84,7 +92,7 @@
service: Strong<dyn IVirtualizationService>,
config_path: &Path,
daemonize: bool,
- log_path: Option<&Path>,
+ console_path: Option<&Path>,
mem: Option<u32>,
) -> Result<(), Error> {
let config_file = File::open(config_path).context("Failed to open config file")?;
@@ -98,7 +106,8 @@
&VirtualMachineConfig::RawConfig(config),
&format!("{:?}", config_path),
daemonize,
- log_path,
+ console_path,
+ None,
)
}
@@ -119,9 +128,20 @@
config: &VirtualMachineConfig,
config_path: &str,
daemonize: bool,
+ console_path: Option<&Path>,
log_path: Option<&Path>,
) -> Result<(), Error> {
- let stdout = if let Some(log_path) = log_path {
+ let console = if let Some(console_path) = console_path {
+ Some(ParcelFileDescriptor::new(
+ File::create(console_path)
+ .with_context(|| format!("Failed to open console file {:?}", console_path))?,
+ ))
+ } else if daemonize {
+ None
+ } else {
+ Some(ParcelFileDescriptor::new(duplicate_stdout()?))
+ };
+ let log = if let Some(log_path) = log_path {
Some(ParcelFileDescriptor::new(
File::create(log_path)
.with_context(|| format!("Failed to open log file {:?}", log_path))?,
@@ -131,7 +151,9 @@
} else {
Some(ParcelFileDescriptor::new(duplicate_stdout()?))
};
- let vm = service.createVm(config, stdout.as_ref()).context("Failed to create VM")?;
+
+ let vm =
+ service.createVm(config, console.as_ref(), log.as_ref()).context("Failed to create VM")?;
let cid = vm.getCid().context("Failed to get CID")?;
println!(