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