Skip to main content

update_rs/
state.rs

1use serde::{Deserialize, Serialize};
2use std::fmt::Display;
3use std::path::PathBuf;
4
5/// The phase of the three-phase update process which an [`UpdateState`] is in.
6///
7/// The update is driven forward by relaunching the application between phases,
8/// passing a serialized [`UpdateState`] each time (see
9/// [`RESUME_FLAG`](crate::RESUME_FLAG)). Each phase runs from a *different*
10/// binary so that the running executable is never asked to overwrite itself.
11#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
12pub enum UpdatePhase {
13    /// No update is in progress (the default, used when resuming with no state).
14    #[default]
15    #[serde(rename = "no-update")]
16    NoUpdate,
17    /// The original application has downloaded the new binary and is about to
18    /// launch it to perform the `replace` phase.
19    #[serde(rename = "prepare")]
20    Prepare,
21    /// The freshly downloaded binary overwrites the original application and
22    /// launches it to perform the `cleanup` phase.
23    #[serde(rename = "replace")]
24    Replace,
25    /// The updated original application removes the temporary downloaded binary.
26    #[serde(rename = "cleanup")]
27    Cleanup,
28}
29
30impl Display for UpdatePhase {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            UpdatePhase::NoUpdate => write!(f, "no-update"),
34            UpdatePhase::Prepare => write!(f, "prepare"),
35            UpdatePhase::Replace => write!(f, "replace"),
36            UpdatePhase::Cleanup => write!(f, "cleanup"),
37        }
38    }
39}
40
41/// The serializable state which is threaded through the three phases of an
42/// update by relaunching the application with the
43/// [`RESUME_FLAG`](crate::RESUME_FLAG) followed by this value as JSON.
44#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
45pub struct UpdateState {
46    /// The path to the application binary which is being updated.
47    #[serde(rename = "app", default, skip_serializing_if = "Option::is_none")]
48    pub target_application: Option<PathBuf>,
49
50    /// The path to the temporary binary which the new release was downloaded to.
51    #[serde(rename = "update", default, skip_serializing_if = "Option::is_none")]
52    pub temporary_application: Option<PathBuf>,
53
54    /// The phase of the update process which this state represents.
55    pub phase: UpdatePhase,
56}
57
58impl UpdateState {
59    /// Produce a copy of this state advanced to the provided [`UpdatePhase`],
60    /// preserving the application paths.
61    pub fn for_phase(&self, phase: UpdatePhase) -> Self {
62        UpdateState {
63            target_application: self.target_application.clone(),
64            temporary_application: self.temporary_application.clone(),
65            phase,
66        }
67    }
68}
69
70impl Display for UpdateState {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "{:?}", self.phase)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_serialize() {
82        assert_eq!(
83            serde_json::to_string(&UpdateState {
84                target_application: Some(PathBuf::from("/bin/app")),
85                temporary_application: Some(PathBuf::from("/tmp/app-update")),
86                phase: UpdatePhase::Replace
87            })
88            .unwrap(),
89            r#"{"app":"/bin/app","update":"/tmp/app-update","phase":"replace"}"#
90        );
91
92        assert_eq!(
93            serde_json::to_string(&UpdateState {
94                target_application: None,
95                temporary_application: Some(PathBuf::from("/tmp/app-update")),
96                phase: UpdatePhase::Cleanup
97            })
98            .unwrap(),
99            r#"{"update":"/tmp/app-update","phase":"cleanup"}"#
100        );
101    }
102
103    #[test]
104    fn test_deserialize() {
105        let update = UpdateState {
106            target_application: None,
107            temporary_application: Some(PathBuf::from("/tmp/app-update")),
108            phase: UpdatePhase::Cleanup,
109        };
110
111        let deserialized: UpdateState =
112            serde_json::from_str(r#"{"update":"/tmp/app-update","phase":"cleanup"}"#).unwrap();
113        assert_eq!(deserialized, update);
114    }
115
116    #[test]
117    fn test_to_string() {
118        assert_eq!(UpdatePhase::Prepare.to_string(), "prepare");
119        assert_eq!(UpdatePhase::Replace.to_string(), "replace");
120        assert_eq!(UpdatePhase::Cleanup.to_string(), "cleanup");
121        assert_eq!(UpdatePhase::NoUpdate.to_string(), "no-update");
122    }
123
124    #[test]
125    fn test_for_phase() {
126        let update = UpdateState {
127            target_application: Some(PathBuf::from("/bin/app")),
128            temporary_application: Some(PathBuf::from("/tmp/app-update")),
129            phase: UpdatePhase::Replace,
130        };
131
132        let new_update = update.for_phase(UpdatePhase::Cleanup);
133        assert_eq!(new_update.target_application, update.target_application);
134        assert_eq!(
135            new_update.temporary_application,
136            update.temporary_application
137        );
138        assert_eq!(
139            update.phase,
140            UpdatePhase::Replace,
141            "the old update entry should not be modified"
142        );
143        assert_eq!(
144            new_update.phase,
145            UpdatePhase::Cleanup,
146            "the new update entry should have the correct phase"
147        );
148    }
149}