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