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 },
52 &self.proxy,
53 Some(reboot_controller_server_end),
54 )
55 .await
56 .context("starting system update")
57 }
58
59 pub async fn install_update(
64 &mut self,
65 update_package: Option<&http::Uri>,
66 ) -> Result<(), Error> {
67 let attempt = self.start_update(update_package).await?;
68
69 let () = Self::monitor_update_attempt(attempt).await.context("monitoring installation")?;
70
71 let () = Self::activate_installed_slot(&self.paver_proxy)
72 .await
73 .context("activating installed slot")?;
74
75 Ok(())
76 }
77
78 async fn monitor_update_attempt(mut attempt: UpdateAttempt) -> Result<(), Error> {
79 while let Some(state) = attempt.try_next().await.context("fetching next update state")? {
80 log::info!("Install: {:?}", state);
81 if state.is_success() {
82 return Ok(());
83 } else if state.is_failure() {
84 return Err(anyhow!("update attempt failed in state {:?}", state));
85 }
86 }
87
88 Err(anyhow!("unexpected end of update attempt"))
89 }
90
91 async fn activate_installed_slot(paver: &PaverProxy) -> Result<(), Error> {
92 let (boot_manager, remote) = fidl::endpoints::create_proxy::<BootManagerMarker>();
93
94 paver.find_boot_manager(remote).context("finding boot manager")?;
95
96 let result = boot_manager.query_active_configuration().await;
97 if let Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. }) =
98 result
99 {
100 log::info!("ABR not supported, not configuring slots.");
102 return Ok(());
103 }
104 let result = result?;
105 if result.is_ok() {
106 return Ok(());
108 }
109
110 zx::Status::ok(
114 boot_manager
115 .set_configuration_active(Configuration::A)
116 .await
117 .context("Sending set active configuration request")?,
118 )
119 .context("Setting A to active configuration")?;
120 Ok(())
121 }
122}
123
124#[cfg(test)]
125pub(crate) mod for_tests {
126 use super::*;
127 use crate::resolver::for_tests::{EMPTY_REPO_PATH, ResolverForTest};
128 use blobfs_ramdisk::BlobfsRamdisk;
129 use fidl_fuchsia_metrics as fmetrics;
130 use fidl_fuchsia_paver::PaverRequestStream;
131 use fuchsia_async as fasync;
132 use fuchsia_component_test::{
133 Capability, ChildOptions, DirectoryContents, RealmBuilder, RealmInstance, Ref, Route,
134 };
135 use fuchsia_merkle::Hash;
136 use fuchsia_pkg_testing::serve::ServedRepository;
137 use fuchsia_pkg_testing::{Package, RepositoryBuilder, SystemImageBuilder};
138 use mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent};
139 use std::collections::BTreeSet;
140 use std::sync::Arc;
141
142 pub const TEST_REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
143 pub struct UpdaterBuilder {
144 paver_builder: MockPaverServiceBuilder,
145 packages: Vec<Package>,
146 fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
148 recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
150 repo_url: fuchsia_url::RepositoryUrl,
151 }
152
153 impl UpdaterBuilder {
154 pub async fn new() -> UpdaterBuilder {
157 UpdaterBuilder {
158 paver_builder: MockPaverServiceBuilder::new(),
159 packages: vec![SystemImageBuilder::new().build().await],
160 fuchsia_image: None,
161 recovery_image: None,
162 repo_url: TEST_REPO_URL.parse().unwrap(),
163 }
164 }
165
166 pub fn add_package(mut self, package: Package) -> Self {
168 self.packages.push(package);
169 self
170 }
171
172 pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
174 assert_eq!(self.fuchsia_image, None);
175 self.fuchsia_image = Some((zbi, vbmeta));
176 self
177 }
178
179 pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
181 assert_eq!(self.recovery_image, None);
182 self.recovery_image = Some((zbi, vbmeta));
183 self
184 }
185
186 pub fn paver<F>(mut self, f: F) -> Self
188 where
189 F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
190 {
191 self.paver_builder = f(self.paver_builder);
192 self
193 }
194
195 pub fn repo_url(mut self, url: &str) -> Self {
196 self.repo_url = url.parse().expect("Valid URL supplied to repo_url()");
197 self
198 }
199
200 fn serve_mock_paver(stream: PaverRequestStream, paver: Arc<MockPaverService>) {
201 let paver_clone = Arc::clone(&paver);
202 fasync::Task::spawn(
203 Arc::clone(&paver_clone)
204 .run_paver_service(stream)
205 .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
206 )
207 .detach();
208 }
209
210 async fn run_mock_paver(
211 handles: fuchsia_component_test::LocalComponentHandles,
212 paver: Arc<MockPaverService>,
213 ) -> Result<(), Error> {
214 let mut fs = fuchsia_component::server::ServiceFs::new();
215 fs.dir("svc")
216 .add_fidl_service(move |stream| Self::serve_mock_paver(stream, Arc::clone(&paver)));
217 fs.serve_connection(handles.outgoing_dir)?;
218 let () = fs.for_each_concurrent(None, |req| async move { req }).await;
219 Ok(())
220 }
221
222 pub async fn build(self) -> UpdaterForTest {
226 let mut update = fuchsia_pkg_testing::UpdatePackageBuilder::new(self.repo_url.clone())
227 .packages(
228 self.packages
229 .iter()
230 .map(|p| {
231 fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
232 self.repo_url.clone(),
233 p.name().clone(),
234 None,
235 *p.hash(),
236 )
237 })
238 .collect::<Vec<_>>(),
239 );
240 if let Some((zbi, vbmeta)) = self.fuchsia_image {
241 update = update.fuchsia_image(zbi, vbmeta);
242 }
243 if let Some((zbi, vbmeta)) = self.recovery_image {
244 update = update.recovery_image(zbi, vbmeta);
245 }
246 let (update, images) = update.build().await;
247
248 let expected_blobfs_contents = self
250 .packages
251 .iter()
252 .chain([update.as_package()])
253 .flat_map(|p| p.list_blobs())
254 .collect();
255
256 let repo = Arc::new(
257 self.packages
258 .iter()
259 .chain([update.as_package(), &images])
260 .fold(
261 RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
262 .add_package(update.as_package()),
263 |repo, package| repo.add_package(package),
264 )
265 .build()
266 .await
267 .expect("Building repo"),
268 );
269
270 let realm_builder = RealmBuilder::new().await.unwrap();
271 let blobfs = BlobfsRamdisk::start().await.context("starting blobfs").unwrap();
272
273 let served_repo = Arc::new(Arc::clone(&repo).server().start().unwrap());
274
275 let resolver_realm = ResolverForTest::realm_setup(
276 &realm_builder,
277 Arc::clone(&served_repo),
278 self.repo_url.clone(),
279 &blobfs,
280 )
281 .await
282 .unwrap();
283
284 let system_updater = realm_builder
285 .add_child("system-updater", "#meta/system-updater.cm", ChildOptions::new())
286 .await
287 .unwrap();
288
289 let service_reflector = realm_builder
290 .add_local_child(
291 "system_updater_service_reflector",
292 move |handles| {
293 let mut fs = fuchsia_component::server::ServiceFs::new();
294 fs.dir("svc").add_fidl_service(move |stream| {
299 fasync::Task::spawn(
300 Arc::new(mock_metrics::MockMetricEventLoggerFactory::new())
301 .run_logger_factory(stream),
302 )
303 .detach()
304 });
305 async move {
306 fs.serve_connection(handles.outgoing_dir).unwrap();
307 let () = fs.collect().await;
308 Ok(())
309 }
310 .boxed()
311 },
312 ChildOptions::new(),
313 )
314 .await
315 .unwrap();
316
317 realm_builder
318 .add_route(
319 Route::new()
320 .capability(
321 Capability::protocol::<fmetrics::MetricEventLoggerFactoryMarker>(),
322 )
323 .from(&service_reflector)
324 .to(&system_updater),
325 )
326 .await
327 .unwrap();
328
329 let paver = Arc::new(self.paver_builder.build());
331 let paver_clone = Arc::clone(&paver);
332 let mock_paver = realm_builder
333 .add_local_child(
334 "paver",
335 move |handles| Box::pin(Self::run_mock_paver(handles, Arc::clone(&paver))),
336 ChildOptions::new(),
337 )
338 .await
339 .unwrap();
340
341 realm_builder
342 .add_route(
343 Route::new()
344 .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
345 .from(&mock_paver)
346 .to(&system_updater),
347 )
348 .await
349 .unwrap();
350 realm_builder
351 .add_route(
352 Route::new()
353 .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
354 .from(&mock_paver)
355 .to(Ref::parent()),
356 )
357 .await
358 .unwrap();
359
360 realm_builder
362 .read_only_directory(
363 "build-info",
364 vec![&system_updater],
365 DirectoryContents::new().add_file("board", "test".as_bytes()),
366 )
367 .await
368 .unwrap();
369
370 realm_builder
372 .add_route(
373 Route::new()
374 .capability(
375 Capability::protocol_by_name("fuchsia.pkg.PackageResolver-ota")
376 .as_("fuchsia.pkg.PackageResolver"),
377 )
378 .from(&resolver_realm.resolver)
379 .to(&system_updater),
380 )
381 .await
382 .unwrap();
383
384 realm_builder
385 .add_route(
386 Route::new()
387 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
388 .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
389 .capability(Capability::protocol_by_name(
390 "fuchsia.pkg.garbagecollector.Manager",
391 ))
392 .from(&resolver_realm.cache)
393 .to(&system_updater),
394 )
395 .await
396 .unwrap();
397 realm_builder
398 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
399 name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
400 value: Vec::<String>::new().into(),
401 }))
402 .await
403 .unwrap();
404 realm_builder
405 .add_route(
406 Route::new()
407 .capability(Capability::configuration(
408 "fuchsia.system-updater.ManifestPublicKeys",
409 ))
410 .from(Ref::self_())
411 .to(&system_updater),
412 )
413 .await
414 .unwrap();
415 realm_builder
416 .add_route(
417 Route::new()
418 .capability(Capability::configuration(
419 "fuchsia.system-updater.ConcurrentBlobFetches",
420 ))
421 .capability(Capability::configuration(
422 "fuchsia.system-updater.ConcurrentPackageResolves",
423 ))
424 .capability(Capability::configuration(
425 "fuchsia.system-updater.VerifyExistingBlobs",
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_async as fasync;
559 use fuchsia_pkg_testing::PackageBuilder;
560 use mock_paver::PaverEvent;
561
562 #[fasync::run_singlethreaded(test)]
563 pub async fn test_updater() {
564 let data = "hello world!".as_bytes();
565 let test_package = PackageBuilder::new("test_package")
566 .add_resource_at("bin/hello", "this is a test".as_bytes())
567 .add_resource_at("data/file", "this is a file".as_bytes())
568 .add_resource_at("meta/test_package.cm", "{}".as_bytes())
569 .build()
570 .await
571 .expect("Building test_package");
572 let updater = UpdaterBuilder::new()
573 .await
574 .paver(|p| {
575 p.boot_manager_close_with_epitaph(zx::Status::NOT_SUPPORTED)
577 })
578 .add_package(test_package)
579 .fuchsia_image(data.to_vec(), Some(data.to_vec()))
580 .recovery_image(data.to_vec(), Some(data.to_vec()));
581 let result = updater.build_and_run().await;
582
583 assert_eq!(
584 result.paver_events,
585 vec![
586 PaverEvent::WriteAsset {
587 configuration: Configuration::A,
588 asset: Asset::Kernel,
589 payload: data.to_vec()
590 },
591 PaverEvent::WriteAsset {
592 configuration: Configuration::B,
593 asset: Asset::Kernel,
594 payload: data.to_vec()
595 },
596 PaverEvent::WriteAsset {
597 configuration: Configuration::A,
598 asset: Asset::VerifiedBootMetadata,
599 payload: data.to_vec()
600 },
601 PaverEvent::WriteAsset {
602 configuration: Configuration::B,
603 asset: Asset::VerifiedBootMetadata,
604 payload: data.to_vec()
605 },
606 PaverEvent::DataSinkFlush,
608 ]
609 );
610
611 let () = result.verify_packages().await;
612 }
613}