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                should_commit: true,
53            },
54            &self.proxy,
55            Some(reboot_controller_server_end),
56        )
57        .await
58        .context("starting system update")
59    }
60
61    /// Perform an update, skipping the final reboot.
62    /// If `update_package` is Some, use the given package URL as the URL for the update package.
63    /// Otherwise, `system-updater` uses the default URL.
64    /// This will not install any images to the recovery partitions.
65    pub async fn install_update(
66        &mut self,
67        update_package: Option<&http::Uri>,
68    ) -> Result<(), Error> {
69        let attempt = self.start_update(update_package).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 { epitaph, .. }) = result
100            && epitaph == zx::Status::NOT_SUPPORTED
101        {
102            // board does not actually support ABR, so return.
103            log::info!("ABR not supported, not configuring slots.");
104            return Ok(());
105        }
106        let result = result?;
107        if result.is_ok() {
108            // active slot is valid - assume that system-updater handled this for us.
109            return Ok(());
110        }
111
112        // In recovery, the paver will return ZX_ERR_NOT_SUPPORTED to query_active_configuration(),
113        // even on devices which support ABR. Handle this manually in case it is actually
114        // supported.
115        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_metrics as fmetrics;
132    use fidl_fuchsia_paver::PaverRequestStream;
133    use fuchsia_async as fasync;
134    use fuchsia_component_test::{
135        Capability, ChildOptions, DirectoryContents, RealmBuilder, RealmInstance, Ref, Route,
136    };
137    use fuchsia_merkle::Hash;
138    use fuchsia_pkg_testing::serve::ServedRepository;
139    use fuchsia_pkg_testing::{Package, RepositoryBuilder, SystemImageBuilder};
140    use mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent};
141    use std::collections::BTreeSet;
142    use std::sync::Arc;
143
144    pub const TEST_REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
145    pub struct UpdaterBuilder {
146        paver_builder: MockPaverServiceBuilder,
147        packages: Vec<Package>,
148        // The zbi and optional vbmeta contents.
149        fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
150        // The zbi and optional vbmeta contents of the recovery partition.
151        recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
152        repo_url: fuchsia_url::RepositoryUrl,
153    }
154
155    impl UpdaterBuilder {
156        /// Construct a new UpdateBuilder. Initially, this contains no images and an empty system
157        /// image package.
158        pub async fn new() -> UpdaterBuilder {
159            UpdaterBuilder {
160                paver_builder: MockPaverServiceBuilder::new(),
161                packages: vec![SystemImageBuilder::new().build().await],
162                fuchsia_image: None,
163                recovery_image: None,
164                repo_url: TEST_REPO_URL.parse().unwrap(),
165            }
166        }
167
168        /// Add a package to the update package this builder will generate.
169        pub fn add_package(mut self, package: Package) -> Self {
170            self.packages.push(package);
171            self
172        }
173
174        /// The zbi and optional vbmeta images to write.
175        pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
176            assert_eq!(self.fuchsia_image, None);
177            self.fuchsia_image = Some((zbi, vbmeta));
178            self
179        }
180
181        /// The zbi and optional vbmeta images to write to the recovery partition.
182        pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
183            assert_eq!(self.recovery_image, None);
184            self.recovery_image = Some((zbi, vbmeta));
185            self
186        }
187
188        /// Mutate the `MockPaverServiceBuilder` contained in this UpdaterBuilder.
189        pub fn paver<F>(mut self, f: F) -> Self
190        where
191            F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
192        {
193            self.paver_builder = f(self.paver_builder);
194            self
195        }
196
197        pub fn repo_url(mut self, url: &str) -> Self {
198            self.repo_url = url.parse().expect("Valid URL supplied to repo_url()");
199            self
200        }
201
202        fn serve_mock_paver(stream: PaverRequestStream, paver: Arc<MockPaverService>) {
203            let paver_clone = Arc::clone(&paver);
204            fasync::Task::spawn(
205                Arc::clone(&paver_clone)
206                    .run_paver_service(stream)
207                    .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
208            )
209            .detach();
210        }
211
212        async fn run_mock_paver(
213            handles: fuchsia_component_test::LocalComponentHandles,
214            paver: Arc<MockPaverService>,
215        ) -> Result<(), Error> {
216            let mut fs = fuchsia_component::server::ServiceFs::new();
217            fs.dir("svc")
218                .add_fidl_service(move |stream| Self::serve_mock_paver(stream, Arc::clone(&paver)));
219            fs.serve_connection(handles.outgoing_dir)?;
220            let () = fs.for_each_concurrent(None, |req| async move { req }).await;
221            Ok(())
222        }
223
224        /// Create an UpdateForTest from this UpdaterBuilder.
225        /// This will construct an update package containing all packages and images added to the
226        /// builder, create a repository containing the packages, and create a MockPaver.
227        pub async fn build(self) -> UpdaterForTest {
228            let mut update = fuchsia_pkg_testing::UpdatePackageBuilder::new(self.repo_url.clone())
229                .packages(
230                    self.packages
231                        .iter()
232                        .map(|p| {
233                            fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
234                                self.repo_url.clone(),
235                                p.name().clone(),
236                                None,
237                                *p.hash(),
238                            )
239                        })
240                        .collect::<Vec<_>>(),
241                );
242            if let Some((zbi, vbmeta)) = self.fuchsia_image {
243                update = update.fuchsia_image(zbi, vbmeta);
244            }
245            if let Some((zbi, vbmeta)) = self.recovery_image {
246                update = update.recovery_image(zbi, vbmeta);
247            }
248            let (update, images) = update.build().await;
249
250            // Do not include the images package, system-updater triggers GC after resolving it.
251            let expected_blobfs_contents = self
252                .packages
253                .iter()
254                .chain([update.as_package()])
255                .flat_map(|p| p.list_blobs())
256                .collect();
257
258            let repo = Arc::new(
259                self.packages
260                    .iter()
261                    .chain([update.as_package(), &images])
262                    .fold(
263                        RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
264                            .add_package(update.as_package()),
265                        |repo, package| repo.add_package(package),
266                    )
267                    .build()
268                    .await
269                    .expect("Building repo"),
270            );
271
272            let realm_builder = RealmBuilder::new().await.unwrap();
273            let blobfs = BlobfsRamdisk::start().await.context("starting blobfs").unwrap();
274
275            let served_repo = Arc::new(Arc::clone(&repo).server().start().unwrap());
276
277            let resolver_realm = ResolverForTest::realm_setup(
278                &realm_builder,
279                Arc::clone(&served_repo),
280                self.repo_url.clone(),
281                &blobfs,
282            )
283            .await
284            .unwrap();
285
286            let system_updater = realm_builder
287                .add_child("system-updater", "#meta/system-updater.cm", ChildOptions::new())
288                .await
289                .unwrap();
290
291            let service_reflector = realm_builder
292                .add_local_child(
293                    "system_updater_service_reflector",
294                    move |handles| {
295                        let mut fs = fuchsia_component::server::ServiceFs::new();
296                        // Not necessary for updates, but without this system-updater will wait 30
297                        // seconds trying to flush cobalt logs before logging an attempt error,
298                        // and the test is torn down before then, so the error is lost. Also
299                        // prevents spam of irrelevant error logs.
300                        fs.dir("svc").add_fidl_service(move |stream| {
301                            fasync::Task::spawn(
302                                Arc::new(mock_metrics::MockMetricEventLoggerFactory::new())
303                                    .run_logger_factory(stream),
304                            )
305                            .detach()
306                        });
307                        async move {
308                            fs.serve_connection(handles.outgoing_dir).unwrap();
309                            let () = fs.collect().await;
310                            Ok(())
311                        }
312                        .boxed()
313                    },
314                    ChildOptions::new(),
315                )
316                .await
317                .unwrap();
318
319            realm_builder
320                .add_route(
321                    Route::new()
322                        .capability(
323                            Capability::protocol::<fmetrics::MetricEventLoggerFactoryMarker>(),
324                        )
325                        .from(&service_reflector)
326                        .to(&system_updater),
327                )
328                .await
329                .unwrap();
330
331            // Set up paver and routes
332            let paver = Arc::new(self.paver_builder.build());
333            let paver_clone = Arc::clone(&paver);
334            let mock_paver = realm_builder
335                .add_local_child(
336                    "paver",
337                    move |handles| Box::pin(Self::run_mock_paver(handles, Arc::clone(&paver))),
338                    ChildOptions::new(),
339                )
340                .await
341                .unwrap();
342
343            realm_builder
344                .add_route(
345                    Route::new()
346                        .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
347                        .from(&mock_paver)
348                        .to(&system_updater),
349                )
350                .await
351                .unwrap();
352            realm_builder
353                .add_route(
354                    Route::new()
355                        .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
356                        .from(&mock_paver)
357                        .to(Ref::parent()),
358                )
359                .await
360                .unwrap();
361
362            // Set up build-info and routes
363            realm_builder
364                .read_only_directory(
365                    "build-info",
366                    vec![&system_updater],
367                    DirectoryContents::new().add_file("board", "test".as_bytes()),
368                )
369                .await
370                .unwrap();
371
372            // Set up pkg-cache routes
373            realm_builder
374                .add_route(
375                    Route::new()
376                        .capability(
377                            Capability::protocol_by_name("fuchsia.pkg.PackageResolver-ota")
378                                .as_("fuchsia.pkg.PackageResolver"),
379                        )
380                        .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
381                        .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
382                        .capability(Capability::protocol_by_name(
383                            "fuchsia.pkg.garbagecollector.Manager",
384                        ))
385                        .from(&resolver_realm.cache)
386                        .to(&system_updater),
387                )
388                .await
389                .unwrap();
390            realm_builder
391                .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
392                    name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
393                    value: Vec::<String>::new().into(),
394                }))
395                .await
396                .unwrap();
397            realm_builder
398                .add_route(
399                    Route::new()
400                        .capability(Capability::configuration(
401                            "fuchsia.system-updater.ManifestPublicKeys",
402                        ))
403                        .from(Ref::self_())
404                        .to(&system_updater),
405                )
406                .await
407                .unwrap();
408            realm_builder
409                .add_route(
410                    Route::new()
411                        .capability(Capability::configuration(
412                            "fuchsia.system-updater.ConcurrentBlobFetches",
413                        ))
414                        .capability(Capability::configuration(
415                            "fuchsia.system-updater.ConcurrentPackageResolves",
416                        ))
417                        .capability(Capability::configuration(
418                            "fuchsia.system-updater.ExcessiveUpdateDurationSeconds",
419                        ))
420                        .capability(Capability::configuration(
421                            "fuchsia.system-updater.VerifyExistingBlobs",
422                        ))
423                        .capability(
424                            Capability::protocol_by_name("fuchsia.feedback.CrashReporter")
425                                .optional(),
426                        )
427                        .from(Ref::void())
428                        .to(&system_updater),
429                )
430                .await
431                .unwrap();
432
433            // Make sure the component under test can log.
434            realm_builder
435                .add_route(
436                    Route::new()
437                        .capability(Capability::protocol_by_name("fuchsia.logger.LogSink"))
438                        .from(Ref::parent())
439                        .to(&system_updater),
440                )
441                .await
442                .unwrap();
443
444            // Expose system_updater to the parent
445            realm_builder
446                .add_route(
447                    Route::new()
448                        .capability(Capability::protocol_by_name(
449                            "fuchsia.update.installer.Installer",
450                        ))
451                        .from(&system_updater)
452                        .to(Ref::parent()),
453                )
454                .await
455                .unwrap();
456
457            // Expose pkg-cache to these tests, for use by verify_packages
458            realm_builder
459                .add_route(
460                    Route::new()
461                        .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
462                        .from(&resolver_realm.cache)
463                        .to(Ref::parent()),
464                )
465                .await
466                .unwrap();
467
468            let realm_instance = realm_builder.build().await.unwrap();
469
470            let installer_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
471            let paver_proxy = realm_instance.root.connect_to_protocol_at_exposed_dir().unwrap();
472
473            let updater = Updater::new_with_proxies(installer_proxy, paver_proxy);
474
475            let resolver = ResolverForTest::new(&realm_instance, blobfs, Arc::clone(&served_repo))
476                .await
477                .unwrap();
478
479            UpdaterForTest {
480                served_repo,
481                paver: paver_clone,
482                expected_blobfs_contents,
483                update_merkle_root: *update.as_package().hash(),
484                repo_url: self.repo_url,
485                updater,
486                resolver,
487                realm_instance,
488            }
489        }
490
491        #[cfg(test)]
492        pub async fn build_and_run(self) -> UpdaterResult {
493            self.build().await.run().await
494        }
495    }
496
497    /// This wraps the `Updater` in order to reduce test boilerplate.
498    /// Should be constructed using `UpdaterBuilder`.
499    pub struct UpdaterForTest {
500        #[expect(dead_code)]
501        pub served_repo: Arc<ServedRepository>,
502        pub paver: Arc<MockPaverService>,
503        pub expected_blobfs_contents: BTreeSet<Hash>,
504        pub update_merkle_root: Hash,
505        #[expect(dead_code)]
506        pub repo_url: fuchsia_url::RepositoryUrl,
507        pub resolver: ResolverForTest,
508        pub updater: Updater,
509        pub realm_instance: RealmInstance,
510    }
511
512    impl UpdaterForTest {
513        /// Run the system update, returning an `UpdaterResult` containing information about the
514        /// result of the update.
515        pub async fn run(mut self) -> UpdaterResult {
516            let () = self.updater.install_update(None).await.expect("installing update");
517
518            UpdaterResult {
519                paver_events: self.paver.take_events(),
520                resolver: self.resolver,
521                expected_blobfs_contents: self.expected_blobfs_contents,
522                realm_instance: self.realm_instance,
523            }
524        }
525    }
526
527    /// Contains information about the state of the system after the updater was run.
528    pub struct UpdaterResult {
529        /// All paver events received by the MockPaver during the update.
530        pub paver_events: Vec<PaverEvent>,
531        /// The resolver used by the updater.
532        pub resolver: ResolverForTest,
533        /// All the blobs that should be in blobfs after the update.
534        pub expected_blobfs_contents: BTreeSet<Hash>,
535        // The RealmInstance used to run this update, for introspection into component states.
536        #[expect(dead_code)]
537        pub realm_instance: RealmInstance,
538    }
539
540    impl UpdaterResult {
541        /// Verify that all packages that should have been resolved by the update
542        /// were resolved.
543        pub async fn verify_packages(&self) {
544            // Verify directly against blobfs to avoid any trickery pkg-resolver or pkg-cache may
545            // engage in.
546            let actual_contents =
547                self.resolver.cache.blobfs.list_blobs().expect("Listing blobfs blobs");
548            assert_eq!(actual_contents, self.expected_blobfs_contents);
549        }
550    }
551}
552
553#[cfg(test)]
554pub mod tests {
555    use super::for_tests::UpdaterBuilder;
556    use super::*;
557    use fidl_fuchsia_paver::Asset;
558    use fuchsia_pkg_testing::PackageBuilder;
559    use mock_paver::PaverEvent;
560
561    #[fuchsia::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}