blob: 21e134f6819ce956ae47524775c1bb4d1ad5a4fe [file] [log] [blame]
// Copyright 2021 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 {
crate::target_handle::TargetHandle,
anyhow::{anyhow, Result},
async_trait::async_trait,
chrono::Utc,
ffx_daemon_events::{FastbootInterface, TargetConnectionState, TargetInfo},
ffx_daemon_target::manual_targets,
ffx_daemon_target::target::{
target_addr_info_to_socketaddr, Target, TargetAddrEntry, TargetAddrType,
},
ffx_daemon_target::target_collection::TargetCollection,
ffx_stream_util::TryStreamUtilExt,
fidl::endpoints::ProtocolMarker,
fidl_fuchsia_developer_ffx as ffx,
fidl_fuchsia_developer_remotecontrol::RemoteControlMarker,
futures::TryStreamExt,
protocols::prelude::*,
std::net::SocketAddr,
std::rc::Rc,
std::time::{Duration, Instant, SystemTime, UNIX_EPOCH},
tasks::TaskManager,
};
mod reboot;
mod target_handle;
#[ffx_protocol(ffx::MdnsMarker, ffx::FastbootTargetStreamMarker)]
pub struct TargetCollectionProtocol {
tasks: TaskManager,
// An online cache of configured target entries (the non-discoverable targets represented in the
// ffx configuration).
// The cache can be updated by calls to AddTarget and RemoveTarget.
// With manual_targets, we have access to the targets.manual field of the configuration (a
// vector of strings). Each target is defined by an IP address and a port.
manual_targets: Rc<dyn manual_targets::ManualTargets>,
}
impl Default for TargetCollectionProtocol {
fn default() -> Self {
#[cfg(not(test))]
let manual_targets = manual_targets::Config::default();
#[cfg(test)]
let manual_targets = manual_targets::Mock::default();
Self { tasks: Default::default(), manual_targets: Rc::new(manual_targets) }
}
}
async fn add_manual_target(
manual_targets: Rc<dyn manual_targets::ManualTargets>,
tc: &TargetCollection,
addr: SocketAddr,
lifetime: Option<Duration>,
) -> Rc<Target> {
// Expiry is the SystemTime (represented as seconds after the UNIX_EPOCH) at which a manual
// target is allowed to expire and enter the Disconnected state. If no lifetime is given,
// the target is allowed to persist indefinitely. This is persisted in FFX config.
// Timeout is the number of seconds until the expiry is met; it is used in-memory only.
let (timeout, expiry, last_seen) = if lifetime.is_none() {
(None, None, None)
} else {
let timeout = SystemTime::now() + lifetime.unwrap();
let expiry = timeout.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_secs();
(Some(timeout), Some(expiry), Some(Instant::now()))
};
let tae = TargetAddrEntry::new(addr.into(), Utc::now(), TargetAddrType::Manual(timeout));
let _ = manual_targets.add(format!("{}", addr), expiry).await.map_err(|e| {
log::error!("Unable to persist manual target: {:?}", e);
});
let target = Target::new_with_addr_entries(Option::<String>::None, Some(tae).into_iter());
if addr.port() != 0 {
target.set_ssh_port(Some(addr.port()));
}
target.update_connection_state(|_| TargetConnectionState::Manual(last_seen));
let target = tc.merge_insert(target);
target.run_host_pipe();
target
}
async fn remove_manual_target(
manual_targets: Rc<dyn manual_targets::ManualTargets>,
tc: &TargetCollection,
target_id: String,
) -> bool {
if let Some(target) = tc.get(target_id.clone()) {
let ssh_port = target.ssh_port();
for addr in target.manual_addrs() {
let mut sockaddr = SocketAddr::from(addr);
ssh_port.map(|p| sockaddr.set_port(p));
let _ = manual_targets.remove(format!("{}", sockaddr)).await.map_err(|e| {
log::error!("Unable to persist target removal: {}", e);
});
}
}
tc.remove_target(target_id)
}
impl TargetCollectionProtocol {
async fn load_manual_targets(&self, tc: &TargetCollection) {
// The FFX config value for a manual target contains a target ID (typically the IP:PORT
// combo) and a timeout (which is None, if the target is indefinitely persistent).
for (str, val) in self.manual_targets.get_or_default().await {
let sa = match str.parse::<std::net::SocketAddr>() {
Ok(sa) => sa,
Err(e) => {
log::error!("Parse of manual target config failed: {}", e);
continue;
}
};
let secs = val.as_u64();
if secs.is_some() {
// If the manual target has a lifetime specified, we need to include it in the
// reloaded entry.
let lifetime_from_epoch = Duration::from_secs(secs.unwrap());
let now = SystemTime::now();
if let Ok(elapsed) = now.duration_since(UNIX_EPOCH) {
let remaining = if lifetime_from_epoch < elapsed {
Duration::ZERO
} else {
lifetime_from_epoch - elapsed
};
add_manual_target(self.manual_targets.clone(), tc, sa, Some(remaining)).await;
}
} else {
// Manual targets without a lifetime are always reloaded.
add_manual_target(self.manual_targets.clone(), tc, sa, None).await;
}
}
}
}
#[async_trait(?Send)]
impl FidlProtocol for TargetCollectionProtocol {
type Protocol = ffx::TargetCollectionMarker;
type StreamHandler = FidlStreamHandler<Self>;
#[tracing::instrument(level = "info", skip(self, cx))]
async fn handle(&self, cx: &Context, req: ffx::TargetCollectionRequest) -> Result<()> {
let target_collection = cx.get_target_collection().await?;
match req {
ffx::TargetCollectionRequest::ListTargets { reader, query, .. } => {
let reader = reader.into_proxy()?;
let targets = match query.string_matcher.as_deref() {
None | Some("") => target_collection
.targets()
.into_iter()
.filter_map(
|t| if t.is_connected() { Some(t.as_ref().into()) } else { None },
)
.collect::<Vec<ffx::TargetInfo>>(),
q => match target_collection.get_connected(q) {
Some(t) => vec![t.as_ref().into()],
None => vec![],
},
};
// This was chosen arbitrarily. It's possible to determine a
// better chunk size using some FIDL constant math.
const TARGET_CHUNK_SIZE: usize = 20;
let mut iter = targets.into_iter();
loop {
let next_chunk = iter.by_ref().take(TARGET_CHUNK_SIZE);
let next_chunk_len = next_chunk.len();
reader.next(&mut next_chunk.collect::<Vec<_>>().into_iter()).await?;
if next_chunk_len == 0 {
break;
}
}
Ok(())
}
ffx::TargetCollectionRequest::OpenTarget { query, responder, target_handle } => {
let target = match target_collection.wait_for_match(query.string_matcher).await {
Ok(t) => t,
Err(e) => {
return responder
.send(&mut match e {
ffx::DaemonError::TargetAmbiguous => {
Err(ffx::OpenTargetError::QueryAmbiguous)
}
ffx::DaemonError::TargetNotFound => {
Err(ffx::OpenTargetError::TargetNotFound)
}
e => {
// This, so far, will only happen if encountering
// TargetCacheError, which is highly unlikely.
log::warn!("encountered unhandled error: {:?}", e);
Err(ffx::OpenTargetError::TargetNotFound)
}
})
.map_err(Into::into);
}
};
self.tasks.spawn(TargetHandle::new(target, cx.clone(), target_handle)?);
responder.send(&mut Ok(())).map_err(Into::into)
}
ffx::TargetCollectionRequest::AddTarget { ip, config, responder } => {
let addr = target_addr_info_to_socketaddr(ip);
match config.verify_connection {
Some(true) => {}
_ => {
let _ = add_manual_target(
self.manual_targets.clone(),
&target_collection,
addr,
None,
)
.await;
return responder.send(&mut Ok(())).map_err(Into::into);
}
};
// The drop guard is here for the impatient user: if the user closes their channel
// prematurely (before this operation either succeeds or fails), then they will
// risk adding a manual target that can never be connected to, and then have to
// manually remove the target themselves.
struct DropGuard(
Option<(
Rc<dyn manual_targets::ManualTargets>,
Rc<TargetCollection>,
SocketAddr,
)>,
);
impl Drop for DropGuard {
fn drop(&mut self) {
match self.0.take() {
Some((mt, tc, addr)) => fuchsia_async::Task::local(async move {
remove_manual_target(mt, &tc, addr.to_string()).await
})
.detach(),
None => {}
}
}
}
let mut drop_guard = DropGuard(Some((
self.manual_targets.clone(),
target_collection.clone(),
addr.clone(),
)));
let target =
add_manual_target(self.manual_targets.clone(), &target_collection, addr, None)
.await;
let rcs = target_handle::wait_for_rcs(&target).await?;
match rcs {
Ok(mut rcs) => {
let (rcs_proxy, server) =
fidl::endpoints::create_proxy::<RemoteControlMarker>()?;
rcs.copy_to_channel(server.into_channel())?;
match rcs::knock_rcs(&rcs_proxy).await {
Ok(_) => {
let _ = drop_guard.0.take();
}
Err(e) => return responder.send(&mut Err(e)).map_err(Into::into),
}
}
Err(e) => {
let _ = remove_manual_target(
self.manual_targets.clone(),
&target_collection,
addr.to_string(),
)
.await;
let _ = drop_guard.0.take();
return responder.send(&mut Err(e)).map_err(Into::into);
}
}
responder.send(&mut Ok(())).map_err(Into::into)
}
ffx::TargetCollectionRequest::AddEphemeralTarget {
ip,
connect_timeout_seconds,
responder,
} => {
let addr = target_addr_info_to_socketaddr(ip);
add_manual_target(
self.manual_targets.clone(),
&target_collection,
addr,
Some(Duration::from_secs(connect_timeout_seconds)),
)
.await;
responder.send().map_err(Into::into)
}
ffx::TargetCollectionRequest::RemoveTarget { target_id, responder } => {
let result = remove_manual_target(
self.manual_targets.clone(),
&target_collection,
target_id,
)
.await;
responder.send(result).map_err(Into::into)
}
}
}
async fn serve<'a>(
&'a self,
cx: &'a Context,
stream: <Self::Protocol as ProtocolMarker>::RequestStream,
) -> Result<()> {
// Necessary to avoid hanging forever when a client drops a connection
// during a call to OpenTarget.
stream
.map_err(|err| anyhow!("{}", err))
.try_for_each_concurrent_while_connected(None, |req| self.handle(cx, req))
.await
}
async fn stop(&mut self, _cx: &Context) -> Result<()> {
drop(self.tasks.drain());
Ok(())
}
async fn start(&mut self, cx: &Context) -> Result<()> {
let target_collection = cx.get_target_collection().await?;
self.load_manual_targets(&target_collection).await;
let mdns = self.open_mdns_proxy(cx).await?;
let fastboot = self.open_fastboot_target_stream_proxy(cx).await?;
let tc = cx.get_target_collection().await?;
let tc_clone = tc.clone();
self.tasks.spawn(async move {
while let Ok(Some(e)) = mdns.get_next_event().await {
match *e {
ffx::MdnsEventType::TargetFound(t)
| ffx::MdnsEventType::TargetRediscovered(t) => {
handle_mdns_event(&tc_clone, t);
}
_ => {}
}
}
});
self.tasks.spawn(async move {
while let Ok(target) = fastboot.get_next().await {
handle_fastboot_target(&tc, target);
}
});
Ok(())
}
}
fn handle_fastboot_target(tc: &Rc<TargetCollection>, target: ffx::FastbootTarget) {
if let Some(ref serial) = target.serial {
log::trace!("Found new target via fastboot: {}", serial);
} else {
log::trace!("Fastboot target has no serial number. Not able to merge.");
return;
}
let t = TargetInfo { serial: target.serial, ..Default::default() };
let target = tc.merge_insert(Target::from_target_info(t.into()));
target.update_connection_state(|s| match s {
TargetConnectionState::Disconnected | TargetConnectionState::Fastboot(_) => {
TargetConnectionState::Fastboot(Instant::now())
}
_ => s,
});
}
fn handle_mdns_event(tc: &Rc<TargetCollection>, t: ffx::TargetInfo) {
let t = TargetInfo {
nodename: t.nodename,
addresses: t
.addresses
.map(|a| a.into_iter().map(Into::into).collect())
.unwrap_or(Vec::new()),
fastboot_interface: if t.target_state == Some(ffx::TargetState::Fastboot) {
t.fastboot_interface.map(|v| match v {
ffx::FastbootInterface::Usb => FastbootInterface::Usb,
ffx::FastbootInterface::Udp => FastbootInterface::Udp,
ffx::FastbootInterface::Tcp => FastbootInterface::Tcp,
})
} else {
None
},
..Default::default()
};
if t.fastboot_interface.is_some() {
log::trace!(
"Found new fastboot target via mdns: {}",
t.nodename.clone().unwrap_or("<unknown>".to_string())
);
let target = tc.merge_insert(match Target::from_fastboot_target_info(t) {
Ok(ret) => ret,
Err(e) => {
log::trace!("Error while making target: {:?}", e);
return;
}
});
target.update_connection_state(|s| match s {
TargetConnectionState::Disconnected | TargetConnectionState::Fastboot(_) => {
TargetConnectionState::Fastboot(Instant::now())
}
_ => s,
});
} else {
log::trace!(
"Found new target via mdns: {}",
t.nodename.clone().unwrap_or("<unknown>".to_string())
);
let new_target = Target::from_target_info(t);
new_target.update_connection_state(|_| TargetConnectionState::Mdns(Instant::now()));
let target = tc.merge_insert(new_target);
target.run_host_pipe();
}
}
#[cfg(test)]
mod tests {
use super::*;
use addr::TargetAddr;
use assert_matches::assert_matches;
use async_channel::{Receiver, Sender};
use fidl_fuchsia_net::{IpAddress, Ipv6Address};
use protocols::testing::FakeDaemonBuilder;
use serde_json::{json, Map, Value};
use std::cell::RefCell;
#[fuchsia_async::run_singlethreaded(test)]
async fn test_handle_mdns_non_fastboot() {
let t = Target::new_named("this-is-a-thing");
let tc = Rc::new(TargetCollection::new());
tc.merge_insert(t.clone());
let before_update = Instant::now();
handle_mdns_event(
&tc,
ffx::TargetInfo { nodename: Some(t.nodename().unwrap()), ..ffx::TargetInfo::EMPTY },
);
assert!(t.is_host_pipe_running());
assert_matches!(t.get_connection_state(), TargetConnectionState::Mdns(t) if t > before_update);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_handle_mdns_fastboot() {
let t = Target::new_named("this-is-a-thing");
let tc = Rc::new(TargetCollection::new());
tc.merge_insert(t.clone());
let before_update = Instant::now();
handle_mdns_event(
&tc,
ffx::TargetInfo {
nodename: Some(t.nodename().unwrap()),
target_state: Some(ffx::TargetState::Fastboot),
fastboot_interface: Some(ffx::FastbootInterface::Tcp),
..ffx::TargetInfo::EMPTY
},
);
assert!(!t.is_host_pipe_running());
assert_matches!(t.get_connection_state(), TargetConnectionState::Fastboot(t) if t > before_update);
}
struct TestMdns {
/// Lets the test know that a call to `GetNextEvent` has started. This
/// is just a hack to avoid using timers for races. This is dependent
/// on the executor running in a single thread.
call_started: Sender<()>,
next_event: Receiver<ffx::MdnsEventType>,
}
impl Default for TestMdns {
fn default() -> Self {
unimplemented!()
}
}
#[async_trait(?Send)]
impl FidlProtocol for TestMdns {
type Protocol = ffx::MdnsMarker;
type StreamHandler = FidlStreamHandler<Self>;
async fn handle(&self, _cx: &Context, req: ffx::MdnsRequest) -> Result<()> {
match req {
ffx::MdnsRequest::GetNextEvent { responder } => {
self.call_started.send(()).await.unwrap();
responder.send(self.next_event.recv().await.ok().as_mut()).map_err(Into::into)
}
_ => panic!("unsupported"),
}
}
}
async fn list_targets(
query: Option<&str>,
tc: &ffx::TargetCollectionProxy,
) -> Vec<ffx::TargetInfo> {
let (reader, server) =
fidl::endpoints::create_endpoints::<ffx::TargetCollectionReaderMarker>().unwrap();
tc.list_targets(
ffx::TargetQuery {
string_matcher: query.map(|s| s.to_owned()),
..ffx::TargetQuery::EMPTY
},
reader,
)
.unwrap();
let mut res = Vec::new();
let mut stream = server.into_stream().unwrap();
while let Ok(Some(ffx::TargetCollectionReaderRequest::Next { entry, responder })) =
stream.try_next().await
{
responder.send().unwrap();
if entry.len() > 0 {
res.extend(entry);
} else {
break;
}
}
res
}
#[derive(Default)]
struct FakeFastboot {}
#[async_trait(?Send)]
impl FidlProtocol for FakeFastboot {
type Protocol = ffx::FastbootTargetStreamMarker;
type StreamHandler = FidlStreamHandler<Self>;
async fn handle(
&self,
_cx: &Context,
_req: ffx::FastbootTargetStreamRequest,
) -> Result<()> {
futures::future::pending::<()>().await;
Ok(())
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_protocol_integration() {
const NAME: &'static str = "foo";
const NAME2: &'static str = "bar";
const NAME3: &'static str = "baz";
const NON_MATCHING_NAME: &'static str = "mlorp";
const PARTIAL_NAME_MATCH: &'static str = "ba";
let (call_started_sender, call_started_receiver) = async_channel::unbounded::<()>();
let (target_sender, r) = async_channel::unbounded::<ffx::MdnsEventType>();
let mdns_protocol =
Rc::new(RefCell::new(TestMdns { call_started: call_started_sender, next_event: r }));
let fake_daemon = FakeDaemonBuilder::new()
.inject_fidl_protocol(mdns_protocol)
.register_fidl_protocol::<FakeFastboot>()
.register_fidl_protocol::<TargetCollectionProtocol>()
.build();
let tc = fake_daemon.open_proxy::<ffx::TargetCollectionMarker>().await;
let res = list_targets(None, &tc).await;
assert_eq!(res.len(), 0);
call_started_receiver.recv().await.unwrap();
target_sender
.send(ffx::MdnsEventType::TargetFound(ffx::TargetInfo {
nodename: Some(NAME.to_owned()),
..ffx::TargetInfo::EMPTY
}))
.await
.unwrap();
target_sender
.send(ffx::MdnsEventType::TargetFound(ffx::TargetInfo {
nodename: Some(NAME2.to_owned()),
..ffx::TargetInfo::EMPTY
}))
.await
.unwrap();
target_sender
.send(ffx::MdnsEventType::TargetFound(ffx::TargetInfo {
nodename: Some(NAME3.to_owned()),
..ffx::TargetInfo::EMPTY
}))
.await
.unwrap();
call_started_receiver.recv().await.unwrap();
let res = list_targets(None, &tc).await;
assert_eq!(res.len(), 3, "received: {:?}", res);
assert!(res.iter().any(|t| t.nodename.as_ref().unwrap() == NAME));
assert!(res.iter().any(|t| t.nodename.as_ref().unwrap() == NAME2));
assert!(res.iter().any(|t| t.nodename.as_ref().unwrap() == NAME3));
let res = list_targets(Some(NON_MATCHING_NAME), &tc).await;
assert_eq!(res.len(), 0, "received: {:?}", res);
let res = list_targets(Some(NAME), &tc).await;
assert_eq!(res.len(), 1, "received: {:?}", res);
assert_eq!(res[0].nodename.as_ref().unwrap(), NAME);
let res = list_targets(Some(NAME2), &tc).await;
assert_eq!(res.len(), 1, "received: {:?}", res);
assert_eq!(res[0].nodename.as_ref().unwrap(), NAME2);
let res = list_targets(Some(NAME3), &tc).await;
assert_eq!(res.len(), 1, "received: {:?}", res);
assert_eq!(res[0].nodename.as_ref().unwrap(), NAME3);
let res = list_targets(Some(PARTIAL_NAME_MATCH), &tc).await;
assert_eq!(res.len(), 1, "received: {:?}", res);
assert!(res.iter().any(|t| {
let name = t.nodename.as_ref().unwrap();
// Check either partial match just in case the backing impl
// changes ordering. Possible todo here would be to return multiple
// targets when there is a partial match.
name == NAME3 || name == NAME2
}));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_handle_fastboot_target_no_serial() {
let tc = Rc::new(TargetCollection::new());
handle_fastboot_target(&tc, ffx::FastbootTarget::EMPTY);
assert_eq!(tc.targets().len(), 0, "target collection should remain empty");
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_handle_fastboot_target() {
let tc = Rc::new(TargetCollection::new());
handle_fastboot_target(
&tc,
ffx::FastbootTarget { serial: Some("12345".to_string()), ..ffx::FastbootTarget::EMPTY },
);
assert_eq!(tc.targets()[0].serial().as_deref(), Some("12345"));
}
#[derive(Default)]
struct FakeMdns {}
#[async_trait(?Send)]
impl FidlProtocol for FakeMdns {
type Protocol = ffx::MdnsMarker;
type StreamHandler = FidlStreamHandler<Self>;
async fn handle(&self, _cx: &Context, _req: ffx::MdnsRequest) -> Result<()> {
futures::future::pending::<()>().await;
Ok(())
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_persisted_manual_target_remove() {
let tc_impl = Rc::new(RefCell::new(TargetCollectionProtocol::default()));
let fake_daemon = FakeDaemonBuilder::new()
.register_fidl_protocol::<FakeMdns>()
.register_fidl_protocol::<FakeFastboot>()
.inject_fidl_protocol(tc_impl.clone())
.build();
// Set one timeout 1 hour in the future; the other will have no timeout.
let expiry = (SystemTime::now() + Duration::from_secs(3600))
.duration_since(UNIX_EPOCH)
.expect("Problem getting a duration relative to epoch.")
.as_secs();
tc_impl.borrow().manual_targets.add("127.0.0.1:8022".to_string(), None).await.unwrap();
tc_impl
.borrow()
.manual_targets
.add("127.0.0.1:8023".to_string(), Some(expiry))
.await
.unwrap();
let target_collection =
Context::new(fake_daemon.clone()).get_target_collection().await.unwrap();
tc_impl.borrow().load_manual_targets(&target_collection).await;
let proxy = fake_daemon.open_proxy::<ffx::TargetCollectionMarker>().await;
let res = list_targets(None, &proxy).await;
assert_eq!(2, res.len());
assert!(proxy.remove_target("127.0.0.1:8022").await.unwrap());
assert!(proxy.remove_target("127.0.0.1:8023").await.unwrap());
assert_eq!(0, list_targets(None, &proxy).await.len());
assert_eq!(
tc_impl.borrow().manual_targets.get_or_default().await,
Map::<String, Value>::new()
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_add_target() {
let fake_daemon = FakeDaemonBuilder::new()
.register_fidl_protocol::<FakeMdns>()
.register_fidl_protocol::<FakeFastboot>()
.register_fidl_protocol::<TargetCollectionProtocol>()
.build();
let target_addr = TargetAddr::new("[::1]:0").unwrap();
let proxy = fake_daemon.open_proxy::<ffx::TargetCollectionMarker>().await;
proxy
.add_target(&mut target_addr.into(), ffx::AddTargetConfig::EMPTY)
.await
.unwrap()
.unwrap();
let target_collection = Context::new(fake_daemon).get_target_collection().await.unwrap();
let target = target_collection.get(target_addr.to_string()).unwrap();
assert_eq!(target.addrs().len(), 1);
assert_eq!(target.addrs().into_iter().next(), Some(target_addr));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_add_ephemeral_target() {
let fake_daemon = FakeDaemonBuilder::new()
.register_fidl_protocol::<FakeMdns>()
.register_fidl_protocol::<FakeFastboot>()
.register_fidl_protocol::<TargetCollectionProtocol>()
.build();
let target_addr = TargetAddr::new("[::1]:0").unwrap();
let proxy = fake_daemon.open_proxy::<ffx::TargetCollectionMarker>().await;
proxy.add_ephemeral_target(&mut target_addr.into(), 3600).await.unwrap();
let target_collection = Context::new(fake_daemon).get_target_collection().await.unwrap();
let target = target_collection.get(target_addr.to_string()).unwrap();
assert_eq!(target.addrs().len(), 1);
assert_eq!(target.addrs().into_iter().next(), Some(target_addr));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_add_target_with_port() {
let fake_daemon = FakeDaemonBuilder::new()
.register_fidl_protocol::<FakeMdns>()
.register_fidl_protocol::<FakeFastboot>()
.register_fidl_protocol::<TargetCollectionProtocol>()
.build();
let target_addr = TargetAddr::new("[::1]:8022").unwrap();
let proxy = fake_daemon.open_proxy::<ffx::TargetCollectionMarker>().await;
proxy
.add_target(&mut target_addr.into(), ffx::AddTargetConfig::EMPTY)
.await
.unwrap()
.unwrap();
let target_collection = Context::new(fake_daemon).get_target_collection().await.unwrap();
let target = target_collection.get(target_addr.to_string()).unwrap();
assert_eq!(target.addrs().len(), 1);
assert_eq!(target.addrs().into_iter().next(), Some(target_addr));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_add_ephemeral_target_with_port() {
let fake_daemon = FakeDaemonBuilder::new()
.register_fidl_protocol::<FakeMdns>()
.register_fidl_protocol::<FakeFastboot>()
.register_fidl_protocol::<TargetCollectionProtocol>()
.build();
let target_addr = TargetAddr::new("[::1]:8022").unwrap();
let proxy = fake_daemon.open_proxy::<ffx::TargetCollectionMarker>().await;
proxy.add_ephemeral_target(&mut target_addr.into(), 3600).await.unwrap();
let target_collection = Context::new(fake_daemon).get_target_collection().await.unwrap();
let target = target_collection.get(target_addr.to_string()).unwrap();
assert_eq!(target.addrs().len(), 1);
assert_eq!(target.addrs().into_iter().next(), Some(target_addr));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_persisted_manual_target_add() {
let tc_impl = Rc::new(RefCell::new(TargetCollectionProtocol::default()));
let fake_daemon = FakeDaemonBuilder::new()
.register_fidl_protocol::<FakeMdns>()
.register_fidl_protocol::<FakeFastboot>()
.inject_fidl_protocol(tc_impl.clone())
.build();
let proxy = fake_daemon.open_proxy::<ffx::TargetCollectionMarker>().await;
proxy
.add_target(
&mut ffx::TargetAddrInfo::IpPort(ffx::TargetIpPort {
ip: IpAddress::Ipv6(Ipv6Address {
addr: [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
}),
port: 8022,
scope_id: 1,
}),
ffx::AddTargetConfig::EMPTY,
)
.await
.unwrap()
.unwrap();
let target_collection = Context::new(fake_daemon).get_target_collection().await.unwrap();
assert_eq!(1, target_collection.targets().len());
let mut map = Map::<String, Value>::new();
map.insert("[fe80::1%1]:8022".to_string(), Value::Null);
assert_eq!(tc_impl.borrow().manual_targets.get().await.unwrap(), json!(map));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_persisted_ephemeral_target_add() {
let tc_impl = Rc::new(RefCell::new(TargetCollectionProtocol::default()));
let fake_daemon = FakeDaemonBuilder::new()
.register_fidl_protocol::<FakeMdns>()
.register_fidl_protocol::<FakeFastboot>()
.inject_fidl_protocol(tc_impl.clone())
.build();
let proxy = fake_daemon.open_proxy::<ffx::TargetCollectionMarker>().await;
proxy
.add_ephemeral_target(
&mut ffx::TargetAddrInfo::IpPort(ffx::TargetIpPort {
ip: IpAddress::Ipv6(Ipv6Address {
addr: [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
}),
port: 8022,
scope_id: 1,
}),
3600,
)
.await
.unwrap();
let target_collection = Context::new(fake_daemon).get_target_collection().await.unwrap();
assert_eq!(1, target_collection.targets().len());
assert!(tc_impl.borrow().manual_targets.get().await.unwrap().is_object());
let value = tc_impl.borrow().manual_targets.get().await.unwrap();
assert!(value.is_object());
let map = value.as_object().unwrap();
assert!(map.contains_key("[fe80::1%1]:8022"));
let target = map.get(&"[fe80::1%1]:8022".to_string());
assert!(target.is_some());
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Couldn't get duration from epoch.")
.as_secs();
assert!(target.unwrap().as_u64().unwrap() > now);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_persisted_manual_target_load() {
let tc_impl = Rc::new(RefCell::new(TargetCollectionProtocol::default()));
let fake_daemon = FakeDaemonBuilder::new()
.register_fidl_protocol::<FakeMdns>()
.register_fidl_protocol::<FakeFastboot>()
.inject_fidl_protocol(tc_impl.clone())
.build();
// We attempt to load three targets:
// - One with no timeout, should load,
// - One with an expired timeout, should load, and
// - One with a future timeout, should load.
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Couldn't load duration since epoch.")
.as_secs();
let expired = now - 3600;
let future = now + 3600;
tc_impl.borrow().manual_targets.add("127.0.0.1:8022".to_string(), None).await.unwrap();
tc_impl
.borrow()
.manual_targets
.add("127.0.0.1:8023".to_string(), Some(expired))
.await
.unwrap();
tc_impl
.borrow()
.manual_targets
.add("127.0.0.1:8024".to_string(), Some(future))
.await
.unwrap();
let target_collection = Context::new(fake_daemon).get_target_collection().await.unwrap();
// This happens in FidlProtocol::start(), but we want to avoid binding the
// network sockets in unit tests, thus not calling start.
tc_impl.borrow().load_manual_targets(&target_collection).await;
let target = target_collection.get("127.0.0.1:8022".to_string()).unwrap();
assert_eq!(target.ssh_address(), Some("127.0.0.1:8022".parse::<SocketAddr>().unwrap()));
let target = target_collection.get("127.0.0.1:8023".to_string()).unwrap();
assert_eq!(target.ssh_address(), Some("127.0.0.1:8023".parse::<SocketAddr>().unwrap()));
let target = target_collection.get("127.0.0.1:8024".to_string()).unwrap();
assert_eq!(target.ssh_address(), Some("127.0.0.1:8024".parse::<SocketAddr>().unwrap()));
}
}