1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::fmt::Display;
4use std::path::PathBuf;
5
6#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
13pub enum UpdatePhase {
14 #[default]
16 #[serde(rename = "no-update")]
17 NoUpdate,
18 #[serde(rename = "prepare")]
21 Prepare,
22 #[serde(rename = "replace")]
25 Replace,
26 #[serde(rename = "cleanup")]
28 Cleanup,
29}
30
31impl Display for UpdatePhase {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 UpdatePhase::NoUpdate => write!(f, "no-update"),
35 UpdatePhase::Prepare => write!(f, "prepare"),
36 UpdatePhase::Replace => write!(f, "replace"),
37 UpdatePhase::Cleanup => write!(f, "cleanup"),
38 }
39 }
40}
41
42#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
46pub struct UpdateState {
47 #[serde(rename = "app", default, skip_serializing_if = "Option::is_none")]
49 pub target_application: Option<PathBuf>,
50
51 #[serde(rename = "update", default, skip_serializing_if = "Option::is_none")]
53 pub temporary_application: Option<PathBuf>,
54
55 #[serde(rename = "trace", default, skip_serializing_if = "Option::is_none")]
63 pub trace_context: Option<HashMap<String, String>>,
64
65 pub phase: UpdatePhase,
67}
68
69impl UpdateState {
70 pub fn for_phase(&self, phase: UpdatePhase) -> Self {
73 UpdateState {
74 target_application: self.target_application.clone(),
75 temporary_application: self.temporary_application.clone(),
76 trace_context: self.trace_context.clone(),
77 phase,
78 }
79 }
80
81 #[cfg(feature = "opentelemetry")]
92 pub(crate) fn capture_trace_context(&mut self) {
93 use tracing_opentelemetry::OpenTelemetrySpanExt;
94
95 let context = tracing::Span::current().context();
96 let mut carrier = HashMap::new();
97 opentelemetry::global::get_text_map_propagator(|propagator| {
98 propagator.inject_context(&context, &mut carrier);
99 });
100
101 if !carrier.is_empty() {
104 self.trace_context = Some(carrier);
105 }
106 }
107
108 #[cfg(not(feature = "opentelemetry"))]
110 pub(crate) fn capture_trace_context(&mut self) {}
111
112 #[cfg(feature = "tracing")]
121 pub(crate) fn adopt_trace_context(&self, span: &tracing::Span) {
122 #[cfg(feature = "opentelemetry")]
123 {
124 use tracing_opentelemetry::OpenTelemetrySpanExt;
125
126 if let Some(carrier) = &self.trace_context {
127 let parent = opentelemetry::global::get_text_map_propagator(|propagator| {
128 propagator.extract(carrier)
129 });
130 let _ = span.set_parent(parent);
133 }
134 }
135
136 #[cfg(not(feature = "opentelemetry"))]
138 let _ = span;
139 }
140}
141
142impl Display for UpdateState {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 write!(f, "{:?}", self.phase)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_serialize() {
154 assert_eq!(
155 serde_json::to_string(&UpdateState {
156 target_application: Some(PathBuf::from("/bin/app")),
157 temporary_application: Some(PathBuf::from("/tmp/app-update")),
158 trace_context: None,
159 phase: UpdatePhase::Replace
160 })
161 .unwrap(),
162 r#"{"app":"/bin/app","update":"/tmp/app-update","phase":"replace"}"#
163 );
164
165 assert_eq!(
166 serde_json::to_string(&UpdateState {
167 target_application: None,
168 temporary_application: Some(PathBuf::from("/tmp/app-update")),
169 trace_context: None,
170 phase: UpdatePhase::Cleanup
171 })
172 .unwrap(),
173 r#"{"update":"/tmp/app-update","phase":"cleanup"}"#
174 );
175 }
176
177 #[test]
178 fn test_deserialize() {
179 let update = UpdateState {
180 target_application: None,
181 temporary_application: Some(PathBuf::from("/tmp/app-update")),
182 trace_context: None,
183 phase: UpdatePhase::Cleanup,
184 };
185
186 let deserialized: UpdateState =
187 serde_json::from_str(r#"{"update":"/tmp/app-update","phase":"cleanup"}"#).unwrap();
188 assert_eq!(deserialized, update);
189 }
190
191 #[test]
192 fn test_trace_context_round_trips_through_json() {
193 let mut carrier = HashMap::new();
194 carrier.insert(
195 "traceparent".to_string(),
196 "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
197 );
198 let state = UpdateState {
199 target_application: Some(PathBuf::from("/bin/app")),
200 temporary_application: Some(PathBuf::from("/tmp/app-update")),
201 trace_context: Some(carrier),
202 phase: UpdatePhase::Replace,
203 };
204
205 let json = serde_json::to_string(&state).unwrap();
206 assert!(
207 json.contains("\"trace\":{"),
208 "the carrier should serialize under the `trace` key: {json}"
209 );
210
211 let restored: UpdateState = serde_json::from_str(&json).unwrap();
212 assert_eq!(
213 restored, state,
214 "the trace context should survive a serialize/deserialize round-trip"
215 );
216 }
217
218 #[test]
219 fn test_to_string() {
220 assert_eq!(UpdatePhase::Prepare.to_string(), "prepare");
221 assert_eq!(UpdatePhase::Replace.to_string(), "replace");
222 assert_eq!(UpdatePhase::Cleanup.to_string(), "cleanup");
223 assert_eq!(UpdatePhase::NoUpdate.to_string(), "no-update");
224 }
225
226 #[test]
227 fn test_for_phase() {
228 let mut carrier = HashMap::new();
229 carrier.insert("traceparent".to_string(), "abc".to_string());
230 let update = UpdateState {
231 target_application: Some(PathBuf::from("/bin/app")),
232 temporary_application: Some(PathBuf::from("/tmp/app-update")),
233 trace_context: Some(carrier),
234 phase: UpdatePhase::Replace,
235 };
236
237 let new_update = update.for_phase(UpdatePhase::Cleanup);
238 assert_eq!(new_update.target_application, update.target_application);
239 assert_eq!(
240 new_update.temporary_application,
241 update.temporary_application
242 );
243 assert_eq!(
244 new_update.trace_context, update.trace_context,
245 "the trace context should be carried forward to the next phase"
246 );
247 assert_eq!(
248 update.phase,
249 UpdatePhase::Replace,
250 "the old update entry should not be modified"
251 );
252 assert_eq!(
253 new_update.phase,
254 UpdatePhase::Cleanup,
255 "the new update entry should have the correct phase"
256 );
257 }
258}
259
260#[cfg(all(test, feature = "opentelemetry"))]
265mod opentelemetry_tests {
266 use super::*;
267 use opentelemetry::trace::TracerProvider as _;
268 use opentelemetry_sdk::propagation::TraceContextPropagator;
269 use opentelemetry_sdk::trace::SdkTracerProvider;
270 use tracing_subscriber::prelude::*;
271
272 fn trace_id_of(carrier: &HashMap<String, String>) -> String {
275 carrier
276 .get("traceparent")
277 .expect("the W3C propagator should emit a traceparent header")
278 .split('-')
279 .nth(1)
280 .expect("a traceparent has a trace-id segment")
281 .to_string()
282 }
283
284 fn subscriber() -> impl tracing::Subscriber {
285 let tracer = SdkTracerProvider::builder()
286 .build()
287 .tracer("update-rs-test");
288 tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer))
289 }
290
291 #[test]
292 fn trace_context_continues_across_a_relaunch() {
293 opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
294
295 let captured = tracing::subscriber::with_default(subscriber(), || {
297 let span = tracing::info_span!("phase-n");
298 let _enter = span.enter();
299 let mut state = UpdateState {
300 phase: UpdatePhase::Replace,
301 ..Default::default()
302 };
303 state.capture_trace_context();
304 state
305 });
306
307 let carrier = captured
308 .trace_context
309 .clone()
310 .expect("an active span should produce a trace context");
311 let expected_trace_id = trace_id_of(&carrier);
312
313 let json = serde_json::to_string(&captured).unwrap();
315 let resumed: UpdateState = serde_json::from_str(&json).unwrap();
316
317 let continued_trace_id = tracing::subscriber::with_default(subscriber(), || {
321 let span = tracing::info_span!("phase-n-plus-1");
322 resumed.adopt_trace_context(&span);
323 span.in_scope(|| {
324 let mut next = UpdateState {
325 phase: UpdatePhase::Cleanup,
326 ..Default::default()
327 };
328 next.capture_trace_context();
329 trace_id_of(
330 &next
331 .trace_context
332 .expect("the adopted span should produce a trace context"),
333 )
334 })
335 });
336
337 assert_eq!(
338 continued_trace_id, expected_trace_id,
339 "the resumed phase should continue the trace captured before the relaunch"
340 );
341 }
342}