blob: a7744f813b27298c09f25211e836fa004c8f7125 [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.
//! This is a temporary Installer implementation that launches system updater directly, we will
//! delete this and switch to the one in mod installer once the FIDL API is ready.
use crate::install_plan::FuchsiaInstallPlan;
use anyhow::anyhow;
use fidl_fuchsia_sys::LauncherProxy;
use fuchsia_async as fasync;
use fuchsia_component::client::{connect_to_service, launcher, AppBuilder, ExitStatus};
use fuchsia_zircon::{self as zx, Duration};
use futures::future::{select, BoxFuture, Either};
use futures::prelude::*;
use log::info;
use omaha_client::{
installer::{Installer, ProgressObserver},
protocol::request::InstallSource,
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FuchsiaInstallError {
#[error("generic error: {}", _0)]
Failure(anyhow::Error),
#[error("System update installer failed: {}", _0)]
Installer(ExitStatus),
}
impl From<anyhow::Error> for FuchsiaInstallError {
fn from(e: anyhow::Error) -> FuchsiaInstallError {
FuchsiaInstallError::Failure(e)
}
}
#[derive(Debug)]
pub struct FuchsiaInstaller {
launcher: LauncherProxy,
}
impl FuchsiaInstaller {
pub fn new() -> Result<Self, FuchsiaInstallError> {
Ok(FuchsiaInstaller { launcher: launcher()? })
}
#[cfg(test)]
fn new_mock() -> (Self, fidl_fuchsia_sys::LauncherRequestStream) {
let (launcher, stream) =
fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_sys::LauncherMarker>().unwrap();
(FuchsiaInstaller { launcher }, stream)
}
}
impl Installer for FuchsiaInstaller {
type InstallPlan = FuchsiaInstallPlan;
type Error = FuchsiaInstallError;
fn perform_install<'a>(
&'a mut self,
install_plan: &FuchsiaInstallPlan,
observer: Option<&'a dyn ProgressObserver>,
) -> BoxFuture<'a, Result<(), FuchsiaInstallError>> {
let url = install_plan.url.to_string();
let initiator = match install_plan.install_source {
InstallSource::ScheduledTask => "automatic",
InstallSource::OnDemand => "manual",
};
info!("starting system_updater");
let system_updater = async move {
AppBuilder::new("fuchsia-pkg://fuchsia.com/amber#meta/system_updater.cmx")
.arg("--initiator")
.arg(initiator)
.arg("--update")
.arg(url)
.arg("--reboot")
.arg("false")
.status(&self.launcher)?
.await?
.ok()
.map_err(FuchsiaInstallError::Installer)
}
.boxed();
let progress_timer = make_progress_timer(observer.clone()).boxed();
async move {
// Send 0 outside of timer, so that 0 is always sent even if timer is never
// scheduled. Allows unit tests to be deterministic.
if let Some(observer) = observer {
observer.receive_progress(None, 0., None, None).await;
}
let update_res = match select(system_updater, progress_timer).await {
Either::Left((update_res, _)) => update_res,
Either::Right(((), system_updater)) => system_updater.await,
};
if update_res.is_ok() {
if let Some(observer) = observer {
observer.receive_progress(None, 1., None, None).await;
}
}
update_res
}
.boxed()
}
fn perform_reboot(&mut self) -> BoxFuture<'_, Result<(), anyhow::Error>> {
async move {
zx::Status::ok(
connect_to_service::<fidl_fuchsia_device_manager::AdministratorMarker>()?
.suspend(fidl_fuchsia_device_manager::SUSPEND_FLAG_REBOOT)
.await?,
)
.map_err(|e| anyhow!("Suspend error: {}", e))
}
.boxed()
}
}
const EXPECTED_UPDATE_DURATION: Duration = Duration::from_minutes(4);
const TIMER_CHUNKS: u16 = 90;
const TIMER_MAX_PROGRESS: f32 = 0.9;
async fn make_progress_timer(observer: Option<&dyn ProgressObserver>) {
let observer = if let Some(observer) = observer {
observer
} else {
return ();
};
fasync::Interval::new(EXPECTED_UPDATE_DURATION / TIMER_CHUNKS)
.enumerate()
.take(TIMER_CHUNKS as usize)
.for_each(|(i, ())| {
observer.receive_progress(
None,
(i + 1) as f32 / (TIMER_CHUNKS as f32 / TIMER_MAX_PROGRESS),
None,
None,
)
})
.await;
}
#[cfg(test)]
mod tests {
use super::*;
use fidl_fuchsia_sys::{LaunchInfo, LauncherRequest, LauncherRequestStream, TerminationReason};
use matches::assert_matches;
use parking_lot::Mutex;
use std::task::Poll;
const TEST_URL: &str = "fuchsia-pkg://fuchsia.com/update/0";
fn install_plan() -> FuchsiaInstallPlan {
FuchsiaInstallPlan {
url: TEST_URL.parse().unwrap(),
install_source: InstallSource::OnDemand,
}
}
async fn mock_launcher(mut reqs: LauncherRequestStream, exit_code: i64) {
match reqs.next().await.unwrap() {
Ok(LauncherRequest::CreateComponent {
launch_info: LaunchInfo { url, arguments, .. },
controller: Some(controller),
..
}) => {
assert_eq!(url, "fuchsia-pkg://fuchsia.com/amber#meta/system_updater.cmx");
assert_eq!(
arguments,
Some(
vec!["--initiator", "manual", "--update", TEST_URL, "--reboot", "false",]
.iter()
.map(|s| s.to_string())
.collect()
)
);
let (_stream, handle) = controller.into_stream_and_control_handle().unwrap();
handle.send_on_terminated(exit_code, TerminationReason::Exited).unwrap();
}
request => panic!("Unexpected request: {:?}", request),
}
}
#[fasync::run_singlethreaded(test)]
async fn test_perform_install() {
let (mut installer, stream) = FuchsiaInstaller::new_mock();
let installer_fut = async move {
installer.perform_install(&install_plan(), None).await.unwrap();
};
let launcher_fut = mock_launcher(stream, 0);
future::join(installer_fut, launcher_fut).await;
}
#[fasync::run_singlethreaded(test)]
async fn test_install_error() {
let (mut installer, stream) = FuchsiaInstaller::new_mock();
let installer_fut = async move {
match installer.perform_install(&install_plan(), None).await {
Err(FuchsiaInstallError::Installer(_)) => {} // expected
result => panic!("Unexpected result: {:?}", result),
}
};
let launcher_fut = mock_launcher(stream, 1);
future::join(installer_fut, launcher_fut).await;
}
struct MockProgressObserver {
progresses: Mutex<Vec<f32>>,
}
impl MockProgressObserver {
fn new() -> Self {
Self { progresses: Mutex::new(vec![]) }
}
}
impl ProgressObserver for MockProgressObserver {
fn receive_progress(
&self,
operation: Option<&str>,
progress: f32,
total_size: Option<u64>,
size_so_far: Option<u64>,
) -> BoxFuture<'_, ()> {
assert_eq!(operation, None);
assert_eq!(total_size, None);
assert_eq!(size_so_far, None);
self.progresses.lock().push(progress);
future::ready(()).boxed()
}
}
#[test]
fn test_progress_timer_sends_all_updates() {
let mut exec = fasync::Executor::new_with_fake_time().unwrap();
let (mut installer, _stream) = FuchsiaInstaller::new_mock();
let observer = MockProgressObserver::new();
let mut installer_fut = installer.perform_install(&install_plan(), Some(&observer));
// Launcher's `_stream` is never handled, so installation will never complete.
assert_matches!(exec.run_until_stalled(&mut installer_fut), Poll::Pending);
while exec.wake_next_timer().is_some() {
assert_matches!(exec.run_until_stalled(&mut installer_fut), Poll::Pending);
}
assert_eq!(
*observer.progresses.lock(),
(0..=90).map(|i| i as f32 / 100.).collect::<Vec<_>>()
);
}
#[test]
fn test_progress_if_install_finishes_before_progress_timer_starts() {
let mut exec = fasync::Executor::new_with_fake_time().unwrap();
let (mut installer, stream) = FuchsiaInstaller::new_mock();
let observer = MockProgressObserver::new();
let launcher_fut = mock_launcher(stream, 0);
let installer_fut = installer.perform_install(&install_plan(), Some(&observer));
// Executor's fake time is never changed and timers are never manually awoken, so
// progress timer will never send progress updates.
assert_matches!(
exec.run_until_stalled(&mut future::join(installer_fut, launcher_fut).boxed()),
Poll::Ready((Ok(()), ()))
);
assert_eq!(*observer.progresses.lock(), vec![0., 1.]);
}
#[test]
fn test_progress_if_install_finishes_before_progress_timer_finishes() {
let mut exec = fasync::Executor::new_with_fake_time().unwrap();
let (mut installer, stream) = FuchsiaInstaller::new_mock();
let observer = MockProgressObserver::new();
let launcher_fut = mock_launcher(stream, 0);
let mut installer_fut = installer.perform_install(&install_plan(), Some(&observer));
assert_matches!(exec.run_until_stalled(&mut installer_fut), Poll::Pending);
exec.set_fake_time(exec.now() + (EXPECTED_UPDATE_DURATION / 10));
while exec.wake_expired_timers() {
assert_matches!(exec.run_until_stalled(&mut installer_fut), Poll::Pending);
}
assert_matches!(
exec.run_until_stalled(&mut future::join(installer_fut, launcher_fut).boxed()),
Poll::Ready((Ok(()), ()))
);
assert_eq!(
*observer.progresses.lock(),
vec![0., 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 1.]
);
}
}