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                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    /// Perform an update, skipping the final reboot.
61    /// If `update_package` is Some, use the given package URL as the URL for the update package.
62    /// Otherwise, `system-updater` uses the default URL.
63    /// This will not install any images to the recovery partitions.
64    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            // board does not actually support ABR, so return.
102            log::info!("ABR not supported, not configuring slots.");
103            return Ok(());
104        }
105        let result = result?;
106        if result.is_ok() {
107            // active slot is valid - assume that system-updater handled this for us.
108            return Ok(());
109        }
110
111        // In recovery, the paver will return ZX_ERR_NOT_SUPPORTED to query_active_configuration(),
112        // even on devices which support ABR. Handle this manually in case it is actually
113        // supported.
114        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        // The zbi and optional vbmeta contents.
148        fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
149        // The zbi and optional vbmeta contents of the recovery partition.
150        recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
151        repo_url: fuchsia_url::RepositoryUrl,
152    }
153
154    impl UpdaterBuilder {
155        /// Construct a new UpdateBuilder. Initially, this contains no images and an empty system
156        /// image package.
157        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        /// Add a package to the update package this builder will generate.
168        pub fn add_package(mut self, package: Package) -> Self {
169            self.packages.push(package);
170            self
171        }
172
173        /// The zbi and optional vbmeta images to write.
174        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        /// The zbi and optional vbmeta images to write to the recovery partition.
181        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        /// Mutate the `MockPaverServiceBuilder` contained in this UpdaterBuilder.
188        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        /// Create an UpdateForTest from this UpdaterBuilder.
224        /// This will construct an update package containing all packages and images added to the
225        /// builder, create a repository containing the packages, and create a MockPaver.
226        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            // Do not include the images package, system-updater triggers GC after resolving it.
250            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                        // Not necessary for updates, but without this system-updater will wait 30
296                        // seconds trying to flush cobalt logs before logging an attempt error,
297                        // and the test is torn down before then, so the error is lost. Also
298                        // prevents spam of irrelevant error logs.
299                        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            // Set up paver and routes
331            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            // Set up build-info and routes
362            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            // Set up pkg-resolver and pkg-cache routes
372            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            // Make sure the component under test can log.
435            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            // Expose system_updater to the parent
446            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            // Expose pkg-cache to these tests, for use by verify_packages
459            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    /// This wraps the `Updater` in order to reduce test boilerplate.
499    /// Should be constructed using `UpdaterBuilder`.
500    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        /// Run the system update, returning an `UpdaterResult` containing information about the
515        /// result of the update.
516        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    /// Contains information about the state of the system after the updater was run.
529    pub struct UpdaterResult {
530        /// All paver events received by the MockPaver during the update.
531        pub paver_events: Vec<PaverEvent>,
532        /// The resolver used by the updater.
533        pub resolver: ResolverForTest,
534        /// All the blobs that should be in blobfs after the update.
535        pub expected_blobfs_contents: BTreeSet<Hash>,
536        // The RealmInstance used to run this update, for introspection into component states.
537        #[expect(dead_code)]
538        pub realm_instance: RealmInstance,
539    }
540
541    impl UpdaterResult {
542        /// Verify that all packages that should have been resolved by the update
543        /// were resolved.
544        pub async fn verify_packages(&self) {
545            // Verify directly against blobfs to avoid any trickery pkg-resolver or pkg-cache may
546            // engage in.
547            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                // Emulate ABR not being supported
576                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                // isolated-swd does not write recovery even if an image is provided.
607                PaverEvent::DataSinkFlush,
608            ]
609        );
610
611        let () = result.verify_packages().await;
612    }
613}