blob: bea09a7cf7bff2b5c179ef31b6549888be85228f [file] [log] [blame]
// 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 {
analytics::{add_crash_event, add_launch_event, show_analytics_notice},
anyhow::{anyhow, Context, Result},
ffx_core::{build_info, ffx_bail, ffx_error, FfxError},
ffx_daemon::{find_and_connect, is_daemon_running, spawn_daemon},
ffx_lib_args::{from_env, Ffx},
ffx_lib_sub_command::Subcommand,
fidl::endpoints::create_proxy,
fidl_fuchsia_developer_bridge::{
DaemonError, DaemonProxy, FastbootError, FastbootMarker, FastbootProxy,
},
fidl_fuchsia_developer_remotecontrol::{RemoteControlMarker, RemoteControlProxy},
fuchsia_async::TimeoutExt,
futures::Future,
futures::FutureExt,
lazy_static::lazy_static,
ring::digest::{Context as ShaContext, Digest, SHA256},
std::error::Error,
std::fs::File,
std::io::{BufReader, Read},
std::sync::{Arc, Mutex},
std::time::{Duration, Instant},
};
// app name for analytics
const APP_NAME: &str = "ffx";
// Config key for event timeout.
const PROXY_TIMEOUT_SECS: &str = "proxy.timeout_secs";
// TODO: a nice way to focus this error message would be to get a list of targets from the daemon
// and be able to distinguish whether there are in fact 0 or multiple available targets.
const TARGET_FAILURE_MSG: &str = "\
We weren't able to open a connection to a target.
Use `ffx target list` to verify the state of connected devices. This error
probably means that either:
1) There are no available targets. Make sure your device is connected.
2) There are multiple available targets and you haven't specified a target or
provided a default.
Tip: You can use `ffx --target \"my-nodename\" <command>` to specify a target
for a particular command, or use `ffx target default set \"my-nodename\"` if
you always want to use a particular target.";
const CURRENT_EXE_HASH: &str = "current.hash";
const NON_FASTBOOT_MSG: &str = "\
This command needs to be run against a target in the Fastboot state.
Try rebooting the device into Fastboot with the command `ffx target
reboot --bootloader` and try re-running this command.";
const TARGET_IN_FASTBOOT: &str = "\
This command cannot be run against a target in the Fastboot state. Try
rebooting the device or flashing the device into a running state.";
lazy_static! {
// Using a mutex to guard the spawning of the daemon - the value it contains is not used.
static ref SPAWN_GUARD: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
}
// This could get called multiple times by the plugin system via multiple threads - so make sure
// the spawning only happens one thread at a time.
async fn get_daemon_proxy() -> Result<DaemonProxy> {
let mut check_hash = false;
let _guard = SPAWN_GUARD.lock().unwrap();
if !is_daemon_running().await {
spawn_daemon().await?;
} else {
check_hash = true;
}
let proxy = find_and_connect(hoist::hoist()).await?;
if check_hash {
// TODO(fxb/67400) Create an e2e test.
let hash: String =
ffx_config::get((CURRENT_EXE_HASH, ffx_config::ConfigLevel::Runtime)).await?;
let daemon_hash = proxy.get_hash().await?;
if hash != daemon_hash {
log::info!("Daemon is a different version. Attempting to restart");
if proxy.quit().await? {
spawn_daemon().await?;
return find_and_connect(hoist::hoist()).await;
} else {
ffx_bail!(
"FFX daemon is a different version. \n\
Try running `ffx doctor --force-daemon-restart` and then retrying your \
command"
)
}
}
}
Ok(proxy)
}
async fn proxy_timeout() -> Result<Duration> {
let proxy_timeout: ffx_config::Value = ffx_config::get(PROXY_TIMEOUT_SECS).await?;
Ok(Duration::from_millis(
(proxy_timeout
.as_f64()
.ok_or(anyhow!("unable to convert to float: {:?}", proxy_timeout))?
* 1000.0) as u64,
))
}
#[derive(Debug)]
struct TimeoutError {}
impl std::fmt::Display for TimeoutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "timed out")
}
}
impl Error for TimeoutError {}
async fn timeout<F, T>(t: Duration, f: F) -> Result<T, TimeoutError>
where
F: Future<Output = T> + Unpin,
{
// TODO(raggi): this could be made more efficient (avoiding the box) with some additional work,
// but for the local use cases here it's not sufficiently important.
let mut timer = fuchsia_async::Timer::new(t).boxed().fuse();
let mut f = f.fuse();
futures::select! {
_ = timer => Err(TimeoutError{}),
res = f => Ok(res),
}
}
async fn get_fastboot_proxy() -> Result<FastbootProxy> {
let daemon_proxy = get_daemon_proxy().await?;
let (fastboot_proxy, fastboot_server_end) = create_proxy::<FastbootMarker>()?;
let app: Ffx = argh::from_env();
let result = timeout(
proxy_timeout().await?,
daemon_proxy
.get_fastboot(app.target().await?.as_ref().map(|s| s.as_str()), fastboot_server_end),
)
.await
.context("timeout")?
.context("connecting to Fastboot")?;
match result {
Ok(_) => Ok(fastboot_proxy),
Err(FastbootError::NonFastbootDevice) => Err(ffx_error!(NON_FASTBOOT_MSG).into()),
Err(e) => Err(anyhow!("unexpected failure connecting to Fastboot: {:?}", e)),
}
}
async fn get_remote_proxy() -> Result<RemoteControlProxy> {
let daemon_proxy = get_daemon_proxy().await?;
let (remote_proxy, remote_server_end) = create_proxy::<RemoteControlMarker>()?;
let app: Ffx = argh::from_env();
let result = timeout(
proxy_timeout().await?,
daemon_proxy.get_remote_control(
app.target().await?.as_ref().map(|s| s.as_str()),
remote_server_end,
),
)
.await
.context("timeout")?
.context("connecting to daemon")?;
match result {
Ok(_) => Ok(remote_proxy),
Err(DaemonError::TargetCacheError) => Err(ffx_error!(TARGET_FAILURE_MSG).into()),
Err(DaemonError::TargetInFastboot) => Err(ffx_error!(TARGET_IN_FASTBOOT).into()),
Err(e) => Err(anyhow!("unexpected failure connecting to RCS: {:?}", e)),
}
}
async fn is_experiment_subcommand_on(key: &'static str) -> bool {
ffx_config::get(key).await.unwrap_or(false)
}
fn is_daemon(subcommand: &Option<Subcommand>) -> bool {
if let Some(Subcommand::FfxDaemonPlugin(ffx_daemon_plugin_args::DaemonCommand {
subcommand: ffx_daemon_plugin_sub_command::Subcommand::FfxDaemonStart(_),
})) = subcommand
{
return true;
}
false
}
fn set_hash_config(overrides: Option<String>) -> Result<Option<String>> {
let input = std::env::current_exe()?;
let reader = BufReader::new(File::open(input)?);
let digest = sha256_digest(reader)?;
let runtime = format!("{}={}", CURRENT_EXE_HASH, hex::encode(digest.as_ref()));
match overrides {
Some(s) => {
if s.is_empty() {
Ok(Some(runtime))
} else {
let new_overrides = format!("{},{}", s, runtime);
Ok(Some(new_overrides))
}
}
None => Ok(Some(runtime)),
}
}
fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest> {
let mut context = ShaContext::new(&SHA256);
let mut buffer = [0; 1024];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
context.update(&buffer[..count]);
}
Ok(context.finish())
}
async fn run() -> Result<()> {
let app: Ffx = from_env();
// Configuration initialization must happen before ANY calls to the config (or the cache won't
// properly have the runtime parameters.
let overrides = set_hash_config(app.runtime_config_overrides())?;
ffx_config::init_config(&app.config, &overrides, &app.env)?;
let log_to_stdio = app.verbose || is_daemon(&app.subcommand);
ffx_config::logging::init(log_to_stdio).await?;
log::info!("starting command: {:?}", std::env::args().collect::<Vec<String>>());
// HACK(64402): hoist uses a lazy static initializer obfuscating access to inject
// this value by other means, so:
let _ = ffx_config::get("overnet.socket").await.map(|sockpath: String| {
std::env::set_var("ASCENDD", sockpath);
});
let notice_writer = Box::new(std::io::stderr());
show_analytics_notice(notice_writer);
let analytics_start = Instant::now();
let analytics_task = fuchsia_async::Task::spawn(async {
let args: Vec<String> = std::env::args().collect();
// drop arg[0]: executable with hard path
// TODO do we want to break out subcommands for analytics?
let args_str = &args[1..].join(" ");
let launch_args = format!("{}", &args_str);
let build_info = build_info();
let build_version = build_info.build_version;
if let Err(e) =
add_launch_event(APP_NAME, build_version.as_deref(), Some(launch_args).as_deref()).await
{
log::error!("analytics submission failed: {}", e);
}
Instant::now()
});
let command_start = Instant::now();
let res = ffx_lib_suite::ffx_plugin_impl(
get_daemon_proxy,
get_remote_proxy,
get_fastboot_proxy,
is_experiment_subcommand_on,
app,
)
.await;
let command_done = Instant::now();
log::info!("Command completed. Success: {}", res.is_ok());
let analytics_done = analytics_task
// TODO(66918): make configurable, and evaluate chosen time value.
.on_timeout(Duration::from_secs(2), || {
log::error!("analytics submission timed out");
// Analytics timeouts should not impact user flows
Instant::now()
})
.await;
log::info!(
"Run finished. success: {}, command time: {}, analytics time: {}",
res.is_ok(),
(command_done - command_start).as_secs_f32(),
(analytics_done - analytics_start).as_secs_f32()
);
res
}
#[fuchsia_async::run_singlethreaded]
async fn main() {
match run().await {
Ok(_) => {
// TODO add event for timing here at end
std::process::exit(0)
}
Err(err) => {
let error_code = if let Some(ffx_err) = err.downcast_ref::<FfxError>() {
eprintln!("{}", ffx_err);
match ffx_err {
FfxError::Error(_, code) => *code,
}
} else {
eprintln!("BUG: An internal command error occurred.\n{:?}", err);
1
};
let err_msg = format!("{}", err);
// TODO(66918): make configurable, and evaluate chosen time value.
if let Err(e) = add_crash_event(&err_msg)
.on_timeout(Duration::from_secs(2), || {
log::error!("analytics timed out reporting crash event");
Ok(())
})
.await
{
log::error!("analytics failed to submit crash event: {}", e);
}
std::process::exit(error_code);
}
}
}