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_paver::PaverRequestStream;
129 use fuchsia_component_test::{
130 Capability, ChildOptions, DirectoryContents, RealmBuilder, RealmInstance, Ref, Route,
131 };
132 use fuchsia_merkle::Hash;
133 use fuchsia_pkg_testing::serve::ServedRepository;
134 use fuchsia_pkg_testing::{Package, RepositoryBuilder, SystemImageBuilder};
135 use mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent};
136 use std::collections::BTreeSet;
137 use std::sync::Arc;
138 use {fidl_fuchsia_metrics as fmetrics, fuchsia_async as fasync};
139
140 pub const TEST_REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
141 pub struct UpdaterBuilder {
142 paver_builder: MockPaverServiceBuilder,
143 packages: Vec<Package>,
144 fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
146 recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
148 repo_url: fuchsia_url::RepositoryUrl,
149 }
150
151 impl UpdaterBuilder {
152 pub async fn new() -> UpdaterBuilder {
155 UpdaterBuilder {
156 paver_builder: MockPaverServiceBuilder::new(),
157 packages: vec![SystemImageBuilder::new().build().await],
158 fuchsia_image: None,
159 recovery_image: None,
160 repo_url: TEST_REPO_URL.parse().unwrap(),
161 }
162 }
163
164 pub fn add_package(mut self, package: Package) -> Self {
166 self.packages.push(package);
167 self
168 }
169
170 pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
172 assert_eq!(self.fuchsia_image, None);
173 self.fuchsia_image = Some((zbi, vbmeta));
174 self
175 }
176
177 pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
179 assert_eq!(self.recovery_image, None);
180 self.recovery_image = Some((zbi, vbmeta));
181 self
182 }
183
184 pub fn paver<F>(mut self, f: F) -> Self
186 where
187 F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
188 {
189 self.paver_builder = f(self.paver_builder);
190 self
191 }
192
193 pub fn repo_url(mut self, url: &str) -> Self {
194 self.repo_url = url.parse().expect("Valid URL supplied to repo_url()");
195 self
196 }
197
198 fn serve_mock_paver(stream: PaverRequestStream, paver: Arc<MockPaverService>) {
199 let paver_clone = Arc::clone(&paver);
200 fasync::Task::spawn(
201 Arc::clone(&paver_clone)
202 .run_paver_service(stream)
203 .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
204 )
205 .detach();
206 }
207
208 async fn run_mock_paver(
209 handles: fuchsia_component_test::LocalComponentHandles,
210 paver: Arc<MockPaverService>,
211 ) -> Result<(), Error> {
212 let mut fs = fuchsia_component::server::ServiceFs::new();
213 fs.dir("svc")
214 .add_fidl_service(move |stream| Self::serve_mock_paver(stream, Arc::clone(&paver)));
215 fs.serve_connection(handles.outgoing_dir)?;
216 let () = fs.for_each_concurrent(None, |req| async move { req }).await;
217 Ok(())
218 }
219
220 pub async fn build(self) -> UpdaterForTest {
224 let mut update = fuchsia_pkg_testing::UpdatePackageBuilder::new(self.repo_url.clone())
225 .packages(
226 self.packages
227 .iter()
228 .map(|p| {
229 fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
230 self.repo_url.clone(),
231 p.name().clone(),
232 None,
233 *p.hash(),
234 )
235 })
236 .collect::<Vec<_>>(),
237 );
238 if let Some((zbi, vbmeta)) = self.fuchsia_image {
239 update = update.fuchsia_image(zbi, vbmeta);
240 }
241 if let Some((zbi, vbmeta)) = self.recovery_image {
242 update = update.recovery_image(zbi, vbmeta);
243 }
244 let (update, images) = update.build().await;
245
246 let expected_blobfs_contents = self
248 .packages
249 .iter()
250 .chain([update.as_package()])
251 .flat_map(|p| p.list_blobs())
252 .collect();
253
254 let repo = Arc::new(
255 self.packages
256 .iter()
257 .chain([update.as_package(), &images])
258 .fold(
259 RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
260 .add_package(update.as_package()),
261 |repo, package| repo.add_package(package),
262 )
263 .build()
264 .await
265 .expect("Building repo"),
266 );
267
268 let realm_builder = RealmBuilder::new().await.unwrap();
269 let blobfs = BlobfsRamdisk::start().await.context("starting blobfs").unwrap();
270
271 let served_repo = Arc::new(Arc::clone(&repo).server().start().unwrap());
272
273 let resolver_realm = ResolverForTest::realm_setup(
274 &realm_builder,
275 Arc::clone(&served_repo),
276 self.repo_url.clone(),
277 &blobfs,
278 )
279 .await
280 .unwrap();
281
282 let system_updater = realm_builder
283 .add_child("system-updater", "#meta/system-updater.cm", ChildOptions::new())
284 .await
285 .unwrap();
286
287 let service_reflector = realm_builder
288 .add_local_child(
289 "system_updater_service_reflector",
290 move |handles| {
291 let mut fs = fuchsia_component::server::ServiceFs::new();
292 fs.dir("svc").add_fidl_service(move |stream| {
297 fasync::Task::spawn(
298 Arc::new(mock_metrics::MockMetricEventLoggerFactory::new())
299 .run_logger_factory(stream),
300 )
301 .detach()
302 });
303 async move {
304 fs.serve_connection(handles.outgoing_dir).unwrap();
305 let () = fs.collect().await;
306 Ok(())
307 }
308 .boxed()
309 },
310 ChildOptions::new(),
311 )
312 .await
313 .unwrap();
314
315 realm_builder
316 .add_route(
317 Route::new()
318 .capability(
319 Capability::protocol::<fmetrics::MetricEventLoggerFactoryMarker>(),
320 )
321 .from(&service_reflector)
322 .to(&system_updater),
323 )
324 .await
325 .unwrap();
326
327 let paver = Arc::new(self.paver_builder.build());
329 let paver_clone = Arc::clone(&paver);
330 let mock_paver = realm_builder
331 .add_local_child(
332 "paver",
333 move |handles| Box::pin(Self::run_mock_paver(handles, Arc::clone(&paver))),
334 ChildOptions::new(),
335 )
336 .await
337 .unwrap();
338
339 realm_builder
340 .add_route(
341 Route::new()
342 .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
343 .from(&mock_paver)
344 .to(&system_updater),
345 )
346 .await
347 .unwrap();
348 realm_builder
349 .add_route(
350 Route::new()
351 .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
352 .from(&mock_paver)
353 .to(Ref::parent()),
354 )
355 .await
356 .unwrap();
357
358 realm_builder
360 .read_only_directory(
361 "build-info",
362 vec![&system_updater],
363 DirectoryContents::new().add_file("board", "test".as_bytes()),
364 )
365 .await
366 .unwrap();
367
368 realm_builder
370 .add_route(
371 Route::new()
372 .capability(
373 Capability::protocol_by_name("fuchsia.pkg.PackageResolver-ota")
374 .as_("fuchsia.pkg.PackageResolver"),
375 )
376 .from(&resolver_realm.resolver)
377 .to(&system_updater),
378 )
379 .await
380 .unwrap();
381
382 realm_builder
383 .add_route(
384 Route::new()
385 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
386 .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
387 .capability(Capability::protocol_by_name(
388 "fuchsia.pkg.garbagecollector.Manager",
389 ))
390 .from(&resolver_realm.cache)
391 .to(&system_updater),
392 )
393 .await
394 .unwrap();
395 realm_builder
396 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
397 name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
398 value: Vec::<String>::new().into(),
399 }))
400 .await
401 .unwrap();
402 realm_builder
403 .add_route(
404 Route::new()
405 .capability(Capability::configuration(
406 "fuchsia.system-updater.ManifestPublicKeys",
407 ))
408 .from(Ref::self_())
409 .to(&system_updater),
410 )
411 .await
412 .unwrap();
413 realm_builder
414 .add_route(
415 Route::new()
416 .capability(Capability::configuration(
417 "fuchsia.system-updater.VerifyExistingBlobs",
418 ))
419 .from(Ref::void())
420 .to(&system_updater),
421 )
422 .await
423 .unwrap();
424
425 realm_builder
427 .add_route(
428 Route::new()
429 .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
430 .from(Ref::parent())
431 .to(&system_updater),
432 )
433 .await
434 .unwrap();
435
436 realm_builder
438 .add_route(
439 Route::new()
440 .capability(Capability::protocol_by_name(
441 "fuchsia.update.installer.Installer",
442 ))
443 .from(&system_updater)
444 .to(Ref::parent()),
445 )
446 .await
447 .unwrap();
448
449 realm_builder
451 .add_route(
452 Route::new()
453 .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
454 .from(&resolver_realm.cache)
455 .to(Ref::parent()),
456 )
457 .await
458 .unwrap();
459
460 let realm_instance = realm_builder.build().await.unwrap();
461
462 let installer_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
463 let paver_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
464
465 let updater = Updater::new_with_proxies(installer_proxy, paver_proxy);
466
467 let resolver = ResolverForTest::new(&realm_instance, blobfs, Arc::clone(&served_repo))
468 .await
469 .unwrap();
470
471 UpdaterForTest {
472 served_repo,
473 paver: paver_clone,
474 expected_blobfs_contents,
475 update_merkle_root: *update.as_package().hash(),
476 repo_url: self.repo_url,
477 updater,
478 resolver,
479 realm_instance,
480 }
481 }
482
483 #[cfg(test)]
484 pub async fn build_and_run(self) -> UpdaterResult {
485 self.build().await.run().await
486 }
487 }
488
489 pub struct UpdaterForTest {
492 #[expect(dead_code)]
493 pub served_repo: Arc<ServedRepository>,
494 pub paver: Arc<MockPaverService>,
495 pub expected_blobfs_contents: BTreeSet<Hash>,
496 pub update_merkle_root: Hash,
497 #[expect(dead_code)]
498 pub repo_url: fuchsia_url::RepositoryUrl,
499 pub resolver: ResolverForTest,
500 pub updater: Updater,
501 pub realm_instance: RealmInstance,
502 }
503
504 impl UpdaterForTest {
505 pub async fn run(mut self) -> UpdaterResult {
508 let () = self.updater.install_update(None).await.expect("installing update");
509
510 UpdaterResult {
511 paver_events: self.paver.take_events(),
512 resolver: self.resolver,
513 expected_blobfs_contents: self.expected_blobfs_contents,
514 realm_instance: self.realm_instance,
515 }
516 }
517 }
518
519 pub struct UpdaterResult {
521 pub paver_events: Vec<PaverEvent>,
523 pub resolver: ResolverForTest,
525 pub expected_blobfs_contents: BTreeSet<Hash>,
527 #[expect(dead_code)]
529 pub realm_instance: RealmInstance,
530 }
531
532 impl UpdaterResult {
533 pub async fn verify_packages(&self) {
536 let actual_contents =
539 self.resolver.cache.blobfs.list_blobs().expect("Listing blobfs blobs");
540 assert_eq!(actual_contents, self.expected_blobfs_contents);
541 }
542 }
543}
544
545#[cfg(test)]
546pub mod tests {
547 use super::for_tests::UpdaterBuilder;
548 use super::*;
549 use fidl_fuchsia_paver::Asset;
550 use fuchsia_async as fasync;
551 use fuchsia_pkg_testing::PackageBuilder;
552 use mock_paver::PaverEvent;
553
554 #[fasync::run_singlethreaded(test)]
555 pub async fn test_updater() {
556 let data = "hello world!".as_bytes();
557 let test_package = PackageBuilder::new("test_package")
558 .add_resource_at("bin/hello", "this is a test".as_bytes())
559 .add_resource_at("data/file", "this is a file".as_bytes())
560 .add_resource_at("meta/test_package.cm", "{}".as_bytes())
561 .build()
562 .await
563 .expect("Building test_package");
564 let updater = UpdaterBuilder::new()
565 .await
566 .paver(|p| {
567 p.boot_manager_close_with_epitaph(zx::Status::NOT_SUPPORTED)
569 })
570 .add_package(test_package)
571 .fuchsia_image(data.to_vec(), Some(data.to_vec()))
572 .recovery_image(data.to_vec(), Some(data.to_vec()));
573 let result = updater.build_and_run().await;
574
575 assert_eq!(
576 result.paver_events,
577 vec![
578 PaverEvent::WriteAsset {
579 configuration: Configuration::A,
580 asset: Asset::Kernel,
581 payload: data.to_vec()
582 },
583 PaverEvent::WriteAsset {
584 configuration: Configuration::B,
585 asset: Asset::Kernel,
586 payload: data.to_vec()
587 },
588 PaverEvent::WriteAsset {
589 configuration: Configuration::A,
590 asset: Asset::VerifiedBootMetadata,
591 payload: data.to_vec()
592 },
593 PaverEvent::WriteAsset {
594 configuration: Configuration::B,
595 asset: Asset::VerifiedBootMetadata,
596 payload: data.to_vec()
597 },
598 PaverEvent::DataSinkFlush,
600 ]
601 );
602
603 let () = result.verify_packages().await;
604 }
605}