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 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
381 .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
382 .capability(Capability::protocol_by_name(
383 "fuchsia.pkg.garbagecollector.Manager",
384 ))
385 .from(&resolver_realm.cache)
386 .to(&system_updater),
387 )
388 .await
389 .unwrap();
390 realm_builder
391 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
392 name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
393 value: Vec::<String>::new().into(),
394 }))
395 .await
396 .unwrap();
397 realm_builder
398 .add_route(
399 Route::new()
400 .capability(Capability::configuration(
401 "fuchsia.system-updater.ManifestPublicKeys",
402 ))
403 .from(Ref::self_())
404 .to(&system_updater),
405 )
406 .await
407 .unwrap();
408 realm_builder
409 .add_route(
410 Route::new()
411 .capability(Capability::configuration(
412 "fuchsia.system-updater.ConcurrentBlobFetches",
413 ))
414 .capability(Capability::configuration(
415 "fuchsia.system-updater.ConcurrentPackageResolves",
416 ))
417 .capability(Capability::configuration(
418 "fuchsia.system-updater.ExcessiveUpdateDurationSeconds",
419 ))
420 .capability(Capability::configuration(
421 "fuchsia.system-updater.VerifyExistingBlobs",
422 ))
423 .capability(
424 Capability::protocol_by_name("fuchsia.feedback.CrashReporter")
425 .optional(),
426 )
427 .from(Ref::void())
428 .to(&system_updater),
429 )
430 .await
431 .unwrap();
432
433 realm_builder
435 .add_route(
436 Route::new()
437 .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
438 .from(Ref::parent())
439 .to(&system_updater),
440 )
441 .await
442 .unwrap();
443
444 realm_builder
446 .add_route(
447 Route::new()
448 .capability(Capability::protocol_by_name(
449 "fuchsia.update.installer.Installer",
450 ))
451 .from(&system_updater)
452 .to(Ref::parent()),
453 )
454 .await
455 .unwrap();
456
457 realm_builder
459 .add_route(
460 Route::new()
461 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
462 .from(&resolver_realm.cache)
463 .to(Ref::parent()),
464 )
465 .await
466 .unwrap();
467
468 let realm_instance = realm_builder.build().await.unwrap();
469
470 let installer_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
471 let paver_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
472
473 let updater = Updater::new_with_proxies(installer_proxy, paver_proxy);
474
475 let resolver = ResolverForTest::new(&realm_instance, blobfs, Arc::clone(&served_repo))
476 .await
477 .unwrap();
478
479 UpdaterForTest {
480 served_repo,
481 paver: paver_clone,
482 expected_blobfs_contents,
483 update_merkle_root: *update.as_package().hash(),
484 repo_url: self.repo_url,
485 updater,
486 resolver,
487 realm_instance,
488 }
489 }
490
491 #[cfg(test)]
492 pub async fn build_and_run(self) -> UpdaterResult {
493 self.build().await.run().await
494 }
495 }
496
497 pub struct UpdaterForTest {
500 #[expect(dead_code)]
501 pub served_repo: Arc<ServedRepository>,
502 pub paver: Arc<MockPaverService>,
503 pub expected_blobfs_contents: BTreeSet<Hash>,
504 pub update_merkle_root: Hash,
505 #[expect(dead_code)]
506 pub repo_url: fuchsia_url::RepositoryUrl,
507 pub resolver: ResolverForTest,
508 pub updater: Updater,
509 pub realm_instance: RealmInstance,
510 }
511
512 impl UpdaterForTest {
513 pub async fn run(mut self) -> UpdaterResult {
516 let () = self.updater.install_update(None).await.expect("installing update");
517
518 UpdaterResult {
519 paver_events: self.paver.take_events(),
520 resolver: self.resolver,
521 expected_blobfs_contents: self.expected_blobfs_contents,
522 realm_instance: self.realm_instance,
523 }
524 }
525 }
526
527 pub struct UpdaterResult {
529 pub paver_events: Vec<PaverEvent>,
531 pub resolver: ResolverForTest,
533 pub expected_blobfs_contents: BTreeSet<Hash>,
535 #[expect(dead_code)]
537 pub realm_instance: RealmInstance,
538 }
539
540 impl UpdaterResult {
541 pub async fn verify_packages(&self) {
544 let actual_contents =
547 self.resolver.cache.blobfs.list_blobs().expect("Listing blobfs blobs");
548 assert_eq!(actual_contents, self.expected_blobfs_contents);
549 }
550 }
551}
552
553#[cfg(test)]
554pub mod tests {
555 use super::for_tests::UpdaterBuilder;
556 use super::*;
557 use fidl_fuchsia_paver::Asset;
558 use fuchsia_pkg_testing::PackageBuilder;
559 use mock_paver::PaverEvent;
560
561 #[fuchsia::test]
562 pub async fn test_updater() {
563 let data = "hello world!".as_bytes();
564 let test_package = PackageBuilder::new("test_package")
565 .add_resource_at("bin/hello", "this is a test".as_bytes())
566 .add_resource_at("data/file", "this is a file".as_bytes())
567 .add_resource_at("meta/test_package.cm", "{}".as_bytes())
568 .build()
569 .await
570 .expect("Building test_package");
571 let updater = UpdaterBuilder::new()
572 .await
573 .paver(|p| {
574 p.boot_manager_close_with_epitaph(zx::Status::NOT_SUPPORTED)
576 })
577 .add_package(test_package)
578 .fuchsia_image(data.to_vec(), Some(data.to_vec()))
579 .recovery_image(data.to_vec(), Some(data.to_vec()));
580 let result = updater.build_and_run().await;
581
582 assert_eq!(
583 result.paver_events,
584 vec![
585 PaverEvent::WriteAsset {
586 configuration: Configuration::A,
587 asset: Asset::Kernel,
588 payload: data.to_vec()
589 },
590 PaverEvent::WriteAsset {
591 configuration: Configuration::B,
592 asset: Asset::Kernel,
593 payload: data.to_vec()
594 },
595 PaverEvent::WriteAsset {
596 configuration: Configuration::A,
597 asset: Asset::VerifiedBootMetadata,
598 payload: data.to_vec()
599 },
600 PaverEvent::WriteAsset {
601 configuration: Configuration::B,
602 asset: Asset::VerifiedBootMetadata,
603 payload: data.to_vec()
604 },
605 PaverEvent::DataSinkFlush,
607 ]
608 );
609
610 let () = result.verify_packages().await;
611 }
612}