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
7use anyhow::Error;
8use argh::FromArgs;
9use diagnostics_reader::{ArchiveReader, InspectArchiveReader};
10use fidl::endpoints::Proxy;
11use fidl_fuchsia_diagnostics as fdiagnostics;
12use fidl_fuchsia_diagnostics_persistence as fpersistence;
13use fidl_fuchsia_hardware_power_statecontrol as fpower;
14use fidl_fuchsia_io as fio;
15use fidl_fuchsia_power_battery as fbattery;
16use fidl_fuchsia_update as fupdate;
17use fuchsia_async as fasync;
18use fuchsia_component::client::{connect_to_protocol, connect_to_protocol_at_path};
19use fuchsia_component::server::ServiceFs;
20use fuchsia_inspect::health::Reporter;
21use futures::lock::Mutex;
22use futures::{FutureExt, StreamExt, TryStreamExt};
23use log::*;
24use persistence_build_config::Config;
25use serde::{Deserialize, Serialize};
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28use zx::{BootInstant, MonotonicDuration, MonotonicInstant};
29
30/// The name of the subcommand and the logs-tag, used by launcher
31pub const PROGRAM_NAME: &str = "persistence";
32
33/// Percentage above threshold required to re-arm the low battery snapshot trigger.
34const BATTERY_TRIGGER_HYSTERESIS_PERCENT: f32 = 5.0;
35
36/// Command line args
37#[derive(FromArgs, Debug, PartialEq)]
38#[argh(subcommand, name = "persistence")]
39pub struct CommandLine {}
40
41#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
42pub struct Metadata {
43    pub monotonic_timestamp: i64,
44    pub boot_timestamp: i64,
45    pub utc_timestamp_ns: Option<i64>,
46}
47
48enum IncomingService {
49    PreviousBootDataProvider(fpersistence::PreviousBootDataProviderRequestStream),
50}
51
52pub async fn main(_args: CommandLine) -> Result<(), Error> {
53    info!("Persistence starting up");
54    let scope = fasync::Scope::new();
55
56    let _inspect_controller = inspect_runtime::publish(
57        fuchsia_inspect::component::inspector(),
58        inspect_runtime::PublishOptions::default().custom_scope(scope.clone()),
59    );
60
61    fuchsia_inspect::component::health().set_starting_up();
62    let config = Config::take_from_startup_handle();
63
64    let cache_dir = Path::new("/cache");
65    if let Err(e) = rotate_active_to_previous_boot(cache_dir) {
66        warn!(e:?; "Error rotating active data to previous boot");
67    }
68
69    let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>();
70    let ready = ready_rx.shared();
71
72    let mut fs = ServiceFs::new();
73    fs.dir("svc").add_fidl_service(IncomingService::PreviousBootDataProvider);
74    fs.take_and_serve_directory_handle()?;
75
76    let cache_dir_buf = cache_dir.to_path_buf();
77    let ready_for_handler = ready.clone();
78    scope.spawn(async move {
79        fs.for_each_concurrent(None, |IncomingService::PreviousBootDataProvider(stream)| {
80            let cache_dir = cache_dir_buf.clone();
81            let ready = ready_for_handler.clone();
82            async move {
83                if let Err(e) = handle_previous_boot_data_provider(stream, &cache_dir, ready).await
84                {
85                    warn!(e:?; "Error handling PreviousBootDataProvider request stream");
86                }
87            }
88        })
89        .await;
90    });
91
92    if config.skip_update_check {
93        info!("Skipping the update check, publishing previous boot data");
94        let _ = ready_tx.send(());
95    } else {
96        scope.spawn(async move {
97            if let Err(e) = wait_for_update().await {
98                warn!(e:?; "Will not publish previous boot data");
99            } else {
100                let _ = ready_tx.send(());
101            }
102        });
103    }
104
105    let proxy = connect_to_protocol_at_path::<fdiagnostics::ArchiveAccessorMarker>(
106        "/svc/fuchsia.diagnostics.ArchiveAccessor.previous_boot",
107    )?;
108
109    let mut reader = ArchiveReader::inspect();
110    reader.with_archive(proxy);
111    let collector = Arc::new(Mutex::new(SnapshotCollector::new(reader, cache_dir.to_path_buf())));
112
113    let period = MonotonicDuration::from_seconds(config.persistence_period_seconds);
114    let collector_periodic = collector.clone();
115    scope.spawn(async move {
116        if let Err(e) = collector_periodic.lock().await.collect_active_snapshot().await {
117            error!(e:?; "Error collecting initial active inspect snapshot");
118        }
119        let mut interval = fasync::Interval::new(period);
120        while let Some(()) = interval.next().await {
121            if let Err(e) = collector_periodic.lock().await.collect_active_snapshot().await {
122                error!(e:?; "Error collecting active inspect snapshot");
123            }
124        }
125    });
126
127    if let Ok(battery_manager) = connect_to_protocol::<fbattery::BatteryManagerMarker>() {
128        let threshold = config.low_battery_threshold_percent as f32;
129        let collector_battery = collector.clone();
130        scope.spawn(async move {
131            if let Err(e) =
132                listen_for_low_battery(battery_manager, threshold, collector_battery).await
133            {
134                warn!(e:?; "BatteryManager watcher task terminated");
135            }
136        });
137    }
138
139    if config.enable_shutdown_snapshot {
140        if let Ok(shutdown_watcher_register) =
141            connect_to_protocol::<fpower::ShutdownWatcherRegisterMarker>()
142        {
143            let collector_shutdown = collector.clone();
144            scope.spawn(async move {
145                if let Err(e) =
146                    listen_for_shutdown(shutdown_watcher_register, collector_shutdown).await
147                {
148                    warn!(e:?; "Shutdown watcher task terminated");
149                }
150            });
151        }
152    } else {
153        info!("Shutdown snapshot trigger disabled by configuration");
154    }
155
156    fuchsia_inspect::component::health().set_ok();
157    scope.await;
158
159    Ok(())
160}
161
162async fn listen_for_low_battery(
163    battery_manager: fbattery::BatteryManagerProxy,
164    threshold_percent: f32,
165    collector: Arc<Mutex<SnapshotCollector>>,
166) -> Result<(), Error> {
167    let (watcher_client, mut request_stream) =
168        fidl::endpoints::create_request_stream::<fbattery::BatteryInfoWatcherMarker>();
169    battery_manager.watch(watcher_client)?;
170
171    let mut triggered = false;
172
173    while let Some(request) = request_stream.try_next().await? {
174        match request {
175            fbattery::BatteryInfoWatcherRequest::OnChangeBatteryInfo {
176                info,
177                wake_lease: _,
178                responder,
179            } => {
180                let level_percent = info.level_percent.unwrap_or(100.0);
181                let level_status = info.level_status.unwrap_or(fbattery::LevelStatus::Unknown);
182
183                let is_low = level_percent <= threshold_percent
184                    || level_status == fbattery::LevelStatus::Low
185                    || level_status == fbattery::LevelStatus::Critical;
186
187                if is_low {
188                    if !triggered {
189                        info!(
190                            level_percent,
191                            level_status:?;
192                            "Low battery threshold reached, collecting active snapshot"
193                        );
194                        if let Err(e) = collector.lock().await.collect_active_snapshot().await {
195                            error!(e:?; "Error collecting active snapshot on low battery");
196                        }
197                        triggered = true;
198                    }
199                } else if level_percent > threshold_percent + BATTERY_TRIGGER_HYSTERESIS_PERCENT {
200                    triggered = false;
201                }
202
203                responder.send()?;
204            }
205        }
206    }
207    Ok(())
208}
209
210async fn listen_for_shutdown(
211    shutdown_watcher_register: fpower::ShutdownWatcherRegisterProxy,
212    collector: Arc<Mutex<SnapshotCollector>>,
213) -> Result<(), Error> {
214    let (watcher_client, mut request_stream) =
215        fidl::endpoints::create_request_stream::<fpower::ShutdownWatcherMarker>();
216    shutdown_watcher_register.register_watcher(watcher_client).await?;
217
218    while let Some(request) = request_stream.try_next().await? {
219        match request {
220            fpower::ShutdownWatcherRequest::OnShutdown { options: _, responder } => {
221                info!("Shutdown initiated, collecting active snapshot");
222                if let Err(e) = collector.lock().await.collect_active_snapshot().await {
223                    error!(e:?; "Error collecting active snapshot on shutdown");
224                }
225                let _ = responder.send();
226                break;
227            }
228            fpower::ShutdownWatcherRequest::_UnknownMethod { .. } => {}
229        }
230    }
231    Ok(())
232}
233
234fn rotate_active_to_previous_boot(cache_dir: &Path) -> Result<(), Error> {
235    let active_dir = cache_dir.join("active");
236    let previous_boot_dir = cache_dir.join("previous_boot");
237
238    std::fs::create_dir_all(&active_dir)?;
239    std::fs::create_dir_all(&previous_boot_dir)?;
240
241    let active_file = active_dir.join("active.json");
242    let active_meta = active_dir.join("metadata.json");
243
244    info!(
245        active_exists = active_file.exists(),
246        meta_exists = active_meta.exists();
247        "Rotating active data to previous boot"
248    );
249
250    // Clean previous_boot directory first
251    for entry in std::fs::read_dir(&previous_boot_dir)? {
252        let entry = entry?;
253        let path = entry.path();
254        if path.is_file() {
255            let _ = std::fs::remove_file(path);
256        }
257    }
258
259    if active_file.exists() {
260        std::fs::rename(&active_file, previous_boot_dir.join("active.json"))?;
261    }
262    if active_meta.exists() {
263        std::fs::rename(&active_meta, previous_boot_dir.join("metadata.json"))?;
264    }
265
266    // Ensure active directory is clean
267    for entry in std::fs::read_dir(&active_dir)? {
268        let entry = entry?;
269        let path = entry.path();
270        if path.is_file() {
271            let _ = std::fs::remove_file(path);
272        }
273    }
274
275    Ok(())
276}
277
278fn maybe_get_utc_timestamp_from_clock<R: zx::Timeline, O: zx::Timeline>(
279    clock: Option<&zx::Clock<R, O>>,
280) -> Option<i64> {
281    let clock = clock?;
282    clock.read().ok().map(|time| time.into_nanos())
283}
284
285pub fn maybe_get_utc_timestamp() -> Option<i64> {
286    let clock = fuchsia_runtime::duplicate_utc_clock_handle(zx::Rights::SAME_RIGHTS).ok();
287    maybe_get_utc_timestamp_from_clock(clock.as_ref())
288}
289
290struct SnapshotCollector {
291    reader: InspectArchiveReader,
292    cache_dir: PathBuf,
293}
294
295impl SnapshotCollector {
296    fn new(reader: InspectArchiveReader, cache_dir: PathBuf) -> Self {
297        Self { reader, cache_dir }
298    }
299
300    async fn collect_active_snapshot(&mut self) -> Result<(), Error> {
301        info!("Collecting active snapshot...");
302        let inspect_data = match self.reader.snapshot().await {
303            Ok(v) => v,
304            Err(e) => {
305                error!(e:?; "ArchiveReader snapshot failed");
306                return Err(e.into());
307            }
308        };
309        let active_dir = self.cache_dir.join("active");
310        std::fs::create_dir_all(&active_dir)?;
311
312        let active_json_data = serde_json::to_vec(&inspect_data)?;
313        let tmp_file = active_dir.join("active.json.tmp");
314        let final_file = active_dir.join("active.json");
315        std::fs::write(&tmp_file, active_json_data)?;
316        std::fs::rename(&tmp_file, &final_file)?;
317
318        let metadata = Metadata {
319            monotonic_timestamp: MonotonicInstant::get().into_nanos(),
320            boot_timestamp: BootInstant::get().into_nanos(),
321            utc_timestamp_ns: maybe_get_utc_timestamp(),
322        };
323
324        let meta_json_data = serde_json::to_vec(&metadata)?;
325        let tmp_meta = active_dir.join("metadata.json.tmp");
326        let final_meta = active_dir.join("metadata.json");
327        std::fs::write(&tmp_meta, meta_json_data)?;
328        std::fs::rename(&tmp_meta, &final_meta)?;
329
330        Ok(())
331    }
332}
333
334async fn handle_previous_boot_data_provider(
335    mut stream: fpersistence::PreviousBootDataProviderRequestStream,
336    cache_dir: &Path,
337    ready: futures::future::Shared<futures::channel::oneshot::Receiver<()>>,
338) -> Result<(), Error> {
339    while let Some(request) = stream.try_next().await? {
340        match request {
341            fpersistence::PreviousBootDataProviderRequest::WatchPreviousBootData {
342                options: _,
343                responder,
344            } => {
345                info!("Received WatchPreviousBootData request, awaiting ready signal...");
346                if ready.clone().await.is_err() {
347                    warn!("Update check failed or cancelled; will not serve previous boot data");
348                    return Ok(());
349                }
350                info!("Ready signal received in WatchPreviousBootData handler");
351                let previous_boot_dir = cache_dir.join("previous_boot");
352                let active_file = previous_boot_dir.join("active.json");
353                let meta_file = previous_boot_dir.join("metadata.json");
354
355                if !active_file.exists() || !meta_file.exists() {
356                    responder.send(fpersistence::PreviousBootData::default())?;
357                    continue;
358                }
359
360                let meta_bytes = std::fs::read(&meta_file)?;
361                let metadata: Metadata = serde_json::from_slice(&meta_bytes)?;
362
363                let active_path_str =
364                    active_file.to_str().ok_or_else(|| anyhow::anyhow!("Invalid path"))?;
365                let file_proxy =
366                    fuchsia_fs::file::open_in_namespace(active_path_str, fio::PERM_READABLE)?;
367                let client_end = file_proxy
368                    .into_client_end()
369                    .map_err(|_| anyhow::anyhow!("Failed to convert to client end"))?;
370
371                let data = fpersistence::PreviousBootData {
372                    monotonic_timestamp: Some(MonotonicInstant::from_nanos(
373                        metadata.monotonic_timestamp,
374                    )),
375                    boot_timestamp: Some(BootInstant::from_nanos(metadata.boot_timestamp)),
376                    utc_timestamp_ns: metadata.utc_timestamp_ns,
377                    inspect: Some(client_end),
378                    ..Default::default()
379                };
380
381                responder.send(data)?;
382            }
383            fpersistence::PreviousBootDataProviderRequest::_UnknownMethod { .. } => {}
384        }
385    }
386    Ok(())
387}
388
389async fn wait_for_update() -> Result<(), Error> {
390    info!("Waiting for post-boot update check...");
391    let (notifier_client, mut notifier_request_stream) =
392        fidl::endpoints::create_request_stream::<fupdate::NotifierMarker>();
393    let proxy = connect_to_protocol::<fupdate::ListenerMarker>()?;
394    proxy.notify_on_first_update_check(fupdate::ListenerNotifyOnFirstUpdateCheckRequest {
395        notifier: Some(notifier_client),
396        ..Default::default()
397    })?;
398
399    match notifier_request_stream.try_next().await {
400        Ok(Some(fupdate::NotifierRequest::Notify { control_handle: _ })) => {}
401        Ok(None) => {
402            return Err(anyhow::anyhow!("Did not receive update notification; not publishing"));
403        }
404        Err(e) => {
405            return Err(anyhow::anyhow!(
406                "Error waiting for update notification; not publishing: {e}"
407            ));
408        }
409    }
410
411    // Start serving previous boot data
412    info!("...Update check has completed; publishing previous boot data");
413    Ok(())
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use tempfile::TempDir;
420
421    #[test]
422    fn test_metadata_serde() {
423        let meta = Metadata {
424            monotonic_timestamp: 1000,
425            boot_timestamp: 2000,
426            utc_timestamp_ns: Some(3000),
427        };
428
429        let serialized = serde_json::to_vec(&meta).unwrap();
430        let deserialized: Metadata = serde_json::from_slice(&serialized).unwrap();
431        assert_eq!(meta, deserialized);
432    }
433
434    #[test]
435    fn test_metadata_serde_without_utc() {
436        let meta =
437            Metadata { monotonic_timestamp: 1000, boot_timestamp: 2000, utc_timestamp_ns: None };
438
439        let serialized = serde_json::to_vec(&meta).unwrap();
440        let deserialized: Metadata = serde_json::from_slice(&serialized).unwrap();
441        assert_eq!(meta, deserialized);
442    }
443
444    #[test]
445    fn test_maybe_get_utc_timestamp_when_utc_not_available() {
446        // When no clock handle is available, returns None.
447        assert_eq!(
448            maybe_get_utc_timestamp_from_clock::<zx::MonotonicTimeline, zx::SyntheticTimeline>(
449                None
450            ),
451            None
452        );
453
454        // When a clock handle without read rights is provided, read fails and returns None.
455        let clock = zx::SyntheticClock::create(zx::ClockOpts::AUTO_START, None)
456            .expect("failed to create clock");
457        let no_read_clock = clock.duplicate_handle(zx::Rights::NONE).expect("duplicate handle");
458        assert_eq!(maybe_get_utc_timestamp_from_clock(Some(&no_read_clock)), None);
459
460        // When a valid readable clock is provided, returns Some timestamp.
461        assert!(maybe_get_utc_timestamp_from_clock(Some(&clock)).is_some());
462    }
463
464    #[test]
465    fn test_rotation() {
466        let _ = std::fs::create_dir_all("/cache");
467        let temp_dir = TempDir::new_in("/cache").unwrap();
468        let cache_dir = temp_dir.path();
469        let active_dir = cache_dir.join("active");
470        let previous_boot_dir = cache_dir.join("previous_boot");
471
472        std::fs::create_dir_all(&active_dir).unwrap();
473        std::fs::write(active_dir.join("active.json"), b"active_data").unwrap();
474        std::fs::write(active_dir.join("metadata.json"), b"meta_data").unwrap();
475
476        rotate_active_to_previous_boot(cache_dir).unwrap();
477
478        assert!(!active_dir.join("active.json").exists());
479        assert!(!active_dir.join("metadata.json").exists());
480        assert_eq!(std::fs::read(previous_boot_dir.join("active.json")).unwrap(), b"active_data");
481        assert_eq!(std::fs::read(previous_boot_dir.join("metadata.json")).unwrap(), b"meta_data");
482    }
483
484    #[test]
485    fn test_rotation_cleans_non_clean_active_dir() {
486        let _ = std::fs::create_dir_all("/cache");
487        let temp_dir = TempDir::new_in("/cache").unwrap();
488        let cache_dir = temp_dir.path();
489        let active_dir = cache_dir.join("active");
490        let previous_boot_dir = cache_dir.join("previous_boot");
491
492        std::fs::create_dir_all(&active_dir).unwrap();
493        std::fs::write(active_dir.join("active.json"), b"active_data").unwrap();
494        std::fs::write(active_dir.join("metadata.json"), b"meta_data").unwrap();
495        // Add leftover temporary or stray files in active dir
496        std::fs::write(active_dir.join("active.json.tmp"), b"tmp_active_data").unwrap();
497        std::fs::write(active_dir.join("metadata.json.tmp"), b"tmp_meta_data").unwrap();
498        std::fs::write(active_dir.join("stray.txt"), b"stray_data").unwrap();
499
500        rotate_active_to_previous_boot(cache_dir).unwrap();
501
502        // Active files should be rotated to previous_boot
503        assert_eq!(std::fs::read(previous_boot_dir.join("active.json")).unwrap(), b"active_data");
504        assert_eq!(std::fs::read(previous_boot_dir.join("metadata.json")).unwrap(), b"meta_data");
505        // Stray files should not be in previous_boot
506        assert!(!previous_boot_dir.join("active.json.tmp").exists());
507        assert!(!previous_boot_dir.join("metadata.json.tmp").exists());
508        assert!(!previous_boot_dir.join("stray.txt").exists());
509
510        // Ensure active directory was completely cleaned
511        assert_eq!(std::fs::read_dir(&active_dir).unwrap().count(), 0);
512    }
513
514    #[test]
515    fn test_rotation_clears_stale_previous_boot_when_active_empty() {
516        let _ = std::fs::create_dir_all("/cache");
517        let temp_dir = TempDir::new_in("/cache").unwrap();
518        let cache_dir = temp_dir.path();
519        let active_dir = cache_dir.join("active");
520        let previous_boot_dir = cache_dir.join("previous_boot");
521
522        std::fs::create_dir_all(&active_dir).unwrap();
523        std::fs::create_dir_all(&previous_boot_dir).unwrap();
524
525        // Populate previous_boot with stale data from Boot N-2
526        std::fs::write(previous_boot_dir.join("active.json"), b"stale_active_data").unwrap();
527        std::fs::write(previous_boot_dir.join("metadata.json"), b"stale_meta_data").unwrap();
528
529        // Active directory is empty (Boot N-1 captured no data)
530        rotate_active_to_previous_boot(cache_dir).unwrap();
531
532        // previous_boot directory MUST be cleared, not retaining stale Boot N-2 data
533        assert!(!previous_boot_dir.join("active.json").exists());
534        assert!(!previous_boot_dir.join("metadata.json").exists());
535    }
536
537    #[fasync::run_singlethreaded(test)]
538    async fn test_handle_previous_boot_data_provider_missing_files() {
539        let _ = std::fs::create_dir_all("/cache");
540        let temp_dir = TempDir::new_in("/cache").unwrap();
541        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
542            fpersistence::PreviousBootDataProviderMarker,
543        >();
544
545        let (ready_tx, ready_rx) = futures::channel::oneshot::channel();
546        let _ = ready_tx.send(());
547        let ready = ready_rx.shared();
548
549        let scope = fasync::Scope::new();
550        let cache_dir = temp_dir.path().to_path_buf();
551        scope.spawn(async move {
552            let _ = handle_previous_boot_data_provider(stream, &cache_dir, ready).await;
553        });
554
555        let data = proxy.watch_previous_boot_data(&Default::default()).await.unwrap();
556        assert!(data.inspect.is_none());
557        assert!(data.monotonic_timestamp.is_none());
558        assert!(data.boot_timestamp.is_none());
559        assert!(data.utc_timestamp_ns.is_none());
560        assert_eq!(data, fpersistence::PreviousBootData::default());
561    }
562
563    #[fasync::run_singlethreaded(test)]
564    async fn test_handle_previous_boot_data_provider_valid_data() {
565        let _ = std::fs::create_dir_all("/cache");
566        let temp_dir = TempDir::new_in("/cache").unwrap();
567        let previous_boot_dir = temp_dir.path().join("previous_boot");
568        std::fs::create_dir_all(&previous_boot_dir).unwrap();
569
570        let meta = Metadata {
571            monotonic_timestamp: 12345,
572            boot_timestamp: 67890,
573            utc_timestamp_ns: Some(100000),
574        };
575        let meta_bytes = serde_json::to_vec(&meta).unwrap();
576        std::fs::write(previous_boot_dir.join("metadata.json"), meta_bytes).unwrap();
577        std::fs::write(previous_boot_dir.join("active.json"), b"valid_inspect_json").unwrap();
578
579        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
580            fpersistence::PreviousBootDataProviderMarker,
581        >();
582
583        let (ready_tx, ready_rx) = futures::channel::oneshot::channel();
584        let _ = ready_tx.send(());
585        let ready = ready_rx.shared();
586
587        let scope = fasync::Scope::new();
588        let cache_dir = temp_dir.path().to_path_buf();
589        scope.spawn(async move {
590            let _ = handle_previous_boot_data_provider(stream, &cache_dir, ready).await;
591        });
592
593        let data = proxy.watch_previous_boot_data(&Default::default()).await.unwrap();
594        assert_eq!(data.monotonic_timestamp, Some(MonotonicInstant::from_nanos(12345)));
595        assert_eq!(data.boot_timestamp, Some(BootInstant::from_nanos(67890)));
596        assert_eq!(data.utc_timestamp_ns, Some(100000));
597        assert!(data.inspect.is_some());
598    }
599
600    #[fasync::run_singlethreaded(test)]
601    async fn test_handle_previous_boot_data_provider_utc_not_available() {
602        let _ = std::fs::create_dir_all("/cache");
603        let temp_dir = TempDir::new_in("/cache").unwrap();
604        let previous_boot_dir = temp_dir.path().join("previous_boot");
605        std::fs::create_dir_all(&previous_boot_dir).unwrap();
606
607        let meta =
608            Metadata { monotonic_timestamp: 12345, boot_timestamp: 67890, utc_timestamp_ns: None };
609        let meta_bytes = serde_json::to_vec(&meta).unwrap();
610        std::fs::write(previous_boot_dir.join("metadata.json"), meta_bytes).unwrap();
611        std::fs::write(previous_boot_dir.join("active.json"), b"valid_inspect_json").unwrap();
612
613        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
614            fpersistence::PreviousBootDataProviderMarker,
615        >();
616
617        let (ready_tx, ready_rx) = futures::channel::oneshot::channel();
618        let _ = ready_tx.send(());
619        let ready = ready_rx.shared();
620
621        let scope = fasync::Scope::new();
622        let cache_dir = temp_dir.path().to_path_buf();
623        scope.spawn(async move {
624            let _ = handle_previous_boot_data_provider(stream, &cache_dir, ready).await;
625        });
626
627        let data = proxy.watch_previous_boot_data(&Default::default()).await.unwrap();
628        assert_eq!(data.monotonic_timestamp, Some(MonotonicInstant::from_nanos(12345)));
629        assert_eq!(data.boot_timestamp, Some(BootInstant::from_nanos(67890)));
630        assert_eq!(data.utc_timestamp_ns, None);
631        assert!(data.inspect.is_some());
632    }
633
634    #[fasync::run_singlethreaded(test)]
635    async fn test_wait_for_update_no_listener() {
636        let res = wait_for_update().await;
637        assert!(res.is_err());
638    }
639
640    #[fasync::run_singlethreaded(test)]
641    async fn test_handle_previous_boot_data_provider_canceled_ready() {
642        let _ = std::fs::create_dir_all("/cache");
643        let temp_dir = TempDir::new_in("/cache").unwrap();
644        let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<
645            fpersistence::PreviousBootDataProviderMarker,
646        >();
647
648        let (ready_tx, ready_rx) = futures::channel::oneshot::channel::<()>();
649        drop(ready_tx);
650        let ready = ready_rx.shared();
651
652        let scope = fasync::Scope::new();
653        let cache_dir = temp_dir.path().to_path_buf();
654        scope.spawn(async move {
655            let _ = handle_previous_boot_data_provider(stream, &cache_dir, ready).await;
656        });
657
658        let res = proxy.watch_previous_boot_data(&Default::default()).await;
659        assert!(res.is_err());
660    }
661}