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<&url::Url>,
34 signature: Option<&[u8]>,
35 ) -> Result<UpdateAttempt, Error> {
36 let update_package = match update_package {
37 Some(url) => url.to_owned(),
38 None => DEFAULT_UPDATE_PACKAGE_URL.parse().unwrap(),
39 };
40
41 let (reboot_controller, reboot_controller_server_end) =
42 fidl::endpoints::create_proxy::<RebootControllerMarker>();
43 let () = reboot_controller.detach().context("disabling automatic reboot")?;
44
45 start_update(
46 &update_package,
47 Options {
48 initiator: Initiator::User,
49 allow_attach_to_existing_attempt: false,
50 should_write_recovery: false,
51 },
52 &self.proxy,
53 Some(reboot_controller_server_end),
54 signature,
55 )
56 .await
57 .context("starting system update")
58 }
59
60 pub async fn install_update(
65 &mut self,
66 update_package: Option<&url::Url>,
67 signature: Option<&[u8]>,
68 ) -> Result<(), Error> {
69 let attempt = self.start_update(update_package, signature).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 { status: zx::Status::NOT_SUPPORTED, .. }) =
100 result
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_paver::PaverRequestStream;
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 use {fidl_fuchsia_metrics as fmetrics, fuchsia_async as fasync};
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::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.VerifyExistingBlobs",
421 ))
422 .from(Ref::void())
423 .to(&system_updater),
424 )
425 .await
426 .unwrap();
427
428 realm_builder
430 .add_route(
431 Route::new()
432 .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
433 .from(Ref::parent())
434 .to(&system_updater),
435 )
436 .await
437 .unwrap();
438
439 realm_builder
441 .add_route(
442 Route::new()
443 .capability(Capability::protocol_by_name(
444 "fuchsia.update.installer.Installer",
445 ))
446 .from(&system_updater)
447 .to(Ref::parent()),
448 )
449 .await
450 .unwrap();
451
452 realm_builder
454 .add_route(
455 Route::new()
456 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
457 .from(&resolver_realm.cache)
458 .to(Ref::parent()),
459 )
460 .await
461 .unwrap();
462
463 let realm_instance = realm_builder.build().await.unwrap();
464
465 let installer_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
466 let paver_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
467
468 let updater = Updater::new_with_proxies(installer_proxy, paver_proxy);
469
470 let resolver = ResolverForTest::new(&realm_instance, blobfs, Arc::clone(&served_repo))
471 .await
472 .unwrap();
473
474 UpdaterForTest {
475 served_repo,
476 paver: paver_clone,
477 expected_blobfs_contents,
478 update_merkle_root: *update.as_package().hash(),
479 repo_url: self.repo_url,
480 updater,
481 resolver,
482 realm_instance,
483 }
484 }
485
486 #[cfg(test)]
487 pub async fn build_and_run(self) -> UpdaterResult {
488 self.build().await.run().await
489 }
490 }
491
492 pub struct UpdaterForTest {
495 #[expect(dead_code)]
496 pub served_repo: Arc<ServedRepository>,
497 pub paver: Arc<MockPaverService>,
498 pub expected_blobfs_contents: BTreeSet<Hash>,
499 pub update_merkle_root: Hash,
500 #[expect(dead_code)]
501 pub repo_url: fuchsia_url::RepositoryUrl,
502 pub resolver: ResolverForTest,
503 pub updater: Updater,
504 pub realm_instance: RealmInstance,
505 }
506
507 impl UpdaterForTest {
508 pub async fn run(mut self) -> UpdaterResult {
511 let () = self.updater.install_update(None, None).await.expect("installing update");
512
513 UpdaterResult {
514 paver_events: self.paver.take_events(),
515 resolver: self.resolver,
516 expected_blobfs_contents: self.expected_blobfs_contents,
517 realm_instance: self.realm_instance,
518 }
519 }
520 }
521
522 pub struct UpdaterResult {
524 pub paver_events: Vec<PaverEvent>,
526 pub resolver: ResolverForTest,
528 pub expected_blobfs_contents: BTreeSet<Hash>,
530 #[expect(dead_code)]
532 pub realm_instance: RealmInstance,
533 }
534
535 impl UpdaterResult {
536 pub async fn verify_packages(&self) {
539 let actual_contents =
542 self.resolver.cache.blobfs.list_blobs().expect("Listing blobfs blobs");
543 assert_eq!(actual_contents, self.expected_blobfs_contents);
544 }
545 }
546}
547
548#[cfg(test)]
549pub mod tests {
550 use super::for_tests::UpdaterBuilder;
551 use super::*;
552 use fidl_fuchsia_paver::Asset;
553 use fuchsia_async as fasync;
554 use fuchsia_pkg_testing::PackageBuilder;
555 use mock_paver::PaverEvent;
556
557 #[fasync::run_singlethreaded(test)]
558 pub async fn test_updater() {
559 let data = "hello world!".as_bytes();
560 let test_package = PackageBuilder::new("test_package")
561 .add_resource_at("bin/hello", "this is a test".as_bytes())
562 .add_resource_at("data/file", "this is a file".as_bytes())
563 .add_resource_at("meta/test_package.cm", "{}".as_bytes())
564 .build()
565 .await
566 .expect("Building test_package");
567 let updater = UpdaterBuilder::new()
568 .await
569 .paver(|p| {
570 p.boot_manager_close_with_epitaph(zx::Status::NOT_SUPPORTED)
572 })
573 .add_package(test_package)
574 .fuchsia_image(data.to_vec(), Some(data.to_vec()))
575 .recovery_image(data.to_vec(), Some(data.to_vec()));
576 let result = updater.build_and_run().await;
577
578 assert_eq!(
579 result.paver_events,
580 vec![
581 PaverEvent::WriteAsset {
582 configuration: Configuration::A,
583 asset: Asset::Kernel,
584 payload: data.to_vec()
585 },
586 PaverEvent::WriteAsset {
587 configuration: Configuration::B,
588 asset: Asset::Kernel,
589 payload: data.to_vec()
590 },
591 PaverEvent::WriteAsset {
592 configuration: Configuration::A,
593 asset: Asset::VerifiedBootMetadata,
594 payload: data.to_vec()
595 },
596 PaverEvent::WriteAsset {
597 configuration: Configuration::B,
598 asset: Asset::VerifiedBootMetadata,
599 payload: data.to_vec()
600 },
601 PaverEvent::DataSinkFlush,
603 ]
604 );
605
606 let () = result.verify_packages().await;
607 }
608}