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