| // 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.] |
| ); |
| } |
| } |