1use anyhow::{Result, anyhow};
9use fuchsia_async::{self as fasync, TimeoutExt};
10use fuchsia_runtime::UtcTimeline;
11use futures::TryFutureExt;
12use log::{debug, error};
13use serde::{Deserialize, Serialize};
14use std::path::Path;
15
16const PERSISTENT_STATE_PATH: &'static str = "/data/persistent_state.json";
18
19const PERSISTENT_STATE_IO_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
21
22const REBOOT_COUNT: u8 = 1;
24
25#[derive(Serialize, Deserialize, Debug, Default, Clone)]
30pub struct State {
31 num_reboots_until_rtc_write_allowed: u8,
34
35 #[serde(default)]
42 reference_boot_instant_ns: i64,
43
44 #[serde(default)]
47 reference_utc_instant_ns: i64,
48}
49
50impl State {
51 pub fn new(update_rtc: bool) -> Self {
52 let mut value = Self { ..Default::default() };
53 value.set_may_update_rtc(update_rtc);
54 value
55 }
56
57 pub fn may_update_rtc(&self) -> bool {
59 self.num_reboots_until_rtc_write_allowed == 0
60 }
61
62 pub fn set_may_update_rtc(&mut self, update_rtc: bool) {
63 self.num_reboots_until_rtc_write_allowed = match update_rtc {
64 true => 0,
65 false => REBOOT_COUNT,
68 };
69 }
70
71 pub fn get_rtc_reference(&self) -> (zx::BootInstant, zx::Instant<UtcTimeline>) {
73 let reference_boot = zx::BootInstant::from_nanos(self.reference_boot_instant_ns);
74 let reference_utc = zx::Instant::from_nanos(self.reference_utc_instant_ns);
75 (reference_boot, reference_utc)
76 }
77
78 pub fn set_rtc_reference(
88 &mut self,
89 boot_reference: zx::BootInstant,
90 utc_reference: zx::Instant<UtcTimeline>,
91 ) {
92 self.reference_boot_instant_ns = boot_reference.into_nanos();
93 self.reference_utc_instant_ns = utc_reference.into_nanos();
94 }
95
96 fn spend_one_reboot_count(&mut self) {
98 self.num_reboots_until_rtc_write_allowed =
99 self.num_reboots_until_rtc_write_allowed.saturating_sub(1);
100 }
101
102 pub async fn read_and_update() -> Result<Self> {
104 Self::read_and_update_internal(PERSISTENT_STATE_PATH).await
105 }
106
107 pub async fn read_and_update_internal<P: AsRef<Path>>(path: P) -> Result<Self> {
108 let path = path.as_ref();
109 let path_str = path.to_str().ok_or_else(|| anyhow!("non-utf8 path: {path:?}"))?;
110 let maybe_state_str: String = fuchsia_fs::file::read_in_namespace_to_string(path_str)
111 .map_err(|e| {
112 debug!("while reading: {e:?}");
113 anyhow::Error::from(e)
114 })
115 .on_timeout(fasync::MonotonicInstant::after(PERSISTENT_STATE_IO_TIMEOUT), || {
116 debug!("timed out reading persistent state from {path:?}");
117 Err(anyhow!("timed out reading persistent state from {path:?}"))
118 })
119 .await?;
120 let state: State = serde_json::from_str(&maybe_state_str).map_err(|e| {
121 debug!("while deserializing: {e:?}");
122 e
123 })?;
124 log::info!("read persistent RTC state: {state:?}");
126 let mut next = state.clone();
127 next.spend_one_reboot_count();
128 Self::write_internal(path, &next).await?;
129 Ok(state)
130 }
131
132 pub async fn write(state: &Self) -> Result<()> {
134 Self::write_internal(PERSISTENT_STATE_PATH, state).await
135 }
136
137 pub async fn write_internal<P: AsRef<Path>>(path: P, state: &Self) -> Result<()> {
138 let path = path.as_ref();
139 let path_str = path.to_str().ok_or_else(|| anyhow!("non-utf8 path: {path:?}"))?;
140 log::info!("writing persistent RTC state: {state:?} to {path:?}");
142 let state_str = serde_json::to_string(state).map_err(|e| {
143 error!("while serializing state: {e:?}");
144 e
145 })?;
146 fuchsia_fs::file::write_in_namespace(path_str, state_str)
147 .map_err(|e| {
148 error!("while writing persistent state: {path:?}: {e:?}");
149 anyhow::Error::from(e)
150 })
151 .on_timeout(fasync::MonotonicInstant::after(PERSISTENT_STATE_IO_TIMEOUT), || {
152 error!("timed out writing persistent state: {path:?}");
153 Err(anyhow!("timed out writing persistent state: {path:?}"))
154 })
155 .await
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use fidl_fuchsia_io as fio;
163 use std::pin::pin;
164
165 #[fuchsia::test]
166 async fn test_roundtrip() {
167 let d = tempfile::TempDir::new().expect("tempdir created");
168 let p = d.path().join("file.json");
169 let s = State::new(false);
170 State::write_internal(&p, &s).await.unwrap();
171
172 assert!(!State::read_and_update_internal(&p).await.unwrap().may_update_rtc());
174 assert!(State::read_and_update_internal(&p).await.unwrap().may_update_rtc());
176 }
177
178 #[fuchsia::test]
179 fn test_read_and_write_timeout() {
180 let mut executor = fasync::TestExecutor::new_with_fake_time();
181 executor.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
182
183 let (dir_client, _dir_server) = fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
186 let ns = fdio::Namespace::installed().expect("installed namespace");
187 ns.bind("/stalled_persistence_dir", dir_client).expect("bind stalled dir");
188
189 let path = "/stalled_persistence_dir/state.json";
190 let state = State::new(true);
191
192 {
194 let mut write_fut = pin!(State::write_internal(path, &state));
195 assert!(executor.run_until_stalled(&mut write_fut).is_pending());
196
197 executor.set_fake_time(fasync::MonotonicInstant::after(
198 PERSISTENT_STATE_IO_TIMEOUT - zx::MonotonicDuration::from_millis(1),
199 ));
200 assert!(executor.run_until_stalled(&mut write_fut).is_pending());
201
202 executor.set_fake_time(fasync::MonotonicInstant::after(
203 zx::MonotonicDuration::from_millis(2),
204 ));
205 let res = executor.run_until_stalled(&mut write_fut);
206 match res {
207 std::task::Poll::Ready(Err(err)) => {
208 assert!(err.to_string().contains("timed out"), "unexpected error: {err:?}");
209 }
210 other => panic!("expected Ready(Err), got {other:?}"),
211 }
212 }
213
214 {
216 let mut read_fut = pin!(State::read_and_update_internal(path));
217 assert!(executor.run_until_stalled(&mut read_fut).is_pending());
218
219 executor.set_fake_time(fasync::MonotonicInstant::after(
220 PERSISTENT_STATE_IO_TIMEOUT + zx::MonotonicDuration::from_millis(1),
221 ));
222 let res = executor.run_until_stalled(&mut read_fut);
223 match res {
224 std::task::Poll::Ready(Err(err)) => {
225 assert!(err.to_string().contains("timed out"), "unexpected error: {err:?}");
226 }
227 other => panic!("expected Ready(Err), got {other:?}"),
228 }
229 }
230
231 let _ = ns.unbind("/stalled_persistence_dir");
232 }
233}