use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use semver::Version;
use serde::Serialize;
use crate::machine::{self, AckOutcome, Active};
use crate::store::Store;
use crate::{Error, Result};
#[derive(Default)]
pub(crate) struct Shared {
activation: OnceLock<Activation>,
}
pub(crate) struct Activation {
store: Store,
active: Active,
active_dir: Option<PathBuf>,
active_version: Version,
embedded_version: Version,
update_lock: tokio::sync::Mutex<()>,
}
impl Activation {
pub(crate) fn store(&self) -> &Store {
&self.store
}
pub(crate) fn embedded_version(&self) -> &Version {
&self.embedded_version
}
pub(crate) fn update_lock(&self) -> &tokio::sync::Mutex<()> {
&self.update_lock
}
}
impl Shared {
pub(crate) fn active_dir(&self) -> Option<&PathBuf> {
self.activation.get()?.active_dir.as_ref()
}
pub(crate) fn is_activated(&self) -> bool {
self.activation.get().is_some()
}
pub(crate) fn activation(&self) -> Result<&Activation> {
self.activation.get().ok_or(Error::NotActive)
}
}
pub(crate) fn initialize(shared: &Shared, root: PathBuf, embedded_version: Version) {
let store = Store::new(root);
let state = store.load_state();
let present = store.present_seqs();
let outcome = machine::resolve_boot(state, &embedded_version, &present);
if let Some(seq) = outcome.rolled_back {
log::warn!(
"hot-update: bundle seq {seq} was armed last boot but never acked; \
rolled back and blacklisted its archive hash"
);
}
let active = match store.save_state(&outcome.state) {
Ok(()) => {
store.apply_effects(&outcome.effects);
store.sweep_foreign_entries();
outcome.active
}
Err(e) => {
log::error!(
"hot-update: failed to persist state.json ({e}); \
serving committed/embedded without arming the staged bundle"
);
match outcome.state.committed {
Some(seq) => Active::Ota(seq),
None => Active::Embedded,
}
}
};
let (active_dir, active_version) = match active {
Active::Ota(seq) => {
let version = outcome
.state
.versions
.get(&seq)
.map(|meta| meta.version.clone())
.unwrap_or_else(|| embedded_version.clone());
(Some(store.bundle_dir(seq)), version)
}
Active::Embedded => (None, embedded_version.clone()),
};
match active {
Active::Ota(seq) => log::info!(
"hot-update: serving OTA bundle seq {seq} (v{active_version}) from {}",
store.bundle_dir(seq).display()
),
Active::Embedded => {
log::info!("hot-update: serving embedded assets (v{embedded_version})")
}
}
let activation = Activation {
store,
active,
active_dir,
active_version,
embedded_version,
update_lock: tokio::sync::Mutex::new(()),
};
if shared.activation.set(activation).is_err() {
log::warn!("hot-update: initialize called twice; keeping the first activation");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum BundleSource {
Embedded,
Ota,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CurrentBundle {
pub source: BundleSource,
pub seq: Option<u64>,
pub version: Version,
}
pub struct HotUpdate {
pub(crate) shared: Arc<Shared>,
}
impl HotUpdate {
pub fn notify_app_ready(&self) -> Result<AckOutcome> {
let activation = self.shared.activation()?;
let (outcome, effects) = activation.store.update(|state| {
let (state, outcome, effects) = machine::ack(state, activation.active);
(state, (outcome, effects))
})?;
activation.store.apply_effects(&effects);
match outcome {
AckOutcome::Committed(seq) => {
log::info!("hot-update: bundle seq {seq} committed as last-good")
}
AckOutcome::Stale(seq) => log::warn!(
"hot-update: ack for seq {seq} no longer matches on-disk state; ignored"
),
AckOutcome::AlreadyCommitted(_) | AckOutcome::EmbeddedNoop => {}
}
Ok(outcome)
}
pub fn current_bundle(&self) -> Result<CurrentBundle> {
let activation = self.shared.activation()?;
Ok(match activation.active {
Active::Ota(seq) => CurrentBundle {
source: BundleSource::Ota,
seq: Some(seq),
version: activation.active_version.clone(),
},
Active::Embedded => CurrentBundle {
source: BundleSource::Embedded,
seq: None,
version: activation.embedded_version.clone(),
},
})
}
pub fn reset(&self) -> Result<()> {
let activation = self.shared.activation()?;
let effects = activation.store.update(|state| {
machine::reset(state, &activation.embedded_version, activation.active)
})?;
activation.store.apply_effects(&effects);
activation.store.sweep_foreign_entries();
log::info!("hot-update: state reset; embedded serving resumes next launch");
Ok(())
}
}
#[cfg(test)]
mod tests;