blob: 4864d921e3d9adc17b983740e4c58f3cdfa2364f [file] [log] [blame]
// Copyright 2020 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, Context, Error},
fidl_fuchsia_io::DirectoryMarker,
fidl_fuchsia_pkg::{PackageResolverMarker, PackageResolverProxy, UpdatePolicy},
fidl_fuchsia_update_installer::{
Initiator, InstallerMarker, InstallerProxy, MonitorMarker, MonitorRequest, Options,
RebootControllerMarker, State,
},
fidl_fuchsia_update_usb::{
CheckError, CheckSuccess, CheckerRequest, CheckerRequestStream,
MonitorMarker as UsbMonitorMarker,
},
fuchsia_component::client::connect_to_service,
fuchsia_syslog::{fx_log_err, fx_log_info, fx_log_warn},
fuchsia_url::pkg_url::PkgUrl,
fuchsia_zircon::Status,
futures::prelude::*,
std::{cmp::Ordering, str::FromStr},
update_package::{SystemVersion, UpdatePackage},
};
const BUILD_INFO_VERSION_PATH: &str = "/config/build-info/version";
pub struct UsbUpdateChecker<'a> {
build_info_path: &'a str,
}
impl UsbUpdateChecker<'_> {
pub fn new() -> Self {
UsbUpdateChecker { build_info_path: BUILD_INFO_VERSION_PATH }
}
pub async fn handle_request_stream(
&self,
stream: &mut CheckerRequestStream,
) -> Result<(), Error> {
while let Some(request) = stream.try_next().await.context("Getting request from stream")? {
match request {
CheckerRequest::Check { update_url, logs_dir, monitor, responder } => {
let mut result = self.do_check(&update_url.url, logs_dir, monitor).await;
responder.send(&mut result).context("Sending check result")?;
}
}
}
Ok(())
}
async fn do_check(
&self,
unpinned_update_url: &str,
_logs_dir: Option<fidl::endpoints::ClientEnd<DirectoryMarker>>,
monitor: Option<fidl::endpoints::ClientEnd<UsbMonitorMarker>>,
) -> Result<CheckSuccess, CheckError> {
let unpinned_update_url = PkgUrl::parse(unpinned_update_url)
.map_err(|e| {
fx_log_warn!(
"Failed to parse update_url '{}': {:#}",
unpinned_update_url,
anyhow!(e)
);
CheckError::InvalidUpdateUrl
})
.and_then(|url| self.validate_url(url))?;
let (package, pinned_update_url) =
self.open_update_package(unpinned_update_url).await.map_err(|e| {
fx_log_warn!("Failed to open update package: {:#}", anyhow!(e));
CheckError::UpdateFailed
})?;
let package_version = package.version().await.map_err(|e| {
fx_log_warn!("Failed to read update package version: {:#}", anyhow!(e));
CheckError::UpdateFailed
})?;
if !self.is_update_required(package_version).await? {
return Ok(CheckSuccess::UpdateNotNeeded);
}
if let Some(client_end) = monitor {
self.send_on_update_started(client_end).map_err(|e| {
fx_log_warn!("Failed to send on update start: {:#}", anyhow!(e));
CheckError::UpdateFailed
})?;
}
let proxy = connect_to_service::<InstallerMarker>().map_err(|e| {
fx_log_warn!("Failed to connect to update installer: {:#}", anyhow!(e));
CheckError::UpdateFailed
})?;
self.install_update(pinned_update_url, proxy).await.map_err(|e| {
fx_log_warn!("Failed to install update: {:#}", anyhow!(e));
CheckError::UpdateFailed
})?;
Ok(CheckSuccess::UpdatePerformed)
}
async fn install_update(
&self,
update_url: PkgUrl,
installer: InstallerProxy,
) -> Result<(), Error> {
let mut url = fidl_fuchsia_pkg::PackageUrl { url: update_url.to_string() };
let options = Options {
initiator: Some(Initiator::User),
allow_attach_to_existing_attempt: Some(false),
should_write_recovery: Some(true),
..Options::EMPTY
};
let (controller, controller_remote) =
fidl::endpoints::create_proxy::<RebootControllerMarker>()
.context("Creating reboot controller")?;
let (monitor_client, mut monitor) =
fidl::endpoints::create_request_stream::<MonitorMarker>()
.context("Creating update monitor")?;
installer
.start_update(&mut url, options, monitor_client, Some(controller_remote))
.await
.context("Sending start update request")?
.map_err(|e| anyhow!("Failed starting update: {:?}", e))?;
controller.detach().context("Detaching the reboot controller")?;
while let Some(request) = monitor.try_next().await.context("Getting monitor event")? {
match request {
MonitorRequest::OnState { state, responder } => {
responder.send().context("Sending monitor ack")?;
match state {
State::Complete(_) | State::DeferReboot(_) => {
fx_log_info!("Update complete!");
return Ok(());
}
State::Reboot(_) => {
fx_log_err!(
"The system updater is rebooting, even though we asked it not to!"
);
return Ok(());
}
State::FailPrepare(_) | State::FailFetch(_) | State::FailStage(_) => {
fx_log_warn!("The update installation failed.");
return Err(anyhow!("Failed to install update"));
}
State::Prepare(_)
| State::Fetch(_)
| State::Stage(_)
| State::WaitToReboot(_) => {}
}
}
}
}
// Fail closed, so that we don't reboot mid-update.
fx_log_warn!("System updater closed monitor connection before the update finished.");
return Err(anyhow!("Update monitor stopped receiving events"));
}
fn send_on_update_started(
&self,
client: fidl::endpoints::ClientEnd<UsbMonitorMarker>,
) -> Result<(), Error> {
let proxy = client.into_proxy().context("Creating monitor proxy")?;
proxy.on_update_started().context("Sending update start")?;
Ok(())
}
async fn is_update_required(&self, new_version: SystemVersion) -> Result<bool, CheckError> {
let system_version = self.get_system_version().await.map_err(|e| {
fx_log_warn!("Failed to get system version: {:#}", anyhow!(e));
CheckError::UpdateFailed
})?;
match new_version.partial_cmp(&system_version) {
None => {
fx_log_warn!(
"Could not compare system version {} with update package version {}",
system_version,
new_version
);
Err(CheckError::UpdateFailed)
}
Some(Ordering::Greater) => Ok(true),
Some(_) => Ok(false),
}
}
fn validate_url(&self, url: PkgUrl) -> Result<PkgUrl, CheckError> {
if url.package_hash().is_some() {
return Err(CheckError::InvalidUpdateUrl);
}
Ok(url)
}
async fn get_system_version(&self) -> Result<SystemVersion, Error> {
let string = io_util::file::read_in_namespace_to_string(self.build_info_path)
.await
.context("Reading build info")?;
Ok(SystemVersion::from_str(&string).context("Parsing system version")?)
}
/// Resolve the update package at "url" using the system resolver.
/// Returns a wrapper around the update package and the input 'url' which is now pinned to the
/// resolved UpdatePackage.
async fn open_update_package(&self, url: PkgUrl) -> Result<(UpdatePackage, PkgUrl), Error> {
let resolver = connect_to_service::<PackageResolverMarker>()
.context("Connecting to package resolver")?;
self.open_update_package_at(url, resolver).await
}
/// Resolve the update package at "url" using the given resolver.
/// Returns a wrapper around the update package and a pinned PkgUrl.
async fn open_update_package_at(
&self,
url: PkgUrl,
resolver: PackageResolverProxy,
) -> Result<(UpdatePackage, PkgUrl), Error> {
let mut policy = UpdatePolicy { fetch_if_absent: true, allow_old_versions: false };
let (dir, remote) = fidl::endpoints::create_proxy::<DirectoryMarker>()
.context("Creating directory proxy")?;
resolver
.resolve(&url.to_string(), &mut vec![].into_iter(), &mut policy, remote)
.await
.context("Sending FIDL request")?
.map_err(Status::from_raw)
.context("Resolving pacakge")?;
let meta_proxy = io_util::open_file(
&dir,
std::path::Path::new("meta"),
fidl_fuchsia_io::OPEN_RIGHT_READABLE,
)
.context("Opening meta in update package")?;
let hash =
io_util::file::read_to_string(&meta_proxy).await.context("Reading package hash")?;
let hash = fuchsia_url::Hash::from_str(&hash).context("Parsing hash")?;
let url = PkgUrl::new_package(url.host().to_string(), url.path().to_string(), Some(hash))
.context("Creating pinned package URL")?;
Ok((UpdatePackage::new(dir), url))
}
}