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