persistence/
file_handler.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
5use diagnostics_data::ExtendedMoniker;
6use glob::glob;
7use log::{info, warn};
8use persistence_config::{ServiceName, Tag};
9use serde::ser::SerializeMap;
10use serde::{Serialize, Serializer};
11use serde_json::Value;
12use std::collections::HashMap;
13use std::fs;
14
15const CURRENT_PATH: &str = "/cache/current";
16const PREVIOUS_PATH: &str = "/cache/previous";
17
18pub(crate) struct PersistSchema {
19    pub timestamps: Timestamps,
20    pub payload: PersistPayload,
21}
22
23pub(crate) enum PersistPayload {
24    Data(PersistData),
25    Error(String),
26}
27
28pub(crate) struct PersistData {
29    pub data_length: usize,
30    pub entries: HashMap<ExtendedMoniker, Value>,
31}
32
33#[derive(Clone, Serialize)]
34pub(crate) struct Timestamps {
35    pub before_monotonic: i64,
36    pub before_utc: i64,
37    pub after_monotonic: i64,
38    pub after_utc: i64,
39}
40
41// Keys for JSON per-tag metadata to be persisted and published
42const TIMESTAMPS_KEY: &str = "@timestamps";
43const SIZE_KEY: &str = "@persist_size";
44const ERROR_KEY: &str = ":error";
45const ERROR_DESCRIPTION_KEY: &str = "description";
46
47impl Serialize for PersistSchema {
48    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
49    where
50        S: Serializer,
51    {
52        match &self.payload {
53            PersistPayload::Data(data) => {
54                let mut s = serializer.serialize_map(Some(data.entries.len() + 2))?;
55                s.serialize_entry(TIMESTAMPS_KEY, &self.timestamps)?;
56                s.serialize_entry(SIZE_KEY, &data.data_length)?;
57                for (k, v) in data.entries.iter() {
58                    s.serialize_entry(&k.to_string(), v)?;
59                }
60                s.end()
61            }
62            PersistPayload::Error(error) => {
63                let mut s = serializer.serialize_map(Some(2))?;
64                s.serialize_entry(TIMESTAMPS_KEY, &self.timestamps)?;
65                s.serialize_entry(ERROR_KEY, &ErrorHelper(error))?;
66                s.end()
67            }
68        }
69    }
70}
71
72impl PersistSchema {
73    pub(crate) fn error(timestamps: Timestamps, description: String) -> Self {
74        Self { timestamps, payload: PersistPayload::Error(description) }
75    }
76}
77
78struct ErrorHelper<'a>(&'a str);
79
80impl Serialize for ErrorHelper<'_> {
81    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
82    where
83        S: Serializer,
84    {
85        let mut s = serializer.serialize_map(Some(1))?;
86        s.serialize_entry(ERROR_DESCRIPTION_KEY, self.0)?;
87        s.end()
88    }
89}
90
91// Throw away stuff from two boots ago. Move stuff in the "current"
92// directory to the "previous" directory.
93pub fn shuffle_at_boot() {
94    // These may fail if /cache was wiped. This is WAI and should not signal an error.
95    fs::remove_dir_all(PREVIOUS_PATH)
96        .map_err(|e| info!("Could not delete {}: {:?}", PREVIOUS_PATH, e))
97        .ok();
98    fs::rename(CURRENT_PATH, PREVIOUS_PATH)
99        .map_err(|e| info!("Could not move {} to {}: {:?}", CURRENT_PATH, PREVIOUS_PATH, e))
100        .ok();
101}
102
103// Write a VMO's contents to the appropriate file.
104pub(crate) fn write(service_name: &ServiceName, tag: &Tag, data: &PersistSchema) {
105    // /cache/ may be deleted any time. It's OK to try to create CURRENT_PATH if it already exists.
106    let path = format!("{}/{}", CURRENT_PATH, service_name);
107    fs::create_dir_all(&path)
108        .map_err(|e| warn!("Could not create directory {}: {:?}", path, e))
109        .ok();
110    let data = match serde_json::to_string(data) {
111        Ok(data) => data,
112        Err(e) => {
113            warn!("Could not serialize data - unexpected error {e}");
114            return;
115        }
116    };
117    fs::write(format!("{}/{}", path, tag), data)
118        .map_err(|e| warn!("Could not write file {}/{}: {:?}", path, tag, e))
119        .ok();
120}
121
122pub(crate) struct ServiceEntry {
123    pub name: String,
124    pub data: Vec<TagEntry>,
125}
126
127pub(crate) struct TagEntry {
128    pub name: String,
129    pub data: String,
130}
131
132/// Read persisted data from the previous boot.
133// TODO(https://fxbug.dev/42150693): If this gets big, use Lazy Inspect.
134pub(crate) fn remembered_data() -> impl Iterator<Item = ServiceEntry> {
135    // Iterate over all subdirectories of /cache/previous which contains
136    // persisted data from the last boot.
137    glob(&format!("{PREVIOUS_PATH}/*"))
138        .expect("Failed to read previous-path glob pattern")
139        .filter_map(|p| match p {
140            Ok(path) => {
141                path.file_name().map(|p| p.to_string_lossy().to_string())
142            }
143            Err(e) => {
144                warn!("Encountered GlobError; contents could not be read to determine if glob pattern was matched: {e:?}");
145                None
146            }
147        })
148        .map(|service_name| {
149            let entries: Vec<TagEntry> = glob(&format!("{PREVIOUS_PATH}/{service_name}/*"))
150                .expect("Failed to read previous service persistence pattern")
151                .filter_map(|p| match p {
152                    Ok(path) => path
153                        .file_name()
154                        .map(|tag| (path.clone(), tag.to_string_lossy().to_string())),
155                    Err(ref e) => {
156                        warn!("Failed to retrieve text persisted at path {p:?}: {e:?}");
157                        None
158                    }
159                })
160                .filter_map(|(path, tag)| match fs::read(&path) {
161                    // TODO(cphoenix): We want to encode failures at retrieving persisted
162                    // metrics in the inspect hierarchy so clients know why their data is
163                    // missing.
164                    Ok(text) => match std::str::from_utf8(&text) {
165                        Ok(contents) => Some(TagEntry { name: tag, data: contents.to_owned() }),
166                        Err(e) => {
167                            warn!("Failed to parse persisted bytes at path: {path:?} into text: {e:?}");
168                            None
169                        }
170                    },
171                    Err(e) => {
172                        warn!("Failed to retrieve text persisted at path: {path:?}: {e:?}");
173                        None
174                    }
175                })
176                .collect();
177
178            if entries.is_empty() {
179                info!("No data available to persist for {service_name:?}.");
180            } else {
181                info!("{} data entries available to persist for {service_name:?}.", entries.len());
182            }
183
184            ServiceEntry { name: service_name, data: entries }
185        })
186}