canic-host 0.111.0

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Exact-session local state discard with interruption and terminal replay records.

use crate::local_fleet::{LocalFleetError, model::LocalResetRecord, ops};
use ic_host_fs::durable;

use std::{fs, path::Path};

fn read(directory: &Path) -> Result<Option<LocalResetRecord>, LocalFleetError> {
    match ic_host_fs::read::read_file_no_follow(&directory.join("reset.json"), 4096)
        .map_err(std::io::Error::from)
    {
        Ok(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(error) => Err(error.into()),
    }
}

/// An interrupted reset must complete before creating another local environment.
pub fn require_complete(directory: &Path) -> Result<(), LocalFleetError> {
    if read(directory)?.is_some_and(|record| record.schema_version != 1 || !record.complete) {
        return Err(LocalFleetError::Session);
    }
    Ok(())
}

/// Persist exact reset authority before removing any simulator file.
pub fn begin(directory: &Path, expected: &str) -> Result<bool, LocalFleetError> {
    let retained = ops::read_record(directory)?;
    if let Some(record) = retained {
        if record.session_id != expected
            || record.configuration.name
                != directory.file_name().unwrap_or_default().to_string_lossy()
        {
            return Err(LocalFleetError::Session);
        }
    } else {
        let prior = read(directory)?.ok_or(LocalFleetError::Session)?;
        if prior.schema_version != 1 || prior.session_id != expected {
            return Err(LocalFleetError::Session);
        }
        if prior.complete {
            return Ok(false);
        }
    }
    let intent = LocalResetRecord {
        schema_version: 1,
        session_id: expected.into(),
        complete: false,
    };
    durable::write_bytes(&directory.join("reset.json"), &serde_json::to_vec(&intent)?)
        .map_err(crate::publication::ops::io_error)?;
    Ok(true)
}

/// Remove only the selected owned instance tree after the durable intent.
pub fn remove_instance(directory: &Path) -> Result<(), LocalFleetError> {
    let reset = read(directory)?.ok_or(LocalFleetError::Session)?;
    let instance = ops::instance_directory(directory, &reset.session_id)?;
    match fs::symlink_metadata(&instance) {
        Ok(metadata) if metadata.is_dir() => {}
        Ok(_) => return Err(LocalFleetError::UnsafePath),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
        Err(error) => return Err(error.into()),
    }
    match fs::remove_dir_all(&instance) {
        Ok(()) => Ok(()),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(error.into()),
    }
}

/// Publish terminal reset replay only after both instance and environment record are removed.
pub fn finish(directory: &Path, expected: &str) -> Result<(), LocalFleetError> {
    for name in ["preparation.json", "desired.toml", "root-key.der"] {
        match fs::remove_file(directory.join(name)) {
            Ok(()) => {}
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(error) => return Err(error.into()),
        }
    }
    match fs::remove_file(directory.join("environment.json")) {
        Ok(()) => {}
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
        Err(error) => return Err(error.into()),
    }
    let record = LocalResetRecord {
        schema_version: 1,
        session_id: expected.into(),
        complete: true,
    };
    durable::write_bytes(&directory.join("reset.json"), &serde_json::to_vec(&record)?)
        .map_err(crate::publication::ops::io_error)?;
    Ok(())
}