adevice status working

Ties together the new adevice_fingerprint binary with host fingerprint
function to report diffs.

Reporting UX is not in the final form, but currently looks like:
% adevice status
Device Needs: 2
	system/test_dir
	system/test_dir/test_file
Device Diffs: 1
	system/bin/adevice_fingerprint
Device Extra: 0

Test: atest
Test: mkdir -p $ANDROID_PRODUCT_OUT/system/test_dir && touch $ANDROID_PRODUCT_OUT/system/test_dir/test_file && adevice status

Change-Id: I7d46921387245e25732df931942a4f964fbcff9e
diff --git a/adevice/Android.bp b/adevice/Android.bp
index 4122130..0b38f7d 100644
--- a/adevice/Android.bp
+++ b/adevice/Android.bp
@@ -58,6 +58,7 @@
 rust_defaults {
     name: "adevice_deps_defaults",
     rustlibs: [
+        "libanyhow",
         "libatty",
         "libclap",
         "libhex",
diff --git a/adevice/src/adevice.rs b/adevice/src/adevice.rs
index 8cce2df..2e7c1f0 100644
--- a/adevice/src/adevice.rs
+++ b/adevice/src/adevice.rs
@@ -2,39 +2,55 @@
 mod cli;
 mod fingerprint;
 
+use anyhow::{anyhow, bail, Context, Result};
 use clap::Parser;
 use cli::Commands;
+use fingerprint::FileMetadata;
 
-use std::collections::HashMap;
-use std::io::{self, Write};
+use std::collections::{BTreeSet, HashMap};
+use std::io::BufReader;
 use std::path::PathBuf;
+use std::process;
 
-// TODO(rbraunstein): Remove all allow(unused) when implementing functions.
-#[allow(unused)]
-fn main() {
-    let adb = Adb {};
+fn main() -> Result<()> {
     let cli = cli::Cli::parse();
 
     let product_out = match &cli.global_options.product_out {
         Some(po) => PathBuf::from(po),
-        None => get_product_out_from_env()
-            .ok_or("ANDROID_PRODUCT_OUT is not set. Please run source build/envsetup.sh and lunch.")
-            .unwrap(),
+        None => get_product_out_from_env().ok_or(anyhow!(
+            "ANDROID_PRODUCT_OUT is not set. Please run source build/envsetup.sh and lunch."
+        ))?,
     };
 
     let partitions: Vec<PathBuf> =
         cli.global_options.partitions.iter().map(PathBuf::from).collect();
-    let device_tree = fingerprint_device(&partitions, &adb).unwrap();
-    let build_tree = fingerprint::fingerprint_partitions(&product_out, &partitions).unwrap();
+    let device_tree = fingerprint_device(&cli.global_options.partitions)?;
+    let build_tree = fingerprint::fingerprint_partitions(&product_out, &partitions)?;
 
     match &cli.command {
         Commands::Status => {
             let changes = fingerprint::diff(&build_tree, &device_tree);
-            io::stdout().write_all(&format!("{changes:?}").into_bytes());
+            report_diffs("Device Needs", &changes.device_needs);
+            report_diffs("Device Diffs", &changes.device_diffs);
+            report_diffs("Device Extra", &changes.device_extra);
         }
         Commands::ShowActions => {}
+        #[allow(unused)]
         Commands::UpdateDevice { verbose, reboot } => {}
     }
+    Ok(())
+}
+
+fn report_diffs(category: &str, file_names: &HashMap<PathBuf, FileMetadata>) {
+    let sorted_names = BTreeSet::from_iter(file_names.keys());
+    println!("{category}: {} files.", sorted_names.len());
+
+    for (index, value) in sorted_names.iter().enumerate() {
+        if index > 10 {
+            break;
+        }
+        println!("\t{}", value.display());
+    }
 }
 
 fn get_product_out_from_env() -> Option<PathBuf> {
@@ -55,15 +71,28 @@
 /// return an entry for each file found.  The entry contains the
 /// digest of the file contents and stat-like data about the file.
 /// Typically, dirs = ["system"]
-#[allow(unused)]
 fn fingerprint_device(
-    partitions: &[PathBuf],
-    adb: &Adb,
-) -> Result<HashMap<PathBuf, fingerprint::FileMetadata>, String> {
-    // Call our helper binary running on device, return errors if we can't contact it.
-    Err("use adb hashdevice command in upcoming PR".to_string())
-}
-
-struct Adb {
-    // Just a placeholder for now.
+    partitions: &[String],
+) -> Result<HashMap<PathBuf, fingerprint::FileMetadata>> {
+    let mut args =
+        vec!["shell".to_string(), "/system/bin/adevice_fingerprint".to_string(), "-p".to_string()];
+    args.extend_from_slice(partitions);
+    let mut adb = process::Command::new("adb")
+        .args(args)
+        .stdout(process::Stdio::piped())
+        .spawn()
+        .context("Error running adb")?;
+    let stdout = adb.stdout.take().ok_or(anyhow!("Unable to read stdout from adb"))?;
+    let result: HashMap<String, fingerprint::FileMetadata> =
+        match serde_json::from_reader(BufReader::new(stdout)) {
+            Err(err) if err.line() == 1 && err.column() == 0 && err.is_eof() => {
+                // This means there was no data. Print a different error, and adb
+                // probably also just printed a line.
+                bail!("Device didn't return any data.");
+            }
+            Err(err) => bail!("Error reading json {}", err),
+            Ok(file_map) => file_map,
+        };
+    adb.wait()?;
+    Ok(result.into_iter().map(|(path, metadata)| (PathBuf::from(path), metadata)).collect())
 }
diff --git a/adevice/src/adevice_fingerprint.rs b/adevice/src/adevice_fingerprint.rs
index 6c9a6a4..213fd5d 100644
--- a/adevice/src/adevice_fingerprint.rs
+++ b/adevice/src/adevice_fingerprint.rs
@@ -1,8 +1,9 @@
 //! Tool to fingerprint the files on the device's filesystem.
 // Run with:
-//   m adevice_helper
-//   adb push $ANDROID_PRODUCT_OUT/system/bin/adevice_helper /data/bin/adevice_helper
-//   adb shell /data/bin/adevice_helper --partitions system
+//   adb root
+//   m adevice_fingerprint
+//   adb push $ANDROID_PRODUCT_OUT/system/bin/adevice_fingerprint /system/bin/adevice_fingerprint
+//   adb shell /system/bin/adevice_fingerprint --partitions system
 
 use clap::Parser;
 use std::io;
diff --git a/adevice/src/fingerprint.rs b/adevice/src/fingerprint.rs
index a8b388b..f8dadf1 100644
--- a/adevice/src/fingerprint.rs
+++ b/adevice/src/fingerprint.rs
@@ -54,7 +54,7 @@
 /// Each file should land in of the three categories (i.e. updated, not
 /// removed and added);
 /// TODO(rbraunstein): Fix allow(unused) by breaking out methods not
-/// needed by adevice_helper.
+/// needed by adevice_fingerprint.
 #[allow(unused)]
 pub fn diff(
     host_files: &HashMap<PathBuf, FileMetadata>,
@@ -69,15 +69,13 @@
     // Files on the host, but not on the device or
     // file on the host that are different on the device.
     for (file_name, metadata) in host_files {
-        // TODO(rbraunstein): Can I make the logic clearer without nesting?
-        if let Some(device_metadata) = device_files.get(file_name) {
-            if device_metadata != metadata {
-                diffs.device_diffs.insert(file_name.clone(), metadata.clone());
-            } else {
-                // NO diff, nothing to do.
-            };
-        } else {
-            diffs.device_needs.insert(file_name.clone(), metadata.clone());
+        match device_files.get(file_name) {
+            Some(device_metadata) if device_metadata != metadata => {
+                diffs.device_diffs.insert(file_name.clone(), metadata.clone())
+            }
+            // If the device metadata == host metadata there is nothing to do.
+            Some(_) => None,
+            None => diffs.device_needs.insert(file_name.clone(), metadata.clone()),
         };
     }