blob: e2c699cc87d79ee7fda11e1444c89e8ba6685cce [file]
// Copyright 2019 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 anyhow::{anyhow, bail, Context, Result};
use ascendd::Ascendd;
use async_trait::async_trait;
use errors::ffx_error;
use ffx_build_version::build_info;
use ffx_config::EnvironmentContext;
use ffx_daemon_core::events::{self, EventHandler};
use ffx_daemon_events::{
DaemonEvent, TargetConnectionState, TargetEvent, TargetInfo, WireTrafficType,
};
use ffx_daemon_protocols::create_protocol_register_map;
use ffx_daemon_target::{
manual_targets::{Config, ManualTargets},
target::Target,
target_collection::TargetCollection,
zedboot::zedboot_discovery,
};
use ffx_metrics::{add_daemon_launch_event, add_daemon_metrics_event};
use ffx_stream_util::TryStreamUtilExt;
use fidl::{endpoints::ClientEnd, prelude::*};
use fidl_fuchsia_developer_ffx::{
self as ffx, DaemonError, DaemonMarker, DaemonRequest, DaemonRequestStream,
RepositoryRegistryMarker, TargetCollectionMarker, VersionInfo,
};
use fidl_fuchsia_developer_remotecontrol::{RemoteControlMarker, RemoteControlProxy};
use fidl_fuchsia_io::OpenFlags;
use fidl_fuchsia_overnet::{Peer, ServiceProviderRequest, ServiceProviderRequestStream};
use fidl_fuchsia_overnet_protocol::NodeId;
use fuchsia_async::{Task, TimeoutExt, Timer};
use futures::{
channel::{mpsc, oneshot},
executor::block_on,
prelude::*,
};
use hoist::{Hoist, OvernetInstance};
use notify::{RecursiveMode, Watcher};
use protocols::{DaemonProtocolProvider, ProtocolError, ProtocolRegister};
use rcs::RcsConnection;
use std::{
cell::Cell,
collections::HashSet,
hash::{Hash, Hasher},
net::SocketAddr,
path::PathBuf,
rc::Rc,
time::{Duration, Instant},
};
#[cfg(not(target_os = "macos"))]
use notify::RecommendedWatcher;
// `RecommendedWatcher` is a type alias to FsEvents in the notify crate.
// On mac this seems to have bugs about what's reported and when regarding
// file removal. Without PollWatcher the watcher would report a fresh file
// as having been deleted even if it is a new file.
//
// See fxbug.dev/114564 for details on what happens when using the Default
// RecommendedWatcher (FsEvents).
//
// It's possible that in future versions of this crate this bug will be fixed,
// so it may be worth revisiting this in the future in order to make the code
// in this file a little cleaner and easier to read.
#[cfg(target_os = "macos")]
use notify::PollWatcher as RecommendedWatcher;
// Daemon
/// Determines if targets discovered should expire. Defaults to "true"
const DISCOVERY_EXPIRE_TARGETS: &str = "discovery.expire_targets";
/// Whether we should discover targets over USB. Defaults to "false"
const OVERNET_ENABLE_USB: &str = "overnet.enable_usb";
// This is just for mocking config values for unit testing.
#[async_trait(?Send)]
trait ConfigReader: Send + Sync {
async fn get(&self, q: &str) -> Result<Option<String>>;
}
#[derive(Default)]
struct DefaultConfigReader {}
#[async_trait(?Send)]
impl ConfigReader for DefaultConfigReader {
async fn get(&self, q: &str) -> Result<Option<String>> {
Ok(ffx_config::get(q).await?)
}
}
pub struct DaemonEventHandler {
hoist: Hoist,
target_collection: Rc<TargetCollection>,
}
impl DaemonEventHandler {
fn new(hoist: Hoist, target_collection: Rc<TargetCollection>) -> Self {
Self { hoist, target_collection }
}
#[tracing::instrument(skip(self))]
async fn handle_overnet_peer(&self, node_id: u64) {
let rcs = match RcsConnection::new(self.hoist.clone(), &mut NodeId { id: node_id }) {
Ok(rcs) => rcs,
Err(e) => {
tracing::error!(
"Target from Overnet {} failed to connect to RCS: {:?}",
node_id,
e
);
return;
}
};
let target = match Target::from_rcs_connection(rcs).await {
Ok(target) => target,
Err(err) => {
tracing::error!(
"Target from Overnet {} could not be identified: {:?}",
node_id,
err
);
return;
}
};
tracing::trace!("Target from Overnet {} is {}", node_id, target.nodename_str());
let target = self.target_collection.merge_insert(target);
target.run_logger();
}
#[tracing::instrument(skip(self))]
async fn handle_overnet_peer_lost(&self, node_id: u64) {
if let Some(target) = self
.target_collection
.targets()
.iter()
.find(|target| target.overnet_node_id() == Some(node_id))
{
target.disconnect();
}
}
#[tracing::instrument(skip(self))]
fn handle_fastboot(&self, t: TargetInfo) {
tracing::trace!(
"Found new target via fastboot: {}",
t.nodename.clone().unwrap_or("<unknown>".to_string())
);
let target = self.target_collection.merge_insert(Target::from_target_info(t.into()));
target.update_connection_state(|s| match s {
TargetConnectionState::Disconnected | TargetConnectionState::Fastboot(_) => {
TargetConnectionState::Fastboot(Instant::now())
}
_ => s,
});
}
#[tracing::instrument(skip(self))]
async fn handle_zedboot(&self, t: TargetInfo) {
tracing::trace!(
"Found new target via zedboot: {}",
t.nodename.clone().unwrap_or("<unknown>".to_string())
);
let target = self.target_collection.merge_insert(Target::from_netsvc_target_info(t.into()));
target.update_connection_state(|s| match s {
TargetConnectionState::Disconnected | TargetConnectionState::Zedboot(_) => {
TargetConnectionState::Zedboot(Instant::now())
}
_ => s,
});
}
}
#[async_trait(?Send)]
impl DaemonProtocolProvider for Daemon {
async fn open_protocol(&self, protocol_name: String) -> Result<fidl::Channel> {
let (server, client) = fidl::Channel::create();
self.protocol_register
.open(
protocol_name,
protocols::Context::new(self.clone()),
fidl::AsyncChannel::from_channel(server)?,
)
.await?;
Ok(client)
}
async fn open_target_proxy(
&self,
target_identifier: Option<String>,
moniker: &str,
capability_name: &str,
) -> Result<fidl::Channel> {
let (_, channel) =
self.open_target_proxy_with_info(target_identifier, moniker, capability_name).await?;
Ok(channel)
}
async fn get_target_event_queue(
&self,
target_identifier: Option<String>,
) -> Result<(Rc<Target>, events::Queue<TargetEvent>)> {
let target = self
.get_target(target_identifier)
.await
.map_err(|e| anyhow!("{:#?}", e))
.context("getting default target")?;
target.run_host_pipe();
let events = target.events.clone();
Ok((target, events))
}
async fn open_target_proxy_with_info(
&self,
target_identifier: Option<String>,
moniker: &str,
capability_name: &str,
) -> Result<(ffx::TargetInfo, fidl::Channel)> {
let target = self.get_rcs_ready_target(target_identifier).await?;
let rcs = target
.rcs()
.ok_or(anyhow!("rcs disconnected after event fired"))
.context("getting rcs instance")?;
let (server, client) = fidl::Channel::create();
// TODO(awdavies): Handle these errors properly so the client knows what happened.
rcs.proxy
.connect_capability(moniker, capability_name, server, OpenFlags::RIGHT_READABLE)
.await
.context("FIDL connection")?
.map_err(|e| anyhow!("{:#?}", e))
.context("proxy connect")?;
tracing::debug!("Returning target and proxy for {}@{}", target.nodename_str(), target.id());
Ok((target.as_ref().into(), client))
}
async fn get_target_info(&self, target_identifier: Option<String>) -> Result<ffx::TargetInfo> {
let target = self
.get_target(target_identifier)
.await
.map_err(|e| anyhow!("{:#?}", e))
.context("getting target")?;
Ok(target.as_ref().into())
}
#[tracing::instrument(level = "info", skip(self))]
async fn open_remote_control(
&self,
target_identifier: Option<String>,
) -> Result<RemoteControlProxy> {
let target = self.get_rcs_ready_target(target_identifier).await?;
// Ensure auto-connect has at least started.
let mut rcs = target
.rcs()
.ok_or(anyhow!("rcs disconnected after event fired"))
.context("getting rcs instance")?;
let (proxy, remote) = fidl::endpoints::create_proxy::<RemoteControlMarker>()?;
rcs.copy_to_channel(remote.into_channel())?;
Ok(proxy)
}
async fn daemon_event_queue(&self) -> events::Queue<DaemonEvent> {
self.event_queue.clone()
}
async fn get_target_collection(&self) -> Result<Rc<TargetCollection>> {
Ok(self.target_collection.clone())
}
}
#[async_trait(?Send)]
impl EventHandler<DaemonEvent> for DaemonEventHandler {
#[tracing::instrument(skip(self))]
async fn on_event(&self, event: DaemonEvent) -> Result<events::Status> {
tracing::debug!("! DaemonEvent::{:?}", event);
match event {
DaemonEvent::WireTraffic(traffic) => match traffic {
WireTrafficType::Mdns(t) => {
tracing::warn!("mdns traffic fired in daemon. This is deprecated: {:?}", t);
}
WireTrafficType::Fastboot(t) => {
self.handle_fastboot(t);
}
WireTrafficType::Zedboot(t) => {
self.handle_zedboot(t).await;
}
},
DaemonEvent::OvernetPeer(node_id) => {
self.handle_overnet_peer(node_id).await;
}
DaemonEvent::OvernetPeerLost(node_id) => {
self.handle_overnet_peer_lost(node_id).await;
}
_ => (),
}
// This handler is never done unless the target_collection is dropped.
Ok(events::Status::Waiting)
}
}
#[derive(Clone)]
/// Defines the daemon object. This is used by "ffx daemon start".
///
/// Typical usage is:
/// let mut daemon = ffx_daemon::Daemon::new(socket_path);
/// daemon.start().await
pub struct Daemon {
// The path to the ascendd socket this daemon will bind to
socket_path: PathBuf,
// The event queue is a collection of subscriptions to which DaemonEvents will be published.
event_queue: events::Queue<DaemonEvent>,
// All the targets currently known to the daemon.
// This may include targets the daemon has no access to.
target_collection: Rc<TargetCollection>,
// ascendd is the overnet daemon running on the Linux host. It manages the mesh and the
// connections to the devices and other peers (for example, a connection to the frontend).
// With ffx, ascendd is embedded within the ffx daemon (when ffx daemon is launched, we don’t
// need an extra process for ascendd).
ascendd: Rc<Cell<Option<Ascendd>>>,
// Handles the registered FIDL protocols and associated handles. This is initialized with the
// list of protocols defined in src/developer/ffx/daemon/protocols/BUILD.gn (the deps field in
// ffx_protocol) using the macro generate_protocol_map in
// src/developer/ffx/build/templates/protocols_macro.rs.jinja.
protocol_register: ProtocolRegister,
// All the persistent long running tasks spawned by the daemon. The tasks are standalone. That
// means that they execute by themselves without any intervention from the daemon.
// The purpose of this vector is to keep the reference strong count positive until the daemon is
// dropped.
tasks: Vec<Rc<Task<()>>>,
}
impl Daemon {
pub fn new(socket_path: PathBuf) -> Daemon {
let target_collection = Rc::new(TargetCollection::new());
let event_queue = events::Queue::new(&target_collection);
target_collection.set_event_queue(event_queue.clone());
Self {
socket_path,
target_collection,
event_queue,
protocol_register: ProtocolRegister::new(create_protocol_register_map()),
ascendd: Rc::new(Cell::new(None)),
tasks: Vec::new(),
}
}
pub async fn start(&mut self, hoist: &Hoist) -> Result<()> {
let context =
ffx_config::global_env_context().context("Discovering ffx environment context")?;
let (quit_tx, quit_rx) = mpsc::channel(1);
self.log_startup_info(&context).await.context("Logging startup info")?;
self.start_protocols().await?;
self.start_discovery(hoist).await?;
self.start_ascendd(hoist).await?;
let _socket_file_watcher =
self.start_socket_watch(quit_tx.clone()).await.context("Starting socket watcher")?;
let should_start_expiry = ffx_config::get(DISCOVERY_EXPIRE_TARGETS).await.unwrap_or(true);
if should_start_expiry == true {
self.start_target_expiry(Duration::from_secs(1));
}
self.serve(&context, hoist, quit_tx, quit_rx).await.context("Serving clients")
}
#[tracing::instrument(skip(self))]
async fn log_startup_info(&self, context: &EnvironmentContext) -> Result<()> {
let pid = std::process::id();
let buildid = context.daemon_version_string()?;
let version_info = build_info();
let commit_hash = version_info.commit_hash.as_deref().unwrap_or("<unknown>");
let commit_timestamp =
version_info.commit_timestamp.map(|t| t.to_string()).unwrap_or("<unknown>".to_owned());
let build_version = version_info.build_version.as_deref().unwrap_or("<unknown>");
tracing::info!(
"Beginning daemon startup\nBuild Version: {build_version}\nCommit Timestamp: {commit_timestamp}\nCommit Hash: {commit_hash}\nBinary Build ID: {buildid}\nPID: {pid}",
);
add_daemon_launch_event().await;
Ok(())
}
async fn start_protocols(&mut self) -> Result<()> {
let cx = protocols::Context::new(self.clone());
let ((), ()) = futures::future::try_join(
self.protocol_register
.start(RepositoryRegistryMarker::PROTOCOL_NAME.to_string(), cx.clone()),
self.protocol_register.start(TargetCollectionMarker::PROTOCOL_NAME.to_string(), cx),
)
.await?;
Ok(())
}
/// Awaits a target that has RCS active.
async fn get_rcs_ready_target(&self, target_query: Option<String>) -> Result<Rc<Target>> {
let target = self
.get_target(target_query)
.await
.map_err(|e| anyhow!("{:#?}", e))
.context("getting default target")?;
if matches!(target.get_connection_state(), TargetConnectionState::Fastboot(_)) {
let nodename = target.nodename().unwrap_or("<No Nodename>".to_string());
bail!("Attempting to open RCS on a fastboot target: {}", nodename);
}
if matches!(target.get_connection_state(), TargetConnectionState::Zedboot(_)) {
let nodename = target.nodename().unwrap_or("<No Nodename>".to_string());
bail!("Attempting to connect to RCS on a zedboot target: {}", nodename);
}
// Ensure auto-connect has at least started.
target.run_host_pipe();
target
.events
.wait_for(None, |e| e == TargetEvent::RcsActivated)
.await
.context("waiting for RCS activation")?;
tracing::debug!("RCS activated for {}@{}", target.nodename_str(), target.id());
Ok(target)
}
/// Start all discovery tasks
async fn start_discovery(&mut self, hoist: &Hoist) -> Result<()> {
let daemon_event_handler =
DaemonEventHandler::new(hoist.clone(), self.target_collection.clone());
self.event_queue.add_handler(daemon_event_handler).await;
// TODO: these tasks could and probably should be managed by the daemon
// instead of being detached.
Daemon::spawn_onet_discovery(hoist, self.event_queue.clone());
let discovery = zedboot_discovery(self.event_queue.clone()).await?;
self.tasks.push(Rc::new(discovery));
Ok(())
}
#[tracing::instrument(skip(self))]
async fn start_ascendd(&mut self, hoist: &Hoist) -> Result<()> {
// Start the ascendd socket only after we have registered our protocols.
tracing::debug!("Starting ascendd");
let client_routing = false; // Don't route between ffx clients
let ascendd = Ascendd::new(
ascendd::Opt {
sockpath: Some(self.socket_path.clone()),
client_routing,
usb: ffx_config::get(OVERNET_ENABLE_USB).await.unwrap_or(false),
..Default::default()
},
&hoist,
// TODO: this just prints serial output to stdout - ffx probably wants to take a more
// nuanced approach here.
blocking::Unblock::new(std::io::stdout()),
)
.await
.map_err(|e| ffx_error!("Error trying to start daemon socket: {e}"))?;
self.ascendd.replace(Some(ascendd));
Ok(())
}
#[tracing::instrument(skip(self))]
async fn start_socket_watch(&self, quit_tx: mpsc::Sender<()>) -> Result<RecommendedWatcher> {
let socket_path = self.socket_path.clone();
let socket_dir = self.socket_path.parent().context("Getting parent directory of socket")?;
let event_handler = move |res| {
let mut quit_tx = quit_tx.clone();
block_on(async {
use notify::event::{Event, EventKind::Remove};
match res {
Ok(Event { kind: Remove(_), paths, .. }) if paths.contains(&socket_path) => {
tracing::info!("daemon socket was deleted, triggering quit message.");
quit_tx.send(()).await.ok();
}
Err(ref e @ notify::Error { ref kind, .. }) => {
match kind {
notify::ErrorKind::Io(ioe) => {
tracing::debug!("IO error. Ignoring {ioe:?}");
}
_ => {
// If we get a non-spurious error, treat that as something that
// should cause us to exit.
tracing::warn!("exiting due to file watcher error: {e:?}");
quit_tx.send(()).await.ok();
}
}
}
Ok(_) => {} // just ignore any non-delete event or for any other file.
}
})
};
#[cfg(target_os = "macos")]
let res = RecommendedWatcher::new(
event_handler,
notify::Config::default().with_poll_interval(Duration::from_millis(500)),
);
#[cfg(not(target_os = "macos"))]
let res = RecommendedWatcher::new(event_handler, notify::Config::default());
let mut watcher = res.context("Creating watcher")?;
// we have to watch the directory because watching a file does weird things and only
// half works. This seems to be a limitation of underlying libraries.
watcher
.watch(&socket_dir, RecursiveMode::NonRecursive)
.context("Setting watcher context")?;
tracing::debug!(
"Watching daemon socket file at {socket_path}, will gracefully exit if it's removed.",
socket_path = self.socket_path.display()
);
Ok(watcher)
}
fn start_target_expiry(&mut self, frequency: Duration) {
let target_collection = Rc::downgrade(&self.target_collection);
self.tasks.push(Rc::new(Task::local(async move {
loop {
Timer::new(frequency.clone()).await;
let manual_targets = Config::default();
match target_collection.upgrade() {
Some(target_collection) => {
for target in target_collection.targets() {
// Manually-added remote targets will not be discovered by mDNS,
// and as a result will not have host-pipe triggered automatically
// by the mDNS event handler.
if target.is_manual() {
target.run_host_pipe();
}
target.expire_state();
if target.is_manual() && !target.is_connected() {
// If a manual target has been allowed to transition to the
// "disconnected" state, it should be removed from the collection.
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| {
tracing::error!(
"Unable to persist ephemeral target removal: {}",
e
);
});
}
target_collection.remove_ephemeral_target(target);
}
}
}
None => return,
}
}
})))
}
/// get_target attempts to get the target that matches the match string if
/// provided, otherwise the default target from the target collection.
async fn get_target(&self, matcher: Option<String>) -> Result<Rc<Target>, DaemonError> {
#[cfg(not(test))]
const GET_TARGET_TIMEOUT: Duration = Duration::from_secs(8);
#[cfg(test)]
const GET_TARGET_TIMEOUT: Duration = Duration::from_secs(1);
// TODO(72818): make target match timeout configurable / paramterable
self.target_collection
.wait_for_match(matcher)
.on_timeout(GET_TARGET_TIMEOUT, || match self.target_collection.is_empty() {
true => Err(DaemonError::TargetCacheEmpty),
false => Err(DaemonError::TargetNotFound),
})
.await
}
#[tracing::instrument(skip(self, quit_tx, stream))]
async fn handle_requests_from_stream(
&self,
quit_tx: &mpsc::Sender<()>,
stream: DaemonRequestStream,
info: &VersionInfo,
) -> Result<()> {
stream
.map_err(|e| anyhow!("reading FIDL stream: {:#}", e))
.try_for_each_concurrent_while_connected(None, |r| async {
let debug_req_string = format!("{:?}", r);
if let Err(e) = self.handle_request(quit_tx, r, info).await {
tracing::error!("error while handling request `{}`: {}", debug_req_string, e);
}
Ok(())
})
.await
}
fn spawn_onet_discovery(hoist: &Hoist, queue: events::Queue<DaemonEvent>) {
let hoist = hoist.clone();
fuchsia_async::Task::local(async move {
let mut known_peers: HashSet<PeerSetElement> = Default::default();
loop {
let svc = match hoist.connect_as_service_consumer() {
Ok(svc) => svc,
Err(err) => {
tracing::info!("Overnet setup failed: {}, will retry in 1s", err);
Timer::new(Duration::from_secs(1)).await;
continue;
}
};
loop {
match svc.list_peers().await {
Ok(new_peers) => {
known_peers =
Self::handle_overnet_peers(&queue, known_peers, new_peers);
}
Err(err) => {
tracing::info!("Overnet peer discovery failed: {}, will retry", err);
Timer::new(Duration::from_secs(1)).await;
// break out of the peer discovery loop on error in
// order to reconnect, in case the error causes the
// overnet interface to go bad.
break;
}
};
}
}
})
.detach();
}
#[tracing::instrument(skip(queue))]
fn handle_overnet_peers(
queue: &events::Queue<DaemonEvent>,
known_peers: HashSet<PeerSetElement>,
peers: Vec<Peer>,
) -> HashSet<PeerSetElement> {
let mut new_peers: HashSet<PeerSetElement> = Default::default();
for peer in peers {
new_peers.insert(PeerSetElement(peer));
}
for peer in new_peers.difference(&known_peers) {
let peer = &peer.0;
let peer_has_rcs = peer
.description
.services
.as_ref()
.map(|v| v.contains(&RemoteControlMarker::PROTOCOL_NAME.to_string()))
.unwrap_or(false);
if peer_has_rcs {
queue.push(DaemonEvent::OvernetPeer(peer.id.id)).unwrap_or_else(|err| {
tracing::warn!(
"Overnet discovery failed to enqueue event {:?}: {}",
DaemonEvent::OvernetPeer(peer.id.id),
err
);
});
}
}
for peer in known_peers.difference(&new_peers) {
let peer = &peer.0;
queue.push(DaemonEvent::OvernetPeerLost(peer.id.id)).unwrap_or_else(|err| {
tracing::warn!(
"Overnet discovery failed to enqueue event {:?}: {}",
DaemonEvent::OvernetPeerLost(peer.id.id),
err
);
});
}
new_peers
}
#[tracing::instrument(skip(self))]
async fn handle_request(
&self,
quit_tx: &mpsc::Sender<()>,
req: DaemonRequest,
info: &VersionInfo,
) -> Result<()> {
tracing::debug!("daemon received request: {:?}", req);
match req {
DaemonRequest::Quit { responder } => {
tracing::info!("Received quit request.");
if cfg!(test) {
panic!("quit() should not be invoked in test code");
}
quit_tx.clone().send(()).await?;
responder.send(true).context("error sending response")?;
}
DaemonRequest::GetVersionInfo { responder } => {
return responder.send(info).context("sending GetVersionInfo response");
}
DaemonRequest::ConnectToProtocol { name, server_end, responder } => {
let name_for_analytics = name.clone();
match self
.protocol_register
.open(
name,
protocols::Context::new(self.clone()),
fidl::AsyncChannel::from_channel(server_end)?,
)
.await
{
Ok(()) => responder.send(Ok(())).context("fidl response")?,
Err(e) => {
tracing::error!("{}", e);
match e {
ProtocolError::NoProtocolFound(_) => {
responder.send(Err(DaemonError::ProtocolNotFound))?
}
ProtocolError::StreamOpenError(_) => {
responder.send(Err(DaemonError::ProtocolOpenError))?
}
ProtocolError::BadRegisterState(_)
| ProtocolError::DuplicateTaskId(..) => {
responder.send(Err(DaemonError::BadProtocolRegisterState))?
}
}
}
}
add_daemon_metrics_event(
format!("connect_to_protocol: {}", &name_for_analytics).as_str(),
)
.await;
}
}
Ok(())
}
#[tracing::instrument(skip(self))]
async fn serve(
&self,
context: &EnvironmentContext,
hoist: &Hoist,
quit_tx: mpsc::Sender<()>,
mut quit_rx: mpsc::Receiver<()>,
) -> Result<()> {
let (s, p) = fidl::Channel::create();
let chan = fidl::AsyncChannel::from_channel(s).context("failed to make async channel")?;
let mut stream = ServiceProviderRequestStream::from_channel(chan);
let mut info = build_info();
info.build_id = Some(context.daemon_version_string()?);
tracing::debug!("Starting daemon overnet server");
hoist.publish_service(DaemonMarker::PROTOCOL_NAME, ClientEnd::new(p))?;
tracing::debug!("Starting daemon serve loop");
let (break_loop_tx, mut break_loop_rx) = oneshot::channel();
let mut break_loop_tx = Some(break_loop_tx);
loop {
futures::select! {
req = stream.try_next() => match req.context("error running protocol provider server")? {
Some(ServiceProviderRequest::ConnectToService {
chan,
info: _,
control_handle: _control_handle,
}) =>
{
tracing::trace!("Received protocol request for protocol");
let chan =
fidl::AsyncChannel::from_channel(chan).context("failed to make async channel")?;
let daemon_clone = self.clone();
let mut quit_tx = quit_tx.clone();
let info = info.clone();
Task::local(async move {
if let Err(err) = daemon_clone.handle_requests_from_stream(&quit_tx, DaemonRequestStream::from_channel(chan), &info).await {
tracing::error!("error handling request: {:?}", err);
quit_tx.send(()).await.expect("Failed to gracefully send quit message, aborting.");
}
})
.detach();
},
o => {
tracing::warn!("Received unknown message or no message on provider server: {o:?}");
break;
}
},
_ = quit_rx.next() => {
if let Some(break_loop_tx) = break_loop_tx.take() {
tracing::debug!("Starting graceful shutdown of daemon socket");
match std::fs::remove_file(self.socket_path.clone()) {
Ok(()) => {}
Err(e) => tracing::error!("failed to remove socket file: {}", e),
}
self.protocol_register
.shutdown(protocols::Context::new(self.clone()))
.await
.unwrap_or_else(|e| {
tracing::error!("shutting down protocol register: {:?}", e)
});
add_daemon_metrics_event("quit").await;
// It is desirable for the client to receive an ACK for the quit
// request. As Overnet has a potentially complicated routing
// path, it is tricky to implement some notion of a bounded
// "flush" for this response, however in practice it is only
// necessary here to wait long enough for the message to likely
// leave the local process before exiting. Enqueue a detached
// timer to shut down the daemon before sending the response.
// This is detached because once the client receives the
// response, the client will disconnect it's socket. If the
// local reactor observes this disconnection before the timer
// expires, an in-line timer wait would never fire, and the
// daemon would never exit.
Task::local(async move {
Timer::new(std::time::Duration::from_millis(20)).await;
break_loop_tx.send(()).expect("failed to send loop break message");
})
.detach();
} else {
tracing::trace!("Received quit message after shutdown was already initiated");
}
},
_ = break_loop_rx => {
tracing::debug!("Breaking main daemon socket loop");
break;
}
}
}
tracing::debug!("Graceful shutdown of daemon loop completed");
ffx_config::logging::disable_stdio_logging();
Ok(())
}
}
// PeerSetElement wraps an overnet Peer object for inclusion in a Set
// or other collection reliant on Eq and HAsh, using the NodeId as the
// discriminator.
#[derive(Debug)]
struct PeerSetElement(Peer);
impl PartialEq for PeerSetElement {
fn eq(&self, other: &Self) -> bool {
self.0.id == other.0.id
}
}
impl Eq for PeerSetElement {}
impl Hash for PeerSetElement {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.id.hash(state);
}
}
#[cfg(test)]
mod test {
use super::*;
use addr::TargetAddr;
use assert_matches::assert_matches;
use chrono::Utc;
use ffx_daemon_target::target::{TargetAddrEntry, TargetAddrType};
use fidl_fuchsia_developer_ffx::{DaemonMarker, DaemonProxy};
use fidl_fuchsia_developer_remotecontrol::RemoteControlMarker;
use fidl_fuchsia_overnet_protocol::PeerDescription;
use fuchsia_async::Task;
use std::{
cell::RefCell, collections::BTreeSet, iter::FromIterator, str::FromStr, time::SystemTime,
};
fn spawn_test_daemon() -> (DaemonProxy, Daemon, Task<Result<()>>) {
let tempdir = tempfile::tempdir().expect("Creating tempdir");
let socket_path = tempdir.path().join("ascendd.sock");
let d = Daemon::new(socket_path);
let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<DaemonMarker>().unwrap();
let (quit_tx, _quit_rx) = mpsc::channel(1);
let d2 = d.clone();
let task = Task::local(async move {
d2.handle_requests_from_stream(&quit_tx, stream, &build_info()).await
});
(proxy, d, task)
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_open_rcs_on_fastboot_error() {
let (_proxy, daemon, _task) = spawn_test_daemon();
let target = Target::new_with_serial("abc");
daemon.target_collection.merge_insert(target);
let result = daemon.open_remote_control(None).await;
assert!(result.is_err());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_open_rcs_on_zedboot_error() {
let (_proxy, daemon, _task) = spawn_test_daemon();
let target = Target::new_with_netsvc_addrs(
Some("abc"),
BTreeSet::from_iter(vec![TargetAddr::from_str("[fe80::1%1]:22").unwrap()].into_iter()),
);
daemon.target_collection.merge_insert(target);
let result = daemon.open_remote_control(None).await;
assert!(result.is_err());
}
struct FakeConfigReader {
query_expected: String,
value: String,
}
#[async_trait(?Send)]
impl ConfigReader for FakeConfigReader {
async fn get(&self, q: &str) -> Result<Option<String>> {
assert_eq!(q, self.query_expected);
Ok(Some(self.value.clone()))
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_target_empty() {
let tempdir = tempfile::tempdir().expect("Creating tempdir");
let socket_path = tempdir.path().join("ascendd.sock");
let d = Daemon::new(socket_path);
let nodename = "where-is-my-hasenpfeffer";
let t = Target::new_autoconnected(nodename);
d.target_collection.merge_insert(t.clone());
assert_eq!(nodename, d.get_target(None).await.unwrap().nodename().unwrap());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_target_query() {
let tempdir = tempfile::tempdir().expect("Creating tempdir");
let socket_path = tempdir.path().join("ascendd.sock");
let d = Daemon::new(socket_path);
let nodename = "where-is-my-hasenpfeffer";
let t = Target::new_autoconnected(nodename);
d.target_collection.merge_insert(t.clone());
assert_eq!(
nodename,
d.get_target(Some(nodename.to_string())).await.unwrap().nodename().unwrap()
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_target_collection_empty_error() {
let tempdir = tempfile::tempdir().expect("Creating tempdir");
let socket_path = tempdir.path().join("ascendd.sock");
let d = Daemon::new(socket_path);
assert_eq!(DaemonError::TargetCacheEmpty, d.get_target(None).await.unwrap_err());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_target_ambiguous() {
let tempdir = tempfile::tempdir().expect("Creating tempdir");
let socket_path = tempdir.path().join("ascendd.sock");
let d = Daemon::new(socket_path);
let t = Target::new_autoconnected("where-is-my-hasenpfeffer");
let t2 = Target::new_autoconnected("it-is-rabbit-season");
d.target_collection.merge_insert(t.clone());
d.target_collection.merge_insert(t2.clone());
assert_eq!(DaemonError::TargetAmbiguous, d.get_target(None).await.unwrap_err());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_target_expiry() {
let tempdir = tempfile::tempdir().expect("Creating tempdir");
let socket_path = tempdir.path().join("ascendd.sock");
let mut daemon = Daemon::new(socket_path);
let target = Target::new_named("goodbye-world");
let then = Instant::now() - Duration::from_secs(10);
target.update_connection_state(|_| TargetConnectionState::Mdns(then));
daemon.target_collection.merge_insert(target.clone());
assert_eq!(TargetConnectionState::Mdns(then), target.get_connection_state());
daemon.start_target_expiry(Duration::from_millis(1));
while target.get_connection_state() == TargetConnectionState::Mdns(then) {
futures_lite::future::yield_now().await
}
assert_eq!(TargetConnectionState::Disconnected, target.get_connection_state());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_ephemeral_target_expiry() {
let tempdir = tempfile::tempdir().expect("Creating tempdir");
let socket_path = tempdir.path().join("ascendd.sock");
let mut daemon = Daemon::new(socket_path);
let expiring_target = Target::new_with_addr_entries(
Some("goodbye-world"),
vec![TargetAddrEntry {
addr: TargetAddr::from_str("127.0.0.1:8088").unwrap(),
timestamp: Utc::now(),
addr_type: TargetAddrType::Manual(Some(SystemTime::now())),
}]
.into_iter(),
);
expiring_target.set_ssh_port(Some(8022));
let persistent_target = Target::new_with_addr_entries(
Some("i-will-stick-around"),
vec![TargetAddrEntry {
addr: TargetAddr::from_str("127.0.0.1:8089").unwrap(),
timestamp: Utc::now(),
addr_type: TargetAddrType::Manual(None),
}]
.into_iter(),
);
persistent_target.set_ssh_port(Some(8023));
let then = Instant::now() - Duration::from_secs(10);
expiring_target.update_connection_state(|_| TargetConnectionState::Mdns(then));
persistent_target.update_connection_state(|_| TargetConnectionState::Mdns(then));
assert!(daemon.target_collection.is_empty());
daemon.target_collection.merge_insert(expiring_target.clone());
daemon.target_collection.merge_insert(persistent_target.clone());
assert_eq!(TargetConnectionState::Mdns(then), expiring_target.get_connection_state());
assert_eq!(TargetConnectionState::Mdns(then), persistent_target.get_connection_state());
daemon.start_target_expiry(Duration::from_millis(1));
while expiring_target.get_connection_state() == TargetConnectionState::Mdns(then) {
futures_lite::future::yield_now().await
}
while persistent_target.get_connection_state() == TargetConnectionState::Mdns(then) {
futures_lite::future::yield_now().await
}
assert_eq!(TargetConnectionState::Disconnected, expiring_target.get_connection_state());
assert_matches!(
persistent_target.get_connection_state(),
TargetConnectionState::Manual(None)
);
assert_eq!(daemon.target_collection.targets().len(), 1);
}
struct NullDaemonEventSynthesizer();
#[async_trait(?Send)]
impl events::EventSynthesizer<DaemonEvent> for NullDaemonEventSynthesizer {
async fn synthesize_events(&self) -> Vec<DaemonEvent> {
return Default::default();
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_handle_overnet_peers_known_peer_exclusion() {
let queue = events::Queue::<DaemonEvent>::new(&Rc::new(NullDaemonEventSynthesizer {}));
let mut known_peers: HashSet<PeerSetElement> = Default::default();
let peer1 = Peer {
description: PeerDescription { services: None, ..Default::default() },
id: NodeId { id: 1 },
is_self: false,
};
let peer2 = Peer {
description: PeerDescription { services: None, ..Default::default() },
id: NodeId { id: 2 },
is_self: false,
};
let new_peers =
Daemon::handle_overnet_peers(&queue, known_peers, vec![peer1.clone(), peer2.clone()]);
assert!(new_peers.contains(&PeerSetElement(peer1.clone())));
assert!(new_peers.contains(&PeerSetElement(peer2.clone())));
known_peers = new_peers;
let new_peers = Daemon::handle_overnet_peers(&queue, known_peers, vec![]);
assert!(!new_peers.contains(&PeerSetElement(peer1.clone())));
assert!(!new_peers.contains(&PeerSetElement(peer2.clone())));
}
struct DaemonEventRecorder {
/// All events observed by the handler will be logged into this field.
event_log: Rc<RefCell<Vec<DaemonEvent>>>,
}
#[async_trait(?Send)]
impl EventHandler<DaemonEvent> for DaemonEventRecorder {
async fn on_event(&self, event: DaemonEvent) -> Result<events::Status> {
self.event_log.borrow_mut().push(event);
Ok(events::Status::Waiting)
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_handle_overnet_peer_leave_and_return() {
let queue = events::Queue::<DaemonEvent>::new(&Rc::new(NullDaemonEventSynthesizer {}));
let mut known_peers: HashSet<PeerSetElement> = Default::default();
let peer1 = Peer {
description: PeerDescription {
services: Some(vec![RemoteControlMarker::PROTOCOL_NAME.to_string()]),
..Default::default()
},
id: NodeId { id: 1 },
is_self: false,
};
let peer2 = Peer {
description: PeerDescription {
services: Some(vec![RemoteControlMarker::PROTOCOL_NAME.to_string()]),
..Default::default()
},
id: NodeId { id: 2 },
is_self: false,
};
// First the targets are discovered:
let new_peers =
Daemon::handle_overnet_peers(&queue, known_peers, vec![peer1.clone(), peer2.clone()]);
assert!(new_peers.contains(&PeerSetElement(peer1.clone())));
assert!(new_peers.contains(&PeerSetElement(peer2.clone())));
known_peers = new_peers;
// Make a new queue so we don't get any of the historical events.
let queue = events::Queue::<DaemonEvent>::new(&Rc::new(NullDaemonEventSynthesizer {}));
let event_log = Rc::new(RefCell::new(Vec::<DaemonEvent>::new()));
// Now wire up the event handler, we want to assert that we observe OvernetPeerLost events for the leaving targets.
queue.add_handler(DaemonEventRecorder { event_log: event_log.clone() }).await;
// Next the targets are lost:
let new_peers = Daemon::handle_overnet_peers(&queue, known_peers, vec![]);
assert!(!new_peers.contains(&PeerSetElement(peer1.clone())));
assert!(!new_peers.contains(&PeerSetElement(peer2.clone())));
let start = Instant::now();
while event_log.borrow().len() != 2 {
if Instant::now().duration_since(start) > Duration::from_secs(1) {
break;
}
futures_lite::future::yield_now().await;
}
assert_eq!(event_log.borrow().len(), 2);
assert_matches!(event_log.borrow()[0], DaemonEvent::OvernetPeerLost(_));
assert_matches!(event_log.borrow()[1], DaemonEvent::OvernetPeerLost(_));
known_peers = new_peers;
assert_eq!(known_peers.len(), 0);
// Make a new queue so we don't get any of the historical events.
let queue = events::Queue::<DaemonEvent>::new(&Rc::new(NullDaemonEventSynthesizer {}));
let event_log = Rc::new(RefCell::new(Vec::<DaemonEvent>::new()));
// Now wire up the event handler, we want to assert that we observe NewTarget events for the returning targets.
queue.add_handler(DaemonEventRecorder { event_log: event_log.clone() }).await;
// Now the targets return:
let new_peers =
Daemon::handle_overnet_peers(&queue, known_peers, vec![peer1.clone(), peer2.clone()]);
assert!(new_peers.contains(&PeerSetElement(peer1.clone())));
assert!(new_peers.contains(&PeerSetElement(peer2.clone())));
let start = Instant::now();
while event_log.borrow().len() != 2 {
if Instant::now().duration_since(start) > Duration::from_secs(1) {
break;
}
futures_lite::future::yield_now().await;
}
// Ensure that we observed a new target event for each target that returned.
assert_eq!(event_log.borrow().len(), 2);
assert_matches!(event_log.borrow()[0], DaemonEvent::OvernetPeer(_));
assert_matches!(event_log.borrow()[1], DaemonEvent::OvernetPeer(_));
}
}