Add the onPayloadStarted callback API

The API is called back to the client when the payload starts in the VM.
The standard output from the payload is accessible via the
ParcelFileDescriptor argument as well.

Bug: 192904048
Test: run MicrodroidDemoApp and check that the payload output is shown.

Change-Id: Ie2afbb455496eec21617b94940ed4386a4865876
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/demo/java/com/android/microdroid/demo/MainActivity.java
index baf0242..b6c7714 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/demo/java/com/android/microdroid/demo/MainActivity.java
@@ -18,7 +18,9 @@
 
 import android.app.Application;
 import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
 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;
@@ -36,6 +38,7 @@
 import androidx.lifecycle.ViewModelProvider;
 
 import java.io.BufferedReader;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.util.concurrent.ExecutorService;
@@ -52,10 +55,12 @@
         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.scrollview);
+        ScrollView scrollView = (ScrollView) findViewById(R.id.scrollConsoleOutput);
 
-        // When the console model is updated, append the new line to the text view.
+        // 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(
@@ -66,6 +71,14 @@
                                 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()
@@ -75,9 +88,10 @@
                             public void onChanged(VirtualMachine.Status status) {
                                 if (status == VirtualMachine.Status.RUNNING) {
                                     runStopButton.setText("Stop");
+                                    consoleView.setText("");
+                                    payloadView.setText("");
                                 } else {
                                     runStopButton.setText("Run");
-                                    consoleView.setText("");
                                 }
                             }
                         });
@@ -101,6 +115,7 @@
     public static class VirtualMachineModel extends AndroidViewModel {
         private VirtualMachine mVirtualMachine;
         private final MutableLiveData<String> mConsoleOutput = new MutableLiveData<>();
+        private final MutableLiveData<String> mPayloadOutput = new MutableLiveData<>();
         private final MutableLiveData<VirtualMachine.Status> mStatus = new MutableLiveData<>();
 
         public VirtualMachineModel(Application app) {
@@ -121,6 +136,31 @@
                 VirtualMachineManager vmm = VirtualMachineManager.getInstance(getApplication());
                 mVirtualMachine = vmm.getOrCreate("demo_vm", config);
                 mVirtualMachine.run();
+                mVirtualMachine.setCallback(
+                        new VirtualMachineCallback() {
+                            @Override
+                            public void onPayloadStarted(
+                                    VirtualMachine vm, ParcelFileDescriptor out) {
+                                try {
+                                    BufferedReader reader =
+                                            new BufferedReader(
+                                                    new InputStreamReader(
+                                                            new FileInputStream(
+                                                                    out.getFileDescriptor())));
+                                    String line;
+                                    while ((line = reader.readLine()) != null) {
+                                        mPayloadOutput.postValue(line);
+                                    }
+                                } catch (IOException e) {
+                                    // Consume
+                                }
+                            }
+
+                            @Override
+                            public void onDied(VirtualMachine vm) {
+                                mStatus.postValue(VirtualMachine.Status.STOPPED);
+                            }
+                        });
                 mStatus.postValue(mVirtualMachine.getStatus());
             } catch (VirtualMachineException e) {
                 throw new RuntimeException(e);
@@ -164,6 +204,11 @@
             return mConsoleOutput;
         }
 
+        /** Returns the payload output from the VM */
+        public LiveData<String> getPayloadOutput() {
+            return mPayloadOutput;
+        }
+
         /** Returns the status of the VM */
         public LiveData<VirtualMachine.Status> getStatus() {
             return mStatus;
diff --git a/demo/res/layout/activity_main.xml b/demo/res/layout/activity_main.xml
index cd30f35..e100027 100644
--- a/demo/res/layout/activity_main.xml
+++ b/demo/res/layout/activity_main.xml
@@ -33,10 +33,38 @@
                 android:text="Debug mode" />
         </LinearLayout>
 
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="App output:" />
+
         <ScrollView
-            android:id="@+id/scrollview"
+            android:id="@+id/scrollPayloadOutput"
             android:layout_width="match_parent"
-            android:layout_height="match_parent">
+            android:layout_height="0dp"
+            android:layout_weight="1">
+
+            <TextView
+                android:id="@+id/payloadOutput"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="#9089e0"
+                android:fontFamily="monospace"
+                android:textColor="#000000" />
+        </ScrollView>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="Console output:" />
+
+        <ScrollView
+            android:id="@+id/scrollConsoleOutput"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="2">
 
             <TextView
                 android:id="@+id/consoleOutput"
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 53d6864..0e549ae 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -17,10 +17,12 @@
 package android.system.virtualmachine;
 
 import android.content.Context;
+import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.system.virtualizationservice.IVirtualMachine;
+import android.system.virtualizationservice.IVirtualMachineCallback;
 import android.system.virtualizationservice.IVirtualizationService;
 
 import java.io.File;
@@ -85,6 +87,9 @@
     /** Handle to the "running" VM. */
     private IVirtualMachine mVirtualMachine;
 
+    /** The registered callback */
+    private VirtualMachineCallback mCallback;
+
     private ParcelFileDescriptor mConsoleReader;
     private ParcelFileDescriptor mConsoleWriter;
 
@@ -186,6 +191,19 @@
     }
 
     /**
+     * Registers the callback object to get events from the virtual machine. If a callback was
+     * already registered, it is replaced with the new one.
+     */
+    public void setCallback(VirtualMachineCallback callback) {
+        mCallback = callback;
+    }
+
+    /** Returns the currently registered callback. */
+    public VirtualMachineCallback getCallback() {
+        return mCallback;
+    }
+
+    /**
      * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
      * actually started running or the OS has booted there. Such events can be notified by
      * registering a callback object (not implemented currently).
@@ -208,6 +226,40 @@
                             android.system.virtualizationservice.VirtualMachineConfig.appConfig(
                                     getConfig().toParcel()),
                             mConsoleWriter);
+
+            mVirtualMachine.registerCallback(
+                    new IVirtualMachineCallback.Stub() {
+                        @Override
+                        public void onPayloadStarted(int cid, ParcelFileDescriptor stream) {
+                            final VirtualMachineCallback cb = mCallback;
+                            if (cb == null) {
+                                return;
+                            }
+                            cb.onPayloadStarted(VirtualMachine.this, stream);
+                        }
+
+                        @Override
+                        public void onDied(int cid) {
+                            final VirtualMachineCallback cb = mCallback;
+                            if (cb == null) {
+                                return;
+                            }
+                            cb.onDied(VirtualMachine.this);
+                        }
+                    });
+            service.asBinder()
+                    .linkToDeath(
+                            new IBinder.DeathRecipient() {
+                                @Override
+                                public void binderDied() {
+                                    final VirtualMachineCallback cb = mCallback;
+                                    if (cb != null) {
+                                        cb.onDied(VirtualMachine.this);
+                                    }
+                                }
+                            },
+                            0);
+
         } catch (IOException e) {
             throw new VirtualMachineException(e);
         } catch (RemoteException e) {
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
new file mode 100644
index 0000000..0267de8
--- /dev/null
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualmachine;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Callback interface to get notified with the events from the virtual machine. The methods are
+ * executed on a binder thread. Implementations can make blocking calls in the methods.
+ *
+ * @hide
+ */
+public interface VirtualMachineCallback {
+
+    /** Called when the payload starts in the VM. */
+    void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stdout);
+
+    /** Called when the VM died. */
+    void onDied(VirtualMachine vm);
+}
diff --git a/microdroid/sepolicy/system/private/microdroid_app.te b/microdroid/sepolicy/system/private/microdroid_app.te
index eff9120..c8e75a4 100644
--- a/microdroid/sepolicy/system/private/microdroid_app.te
+++ b/microdroid/sepolicy/system/private/microdroid_app.te
@@ -43,3 +43,6 @@
     rebind
     use
 };
+
+# Allow microdroid_app to use vsock inherited from microdroid_manager
+allow microdroid_app microdroid_manager:vsock_socket { read write };
diff --git a/microdroid/sepolicy/system/private/microdroid_manager.te b/microdroid/sepolicy/system/private/microdroid_manager.te
index 53c63ae..781a5e1 100644
--- a/microdroid/sepolicy/system/private/microdroid_manager.te
+++ b/microdroid/sepolicy/system/private/microdroid_manager.te
@@ -29,3 +29,6 @@
   allow microdroid_manager fuse:dir r_dir_perms;
   allow microdroid_manager fuse:file rx_file_perms;
 ')
+
+# Let microdroid_manager to create a vsock connection back to the host VM
+allow microdroid_manager self:vsock_socket { create_socket_perms_no_ioctl };
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index 267147f..15c439b 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -18,6 +18,7 @@
         "libprotobuf",
         "libserde",
         "libserde_json",
+        "libvsock",
     ],
     init_rc: ["microdroid_manager.rc"],
 }
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 9bcfa67..d88ba1a 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -19,12 +19,15 @@
 
 use anyhow::{anyhow, bail, Result};
 use keystore2_system_property::PropertyWatcher;
-use log::info;
+use log::{error, info};
 use microdroid_payload_config::{Task, TaskType, VmPayloadConfig};
-use std::fs;
+use std::fs::{self, File};
+use std::os::unix::io::{FromRawFd, IntoRawFd};
 use std::path::Path;
-use std::process::Command;
+use std::process::{Command, Stdio};
+use std::str;
 use std::time::Duration;
+use vsock::VsockStream;
 
 const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
 
@@ -38,7 +41,10 @@
 
         // TODO(jooyung): wait until sys.boot_completed?
         if let Some(main_task) = &config.task {
-            exec_task(main_task)?;
+            exec_task(main_task).map_err(|e| {
+                error!("failed to execute task: {}", e);
+                e
+            })?;
         }
     }
 
@@ -51,16 +57,38 @@
     Ok(serde_json::from_reader(file)?)
 }
 
+/// Executes the given task. Stdout of the task is piped into the vsock stream to the
+/// virtualizationservice in the host side.
 fn exec_task(task: &Task) -> Result<()> {
-    info!("executing main task {:?}...", task);
-    let exit_status = build_command(task)?.spawn()?.wait()?;
-    if exit_status.success() {
-        Ok(())
-    } else {
-        match exit_status.code() {
-            Some(code) => bail!("task exited with exit code: {}", code),
-            None => bail!("task terminated by signal"),
+    const VMADDR_CID_HOST: u32 = 2;
+    const PORT_VIRT_SVC: u32 = 3000;
+    let stdout = match VsockStream::connect_with_cid_port(VMADDR_CID_HOST, PORT_VIRT_SVC) {
+        Ok(stream) => {
+            // SAFETY: the ownership of the underlying file descriptor is transferred from stream
+            // to the file object, and then into the Command object. When the command is finished,
+            // the file descriptor is closed.
+            let f = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
+            Stdio::from(f)
         }
+        Err(e) => {
+            error!("failed to connect to virtualization service: {}", e);
+            // Don't fail hard here. Even if we failed to connect to the virtualizationservice,
+            // we keep executing the task. This can happen if the owner of the VM doesn't register
+            // callback to accept the stream. Use /dev/null as the stdout so that the task can
+            // make progress without waiting for someone to consume the output.
+            Stdio::null()
+        }
+    };
+    info!("executing main task {:?}...", task);
+    // TODO(jiyong): consider piping the stream into stdio (and probably stderr) as well.
+    let mut child = build_command(task)?.stdout(stdout).spawn()?;
+    match child.wait()?.code() {
+        Some(0) => {
+            info!("task successfully finished");
+            Ok(())
+        }
+        Some(code) => bail!("task exited with exit code: {}", code),
+        None => bail!("task terminated by signal"),
     }
 }
 
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 0b8f2e5..40aa139 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -39,6 +39,7 @@
         "libuuid",
         "libvmconfig",
         "libzip",
+        "libvsock",
     ],
 }
 
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
index e864414..33c9716 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
@@ -27,6 +27,9 @@
     /**
      * Register a Binder object to get callbacks when the state of the VM changes, such as if it
      * dies.
+     *
+     * TODO(jiyong): this should be registered when IVirtualizationService.run is called. Otherwise,
+     * we might miss some events that happen before the registration is done.
      */
     void registerCallback(IVirtualMachineCallback callback);
 }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
index 10ef31b..7bb18a4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
@@ -23,6 +23,16 @@
  */
 oneway interface IVirtualMachineCallback {
     /**
+     * Called when the payload starts in the VM. `stdout` is the stdout of the payload.
+     *
+     * <p>Note: when the virtual machine object is shared to multiple processes and they register
+     * this callback to the same virtual machine object, the processes will compete to read from the
+     * same payload stdout. As a result, each process might get only a part of the entire output
+     * stream. To avoid such a case, keep only one process to read from the stdout.
+     */
+    void onPayloadStarted(int cid, in ParcelFileDescriptor stdout);
+
+    /**
      * Called when the VM dies.
      *
      * Note that this will not be called if the VirtualizationService itself dies, so you should
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 8bdfa9d..661abdc 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -36,16 +36,17 @@
 };
 use anyhow::{bail, Result};
 use disk::QcowFile;
-use log::{debug, error, warn};
+use log::{debug, error, warn, info};
 use microdroid_payload_config::{ApexConfig, VmPayloadConfig};
 use std::convert::TryInto;
 use std::ffi::CString;
 use std::fs::{File, create_dir};
 use std::num::NonZeroU32;
-use std::os::unix::io::AsRawFd;
+use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak};
 use vmconfig::{VmConfig, Partition};
+use vsock::{VsockListener, SockAddr, VsockStream};
 use zip::ZipArchive;
 
 pub const BINDER_SERVICE_IDENTIFIER: &str = "android.system.virtualizationservice";
@@ -62,10 +63,17 @@
 const MICRODROID_REQUIRED_APEXES: [&str; 3] =
     ["com.android.adbd", "com.android.i18n", "com.android.os.statsd"];
 
+/// The CID representing the host VM
+const VMADDR_CID_HOST: u32 = 2;
+
+/// Port number that virtualizationservice listens on connections from the guest VMs for the
+/// payload output
+const PORT_VIRT_SERVICE: u32 = 3000;
+
 /// Implementation of `IVirtualizationService`, the entry point of the AIDL service.
 #[derive(Debug, Default)]
 pub struct VirtualizationService {
-    state: Mutex<State>,
+    state: Arc<Mutex<State>>,
 }
 
 impl Interface for VirtualizationService {}
@@ -235,6 +243,45 @@
     }
 }
 
+impl VirtualizationService {
+    pub fn init() -> VirtualizationService {
+        let service = VirtualizationService::default();
+        let state = service.state.clone(); // reference to state (not the state itself) is copied
+        std::thread::spawn(move || {
+            handle_connection_from_vm(state).unwrap();
+        });
+        service
+    }
+}
+
+/// Waits for incoming connections from VM. If a new connection is made, notify the event to the
+/// client via the callback (if registered).
+fn handle_connection_from_vm(state: Arc<Mutex<State>>) -> Result<()> {
+    let listener = VsockListener::bind_with_cid_port(VMADDR_CID_HOST, PORT_VIRT_SERVICE)?;
+    for stream in listener.incoming() {
+        let stream = match stream {
+            Err(e) => {
+                warn!("invalid incoming connection: {}", e);
+                continue;
+            }
+            Ok(s) => s,
+        };
+        if let Ok(SockAddr::Vsock(addr)) = stream.peer_addr() {
+            let cid = addr.cid();
+            let port = addr.port();
+            info!("connected from cid={}, port={}", cid, port);
+            if cid < FIRST_GUEST_CID {
+                warn!("connection is not from a guest VM");
+                continue;
+            }
+            if let Some(vm) = state.lock().unwrap().get_vm(cid) {
+                vm.callbacks.notify_payload_started(cid, stream);
+            }
+        }
+    }
+    Ok(())
+}
+
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
 ///
 /// This may involve assembling a composite disk from a set of partition images.
@@ -442,12 +489,25 @@
 pub struct VirtualMachineCallbacks(Mutex<Vec<Strong<dyn IVirtualMachineCallback>>>);
 
 impl VirtualMachineCallbacks {
+    /// Call all registered callbacks to notify that the payload has started.
+    pub fn notify_payload_started(&self, cid: Cid, stream: VsockStream) {
+        let callbacks = &*self.0.lock().unwrap();
+        // SAFETY: ownership is transferred from stream to f
+        let f = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
+        let pfd = ParcelFileDescriptor::new(f);
+        for callback in callbacks {
+            if let Err(e) = callback.onPayloadStarted(cid as i32, &pfd) {
+                error!("Error notifying payload start event from VM CID {}: {}", cid, e);
+            }
+        }
+    }
+
     /// Call all registered callbacks to say that the VM has died.
     pub fn callback_on_died(&self, cid: Cid) {
         let callbacks = &*self.0.lock().unwrap();
         for callback in callbacks {
             if let Err(e) = callback.onDied(cid as i32) {
-                error!("Error calling callback: {}", e);
+                error!("Error notifying exit of VM CID {}: {}", cid, e);
             }
         }
     }
@@ -492,6 +552,11 @@
         self.vms.push(vm);
     }
 
+    /// Get a VM that corresponds to the given cid
+    fn get_vm(&self, cid: Cid) -> Option<Arc<VmInstance>> {
+        self.vms().into_iter().find(|vm| vm.cid == cid)
+    }
+
     /// Store a strong VM reference.
     fn debug_hold_vm(&mut self, vm: Strong<dyn IVirtualMachine>) {
         self.debug_held_vms.push(vm);
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 658203b..46ddd2e 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -39,7 +39,7 @@
         android_logger::Config::default().with_tag(LOG_TAG).with_min_level(Level::Trace),
     );
 
-    let service = VirtualizationService::default();
+    let service = VirtualizationService::init();
     let service = BnVirtualizationService::new_binder(
         service,
         BinderFeatures { set_requesting_sid: true, ..BinderFeatures::default() },
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 01fc724..184a396 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -30,7 +30,7 @@
 use android_system_virtualizationservice::binder::{Interface, Result as BinderResult};
 use anyhow::{Context, Error};
 use std::fs::File;
-use std::io;
+use std::io::{self, BufRead, BufReader};
 use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::path::Path;
 use vmconfig::VmConfig;
@@ -129,7 +129,7 @@
 /// If the returned DeathRecipient is dropped then this will no longer do anything.
 fn wait_for_death(binder: &mut impl IBinder, dead: AtomicFlag) -> Result<DeathRecipient, Error> {
     let mut death_recipient = DeathRecipient::new(move || {
-        println!("VirtualizationService died");
+        eprintln!("VirtualizationService unexpectedly died");
         dead.raise();
     });
     binder.link_to_death(&mut death_recipient)?;
@@ -144,8 +144,26 @@
 impl Interface for VirtualMachineCallback {}
 
 impl IVirtualMachineCallback for VirtualMachineCallback {
+    fn onPayloadStarted(&self, _cid: i32, stdout: &ParcelFileDescriptor) -> BinderResult<()> {
+        // Show the stdout of the payload
+        let mut reader = BufReader::new(stdout.as_ref());
+        loop {
+            let mut s = String::new();
+            match reader.read_line(&mut s) {
+                Ok(0) => break,
+                Ok(_) => print!("{}", s),
+                Err(e) => eprintln!("error reading from virtual machine: {}", e),
+            };
+        }
+        Ok(())
+    }
+
     fn onDied(&self, _cid: i32) -> BinderResult<()> {
-        println!("VM died");
+        // No need to explicitly report the event to the user (e.g. via println!) because this
+        // callback is registered only when the vm tool is invoked as interactive mode (e.g. not
+        // --daemonize) in which case the tool will exit to the shell prompt upon VM shutdown.
+        // Printing something will actually even confuse the user as the output from the app
+        // payload is printed.
         self.dead.raise();
         Ok(())
     }