pub mod fuse;
use std::{
collections::HashMap,
fs::{self, File},
io::Write,
path::{Path, PathBuf},
sync::Arc,
};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use uuid::Uuid;
use crate::{
dicfuse::{Dicfuse, DicfuseManager},
util::config,
};
#[derive(Debug, Clone)]
pub struct AntaresPaths {
pub upper_root: PathBuf,
pub cl_root: PathBuf,
pub mount_root: PathBuf,
pub state_file: PathBuf,
}
impl AntaresPaths {
pub fn new(
upper_root: PathBuf,
cl_root: PathBuf,
mount_root: PathBuf,
state_file: PathBuf,
) -> Self {
Self {
upper_root,
cl_root,
mount_root,
state_file,
}
}
pub fn from_global_config() -> Self {
Self {
upper_root: PathBuf::from(config::antares_upper_root()),
cl_root: PathBuf::from(config::antares_cl_root()),
mount_root: PathBuf::from(config::antares_mount_root()),
state_file: PathBuf::from(config::antares_state_file()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AntaresConfig {
pub job_id: String,
pub mountpoint: PathBuf,
pub upper_id: String,
pub upper_dir: PathBuf,
pub cl_dir: Option<PathBuf>,
pub cl_id: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct AntaresState {
mounts: Vec<AntaresConfig>,
}
pub struct AntaresManager {
dic: Arc<Dicfuse>,
paths: AntaresPaths,
instances: Arc<Mutex<HashMap<String, AntaresConfig>>>,
fuse_handles: Arc<Mutex<HashMap<String, fuse::AntaresFuse>>>,
}
impl AntaresManager {
pub async fn new(paths: AntaresPaths) -> Self {
let dic = DicfuseManager::global().await;
let instances = Self::load_state(&paths.state_file).unwrap_or_default();
Self {
dic,
paths,
instances: Arc::new(Mutex::new(instances)),
fuse_handles: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn mount_job(
&self,
job_id: &str,
cl_name: Option<&str>,
) -> std::io::Result<AntaresConfig> {
let mountpoint = self.paths.mount_root.join(job_id);
self.mount_job_at(job_id, mountpoint, cl_name).await
}
pub async fn mount_job_at(
&self,
job_id: &str,
mountpoint: impl Into<PathBuf>,
cl_name: Option<&str>,
) -> std::io::Result<AntaresConfig> {
let mountpoint = mountpoint.into();
let start = std::time::Instant::now();
tracing::info!(
"antares: mount_job_at start job_id={} mountpoint={} cl={:?}",
job_id,
mountpoint.display(),
cl_name
);
let upper_id = Uuid::new_v4().to_string();
let upper_dir = self.paths.upper_root.join(&upper_id);
let (cl_id, cl_dir) = match cl_name {
Some(_) => {
let id = Uuid::new_v4().to_string();
(Some(id.clone()), Some(self.paths.cl_root.join(id)))
}
None => (None, None),
};
std::fs::create_dir_all(&upper_dir)?;
if let Some(cl) = &cl_dir {
std::fs::create_dir_all(cl)?;
}
std::fs::create_dir_all(&mountpoint)?;
const DICFUSE_INIT_TIMEOUT_SECS: u64 = 120;
tracing::info!(
"antares: mount_job_at waiting for Dicfuse ready (timeout: {}s)",
DICFUSE_INIT_TIMEOUT_SECS
);
match tokio::time::timeout(
std::time::Duration::from_secs(DICFUSE_INIT_TIMEOUT_SECS),
self.dic.store.wait_for_ready(),
)
.await
{
Ok(_) => {
tracing::info!("antares: mount_job_at Dicfuse ready");
}
Err(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"Dicfuse initialization timed out after {}s for job {}",
DICFUSE_INIT_TIMEOUT_SECS, job_id
),
));
}
}
let mut antares_fuse = fuse::AntaresFuse::new(
mountpoint.clone(),
self.dic.clone(),
upper_dir.clone(),
cl_dir.clone(),
)
.await?;
antares_fuse.mount().await?;
tracing::info!(
"antares: mount_job_at FUSE mounted job_id={} mountpoint={}",
job_id,
mountpoint.display()
);
let instance = AntaresConfig {
job_id: job_id.to_string(),
mountpoint,
upper_id,
upper_dir,
cl_dir,
cl_id,
};
self.instances
.lock()
.await
.insert(job_id.to_string(), instance.clone());
self.fuse_handles
.lock()
.await
.insert(job_id.to_string(), antares_fuse);
self.persist_state().await?;
tracing::info!(
"antares: mount_job done job_id={} mountpoint={} elapsed={:.2}s",
job_id,
instance.mountpoint.display(),
start.elapsed().as_secs_f64()
);
Ok(instance)
}
pub async fn umount_job(&self, job_id: &str) -> std::io::Result<Option<AntaresConfig>> {
use tracing::{info, warn};
let mut instances = self.instances.lock().await;
let config = match instances.get(job_id) {
Some(cfg) => cfg.clone(),
None => return Ok(None),
};
let mount_path = &config.mountpoint;
info!("Attempting to unmount FUSE mount at {:?}", mount_path);
let mut fuse_handles = self.fuse_handles.lock().await;
if let Some(mut fuse) = fuse_handles.remove(job_id) {
match fuse.unmount().await {
Ok(()) => {
info!("Successfully unmounted {:?} via FUSE handle", mount_path);
}
Err(e) => {
warn!(
"FUSE handle unmount failed for {:?}: {}, falling back to fusermount",
mount_path, e
);
let _ = tokio::process::Command::new("fusermount")
.arg("-u")
.arg(mount_path)
.output()
.await;
}
}
} else {
let output = tokio::process::Command::new("fusermount")
.arg("-u")
.arg(mount_path)
.output()
.await?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
if error_msg.contains("not mounted") || error_msg.contains("Invalid argument") {
warn!(
"Filesystem at {:?} is not mounted, removing bookkeeping only: {}",
mount_path, error_msg
);
} else {
warn!(
"fusermount -u failed with status {} for {:?}: {}",
output.status, mount_path, error_msg
);
}
} else {
info!("Successfully unmounted {:?} via fusermount", mount_path);
}
}
drop(fuse_handles);
let removed = instances.remove(job_id);
drop(instances);
self.persist_state().await?;
Ok(removed)
}
pub async fn list(&self) -> Vec<AntaresConfig> {
self.instances.lock().await.values().cloned().collect()
}
pub fn dicfuse(&self) -> Arc<Dicfuse> {
self.dic.clone()
}
fn load_state(path: &Path) -> std::io::Result<HashMap<String, AntaresConfig>> {
if !path.exists() {
return Ok(HashMap::new());
}
let content = fs::read_to_string(path)?;
let state: AntaresState = toml::from_str(&content).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("parse state: {e}"))
})?;
let mut map = HashMap::new();
for m in state.mounts {
map.insert(m.job_id.clone(), m);
}
Ok(map)
}
async fn persist_state(&self) -> std::io::Result<()> {
let mounts: Vec<AntaresConfig> = self.instances.lock().await.values().cloned().collect();
let state = AntaresState { mounts };
let data = toml::to_string_pretty(&state)
.map_err(|e| std::io::Error::other(format!("encode state: {e}")))?;
if let Some(parent) = self.paths.state_file.parent() {
fs::create_dir_all(parent)?;
}
let mut f = File::create(&self.paths.state_file)?;
f.write_all(data.as_bytes())?;
Ok(())
}
}