use std::path::{Path, PathBuf};
use meerkat_core::storage_diagnostics::{DiagnoseScope, StorageDiagnosis};
use serde_json::Value;
use crate::storage_doctor;
use crate::storage_health::ResolvedStorageSummary;
use super::JsonRpcError;
pub(crate) const STORAGE_DOCTOR_METHOD: &str = "mobkit/storage/doctor";
pub(crate) struct StorageDoctorParams {
pub state_dir: PathBuf,
pub identity: Option<String>,
}
impl StorageDoctorParams {
pub(crate) fn scope(&self) -> DiagnoseScope {
let scope = DiagnoseScope::new(vec![self.state_dir.clone()]);
match &self.identity {
Some(identity) => scope.with_realm(identity.clone()),
None => scope,
}
}
}
pub(crate) fn parse_storage_doctor_params(
params: &Value,
) -> Result<Option<StorageDoctorParams>, String> {
if !params.is_null() && !params.is_object() {
return Err("params must be an object".to_string());
}
let state_dir = match params.get("state_dir") {
None | Some(Value::Null) => return Ok(None),
Some(Value::String(dir)) if !dir.trim().is_empty() => PathBuf::from(dir),
Some(_) => return Err("state_dir must be a non-empty string".to_string()),
};
let identity = match params.get("identity") {
None | Some(Value::Null) => None,
Some(Value::String(identity)) => Some(identity.clone()),
Some(_) => return Err("identity must be a string".to_string()),
};
Ok(Some(StorageDoctorParams {
state_dir,
identity,
}))
}
pub(crate) fn storage_doctor_state_dir_unavailable_error() -> JsonRpcError {
JsonRpcError {
code: -32004,
message: "storage doctor requires params.state_dir: the runtime does not expose its \
persistent state directory (the storage layout authority lands in Phase M2)"
.to_string(),
data: None,
}
}
pub(crate) fn storage_doctor_result_json(
params: &StorageDoctorParams,
diagnosis: &StorageDiagnosis,
resolved: Option<ResolvedStorageSummary>,
) -> Value {
serde_json::json!({
"state_dir": params.state_dir.display().to_string(),
"diagnosis": serde_json::to_value(diagnosis).unwrap_or(Value::Null),
"storage": resolved
.map(|summary| summary.status_json())
.unwrap_or(Value::Null),
})
}
pub(crate) async fn run_storage_doctor(
params: &StorageDoctorParams,
resolved: Option<ResolvedStorageSummary>,
) -> Value {
let (census, note) = match resolved {
Some(summary) if summary_covers_state_dir(&summary, ¶ms.state_dir) => {
(Some(summary), None)
}
Some(_) => (
None,
Some(
"live durability census omitted: params.state_dir is not this runtime's own \
state directory (or the runtime recorded none); the diagnosis is disk-only",
),
),
None => (None, None),
};
let diagnosis =
storage_doctor::diagnose_state_dir_with_runtime(¶ms.scope(), census.clone()).await;
let mut result = storage_doctor_result_json(params, &diagnosis, census);
if let (Some(note), Some(map)) = (note, result.as_object_mut()) {
map.insert("storage_note".to_string(), Value::String(note.to_string()));
}
result
}
fn summary_covers_state_dir(summary: &ResolvedStorageSummary, requested: &Path) -> bool {
let Some(own) = summary.state_dir.as_deref() else {
return false;
};
let canonical =
|path: &Path| std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
canonical(own) == canonical(requested)
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::storage_health::BlobDurability;
fn summary_with_state_dir(dir: Option<&Path>) -> ResolvedStorageSummary {
let summary = ResolvedStorageSummary::new(BlobDurability::PersistentDisk, Some(true));
match dir {
Some(dir) => summary.with_state_dir(dir),
None => summary,
}
}
#[tokio::test]
async fn census_attaches_only_for_the_runtimes_own_state_dir() {
let own = tempfile::tempdir().expect("own state dir");
let other = tempfile::tempdir().expect("other state dir");
let params = StorageDoctorParams {
state_dir: own.path().to_path_buf(),
identity: None,
};
let result =
run_storage_doctor(¶ms, Some(summary_with_state_dir(Some(own.path())))).await;
assert!(!result["storage"].is_null(), "{result}");
assert!(result.get("storage_note").is_none(), "{result}");
let params_other = StorageDoctorParams {
state_dir: other.path().to_path_buf(),
identity: None,
};
let result = run_storage_doctor(
¶ms_other,
Some(summary_with_state_dir(Some(own.path()))),
)
.await;
assert!(result["storage"].is_null(), "{result}");
assert!(
result["storage_note"]
.as_str()
.is_some_and(|note| note.contains("disk-only")),
"{result}"
);
let result = run_storage_doctor(¶ms, Some(summary_with_state_dir(None))).await;
assert!(result["storage"].is_null(), "{result}");
assert!(result.get("storage_note").is_some(), "{result}");
}
}