use crate::{
core::error::{AppError, Result},
storage::references::References,
storage::state::{Instance, Store, atomic_json, private_dir, reject_links},
};
use serde::{Deserialize, Serialize};
use std::time::Instant;
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Record {
schema_version: u8,
request_id: String,
version: String,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct Pointer {
schema_version: u8,
reference: String,
}
pub enum Selection {
Existing(Instance),
Create { request_id: String },
}
pub fn select(
store: &Store,
refs: &References,
version: &str,
deadline: Instant,
) -> Result<Selection> {
let tx = store.transaction(deadline)?;
let selected = if tx.registry.instances.is_empty() {
None
} else {
Some(crate::storage::state::select(&tx.registry, None)?)
};
if let Some(instance) = &selected {
let installation = tx
.registry
.installations
.iter()
.find(|i| i.id == instance.installation_id)
.ok_or_else(AppError::storage)?;
if installation.version != version {
return Err(AppError::new(
"INSTANCE_VERSION_CONFLICT",
"Selected instance uses another installation version; no automatic creation occurred.",
4,
));
}
if !matches!(instance.state.as_str(), "preparing" | "prepared") {
return Ok(Selection::Existing(instance.clone()));
}
}
let directory = store.root().join("automatic-start");
let path = directory.join("active.json");
reject_links(&path)?;
let record: Record = match crate::storage::config::read_json::<Pointer>(&path, 4096) {
Ok(pointer) => {
if pointer.schema_version != 1 {
return Err(conflict());
}
let record: Record = refs.get("d1", &pointer.reference)?;
if record.schema_version != 1 || record.version != version {
return Err(conflict());
}
crate::core::domain::request_key(&record.request_id).map_err(|_| conflict())?;
record
}
Err(error) if error.code == "FILE_NOT_FOUND" && selected.is_none() => {
let mut random = [0_u8; 16];
getrandom::fill(&mut random).map_err(|_| AppError::storage())?;
let request_id = format!(
"automatic-{}",
random
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>()
);
let record = Record {
schema_version: 1,
request_id,
version: version.into(),
};
private_dir(&directory)?;
atomic_json(
&path,
&Pointer {
schema_version: 1,
reference: refs.issue("d1", &record)?,
},
)?;
record
}
Err(error) if error.code == "FILE_NOT_FOUND" => return Err(conflict()),
Err(error) => return Err(error),
};
if let Some(instance) = selected {
let plan = crate::napcat::runtime::instance_creation::reserved_plan(
store,
refs,
&record.request_id,
)?
.ok_or_else(conflict)?;
if plan.instance_id != instance.id
|| plan.run_id != instance.run_id
|| plan.version != version
{
return Err(conflict());
}
}
crate::core::deadline::check(deadline)?;
Ok(Selection::Create {
request_id: record.request_id,
})
}
fn conflict() -> AppError {
AppError::new(
"AUTOMATIC_START_CONFLICT",
"Automatic startup has no matching signed creation checkpoint; inspect the existing instance or original creation request.",
4,
)
}
pub(crate) fn retire(store: &Store, refs: &References, instance: &Instance) -> Result<()> {
let path = store.root().join("automatic-start").join("active.json");
reject_links(&path)?;
let pointer: Pointer = match crate::storage::config::read_json(&path, 4096) {
Ok(pointer) => pointer,
Err(error) if error.code == "FILE_NOT_FOUND" => return Ok(()),
Err(error) => return Err(error),
};
if pointer.schema_version != 1 {
return Err(conflict());
}
let record: Record = refs.get("d1", &pointer.reference)?;
if record.schema_version != 1 {
return Err(conflict());
}
let plan = super::instance_creation::reserved_plan(store, refs, &record.request_id)?
.ok_or_else(conflict)?;
let same_run = plan.run_id == instance.run_id;
#[cfg(windows)]
let same_run = same_run
|| (plan.instance_id == instance.id
&& super::generation::descends_from(store, refs, instance, &plan.run_id)?);
if plan.instance_id == instance.id && same_run {
std::fs::remove_file(path).map_err(|_| AppError::storage())?;
}
Ok(())
}
#[cfg(test)]
mod tests;