blob: 4b634cf95416262d32b3608c41fe24644dd315b8 [file] [log] [blame]
use crate::cli;
use crate::commands;
use crate::device;
use crate::fingerprint;
use crate::metrics;
use crate::progress;
use crate::restart_chooser;
use crate::tracking::Config;
use anyhow::{anyhow, bail, Context, Result};
use fingerprint::{DiffMode, FileMetadata};
use itertools::Itertools;
use metrics::MetricSender;
use rayon::prelude::*;
use regex::Regex;
use restart_chooser::RestartChooser;
use tracing::{debug, Level};
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::fs;
use std::fs::File;
use std::io::{stdin, Write};
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use std::time::Duration;
/// Methods that interact with the host, like fingerprinting and calling ninja to get deps.
pub trait Host {
/// Return all files in the given partitions at the partition_root along with metadata for those files.
/// The keys in the returned hashmap will be relative to partition_root.
fn fingerprint(
&self,
partition_root: &Path,
partitions: &[PathBuf],
) -> Result<HashMap<PathBuf, FileMetadata>>;
/// Return a list of all files that compose `droid` or whatever base and tracked
/// modules are listed in `config`.
/// Result strings are device relative. (i.e. start with system)
fn tracked_files(&self, config: &Config) -> Result<Vec<String>>;
}
/// Methods to interact with the device, like adb, rebooting, and fingerprinting.
pub trait Device {
/// Run the `commands` and return the stdout as a string. If there is non-zero return code
/// or output on stderr, then the result is an Err.
fn run_adb_command(&self, args: &commands::AdbCommand) -> Result<String>;
fn run_raw_adb_command(&self, args: &[String]) -> Result<String>;
/// Send commands to reboot device.
fn reboot(&self) -> Result<String>;
/// Send commands to do a soft restart.
fn soft_restart(&self) -> Result<String>;
/// Call the fingerprint program on the device.
fn fingerprint(&self, partitions: &[String]) -> Result<HashMap<PathBuf, FileMetadata>>;
/// Return the list apks that are currently installed, i.e. `adb install`
/// which live on the /data partition.
/// Returns the package name, i.e. "com.android.shell".
fn get_installed_apks(&self) -> Result<HashSet<String>>;
/// Wait for the device to be ready after reboots/restarts.
/// Returns any relevant output from waiting.
fn wait(&self, profiler: &mut Profiler) -> Result<String>;
/// Run the commands needed to prep a userdebug device after a flash.
fn prep_after_flash(&self, profiler: &mut Profiler) -> Result<()>;
}
pub struct RealHost {}
impl Default for RealHost {
fn default() -> Self {
Self::new()
}
}
impl RealHost {
pub fn new() -> RealHost {
RealHost {}
}
}
impl Host for RealHost {
fn fingerprint(
&self,
partition_root: &Path,
partitions: &[PathBuf],
) -> Result<HashMap<PathBuf, FileMetadata>> {
fingerprint::fingerprint_partitions(partition_root, partitions)
}
fn tracked_files(&self, config: &Config) -> Result<Vec<String>> {
config.tracked_files()
}
}
/// Time how long it takes to run the function and store the
/// result in the given profiler field.
// TODO(rbraunstein): Ideally, use tracing or flamegraph crate or
// use Map rather than name all the fields.
// See: https://docs.rs/tracing/latest/tracing/index.html#using-the-macros and span!
#[macro_export]
macro_rules! time {
($fn:expr, $ident:expr) => {{
let start = std::time::Instant::now();
let result = $fn;
$ident = start.elapsed();
result
}};
}
pub fn adevice(
host: &impl Host,
device: &impl Device,
cli: &cli::Cli,
stdout: &mut impl Write,
metrics: &mut impl MetricSender,
opt_log_file: Option<File>,
profiler: &mut Profiler,
) -> Result<()> {
// If we can initialize a log file, then setup the tracing/log subscriber to write there.
// Otherwise, logs will be dropped.
if let Some(log_file) = opt_log_file {
let subscriber = tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.with_writer(Mutex::new(log_file))
.finish();
tracing::subscriber::set_global_default(subscriber)?;
}
let restart_choice = cli.global_options.restart_choice.clone();
let product_out = match &cli.global_options.product_out {
Some(po) => PathBuf::from(po),
None => get_product_out_from_env().ok_or(anyhow!(
"ANDROID_PRODUCT_OUT is not set. Please run source build/envsetup.sh and lunch."
))?,
};
let track_time = std::time::Instant::now();
let mut config = Config::load(&cli.global_options.config_path)?;
let command_line = std::env::args().collect::<Vec<String>>().join(" ");
metrics.add_start_event(&command_line, &config.src_root()?);
// Early return for track/untrack commands.
match &cli.command {
cli::Commands::Track(names) => return config.track(&names.modules),
cli::Commands::TrackBase(base) => return config.trackbase(&base.base),
cli::Commands::Untrack(names) => return config.untrack(&names.modules),
_ => (),
}
config.print();
writeln!(stdout, " * Checking for files to push to device")?;
progress::start("Checking ninja installed files");
let mut ninja_installed_files =
time!(host.tracked_files(&config)?, profiler.ninja_deps_computer);
let partitions =
&validate_partitions(&product_out, &ninja_installed_files, &cli.global_options.partitions)?;
// Filter to paths on any partitions.
ninja_installed_files
.retain(|nif| partitions.iter().any(|p| PathBuf::from(nif).starts_with(p)));
debug!("Stale file tracking took {} millis", track_time.elapsed().as_millis());
progress::update("Checking files on device");
let mut device_tree: HashMap<PathBuf, FileMetadata> =
time!(device.fingerprint(partitions)?, profiler.device_fingerprint);
// We expect the device to create lost+found dirs when mounting
// new partitions. Filter them out as if they don't exist.
// However, if there are file inside of them, don't filter the
// inner files.
for p in partitions {
device_tree.remove(&PathBuf::from(p).join("lost+found"));
}
progress::update("Checking files on host");
let partition_paths: Vec<PathBuf> = partitions.iter().map(PathBuf::from).collect();
let host_tree =
time!(host.fingerprint(&product_out, &partition_paths)?, profiler.host_fingerprint);
progress::update("Calculating diffs");
// For now ignore diffs in permissions. This will allow us to have a new adevice host tool
// still working with an older adevice_fingerprint device tool.
// [It also works on windows hosts]
// Version 0.2 of the device tool will support permission mode.
// We can check for that version of the tool or check to see if the metadata
// on a well-known file (like system/bin/adevice_fingerprint) contains permission
// bits before we change this to UsePermissions.
let diff_mode = fingerprint::DiffMode::IgnorePermissions;
let commands = &get_update_commands(
&device_tree,
&host_tree,
&ninja_installed_files,
product_out.clone(),
&device.get_installed_apks()?,
diff_mode,
&partition_paths,
cli.global_options.force,
stdout,
)?;
progress::stop();
#[allow(clippy::collapsible_if)]
if matches!(cli.command, cli::Commands::Status) {
if commands.is_empty() {
println!(" Device already up to date.");
}
}
let max_changes = cli.global_options.max_allowed_changes;
if matches!(cli.command, cli::Commands::Clean { .. }) {
let deletes = &commands.deletes;
if deletes.is_empty() {
println!(" Nothing to clean.");
return Ok(());
}
if deletes.len() > max_changes {
bail!("There are {} files to be deleted which exceeds the configured limit of {}.\n It is recommended that you reimage your device instead. For small increases in the limit, you can run `adevice clean --max-allowed-changes={}.", deletes.len(), max_changes, deletes.len());
}
if matches!(cli.command, cli::Commands::Clean { force } if !force) {
println!(
"You are about to delete {} [untracked pushed] files. Are you sure? y/N",
deletes.len()
);
let mut should_delete = String::new();
stdin().read_line(&mut should_delete)?;
if should_delete.trim().to_lowercase() != "y" {
bail!("Not deleting");
}
}
// Consider always reboot instead of soft restart after a clean.
let restart_chooser = &RestartChooser::new(&restart_choice);
device::update(restart_chooser, deletes, profiler, device, cli.should_wait())?;
}
if matches!(cli.command, cli::Commands::Update) {
// Status
if commands.is_empty() {
println!(" Device already up to date.");
return Ok(());
}
let all_cmds: HashMap<PathBuf, commands::AdbCommand> =
commands.upserts.clone().into_iter().chain(commands.deletes.clone()).collect();
if all_cmds.len() > max_changes {
bail!("There are {} files out of date on the device, which exceeds the configured limit of {}.\n It is recommended to reimage your device. For small increases in the limit, you can run `adevice update --max-allowed-changes={}.", all_cmds.len(), max_changes, all_cmds.len());
}
writeln!(stdout, "\n * Updating {} files on device.", all_cmds.len())?;
let changed_files = all_cmds.iter().map(|cmd| format!("{:?}", cmd.1.file)).collect();
metrics.add_action_event_with_files_changed(
"file_updates",
Duration::new(0, 0),
changed_files,
);
// Send the update commands, but retry once if we need to remount rw an extra time after a flash.
for retry in 0..=1 {
let update_result = device::update(
&RestartChooser::new(&restart_choice),
&all_cmds,
profiler,
device,
cli.should_wait(),
);
progress::stop();
if update_result.is_ok() {
break;
}
if let Err(problem) = update_result {
if retry == 1 {
println!("\n\n");
bail!(" !! Error. Unable to push to device event after remount/reboot.\n !! ADB command error: {:?}", problem);
}
// TODO(rbraunstein): Avoid string checks. Either check mounts directly for this case
// or return json with the error message and code from adevice_fingerprint.
if problem.root_cause().to_string().contains("Read-only file system") {
println!(" * The device has a read-only file system. ");
println!(" After a fresh image, the device needs an extra `remount` and `reboot` to adb push files.");
println!(" Performing remount and reboot.");
println!();
}
time!(device.prep_after_flash(profiler)?, profiler.first_remount_rw);
}
println!(" * Trying update again after remount and reboot.");
}
}
metrics.display_survey();
println!("New android update workflow tool available! go/a-update");
Ok(())
}
/// Returns the commands to update the device for every file that should be updated.
/// If there are errors, like some files in the staging set have not been built, then
/// an error result is returned.
#[allow(clippy::too_many_arguments)]
fn get_update_commands(
device_tree: &HashMap<PathBuf, FileMetadata>,
host_tree: &HashMap<PathBuf, FileMetadata>,
ninja_installed_files: &[String],
product_out: PathBuf,
installed_packages: &HashSet<String>,
diff_mode: DiffMode,
partitions: &[PathBuf],
force: bool,
stdout: &mut impl Write,
) -> Result<commands::Commands> {
// NOTE: The Ninja deps list can be _ahead_of_ the product tree output list.
// i.e. m `nothing` will update our ninja list even before someone
// does a build to populate product out.
// We don't have a way to know if we are in this case or if the user
// ever did a `m droid`
// We add implicit dirs up to the partition name to the tracked set so the set matches the staging set.
let mut ninja_installed_dirs: HashSet<PathBuf> =
ninja_installed_files.iter().flat_map(|p| parents(p, partitions)).collect();
for p in partitions {
ninja_installed_dirs.insert(PathBuf::from(p));
}
let tracked_set: HashSet<PathBuf> =
ninja_installed_files.iter().map(PathBuf::from).chain(ninja_installed_dirs).collect();
let host_set: HashSet<PathBuf> = host_tree.keys().map(PathBuf::clone).collect();
// Files that are in the tracked set but NOT in the build directory. These need
// to be built.
let needs_building: HashSet<&PathBuf> = tracked_set.difference(&host_set).collect();
let status_per_file = &collect_status_per_file(
&tracked_set,
host_tree,
device_tree,
&product_out,
installed_packages,
diff_mode,
)?;
progress::stop();
print_status(stdout, status_per_file)?;
// Shadow apks are apks that are installed outside the system partition with `adb install`
// If they exist, we should print instructions to uninstall and stop the update.
shadow_apk_check(stdout, status_per_file)?;
#[allow(clippy::len_zero)]
if needs_building.len() > 0 {
if force {
println!("UNSAFE: The above modules should be built, but were not. This may cause the device to crash:\nProceeding due to \"--force\" flag.");
} else {
bail!("ERROR: Please build the above modules before updating.\nIf you want to continue anyway (which may cause the device to crash), rerun adevice with the \"--force\" flag.");
}
}
// Restrict the host set down to the ones that are in the tracked set and not installed in the data partition.
let filtered_host_set: HashMap<PathBuf, FileMetadata> = host_tree
.iter()
.filter_map(|(key, value)| {
if tracked_set.contains(key) {
Some((key.clone(), value.clone()))
} else {
None
}
})
.collect();
let filtered_changes = fingerprint::diff(&filtered_host_set, device_tree, diff_mode);
Ok(commands::compose(&filtered_changes, &product_out))
}
// These are the partitions we will try to install to.
// ADB sync also has data, oem and vendor.
// There are some partition images (like boot.img) that we don't have a good way of determining
// the changed status of. (i.e. did they touch files that forces a flash/reimage).
// By default we will clean all the default partitions of stale files.
const DEFAULT_PARTITIONS: &[&str] = &["system", "system_ext", "odm", "product"];
/// If a user explicitly passes a partition, but that doesn't exist in the tracked files,
/// then bail.
/// Otherwise, if one of the default partitions does not exist (like system_ext), then
/// just remove it from the default.
fn validate_partitions(
partition_root: &Path,
tracked_files: &[String],
cli_partitions: &Option<Vec<String>>,
) -> Result<Vec<String>> {
// NOTE: We use PathBuf instead of String so starts_with matches path components.
// Use the partitions the user passed in or default to system and system_ext
if let Some(partitions) = cli_partitions {
for partition in partitions {
if !tracked_files.iter().any(|t| PathBuf::from(t).starts_with(partition)) {
bail!("{partition:?} is not a valid partition for current lunch target.");
}
}
for partition in partitions {
if fs::read_dir(partition_root.join(partition)).is_err() {
bail!("{partition:?} partition does not exist on host. Try rebuilding with m");
}
}
return Ok(partitions.clone());
}
let found_partitions: Vec<String> = DEFAULT_PARTITIONS
.iter()
.filter_map(|part| match tracked_files.iter().any(|t| PathBuf::from(t).starts_with(part)) {
true => Some(part.to_string()),
false => None,
})
.collect();
for partition in &found_partitions {
if fs::read_dir(partition_root.join(partition)).is_err() {
bail!("{partition:?} partition does not exist on host. Try rebuilding with m");
}
}
Ok(found_partitions)
}
#[derive(Clone, PartialEq)]
enum PushState {
Push,
/// File is tracked and the device and host fingerprints match.
UpToDate,
/// File is not tracked but exists on device and host.
TrackOrClean,
/// File is on the device, but not host and not tracked.
TrackAndBuildOrClean,
/// File is tracked and on host but not on device.
//PushNew,
/// File is on host, but not tracked and not on device.
TrackOrMakeClean,
/// File is tracked and on the device, but is not in the build tree.
/// `m` the module to build it.
UntrackOrBuild,
/// The apk was `installed` on top of the system image. It will shadow any push
/// we make to the system partitions. It should be explicitly installed or uninstalled, not pushed.
// TODO(rbraunstein): Store package name and path to file on disk so we can print a better
// message to the user.
ApkInstalled,
}
impl PushState {
/// Message to print indicating what actions the user should take based on the
/// state of the file.
pub fn get_action_msg(self) -> String {
match self {
PushState::Push => "Ready to push:\n (These files are out of date on the device and will be pushed when you run `adevice update`)".to_string(),
// Note: we don't print up to date files.
PushState::UpToDate => "Up to date: (These files are up to date on the device. There is nothing to do.)".to_string(),
PushState::TrackOrClean => "Untracked pushed files:\n (These files are not tracked but exist on the device and host.)\n (Use `adevice track` for the appropriate module to have them pushed.)".to_string(),
PushState::TrackAndBuildOrClean => "Stale device files:\n (These files are on the device, but not built or tracked.)\n (They will be cleaned with `adevice update` or `adevice clean`.)".to_string(),
PushState::TrackOrMakeClean => "Untracked built files:\n (These files are in the build tree but not tracked or on the device.)\n (You might want to `adevice track` the module. It is safe to do nothing.)".to_string(),
PushState::UntrackOrBuild => "Unbuilt files:\n (These files should be built so the device can be updated.)\n (Rebuild and `adevice update`)".to_string(),
PushState::ApkInstalled => format!("ADB Installed files:\n{RED_WARNING_LINE} (These files were installed with `adb install` or similar. Pushing to the system partition will not make them available.)\n (Either `adb uninstall` these packages or `adb install` by hand.`)"),
}
}
}
// TODO(rbraunstein): Create a struct for each of the sections above for better formatting.
const RED_WARNING_LINE: &str = " \x1b[1;31m!! Warning: !!\x1b[0m\n";
/// Group each file by state and print the state message followed by the files in that state.
fn print_status(stdout: &mut impl Write, files: &HashMap<PathBuf, PushState>) -> Result<()> {
for state in [
PushState::Push,
// Skip UpToDate and TrackOrMakeClean, don't print those.
PushState::TrackOrClean,
PushState::TrackAndBuildOrClean,
PushState::UntrackOrBuild,
// Skip APKInstalled, it is handleded in shadow_apk_check.
] {
print_files_in_state(stdout, files, state)?;
}
Ok(())
}
/// Determine if file is an apk and decide if we need to give a warning
/// about pushing to a system directory because it is already installed in /data
/// and will shadow a system apk if we push it.
fn installed_apk_action(
file: &Path,
product_out: &Path,
installed_packages: &HashSet<String>,
) -> Result<PushState> {
if file.extension() != Some(OsString::from("apk").as_os_str()) {
return Ok(PushState::Push);
}
// See if this file was installed.
if is_apk_installed(&product_out.join(file), installed_packages)? {
Ok(PushState::ApkInstalled)
} else {
Ok(PushState::Push)
}
}
/// Determine if the given apk has been installed via `adb install`.
/// This will allow us to decide if pushing to /system will cause problems because the
/// version we push would be shadowed by the `installed` version.
/// Run PackageManager commands from the shell to check if something is installed.
/// If this is a problem, we can build something in to adevice_fingerprint that
/// calls PackageManager#getInstalledApplications.
/// adb exec-out pm list packages -s -f
fn is_apk_installed(host_path: &Path, installed_packages: &HashSet<String>) -> Result<bool> {
let host_apk_path = host_path.as_os_str().to_str().unwrap();
let aapt_output = std::process::Command::new("aapt2")
.args(["dump", "permissions", host_apk_path])
.output()
.context(format!("Running aapt2 on host to see if apk installed: {}", host_apk_path))?;
if !aapt_output.status.success() {
let stderr = String::from_utf8(aapt_output.stderr)?;
bail!("Unable to run aapt2 to get installed packages {:?}", stderr);
}
match package_from_aapt_dump_output(aapt_output.stdout) {
Ok(package) => {
debug!("AAPT dump found package: {package}");
Ok(installed_packages.contains(&package))
}
Err(e) => bail!("Unable to run aapt2 to get package information {e:?}"),
}
}
static AAPT_PACKAGE_MATCHER: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^package: (.+)$").expect("regex does not compile"));
/// Filter aapt2 dump output to parse out the package name for the apk.
fn package_from_aapt_dump_output(stdout: Vec<u8>) -> Result<String> {
let package_match = String::from_utf8(stdout)?
.lines()
.filter_map(|line| AAPT_PACKAGE_MATCHER.captures(line).map(|x| x[1].to_string()))
.collect();
Ok(package_match)
}
/// Go through all files that exist on the host, device, and tracking set.
/// Ignore any file that is in all three and has the same fingerprint on the host and device.
/// States where the user should take action:
/// Build
/// Clean
/// Track
/// Untrack
fn collect_status_per_file(
tracked_set: &HashSet<PathBuf>,
host_tree: &HashMap<PathBuf, FileMetadata>,
device_tree: &HashMap<PathBuf, FileMetadata>,
product_out: &Path,
installed_packages: &HashSet<String>,
diff_mode: DiffMode,
) -> Result<HashMap<PathBuf, PushState>> {
let mut all_files: Vec<&PathBuf> =
host_tree.keys().chain(device_tree.keys()).chain(tracked_set.iter()).collect();
all_files.dedup();
let states: HashMap<PathBuf, PushState> = all_files
.par_iter()
.map(|f| {
let on_device = device_tree.contains_key(*f);
let on_host = host_tree.contains_key(*f);
let tracked = tracked_set.contains(*f);
// I think keeping tracked/untracked else is clearer than collapsing.
#[allow(clippy::collapsible_else_if)]
let push_state = if tracked {
if on_device && on_host {
if fingerprint::is_metadata_diff(
device_tree.get(*f).unwrap(),
host_tree.get(*f).unwrap(),
diff_mode,
) {
// PushDiff
installed_apk_action(f, product_out, installed_packages).expect("checking if apk installed")
} else {
// Else normal case, do nothing.
// TODO(rbraunstein): Do we need to check for installed apk and warn.
// 1) User updates apk
// 2) User adb install
// 3) User reverts code and builds
// (host and device match but installed apk shadows system version).
// For now, don't look for extra problems.
PushState::UpToDate
}
} else if !on_host {
// We don't care if it is on the device or not, it has to built if it isn't
// on the host.
PushState::UntrackOrBuild
} else {
assert!(
!on_device && on_host,
"Unexpected state for file: {f:?}, tracked: {tracked} on_device: {on_device}, on_host: {on_host}"
);
// TODO(rbraunstein): Is it possible for an apk to be adb installed, but not in the system image?
// I guess so, but seems weird. Add check InstalledApk here too.
// PushNew
PushState::Push
}
} else {
if on_device && on_host {
PushState::TrackOrClean
} else if on_device && !on_host {
PushState::TrackAndBuildOrClean
} else {
// Note: case of !tracked, !on_host, !on_device is not possible.
// So only one case left.
assert!(
!on_device && on_host,
"Unexpected state for file: {f:?}, tracked: {tracked} on_device: {on_device}, on_host: {on_host}"
);
PushState::TrackOrMakeClean
}
};
(PathBuf::from(f), push_state)
})
.collect();
Ok(states)
}
/// Find all files in a given state, and if that file list is not empty, print the
/// state message and all the files (sorted).
/// Only prints stages that files in that stage.
fn print_files_in_state(
stdout: &mut impl Write,
files: &HashMap<PathBuf, PushState>,
push_state: PushState,
) -> Result<()> {
let filtered_files: HashMap<&PathBuf, &PushState> =
files.iter().filter(|(_, state)| *state == &push_state).collect();
if filtered_files.is_empty() {
return Ok(());
}
writeln!(stdout, "{}", &push_state.get_action_msg())?;
let file_list_output = filtered_files
.keys()
.sorted()
.map(|path| format!("\t{}", path.display()))
.collect::<Vec<String>>()
.join("\n");
writeln!(stdout, "{}", file_list_output)?;
Ok(())
}
fn get_product_out_from_env() -> Option<PathBuf> {
match std::env::var("ANDROID_PRODUCT_OUT") {
Ok(x) if !x.is_empty() => Some(PathBuf::from(x)),
_ => None,
}
}
/// Prints uninstall commands for every package installed
/// Bails if there are any installed packages.
fn shadow_apk_check(stdout: &mut impl Write, files: &HashMap<PathBuf, PushState>) -> Result<()> {
let filtered_files: HashMap<&PathBuf, &PushState> =
files.iter().filter(|(_, state)| *state == &PushState::ApkInstalled).collect();
if filtered_files.is_empty() {
return Ok(());
}
writeln!(stdout, "{}", PushState::ApkInstalled.get_action_msg())?;
let file_list_output = filtered_files
.keys()
.sorted()
.map(|path| format!("adb uninstall {};", path.display()))
.collect::<Vec<String>>()
.join("\n");
writeln!(stdout, "{}", file_list_output)?;
bail!("{} shadowing apks found. Uninstall to continue.", filtered_files.keys().len());
}
/// Return all path components of file_path up to a passed partition.
/// Given system/bin/logd and partition "system",
/// return ["system/bin/logd", "system/bin"], not "system" or ""
fn parents(file_path: &str, partitions: &[PathBuf]) -> Vec<PathBuf> {
PathBuf::from(file_path)
.ancestors()
.map(|p| p.to_path_buf())
.take_while(|p| !partitions.contains(p))
.collect()
}
#[allow(missing_docs)]
#[derive(Default)]
pub struct Profiler {
pub device_fingerprint: Duration,
pub host_fingerprint: Duration,
pub ninja_deps_computer: Duration,
/// Time to run all the "adb push" or "adb rm" commands.
pub adb_cmds: Duration,
/// Time to run "adb reboot" or "exec-out start".
pub restart: Duration,
pub restart_type: String,
/// Time for device to respond to "wait-for-device".
pub wait_for_device: Duration,
/// Time for sys.boot_completed to be 1 after wait-for-device.
pub wait_for_boot_completed: Duration,
/// The first time after a userdebug build is flashed/created, we need
/// to mount rw and reboot.
pub first_remount_rw: Duration,
pub total: Duration,
}
impl std::fmt::Display for Profiler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
[
" Operation profile: (secs)".to_string(),
format!("Device Fingerprint - {}", self.device_fingerprint.as_secs()),
format!("Host fingerprint - {}", self.host_fingerprint.as_secs()),
format!("Ninja - {}", self.ninja_deps_computer.as_secs()),
format!("Adb Cmds - {}", self.adb_cmds.as_secs()),
format!("Restart({})- {}", self.restart_type, self.restart.as_secs()),
format!("Wait For device connected - {}", self.wait_for_device.as_secs()),
format!("Wait For boot completed - {}", self.wait_for_boot_completed.as_secs()),
format!("First remount RW - {}", self.first_remount_rw.as_secs()),
format!("TOTAL - {}", self.total.as_secs()),
]
.join("\n\t")
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fingerprint::{self, DiffMode};
use std::path::PathBuf;
use tempfile::TempDir;
// TODO(rbraunstein): Capture/test stdout and logging.
// Test stdout: https://users.rust-lang.org/t/how-to-test-functions-that-use-println/67188/5
#[test]
fn empty_inputs() -> Result<()> {
let device_files: HashMap<PathBuf, FileMetadata> = HashMap::new();
let host_files: HashMap<PathBuf, FileMetadata> = HashMap::new();
let ninja_deps: Vec<String> = vec![];
let product_out = PathBuf::from("");
let installed_apks = HashSet::<String>::new();
let partitions = Vec::new();
let force = false;
let mut stdout = Vec::new();
let results = get_update_commands(
&device_files,
&host_files,
&ninja_deps,
product_out,
&installed_apks,
DiffMode::UsePermissions,
&partitions,
force,
&mut stdout,
)?;
assert_eq!(results.upserts.values().len(), 0);
Ok(())
}
#[test]
fn host_and_ninja_file_not_on_device() -> Result<()> {
// Relative to product out?
let product_out = PathBuf::from("");
let installed_apks = HashSet::<String>::new();
let partitions = Vec::new();
let mut stdout = Vec::new();
let force = true;
let results = get_update_commands(
// Device files
&HashMap::new(),
// Host files
&HashMap::from([
(PathBuf::from("system/myfile"), file_metadata("digest1")),
(PathBuf::from("system"), dir_metadata()),
]),
// Ninja deps
&["system".to_string(), "system/myfile".to_string()],
product_out,
&installed_apks,
DiffMode::UsePermissions,
&partitions,
force,
&mut stdout,
)?;
assert_eq!(results.upserts.values().len(), 2);
Ok(())
}
#[test]
fn host_and_ninja_file_not_on_device_force_false() -> Result<()> {
let product_out = PathBuf::from("");
let installed_apks = HashSet::<String>::new();
let partitions = Vec::new();
let mut stdout = Vec::new();
let force = false;
let results = get_update_commands(
// Device files
&HashMap::new(),
// Host files
&HashMap::from([
(PathBuf::from("system/myfile"), file_metadata("digest1")),
(PathBuf::from("system"), dir_metadata()),
]),
// Ninja deps
&["system".to_string(), "system/myfile".to_string()],
product_out,
&installed_apks,
DiffMode::UsePermissions,
&partitions,
force,
&mut stdout,
);
assert!(results.is_err());
if let Err(e) = results {
assert!(e
.to_string()
.contains("ERROR: Please build the above modules before updating."));
}
Ok(())
}
#[test]
fn test_shadow_apk_check_no_shadowing_apks() -> Result<()> {
let mut output = Vec::new();
let files = &HashMap::from([(PathBuf::from("/system/app1.apk"), PushState::Push)]);
let result = shadow_apk_check(&mut output, files);
assert!(result.is_ok());
assert!(output.is_empty());
Ok(())
}
#[test]
fn test_shadow_apk_check_with_shadowing_apks() -> Result<()> {
let mut output = Vec::new();
let files = &HashMap::from([
(PathBuf::from("/system/app1.apk"), PushState::Push),
(PathBuf::from("/data/app2.apk"), PushState::ApkInstalled),
(PathBuf::from("/data/app3.apk"), PushState::ApkInstalled),
]);
let result = shadow_apk_check(&mut output, files);
assert!(result.is_err());
let output_str = String::from_utf8(output).unwrap();
assert!(
output_str.contains("Either `adb uninstall` these packages or `adb install` by hand.")
);
assert!(output_str.contains("adb uninstall /data/app2.apk;"));
assert!(output_str.contains("adb uninstall /data/app3.apk;"));
Ok(())
}
#[test]
fn on_host_not_in_tracked_on_device() -> Result<()> {
let results = call_update(&FakeState {
device_data: &["system/f1"],
host_data: &["system/f1"],
tracked_set: &[],
})?
.upserts;
assert_eq!(0, results.values().len());
Ok(())
}
#[test]
fn in_host_not_in_tracked_not_on_device() -> Result<()> {
let results = call_update(&FakeState {
device_data: &[""],
host_data: &["system/f1"],
tracked_set: &[],
})?
.upserts;
assert_eq!(0, results.values().len());
Ok(())
}
#[test]
fn test_parents_stops_at_partition() {
assert_eq!(
vec![
PathBuf::from("some/long/path/file"),
PathBuf::from("some/long/path"),
PathBuf::from("some/long"),
],
parents("some/long/path/file", &[PathBuf::from("some")]),
);
}
#[test]
fn validate_partition_removes_unused_default_partition() -> Result<()> {
let tmp_root = TempDir::new().unwrap();
fs::create_dir_all(tmp_root.path().join("system")).unwrap();
// No system_ext here, so remove from default partitions
let ninja_deps = vec![
"system/file1".to_string(),
"file3".to_string(),
"system/dir2/file1".to_string(),
"data/sys/file4".to_string(),
];
assert_eq!(
vec!["system".to_string(),],
validate_partitions(tmp_root.path(), &ninja_deps, &None)?
);
Ok(())
}
#[test]
fn validate_partition_bails_on_bad_partition_name() {
let tmp_root = TempDir::new().unwrap();
fs::create_dir_all(tmp_root.path().join("system")).unwrap();
fs::create_dir_all(tmp_root.path().join("sys")).unwrap();
let ninja_deps = vec![
"system/file1".to_string(),
"file3".to_string(),
"system/dir2/file1".to_string(),
"data/sys/file4".to_string(),
];
// "sys" isn't a valid partition name, but it matches a prefix of "system".
// Should bail.
match validate_partitions(tmp_root.path(), &ninja_deps, &Some(vec!["sys".to_string()])) {
Ok(_) => panic!("Expected error"),
Err(e) => {
assert!(
e.to_string().contains("\"sys\" is not a valid partition"),
"{}",
e.to_string()
)
}
}
}
#[test]
fn validate_partition_bails_on_no_partition_on_host() {
let tmp_root = TempDir::new().unwrap();
let ninja_deps = vec!["system/file1".to_string()];
match validate_partitions(tmp_root.path(), &ninja_deps, &Some(vec!["system".to_string()])) {
Ok(_) => panic!("Expected error"),
Err(e) => {
assert!(
e.to_string().contains("\"system\" partition does not exist on host"),
"{}",
e.to_string()
)
}
}
}
// TODO(rbraunstein): Test case where on device and up to date, but not tracked.
struct FakeState {
device_data: &'static [&'static str],
host_data: &'static [&'static str],
tracked_set: &'static [&'static str],
}
// Helper to call update.
// Uses filename for the digest in the fingerprint
// Add directories for every file on the host like walkdir would do.
// `update` adds the directories for the tracked set so we don't do that here.
fn call_update(fake_state: &FakeState) -> Result<commands::Commands> {
let product_out = PathBuf::from("");
let installed_apks = HashSet::<String>::new();
let partitions = Vec::new();
let force = false;
let mut device_files: HashMap<PathBuf, FileMetadata> = HashMap::new();
let mut host_files: HashMap<PathBuf, FileMetadata> = HashMap::new();
for d in fake_state.device_data {
// Set the digest to the filename for now.
device_files.insert(PathBuf::from(d), file_metadata(d));
}
for h in fake_state.host_data {
// Set the digest to the filename for now.
host_files.insert(PathBuf::from(h), file_metadata(h));
// Add the dir too.
}
let tracked_set: Vec<String> =
fake_state.tracked_set.iter().map(|s| s.to_string()).collect();
let mut stdout = Vec::new();
get_update_commands(
&device_files,
&host_files,
&tracked_set,
product_out,
&installed_apks,
DiffMode::UsePermissions,
&partitions,
force,
&mut stdout,
)
}
fn file_metadata(digest: &str) -> FileMetadata {
FileMetadata {
file_type: fingerprint::FileType::File,
digest: digest.to_string(),
..Default::default()
}
}
fn dir_metadata() -> FileMetadata {
FileMetadata { file_type: fingerprint::FileType::Directory, ..Default::default() }
}
// TODO(rbraunstein): Add tests for collect_status_per_file after we decide on output.
}