blob: f2c96e3ff04aa4c4c2bac06f6409153c3b6d8cc0 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use addr::TargetAddr;
use anyhow::Result;
use std::io::{BufRead, Write};
use thiserror::Error;
use crate::errors::IntoExitCode;
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct TargetInfo {
pub(crate) nodename: String,
pub(crate) addresses: Vec<TargetAddr>,
pub(crate) mode: TargetMode,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) enum TargetMode {
Fastboot,
#[default]
Product,
}
impl std::fmt::Display for TargetInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let addrs = self.addresses.iter().map(|x| format!("{}", x)).collect::<Vec<_>>().join(",");
write!(f, "{}\t{}", self.nodename, addrs)
}
}
#[derive(Debug, Error)]
pub enum ChooseTargetError {
#[error(
"No valid targets discovered.\nEnsure your Target is in Product mode or Fastboot Over TCP"
)]
NoValidTargets,
#[error("Internal error: list of targets was length 1, but the `first` one was None.")]
InvalidState,
#[error("Specified target ({}) does not exist", .0)]
DoesNotExist(String),
#[error("Invalid choice: {}", .0)]
OutOfRangeChoice(usize),
#[error("Choice {} was not an unsigned integer", .0)]
InvalidChoice(String),
#[error("Could not get target {} from collection", .0)]
CollectionError(usize),
#[error("{}", .0)]
IoError(#[from] std::io::Error),
}
impl IntoExitCode for ChooseTargetError {
fn exit_code(&self) -> i32 {
match self {
Self::NoValidTargets => 80,
Self::InvalidState => 81,
Self::DoesNotExist(_) => 82,
Self::OutOfRangeChoice(_) => 83,
Self::InvalidChoice(_) => 84,
Self::CollectionError(_) => 85,
Self::IoError(e) => e.raw_os_error().unwrap_or_else(|| 86),
}
}
}
pub async fn choose_target<R, W>(
input: &mut R,
output: &mut W,
targets: Vec<TargetInfo>,
def: Option<String>,
) -> Result<TargetInfo, ChooseTargetError>
where
R: BufRead,
W: Write,
{
// If they specified a target...
if let Some(t) = def {
let filtered_targets =
targets.iter().filter(|x| x.nodename == t).cloned().collect::<Vec<TargetInfo>>();
let found_target = filtered_targets.first();
if found_target.is_none() {
return Err(ChooseTargetError::DoesNotExist(t));
}
return Ok(found_target.cloned().unwrap());
}
match targets.len() {
0 => return Err(ChooseTargetError::NoValidTargets),
1 => {
let first = targets.first();
match first {
Some(f) => Ok(f.clone()),
None => Err(ChooseTargetError::InvalidState),
}
}
_ => {
// Okay there is more than one target available and they haven't
// specified a target to use, lets prompt them for one
prompt_for_target(input, output, targets).await
}
}
}
async fn prompt_for_target<R, W>(
mut input: R,
mut output: W,
targets: Vec<TargetInfo>,
) -> Result<TargetInfo, ChooseTargetError>
where
R: BufRead,
W: Write,
{
writeln!(output, "Multiple devices detected. Please choose the target by number:")?;
for (i, t) in targets.iter().enumerate() {
let name = t.clone().nodename;
writeln!(output, " {i}: {name}")?;
}
write!(output, "Choice: ")?;
output.flush()?;
let mut choice = String::new();
input.read_line(&mut choice)?;
let idx: usize = choice.trim().parse().map_err(|_| ChooseTargetError::InvalidChoice(choice))?;
if idx >= targets.len() {
return Err(ChooseTargetError::OutOfRangeChoice(idx));
}
targets.get(idx).ok_or_else(|| ChooseTargetError::CollectionError(idx)).cloned()
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[fuchsia_async::run_singlethreaded(test)]
async fn test_prompt_for_first_target() -> Result<()> {
let input = b"0";
let output = Vec::new();
let targets = vec![
TargetInfo { nodename: "cytherera".to_string(), ..Default::default() },
TargetInfo { nodename: "alecto".to_string(), ..Default::default() },
];
let res = prompt_for_target(&input[..], output, targets).await?;
assert_eq!(res, TargetInfo { nodename: "cytherera".to_string(), ..Default::default() });
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_prompt_for_last_target() -> Result<()> {
let input = b"1";
let output = Vec::new();
let targets = vec![
TargetInfo { nodename: "cytherera".to_string(), ..Default::default() },
TargetInfo { nodename: "alecto".to_string(), ..Default::default() },
];
let res = prompt_for_target(&input[..], output, targets).await?;
assert_eq!(res, TargetInfo { nodename: "alecto".to_string(), ..Default::default() });
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_prompt_for_negative_target() -> Result<()> {
let input = b"-1";
let output = Vec::new();
let targets = vec![
TargetInfo { nodename: "cytherera".to_string(), ..Default::default() },
TargetInfo { nodename: "alecto".to_string(), ..Default::default() },
];
let res = prompt_for_target(&input[..], output, targets).await;
assert!(res.is_err());
assert_eq!(format!("{}", res.unwrap_err()), "Choice -1 was not an unsigned integer");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_prompt_for_target_not_usize_error() -> Result<()> {
let input = b"asdf";
let output = Vec::new();
let targets = vec![
TargetInfo { nodename: "cytherera".to_string(), ..Default::default() },
TargetInfo { nodename: "alecto".to_string(), ..Default::default() },
];
let res = prompt_for_target(&input[..], output, targets).await;
assert!(res.is_err());
assert_eq!(format!("{}", res.unwrap_err()), "Choice asdf was not an unsigned integer");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_prompt_for_target_out_of_range_error() -> Result<()> {
let input = b"2";
let output = Vec::new();
let targets = vec![
TargetInfo { nodename: "cytherera".to_string(), ..Default::default() },
TargetInfo { nodename: "alecto".to_string(), ..Default::default() },
];
let res = prompt_for_target(&input[..], output, targets).await;
assert!(res.is_err());
assert_eq!(format!("{}", res.unwrap_err()), "Invalid choice: 2");
Ok(())
}
}