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