microdroid_manager: waits for a config file

Ideally, microdroid_manager should start zipfuse only if necessary and
access the VM config file from the APK via fusefs when it's ready.

But for now zipfuse doesn't report when it's ready to access files in a
zip file. So, microdroid_manager should wait a while to access the
config file when it is from the APK.

Bug: 189301496
Test: MicrodroidHostTestCases
Test: microdroid_manager_test
Change-Id: I0dc304a8f135f52a846fc1bb6f4e476f6162697a
diff --git a/microdroid/microdroid_payload.json b/microdroid/microdroid_payload.json
index 287aabd..ec4ff17 100644
--- a/microdroid/microdroid_payload.json
+++ b/microdroid/microdroid_payload.json
@@ -6,5 +6,6 @@
   "apk": {
     "name": "com.android.microdroid.test",
     "path": "/apex/com.android.virt/app/MicrodroidTestApp/MicrodroidTestApp.apk"
-  }
+  },
+  "payload_config_path": "/mnt/apk/assets/vm_config.json"
 }
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index 2c79196..38d500c 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -2,8 +2,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_binary {
-    name: "microdroid_manager",
+rust_defaults {
+    name: "microdroid_manager_defaults",
     crate_name: "microdroid_manager",
     srcs: ["src/main.rs"],
     edition: "2018",
@@ -19,3 +19,22 @@
     ],
     init_rc: ["microdroid_manager.rc"],
 }
+
+rust_binary {
+    name: "microdroid_manager",
+    defaults: ["microdroid_manager_defaults"],
+}
+
+rust_test {
+    name: "microdroid_manager_test",
+    defaults: ["microdroid_manager_defaults"],
+    test_suites: ["device-tests"],
+    rustlibs: [
+        "libtempfile",
+    ],
+    multilib: {
+        lib32: {
+            enabled: false,
+        },
+    },
+}
diff --git a/microdroid_manager/src/ioutil.rs b/microdroid_manager/src/ioutil.rs
new file mode 100644
index 0000000..e8732ad
--- /dev/null
+++ b/microdroid_manager/src/ioutil.rs
@@ -0,0 +1,74 @@
+// Copyright 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.
+
+//! IO utilities
+
+use std::fs::File;
+use std::io;
+use std::path::Path;
+use std::thread;
+use std::time::{Duration, Instant};
+
+const SLEEP_DURATION: Duration = Duration::from_millis(5);
+
+/// waits for a file with a timeout and returns it
+pub fn wait_for_file<P: AsRef<Path>>(path: P, timeout: Duration) -> io::Result<File> {
+    let begin = Instant::now();
+    loop {
+        match File::open(&path) {
+            Ok(file) => return Ok(file),
+            Err(error) => {
+                if error.kind() != io::ErrorKind::NotFound {
+                    return Err(error);
+                }
+                if begin.elapsed() > timeout {
+                    return Err(io::Error::from(io::ErrorKind::NotFound));
+                }
+                thread::sleep(SLEEP_DURATION);
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{Read, Write};
+
+    #[test]
+    fn test_wait_for_file() -> io::Result<()> {
+        let test_dir = tempfile::TempDir::new().unwrap();
+        let test_file = test_dir.path().join("test.txt");
+        thread::spawn(move || -> io::Result<()> {
+            thread::sleep(Duration::from_secs(1));
+            File::create(test_file)?.write_all(b"test")
+        });
+
+        let test_file = test_dir.path().join("test.txt");
+        let mut file = wait_for_file(&test_file, Duration::from_secs(5))?;
+        let mut buffer = String::new();
+        file.read_to_string(&mut buffer)?;
+        assert_eq!("test", buffer);
+        Ok(())
+    }
+
+    #[test]
+    fn test_wait_for_file_fails() {
+        let test_dir = tempfile::TempDir::new().unwrap();
+        let test_file = test_dir.path().join("test.txt");
+        let file = wait_for_file(&test_file, Duration::from_secs(1));
+        assert!(file.is_err());
+        assert_eq!(io::ErrorKind::NotFound, file.unwrap_err().kind());
+    }
+}
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 4f87a40..fbbe8f3 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -14,6 +14,7 @@
 
 //! Microdroid Manager
 
+mod ioutil;
 mod payload_config;
 mod signature;
 
@@ -21,7 +22,8 @@
 use log::{info, Level};
 use payload_config::{Task, VmPayloadConfig};
 use std::io;
-use std::process::Command;
+use std::path::Path;
+use std::process::{Command, Stdio};
 
 const LOG_TAG: &str = "MicrodroidManager";
 
@@ -32,7 +34,7 @@
 
     let signature = signature::load()?;
     if !signature.payload_config_path.is_empty() {
-        let config = VmPayloadConfig::load_from(&signature.payload_config_path)?;
+        let config = VmPayloadConfig::load_from(Path::new(&signature.payload_config_path))?;
         if let Some(main_task) = &config.task {
             exec(main_task)?;
         }
@@ -45,7 +47,8 @@
 /// TODO(jooyung): fork a child process
 fn exec(task: &Task) -> io::Result<()> {
     info!("executing main task {} {:?}...", task.command, task.args);
-    let exit_status = Command::new(&task.command).args(&task.args).status()?;
+    let exit_status =
+        Command::new(&task.command).args(&task.args).stdout(Stdio::inherit()).status()?;
     info!("exit with {}", &exit_status);
     Ok(())
 }
diff --git a/microdroid_manager/src/payload_config.rs b/microdroid_manager/src/payload_config.rs
index 1dd6d92..bac841a 100644
--- a/microdroid_manager/src/payload_config.rs
+++ b/microdroid_manager/src/payload_config.rs
@@ -16,8 +16,13 @@
 
 use log::info;
 use serde::{Deserialize, Serialize};
-use std::fs::File;
 use std::io;
+use std::path::Path;
+use std::time::Duration;
+
+use crate::ioutil;
+
+const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
 
 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
 pub struct VmPayloadConfig {
@@ -33,9 +38,9 @@
 }
 
 impl VmPayloadConfig {
-    pub fn load_from(path: &str) -> io::Result<VmPayloadConfig> {
-        info!("loading config from {}...", path);
-        let file = File::open(path)?;
+    pub fn load_from(path: &Path) -> io::Result<VmPayloadConfig> {
+        info!("loading config from {:?}...", path);
+        let file = ioutil::wait_for_file(path, WAIT_TIMEOUT)?;
         Ok(serde_json::from_reader(file)?)
     }
 }
diff --git a/tests/testapk/assets/vm_config.json b/tests/testapk/assets/vm_config.json
new file mode 100644
index 0000000..e619c3b
--- /dev/null
+++ b/tests/testapk/assets/vm_config.json
@@ -0,0 +1,6 @@
+{
+  "task": {
+    "command": "echo",
+    "args": ["hello", "microdroid"]
+  }
+}
\ No newline at end of file