Skip to main content

omaha_client_fuchsia/
installer.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! This is the Fuchsia Installer implementation that talks to fuchsia.update.installer FIDL API.
6
7use crate::app_set::FuchsiaAppSet;
8use crate::install_plan::{FuchsiaInstallPlan, UpdatePackageUrl};
9use anyhow::{Context as _, anyhow};
10use fidl_connector::{Connect, ServiceReconnector};
11use fidl_fuchsia_pkg::{self as fpkg, CupData, CupMarker, CupProxy, WriteError};
12use fidl_fuchsia_update_installer::{
13    InstallerMarker, InstallerProxy, RebootControllerMarker, RebootControllerProxy,
14};
15use fidl_fuchsia_update_installer_ext::{
16    FetchFailureReason, Initiator, MonitorUpdateAttemptError, Options, PrepareFailureReason, State,
17    StateId, UpdateAttemptError, start_update,
18};
19use fuchsia_async as fasync;
20use fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl;
21use futures::future::LocalBoxFuture;
22use futures::lock::Mutex as AsyncMutex;
23use futures::prelude::*;
24use log::{info, warn};
25use omaha_client::app_set::AppSet as _;
26use omaha_client::cup_ecdsa::RequestMetadata;
27use omaha_client::installer::{AppInstallResult, Installer, ProgressObserver};
28use omaha_client::protocol::request::InstallSource;
29use omaha_client::protocol::response::{OmahaStatus, Response, UpdateCheck};
30use omaha_client::request_builder::RequestParams;
31use std::rc::Rc;
32use std::time::Duration;
33use thiserror::Error;
34
35/// Represents possible reasons the installer could have ended in a failure state. Not exhaustive.
36#[derive(Copy, Clone, Debug, PartialEq, Eq)]
37pub enum InstallerFailureReason {
38    Internal,
39    OutOfSpace,
40    UnsupportedDowngrade,
41}
42
43impl From<FetchFailureReason> for InstallerFailureReason {
44    fn from(r: FetchFailureReason) -> InstallerFailureReason {
45        match r {
46            FetchFailureReason::Internal => InstallerFailureReason::Internal,
47            FetchFailureReason::OutOfSpace => InstallerFailureReason::OutOfSpace,
48        }
49    }
50}
51
52impl From<PrepareFailureReason> for InstallerFailureReason {
53    fn from(r: PrepareFailureReason) -> InstallerFailureReason {
54        match r {
55            PrepareFailureReason::Internal => InstallerFailureReason::Internal,
56            PrepareFailureReason::OutOfSpace => InstallerFailureReason::OutOfSpace,
57            PrepareFailureReason::UnsupportedDowngrade => {
58                InstallerFailureReason::UnsupportedDowngrade
59            }
60        }
61    }
62}
63
64/// Information from the config about whether an update is urgent.
65#[derive(Debug)]
66pub struct InstallResult {
67    pub urgent_update: bool,
68}
69
70/// Information about a specific failure state that the installer ended in.
71#[derive(Debug, Copy, Clone)]
72pub struct InstallerFailure {
73    state_name: &'static str,
74    reason: InstallerFailureReason,
75}
76
77impl InstallerFailure {
78    /// Returns the name of the system-updater state this failure occurred in
79    pub fn state_name(self) -> &'static str {
80        self.state_name
81    }
82
83    /// Returns the reason this failure occurred
84    pub fn reason(self) -> InstallerFailureReason {
85        self.reason
86    }
87
88    pub fn new(state_name: &'static str, reason: InstallerFailureReason) -> Self {
89        Self { state_name, reason }
90    }
91}
92
93#[derive(Debug, Error)]
94pub enum FuchsiaInstallError {
95    #[error("generic error")]
96    Failure(#[from] anyhow::Error),
97
98    #[error("FIDL error")]
99    Fidl(#[from] fidl::Error),
100
101    /// System update installer error.
102    #[error("start update installer failed")]
103    StartUpdate(#[from] UpdateAttemptError),
104
105    #[error("monitor update installer failed")]
106    MonitorUpdate(#[from] MonitorUpdateAttemptError),
107
108    #[error("installer encountered failure state: {0:?}")]
109    InstallerFailureState(InstallerFailure),
110
111    #[error("installation ended unexpectedly")]
112    InstallationEndedUnexpectedly,
113
114    #[error("connect to installer service failed")]
115    Connect(#[source] anyhow::Error),
116
117    #[error("eager package cup write failed: {0:?}")]
118    CupWrite(WriteError),
119
120    #[error("CupWrite failed, missing request metadata")]
121    MissingRequestMetadata,
122}
123
124#[derive(Debug)]
125pub struct FuchsiaInstaller<
126    I = ServiceReconnector<InstallerMarker>,
127    C = ServiceReconnector<CupMarker>,
128> {
129    installer_connector: I,
130    cup_connector: C,
131    reboot_controller: Option<RebootControllerProxy>,
132    app_set: Rc<AsyncMutex<FuchsiaAppSet>>,
133    allow_reboot: bool,
134}
135
136impl FuchsiaInstaller<ServiceReconnector<InstallerMarker>, ServiceReconnector<CupMarker>> {
137    pub fn new(app_set: Rc<AsyncMutex<FuchsiaAppSet>>) -> Self {
138        Self::new_set_allow_reboot(app_set, true)
139    }
140
141    pub fn new_set_allow_reboot(
142        app_set: Rc<AsyncMutex<FuchsiaAppSet>>,
143        allow_reboot: bool,
144    ) -> Self {
145        let installer_connector = ServiceReconnector::<InstallerMarker>::new();
146        let cup_connector = ServiceReconnector::<CupMarker>::new();
147        Self { installer_connector, cup_connector, reboot_controller: None, app_set, allow_reboot }
148    }
149}
150
151impl<I, C> FuchsiaInstaller<I, C>
152where
153    I: Connect<Proxy = InstallerProxy> + Send,
154    C: Connect<Proxy = CupProxy> + Send,
155{
156    async fn perform_install_system_update<'a>(
157        &'a mut self,
158        url: &'a http::Uri,
159        install_source: &'a InstallSource,
160        observer: Option<&'a dyn ProgressObserver>,
161    ) -> Result<(), FuchsiaInstallError> {
162        let options = Options {
163            initiator: match install_source {
164                InstallSource::ScheduledTask => Initiator::Service,
165                InstallSource::OnDemand => Initiator::User,
166            },
167            should_write_recovery: true,
168            allow_attach_to_existing_attempt: true,
169        };
170
171        let proxy = self.installer_connector.connect().map_err(FuchsiaInstallError::Connect)?;
172        let (reboot_controller, reboot_controller_server_end) =
173            fidl::endpoints::create_proxy::<RebootControllerMarker>();
174
175        if self.allow_reboot {
176            self.reboot_controller = Some(reboot_controller);
177        } else {
178            let () = reboot_controller.detach().context("disabling automatic reboot")?;
179        }
180
181        let mut update_attempt =
182            start_update(url, options, &proxy, Some(reboot_controller_server_end)).await?;
183
184        while let Some(state) = update_attempt.try_next().await? {
185            info!("Installer entered state: {}", state.name());
186            if let Some(observer) = observer {
187                if let Some(progress) = state.progress() {
188                    observer
189                        .receive_progress(
190                            Some(state.name()),
191                            progress.fraction_completed(),
192                            state.download_size(),
193                            Some(progress.bytes_downloaded()),
194                        )
195                        .await;
196                } else {
197                    observer.receive_progress(Some(state.name()), 0., None, None).await;
198                }
199            }
200            if state.id() == StateId::WaitToReboot || state.is_success() {
201                return Ok(());
202            } else if state.is_failure() {
203                match state {
204                    State::FailFetch(fail_fetch_data) => {
205                        return Err(FuchsiaInstallError::InstallerFailureState(InstallerFailure {
206                            state_name: state.name(),
207                            reason: fail_fetch_data.reason().into(),
208                        }));
209                    }
210                    State::FailPrepare(prepare_failure_reason) => {
211                        return Err(FuchsiaInstallError::InstallerFailureState(InstallerFailure {
212                            state_name: state.name(),
213                            reason: prepare_failure_reason.into(),
214                        }));
215                    }
216                    _ => {
217                        return Err(FuchsiaInstallError::InstallerFailureState(InstallerFailure {
218                            state_name: state.name(),
219                            reason: InstallerFailureReason::Internal,
220                        }));
221                    }
222                }
223            }
224        }
225
226        Err(FuchsiaInstallError::InstallationEndedUnexpectedly)
227    }
228
229    async fn perform_install_eager_package(
230        &mut self,
231        url: &PinnedAbsolutePackageUrl,
232        install_plan: &FuchsiaInstallPlan,
233    ) -> Result<(), FuchsiaInstallError> {
234        let proxy = self.cup_connector.connect().map_err(FuchsiaInstallError::Connect)?;
235        let url = fpkg::PackageUrl { url: url.to_string() };
236        let rm = install_plan
237            .request_metadata
238            .as_ref()
239            .ok_or(FuchsiaInstallError::MissingRequestMetadata)?;
240        let cup_data: CupData = CupData {
241            request: Some(rm.request_body.clone()),
242            key_id: Some(rm.public_key_id),
243            nonce: Some(rm.nonce.into()),
244            response: Some(install_plan.omaha_response.clone()),
245            signature: install_plan.ecdsa_signature.as_ref().cloned(),
246            ..Default::default()
247        };
248        proxy.write(&url, &cup_data).await?.map_err(FuchsiaInstallError::CupWrite)
249    }
250}
251
252impl<I, C> Installer for FuchsiaInstaller<I, C>
253where
254    I: Connect<Proxy = InstallerProxy> + Send,
255    C: Connect<Proxy = CupProxy> + Send,
256{
257    type InstallPlan = FuchsiaInstallPlan;
258    type Error = FuchsiaInstallError;
259    type InstallResult = InstallResult;
260
261    fn perform_install<'a>(
262        &'a mut self,
263        install_plan: &'a FuchsiaInstallPlan,
264        observer: Option<&'a dyn ProgressObserver>,
265    ) -> LocalBoxFuture<'a, (Self::InstallResult, Vec<AppInstallResult<Self::Error>>)> {
266        let is_system_update = install_plan.is_system_update();
267
268        async move {
269            let mut app_results = vec![];
270            for (i, url) in install_plan.update_package_urls.iter().enumerate() {
271                app_results.push(match url {
272                    UpdatePackageUrl::System(url) => self
273                        .perform_install_system_update(url, &install_plan.install_source, observer)
274                        .await
275                        .into(),
276                    UpdatePackageUrl::Package(url) => {
277                        if is_system_update {
278                            AppInstallResult::Deferred
279                        } else {
280                            let result =
281                                self.perform_install_eager_package(url, install_plan).await.into();
282                            if let Some(observer) = observer {
283                                observer
284                                    .receive_progress(
285                                        Some(&url.to_string()),
286                                        (i + 1) as f32
287                                            / install_plan.update_package_urls.len() as f32,
288                                        None,
289                                        None,
290                                    )
291                                    .await;
292                            }
293                            result
294                        }
295                    }
296                });
297            }
298            (InstallResult { urgent_update: install_plan.urgent_update }, app_results)
299        }
300        .boxed_local()
301    }
302
303    fn perform_reboot(&mut self) -> LocalBoxFuture<'_, Result<(), anyhow::Error>> {
304        async move {
305            match self.reboot_controller.take() {
306                Some(reboot_controller) => {
307                    reboot_controller
308                        .unblock()
309                        .context("notify installer it can reboot when ready")?;
310                }
311                None => {
312                    if self.allow_reboot {
313                        warn!("We should reboot due to the policy but the reboot controller has been dropped!");
314                    }
315                }
316            }
317
318            // Device should be rebooting now, do not return because state machine expects
319            // perform_reboot() to block, wait for 5 minutes and if reboot still hasn't happened,
320            // return an error.
321            fasync::Timer::new(Duration::from_secs(60 * 5)).await;
322
323            Err(anyhow!("timed out while waiting for device to reboot"))
324        }
325        .boxed_local()
326    }
327
328    fn try_create_install_plan<'a>(
329        &'a self,
330        request_params: &'a RequestParams,
331        request_metadata: Option<&'a RequestMetadata>,
332        response: &'a Response,
333        response_bytes: Vec<u8>,
334        ecdsa_signature: Option<Vec<u8>>,
335    ) -> LocalBoxFuture<'a, Result<Self::InstallPlan, Self::Error>> {
336        async move {
337            let system_app_id = self.app_set.lock().await.get_system_app_id().to_owned();
338            try_create_install_plan_impl(
339                request_params,
340                request_metadata,
341                response,
342                response_bytes,
343                ecdsa_signature,
344                system_app_id,
345            )
346        }
347        .boxed_local()
348    }
349}
350
351fn try_create_install_plan_impl(
352    request_params: &RequestParams,
353    request_metadata: Option<&RequestMetadata>,
354    response: &Response,
355    response_bytes: Vec<u8>,
356    ecdsa_signature: Option<Vec<u8>>,
357    system_app_id: String,
358) -> Result<FuchsiaInstallPlan, FuchsiaInstallError> {
359    let mut update_package_urls = vec![];
360    let mut urgent_update = false;
361
362    if response.apps.is_empty() {
363        return Err(FuchsiaInstallError::Failure(anyhow!("No app in Omaha response")));
364    }
365
366    for app in &response.apps {
367        if app.status != OmahaStatus::Ok {
368            return Err(FuchsiaInstallError::Failure(anyhow!(
369                "Found non-ok app status for {:?}: {:?}",
370                app.id,
371                app.status
372            )));
373        }
374        let update_check = if let Some(update_check) = &app.update_check {
375            update_check
376        } else {
377            return Err(FuchsiaInstallError::Failure(anyhow!("No update_check in Omaha response")));
378        };
379
380        let mut urls = match update_check.status {
381            OmahaStatus::Ok => update_check.get_all_url_codebases(),
382            OmahaStatus::NoUpdate => {
383                continue;
384            }
385            _ => {
386                if let Some(info) = &update_check.info {
387                    warn!("update check status info: {}", info);
388                }
389                return Err(FuchsiaInstallError::Failure(anyhow!(
390                    "Unexpected update check status: {:?}",
391                    update_check.status
392                )));
393            }
394        };
395        let url = urls
396            .next()
397            .ok_or_else(|| FuchsiaInstallError::Failure(anyhow!("No url in Omaha response")))?;
398
399        let rest_count = urls.count();
400        if rest_count != 0 {
401            warn!("Only 1 url is supported, found {}", rest_count + 1);
402        }
403
404        let mut packages = update_check.get_all_packages();
405        let package = packages
406            .next()
407            .ok_or_else(|| FuchsiaInstallError::Failure(anyhow!("No package in Omaha response")))?;
408
409        let rest_count = packages.count();
410        if rest_count != 0 {
411            warn!("Only 1 package is supported, found {}", rest_count + 1);
412        }
413
414        let full_url = url.to_owned() + &package.name;
415
416        update_package_urls.push(if app.id == system_app_id {
417            // If urgent_update is present, assume true.
418            urgent_update = is_update_urgent(update_check);
419            let pkg_url = full_url
420                .parse()
421                .with_context(|| format!("Failed to parse {full_url} to Url"))
422                .map_err(FuchsiaInstallError::Failure)?;
423            UpdatePackageUrl::System(pkg_url)
424        } else {
425            let pkg_url = full_url
426                .parse()
427                .with_context(|| format!("Failed to parse {full_url} to PinnedAbsolutePackageUrl"))
428                .map_err(FuchsiaInstallError::Failure)?;
429            UpdatePackageUrl::Package(pkg_url)
430        });
431    }
432    if update_package_urls.is_empty() {
433        return Err(FuchsiaInstallError::Failure(anyhow!("No app has update available")));
434    }
435    Ok(FuchsiaInstallPlan {
436        update_package_urls,
437        install_source: request_params.source,
438        urgent_update,
439        omaha_response: response_bytes,
440        request_metadata: request_metadata.cloned(),
441        ecdsa_signature,
442    })
443}
444
445pub fn is_update_urgent(update_check: &UpdateCheck) -> bool {
446    update_check.extra_attributes.contains_key("_urgent_update")
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452    use crate::app_set::{AppIdSource, AppMetadata};
453    use assert_matches::assert_matches;
454    use fidl_fuchsia_pkg::{CupRequest, CupRequestStream};
455    use fidl_fuchsia_update_installer::{
456        FailPrepareData, InstallationProgress, InstallerRequest, InstallerRequestStream,
457        RebootControllerRequest, State, UpdateInfo,
458    };
459    use fuchsia_async::{self as fasync, TestExecutor};
460    use fuchsia_sync::Mutex;
461    use futures::future::BoxFuture;
462    use omaha_client::protocol::response::{App, Manifest, Package, Packages};
463    use serde_json::Map;
464    use std::sync::Arc;
465    use std::task::Poll;
466
467    const TEST_URL: &str = "fuchsia-pkg://fuchsia.test/update/0?hash=0000000000000000000000000000000000000000000000000000000000000000";
468    const TEST_URL_BASE: &str = "fuchsia-pkg://fuchsia.test/";
469    const TEST_PACKAGE_NAME: &str =
470        "update/0?hash=0000000000000000000000000000000000000000000000000000000000000000";
471
472    #[derive(Debug, PartialEq)]
473    struct Progress {
474        operation: Option<String>,
475        progress: f32,
476        total_size: Option<u64>,
477        size_so_far: Option<u64>,
478    }
479
480    impl Eq for Progress {}
481    struct MockProgressObserver {
482        progresses: Arc<Mutex<Vec<Progress>>>,
483    }
484
485    impl MockProgressObserver {
486        fn new() -> Self {
487            Self { progresses: Arc::new(Mutex::new(vec![])) }
488        }
489        fn progresses(&self) -> Arc<Mutex<Vec<Progress>>> {
490            Arc::clone(&self.progresses)
491        }
492    }
493    impl ProgressObserver for MockProgressObserver {
494        fn receive_progress(
495            &self,
496            operation: Option<&str>,
497            progress: f32,
498            total_size: Option<u64>,
499            size_so_far: Option<u64>,
500        ) -> BoxFuture<'_, ()> {
501            let operation = operation.map(|s| s.into());
502            self.progresses.lock().push(Progress { operation, progress, total_size, size_so_far });
503            future::ready(()).boxed()
504        }
505    }
506
507    struct MockConnector<T> {
508        proxy: Option<T>,
509    }
510
511    impl<T> MockConnector<T> {
512        fn new(proxy: T) -> Self {
513            Self { proxy: Some(proxy) }
514        }
515        fn failing() -> Self {
516            Self { proxy: None }
517        }
518    }
519
520    impl<T: fidl::endpoints::Proxy + Clone> Connect for MockConnector<T> {
521        type Proxy = T;
522
523        fn connect(&self) -> Result<Self::Proxy, anyhow::Error> {
524            self.proxy.clone().ok_or_else(|| anyhow::anyhow!("no proxy available"))
525        }
526    }
527
528    fn new_mock_installer(
529        allow_reboot: bool,
530    ) -> (
531        FuchsiaInstaller<MockConnector<InstallerProxy>, MockConnector<CupProxy>>,
532        InstallerRequestStream,
533        CupRequestStream,
534    ) {
535        let (installer_proxy, installer_stream) =
536            fidl::endpoints::create_proxy_and_stream::<InstallerMarker>();
537        let (cup_proxy, cup_stream) = fidl::endpoints::create_proxy_and_stream::<CupMarker>();
538        let app = omaha_client::common::App::builder().id("system_id").version([1]).build();
539        let app_metadata = AppMetadata { appid_source: AppIdSource::VbMetadata };
540        let app_set = Rc::new(AsyncMutex::new(FuchsiaAppSet::new(app, app_metadata)));
541        let installer = FuchsiaInstaller {
542            installer_connector: MockConnector::new(installer_proxy),
543            cup_connector: MockConnector::new(cup_proxy),
544            reboot_controller: None,
545            app_set,
546            allow_reboot,
547        };
548        (installer, installer_stream, cup_stream)
549    }
550
551    fn new_installer() -> FuchsiaInstaller<ServiceReconnector<InstallerMarker>> {
552        let app = omaha_client::common::App::builder().id("system_id").version([1]).build();
553        let app_metadata = AppMetadata { appid_source: AppIdSource::VbMetadata };
554        let app_set = Rc::new(AsyncMutex::new(FuchsiaAppSet::new(app, app_metadata)));
555        FuchsiaInstaller::new(app_set)
556    }
557
558    #[fasync::run_singlethreaded(test)]
559    async fn test_start_update_with_reboot() {
560        let (mut installer, mut stream, _) = new_mock_installer(true);
561        let plan = FuchsiaInstallPlan {
562            update_package_urls: vec![
563                UpdatePackageUrl::System(TEST_URL.parse().unwrap()),
564                UpdatePackageUrl::Package(TEST_URL.parse().unwrap()),
565            ],
566            install_source: InstallSource::OnDemand,
567            ..FuchsiaInstallPlan::default()
568        };
569        let observer = MockProgressObserver::new();
570        let progresses = observer.progresses();
571        let installer_fut = async move {
572            let (install_result, app_install_results) =
573                installer.perform_install(&plan, Some(&observer)).await;
574            assert!(!install_result.urgent_update);
575            assert_matches!(
576                app_install_results.as_slice(),
577                &[AppInstallResult::Installed, AppInstallResult::Deferred]
578            );
579            assert_matches!(installer.reboot_controller, Some(_));
580        };
581        let stream_fut = async move {
582            match stream.next().await.unwrap() {
583                Ok(InstallerRequest::StartUpdate {
584                    url,
585                    options,
586                    monitor,
587                    reboot_controller,
588                    responder,
589                }) => {
590                    assert_eq!(url.url, TEST_URL);
591                    let Options {
592                        initiator,
593                        should_write_recovery,
594                        allow_attach_to_existing_attempt,
595                    } = options.try_into().unwrap();
596                    assert_eq!(initiator, Initiator::User);
597                    assert_matches!(reboot_controller, Some(_));
598                    assert!(should_write_recovery);
599                    assert!(allow_attach_to_existing_attempt);
600                    responder.send(Ok("00000000-0000-0000-0000-000000000001")).unwrap();
601                    let monitor = monitor.into_proxy();
602                    let () = monitor
603                        .on_state(&State::Stage(fidl_fuchsia_update_installer::StageData {
604                            info: Some(UpdateInfo {
605                                download_size: Some(1000),
606                                ..Default::default()
607                            }),
608                            progress: Some(InstallationProgress {
609                                fraction_completed: Some(0.5),
610                                bytes_downloaded: Some(500),
611                                ..Default::default()
612                            }),
613                            ..Default::default()
614                        }))
615                        .await
616                        .unwrap();
617                    let () = monitor
618                        .on_state(&State::WaitToReboot(
619                            fidl_fuchsia_update_installer::WaitToRebootData {
620                                info: Some(UpdateInfo {
621                                    download_size: Some(1000),
622                                    ..Default::default()
623                                }),
624                                progress: Some(InstallationProgress {
625                                    fraction_completed: Some(1.0),
626                                    bytes_downloaded: Some(1000),
627                                    ..Default::default()
628                                }),
629                                ..Default::default()
630                            },
631                        ))
632                        .await
633                        .unwrap();
634                }
635                request => panic!("Unexpected request: {request:?}"),
636            }
637        };
638        future::join(installer_fut, stream_fut).await;
639        assert_eq!(
640            *progresses.lock(),
641            vec![
642                Progress {
643                    operation: Some("stage".to_string()),
644                    progress: 0.5,
645                    total_size: Some(1000),
646                    size_so_far: Some(500)
647                },
648                Progress {
649                    operation: Some("wait_to_reboot".to_string()),
650                    progress: 1.0,
651                    total_size: Some(1000),
652                    size_so_far: Some(1000)
653                }
654            ]
655        );
656    }
657
658    #[fasync::run_singlethreaded(test)]
659    async fn test_start_update_no_reboot() {
660        let (mut installer, mut stream, _) = new_mock_installer(false);
661        let plan = FuchsiaInstallPlan {
662            update_package_urls: vec![
663                UpdatePackageUrl::System(TEST_URL.parse().unwrap()),
664                UpdatePackageUrl::Package(TEST_URL.parse().unwrap()),
665            ],
666            install_source: InstallSource::OnDemand,
667            ..FuchsiaInstallPlan::default()
668        };
669        let observer = MockProgressObserver::new();
670        let progresses = observer.progresses();
671        let installer_fut = async move {
672            let (install_result, app_install_results) =
673                installer.perform_install(&plan, Some(&observer)).await;
674            assert!(!install_result.urgent_update);
675            assert_matches!(
676                app_install_results.as_slice(),
677                &[AppInstallResult::Installed, AppInstallResult::Deferred]
678            );
679            assert_matches!(installer.reboot_controller, None);
680        };
681        let stream_fut = async move {
682            match stream.next().await.unwrap() {
683                Ok(InstallerRequest::StartUpdate {
684                    url,
685                    options,
686                    monitor,
687                    reboot_controller,
688                    responder,
689                }) => {
690                    assert_eq!(url.url, TEST_URL);
691                    let Options {
692                        initiator,
693                        should_write_recovery,
694                        allow_attach_to_existing_attempt,
695                    } = options.try_into().unwrap();
696                    assert_eq!(initiator, Initiator::User);
697                    assert!(should_write_recovery);
698                    assert!(allow_attach_to_existing_attempt);
699                    responder.send(Ok("00000000-0000-0000-0000-000000000001")).unwrap();
700                    let monitor = monitor.into_proxy();
701                    let () = monitor
702                        .on_state(&State::Stage(fidl_fuchsia_update_installer::StageData {
703                            info: Some(UpdateInfo {
704                                download_size: Some(1000),
705                                ..Default::default()
706                            }),
707                            progress: Some(InstallationProgress {
708                                fraction_completed: Some(0.5),
709                                bytes_downloaded: Some(500),
710                                ..Default::default()
711                            }),
712                            ..Default::default()
713                        }))
714                        .await
715                        .unwrap();
716                    let () = monitor
717                        .on_state(&State::WaitToReboot(
718                            fidl_fuchsia_update_installer::WaitToRebootData {
719                                info: Some(UpdateInfo {
720                                    download_size: Some(1000),
721                                    ..Default::default()
722                                }),
723                                progress: Some(InstallationProgress {
724                                    fraction_completed: Some(1.0),
725                                    bytes_downloaded: Some(1000),
726                                    ..Default::default()
727                                }),
728                                ..Default::default()
729                            },
730                        ))
731                        .await
732                        .unwrap();
733
734                    let mut reboot_controller_request_stream =
735                        reboot_controller.unwrap().into_stream();
736                    assert_matches!(
737                        reboot_controller_request_stream.next().await.unwrap(),
738                        Ok(RebootControllerRequest::Detach { .. })
739                    );
740                }
741                request => panic!("Unexpected request: {request:?}"),
742            }
743        };
744        future::join(installer_fut, stream_fut).await;
745        assert_eq!(
746            *progresses.lock(),
747            vec![
748                Progress {
749                    operation: Some("stage".to_string()),
750                    progress: 0.5,
751                    total_size: Some(1000),
752                    size_so_far: Some(500)
753                },
754                Progress {
755                    operation: Some("wait_to_reboot".to_string()),
756                    progress: 1.0,
757                    total_size: Some(1000),
758                    size_so_far: Some(1000)
759                }
760            ]
761        );
762    }
763
764    #[fasync::run_singlethreaded(test)]
765    async fn test_eager_package_update() {
766        let (mut installer, _, mut stream) = new_mock_installer(true);
767        let plan = FuchsiaInstallPlan {
768            update_package_urls: vec![UpdatePackageUrl::Package(TEST_URL.parse().unwrap())],
769            install_source: InstallSource::OnDemand,
770            omaha_response: vec![1, 2, 3],
771            request_metadata: Some(RequestMetadata {
772                request_body: vec![4, 5, 6],
773                public_key_id: 7_u64,
774                nonce: [8_u8; 32].into(),
775            }),
776            ecdsa_signature: Some(vec![10, 11, 12]),
777            ..FuchsiaInstallPlan::default()
778        };
779        let observer = MockProgressObserver::new();
780        let progresses = observer.progresses();
781        let installer_fut = async move {
782            let (install_result, app_install_results) =
783                installer.perform_install(&plan, Some(&observer)).await;
784            assert!(!install_result.urgent_update);
785            assert_matches!(app_install_results.as_slice(), &[AppInstallResult::Installed]);
786            assert_matches!(installer.reboot_controller, None);
787        };
788        let stream_fut = async move {
789            match stream.next().await.unwrap() {
790                Ok(CupRequest::Write { url, cup, responder }) => {
791                    assert_eq!(url.url, TEST_URL);
792                    let CupData { request, key_id, nonce, response, signature, .. } = cup;
793                    assert_eq!(request, Some(vec![4, 5, 6]));
794                    assert_eq!(key_id, Some(7_u64));
795                    assert_eq!(nonce, Some([8_u8; 32]));
796                    assert_eq!(response, Some(vec![1, 2, 3]));
797                    assert_eq!(signature, Some(vec![10, 11, 12]));
798                    responder.send(Ok(())).unwrap();
799                }
800                request => panic!("Unexpected request: {request:?}"),
801            }
802        };
803        future::join(installer_fut, stream_fut).await;
804        assert_eq!(
805            *progresses.lock(),
806            vec![Progress {
807                operation: Some(TEST_URL.to_string()),
808                progress: 1.0,
809                total_size: None,
810                size_so_far: None
811            }]
812        );
813    }
814
815    #[fasync::run_singlethreaded(test)]
816    async fn test_install_error() {
817        let (mut installer, mut stream, _) = new_mock_installer(true);
818        let plan = FuchsiaInstallPlan {
819            update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
820            ..FuchsiaInstallPlan::default()
821        };
822        let installer_fut = async move {
823            assert_matches!(
824                installer.perform_install(&plan, None).await.1.as_slice(),
825                &[AppInstallResult::Failed(FuchsiaInstallError::InstallerFailureState(
826                    InstallerFailure {
827                        state_name: "fail_prepare",
828                        reason: InstallerFailureReason::OutOfSpace
829                    }
830                ))]
831            );
832        };
833        let stream_fut = async move {
834            match stream.next().await.unwrap() {
835                Ok(InstallerRequest::StartUpdate { monitor, responder, .. }) => {
836                    responder.send(Ok("00000000-0000-0000-0000-000000000002")).unwrap();
837
838                    let monitor = monitor.into_proxy();
839                    let () = monitor
840                        .on_state(&State::FailPrepare(FailPrepareData {
841                            reason: Some(
842                                fidl_fuchsia_update_installer::PrepareFailureReason::OutOfSpace,
843                            ),
844                            ..Default::default()
845                        }))
846                        .await
847                        .unwrap();
848                }
849                request => panic!("Unexpected request: {request:?}"),
850            }
851        };
852        future::join(installer_fut, stream_fut).await;
853    }
854
855    #[fasync::run_singlethreaded(test)]
856    async fn test_server_close_unexpectedly() {
857        let (mut installer, mut stream, _) = new_mock_installer(true);
858        let plan = FuchsiaInstallPlan {
859            update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
860            ..FuchsiaInstallPlan::default()
861        };
862        let installer_fut = async move {
863            assert_matches!(
864                installer.perform_install(&plan, None).await.1.as_slice(),
865                &[AppInstallResult::Failed(FuchsiaInstallError::InstallationEndedUnexpectedly)]
866            );
867        };
868        let stream_fut = async move {
869            match stream.next().await.unwrap() {
870                Ok(InstallerRequest::StartUpdate { monitor, responder, .. }) => {
871                    responder.send(Ok("00000000-0000-0000-0000-000000000003")).unwrap();
872
873                    let monitor = monitor.into_proxy();
874                    let () = monitor
875                        .on_state(&State::Prepare(
876                            fidl_fuchsia_update_installer::PrepareData::default(),
877                        ))
878                        .await
879                        .unwrap();
880                    let () = monitor
881                        .on_state(&State::Fetch(fidl_fuchsia_update_installer::FetchData {
882                            info: Some(UpdateInfo { download_size: None, ..Default::default() }),
883                            progress: Some(InstallationProgress {
884                                fraction_completed: Some(0.0),
885                                bytes_downloaded: None,
886                                ..Default::default()
887                            }),
888                            ..Default::default()
889                        }))
890                        .await
891                        .unwrap();
892                }
893                request => panic!("Unexpected request: {request:?}"),
894            }
895        };
896        future::join(installer_fut, stream_fut).await;
897    }
898
899    #[fasync::run_singlethreaded(test)]
900    async fn test_connect_to_installer_failed() {
901        let (mut installer, _, _) = new_mock_installer(true);
902        installer.installer_connector = MockConnector::failing();
903        let plan = FuchsiaInstallPlan {
904            update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
905            ..FuchsiaInstallPlan::default()
906        };
907        assert_matches!(
908            installer.perform_install(&plan, None).await.1.as_slice(),
909            &[AppInstallResult::Failed(FuchsiaInstallError::Connect(_))]
910        );
911    }
912
913    #[fuchsia::test(allow_stalls = false)]
914    async fn test_reboot() {
915        let mut installer = new_installer();
916        let (reboot_controller, mut stream) =
917            fidl::endpoints::create_proxy_and_stream::<RebootControllerMarker>();
918        installer.reboot_controller = Some(reboot_controller);
919
920        {
921            let mut reboot_future = installer.perform_reboot();
922            assert_matches!(
923                TestExecutor::poll_until_stalled(&mut reboot_future).await,
924                Poll::Pending
925            );
926            TestExecutor::advance_to(TestExecutor::next_timer().unwrap()).await;
927            assert_matches!(reboot_future.await, Err(_));
928        }
929
930        assert_matches!(installer.reboot_controller, None);
931        assert_matches!(stream.next().await, Some(Ok(RebootControllerRequest::Unblock { .. })));
932        assert_matches!(stream.next().await, None);
933    }
934
935    #[fasync::run_singlethreaded(test)]
936    async fn test_simple_response() {
937        let request_params = RequestParams::default();
938        let request_metadata = RequestMetadata {
939            request_body: vec![4, 5, 6],
940            public_key_id: 7_u64,
941            nonce: [8_u8; 32].into(),
942        };
943        let signature = Some(vec![13, 14, 15]);
944        let mut update_check = UpdateCheck::ok([TEST_URL_BASE]);
945        update_check.manifest = Some(Manifest {
946            packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
947            ..Manifest::default()
948        });
949        let response = Response {
950            apps: vec![App {
951                update_check: Some(update_check),
952                id: "system_id".into(),
953                ..App::default()
954            }],
955            ..Response::default()
956        };
957
958        let install_plan = new_installer()
959            .try_create_install_plan(
960                &request_params,
961                Some(&request_metadata),
962                &response,
963                vec![1, 2, 3],
964                signature,
965            )
966            .await
967            .unwrap();
968        assert_eq!(
969            install_plan.update_package_urls,
970            vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
971        );
972        assert_eq!(install_plan.install_source, request_params.source);
973        assert!(!install_plan.urgent_update);
974        assert_eq!(install_plan.omaha_response, vec![1, 2, 3]);
975        assert_eq!(
976            install_plan.request_metadata,
977            Some(RequestMetadata {
978                request_body: vec![4, 5, 6],
979                public_key_id: 7_u64,
980                nonce: [8_u8; 32].into(),
981            })
982        );
983        assert_eq!(install_plan.ecdsa_signature, Some(vec![13, 14, 15]));
984    }
985
986    #[fasync::run_singlethreaded(test)]
987    async fn test_no_app() {
988        let request_params = RequestParams::default();
989        let request_metadata = None;
990        let signature = None;
991        let response = Response::default();
992
993        assert_matches!(
994            new_installer()
995                .try_create_install_plan(
996                    &request_params,
997                    request_metadata,
998                    &response,
999                    vec![],
1000                    signature
1001                )
1002                .await,
1003            Err(FuchsiaInstallError::Failure(_))
1004        );
1005    }
1006
1007    #[fasync::run_singlethreaded(test)]
1008    async fn test_multiple_app() {
1009        let request_params = RequestParams::default();
1010        let request_metadata = None;
1011        let signature = None;
1012
1013        let system_app = App {
1014            update_check: Some(UpdateCheck {
1015                manifest: Some(Manifest {
1016                    packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1017                    ..Manifest::default()
1018                }),
1019                ..UpdateCheck::ok([TEST_URL_BASE])
1020            }),
1021            id: "system_id".into(),
1022            ..App::default()
1023        };
1024        let response = Response {
1025            apps: vec![
1026                system_app,
1027                App { update_check: Some(UpdateCheck::no_update()), ..App::default() },
1028            ],
1029            ..Response::default()
1030        };
1031
1032        let install_plan = new_installer()
1033            .try_create_install_plan(
1034                &request_params,
1035                request_metadata,
1036                &response,
1037                vec![],
1038                signature,
1039            )
1040            .await
1041            .unwrap();
1042        assert_eq!(
1043            install_plan.update_package_urls,
1044            vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
1045        );
1046        assert_eq!(install_plan.install_source, request_params.source);
1047    }
1048
1049    #[fasync::run_singlethreaded(test)]
1050    async fn test_multiple_package_updates() {
1051        let request_params = RequestParams::default();
1052        let request_metadata = None;
1053        let signature = None;
1054
1055        let system_app = App {
1056            update_check: Some(UpdateCheck::no_update()),
1057            id: "system_id".into(),
1058            ..App::default()
1059        };
1060        let package1_app = App {
1061            update_check: Some(UpdateCheck {
1062                manifest: Some(Manifest {
1063                    packages: Packages::new(vec![Package::with_name(
1064                        "package1?hash=0000000000000000000000000000000000000000000000000000000000000000",
1065                    )]),
1066                    ..Manifest::default()
1067                }),
1068                ..UpdateCheck::ok([TEST_URL_BASE])
1069            }),
1070            id: "package1_id".into(),
1071            ..App::default()
1072        };
1073        let package2_app = App {
1074            update_check: Some(UpdateCheck::no_update()),
1075            id: "package2_id".into(),
1076            ..App::default()
1077        };
1078        let package3_app = App {
1079            update_check: Some(UpdateCheck {
1080                manifest: Some(Manifest {
1081                    packages: Packages::new(vec![Package::with_name(
1082                        "package3?hash=0000000000000000000000000000000000000000000000000000000000000000",
1083                    )]),
1084                    ..Manifest::default()
1085                }),
1086                ..UpdateCheck::ok([TEST_URL_BASE])
1087            }),
1088            id: "package3_id".into(),
1089            ..App::default()
1090        };
1091        let response = Response {
1092            apps: vec![system_app, package1_app, package2_app, package3_app],
1093            ..Response::default()
1094        };
1095
1096        let install_plan = new_installer()
1097            .try_create_install_plan(
1098                &request_params,
1099                request_metadata,
1100                &response,
1101                vec![],
1102                signature,
1103            )
1104            .await
1105            .unwrap();
1106        assert_eq!(
1107            install_plan.update_package_urls,
1108            vec![
1109                UpdatePackageUrl::Package(format!("{TEST_URL_BASE}package1?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap()),
1110                UpdatePackageUrl::Package(format!("{TEST_URL_BASE}package3?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap())
1111            ]
1112        );
1113        assert_eq!(install_plan.install_source, request_params.source);
1114    }
1115
1116    #[fasync::run_singlethreaded(test)]
1117    async fn test_mixed_update() {
1118        let request_params = RequestParams::default();
1119        let request_metadata = None;
1120        let signature = None;
1121        let system_app = App {
1122            update_check: Some(UpdateCheck {
1123                manifest: Some(Manifest {
1124                    packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1125                    ..Manifest::default()
1126                }),
1127                ..UpdateCheck::ok([TEST_URL_BASE])
1128            }),
1129            id: "system_id".into(),
1130            ..App::default()
1131        };
1132        let package_app = App {
1133            update_check: Some(UpdateCheck {
1134                manifest: Some(Manifest {
1135                    packages: Packages::new(vec![Package::with_name(
1136                        "some-package?hash=0000000000000000000000000000000000000000000000000000000000000000",
1137                    )]),
1138                    ..Manifest::default()
1139                }),
1140                ..UpdateCheck::ok([TEST_URL_BASE])
1141            }),
1142            id: "package_id".into(),
1143            ..App::default()
1144        };
1145        let response = Response { apps: vec![package_app, system_app], ..Response::default() };
1146
1147        let install_plan = new_installer()
1148            .try_create_install_plan(
1149                &request_params,
1150                request_metadata,
1151                &response,
1152                vec![],
1153                signature,
1154            )
1155            .await
1156            .unwrap();
1157        assert_eq!(
1158            install_plan.update_package_urls,
1159            vec![
1160                UpdatePackageUrl::Package(format!("{TEST_URL_BASE}some-package?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap()),
1161                UpdatePackageUrl::System(TEST_URL.parse().unwrap())
1162            ],
1163        );
1164        assert_eq!(install_plan.install_source, request_params.source);
1165    }
1166
1167    #[fasync::run_singlethreaded(test)]
1168    async fn test_no_update_check() {
1169        let request_params = RequestParams::default();
1170        let request_metadata = None;
1171        let signature = None;
1172        let response = Response {
1173            apps: vec![App { id: "system_id".into(), ..App::default() }],
1174            ..Response::default()
1175        };
1176
1177        assert_matches!(
1178            new_installer()
1179                .try_create_install_plan(
1180                    &request_params,
1181                    request_metadata,
1182                    &response,
1183                    vec![],
1184                    signature
1185                )
1186                .await,
1187            Err(FuchsiaInstallError::Failure(_))
1188        );
1189    }
1190
1191    #[fasync::run_singlethreaded(test)]
1192    async fn test_no_urls() {
1193        let request_params = RequestParams::default();
1194        let request_metadata = None;
1195        let signature = None;
1196        let response = Response {
1197            apps: vec![App {
1198                update_check: Some(UpdateCheck::default()),
1199                id: "system_id".into(),
1200                ..App::default()
1201            }],
1202            ..Response::default()
1203        };
1204
1205        assert_matches!(
1206            new_installer()
1207                .try_create_install_plan(
1208                    &request_params,
1209                    request_metadata,
1210                    &response,
1211                    vec![],
1212                    signature
1213                )
1214                .await,
1215            Err(FuchsiaInstallError::Failure(_))
1216        );
1217    }
1218
1219    #[fasync::run_singlethreaded(test)]
1220    async fn test_app_error_status() {
1221        let request_params = RequestParams::default();
1222        let request_metadata = None;
1223        let signature = None;
1224        let response = Response {
1225            apps: vec![App {
1226                status: OmahaStatus::Error("error-unknownApplication".to_string()),
1227                ..App::default()
1228            }],
1229            ..Response::default()
1230        };
1231
1232        assert_matches!(
1233            new_installer()
1234                .try_create_install_plan(
1235                    &request_params,
1236                    request_metadata,
1237                    &response,
1238                    vec![],
1239                    signature
1240                )
1241                .await,
1242            Err(FuchsiaInstallError::Failure(_))
1243        );
1244    }
1245
1246    #[fasync::run_singlethreaded(test)]
1247    async fn test_no_update() {
1248        let request_params = RequestParams::default();
1249        let request_metadata = None;
1250        let signature = None;
1251        let response = Response {
1252            apps: vec![App { update_check: Some(UpdateCheck::no_update()), ..App::default() }],
1253            ..Response::default()
1254        };
1255
1256        assert_matches!(
1257            new_installer()
1258                .try_create_install_plan(
1259                    &request_params,
1260                    request_metadata,
1261                    &response,
1262                    vec![],
1263                    signature
1264                )
1265                .await,
1266            Err(FuchsiaInstallError::Failure(_))
1267        );
1268    }
1269
1270    #[fasync::run_singlethreaded(test)]
1271    async fn test_invalid_url() {
1272        let request_params = RequestParams::default();
1273        let request_metadata = None;
1274        let signature = None;
1275        let response = Response {
1276            apps: vec![App {
1277                update_check: Some(UpdateCheck::ok(["invalid-url"])),
1278                id: "system_id".into(),
1279                ..App::default()
1280            }],
1281            ..Response::default()
1282        };
1283
1284        assert_matches!(
1285            new_installer()
1286                .try_create_install_plan(
1287                    &request_params,
1288                    request_metadata,
1289                    &response,
1290                    vec![],
1291                    signature
1292                )
1293                .await,
1294            Err(FuchsiaInstallError::Failure(_))
1295        );
1296    }
1297
1298    #[fasync::run_singlethreaded(test)]
1299    async fn test_no_manifest() {
1300        let request_params = RequestParams::default();
1301        let request_metadata = None;
1302        let signature = None;
1303        let response = Response {
1304            apps: vec![App {
1305                update_check: Some(UpdateCheck::ok([TEST_URL_BASE])),
1306                id: "system_id".into(),
1307                ..App::default()
1308            }],
1309            ..Response::default()
1310        };
1311
1312        assert_matches!(
1313            new_installer()
1314                .try_create_install_plan(
1315                    &request_params,
1316                    request_metadata,
1317                    &response,
1318                    vec![],
1319                    signature
1320                )
1321                .await,
1322            Err(FuchsiaInstallError::Failure(_))
1323        );
1324    }
1325
1326    #[fasync::run_singlethreaded(test)]
1327    async fn test_urgent_update_attribute_true() {
1328        let request_params = RequestParams::default();
1329        let request_metadata = None;
1330        let signature = None;
1331        let mut update_check = UpdateCheck::ok([TEST_URL_BASE]);
1332
1333        let mut extra_attributes = Map::new();
1334        extra_attributes.insert("_urgent_update".to_string(), "true".into());
1335        update_check.extra_attributes = extra_attributes;
1336
1337        update_check.manifest = Some(Manifest {
1338            packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1339            ..Manifest::default()
1340        });
1341        let response = Response {
1342            apps: vec![App {
1343                update_check: Some(update_check),
1344                id: "system_id".into(),
1345                ..App::default()
1346            }],
1347            ..Response::default()
1348        };
1349
1350        let install_plan = new_installer()
1351            .try_create_install_plan(
1352                &request_params,
1353                request_metadata,
1354                &response,
1355                vec![],
1356                signature,
1357            )
1358            .await
1359            .unwrap();
1360
1361        assert!(install_plan.urgent_update);
1362    }
1363}