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!(