| // 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(()) |
| } |
| } |