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