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