tauri_plugin_hot_update/
runtime.rs1use std::path::PathBuf;
14use std::sync::{Arc, OnceLock};
15
16use semver::Version;
17use serde::Serialize;
18
19use crate::machine::{self, AckOutcome, Active};
20use crate::store::Store;
21use crate::{Error, Result};
22
23#[derive(Default)]
27pub(crate) struct Shared {
28 activation: OnceLock<Activation>,
29}
30
31pub(crate) struct Activation {
33 store: Store,
34 active: Active,
37 active_dir: Option<PathBuf>,
39 active_version: Version,
41 embedded_version: Version,
42 update_lock: tokio::sync::Mutex<()>,
45}
46
47impl Activation {
48 pub(crate) fn store(&self) -> &Store {
49 &self.store
50 }
51
52 pub(crate) fn embedded_version(&self) -> &Version {
56 &self.embedded_version
57 }
58
59 pub(crate) fn update_lock(&self) -> &tokio::sync::Mutex<()> {
60 &self.update_lock
61 }
62}
63
64impl Shared {
65 pub(crate) fn active_dir(&self) -> Option<&PathBuf> {
68 self.activation.get()?.active_dir.as_ref()
69 }
70
71 pub(crate) fn is_activated(&self) -> bool {
72 self.activation.get().is_some()
73 }
74
75 pub(crate) fn activation(&self) -> Result<&Activation> {
76 self.activation.get().ok_or(Error::NotActive)
77 }
78}
79
80pub(crate) fn initialize(shared: &Shared, root: PathBuf, embedded_version: Version) {
84 let store = Store::new(root);
85 let state = store.load_state();
86 let present = store.present_seqs();
87 let outcome = machine::resolve_boot(state, &embedded_version, &present);
88
89 if let Some(seq) = outcome.rolled_back {
90 log::warn!(
91 "hot-update: bundle seq {seq} was armed last boot but never acked; \
92 rolled back and blacklisted its archive hash"
93 );
94 }
95
96 let active = match store.save_state(&outcome.state) {
101 Ok(()) => {
102 store.apply_effects(&outcome.effects);
103 store.sweep_foreign_entries();
104 outcome.active
105 }
106 Err(e) => {
107 log::error!(
108 "hot-update: failed to persist state.json ({e}); \
109 serving committed/embedded without arming the staged bundle"
110 );
111 match outcome.state.committed {
112 Some(seq) => Active::Ota(seq),
113 None => Active::Embedded,
114 }
115 }
116 };
117
118 let (active_dir, active_version) = match active {
119 Active::Ota(seq) => {
120 let version = outcome
122 .state
123 .versions
124 .get(&seq)
125 .map(|meta| meta.version.clone())
126 .unwrap_or_else(|| embedded_version.clone());
127 (Some(store.bundle_dir(seq)), version)
128 }
129 Active::Embedded => (None, embedded_version.clone()),
130 };
131
132 match active {
133 Active::Ota(seq) => log::info!(
134 "hot-update: serving OTA bundle seq {seq} (v{active_version}) from {}",
135 store.bundle_dir(seq).display()
136 ),
137 Active::Embedded => {
138 log::info!("hot-update: serving embedded assets (v{embedded_version})")
139 }
140 }
141
142 let activation = Activation {
143 store,
144 active,
145 active_dir,
146 active_version,
147 embedded_version,
148 update_lock: tokio::sync::Mutex::new(()),
149 };
150 if shared.activation.set(activation).is_err() {
151 log::warn!("hot-update: initialize called twice; keeping the first activation");
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
157#[serde(rename_all = "lowercase")]
158pub enum BundleSource {
159 Embedded,
160 Ota,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
165#[serde(rename_all = "camelCase")]
166pub struct CurrentBundle {
167 pub source: BundleSource,
168 pub seq: Option<u64>,
170 pub version: Version,
171}
172
173pub struct HotUpdate {
176 pub(crate) shared: Arc<Shared>,
177}
178
179impl HotUpdate {
180 pub fn notify_app_ready(&self) -> Result<AckOutcome> {
186 let activation = self.shared.activation()?;
187 let (outcome, effects) = activation.store.update(|state| {
188 let (state, outcome, effects) = machine::ack(state, activation.active);
189 (state, (outcome, effects))
190 })?;
191 activation.store.apply_effects(&effects);
192 match outcome {
193 AckOutcome::Committed(seq) => {
194 log::info!("hot-update: bundle seq {seq} committed as last-good")
195 }
196 AckOutcome::Stale(seq) => {
197 log::warn!("hot-update: ack for seq {seq} no longer matches on-disk state; ignored")
198 }
199 AckOutcome::AlreadyCommitted(_) | AckOutcome::EmbeddedNoop => {}
200 }
201 Ok(outcome)
202 }
203
204 pub fn current_bundle(&self) -> Result<CurrentBundle> {
206 let activation = self.shared.activation()?;
207 Ok(match activation.active {
208 Active::Ota(seq) => CurrentBundle {
209 source: BundleSource::Ota,
210 seq: Some(seq),
211 version: activation.active_version.clone(),
212 },
213 Active::Embedded => CurrentBundle {
214 source: BundleSource::Embedded,
215 seq: None,
216 version: activation.embedded_version.clone(),
217 },
218 })
219 }
220
221 pub fn reset(&self) -> Result<()> {
226 let activation = self.shared.activation()?;
227 let effects = activation.store.update(|state| {
228 machine::reset(state, &activation.embedded_version, activation.active)
229 })?;
230 activation.store.apply_effects(&effects);
231 activation.store.sweep_foreign_entries();
232 log::info!("hot-update: state reset; embedded serving resumes next launch");
233 Ok(())
234 }
235}
236
237#[cfg(test)]
238mod tests;