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::{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// Forget persisted inspect data from two boots ago, except for tags with
92// persist_across_boot enabled.
93//
94// Persisted inspect data is held in both /cache/current and /cache/previous,
95// corresponding to the current and previous boot, respectively. When a boot
96// occurs, this function will move /cache/current to /cache/previous then copy
97// tags with persist_across_boot back into /cache/current.
98pub fn forget_old_data(config: &Config) {
99    info!("Forgetting persisted inspect data from two boots ago, except for tags with persist_across_boot enabled");
100
101    // These may fail if /cache was wiped. This is WAI and should not signal an error.
102    fs::remove_dir_all(PREVIOUS_PATH)
103        .map_err(|e| info!("Could not delete {}: {:?}", PREVIOUS_PATH, e))
104        .ok();
105    fs::rename(CURRENT_PATH, PREVIOUS_PATH)
106        .map_err(|e| info!("Could not move {} to {}: {:?}", CURRENT_PATH, PREVIOUS_PATH, e))
107        .ok();
108
109    // Copy tags that should persist across multiple reboots.
110    let mut copied_count = 0;
111
112    for (service, tag) in config.iter().flat_map(|(service, tags)| {
113        tags.iter().filter(|(_, c)| c.persist_across_boot).map(move |(tag, _)| (service, tag))
114    }) {
115        match fs::read(format!("{PREVIOUS_PATH}/{service}/{tag}")) {
116            Ok(data) => {
117                match fs::create_dir(format!("{CURRENT_PATH}/{service}")) {
118                    Ok(()) => {}
119                    Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
120                    Err(e) => {
121                        warn!("Error creating directory {CURRENT_PATH}/{service}: {e:?}");
122                        continue;
123                    }
124                }
125                match fs::write(format!("{CURRENT_PATH}/{service}/{tag}"), data) {
126                    Ok(()) => {
127                        copied_count += 1;
128                    }
129                    Err(e) => {
130                        warn!("Error writing persisted data for {service}/{tag}: {e:?}");
131                    }
132                }
133            }
134            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
135                // No data was made available in the last boot.
136            }
137            Err(e) => {
138                warn!("Error reading persisted data for {service}/{tag}: {e:?}")
139            }
140        }
141    }
142
143    info!("Persisted {copied_count} tags across boot");
144}
145
146// Write a VMO's contents to the appropriate file.
147pub(crate) fn write(service_name: &ServiceName, tag: &Tag, data: &PersistSchema) {
148    // /cache/ may be deleted any time. It's OK to try to create CURRENT_PATH if it already exists.
149    let path = format!("{}/{}", CURRENT_PATH, service_name);
150    fs::create_dir_all(&path)
151        .map_err(|e| warn!("Could not create directory {}: {:?}", path, e))
152        .ok();
153    let data = match serde_json::to_string(data) {
154        Ok(data) => data,
155        Err(e) => {
156            warn!("Could not serialize data - unexpected error {e}");
157            return;
158        }
159    };
160    fs::write(format!("{}/{}", path, tag), data)
161        .map_err(|e| warn!("Could not write file {}/{}: {:?}", path, tag, e))
162        .ok();
163}
164
165pub(crate) struct ServiceEntry {
166    pub name: String,
167    pub data: Vec<TagEntry>,
168}
169
170pub(crate) struct TagEntry {
171    pub name: String,
172    pub data: String,
173}
174
175/// Read persisted data from the previous boot.
176// TODO(https://fxbug.dev/42150693): If this gets big, use Lazy Inspect.
177pub(crate) fn remembered_data() -> impl Iterator<Item = ServiceEntry> {
178    // Iterate over all subdirectories of /cache/previous which contains
179    // persisted data from the last boot.
180    glob(&format!("{PREVIOUS_PATH}/*"))
181        .expect("Failed to read previous-path glob pattern")
182        .filter_map(|p| match p {
183            Ok(path) => {
184                path.file_name().map(|p| p.to_string_lossy().to_string())
185            }
186            Err(e) => {
187                warn!("Encountered GlobError; contents could not be read to determine if glob pattern was matched: {e:?}");
188                None
189            }
190        })
191        .map(|service_name| {
192            let entries: Vec<TagEntry> = glob(&format!("{PREVIOUS_PATH}/{service_name}/*"))
193                .expect("Failed to read previous service persistence pattern")
194                .filter_map(|p| match p {
195                    Ok(path) => path
196                        .file_name()
197                        .map(|tag| (path.clone(), tag.to_string_lossy().to_string())),
198                    Err(ref e) => {
199                        warn!("Failed to retrieve text persisted at path {p:?}: {e:?}");
200                        None
201                    }
202                })
203                .filter_map(|(path, tag)| match fs::read(&path) {
204                    // TODO(cphoenix): We want to encode failures at retrieving persisted
205                    // metrics in the inspect hierarchy so clients know why their data is
206                    // missing.
207                    Ok(text) => match std::str::from_utf8(&text) {
208                        Ok(contents) => Some(TagEntry { name: tag, data: contents.to_owned() }),
209                        Err(e) => {
210                            warn!("Failed to parse persisted bytes at path: {path:?} into text: {e:?}");
211                            None
212                        }
213                    },
214                    Err(e) => {
215                        warn!("Failed to retrieve text persisted at path: {path:?}: {e:?}");
216                        None
217                    }
218                })
219                .collect();
220
221            if entries.is_empty() {
222                info!("No data available to persist for {service_name:?}.");
223            } else {
224                info!("{} data entries available to persist for {service_name:?}.", entries.len());
225            }
226
227            ServiceEntry { name: service_name, data: entries }
228        })
229}