use crate::config::{AssistantConfig, RigConfig};
use crate::node::Node;
use fs4::fs_std::FileExt;
use std::fs::File;
use std::path::PathBuf;
use std::time::{Duration, Instant};
const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(300);
pub enum Acquire {
Skip(String),
Fail(anyhow::Error),
}
impl From<anyhow::Error> for Acquire {
fn from(e: anyhow::Error) -> Self {
Acquire::Fail(e)
}
}
pub struct Rig {
pub config: RigConfig,
pub base_dir: PathBuf,
_lock: Exclusive,
}
enum Exclusive {
Flock(#[allow(dead_code)] RigLock),
Lease(#[allow(dead_code)] crate::net::lease::LeaseClient),
}
impl Rig {
pub fn acquire() -> Result<Rig, Acquire> {
let Some(path) = RigConfig::locate()? else {
return Err(Acquire::Skip(format!(
"no rig: {} not found (set {} or create one to run on hardware)",
crate::config::CONFIG_FILE,
crate::config::ENV_VAR,
)));
};
let config = RigConfig::load(&path)?;
let base_dir = path.parent().unwrap_or(std::path::Path::new(".")).to_path_buf();
let timeout = std::env::var("BANC_LOCK_TIMEOUT_SECS")
.ok()
.and_then(|s| s.parse().ok())
.map(Duration::from_secs)
.unwrap_or(DEFAULT_LOCK_TIMEOUT);
let lock = if let Some(lease) = &config.rig.lease {
let token = read_token(&lease.token_file, &base_dir)?;
let holder = std::env::var("BANC_LEASE_HOLDER").unwrap_or_else(|_| {
let host = std::env::var("HOSTNAME").unwrap_or_else(|_| "?".into());
format!("{host}:{}", std::process::id())
});
Exclusive::Lease(
crate::net::lease::LeaseClient::acquire(&lease.addr, &token, &holder, timeout)
.map_err(|e| anyhow::anyhow!("acquiring rig lease: {e}"))?,
)
} else {
let lock_path = config
.rig
.lock_file
.clone()
.map(|p| if p.is_absolute() { p } else { base_dir.join(p) })
.unwrap_or_else(|| base_dir.join("target").join("banc.lock"));
Exclusive::Flock(RigLock::take(lock_path, timeout)?)
};
Ok(Rig { config, base_dir, _lock: lock })
}
pub async fn assistant(&self, name: &str) -> anyhow::Result<Node> {
let cfg: &AssistantConfig = self
.config
.assistant(name)
.ok_or_else(|| anyhow::anyhow!("no assistant '{name}' in rig config"))?;
let token = cfg
.token_file
.as_ref()
.map(|p| read_token(p, &self.base_dir))
.transpose()?;
Node::connect(cfg, token.as_deref()).await
}
}
fn read_token(path: &std::path::Path, base_dir: &std::path::Path) -> anyhow::Result<String> {
let path = if path.is_absolute() { path.to_path_buf() } else { base_dir.join(path) };
let token = std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("reading token file {}: {e}", path.display()))?;
Ok(token.trim().to_owned())
}
struct RigLock {
file: File,
}
impl RigLock {
fn take(path: PathBuf, timeout: Duration) -> anyhow::Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = File::create(&path)
.map_err(|e| anyhow::anyhow!("creating rig lock {}: {e}", path.display()))?;
let deadline = Instant::now() + timeout;
loop {
if file.try_lock_exclusive()? {
return Ok(RigLock { file });
}
if Instant::now() >= deadline {
anyhow::bail!(
"rig lock {} held by another process for over {timeout:?}",
path.display()
);
}
std::thread::sleep(Duration::from_millis(200));
}
}
}
impl Drop for RigLock {
fn drop(&mut self) {
let _ = FileExt::unlock(&self.file);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_excludes_second_taker_until_dropped() {
let path = std::env::temp_dir()
.join(format!("banc-rig-lock-test-{}", std::process::id()));
let held = RigLock::take(path.clone(), Duration::ZERO).unwrap();
let contended = RigLock::take(path.clone(), Duration::ZERO);
assert!(contended.is_err(), "second take must fail while lock is held");
drop(held);
RigLock::take(path.clone(), Duration::ZERO).unwrap();
std::fs::remove_file(path).ok();
}
}