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::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 url::Url,
159 install_source: &'a InstallSource,
160 observer: Option<&'a dyn ProgressObserver>,
161 ) -> Result<(), FuchsiaInstallError> {
162 let options = Options {
163 initiator: match install_source {
164 InstallSource::ScheduledTask => Initiator::Service,
165 InstallSource::OnDemand => Initiator::User,
166 },
167 should_write_recovery: true,
168 allow_attach_to_existing_attempt: true,
169 };
170
171 let proxy = self.installer_connector.connect().map_err(FuchsiaInstallError::Connect)?;
172 let (reboot_controller, reboot_controller_server_end) =
173 fidl::endpoints::create_proxy::<RebootControllerMarker>();
174
175 if self.allow_reboot {
176 self.reboot_controller = Some(reboot_controller);
177 } else {
178 let () = reboot_controller.detach().context("disabling automatic reboot")?;
179 }
180
181 let mut update_attempt =
182 start_update(url, options, &proxy, Some(reboot_controller_server_end), None).await?;
183
184 while let Some(state) = update_attempt.try_next().await? {
185 info!("Installer entered state: {}", state.name());
186 if let Some(observer) = observer {
187 if let Some(progress) = state.progress() {
188 observer
189 .receive_progress(
190 Some(state.name()),
191 progress.fraction_completed(),
192 state.download_size(),
193 Some(progress.bytes_downloaded()),
194 )
195 .await;
196 } else {
197 observer.receive_progress(Some(state.name()), 0., None, None).await;
198 }
199 }
200 if state.id() == StateId::WaitToReboot || state.is_success() {
201 return Ok(());
202 } else if state.is_failure() {
203 match state {
204 State::FailFetch(fail_fetch_data) => {
205 return Err(FuchsiaInstallError::InstallerFailureState(InstallerFailure {
206 state_name: state.name(),
207 reason: fail_fetch_data.reason().into(),
208 }));
209 }
210 State::FailPrepare(prepare_failure_reason) => {
211 return Err(FuchsiaInstallError::InstallerFailureState(InstallerFailure {
212 state_name: state.name(),
213 reason: prepare_failure_reason.into(),
214 }));
215 }
216 _ => {
217 return Err(FuchsiaInstallError::InstallerFailureState(InstallerFailure {
218 state_name: state.name(),
219 reason: InstallerFailureReason::Internal,
220 }));
221 }
222 }
223 }
224 }
225
226 Err(FuchsiaInstallError::InstallationEndedUnexpectedly)
227 }
228
229 async fn perform_install_eager_package(
230 &mut self,
231 url: &PinnedAbsolutePackageUrl,
232 install_plan: &FuchsiaInstallPlan,
233 ) -> Result<(), FuchsiaInstallError> {
234 let proxy = self.cup_connector.connect().map_err(FuchsiaInstallError::Connect)?;
235 let url = fpkg::PackageUrl { url: url.to_string() };
236 let rm = install_plan
237 .request_metadata
238 .as_ref()
239 .ok_or(FuchsiaInstallError::MissingRequestMetadata)?;
240 let cup_data: CupData = CupData {
241 request: Some(rm.request_body.clone()),
242 key_id: Some(rm.public_key_id),
243 nonce: Some(rm.nonce.into()),
244 response: Some(install_plan.omaha_response.clone()),
245 signature: install_plan.ecdsa_signature.as_ref().cloned(),
246 ..Default::default()
247 };
248 proxy.write(&url, &cup_data).await?.map_err(FuchsiaInstallError::CupWrite)
249 }
250}
251
252impl<I, C> Installer for FuchsiaInstaller<I, C>
253where
254 I: Connect<Proxy = InstallerProxy> + Send,
255 C: Connect<Proxy = CupProxy> + Send,
256{
257 type InstallPlan = FuchsiaInstallPlan;
258 type Error = FuchsiaInstallError;
259 type InstallResult = InstallResult;
260
261 fn perform_install<'a>(
262 &'a mut self,
263 install_plan: &'a FuchsiaInstallPlan,
264 observer: Option<&'a dyn ProgressObserver>,
265 ) -> LocalBoxFuture<'a, (Self::InstallResult, Vec<AppInstallResult<Self::Error>>)> {
266 let is_system_update = install_plan.is_system_update();
267
268 async move {
269 let mut app_results = vec![];
270 for (i, url) in install_plan.update_package_urls.iter().enumerate() {
271 app_results.push(match url {
272 UpdatePackageUrl::System(url) => self
273 .perform_install_system_update(url, &install_plan.install_source, observer)
274 .await
275 .into(),
276 UpdatePackageUrl::Package(url) => {
277 if is_system_update {
278 AppInstallResult::Deferred
279 } else {
280 let result =
281 self.perform_install_eager_package(url, install_plan).await.into();
282 if let Some(observer) = observer {
283 observer
284 .receive_progress(
285 Some(&url.to_string()),
286 (i + 1) as f32
287 / install_plan.update_package_urls.len() as f32,
288 None,
289 None,
290 )
291 .await;
292 }
293 result
294 }
295 }
296 });
297 }
298 (InstallResult { urgent_update: install_plan.urgent_update }, app_results)
299 }
300 .boxed_local()
301 }
302
303 fn perform_reboot(&mut self) -> LocalBoxFuture<'_, Result<(), anyhow::Error>> {
304 async move {
305 match self.reboot_controller.take() {
306 Some(reboot_controller) => {
307 reboot_controller
308 .unblock()
309 .context("notify installer it can reboot when ready")?;
310 }
311 None => {
312 if self.allow_reboot {
313 warn!("We should reboot due to the policy but the reboot controller has been dropped!");
314 }
315 }
316 }
317
318 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 signature: _,
589 responder,
590 }) => {
591 assert_eq!(url.url, TEST_URL);
592 let Options {
593 initiator,
594 should_write_recovery,
595 allow_attach_to_existing_attempt,
596 } = options.try_into().unwrap();
597 assert_eq!(initiator, Initiator::User);
598 assert_matches!(reboot_controller, Some(_));
599 assert!(should_write_recovery);
600 assert!(allow_attach_to_existing_attempt);
601 responder.send(Ok("00000000-0000-0000-0000-000000000001")).unwrap();
602 let monitor = monitor.into_proxy();
603 let () = monitor
604 .on_state(&State::Stage(fidl_fuchsia_update_installer::StageData {
605 info: Some(UpdateInfo {
606 download_size: Some(1000),
607 ..Default::default()
608 }),
609 progress: Some(InstallationProgress {
610 fraction_completed: Some(0.5),
611 bytes_downloaded: Some(500),
612 ..Default::default()
613 }),
614 ..Default::default()
615 }))
616 .await
617 .unwrap();
618 let () = monitor
619 .on_state(&State::WaitToReboot(
620 fidl_fuchsia_update_installer::WaitToRebootData {
621 info: Some(UpdateInfo {
622 download_size: Some(1000),
623 ..Default::default()
624 }),
625 progress: Some(InstallationProgress {
626 fraction_completed: Some(1.0),
627 bytes_downloaded: Some(1000),
628 ..Default::default()
629 }),
630 ..Default::default()
631 },
632 ))
633 .await
634 .unwrap();
635 }
636 request => panic!("Unexpected request: {request:?}"),
637 }
638 };
639 future::join(installer_fut, stream_fut).await;
640 assert_eq!(
641 *progresses.lock(),
642 vec![
643 Progress {
644 operation: Some("stage".to_string()),
645 progress: 0.5,
646 total_size: Some(1000),
647 size_so_far: Some(500)
648 },
649 Progress {
650 operation: Some("wait_to_reboot".to_string()),
651 progress: 1.0,
652 total_size: Some(1000),
653 size_so_far: Some(1000)
654 }
655 ]
656 );
657 }
658
659 #[fasync::run_singlethreaded(test)]
660 async fn test_start_update_no_reboot() {
661 let (mut installer, mut stream, _) = new_mock_installer(false);
662 let plan = FuchsiaInstallPlan {
663 update_package_urls: vec![
664 UpdatePackageUrl::System(TEST_URL.parse().unwrap()),
665 UpdatePackageUrl::Package(TEST_URL.parse().unwrap()),
666 ],
667 install_source: InstallSource::OnDemand,
668 ..FuchsiaInstallPlan::default()
669 };
670 let observer = MockProgressObserver::new();
671 let progresses = observer.progresses();
672 let installer_fut = async move {
673 let (install_result, app_install_results) =
674 installer.perform_install(&plan, Some(&observer)).await;
675 assert!(!install_result.urgent_update);
676 assert_matches!(
677 app_install_results.as_slice(),
678 &[AppInstallResult::Installed, AppInstallResult::Deferred]
679 );
680 assert_matches!(installer.reboot_controller, None);
681 };
682 let stream_fut = async move {
683 match stream.next().await.unwrap() {
684 Ok(InstallerRequest::StartUpdate {
685 url,
686 options,
687 monitor,
688 reboot_controller,
689 signature: _,
690 responder,
691 }) => {
692 assert_eq!(url.url, TEST_URL);
693 let Options {
694 initiator,
695 should_write_recovery,
696 allow_attach_to_existing_attempt,
697 } = options.try_into().unwrap();
698 assert_eq!(initiator, Initiator::User);
699 assert!(should_write_recovery);
700 assert!(allow_attach_to_existing_attempt);
701 responder.send(Ok("00000000-0000-0000-0000-000000000001")).unwrap();
702 let monitor = monitor.into_proxy();
703 let () = monitor
704 .on_state(&State::Stage(fidl_fuchsia_update_installer::StageData {
705 info: Some(UpdateInfo {
706 download_size: Some(1000),
707 ..Default::default()
708 }),
709 progress: Some(InstallationProgress {
710 fraction_completed: Some(0.5),
711 bytes_downloaded: Some(500),
712 ..Default::default()
713 }),
714 ..Default::default()
715 }))
716 .await
717 .unwrap();
718 let () = monitor
719 .on_state(&State::WaitToReboot(
720 fidl_fuchsia_update_installer::WaitToRebootData {
721 info: Some(UpdateInfo {
722 download_size: Some(1000),
723 ..Default::default()
724 }),
725 progress: Some(InstallationProgress {
726 fraction_completed: Some(1.0),
727 bytes_downloaded: Some(1000),
728 ..Default::default()
729 }),
730 ..Default::default()
731 },
732 ))
733 .await
734 .unwrap();
735
736 let mut reboot_controller_request_stream =
737 reboot_controller.unwrap().into_stream();
738 assert_matches!(
739 reboot_controller_request_stream.next().await.unwrap(),
740 Ok(RebootControllerRequest::Detach { .. })
741 );
742 }
743 request => panic!("Unexpected request: {request:?}"),
744 }
745 };
746 future::join(installer_fut, stream_fut).await;
747 assert_eq!(
748 *progresses.lock(),
749 vec![
750 Progress {
751 operation: Some("stage".to_string()),
752 progress: 0.5,
753 total_size: Some(1000),
754 size_so_far: Some(500)
755 },
756 Progress {
757 operation: Some("wait_to_reboot".to_string()),
758 progress: 1.0,
759 total_size: Some(1000),
760 size_so_far: Some(1000)
761 }
762 ]
763 );
764 }
765
766 #[fasync::run_singlethreaded(test)]
767 async fn test_eager_package_update() {
768 let (mut installer, _, mut stream) = new_mock_installer(true);
769 let plan = FuchsiaInstallPlan {
770 update_package_urls: vec![UpdatePackageUrl::Package(TEST_URL.parse().unwrap())],
771 install_source: InstallSource::OnDemand,
772 omaha_response: vec![1, 2, 3],
773 request_metadata: Some(RequestMetadata {
774 request_body: vec![4, 5, 6],
775 public_key_id: 7_u64,
776 nonce: [8_u8; 32].into(),
777 }),
778 ecdsa_signature: Some(vec![10, 11, 12]),
779 ..FuchsiaInstallPlan::default()
780 };
781 let observer = MockProgressObserver::new();
782 let progresses = observer.progresses();
783 let installer_fut = async move {
784 let (install_result, app_install_results) =
785 installer.perform_install(&plan, Some(&observer)).await;
786 assert!(!install_result.urgent_update);
787 assert_matches!(app_install_results.as_slice(), &[AppInstallResult::Installed]);
788 assert_matches!(installer.reboot_controller, None);
789 };
790 let stream_fut = async move {
791 match stream.next().await.unwrap() {
792 Ok(CupRequest::Write { url, cup, responder }) => {
793 assert_eq!(url.url, TEST_URL);
794 let CupData { request, key_id, nonce, response, signature, .. } = cup;
795 assert_eq!(request, Some(vec![4, 5, 6]));
796 assert_eq!(key_id, Some(7_u64));
797 assert_eq!(nonce, Some([8_u8; 32]));
798 assert_eq!(response, Some(vec![1, 2, 3]));
799 assert_eq!(signature, Some(vec![10, 11, 12]));
800 responder.send(Ok(())).unwrap();
801 }
802 request => panic!("Unexpected request: {request:?}"),
803 }
804 };
805 future::join(installer_fut, stream_fut).await;
806 assert_eq!(
807 *progresses.lock(),
808 vec![Progress {
809 operation: Some(TEST_URL.to_string()),
810 progress: 1.0,
811 total_size: None,
812 size_so_far: None
813 }]
814 );
815 }
816
817 #[fasync::run_singlethreaded(test)]
818 async fn test_install_error() {
819 let (mut installer, mut stream, _) = new_mock_installer(true);
820 let plan = FuchsiaInstallPlan {
821 update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
822 ..FuchsiaInstallPlan::default()
823 };
824 let installer_fut = async move {
825 assert_matches!(
826 installer.perform_install(&plan, None).await.1.as_slice(),
827 &[AppInstallResult::Failed(FuchsiaInstallError::InstallerFailureState(
828 InstallerFailure {
829 state_name: "fail_prepare",
830 reason: InstallerFailureReason::OutOfSpace
831 }
832 ))]
833 );
834 };
835 let stream_fut = async move {
836 match stream.next().await.unwrap() {
837 Ok(InstallerRequest::StartUpdate { monitor, responder, .. }) => {
838 responder.send(Ok("00000000-0000-0000-0000-000000000002")).unwrap();
839
840 let monitor = monitor.into_proxy();
841 let () = monitor
842 .on_state(&State::FailPrepare(FailPrepareData {
843 reason: Some(
844 fidl_fuchsia_update_installer::PrepareFailureReason::OutOfSpace,
845 ),
846 ..Default::default()
847 }))
848 .await
849 .unwrap();
850 }
851 request => panic!("Unexpected request: {request:?}"),
852 }
853 };
854 future::join(installer_fut, stream_fut).await;
855 }
856
857 #[fasync::run_singlethreaded(test)]
858 async fn test_server_close_unexpectedly() {
859 let (mut installer, mut stream, _) = new_mock_installer(true);
860 let plan = FuchsiaInstallPlan {
861 update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
862 ..FuchsiaInstallPlan::default()
863 };
864 let installer_fut = async move {
865 assert_matches!(
866 installer.perform_install(&plan, None).await.1.as_slice(),
867 &[AppInstallResult::Failed(FuchsiaInstallError::InstallationEndedUnexpectedly)]
868 );
869 };
870 let stream_fut = async move {
871 match stream.next().await.unwrap() {
872 Ok(InstallerRequest::StartUpdate { monitor, responder, .. }) => {
873 responder.send(Ok("00000000-0000-0000-0000-000000000003")).unwrap();
874
875 let monitor = monitor.into_proxy();
876 let () = monitor
877 .on_state(&State::Prepare(
878 fidl_fuchsia_update_installer::PrepareData::default(),
879 ))
880 .await
881 .unwrap();
882 let () = monitor
883 .on_state(&State::Fetch(fidl_fuchsia_update_installer::FetchData {
884 info: Some(UpdateInfo { download_size: None, ..Default::default() }),
885 progress: Some(InstallationProgress {
886 fraction_completed: Some(0.0),
887 bytes_downloaded: None,
888 ..Default::default()
889 }),
890 ..Default::default()
891 }))
892 .await
893 .unwrap();
894 }
895 request => panic!("Unexpected request: {request:?}"),
896 }
897 };
898 future::join(installer_fut, stream_fut).await;
899 }
900
901 #[fasync::run_singlethreaded(test)]
902 async fn test_connect_to_installer_failed() {
903 let (mut installer, _, _) = new_mock_installer(true);
904 installer.installer_connector = MockConnector::failing();
905 let plan = FuchsiaInstallPlan {
906 update_package_urls: vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
907 ..FuchsiaInstallPlan::default()
908 };
909 assert_matches!(
910 installer.perform_install(&plan, None).await.1.as_slice(),
911 &[AppInstallResult::Failed(FuchsiaInstallError::Connect(_))]
912 );
913 }
914
915 #[fuchsia::test(allow_stalls = false)]
916 async fn test_reboot() {
917 let mut installer = new_installer();
918 let (reboot_controller, mut stream) =
919 fidl::endpoints::create_proxy_and_stream::<RebootControllerMarker>();
920 installer.reboot_controller = Some(reboot_controller);
921
922 {
923 let mut reboot_future = installer.perform_reboot();
924 assert_matches!(
925 TestExecutor::poll_until_stalled(&mut reboot_future).await,
926 Poll::Pending
927 );
928 TestExecutor::advance_to(TestExecutor::next_timer().unwrap()).await;
929 assert_matches!(reboot_future.await, Err(_));
930 }
931
932 assert_matches!(installer.reboot_controller, None);
933 assert_matches!(stream.next().await, Some(Ok(RebootControllerRequest::Unblock { .. })));
934 assert_matches!(stream.next().await, None);
935 }
936
937 #[fasync::run_singlethreaded(test)]
938 async fn test_simple_response() {
939 let request_params = RequestParams::default();
940 let request_metadata = RequestMetadata {
941 request_body: vec![4, 5, 6],
942 public_key_id: 7_u64,
943 nonce: [8_u8; 32].into(),
944 };
945 let signature = Some(vec![13, 14, 15]);
946 let mut update_check = UpdateCheck::ok([TEST_URL_BASE]);
947 update_check.manifest = Some(Manifest {
948 packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
949 ..Manifest::default()
950 });
951 let response = Response {
952 apps: vec![App {
953 update_check: Some(update_check),
954 id: "system_id".into(),
955 ..App::default()
956 }],
957 ..Response::default()
958 };
959
960 let install_plan = new_installer()
961 .try_create_install_plan(
962 &request_params,
963 Some(&request_metadata),
964 &response,
965 vec![1, 2, 3],
966 signature,
967 )
968 .await
969 .unwrap();
970 assert_eq!(
971 install_plan.update_package_urls,
972 vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
973 );
974 assert_eq!(install_plan.install_source, request_params.source);
975 assert!(!install_plan.urgent_update);
976 assert_eq!(install_plan.omaha_response, vec![1, 2, 3]);
977 assert_eq!(
978 install_plan.request_metadata,
979 Some(RequestMetadata {
980 request_body: vec![4, 5, 6],
981 public_key_id: 7_u64,
982 nonce: [8_u8; 32].into(),
983 })
984 );
985 assert_eq!(install_plan.ecdsa_signature, Some(vec![13, 14, 15]));
986 }
987
988 #[fasync::run_singlethreaded(test)]
989 async fn test_no_app() {
990 let request_params = RequestParams::default();
991 let request_metadata = None;
992 let signature = None;
993 let response = Response::default();
994
995 assert_matches!(
996 new_installer()
997 .try_create_install_plan(
998 &request_params,
999 request_metadata,
1000 &response,
1001 vec![],
1002 signature
1003 )
1004 .await,
1005 Err(FuchsiaInstallError::Failure(_))
1006 );
1007 }
1008
1009 #[fasync::run_singlethreaded(test)]
1010 async fn test_multiple_app() {
1011 let request_params = RequestParams::default();
1012 let request_metadata = None;
1013 let signature = None;
1014
1015 let system_app = App {
1016 update_check: Some(UpdateCheck {
1017 manifest: Some(Manifest {
1018 packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1019 ..Manifest::default()
1020 }),
1021 ..UpdateCheck::ok([TEST_URL_BASE])
1022 }),
1023 id: "system_id".into(),
1024 ..App::default()
1025 };
1026 let response = Response {
1027 apps: vec![
1028 system_app,
1029 App { update_check: Some(UpdateCheck::no_update()), ..App::default() },
1030 ],
1031 ..Response::default()
1032 };
1033
1034 let install_plan = new_installer()
1035 .try_create_install_plan(
1036 &request_params,
1037 request_metadata,
1038 &response,
1039 vec![],
1040 signature,
1041 )
1042 .await
1043 .unwrap();
1044 assert_eq!(
1045 install_plan.update_package_urls,
1046 vec![UpdatePackageUrl::System(TEST_URL.parse().unwrap())],
1047 );
1048 assert_eq!(install_plan.install_source, request_params.source);
1049 }
1050
1051 #[fasync::run_singlethreaded(test)]
1052 async fn test_multiple_package_updates() {
1053 let request_params = RequestParams::default();
1054 let request_metadata = None;
1055 let signature = None;
1056
1057 let system_app = App {
1058 update_check: Some(UpdateCheck::no_update()),
1059 id: "system_id".into(),
1060 ..App::default()
1061 };
1062 let package1_app = App {
1063 update_check: Some(UpdateCheck {
1064 manifest: Some(Manifest {
1065 packages: Packages::new(vec![Package::with_name(
1066 "package1?hash=0000000000000000000000000000000000000000000000000000000000000000",
1067 )]),
1068 ..Manifest::default()
1069 }),
1070 ..UpdateCheck::ok([TEST_URL_BASE])
1071 }),
1072 id: "package1_id".into(),
1073 ..App::default()
1074 };
1075 let package2_app = App {
1076 update_check: Some(UpdateCheck::no_update()),
1077 id: "package2_id".into(),
1078 ..App::default()
1079 };
1080 let package3_app = App {
1081 update_check: Some(UpdateCheck {
1082 manifest: Some(Manifest {
1083 packages: Packages::new(vec![Package::with_name(
1084 "package3?hash=0000000000000000000000000000000000000000000000000000000000000000",
1085 )]),
1086 ..Manifest::default()
1087 }),
1088 ..UpdateCheck::ok([TEST_URL_BASE])
1089 }),
1090 id: "package3_id".into(),
1091 ..App::default()
1092 };
1093 let response = Response {
1094 apps: vec![system_app, package1_app, package2_app, package3_app],
1095 ..Response::default()
1096 };
1097
1098 let install_plan = new_installer()
1099 .try_create_install_plan(
1100 &request_params,
1101 request_metadata,
1102 &response,
1103 vec![],
1104 signature,
1105 )
1106 .await
1107 .unwrap();
1108 assert_eq!(
1109 install_plan.update_package_urls,
1110 vec![
1111 UpdatePackageUrl::Package(format!("{TEST_URL_BASE}package1?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap()),
1112 UpdatePackageUrl::Package(format!("{TEST_URL_BASE}package3?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap())
1113 ]
1114 );
1115 assert_eq!(install_plan.install_source, request_params.source);
1116 }
1117
1118 #[fasync::run_singlethreaded(test)]
1119 async fn test_mixed_update() {
1120 let request_params = RequestParams::default();
1121 let request_metadata = None;
1122 let signature = None;
1123 let system_app = App {
1124 update_check: Some(UpdateCheck {
1125 manifest: Some(Manifest {
1126 packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1127 ..Manifest::default()
1128 }),
1129 ..UpdateCheck::ok([TEST_URL_BASE])
1130 }),
1131 id: "system_id".into(),
1132 ..App::default()
1133 };
1134 let package_app = App {
1135 update_check: Some(UpdateCheck {
1136 manifest: Some(Manifest {
1137 packages: Packages::new(vec![Package::with_name(
1138 "some-package?hash=0000000000000000000000000000000000000000000000000000000000000000",
1139 )]),
1140 ..Manifest::default()
1141 }),
1142 ..UpdateCheck::ok([TEST_URL_BASE])
1143 }),
1144 id: "package_id".into(),
1145 ..App::default()
1146 };
1147 let response = Response { apps: vec![package_app, system_app], ..Response::default() };
1148
1149 let install_plan = new_installer()
1150 .try_create_install_plan(
1151 &request_params,
1152 request_metadata,
1153 &response,
1154 vec![],
1155 signature,
1156 )
1157 .await
1158 .unwrap();
1159 assert_eq!(
1160 install_plan.update_package_urls,
1161 vec![
1162 UpdatePackageUrl::Package(format!("{TEST_URL_BASE}some-package?hash=0000000000000000000000000000000000000000000000000000000000000000").parse().unwrap()),
1163 UpdatePackageUrl::System(TEST_URL.parse().unwrap())
1164 ],
1165 );
1166 assert_eq!(install_plan.install_source, request_params.source);
1167 }
1168
1169 #[fasync::run_singlethreaded(test)]
1170 async fn test_no_update_check() {
1171 let request_params = RequestParams::default();
1172 let request_metadata = None;
1173 let signature = None;
1174 let response = Response {
1175 apps: vec![App { id: "system_id".into(), ..App::default() }],
1176 ..Response::default()
1177 };
1178
1179 assert_matches!(
1180 new_installer()
1181 .try_create_install_plan(
1182 &request_params,
1183 request_metadata,
1184 &response,
1185 vec![],
1186 signature
1187 )
1188 .await,
1189 Err(FuchsiaInstallError::Failure(_))
1190 );
1191 }
1192
1193 #[fasync::run_singlethreaded(test)]
1194 async fn test_no_urls() {
1195 let request_params = RequestParams::default();
1196 let request_metadata = None;
1197 let signature = None;
1198 let response = Response {
1199 apps: vec![App {
1200 update_check: Some(UpdateCheck::default()),
1201 id: "system_id".into(),
1202 ..App::default()
1203 }],
1204 ..Response::default()
1205 };
1206
1207 assert_matches!(
1208 new_installer()
1209 .try_create_install_plan(
1210 &request_params,
1211 request_metadata,
1212 &response,
1213 vec![],
1214 signature
1215 )
1216 .await,
1217 Err(FuchsiaInstallError::Failure(_))
1218 );
1219 }
1220
1221 #[fasync::run_singlethreaded(test)]
1222 async fn test_app_error_status() {
1223 let request_params = RequestParams::default();
1224 let request_metadata = None;
1225 let signature = None;
1226 let response = Response {
1227 apps: vec![App {
1228 status: OmahaStatus::Error("error-unknownApplication".to_string()),
1229 ..App::default()
1230 }],
1231 ..Response::default()
1232 };
1233
1234 assert_matches!(
1235 new_installer()
1236 .try_create_install_plan(
1237 &request_params,
1238 request_metadata,
1239 &response,
1240 vec![],
1241 signature
1242 )
1243 .await,
1244 Err(FuchsiaInstallError::Failure(_))
1245 );
1246 }
1247
1248 #[fasync::run_singlethreaded(test)]
1249 async fn test_no_update() {
1250 let request_params = RequestParams::default();
1251 let request_metadata = None;
1252 let signature = None;
1253 let response = Response {
1254 apps: vec![App { update_check: Some(UpdateCheck::no_update()), ..App::default() }],
1255 ..Response::default()
1256 };
1257
1258 assert_matches!(
1259 new_installer()
1260 .try_create_install_plan(
1261 &request_params,
1262 request_metadata,
1263 &response,
1264 vec![],
1265 signature
1266 )
1267 .await,
1268 Err(FuchsiaInstallError::Failure(_))
1269 );
1270 }
1271
1272 #[fasync::run_singlethreaded(test)]
1273 async fn test_invalid_url() {
1274 let request_params = RequestParams::default();
1275 let request_metadata = None;
1276 let signature = None;
1277 let response = Response {
1278 apps: vec![App {
1279 update_check: Some(UpdateCheck::ok(["invalid-url"])),
1280 id: "system_id".into(),
1281 ..App::default()
1282 }],
1283 ..Response::default()
1284 };
1285
1286 assert_matches!(
1287 new_installer()
1288 .try_create_install_plan(
1289 &request_params,
1290 request_metadata,
1291 &response,
1292 vec![],
1293 signature
1294 )
1295 .await,
1296 Err(FuchsiaInstallError::Failure(_))
1297 );
1298 }
1299
1300 #[fasync::run_singlethreaded(test)]
1301 async fn test_no_manifest() {
1302 let request_params = RequestParams::default();
1303 let request_metadata = None;
1304 let signature = None;
1305 let response = Response {
1306 apps: vec![App {
1307 update_check: Some(UpdateCheck::ok([TEST_URL_BASE])),
1308 id: "system_id".into(),
1309 ..App::default()
1310 }],
1311 ..Response::default()
1312 };
1313
1314 assert_matches!(
1315 new_installer()
1316 .try_create_install_plan(
1317 &request_params,
1318 request_metadata,
1319 &response,
1320 vec![],
1321 signature
1322 )
1323 .await,
1324 Err(FuchsiaInstallError::Failure(_))
1325 );
1326 }
1327
1328 #[fasync::run_singlethreaded(test)]
1329 async fn test_urgent_update_attribute_true() {
1330 let request_params = RequestParams::default();
1331 let request_metadata = None;
1332 let signature = None;
1333 let mut update_check = UpdateCheck::ok([TEST_URL_BASE]);
1334
1335 let mut extra_attributes = Map::new();
1336 extra_attributes.insert("_urgent_update".to_string(), "true".into());
1337 update_check.extra_attributes = extra_attributes;
1338
1339 update_check.manifest = Some(Manifest {
1340 packages: Packages::new(vec![Package::with_name(TEST_PACKAGE_NAME)]),
1341 ..Manifest::default()
1342 });
1343 let response = Response {
1344 apps: vec![App {
1345 update_check: Some(update_check),
1346 id: "system_id".into(),
1347 ..App::default()
1348 }],
1349 ..Response::default()
1350 };
1351
1352 let install_plan = new_installer()
1353 .try_create_install_plan(
1354 &request_params,
1355 request_metadata,
1356 &response,
1357 vec![],
1358 signature,
1359 )
1360 .await
1361 .unwrap();
1362
1363 assert!(install_plan.urgent_update);
1364 }
1365}