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 },
53 &self.proxy,
54 Some(reboot_controller_server_end),
55 )
56 .await
57 .context("starting system update")
58 }
59
60 pub async fn install_update(
65 &mut self,
66 update_package: Option<&http::Uri>,
67 ) -> Result<(), Error> {
68 let attempt = self.start_update(update_package).await?;
69
70 let () = Self::monitor_update_attempt(attempt).await.context("monitoring installation")?;
71
72 let () = Self::activate_installed_slot(&self.paver_proxy)
73 .await
74 .context("activating installed slot")?;
75
76 Ok(())
77 }
78
79 async fn monitor_update_attempt(mut attempt: UpdateAttempt) -> Result<(), Error> {
80 while let Some(state) = attempt.try_next().await.context("fetching next update state")? {
81 log::info!("Install: {:?}", state);
82 if state.is_success() {
83 return Ok(());
84 } else if state.is_failure() {
85 return Err(anyhow!("update attempt failed in state {:?}", state));
86 }
87 }
88
89 Err(anyhow!("unexpected end of update attempt"))
90 }
91
92 async fn activate_installed_slot(paver: &PaverProxy) -> Result<(), Error> {
93 let (boot_manager, remote) = fidl::endpoints::create_proxy::<BootManagerMarker>();
94
95 paver.find_boot_manager(remote).context("finding boot manager")?;
96
97 let result = boot_manager.query_active_configuration().await;
98 if let Err(fidl::Error::ClientChannelClosed { epitaph, .. }) = result
99 && epitaph == zx::Status::NOT_SUPPORTED
100 {
101 log::info!("ABR not supported, not configuring slots.");
103 return Ok(());
104 }
105 let result = result?;
106 if result.is_ok() {
107 return Ok(());
109 }
110
111 zx::Status::ok(
115 boot_manager
116 .set_configuration_active(Configuration::A)
117 .await
118 .context("Sending set active configuration request")?,
119 )
120 .context("Setting A to active configuration")?;
121 Ok(())
122 }
123}
124
125#[cfg(test)]
126pub(crate) mod for_tests {
127 use super::*;
128 use crate::resolver::for_tests::{EMPTY_REPO_PATH, ResolverForTest};
129 use blobfs_ramdisk::BlobfsRamdisk;
130 use fidl_fuchsia_metrics as fmetrics;
131 use fidl_fuchsia_paver::PaverRequestStream;
132 use fuchsia_async as fasync;
133 use fuchsia_component_test::{
134 Capability, ChildOptions, DirectoryContents, RealmBuilder, RealmInstance, Ref, Route,
135 };
136 use fuchsia_merkle::Hash;
137 use fuchsia_pkg_testing::serve::ServedRepository;
138 use fuchsia_pkg_testing::{Package, RepositoryBuilder, SystemImageBuilder};
139 use mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent};
140 use std::collections::BTreeSet;
141 use std::sync::Arc;
142
143 pub const TEST_REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
144 pub struct UpdaterBuilder {
145 paver_builder: MockPaverServiceBuilder,
146 packages: Vec<Package>,
147 fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
149 recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
151 repo_url: fuchsia_url::RepositoryUrl,
152 }
153
154 impl UpdaterBuilder {
155 pub async fn new() -> UpdaterBuilder {
158 UpdaterBuilder {
159 paver_builder: MockPaverServiceBuilder::new(),
160 packages: vec![SystemImageBuilder::new().build().await],
161 fuchsia_image: None,
162 recovery_image: None,
163 repo_url: TEST_REPO_URL.parse().unwrap(),
164 }
165 }
166
167 pub fn add_package(mut self, package: Package) -> Self {
169 self.packages.push(package);
170 self
171 }
172
173 pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
175 assert_eq!(self.fuchsia_image, None);
176 self.fuchsia_image = Some((zbi, vbmeta));
177 self
178 }
179
180 pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
182 assert_eq!(self.recovery_image, None);
183 self.recovery_image = Some((zbi, vbmeta));
184 self
185 }
186
187 pub fn paver<F>(mut self, f: F) -> Self
189 where
190 F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
191 {
192 self.paver_builder = f(self.paver_builder);
193 self
194 }
195
196 pub fn repo_url(mut self, url: &str) -> Self {
197 self.repo_url = url.parse().expect("Valid URL supplied to repo_url()");
198 self
199 }
200
201 fn serve_mock_paver(stream: PaverRequestStream, paver: Arc<MockPaverService>) {
202 let paver_clone = Arc::clone(&paver);
203 fasync::Task::spawn(
204 Arc::clone(&paver_clone)
205 .run_paver_service(stream)
206 .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
207 )
208 .detach();
209 }
210
211 async fn run_mock_paver(
212 handles: fuchsia_component_test::LocalComponentHandles,
213 paver: Arc<MockPaverService>,
214 ) -> Result<(), Error> {
215 let mut fs = fuchsia_component::server::ServiceFs::new();
216 fs.dir("svc")
217 .add_fidl_service(move |stream| Self::serve_mock_paver(stream, Arc::clone(&paver)));
218 fs.serve_connection(handles.outgoing_dir)?;
219 let () = fs.for_each_concurrent(None, |req| async move { req }).await;
220 Ok(())
221 }
222
223 pub async fn build(self) -> UpdaterForTest {
227 let mut update = fuchsia_pkg_testing::UpdatePackageBuilder::new(self.repo_url.clone())
228 .packages(
229 self.packages
230 .iter()
231 .map(|p| {
232 fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
233 self.repo_url.clone(),
234 p.name().clone(),
235 None,
236 *p.hash(),
237 )
238 })
239 .collect::<Vec<_>>(),
240 );
241 if let Some((zbi, vbmeta)) = self.fuchsia_image {
242 update = update.fuchsia_image(zbi, vbmeta);
243 }
244 if let Some((zbi, vbmeta)) = self.recovery_image {
245 update = update.recovery_image(zbi, vbmeta);
246 }
247 let (update, images) = update.build().await;
248
249 let expected_blobfs_contents = self
251 .packages
252 .iter()
253 .chain([update.as_package()])
254 .flat_map(|p| p.list_blobs())
255 .collect();
256
257 let repo = Arc::new(
258 self.packages
259 .iter()
260 .chain([update.as_package(), &images])
261 .fold(
262 RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
263 .add_package(update.as_package()),
264 |repo, package| repo.add_package(package),
265 )
266 .build()
267 .await
268 .expect("Building repo"),
269 );
270
271 let realm_builder = RealmBuilder::new().await.unwrap();
272 let blobfs = BlobfsRamdisk::start().await.context("starting blobfs").unwrap();
273
274 let served_repo = Arc::new(Arc::clone(&repo).server().start().unwrap());
275
276 let resolver_realm = ResolverForTest::realm_setup(
277 &realm_builder,
278 Arc::clone(&served_repo),
279 self.repo_url.clone(),
280 &blobfs,
281 )
282 .await
283 .unwrap();
284
285 let system_updater = realm_builder
286 .add_child("system-updater", "#meta/system-updater.cm", ChildOptions::new())
287 .await
288 .unwrap();
289
290 let service_reflector = realm_builder
291 .add_local_child(
292 "system_updater_service_reflector",
293 move |handles| {
294 let mut fs = fuchsia_component::server::ServiceFs::new();
295 fs.dir("svc").add_fidl_service(move |stream| {
300 fasync::Task::spawn(
301 Arc::new(mock_metrics::MockMetricEventLoggerFactory::new())
302 .run_logger_factory(stream),
303 )
304 .detach()
305 });
306 async move {
307 fs.serve_connection(handles.outgoing_dir).unwrap();
308 let () = fs.collect().await;
309 Ok(())
310 }
311 .boxed()
312 },
313 ChildOptions::new(),
314 )
315 .await
316 .unwrap();
317
318 realm_builder
319 .add_route(
320 Route::new()
321 .capability(
322 Capability::protocol::<fmetrics::MetricEventLoggerFactoryMarker>(),
323 )
324 .from(&service_reflector)
325 .to(&system_updater),
326 )
327 .await
328 .unwrap();
329
330 let paver = Arc::new(self.paver_builder.build());
332 let paver_clone = Arc::clone(&paver);
333 let mock_paver = realm_builder
334 .add_local_child(
335 "paver",
336 move |handles| Box::pin(Self::run_mock_paver(handles, Arc::clone(&paver))),
337 ChildOptions::new(),
338 )
339 .await
340 .unwrap();
341
342 realm_builder
343 .add_route(
344 Route::new()
345 .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
346 .from(&mock_paver)
347 .to(&system_updater),
348 )
349 .await
350 .unwrap();
351 realm_builder
352 .add_route(
353 Route::new()
354 .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
355 .from(&mock_paver)
356 .to(Ref::parent()),
357 )
358 .await
359 .unwrap();
360
361 realm_builder
363 .read_only_directory(
364 "build-info",
365 vec![&system_updater],
366 DirectoryContents::new().add_file("board", "test".as_bytes()),
367 )
368 .await
369 .unwrap();
370
371 realm_builder
373 .add_route(
374 Route::new()
375 .capability(
376 Capability::protocol_by_name("fuchsia.pkg.PackageResolver-ota")
377 .as_("fuchsia.pkg.PackageResolver"),
378 )
379 .from(&resolver_realm.resolver)
380 .to(&system_updater),
381 )
382 .await
383 .unwrap();
384
385 realm_builder
386 .add_route(
387 Route::new()
388 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
389 .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
390 .capability(Capability::protocol_by_name(
391 "fuchsia.pkg.garbagecollector.Manager",
392 ))
393 .from(&resolver_realm.cache)
394 .to(&system_updater),
395 )
396 .await
397 .unwrap();
398 realm_builder
399 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
400 name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
401 value: Vec::<String>::new().into(),
402 }))
403 .await
404 .unwrap();
405 realm_builder
406 .add_route(
407 Route::new()
408 .capability(Capability::configuration(
409 "fuchsia.system-updater.ManifestPublicKeys",
410 ))
411 .from(Ref::self_())
412 .to(&system_updater),
413 )
414 .await
415 .unwrap();
416 realm_builder
417 .add_route(
418 Route::new()
419 .capability(Capability::configuration(
420 "fuchsia.system-updater.ConcurrentBlobFetches",
421 ))
422 .capability(Capability::configuration(
423 "fuchsia.system-updater.ConcurrentPackageResolves",
424 ))
425 .capability(Capability::configuration(
426 "fuchsia.system-updater.VerifyExistingBlobs",
427 ))
428 .from(Ref::void())
429 .to(&system_updater),
430 )
431 .await
432 .unwrap();
433
434 realm_builder
436 .add_route(
437 Route::new()
438 .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
439 .from(Ref::parent())
440 .to(&system_updater),
441 )
442 .await
443 .unwrap();
444
445 realm_builder
447 .add_route(
448 Route::new()
449 .capability(Capability::protocol_by_name(
450 "fuchsia.update.installer.Installer",
451 ))
452 .from(&system_updater)
453 .to(Ref::parent()),
454 )
455 .await
456 .unwrap();
457
458 realm_builder
460 .add_route(
461 Route::new()
462 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
463 .from(&resolver_realm.cache)
464 .to(Ref::parent()),
465 )
466 .await
467 .unwrap();
468
469 let realm_instance = realm_builder.build().await.unwrap();
470
471 let installer_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
472 let paver_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
473
474 let updater = Updater::new_with_proxies(installer_proxy, paver_proxy);
475
476 let resolver = ResolverForTest::new(&realm_instance, blobfs, Arc::clone(&served_repo))
477 .await
478 .unwrap();
479
480 UpdaterForTest {
481 served_repo,
482 paver: paver_clone,
483 expected_blobfs_contents,
484 update_merkle_root: *update.as_package().hash(),
485 repo_url: self.repo_url,
486 updater,
487 resolver,
488 realm_instance,
489 }
490 }
491
492 #[cfg(test)]
493 pub async fn build_and_run(self) -> UpdaterResult {
494 self.build().await.run().await
495 }
496 }
497
498 pub struct UpdaterForTest {
501 #[expect(dead_code)]
502 pub served_repo: Arc<ServedRepository>,
503 pub paver: Arc<MockPaverService>,
504 pub expected_blobfs_contents: BTreeSet<Hash>,
505 pub update_merkle_root: Hash,
506 #[expect(dead_code)]
507 pub repo_url: fuchsia_url::RepositoryUrl,
508 pub resolver: ResolverForTest,
509 pub updater: Updater,
510 pub realm_instance: RealmInstance,
511 }
512
513 impl UpdaterForTest {
514 pub async fn run(mut self) -> UpdaterResult {
517 let () = self.updater.install_update(None).await.expect("installing update");
518
519 UpdaterResult {
520 paver_events: self.paver.take_events(),
521 resolver: self.resolver,
522 expected_blobfs_contents: self.expected_blobfs_contents,
523 realm_instance: self.realm_instance,
524 }
525 }
526 }
527
528 pub struct UpdaterResult {
530 pub paver_events: Vec<PaverEvent>,
532 pub resolver: ResolverForTest,
534 pub expected_blobfs_contents: BTreeSet<Hash>,
536 #[expect(dead_code)]
538 pub realm_instance: RealmInstance,
539 }
540
541 impl UpdaterResult {
542 pub async fn verify_packages(&self) {
545 let actual_contents =
548 self.resolver.cache.blobfs.list_blobs().expect("Listing blobfs blobs");
549 assert_eq!(actual_contents, self.expected_blobfs_contents);
550 }
551 }
552}
553
554#[cfg(test)]
555pub mod tests {
556 use super::for_tests::UpdaterBuilder;
557 use super::*;
558 use fidl_fuchsia_paver::Asset;
559 use fuchsia_pkg_testing::PackageBuilder;
560 use mock_paver::PaverEvent;
561
562 #[fuchsia::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}