1use anyhow::{Context, Error, anyhow};
5use fidl_fuchsia_paver::{BootManagerMarker, Configuration, PaverMarker, PaverProxy};
6use fidl_fuchsia_update_installer::{InstallerMarker, InstallerProxy, RebootControllerMarker};
7use fidl_fuchsia_update_installer_ext::options::{Initiator, Options};
8use fidl_fuchsia_update_installer_ext::{UpdateAttempt, start_update};
9
10use futures::prelude::*;
11
12pub const DEFAULT_UPDATE_PACKAGE_URL: &str = "fuchsia-pkg://fuchsia.com/update";
13
14pub struct Updater {
15 proxy: InstallerProxy,
16 paver_proxy: PaverProxy,
17}
18
19impl Updater {
20 pub fn new_with_proxies(proxy: InstallerProxy, paver_proxy: PaverProxy) -> Self {
21 Self { proxy, paver_proxy }
22 }
23
24 pub fn new() -> Result<Self, Error> {
25 Ok(Self::new_with_proxies(
26 fuchsia_component::client::connect_to_protocol::<InstallerMarker>()?,
27 fuchsia_component::client::connect_to_protocol::<PaverMarker>()?,
28 ))
29 }
30
31 pub async fn start_update(
32 &mut self,
33 update_package: Option<&http::Uri>,
34 ) -> Result<UpdateAttempt, Error> {
35 let update_package = match update_package {
36 Some(url) => url.to_owned(),
37 None => DEFAULT_UPDATE_PACKAGE_URL.parse().unwrap(),
38 };
39
40 let (reboot_controller, reboot_controller_server_end) =
41 fidl::endpoints::create_proxy::<RebootControllerMarker>();
42 let () = reboot_controller.detach().context("disabling automatic reboot")?;
43
44 start_update(
45 &update_package,
46 Options {
47 initiator: Initiator::User,
48 allow_attach_to_existing_attempt: false,
49 should_write_recovery: false,
50 manifest_range: None,
51 manifest_headers: vec![],
52 should_commit: true,
53 },
54 &self.proxy,
55 Some(reboot_controller_server_end),
56 )
57 .await
58 .context("starting system update")
59 }
60
61 pub async fn install_update(
66 &mut self,
67 update_package: Option<&http::Uri>,
68 ) -> Result<(), Error> {
69 let attempt = self.start_update(update_package).await?;
70
71 let () = Self::monitor_update_attempt(attempt).await.context("monitoring installation")?;
72
73 let () = Self::activate_installed_slot(&self.paver_proxy)
74 .await
75 .context("activating installed slot")?;
76
77 Ok(())
78 }
79
80 async fn monitor_update_attempt(mut attempt: UpdateAttempt) -> Result<(), Error> {
81 while let Some(state) = attempt.try_next().await.context("fetching next update state")? {
82 log::info!("Install: {:?}", state);
83 if state.is_success() {
84 return Ok(());
85 } else if state.is_failure() {
86 return Err(anyhow!("update attempt failed in state {:?}", state));
87 }
88 }
89
90 Err(anyhow!("unexpected end of update attempt"))
91 }
92
93 async fn activate_installed_slot(paver: &PaverProxy) -> Result<(), Error> {
94 let (boot_manager, remote) = fidl::endpoints::create_proxy::<BootManagerMarker>();
95
96 paver.find_boot_manager(remote).context("finding boot manager")?;
97
98 let result = boot_manager.query_active_configuration().await;
99 if let Err(fidl::Error::ClientChannelClosed { epitaph, .. }) = result
100 && epitaph == zx::Status::NOT_SUPPORTED
101 {
102 log::info!("ABR not supported, not configuring slots.");
104 return Ok(());
105 }
106 let result = result?;
107 if result.is_ok() {
108 return Ok(());
110 }
111
112 zx::Status::ok(
116 boot_manager
117 .set_configuration_active(Configuration::A)
118 .await
119 .context("Sending set active configuration request")?,
120 )
121 .context("Setting A to active configuration")?;
122 Ok(())
123 }
124}
125
126#[cfg(test)]
127pub(crate) mod for_tests {
128 use super::*;
129 use crate::resolver::for_tests::{EMPTY_REPO_PATH, ResolverForTest};
130 use blobfs_ramdisk::BlobfsRamdisk;
131 use fidl_fuchsia_metrics as fmetrics;
132 use fidl_fuchsia_paver::PaverRequestStream;
133 use fuchsia_async as fasync;
134 use fuchsia_component_test::{
135 Capability, ChildOptions, DirectoryContents, RealmBuilder, RealmInstance, Ref, Route,
136 };
137 use fuchsia_merkle::Hash;
138 use fuchsia_pkg_testing::serve::ServedRepository;
139 use fuchsia_pkg_testing::{Package, RepositoryBuilder, SystemImageBuilder};
140 use mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent};
141 use std::collections::BTreeSet;
142 use std::sync::Arc;
143
144 pub const TEST_REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
145 pub struct UpdaterBuilder {
146 paver_builder: MockPaverServiceBuilder,
147 packages: Vec<Package>,
148 fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
150 recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
152 repo_url: fuchsia_url::RepositoryUrl,
153 }
154
155 impl UpdaterBuilder {
156 pub async fn new() -> UpdaterBuilder {
159 UpdaterBuilder {
160 paver_builder: MockPaverServiceBuilder::new(),
161 packages: vec![SystemImageBuilder::new().build().await],
162 fuchsia_image: None,
163 recovery_image: None,
164 repo_url: TEST_REPO_URL.parse().unwrap(),
165 }
166 }
167
168 pub fn add_package(mut self, package: Package) -> Self {
170 self.packages.push(package);
171 self
172 }
173
174 pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
176 assert_eq!(self.fuchsia_image, None);
177 self.fuchsia_image = Some((zbi, vbmeta));
178 self
179 }
180
181 pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
183 assert_eq!(self.recovery_image, None);
184 self.recovery_image = Some((zbi, vbmeta));
185 self
186 }
187
188 pub fn paver<F>(mut self, f: F) -> Self
190 where
191 F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
192 {
193 self.paver_builder = f(self.paver_builder);
194 self
195 }
196
197 pub fn repo_url(mut self, url: &str) -> Self {
198 self.repo_url = url.parse().expect("Valid URL supplied to repo_url()");
199 self
200 }
201
202 fn serve_mock_paver(stream: PaverRequestStream, paver: Arc<MockPaverService>) {
203 let paver_clone = Arc::clone(&paver);
204 fasync::Task::spawn(
205 Arc::clone(&paver_clone)
206 .run_paver_service(stream)
207 .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
208 )
209 .detach();
210 }
211
212 async fn run_mock_paver(
213 handles: fuchsia_component_test::LocalComponentHandles,
214 paver: Arc<MockPaverService>,
215 ) -> Result<(), Error> {
216 let mut fs = fuchsia_component::server::ServiceFs::new();
217 fs.dir("svc")
218 .add_fidl_service(move |stream| Self::serve_mock_paver(stream, Arc::clone(&paver)));
219 fs.serve_connection(handles.outgoing_dir)?;
220 let () = fs.for_each_concurrent(None, |req| async move { req }).await;
221 Ok(())
222 }
223
224 pub async fn build(self) -> UpdaterForTest {
228 let mut update = fuchsia_pkg_testing::UpdatePackageBuilder::new(self.repo_url.clone())
229 .packages(
230 self.packages
231 .iter()
232 .map(|p| {
233 fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
234 self.repo_url.clone(),
235 p.name().clone(),
236 None,
237 *p.hash(),
238 )
239 })
240 .collect::<Vec<_>>(),
241 );
242 if let Some((zbi, vbmeta)) = self.fuchsia_image {
243 update = update.fuchsia_image(zbi, vbmeta);
244 }
245 if let Some((zbi, vbmeta)) = self.recovery_image {
246 update = update.recovery_image(zbi, vbmeta);
247 }
248 let (update, images) = update.build().await;
249
250 let expected_blobfs_contents = self
252 .packages
253 .iter()
254 .chain([update.as_package()])
255 .flat_map(|p| p.list_blobs())
256 .collect();
257
258 let repo = Arc::new(
259 self.packages
260 .iter()
261 .chain([update.as_package(), &images])
262 .fold(
263 RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
264 .add_package(update.as_package()),
265 |repo, package| repo.add_package(package),
266 )
267 .build()
268 .await
269 .expect("Building repo"),
270 );
271
272 let realm_builder = RealmBuilder::new().await.unwrap();
273 let blobfs = BlobfsRamdisk::start().await.context("starting blobfs").unwrap();
274
275 let served_repo = Arc::new(Arc::clone(&repo).server().start().unwrap());
276
277 let resolver_realm = ResolverForTest::realm_setup(
278 &realm_builder,
279 Arc::clone(&served_repo),
280 self.repo_url.clone(),
281 &blobfs,
282 )
283 .await
284 .unwrap();
285
286 let system_updater = realm_builder
287 .add_child("system-updater", "#meta/system-updater.cm", ChildOptions::new())
288 .await
289 .unwrap();
290
291 let service_reflector = realm_builder
292 .add_local_child(
293 "system_updater_service_reflector",
294 move |handles| {
295 let mut fs = fuchsia_component::server::ServiceFs::new();
296 fs.dir("svc").add_fidl_service(move |stream| {
301 fasync::Task::spawn(
302 Arc::new(mock_metrics::MockMetricEventLoggerFactory::new())
303 .run_logger_factory(stream),
304 )
305 .detach()
306 });
307 async move {
308 fs.serve_connection(handles.outgoing_dir).unwrap();
309 let () = fs.collect().await;
310 Ok(())
311 }
312 .boxed()
313 },
314 ChildOptions::new(),
315 )
316 .await
317 .unwrap();
318
319 realm_builder
320 .add_route(
321 Route::new()
322 .capability(
323 Capability::protocol::<fmetrics::MetricEventLoggerFactoryMarker>(),
324 )
325 .from(&service_reflector)
326 .to(&system_updater),
327 )
328 .await
329 .unwrap();
330
331 let paver = Arc::new(self.paver_builder.build());
333 let paver_clone = Arc::clone(&paver);
334 let mock_paver = realm_builder
335 .add_local_child(
336 "paver",
337 move |handles| Box::pin(Self::run_mock_paver(handles, Arc::clone(&paver))),
338 ChildOptions::new(),
339 )
340 .await
341 .unwrap();
342
343 realm_builder
344 .add_route(
345 Route::new()
346 .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
347 .from(&mock_paver)
348 .to(&system_updater),
349 )
350 .await
351 .unwrap();
352 realm_builder
353 .add_route(
354 Route::new()
355 .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
356 .from(&mock_paver)
357 .to(Ref::parent()),
358 )
359 .await
360 .unwrap();
361
362 realm_builder
364 .read_only_directory(
365 "build-info",
366 vec![&system_updater],
367 DirectoryContents::new().add_file("board", "test".as_bytes()),
368 )
369 .await
370 .unwrap();
371
372 realm_builder
374 .add_route(
375 Route::new()
376 .capability(
377 Capability::protocol_by_name("fuchsia.pkg.PackageResolver-ota")
378 .as_("fuchsia.pkg.PackageResolver"),
379 )
380 .from(&resolver_realm.resolver)
381 .to(&system_updater),
382 )
383 .await
384 .unwrap();
385
386 realm_builder
387 .add_route(
388 Route::new()
389 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
390 .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
391 .capability(Capability::protocol_by_name(
392 "fuchsia.pkg.garbagecollector.Manager",
393 ))
394 .from(&resolver_realm.cache)
395 .to(&system_updater),
396 )
397 .await
398 .unwrap();
399 realm_builder
400 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
401 name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
402 value: Vec::<String>::new().into(),
403 }))
404 .await
405 .unwrap();
406 realm_builder
407 .add_route(
408 Route::new()
409 .capability(Capability::configuration(
410 "fuchsia.system-updater.ManifestPublicKeys",
411 ))
412 .from(Ref::self_())
413 .to(&system_updater),
414 )
415 .await
416 .unwrap();
417 realm_builder
418 .add_route(
419 Route::new()
420 .capability(Capability::configuration(
421 "fuchsia.system-updater.ConcurrentBlobFetches",
422 ))
423 .capability(Capability::configuration(
424 "fuchsia.system-updater.ConcurrentPackageResolves",
425 ))
426 .capability(Capability::configuration(
427 "fuchsia.system-updater.ExcessiveUpdateDurationSeconds",
428 ))
429 .capability(Capability::configuration(
430 "fuchsia.system-updater.VerifyExistingBlobs",
431 ))
432 .capability(
433 Capability::protocol_by_name("fuchsia.feedback.CrashReporter")
434 .optional(),
435 )
436 .from(Ref::void())
437 .to(&system_updater),
438 )
439 .await
440 .unwrap();
441
442 realm_builder
444 .add_route(
445 Route::new()
446 .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
447 .from(Ref::parent())
448 .to(&system_updater),
449 )
450 .await
451 .unwrap();
452
453 realm_builder
455 .add_route(
456 Route::new()
457 .capability(Capability::protocol_by_name(
458 "fuchsia.update.installer.Installer",
459 ))
460 .from(&system_updater)
461 .to(Ref::parent()),
462 )
463 .await
464 .unwrap();
465
466 realm_builder
468 .add_route(
469 Route::new()
470 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
471 .from(&resolver_realm.cache)
472 .to(Ref::parent()),
473 )
474 .await
475 .unwrap();
476
477 let realm_instance = realm_builder.build().await.unwrap();
478
479 let installer_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
480 let paver_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
481
482 let updater = Updater::new_with_proxies(installer_proxy, paver_proxy);
483
484 let resolver = ResolverForTest::new(&realm_instance, blobfs, Arc::clone(&served_repo))
485 .await
486 .unwrap();
487
488 UpdaterForTest {
489 served_repo,
490 paver: paver_clone,
491 expected_blobfs_contents,
492 update_merkle_root: *update.as_package().hash(),
493 repo_url: self.repo_url,
494 updater,
495 resolver,
496 realm_instance,
497 }
498 }
499
500 #[cfg(test)]
501 pub async fn build_and_run(self) -> UpdaterResult {
502 self.build().await.run().await
503 }
504 }
505
506 pub struct UpdaterForTest {
509 #[expect(dead_code)]
510 pub served_repo: Arc<ServedRepository>,
511 pub paver: Arc<MockPaverService>,
512 pub expected_blobfs_contents: BTreeSet<Hash>,
513 pub update_merkle_root: Hash,
514 #[expect(dead_code)]
515 pub repo_url: fuchsia_url::RepositoryUrl,
516 pub resolver: ResolverForTest,
517 pub updater: Updater,
518 pub realm_instance: RealmInstance,
519 }
520
521 impl UpdaterForTest {
522 pub async fn run(mut self) -> UpdaterResult {
525 let () = self.updater.install_update(None).await.expect("installing update");
526
527 UpdaterResult {
528 paver_events: self.paver.take_events(),
529 resolver: self.resolver,
530 expected_blobfs_contents: self.expected_blobfs_contents,
531 realm_instance: self.realm_instance,
532 }
533 }
534 }
535
536 pub struct UpdaterResult {
538 pub paver_events: Vec<PaverEvent>,
540 pub resolver: ResolverForTest,
542 pub expected_blobfs_contents: BTreeSet<Hash>,
544 #[expect(dead_code)]
546 pub realm_instance: RealmInstance,
547 }
548
549 impl UpdaterResult {
550 pub async fn verify_packages(&self) {
553 let actual_contents =
556 self.resolver.cache.blobfs.list_blobs().expect("Listing blobfs blobs");
557 assert_eq!(actual_contents, self.expected_blobfs_contents);
558 }
559 }
560}
561
562#[cfg(test)]
563pub mod tests {
564 use super::for_tests::UpdaterBuilder;
565 use super::*;
566 use fidl_fuchsia_paver::Asset;
567 use fuchsia_pkg_testing::PackageBuilder;
568 use mock_paver::PaverEvent;
569
570 #[fuchsia::test]
571 pub async fn test_updater() {
572 let data = "hello world!".as_bytes();
573 let test_package = PackageBuilder::new("test_package")
574 .add_resource_at("bin/hello", "this is a test".as_bytes())
575 .add_resource_at("data/file", "this is a file".as_bytes())
576 .add_resource_at("meta/test_package.cm", "{}".as_bytes())
577 .build()
578 .await
579 .expect("Building test_package");
580 let updater = UpdaterBuilder::new()
581 .await
582 .paver(|p| {
583 p.boot_manager_close_with_epitaph(zx::Status::NOT_SUPPORTED)
585 })
586 .add_package(test_package)
587 .fuchsia_image(data.to_vec(), Some(data.to_vec()))
588 .recovery_image(data.to_vec(), Some(data.to_vec()));
589 let result = updater.build_and_run().await;
590
591 assert_eq!(
592 result.paver_events,
593 vec![
594 PaverEvent::WriteAsset {
595 configuration: Configuration::A,
596 asset: Asset::Kernel,
597 payload: data.to_vec()
598 },
599 PaverEvent::WriteAsset {
600 configuration: Configuration::B,
601 asset: Asset::Kernel,
602 payload: data.to_vec()
603 },
604 PaverEvent::WriteAsset {
605 configuration: Configuration::A,
606 asset: Asset::VerifiedBootMetadata,
607 payload: data.to_vec()
608 },
609 PaverEvent::WriteAsset {
610 configuration: Configuration::B,
611 asset: Asset::VerifiedBootMetadata,
612 payload: data.to_vec()
613 },
614 PaverEvent::DataSinkFlush,
616 ]
617 );
618
619 let () = result.verify_packages().await;
620 }
621}