omaha_client/state_machine/
update_check.rs

1// Copyright 2019 The Fuchsia Authors
2//
3// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
6// This file may not be copied, modified, or distributed except according to
7// those terms.
8
9/// The update_check module contains the structures and functions for performing a single update
10/// check with Omaha.
11use crate::{
12    common::{ProtocolState, UpdateCheckSchedule, UserCounting},
13    protocol::Cohort,
14    storage::{Storage, StorageExt},
15    time::PartialComplexTime,
16};
17use std::convert::{TryFrom, TryInto};
18use std::time::Duration;
19use tracing::error;
20
21// These are the keys used to persist data to storage.
22pub const CONSECUTIVE_FAILED_UPDATE_CHECKS: &str = "consecutive_failed_update_checks";
23pub const LAST_UPDATE_TIME: &str = "last_update_time";
24pub const SERVER_DICTATED_POLL_INTERVAL: &str = "server_dictated_poll_interval";
25
26/// The Context provides the protocol context for a given update check operation.
27///
28/// The Context provides the information that's passed to the Policy to allow
29/// it to properly reason about what can and cannot be done at this time.
30#[derive(Clone, Debug)]
31pub struct Context {
32    /// The last-computed time to next check for an update.
33    pub schedule: UpdateCheckSchedule,
34
35    /// The state of the protocol (retries, errors, etc.) as of the last update check that was
36    /// attempted.
37    pub state: ProtocolState,
38}
39
40impl Context {
41    /// Load and initialize update check context from persistent storage.
42    pub async fn load(storage: &impl Storage) -> Self {
43        let last_update_time = storage
44            .get_time(LAST_UPDATE_TIME)
45            .await
46            .map(PartialComplexTime::Wall);
47        let server_dictated_poll_interval = storage
48            .get_int(SERVER_DICTATED_POLL_INTERVAL)
49            .await
50            .and_then(|t| u64::try_from(t).ok())
51            .map(Duration::from_micros);
52
53        let consecutive_failed_update_checks: u32 = storage
54            .get_int(CONSECUTIVE_FAILED_UPDATE_CHECKS)
55            .await
56            .unwrap_or(0)
57            .try_into()
58            .unwrap_or_default();
59
60        // last_check_time isn't really last_update_time, but we're not persisting our
61        // between-check wall time for reporting, and this is a reasonable-enough proxy.
62        Context {
63            schedule: UpdateCheckSchedule::builder()
64                .last_update_time(last_update_time)
65                .last_update_check_time(last_update_time)
66                .build(),
67            state: ProtocolState {
68                server_dictated_poll_interval,
69                consecutive_failed_update_checks,
70                ..Default::default()
71            },
72        }
73    }
74
75    /// Persist data in Context to |storage|, will try to set all of them to storage even if
76    /// previous set fails.
77    /// It will NOT call commit() on |storage|, caller is responsible to call commit().
78    pub async fn persist<'a>(&'a self, storage: &'a mut impl Storage) {
79        if let Err(e) = storage
80            .set_option_int(
81                LAST_UPDATE_TIME,
82                self.schedule
83                    .last_update_time
84                    .and_then(PartialComplexTime::checked_to_micros_since_epoch),
85            )
86            .await
87        {
88            error!("Unable to persist {}: {}", LAST_UPDATE_TIME, e);
89        }
90
91        if let Err(e) = storage
92            .set_option_int(
93                SERVER_DICTATED_POLL_INTERVAL,
94                self.state
95                    .server_dictated_poll_interval
96                    .map(|t| t.as_micros())
97                    .and_then(|t| i64::try_from(t).ok()),
98            )
99            .await
100        {
101            error!("Unable to persist {}: {}", SERVER_DICTATED_POLL_INTERVAL, e);
102        }
103
104        // By converting to an option, set_option_int will clean up storage associated with this
105        // value if it's the default (0).
106        let consecutive_failed_update_checks_option = {
107            if self.state.consecutive_failed_update_checks == 0 {
108                None
109            } else {
110                Some(self.state.consecutive_failed_update_checks as i64)
111            }
112        };
113
114        if let Err(e) = storage
115            .set_option_int(
116                CONSECUTIVE_FAILED_UPDATE_CHECKS,
117                consecutive_failed_update_checks_option,
118            )
119            .await
120        {
121            error!(
122                "Unable to persist {}: {}",
123                CONSECUTIVE_FAILED_UPDATE_CHECKS, e
124            );
125        }
126    }
127}
128
129/// The response context from the update check contains any extra information that Omaha returns to
130/// the client, separate from the data about a particular app itself.
131#[derive(Debug)]
132pub struct Response {
133    /// The set of responses for all the apps in the request.
134    pub app_responses: Vec<AppResponse>,
135}
136
137/// For each application that had an update check performed, a new App (potentially with new Cohort
138/// and UserCounting data) and a corresponding response Action are returned from the update check.
139#[derive(Debug)]
140pub struct AppResponse {
141    /// The returned information about an application.
142    pub app_id: String,
143
144    /// Cohort data returned from Omaha
145    pub cohort: Cohort,
146
147    pub user_counting: UserCounting,
148
149    /// The resultant action of its update check.
150    pub result: Action,
151}
152
153/// The Action is the result of an update check for a single App.
154///
155/// This is just informational, for the purposes of updating the protocol state.
156/// Any update action should already have been taken by the Installer.
157#[derive(Debug, Clone, PartialEq)]
158pub enum Action {
159    /// Omaha's response was "no update"
160    NoUpdate,
161
162    /// Policy deferred the update.  The update check was successful, and Omaha returned that an
163    /// update is available, but it is not able to be acted on at this time.
164    DeferredByPolicy,
165
166    /// Policy Denied the update.  The update check was successful, and Omaha returned that an
167    /// update is available, but it is not allowed to be installed per Policy.
168    DeniedByPolicy,
169
170    /// The install process encountered an error.
171    /// TODO: Attach an error to this
172    InstallPlanExecutionError,
173
174    /// An update was performed.
175    Updated,
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::storage::MemStorage;
182    use futures::executor::block_on;
183
184    #[test]
185    fn test_load_context() {
186        block_on(async {
187            let mut storage = MemStorage::new();
188            let last_update_time = 123456789;
189            let poll_interval = Duration::from_micros(56789u64);
190            storage
191                .set_int(LAST_UPDATE_TIME, last_update_time)
192                .await
193                .unwrap();
194            storage
195                .set_int(
196                    SERVER_DICTATED_POLL_INTERVAL,
197                    poll_interval.as_micros() as i64,
198                )
199                .await
200                .unwrap();
201
202            storage
203                .set_int(CONSECUTIVE_FAILED_UPDATE_CHECKS, 1234)
204                .await
205                .unwrap();
206
207            let context = Context::load(&storage).await;
208
209            let last_update_time = PartialComplexTime::from_micros_since_epoch(last_update_time);
210            assert_eq!(context.schedule.last_update_time, Some(last_update_time));
211            assert_eq!(
212                context.state.server_dictated_poll_interval,
213                Some(poll_interval)
214            );
215            assert_eq!(context.state.consecutive_failed_update_checks, 1234);
216        });
217    }
218
219    #[test]
220    fn test_load_context_empty_storage() {
221        block_on(async {
222            let storage = MemStorage::new();
223            let context = Context::load(&storage).await;
224            assert_eq!(None, context.schedule.last_update_time);
225            assert_eq!(None, context.state.server_dictated_poll_interval);
226            assert_eq!(0, context.state.consecutive_failed_update_checks);
227        });
228    }
229
230    #[test]
231    fn test_persist_context() {
232        block_on(async {
233            let mut storage = MemStorage::new();
234            let last_update_time = PartialComplexTime::from_micros_since_epoch(123456789);
235            let server_dictated_poll_interval = Some(Duration::from_micros(56789));
236            let consecutive_failed_update_checks = 1234;
237            let context = Context {
238                schedule: UpdateCheckSchedule::builder()
239                    .last_update_time(last_update_time)
240                    .build(),
241                state: ProtocolState {
242                    server_dictated_poll_interval,
243                    consecutive_failed_update_checks,
244                    ..ProtocolState::default()
245                },
246            };
247            context.persist(&mut storage).await;
248            assert_eq!(Some(123456789), storage.get_int(LAST_UPDATE_TIME).await);
249            assert_eq!(
250                Some(56789),
251                storage.get_int(SERVER_DICTATED_POLL_INTERVAL).await
252            );
253            assert_eq!(
254                Some(1234),
255                storage.get_int(CONSECUTIVE_FAILED_UPDATE_CHECKS).await
256            );
257            assert!(!storage.committed());
258        });
259    }
260
261    #[test]
262    fn test_persist_context_remove_defaults() {
263        block_on(async {
264            let mut storage = MemStorage::new();
265            let last_update_time = PartialComplexTime::from_micros_since_epoch(123456789);
266            storage
267                .set_int(SERVER_DICTATED_POLL_INTERVAL, 987654)
268                .await
269                .unwrap();
270            storage
271                .set_int(CONSECUTIVE_FAILED_UPDATE_CHECKS, 1234)
272                .await
273                .unwrap();
274
275            let context = Context {
276                schedule: UpdateCheckSchedule::builder()
277                    .last_update_time(last_update_time)
278                    .build(),
279                state: ProtocolState {
280                    server_dictated_poll_interval: None,
281                    consecutive_failed_update_checks: 0,
282                    ..ProtocolState::default()
283                },
284            };
285            context.persist(&mut storage).await;
286            assert_eq!(Some(123456789), storage.get_int(LAST_UPDATE_TIME).await);
287            assert_eq!(None, storage.get_int(SERVER_DICTATED_POLL_INTERVAL).await);
288            assert_eq!(
289                None,
290                storage.get_int(CONSECUTIVE_FAILED_UPDATE_CHECKS).await
291            );
292            assert!(!storage.committed());
293        });
294    }
295}