Skip to main content

persistence/
lib.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.
4
5//! `diagnostics-persistence` component persists Inspect VMOs and serves them at the next boot.
6
7mod fetcher;
8mod file_handler;
9mod inspect_server;
10mod scheduler;
11
12use anyhow::{Context, Error, anyhow};
13use argh::FromArgs;
14use fidl::endpoints::ControlHandle;
15use fidl_fuchsia_component_sandbox as fsandbox;
16use fidl_fuchsia_diagnostics as fdiagnostics;
17use fidl_fuchsia_inspect as finspect;
18use fidl_fuchsia_update as fupdate;
19use fuchsia_async as fasync;
20use fuchsia_component::escrow::EscrowOperation;
21use fuchsia_component::server::ServiceFs;
22use fuchsia_inspect::component;
23use fuchsia_inspect::health::Reporter;
24use fuchsia_runtime::{HandleInfo, HandleType};
25use fuchsia_sync::Mutex;
26use futures::{StreamExt, TryStreamExt};
27use log::*;
28use persistence_build_config::Config;
29use sandbox::CapabilityRef;
30use scheduler::Scheduler;
31use serde::{Deserialize, Serialize};
32use std::pin::pin;
33use std::sync::{Arc, LazyLock};
34use zx::BootInstant;
35
36/// The name of the subcommand and the logs-tag.
37pub const PROGRAM_NAME: &str = "persistence";
38pub const PERSIST_NODE_NAME: &str = "persist";
39/// Added after persisted data is fully published
40pub const PUBLISHED_TIME_KEY: &str = "published";
41
42/// Key in escrowed dictionary to immutable state persisted across instances of
43/// this component across the same boot.
44const INSTANCE_STATE_KEY: &str = "InstanceState";
45/// Key in escrowed dictionary to frozen Inspect VMO.
46const FROZEN_INSPECT_VMO_KEY: &str = "FrozenInspectVMO";
47
48/// Parsed CML structured configuration.
49#[derive(Clone, Debug)]
50pub(crate) struct BuildConfig {
51    /// If true, don't wait for a successful update check before publishing
52    /// previous boot's persisted Inspect data.
53    skip_update_check: bool,
54    /// Duration to wait for FIDL requests before stalling the connection.
55    stall_interval: zx::MonotonicDuration,
56}
57
58/// Build config, as defined by the CML structured configuration.
59pub(crate) static BUILD_CONFIG: LazyLock<BuildConfig> = LazyLock::new(|| {
60    let config = Config::take_from_startup_handle();
61    component::inspector().root().record_child("config", |node| config.record_inspect(node));
62
63    let Config { skip_update_check, stop_on_idle_timeout_millis } = config;
64
65    if skip_update_check {
66        info!("Configured to skip update check");
67    }
68
69    let stall_interval = if stop_on_idle_timeout_millis >= 0 {
70        info!("Configured to idle after {stop_on_idle_timeout_millis}ms of inactivity");
71        zx::MonotonicDuration::from_millis(stop_on_idle_timeout_millis)
72    } else {
73        info!("Not configured to idle after inactivity");
74        zx::MonotonicDuration::INFINITE
75    };
76
77    BuildConfig { skip_update_check, stall_interval }
78});
79
80/// Command line args
81#[derive(FromArgs, Debug, PartialEq)]
82#[argh(subcommand, name = "persistence")]
83pub struct CommandLine {}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86enum UpdateCheckStage {
87    /// Waiting for the first update check before publishing previous boot
88    /// inspect data.
89    Waiting,
90    /// First update check was skipped, previous boot inspect data has been published.
91    Skipped,
92    /// First update check has completed, previous boot inspect data has been
93    /// published.
94    Done,
95    /// Unable to subscribe to the first update check.
96    Error,
97}
98/// State to be persisted between instances of this component across
99/// the same boot.
100#[derive(Debug, Serialize, Deserialize)]
101struct PersistedState {
102    /// Persistence config loaded from disk.
103    config: persistence_config::Config,
104    /// Stage of the update check.
105    update_stage: Mutex<UpdateCheckStage>,
106}
107
108enum InspectState {
109    /// Inspect data is actively being served by this component instance.
110    Active(inspect_runtime::PublishedInspectController),
111    /// Inspect data is escrowed with the Component Framework.
112    ///
113    /// Archivist monitors this handle for OBJECT_PEER_CLOSED. If the handle is dropped, the
114    /// Archivist removes the escrowed Inspect data. By preserving it here, we ensure data
115    /// availability even when we restart but skip active republication.
116    Escrowed(zx::NullableHandle),
117}
118
119/// All component-specific state.
120#[derive(Clone)]
121struct ComponentState {
122    /// State persisted across instances of this component.
123    persisted: Arc<PersistedState>,
124    /// Listener for Sample
125    scheduler: Scheduler,
126    /// Shared state for Inspect data.
127    inspect: Arc<Mutex<Option<InspectState>>>,
128}
129
130impl ComponentState {
131    /// Load state from a previous instance if possible, otherwise initialize
132    /// new state.
133    async fn load(
134        scope: fasync::ScopeHandle,
135        store: &sandbox::CapabilityStore,
136    ) -> Result<Self, Error> {
137        if let Some(dictionary) =
138            fuchsia_runtime::take_startup_handle(HandleInfo::new(HandleType::EscrowedDictionary, 0))
139        {
140            debug!("Loading component state from escrowed dictionary");
141            return ComponentState::from_escrow(scope.clone(), store, dictionary)
142                .await
143                .context("Failed to load component state from escrowed dictionary");
144        }
145
146        debug!("No escrowed dictionary available; generating one");
147        Self::new(scope.clone()).await.context("Failed to create component state")
148    }
149
150    async fn new(scope: fasync::ScopeHandle) -> Result<Self, Error> {
151        let inspect_controller = inspect_runtime::publish(
152            component::inspector(),
153            inspect_runtime::PublishOptions::default().custom_scope(scope.clone()),
154        )
155        .ok_or_else(|| anyhow!("failed to publish inspect"))?;
156
157        let config =
158            persistence_config::load_configuration_files().context("Error loading configs")?;
159        file_handler::forget_old_data(&config).await?;
160
161        let scheduler = Scheduler::new(&config);
162        scheduler
163            .subscribe(scope.clone(), &config)
164            .await
165            .context("Failed to subscribe to fuchsia.diagnostics.Sample")?;
166
167        let persisted = {
168            let update_stage = if BUILD_CONFIG.skip_update_check {
169                UpdateCheckStage::Skipped
170            } else {
171                UpdateCheckStage::Waiting
172            };
173            Arc::new(PersistedState { config, update_stage: Mutex::new(update_stage) })
174        };
175
176        if BUILD_CONFIG.skip_update_check {
177            publish_inspect_data().await;
178        } else {
179            // Listen for the first update check.
180            let notifier_client = {
181                let (notifier_client, notifier_request_stream) =
182                    fidl::endpoints::create_request_stream::<fupdate::NotifierMarker>();
183                let persisted = persisted.clone();
184                scope.spawn(async move {
185                    if let Err(e) = handle_update_done(notifier_request_stream, persisted).await {
186                        error!("Failed to handle NotifierRequest: {e}");
187                    }
188                });
189                notifier_client
190            };
191
192            match fuchsia_component::client::connect_to_protocol::<fupdate::ListenerMarker>() {
193                Ok(proxy) => {
194                    if let Err(e) = proxy.notify_on_first_update_check(
195                        fupdate::ListenerNotifyOnFirstUpdateCheckRequest {
196                            notifier: Some(notifier_client),
197                            ..Default::default()
198                        },
199                    ) {
200                        error!("Error subscribing to first update check; not publishing: {e:?}");
201                        *persisted.update_stage.lock() = UpdateCheckStage::Error;
202                    }
203                }
204                Err(e) => {
205                    // TODO(https://fxbug.dev/444526593): Consider bailing
206                    // if the update checker is not available.
207                    warn!(e:?; "Unable to connect to fuchsia.update.Listener; will publish immediately");
208                    *persisted.update_stage.lock() = UpdateCheckStage::Done;
209                }
210            }
211        }
212
213        Ok(Self {
214            persisted,
215            scheduler,
216            inspect: Arc::new(Mutex::new(Some(InspectState::Active(inspect_controller)))),
217        })
218    }
219
220    async fn from_escrow<'a>(
221        scope: fasync::ScopeHandle,
222        store: &'a sandbox::CapabilityStore,
223        dictionary: zx::NullableHandle,
224    ) -> Result<Self, Error> {
225        let dict = store
226            .import(fsandbox::DictionaryRef { token: dictionary.into() })
227            .await
228            .context("Error importing from component startup handle")?;
229
230        let persisted_bytes = dict
231            .get::<sandbox::Data<'a>>(INSTANCE_STATE_KEY)
232            .await
233            .context("Error getting instance state")?
234            .export::<Vec<u8>>()
235            .await
236            .context("Error exporting as buffer")?;
237        let persisted: PersistedState = ciborium::from_reader(&persisted_bytes[..])
238            .context("Failed to deserialize InstanceState")?;
239        let update_stage = persisted.update_stage.lock().clone();
240
241        let escrow_token = dict
242            .get::<sandbox::Handle<'a>>(FROZEN_INSPECT_VMO_KEY)
243            .await
244            .context("Failed to get frozen Inspect VMO")?
245            .export::<zx::NullableHandle>()
246            .await
247            .context("Failed to export handle")?;
248
249        let inspect = match update_stage {
250            UpdateCheckStage::Waiting | UpdateCheckStage::Error => {
251                // Create a new, writable Inspect tree. The previous instance of
252                // Persistence did not receive the signal to persist data from
253                // the last boot, but this instance might.
254                let escrow_token =
255                    finspect::EscrowToken { token: zx::EventPair::from(escrow_token) };
256
257                // Swap escrowed Inspect data with a new Tree server.
258                let inspect_runtime::FetchEscrowResult { vmo: _, server } =
259                    inspect_runtime::fetch_escrow(
260                        escrow_token,
261                        inspect_runtime::FetchEscrowOptions::new().replace_with_tree(),
262                    )
263                    .await
264                    .context("Failed to fetch escrowed Inspect data")?;
265
266                let opts = inspect_runtime::PublishOptions::default()
267                    .custom_scope(scope.clone())
268                    .on_tree_server(server.context("FetchEscrow did not return a TreeHandle")?);
269
270                let inspect_controller = inspect_runtime::publish(component::inspector(), opts)
271                    .context("Failed to publish Inspect data")?;
272
273                InspectState::Active(inspect_controller)
274            }
275            UpdateCheckStage::Done | UpdateCheckStage::Skipped => {
276                // Persistence has already published persisted data from last
277                // boot. By not republishing, the existing frozen Inspect data
278                // remains published.
279                //
280                // Persistence needs to continue running to record data to
281                // persist for the next boot.
282                InspectState::Escrowed(escrow_token)
283            }
284        };
285
286        // Do not spawn FIDL request handlers when returning from escrow. The
287        // previous component instance escrowed its request streams, sending
288        // them to the Component Framework. When an incoming request is received
289        // on escrowed channels held by the Component Framework, it will be
290        // routed to this instance's incoming namespace (via IncomingRequest)
291        // then this instance will spawn new request handlers.
292
293        Ok(Self {
294            scheduler: Scheduler::new(&persisted.config),
295            persisted: Arc::new(persisted),
296            inspect: Arc::new(Mutex::new(Some(inspect))),
297        })
298    }
299
300    async fn as_escrowed_dict(
301        store: &sandbox::CapabilityStore,
302        persisted: impl AsRef<PersistedState>,
303        inspect: Arc<Mutex<Option<InspectState>>>,
304    ) -> Result<fsandbox::DictionaryRef, Error> {
305        let dict = store.create_dictionary().await?;
306
307        // Save PersistedState
308        let mut persisted_bytes: Vec<u8> = Vec::new();
309        ciborium::into_writer(persisted.as_ref(), &mut persisted_bytes)
310            .context("Failed to serialize InstanceState")?;
311        let data = store.import(persisted_bytes).await?;
312        dict.insert(INSTANCE_STATE_KEY, data).await?;
313
314        // Save frozen Inspect VMO.
315        let inspect = inspect.lock().take();
316        match inspect {
317            Some(InspectState::Active(inspect_controller)) => {
318                match inspect_controller
319                    .escrow_frozen(inspect_runtime::EscrowOptions::default())
320                    .await
321                {
322                    Ok(escrow_token) => {
323                        let handle = escrow_token.token.into_handle();
324                        let data = store.import(handle).await?;
325                        dict.insert(FROZEN_INSPECT_VMO_KEY, data).await?;
326                    }
327                    Err(e) => {
328                        error!("Failed to escrow frozen Inspect VMO: {e:?}");
329                    }
330                }
331            }
332            Some(InspectState::Escrowed(handle)) => {
333                let data = store.import(handle).await?;
334                dict.insert(FROZEN_INSPECT_VMO_KEY, data).await?;
335            }
336            None => {}
337        }
338
339        dict.export().await.context("Failed to export escrowed dictionary")
340    }
341}
342
343/// Handle fuchsia.update/Notifier requests. Notifies of when an update check
344/// has been completed, signaling this component to publish persisted data to
345/// Inspect.
346async fn handle_update_done(
347    stream: fupdate::NotifierRequestStream,
348    persisted: Arc<PersistedState>,
349) -> Result<(), Error> {
350    let (stream, stalled) = detect_stall::until_stalled(stream, BUILD_CONFIG.stall_interval);
351    let mut stream = pin!(stream);
352    if let Ok(Some(request)) = stream.try_next().await {
353        debug!("Received fuchsia.update.NotifierRequest");
354        match request {
355            fupdate::NotifierRequest::Notify { control_handle } => {
356                debug!("Received notification that the update check has completed");
357                let stage = persisted.update_stage.lock().clone();
358                match stage {
359                    UpdateCheckStage::Skipped | UpdateCheckStage::Error => {
360                        unreachable!("Received impossible notification")
361                    }
362                    UpdateCheckStage::Waiting => {
363                        *persisted.update_stage.lock() = UpdateCheckStage::Done;
364                        info!("...Update check has completed; publishing previous boot data");
365                        publish_inspect_data().await;
366                        control_handle.shutdown();
367                        return Ok(());
368                    }
369                    UpdateCheckStage::Done => {
370                        debug!("Ignoring update check notification; already received one");
371                        control_handle.shutdown();
372                        return Ok(());
373                    }
374                }
375            }
376        }
377    }
378    if let Ok(Some(server_end)) = stalled.await {
379        // Send the server endpoint back to the framework.
380        debug!("Escrowing fuchsia.update.Notifier");
381        fuchsia_component::client::connect_channel_to_protocol_at_path(
382            server_end,
383            "/escrow/fuchsia.update.Notifier",
384        )
385        .context("Failed to connect to fuchsia.update.Notifier")?;
386    }
387    Ok(())
388}
389
390async fn publish_inspect_data() {
391    // TODO(https://fxbug.dev/444525059): Set health properly.
392    component::health().set_ok();
393    if let Err(e) = inspect_server::record_persist_node(PERSIST_NODE_NAME).await {
394        error!("Failed to serve persisted Inspect data from previous boot: {e}");
395    }
396    component::inspector().root().record_int(PUBLISHED_TIME_KEY, BootInstant::get().into_nanos());
397}
398
399enum IncomingRequest {
400    UpdateDone(fupdate::NotifierRequestStream),
401    SampleSink(fdiagnostics::SampleSinkRequestStream),
402}
403
404pub async fn main(_args: CommandLine) -> Result<(), Error> {
405    info!("Starting Diagnostics Persistence service");
406    // initialize to 5MiB
407    component::init_inspector_with_size(1024 * 1024 * 5);
408    let scope = fasync::Scope::new();
409    let store = sandbox::CapabilityStore::connect()?;
410    let state = ComponentState::load(scope.to_handle(), &store)
411        .await
412        .context("Error getting escrowed state")?;
413    component::health().set_starting_up();
414
415    let mut fs = ServiceFs::new();
416    fs.dir("svc").add_fidl_service(IncomingRequest::UpdateDone);
417    fs.dir("svc").add_fidl_service(IncomingRequest::SampleSink);
418    fs.take_and_serve_directory_handle().expect("Failed to take service directory handle");
419
420    let escrow_operation = EscrowOperation::new();
421    escrow_operation.watch_for_stop().context("Failed to watch for stop on lifecycle handle")?;
422
423    let outgoing_dir_task =
424        pin!(fs.until_stalled(BUILD_CONFIG.stall_interval).for_each_concurrent(None, move |item| {
425            let escrow_operation = escrow_operation.clone();
426            let state = state.clone();
427            let store = store.clone();
428            async move {
429                match item {
430                    fuchsia_component::server::Item::Request(req, _active_guard) => match req {
431                        IncomingRequest::UpdateDone(stream) => {
432                            if let Err(e) = handle_update_done(stream, state.persisted).await {
433                                error!("Failed to handle NotifierRequest: {e}");
434                            }
435                        },
436                        IncomingRequest::SampleSink(stream) => {
437                            if let Err(e) = state.scheduler.handle_sample_sink(stream).await {
438                                error!("Failed to handle SampleSinkRequest: {e}");
439                            }
440                        },
441                    },
442                    fuchsia_component::server::Item::Stalled(outgoing_directory) => {
443                        match ComponentState::as_escrowed_dict(
444                            &store,
445                            state.persisted,
446                            state.inspect,
447                        ).await {
448                            Ok(dict) => escrow_operation.with_fsandbox_dictionary(dict),
449                            Err(e) => {
450                                error!(
451                                    "Failed to serialize PersistedState into component dictionary: {e}"
452                                );
453                            }
454                        };
455                        escrow_operation.run(outgoing_directory.into()).expect("failed to escrow handles");
456                    }
457                }
458            }
459        }));
460
461    outgoing_dir_task.await;
462    info!("Stopping due to idle activity");
463    scope.join().await;
464
465    Ok(())
466}