1mod 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
36pub const PROGRAM_NAME: &str = "persistence";
38pub const PERSIST_NODE_NAME: &str = "persist";
39pub const PUBLISHED_TIME_KEY: &str = "published";
41
42const INSTANCE_STATE_KEY: &str = "InstanceState";
45const FROZEN_INSPECT_VMO_KEY: &str = "FrozenInspectVMO";
47
48#[derive(Clone, Debug)]
50pub(crate) struct BuildConfig {
51 skip_update_check: bool,
54 stall_interval: zx::MonotonicDuration,
56}
57
58pub(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#[derive(FromArgs, Debug, PartialEq)]
82#[argh(subcommand, name = "persistence")]
83pub struct CommandLine {}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86enum UpdateCheckStage {
87 Waiting,
90 Skipped,
92 Done,
95 Error,
97}
98#[derive(Debug, Serialize, Deserialize)]
101struct PersistedState {
102 config: persistence_config::Config,
104 update_stage: Mutex<UpdateCheckStage>,
106}
107
108enum InspectState {
109 Active(inspect_runtime::PublishedInspectController),
111 Escrowed(zx::NullableHandle),
117}
118
119#[derive(Clone)]
121struct ComponentState {
122 persisted: Arc<PersistedState>,
124 scheduler: Scheduler,
126 inspect: Arc<Mutex<Option<InspectState>>>,
128}
129
130impl ComponentState {
131 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 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 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 let escrow_token =
255 finspect::EscrowToken { token: zx::EventPair::from(escrow_token) };
256
257 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 InspectState::Escrowed(escrow_token)
283 }
284 };
285
286 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 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 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
343async 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 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 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 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}