Async interface for running odrefresh

This creates a variant of startTestOdrefresh that uses the
ICompilationTask async mechanism of startTestCompile.

OdrefreshTask implements ICompilationTask; instead of owning a spawned
odrefresh process it owns the VM and runs the blocking call to compsvc
inside a thread.

Add a new command to composd_cmd to exercise this.

Test: composd_cmd async-odrefresh
Bug: 209572296
Change-Id: I3780d40876dbca17d4e2a3d8f7c2b809f5561aee
diff --git a/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl b/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
index 5e72cd2..e73963d 100644
--- a/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
+++ b/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
@@ -50,6 +50,19 @@
      * and writes the results to a test directory to avoid disrupting any real artifacts in
      * existence.
      *
+     * Compilation continues in the background, and success/failure is reported via the supplied
+     * callback, unless the returned ICompilationTask is cancelled. The caller should maintain
+     * a reference to the ICompilationTask until compilation completes or is cancelled.
+     */
+    ICompilationTask startAsyncOdrefresh(ICompilationTaskCallback callback);
+
+    /**
+     * Run odrefresh in a test instance of CompOS until completed or failed.
+     *
+     * This compiles BCP extensions and system server, even if the system artifacts are up to date,
+     * and writes the results to a test directory to avoid disrupting any real artifacts in
+     * existence.
+     *
      * TODO(205750213): Change the API to async.
      */
     byte startTestOdrefresh();
diff --git a/compos/composd/src/composd_main.rs b/compos/composd/src/composd_main.rs
index 67b5974..2915a58 100644
--- a/compos/composd/src/composd_main.rs
+++ b/compos/composd/src/composd_main.rs
@@ -24,6 +24,7 @@
 mod instance_starter;
 mod internal_service;
 mod odrefresh;
+mod odrefresh_task;
 mod service;
 mod util;
 
diff --git a/compos/composd/src/odrefresh_task.rs b/compos/composd/src/odrefresh_task.rs
new file mode 100644
index 0000000..f017f67
--- /dev/null
+++ b/compos/composd/src/odrefresh_task.rs
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+use crate::fd_server_helper::FdServerConfig;
+use crate::instance_starter::CompOsInstance;
+use crate::odrefresh;
+use crate::service::open_dir;
+use android_system_composd::aidl::android::system::composd::{
+    ICompilationTask::ICompilationTask, ICompilationTaskCallback::ICompilationTaskCallback,
+};
+use android_system_composd::binder::{Interface, Result as BinderResult, Strong};
+use anyhow::{bail, Result};
+use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
+use log::{error, warn};
+use num_traits::FromPrimitive;
+use rustutils::system_properties;
+use std::os::unix::io::AsRawFd;
+use std::path::{Path, PathBuf};
+use std::sync::{Arc, Mutex};
+use std::thread;
+
+#[derive(Clone)]
+pub struct OdrefreshTask {
+    running_task: Arc<Mutex<Option<RunningTask>>>,
+}
+
+impl Interface for OdrefreshTask {}
+
+impl ICompilationTask for OdrefreshTask {
+    fn cancel(&self) -> BinderResult<()> {
+        let task = self.take();
+        // Drop the VM, which should end compilation - and cause our thread to exit
+        drop(task);
+        Ok(())
+    }
+}
+
+impl OdrefreshTask {
+    /// Return the current running task, if any, removing it from this CompilationTask.
+    /// Once removed, meaning the task has ended or been canceled, further calls will always return
+    /// None.
+    fn take(&self) -> Option<RunningTask> {
+        self.running_task.lock().unwrap().take()
+    }
+
+    pub fn start(
+        comp_os: Arc<CompOsInstance>,
+        output_dir_path: PathBuf,
+        callback: &Strong<dyn ICompilationTaskCallback>,
+    ) -> Result<OdrefreshTask> {
+        let service = comp_os.get_service();
+        let task = RunningTask { comp_os, callback: callback.clone() };
+        let task = OdrefreshTask { running_task: Arc::new(Mutex::new(Some(task))) };
+
+        task.clone().start_thread(service, output_dir_path);
+
+        Ok(task)
+    }
+
+    fn start_thread(self, service: Strong<dyn ICompOsService>, output_dir_path: PathBuf) {
+        thread::spawn(move || {
+            let exit_code = try_odrefresh(service, &output_dir_path);
+
+            let task = self.take();
+            // We don't do the callback if cancel has already happened.
+            if let Some(task) = task {
+                let result = match exit_code {
+                    Ok(odrefresh::ExitCode::CompilationSuccess) => task.callback.onSuccess(),
+                    Ok(exit_code) => {
+                        error!("Unexpected odrefresh result: {:?}", exit_code);
+                        task.callback.onFailure()
+                    }
+                    Err(e) => {
+                        error!("Running odrefresh failed: {:?}", e);
+                        task.callback.onFailure()
+                    }
+                };
+                if let Err(e) = result {
+                    warn!("Failed to deliver callback: {:?}", e);
+                }
+            }
+        });
+    }
+}
+
+fn try_odrefresh(
+    service: Strong<dyn ICompOsService>,
+    output_dir_path: &Path,
+) -> Result<odrefresh::ExitCode> {
+    let output_dir = open_dir(output_dir_path)?;
+    let system_dir = open_dir(Path::new("/system"))?;
+
+    // Spawn a fd_server to serve the FDs.
+    let fd_server_config = FdServerConfig {
+        ro_dir_fds: vec![system_dir.as_raw_fd()],
+        rw_dir_fds: vec![output_dir.as_raw_fd()],
+        ..Default::default()
+    };
+    let fd_server_raii = fd_server_config.into_fd_server()?;
+
+    let zygote_arch = system_properties::read("ro.zygote")?;
+    let exit_code =
+        service.odrefresh(system_dir.as_raw_fd(), output_dir.as_raw_fd(), &zygote_arch)?.exitCode;
+
+    drop(fd_server_raii);
+    if let Some(exit_code) = FromPrimitive::from_i8(exit_code) {
+        Ok(exit_code)
+    } else {
+        bail!("odrefresh exited with {}", exit_code)
+    }
+}
+
+struct RunningTask {
+    callback: Strong<dyn ICompilationTaskCallback>,
+    #[allow(dead_code)] // Keeps the CompOS VM alive
+    comp_os: Arc<CompOsInstance>,
+}
diff --git a/compos/composd/src/service.rs b/compos/composd/src/service.rs
index a2898a2..ab28e0a 100644
--- a/compos/composd/src/service.rs
+++ b/compos/composd/src/service.rs
@@ -20,6 +20,7 @@
 use crate::compilation_task::CompilationTask;
 use crate::fd_server_helper::FdServerConfig;
 use crate::instance_manager::InstanceManager;
+use crate::odrefresh_task::OdrefreshTask;
 use crate::util::to_binder_result;
 use android_system_composd::aidl::android::system::composd::{
     ICompilationTask::{BnCompilationTask, ICompilationTask},
@@ -67,6 +68,14 @@
         to_binder_result(self.do_start_test_compile(callback))
     }
 
+    fn startAsyncOdrefresh(
+        &self,
+        callback: &Strong<dyn ICompilationTaskCallback>,
+    ) -> binder::Result<Strong<dyn ICompilationTask>> {
+        check_permissions()?;
+        to_binder_result(self.do_start_async_odrefresh(callback))
+    }
+
     fn startTestOdrefresh(&self) -> binder::Result<i8> {
         check_permissions()?;
         to_binder_result(self.do_odrefresh_for_test())
@@ -97,6 +106,20 @@
         Ok(BnCompilationTask::new_binder(task, BinderFeatures::default()))
     }
 
+    fn do_start_async_odrefresh(
+        &self,
+        callback: &Strong<dyn ICompilationTaskCallback>,
+    ) -> Result<Strong<dyn ICompilationTask>> {
+        let output_dir_path =
+            composd_native::palette_create_odrefresh_staging_directory()?.to_path_buf();
+
+        let comp_os = self.instance_manager.start_test_instance().context("Starting CompOS")?;
+
+        let task = OdrefreshTask::start(comp_os, output_dir_path, callback)?;
+
+        Ok(BnCompilationTask::new_binder(task, BinderFeatures::default()))
+    }
+
     fn do_odrefresh_for_test(&self) -> Result<i8> {
         let compos = self
             .instance_manager
@@ -137,7 +160,7 @@
 
 /// Returns an owned FD of the directory. It currently returns a `File` as a FD owner, but
 /// it's better to use `std::os::unix::io::OwnedFd` once/if it becomes standard.
-fn open_dir(path: &Path) -> Result<File> {
+pub fn open_dir(path: &Path) -> Result<File> {
     OpenOptions::new()
         .custom_flags(libc::O_DIRECTORY)
         .read(true) // O_DIRECTORY can only be opened with read
diff --git a/compos/composd_cmd/composd_cmd.rs b/compos/composd_cmd/composd_cmd.rs
index f22dc13..37d5378 100644
--- a/compos/composd_cmd/composd_cmd.rs
+++ b/compos/composd_cmd/composd_cmd.rs
@@ -34,11 +34,9 @@
 
 fn main() -> Result<()> {
     let app = clap::App::new("composd_cmd").arg(
-        clap::Arg::with_name("command")
-            .index(1)
-            .takes_value(true)
-            .required(true)
-            .possible_values(&["staged-apex-compile", "forced-compile-test", "forced-odrefresh"]),
+        clap::Arg::with_name("command").index(1).takes_value(true).required(true).possible_values(
+            &["staged-apex-compile", "forced-compile-test", "forced-odrefresh", "async-odrefresh"],
+        ),
     );
     let args = app.get_matches();
     let command = args.value_of("command").unwrap();
@@ -49,6 +47,7 @@
         "staged-apex-compile" => run_staged_apex_compile()?,
         "forced-compile-test" => run_forced_compile_for_test()?,
         "forced-odrefresh" => run_forced_odrefresh_for_test()?,
+        "async-odrefresh" => run_async_odrefresh_for_test()?,
         _ => panic!("Unexpected command {}", command),
     }
 
@@ -113,6 +112,10 @@
     run_async_compilation(|service, callback| service.startTestCompile(callback))
 }
 
+fn run_async_odrefresh_for_test() -> Result<()> {
+    run_async_compilation(|service, callback| service.startAsyncOdrefresh(callback))
+}
+
 fn run_async_compilation<F>(start_compile_fn: F) -> Result<()>
 where
     F: FnOnce(