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