Skip to main content

isolated_swd/
updater.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use 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    /// Perform an update, skipping the final reboot.
59    /// If `update_package` is Some, use the given package URL as the URL for the update package.
60    /// Otherwise, `system-updater` uses the default URL.
61    /// This will not install any images to the recovery partitions.
62    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            // board does not actually support ABR, so return.
100            log::info!("ABR not supported, not configuring slots.");
101            return Ok(());
102        }
103        let result = result?;
104        if result.is_ok() {
105            // active slot is valid - assume that system-updater handled this for us.
106            return Ok(());
107        }
108
109        // In recovery, the paver will return ZX_ERR_NOT_SUPPORTED to query_active_configuration(),
110        // even on devices which support ABR. Handle this manually in case it is actually
111        // supported.
112        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        // The zbi and optional vbmeta contents.
146        fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
147        // The zbi and optional vbmeta contents of the recovery partition.
148        recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
149        repo_url: fuchsia_url::RepositoryUrl,
150    }
151
152    impl UpdaterBuilder {
153        /// Construct a new UpdateBuilder. Initially, this contains no images and an empty system
154        /// image package.
155        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        /// Add a package to the update package this builder will generate.
166        pub fn add_package(mut self, package: Package) -> Self {
167            self.packages.push(package);
168            self
169        }
170
171        /// The zbi and optional vbmeta images to write.
172        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        /// The zbi and optional vbmeta images to write to the recovery partition.
179        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        /// Mutate the `MockPaverServiceBuilder` contained in this UpdaterBuilder.
186        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        /// Create an UpdateForTest from this UpdaterBuilder.
222        /// This will construct an update package containing all packages and images added to the
223        /// builder, create a repository containing the packages, and create a MockPaver.
224        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            // Do not include the images package, system-updater triggers GC after resolving it.
248            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                        // Not necessary for updates, but without this system-updater will wait 30
294                        // seconds trying to flush cobalt logs before logging an attempt error,
295                        // and the test is torn down before then, so the error is lost. Also
296                        // prevents spam of irrelevant error logs.
297                        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            // Set up paver and routes
329            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            // Set up build-info and routes
360            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            // Set up pkg-resolver and pkg-cache routes
370            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            // Make sure the component under test can log.
433            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            // Expose system_updater to the parent
444            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            // Expose pkg-cache to these tests, for use by verify_packages
457            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    /// This wraps the `Updater` in order to reduce test boilerplate.
497    /// Should be constructed using `UpdaterBuilder`.
498    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        /// Run the system update, returning an `UpdaterResult` containing information about the
513        /// result of the update.
514        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    /// Contains information about the state of the system after the updater was run.
527    pub struct UpdaterResult {
528        /// All paver events received by the MockPaver during the update.
529        pub paver_events: Vec<PaverEvent>,
530        /// The resolver used by the updater.
531        pub resolver: ResolverForTest,
532        /// All the blobs that should be in blobfs after the update.
533        pub expected_blobfs_contents: BTreeSet<Hash>,
534        // The RealmInstance used to run this update, for introspection into component states.
535        #[expect(dead_code)]
536        pub realm_instance: RealmInstance,
537    }
538
539    impl UpdaterResult {
540        /// Verify that all packages that should have been resolved by the update
541        /// were resolved.
542        pub async fn verify_packages(&self) {
543            // Verify directly against blobfs to avoid any trickery pkg-resolver or pkg-cache may
544            // engage in.
545            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                // Emulate ABR not being supported
575                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                // isolated-swd does not write recovery even if an image is provided.
606                PaverEvent::DataSinkFlush,
607            ]
608        );
609
610        let () = result.verify_packages().await;
611    }
612}