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/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"),
     }
 }