Merge "Add the onPayloadStarted callback API"
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(())
     }