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            },
52            &self.proxy,
53            Some(reboot_controller_server_end),
54        )
55        .await
56        .context("starting system update")
57    }
58
59    /// Perform an update, skipping the final reboot.
60    /// If `update_package` is Some, use the given package URL as the URL for the update package.
61    /// Otherwise, `system-updater` uses the default URL.
62    /// This will not install any images to the recovery partitions.
63    pub async fn install_update(
64        &mut self,
65        update_package: Option<&http::Uri>,
66    ) -> Result<(), Error> {
67        let attempt = self.start_update(update_package).await?;
68
69        let () = Self::monitor_update_attempt(attempt).await.context("monitoring installation")?;
70
71        let () = Self::activate_installed_slot(&self.paver_proxy)
72            .await
73            .context("activating installed slot")?;
74
75        Ok(())
76    }
77
78    async fn monitor_update_attempt(mut attempt: UpdateAttempt) -> Result<(), Error> {
79        while let Some(state) = attempt.try_next().await.context("fetching next update state")? {
80            log::info!("Install: {:?}", state);
81            if state.is_success() {
82                return Ok(());
83            } else if state.is_failure() {
84                return Err(anyhow!("update attempt failed in state {:?}", state));
85            }
86        }
87
88        Err(anyhow!("unexpected end of update attempt"))
89    }
90
91    async fn activate_installed_slot(paver: &PaverProxy) -> Result<(), Error> {
92        let (boot_manager, remote) = fidl::endpoints::create_proxy::<BootManagerMarker>();
93
94        paver.find_boot_manager(remote).context("finding boot manager")?;
95
96        let result = boot_manager.query_active_configuration().await;
97        if let Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. }) =
98            result
99        {
100            // board does not actually support ABR, so return.
101            log::info!("ABR not supported, not configuring slots.");
102            return Ok(());
103        }
104        let result = result?;
105        if result.is_ok() {
106            // active slot is valid - assume that system-updater handled this for us.
107            return Ok(());
108        }
109
110        // In recovery, the paver will return ZX_ERR_NOT_SUPPORTED to query_active_configuration(),
111        // even on devices which support ABR. Handle this manually in case it is actually
112        // supported.
113        zx::Status::ok(
114            boot_manager
115                .set_configuration_active(Configuration::A)
116                .await
117                .context("Sending set active configuration request")?,
118        )
119        .context("Setting A to active configuration")?;
120        Ok(())
121    }
122}
123
124#[cfg(test)]
125pub(crate) mod for_tests {
126    use super::*;
127    use crate::resolver::for_tests::{EMPTY_REPO_PATH, ResolverForTest};
128    use blobfs_ramdisk::BlobfsRamdisk;
129    use fidl_fuchsia_metrics as fmetrics;
130    use fidl_fuchsia_paver::PaverRequestStream;
131    use fuchsia_async as fasync;
132    use fuchsia_component_test::{
133        Capability, ChildOptions, DirectoryContents, RealmBuilder, RealmInstance, Ref, Route,
134    };
135    use fuchsia_merkle::Hash;
136    use fuchsia_pkg_testing::serve::ServedRepository;
137    use fuchsia_pkg_testing::{Package, RepositoryBuilder, SystemImageBuilder};
138    use mock_paver::{MockPaverService, MockPaverServiceBuilder, PaverEvent};
139    use std::collections::BTreeSet;
140    use std::sync::Arc;
141
142    pub const TEST_REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
143    pub struct UpdaterBuilder {
144        paver_builder: MockPaverServiceBuilder,
145        packages: Vec<Package>,
146        // The zbi and optional vbmeta contents.
147        fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
148        // The zbi and optional vbmeta contents of the recovery partition.
149        recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
150        repo_url: fuchsia_url::RepositoryUrl,
151    }
152
153    impl UpdaterBuilder {
154        /// Construct a new UpdateBuilder. Initially, this contains no images and an empty system
155        /// image package.
156        pub async fn new() -> UpdaterBuilder {
157            UpdaterBuilder {
158                paver_builder: MockPaverServiceBuilder::new(),
159                packages: vec![SystemImageBuilder::new().build().await],
160                fuchsia_image: None,
161                recovery_image: None,
162                repo_url: TEST_REPO_URL.parse().unwrap(),
163            }
164        }
165
166        /// Add a package to the update package this builder will generate.
167        pub fn add_package(mut self, package: Package) -> Self {
168            self.packages.push(package);
169            self
170        }
171
172        /// The zbi and optional vbmeta images to write.
173        pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
174            assert_eq!(self.fuchsia_image, None);
175            self.fuchsia_image = Some((zbi, vbmeta));
176            self
177        }
178
179        /// The zbi and optional vbmeta images to write to the recovery partition.
180        pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
181            assert_eq!(self.recovery_image, None);
182            self.recovery_image = Some((zbi, vbmeta));
183            self
184        }
185
186        /// Mutate the `MockPaverServiceBuilder` contained in this UpdaterBuilder.
187        pub fn paver<F>(mut self, f: F) -> Self
188        where
189            F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
190        {
191            self.paver_builder = f(self.paver_builder);
192            self
193        }
194
195        pub fn repo_url(mut self, url: &str) -> Self {
196            self.repo_url = url.parse().expect("Valid URL supplied to repo_url()");
197            self
198        }
199
200        fn serve_mock_paver(stream: PaverRequestStream, paver: Arc<MockPaverService>) {
201            let paver_clone = Arc::clone(&paver);
202            fasync::Task::spawn(
203                Arc::clone(&paver_clone)
204                    .run_paver_service(stream)
205                    .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
206            )
207            .detach();
208        }
209
210        async fn run_mock_paver(
211            handles: fuchsia_component_test::LocalComponentHandles,
212            paver: Arc<MockPaverService>,
213        ) -> Result<(), Error> {
214            let mut fs = fuchsia_component::server::ServiceFs::new();
215            fs.dir("svc")
216                .add_fidl_service(move |stream| Self::serve_mock_paver(stream, Arc::clone(&paver)));
217            fs.serve_connection(handles.outgoing_dir)?;
218            let () = fs.for_each_concurrent(None, |req| async move { req }).await;
219            Ok(())
220        }
221
222        /// Create an UpdateForTest from this UpdaterBuilder.
223        /// This will construct an update package containing all packages and images added to the
224        /// builder, create a repository containing the packages, and create a MockPaver.
225        pub async fn build(self) -> UpdaterForTest {
226            let mut update = fuchsia_pkg_testing::UpdatePackageBuilder::new(self.repo_url.clone())
227                .packages(
228                    self.packages
229                        .iter()
230                        .map(|p| {
231                            fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl::new(
232                                self.repo_url.clone(),
233                                p.name().clone(),
234                                None,
235                                *p.hash(),
236                            )
237                        })
238                        .collect::<Vec<_>>(),
239                );
240            if let Some((zbi, vbmeta)) = self.fuchsia_image {
241                update = update.fuchsia_image(zbi, vbmeta);
242            }
243            if let Some((zbi, vbmeta)) = self.recovery_image {
244                update = update.recovery_image(zbi, vbmeta);
245            }
246            let (update, images) = update.build().await;
247
248            // Do not include the images package, system-updater triggers GC after resolving it.
249            let expected_blobfs_contents = self
250                .packages
251                .iter()
252                .chain([update.as_package()])
253                .flat_map(|p| p.list_blobs())
254                .collect();
255
256            let repo = Arc::new(
257                self.packages
258                    .iter()
259                    .chain([update.as_package(), &images])
260                    .fold(
261                        RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
262                            .add_package(update.as_package()),
263                        |repo, package| repo.add_package(package),
264                    )
265                    .build()
266                    .await
267                    .expect("Building repo"),
268            );
269
270            let realm_builder = RealmBuilder::new().await.unwrap();
271            let blobfs = BlobfsRamdisk::start().await.context("starting blobfs").unwrap();
272
273            let served_repo = Arc::new(Arc::clone(&repo).server().start().unwrap());
274
275            let resolver_realm = ResolverForTest::realm_setup(
276                &realm_builder,
277                Arc::clone(&served_repo),
278                self.repo_url.clone(),
279                &blobfs,
280            )
281            .await
282            .unwrap();
283
284            let system_updater = realm_builder
285                .add_child("system-updater", "#meta/system-updater.cm", ChildOptions::new())
286                .await
287                .unwrap();
288
289            let service_reflector = realm_builder
290                .add_local_child(
291                    "system_updater_service_reflector",
292                    move |handles| {
293                        let mut fs = fuchsia_component::server::ServiceFs::new();
294                        // Not necessary for updates, but without this system-updater will wait 30
295                        // seconds trying to flush cobalt logs before logging an attempt error,
296                        // and the test is torn down before then, so the error is lost. Also
297                        // prevents spam of irrelevant error logs.
298                        fs.dir("svc").add_fidl_service(move |stream| {
299                            fasync::Task::spawn(
300                                Arc::new(mock_metrics::MockMetricEventLoggerFactory::new())
301                                    .run_logger_factory(stream),
302                            )
303                            .detach()
304                        });
305                        async move {
306                            fs.serve_connection(handles.outgoing_dir).unwrap();
307                            let () = fs.collect().await;
308                            Ok(())
309                        }
310                        .boxed()
311                    },
312                    ChildOptions::new(),
313                )
314                .await
315                .unwrap();
316
317            realm_builder
318                .add_route(
319                    Route::new()
320                        .capability(
321                            Capability::protocol::<fmetrics::MetricEventLoggerFactoryMarker>(),
322                        )
323                        .from(&service_reflector)
324                        .to(&system_updater),
325                )
326                .await
327                .unwrap();
328
329            // Set up paver and routes
330            let paver = Arc::new(self.paver_builder.build());
331            let paver_clone = Arc::clone(&paver);
332            let mock_paver = realm_builder
333                .add_local_child(
334                    "paver",
335                    move |handles| Box::pin(Self::run_mock_paver(handles, Arc::clone(&paver))),
336                    ChildOptions::new(),
337                )
338                .await
339                .unwrap();
340
341            realm_builder
342                .add_route(
343                    Route::new()
344                        .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
345                        .from(&mock_paver)
346                        .to(&system_updater),
347                )
348                .await
349                .unwrap();
350            realm_builder
351                .add_route(
352                    Route::new()
353                        .capability(Capability::protocol_by_name("fuchsia.paver.Paver"))
354                        .from(&mock_paver)
355                        .to(Ref::parent()),
356                )
357                .await
358                .unwrap();
359
360            // Set up build-info and routes
361            realm_builder
362                .read_only_directory(
363                    "build-info",
364                    vec![&system_updater],
365                    DirectoryContents::new().add_file("board", "test".as_bytes()),
366                )
367                .await
368                .unwrap();
369
370            // Set up pkg-resolver and pkg-cache routes
371            realm_builder
372                .add_route(
373                    Route::new()
374                        .capability(
375                            Capability::protocol_by_name("fuchsia.pkg.PackageResolver-ota")
376                                .as_("fuchsia.pkg.PackageResolver"),
377                        )
378                        .from(&resolver_realm.resolver)
379                        .to(&system_updater),
380                )
381                .await
382                .unwrap();
383
384            realm_builder
385                .add_route(
386                    Route::new()
387                        .capability(Capability::protocol_by_name("fuchsia.pkg.PackageCache"))
388                        .capability(Capability::protocol_by_name("fuchsia.pkg.RetainedPackages"))
389                        .capability(Capability::protocol_by_name(
390                            "fuchsia.pkg.garbagecollector.Manager",
391                        ))
392                        .from(&resolver_realm.cache)
393                        .to(&system_updater),
394                )
395                .await
396                .unwrap();
397            realm_builder
398                .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
399                    name: "fuchsia.system-updater.ManifestPublicKeys".parse().unwrap(),
400                    value: Vec::<String>::new().into(),
401                }))
402                .await
403                .unwrap();
404            realm_builder
405                .add_route(
406                    Route::new()
407                        .capability(Capability::configuration(
408                            "fuchsia.system-updater.ManifestPublicKeys",
409                        ))
410                        .from(Ref::self_())
411                        .to(&system_updater),
412                )
413                .await
414                .unwrap();
415            realm_builder
416                .add_route(
417                    Route::new()
418                        .capability(Capability::configuration(
419                            "fuchsia.system-updater.ConcurrentBlobFetches",
420                        ))
421                        .capability(Capability::configuration(
422                            "fuchsia.system-updater.ConcurrentPackageResolves",
423                        ))
424                        .capability(Capability::configuration(
425                            "fuchsia.system-updater.VerifyExistingBlobs",
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_async as fasync;
559    use fuchsia_pkg_testing::PackageBuilder;
560    use mock_paver::PaverEvent;
561
562    #[fasync::run_singlethreaded(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}