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 };
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)).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 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 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 responder,
589 }) => {
590 assert_eq!(url.url, TEST_URL);
591 let Options {
592 initiator,
593 should_write_recovery,
594 allow_attach_to_existing_attempt,
595 } = options.try_into().unwrap();
596 assert_eq!(initiator, Initiator::User);
597 assert_matches!(reboot_controller, Some(_));
598 assert!(should_write_recovery);
599 assert!(allow_attach_to_existing_attempt);
600 responder.send(Ok("00000000-0000-0000-0000-000000000001")).unwrap();
601 let monitor = monitor.into_proxy();
602 let () = monitor
603 .on_state(&State::Stage(fidl_fuchsia_update_installer::StageData {
604 info: Some(UpdateInfo {
605 download_size: Some(1000),
606 ..Default::default()
607 }),
608 progress: Some(InstallationProgress {
609 fraction_completed: Some(0.5),
610 bytes_downloaded: Some(500),
611 ..Default::default()
612 }),
613 ..Default::default()
614 }))
615 .await
616 .unwrap();
617 let () = monitor
618 .on_state(&State::WaitToReboot(
619 fidl_fuchsia_update_installer::WaitToRebootData {
620 info: Some(UpdateInfo {
621 download_size: Some(1000),
622 ..Default::default()
623 }),
624 progress: Some(InstallationProgress {
625 fraction_completed: Some(1.0),
626 bytes_downloaded: Some(1000),
627 ..Default::default()
628 }),
629 ..Default::default()
630 },
631 ))
632 .await
633 .unwrap();
634 }
635 request => panic!("Unexpected request: {request:?}"),
636 }
637 };
638 future::join(installer_fut, stream_fut).await;
639 assert_eq!(
640 *progresses.lock(),
641 vec![
642 Progress {
643 operation: Some("stage".to_string()),
644 progress: 0.5,
645 total_size: Some(1000),
646 size_so_far: Some(500)
647 },
648 Progress {
649 operation: Some("wait_to_reboot".to_string()),
650 progress: 1.0,
651 total_size: Some(1000),
652 size_so_far: Some(1000)
653 }
654 ]
655 );
656 }
657
658 #[fasync::run_singlethreaded(test)]
659 async fn test_start_update_no_reboot() {
660 let (mut installer, mut stream, _) = new_mock_installer(false);
661 let plan = FuchsiaInstallPlan {
662 update_package_urls: vec![
663 UpdatePackageUrl::System(TEST_URL.parse().unwrap()),
664 UpdatePackageUrl::Package(TEST_URL.parse().unwrap()),
665 ],
666 install_source: InstallSource::OnDemand,
667 ..FuchsiaInstallPlan::default()
668 };
669 let observer = MockProgressObserver::new();
670 let progresses = observer.progresses();
671 let installer_fut = async move {
672 let (install_result, app_install_results) =
673 installer.perform_install(&plan, Some(&observer)).await;
674 assert!(!install_result.urgent_update);
675 assert_matches!(
676 app_install_results.as_slice(),
677 &[AppInstallResult::Installed, AppInstallResult::Deferred]
678 );
679 assert_matches!(installer.reboot_controller, None);
680 };
681 let stream_fut = async move {
682 match stream.next().await.unwrap() {
683 Ok(InstallerRequest::StartUpdate {
684 url,
685 options,
686 monitor,
687 reboot_controller,
688 responder,
689 }) => {
690 assert_eq!(url.url, TEST_URL);
691 let Options {
692 initiator,
693 should_write_recovery,
694 allow_attach_to_existing_attempt,
695 } = options.try_into().unwrap();
696 assert_eq!(initiator, Initiator::User);
697 assert!(should_write_recovery);
698 assert!(allow_attach_to_existing_attempt);
699 responder.send(Ok("00000000-0000-0000-0000-000000000001")).unwrap();
700 let monitor = monitor.into_proxy();
701 let () = monitor
702 .on_state(&State::Stage(fidl_fuchsia_update_installer::StageData {
703 info: Some(UpdateInfo {
704 download_size: Some(1000),
705 ..Default::default()
706 }),
707 progress: Some(InstallationProgress {
708 fraction_completed: Some(0.5),
709 bytes_downloaded: Some(500),
710 ..Default::default()
711 }),
712 ..Default::default()
713 }))
714 .await
715 .unwrap();
716 let () = monitor
717 .on_state(&State::WaitToReboot(
718 fidl_fuchsia_update_installer::WaitToRebootData {
719 info: Some(UpdateInfo {
720 download_size: Some(1000),
721 ..Default::default()
722 }),
723 progress: Some(InstallationProgress {
724 fraction_completed: Some(1.0),
725 bytes_downloaded: Some(1000),
726 ..Default::default()
727 }),
728 ..Default::default()
729 },
730 ))
731 .await
732 .unwrap();
733
734 let mut reboot_controller_request_stream =
735 reboot_controller.unwrap().into_stream();
736 assert_matches!(
737 reboot_controller_request_stream.next().await.unwrap(),
738 Ok(RebootControllerRequest::Detach { .. })
739 );
740 }
741 request => panic!("Unexpected request: {request:?}"),
742 }
743 };
744 future::join(installer_fut, stream_fut).await;
745 assert_eq!(
746 *progresses.lock(),
747 vec![
748 Progress {
749 operation: Some("stage".to_string()),
750 progress: 0.5,
751 total_size: Some(1000),
752 size_so_far: Some(500)
753 },
754 Progress {
755 operation: Some("wait_to_reboot".to_string()),
756 progress: 1.0,
757 total_size: Some(1000),
758 size_so_far: Some(1000)
759 }
760 ]
761 );
762 }
763
764 #[fasync::run_singlethreaded(test)]
765 async fn test_eager_package_update() {
766 let (mut installer, _, mut stream) = new_mock_installer(true);
767 let plan = FuchsiaInstallPlan {
768 update_package_urls: vec![UpdatePackageUrl::Package(TEST_URL.parse().unwrap())],
769 install_source: InstallSource::OnDemand,
770 omaha_response: vec![1, 2, 3],
771 request_metadata: Some(RequestMetadata {
772 request_body: vec![4, 5, 6],
773 public_key_id: 7_u64,
774 nonce: [8_u8; 32].into(),
775 }),
776 ecdsa_signature: Some(vec![10, 11, 12]),
777 ..FuchsiaInstallPlan::default()
778 };
779 let observer = MockProgressObserver::new();
780 let progresses = observer.progresses();
781 let installer_fut = async move {
782 let (install_result, app_install_results) =
783 installer.perform_install(&plan, Some(&observer)).await;
784 assert!(!install_result.urgent_update);
785 assert_matches!(app_install_results.as_slice(), &[AppInstallResult::Installed]);
786 assert_matches!(installer.reboot_controller, None);
787 };
788 let stream_fut = async move {
789 match stream.next().await.unwrap() {
790 Ok(CupRequest::Write { url, cup, responder }) => {
791 assert_eq!(url.url, TEST_URL);
792 let CupData { request, key_id, nonce, response, signature, .. } = cup;
793 assert_eq!(request, Some(vec![4, 5, 6]));
794 assert_eq!(key_id, Some(7_u64));
795 assert_eq!(nonce, Some([8_u8; 32]));
796 assert_eq!(response, Some(vec![1, 2, 3]));
797 assert_eq!(signature, Some(vec![10, 11, 12]));
798 responder.send(Ok(())).unwrap();
799 }
800 request => panic!("Unexpected request: {request:?}"),
801 }
802 };
803 future::join(installer_fut, stream_fut).await;
804 assert_eq!(
805 *progresses.lock(),
806 vec![Progress {
807 operation: Some(TEST_URL.to_string()),
808 progress: 1.0,
809 total_size: None,
810 size_so_far: None
811 }]
812 );
813 }
814
815 #[fasync::run_singlethreaded(test)]
816 async fn test_install_error() {
817 let (mut installer, mut stream, _) = new_mock_installer(true);
818 let plan = FuchsiaInstallPlan {
819 update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
820 ..FuchsiaInstallPlan::default()
821 };
822 let installer_fut = async move {
823 assert_matches!(
824 installer.perform_install(&plan, None).await.1.as_slice(),
825 &[AppInstallResult::Failed(FuchsiaInstallError::InstallerFailureState(
826 InstallerFailure {
827 state_name: "fail_prepare",
828 reason: InstallerFailureReason::OutOfSpace
829 }
830 ))]
831 );
832 };
833 let stream_fut = async move {
834 match stream.next().await.unwrap() {
835 Ok(InstallerRequest::StartUpdate { monitor, responder, .. }) => {
836 responder.send(Ok("00000000-0000-0000-0000-000000000002")).unwrap();
837
838 let monitor = monitor.into_proxy();
839 let () = monitor
840 .on_state(&State::FailPrepare(FailPrepareData {
841 reason: Some(
842 fidl_fuchsia_update_installer::PrepareFailureReason::OutOfSpace,
843 ),
844 ..Default::default()
845 }))
846 .await
847 .unwrap();
848 }
849 request => panic!("Unexpected request: {request:?}"),
850 }
851 };
852 future::join(installer_fut, stream_fut).await;
853 }
854
855 #[fasync::run_singlethreaded(test)]
856 async fn test_server_close_unexpectedly() {
857 let (mut installer, mut stream, _) = new_mock_installer(true);
858 let plan = FuchsiaInstallPlan {
859 update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
860 ..FuchsiaInstallPlan::default()
861 };
862 let installer_fut = async move {
863 assert_matches!(
864 installer.perform_install(&plan, None).await.1.as_slice(),
865 &[AppInstallResult::Failed(FuchsiaInstallError::InstallationEndedUnexpectedly)]
866 );
867 };
868 let stream_fut = async move {
869 match stream.next().await.unwrap() {
870 Ok(InstallerRequest::StartUpdate { monitor, responder, .. }) => {
871 responder.send(Ok("00000000-0000-0000-0000-000000000003")).unwrap();
872
873 let monitor = monitor.into_proxy();
874 let () = monitor
875 .on_state(&State::Prepare(
876 fidl_fuchsia_update_installer::PrepareData::default(),
877 ))
878 .await
879 .unwrap();
880 let () = monitor
881 .on_state(&State::Fetch(fidl_fuchsia_update_installer::FetchData {
882 info: Some(UpdateInfo { download_size: None, ..Default::default() }),
883 progress: Some(InstallationProgress {
884 fraction_completed: Some(0.0),
885 bytes_downloaded: None,
886 ..Default::default()
887 }),
888 ..Default::default()
889 }))
890 .await
891 .unwrap();
892 }
893 request => panic!("Unexpected request: {request:?}"),
894 }
895 };
896 future::join(installer_fut, stream_fut).await;
897 }
898
899 #[fasync::run_singlethreaded(test)]
900 async fn test_connect_to_installer_failed() {
901 let (mut installer, _, _) = new_mock_installer(true);
902 installer.installer_connector = MockConnector::failing();
903 let plan = FuchsiaInstallPlan {
904 update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
905 ..FuchsiaInstallPlan::default()
906 };
907 assert_matches!(
908 installer.perform_install(&plan, None).await.1.as_slice(),
909 &[AppInstallResult::Failed(FuchsiaInstallError::Connect(_))]
910 );
911 }
912
913 #[fuchsia::test(allow_stalls = false)]
914 async fn test_reboot() {
915 let mut installer = new_installer();
916 let (reboot_controller, mut stream) =
917 fidl::endpoints::create_proxy_and_stream::<RebootControllerMarker>();
918 installer.reboot_controller = Some(reboot_controller);
919
920 {
921 let mut reboot_future = installer.perform_reboot();
922 assert_matches!(
923 TestExecutor::poll_until_stalled(&mut reboot_future).await,
924 Poll::Pending
925 );
926 TestExecutor::advance_to(TestExecutor::next_timer().unwrap()).await;
927 assert_matches!(reboot_future.await, Err(_));
928 }
929
930 assert_matches!(installer.reboot_controller, None);
931 assert_matches!(stream.next().await, Some(Ok(RebootControllerRequest::Unblock { .. })));
932 assert_matches!(stream.next().await, None);
933 }
934
935 #[fasync::run_singlethreaded(test)]
936 async fn test_simple_response() {
937 let request_params = RequestParams::default();
938 let request_metadata = RequestMetadata {
939 request_body: vec![4, 5, 6],
940 public_key_id: 7_u64,
941 nonce: [8_u8; 32].into(),
942 };
943 let signature = Some(vec![13, 14, 15]);
944 let mut update_check = UpdateCheck::ok([TEST_URL_BASE]);
945 update_check.manifest = Some(Manifest {
946 packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
947 ..Manifest::default()
948 });
949 let response = Response {
950 apps: vec![App {
951 update_check: Some(update_check),
952 id: "system_id".into(),
953 ..App::default()
954 }],
955 ..Response::default()
956 };
957
958 let install_plan = new_installer()
959 .try_create_install_plan(
960 &request_params,
961 Some(&request_metadata),
962 &response,
963 vec![1, 2, 3],
964 signature,
965 )
966 .await
967 .unwrap();
968 assert_eq!(
969 install_plan.update_package_urls,
970 vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
971 );
972 assert_eq!(install_plan.install_source, request_params.source);
973 assert!(!install_plan.urgent_update);
974 assert_eq!(install_plan.omaha_response, vec![1, 2, 3]);
975 assert_eq!(
976 install_plan.request_metadata,
977 Some(RequestMetadata {
978 request_body: vec![4, 5, 6],
979 public_key_id: 7_u64,
980 nonce: [8_u8; 32].into(),
981 })
982 );
983 assert_eq!(install_plan.ecdsa_signature, Some(vec![13, 14, 15]));
984 }
985
986 #[fasync::run_singlethreaded(test)]
987 async fn test_no_app() {
988 let request_params = RequestParams::default();
989 let request_metadata = None;
990 let signature = None;
991 let response = Response::default();
992
993 assert_matches!(
994 new_installer()
995 .try_create_install_plan(
996 &request_params,
997 request_metadata,
998 &response,
999 vec![],
1000 signature
1001 )
1002 .await,
1003 Err(FuchsiaInstallError::Failure(_))
1004 );
1005 }
1006
1007 #[fasync::run_singlethreaded(test)]
1008 async fn test_multiple_app() {
1009 let request_params = RequestParams::default();
1010 let request_metadata = None;
1011 let signature = None;
1012
1013 let system_app = App {
1014 update_check: Some(UpdateCheck {
1015 manifest: Some(Manifest {
1016 packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1017 ..Manifest::default()
1018 }),
1019 ..UpdateCheck::ok([TEST_URL_BASE])
1020 }),
1021 id: "system_id".into(),
1022 ..App::default()
1023 };
1024 let response = Response {
1025 apps: vec![
1026 system_app,
1027 App { update_check: Some(UpdateCheck::no_update()), ..App::default() },
1028 ],
1029 ..Response::default()
1030 };
1031
1032 let install_plan = new_installer()
1033 .try_create_install_plan(
1034 &request_params,
1035 request_metadata,
1036 &response,
1037 vec![],
1038 signature,
1039 )
1040 .await
1041 .unwrap();
1042 assert_eq!(
1043 install_plan.update_package_urls,
1044 vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
1045 );
1046 assert_eq!(install_plan.install_source, request_params.source);
1047 }
1048
1049 #[fasync::run_singlethreaded(test)]
1050 async fn test_multiple_package_updates() {
1051 let request_params = RequestParams::default();
1052 let request_metadata = None;
1053 let signature = None;
1054
1055 let system_app = App {
1056 update_check: Some(UpdateCheck::no_update()),
1057 id: "system_id".into(),
1058 ..App::default()
1059 };
1060 let package1_app = App {
1061 update_check: Some(UpdateCheck {
1062 manifest: Some(Manifest {
1063 packages: Packages::new(vec![Package::with_name(
1064 "package1?hash=0000000000000000000000000000000000000000000000000000000000000000",
1065 )]),
1066 ..Manifest::default()
1067 }),
1068 ..UpdateCheck::ok([TEST_URL_BASE])
1069 }),
1070 id: "package1_id".into(),
1071 ..App::default()
1072 };
1073 let package2_app = App {
1074 update_check: Some(UpdateCheck::no_update()),
1075 id: "package2_id".into(),
1076 ..App::default()
1077 };
1078 let package3_app = App {
1079 update_check: Some(UpdateCheck {
1080 manifest: Some(Manifest {
1081 packages: Packages::new(vec![Package::with_name(
1082 "package3?hash=0000000000000000000000000000000000000000000000000000000000000000",
1083 )]),
1084 ..Manifest::default()
1085 }),
1086 ..UpdateCheck::ok([TEST_URL_BASE])
1087 }),
1088 id: "package3_id".into(),
1089 ..App::default()
1090 };
1091 let response = Response {
1092 apps: vec![system_app, package1_app, package2_app, package3_app],
1093 ..Response::default()
1094 };
1095
1096 let install_plan = new_installer()
1097 .try_create_install_plan(
1098 &request_params,
1099 request_metadata,
1100 &response,
1101 vec![],
1102 signature,
1103 )
1104 .await
1105 .unwrap();
1106 assert_eq!(
1107 install_plan.update_package_urls,
1108 vec![
1109 UpdatePackageUrl::Package(format!("{TEST_URL_BASE}package1?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap()),
1110 UpdatePackageUrl::Package(format!("{TEST_URL_BASE}package3?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap())
1111 ]
1112 );
1113 assert_eq!(install_plan.install_source, request_params.source);
1114 }
1115
1116 #[fasync::run_singlethreaded(test)]
1117 async fn test_mixed_update() {
1118 let request_params = RequestParams::default();
1119 let request_metadata = None;
1120 let signature = None;
1121 let system_app = App {
1122 update_check: Some(UpdateCheck {
1123 manifest: Some(Manifest {
1124 packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1125 ..Manifest::default()
1126 }),
1127 ..UpdateCheck::ok([TEST_URL_BASE])
1128 }),
1129 id: "system_id".into(),
1130 ..App::default()
1131 };
1132 let package_app = App {
1133 update_check: Some(UpdateCheck {
1134 manifest: Some(Manifest {
1135 packages: Packages::new(vec![Package::with_name(
1136 "some-package?hash=0000000000000000000000000000000000000000000000000000000000000000",
1137 )]),
1138 ..Manifest::default()
1139 }),
1140 ..UpdateCheck::ok([TEST_URL_BASE])
1141 }),
1142 id: "package_id".into(),
1143 ..App::default()
1144 };
1145 let response = Response { apps: vec![package_app, system_app], ..Response::default() };
1146
1147 let install_plan = new_installer()
1148 .try_create_install_plan(
1149 &request_params,
1150 request_metadata,
1151 &response,
1152 vec![],
1153 signature,
1154 )
1155 .await
1156 .unwrap();
1157 assert_eq!(
1158 install_plan.update_package_urls,
1159 vec![
1160 UpdatePackageUrl::Package(format!("{TEST_URL_BASE}some-package?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap()),
1161 UpdatePackageUrl::System(TEST_URL.parse().unwrap())
1162 ],
1163 );
1164 assert_eq!(install_plan.install_source, request_params.source);
1165 }
1166
1167 #[fasync::run_singlethreaded(test)]
1168 async fn test_no_update_check() {
1169 let request_params = RequestParams::default();
1170 let request_metadata = None;
1171 let signature = None;
1172 let response = Response {
1173 apps: vec![App { id: "system_id".into(), ..App::default() }],
1174 ..Response::default()
1175 };
1176
1177 assert_matches!(
1178 new_installer()
1179 .try_create_install_plan(
1180 &request_params,
1181 request_metadata,
1182 &response,
1183 vec![],
1184 signature
1185 )
1186 .await,
1187 Err(FuchsiaInstallError::Failure(_))
1188 );
1189 }
1190
1191 #[fasync::run_singlethreaded(test)]
1192 async fn test_no_urls() {
1193 let request_params = RequestParams::default();
1194 let request_metadata = None;
1195 let signature = None;
1196 let response = Response {
1197 apps: vec![App {
1198 update_check: Some(UpdateCheck::default()),
1199 id: "system_id".into(),
1200 ..App::default()
1201 }],
1202 ..Response::default()
1203 };
1204
1205 assert_matches!(
1206 new_installer()
1207 .try_create_install_plan(
1208 &request_params,
1209 request_metadata,
1210 &response,
1211 vec![],
1212 signature
1213 )
1214 .await,
1215 Err(FuchsiaInstallError::Failure(_))
1216 );
1217 }
1218
1219 #[fasync::run_singlethreaded(test)]
1220 async fn test_app_error_status() {
1221 let request_params = RequestParams::default();
1222 let request_metadata = None;
1223 let signature = None;
1224 let response = Response {
1225 apps: vec![App {
1226 status: OmahaStatus::Error("error-unknownApplication".to_string()),
1227 ..App::default()
1228 }],
1229 ..Response::default()
1230 };
1231
1232 assert_matches!(
1233 new_installer()
1234 .try_create_install_plan(
1235 &request_params,
1236 request_metadata,
1237 &response,
1238 vec![],
1239 signature
1240 )
1241 .await,
1242 Err(FuchsiaInstallError::Failure(_))
1243 );
1244 }
1245
1246 #[fasync::run_singlethreaded(test)]
1247 async fn test_no_update() {
1248 let request_params = RequestParams::default();
1249 let request_metadata = None;
1250 let signature = None;
1251 let response = Response {
1252 apps: vec![App { update_check: Some(UpdateCheck::no_update()), ..App::default() }],
1253 ..Response::default()
1254 };
1255
1256 assert_matches!(
1257 new_installer()
1258 .try_create_install_plan(
1259 &request_params,
1260 request_metadata,
1261 &response,
1262 vec![],
1263 signature
1264 )
1265 .await,
1266 Err(FuchsiaInstallError::Failure(_))
1267 );
1268 }
1269
1270 #[fasync::run_singlethreaded(test)]
1271 async fn test_invalid_url() {
1272 let request_params = RequestParams::default();
1273 let request_metadata = None;
1274 let signature = None;
1275 let response = Response {
1276 apps: vec![App {
1277 update_check: Some(UpdateCheck::ok(["invalid-url"])),
1278 id: "system_id".into(),
1279 ..App::default()
1280 }],
1281 ..Response::default()
1282 };
1283
1284 assert_matches!(
1285 new_installer()
1286 .try_create_install_plan(
1287 &request_params,
1288 request_metadata,
1289 &response,
1290 vec![],
1291 signature
1292 )
1293 .await,
1294 Err(FuchsiaInstallError::Failure(_))
1295 );
1296 }
1297
1298 #[fasync::run_singlethreaded(test)]
1299 async fn test_no_manifest() {
1300 let request_params = RequestParams::default();
1301 let request_metadata = None;
1302 let signature = None;
1303 let response = Response {
1304 apps: vec![App {
1305 update_check: Some(UpdateCheck::ok([TEST_URL_BASE])),
1306 id: "system_id".into(),
1307 ..App::default()
1308 }],
1309 ..Response::default()
1310 };
1311
1312 assert_matches!(
1313 new_installer()
1314 .try_create_install_plan(
1315 &request_params,
1316 request_metadata,
1317 &response,
1318 vec![],
1319 signature
1320 )
1321 .await,
1322 Err(FuchsiaInstallError::Failure(_))
1323 );
1324 }
1325
1326 #[fasync::run_singlethreaded(test)]
1327 async fn test_urgent_update_attribute_true() {
1328 let request_params = RequestParams::default();
1329 let request_metadata = None;
1330 let signature = None;
1331 let mut update_check = UpdateCheck::ok([TEST_URL_BASE]);
1332
1333 let mut extra_attributes = Map::new();
1334 extra_attributes.insert("_urgent_update".to_string(), "true".into());
1335 update_check.extra_attributes = extra_attributes;
1336
1337 update_check.manifest = Some(Manifest {
1338 packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1339 ..Manifest::default()
1340 });
1341 let response = Response {
1342 apps: vec![App {
1343 update_check: Some(update_check),
1344 id: "system_id".into(),
1345 ..App::default()
1346 }],
1347 ..Response::default()
1348 };
1349
1350 let install_plan = new_installer()
1351 .try_create_install_plan(
1352 &request_params,
1353 request_metadata,
1354 &response,
1355 vec![],
1356 signature,
1357 )
1358 .await
1359 .unwrap();
1360
1361 assert!(install_plan.urgent_update);
1362 }
1363}