1use crate::config::{AssistantConfig, RigConfig};
8use crate::node::Node;
9use fs4::fs_std::FileExt;
10use std::fs::File;
11use std::path::PathBuf;
12use std::time::{Duration, Instant};
13
14const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(300);
23
24pub enum Acquire {
26 Skip(String),
28 Fail(anyhow::Error),
30}
31
32impl From<anyhow::Error> for Acquire {
33 fn from(e: anyhow::Error) -> Self {
34 Acquire::Fail(e)
35 }
36}
37
38pub struct Rig {
39 pub config: RigConfig,
40 pub base_dir: PathBuf,
42 _lock: RigLock,
43}
44
45impl Rig {
46 pub fn acquire() -> Result<Rig, Acquire> {
52 let Some(path) = RigConfig::locate()? else {
53 return Err(Acquire::Skip(format!(
54 "no rig: {} not found (set {} or create one to run on hardware)",
55 crate::config::CONFIG_FILE,
56 crate::config::ENV_VAR,
57 )));
58 };
59 let config = RigConfig::load(&path)?;
60 let base_dir = path.parent().unwrap_or(std::path::Path::new(".")).to_path_buf();
61
62 let lock_path = config
63 .rig
64 .lock_file
65 .clone()
66 .map(|p| if p.is_absolute() { p } else { base_dir.join(p) })
67 .unwrap_or_else(|| base_dir.join("target").join("banc.lock"));
68 let timeout = std::env::var("BANC_LOCK_TIMEOUT_SECS")
69 .ok()
70 .and_then(|s| s.parse().ok())
71 .map(Duration::from_secs)
72 .unwrap_or(DEFAULT_LOCK_TIMEOUT);
73 let lock = RigLock::take(lock_path, timeout)?;
74
75 Ok(Rig { config, base_dir, _lock: lock })
76 }
77
78 pub async fn assistant(&self, name: &str) -> anyhow::Result<Node> {
80 let cfg: &AssistantConfig = self
81 .config
82 .assistant(name)
83 .ok_or_else(|| anyhow::anyhow!("no assistant '{name}' in rig config"))?;
84 Node::connect(cfg).await
85 }
86}
87
88struct RigLock {
92 file: File,
93}
94
95impl RigLock {
96 fn take(path: PathBuf, timeout: Duration) -> anyhow::Result<Self> {
97 if let Some(parent) = path.parent() {
98 std::fs::create_dir_all(parent)?;
99 }
100 let file = File::create(&path)
101 .map_err(|e| anyhow::anyhow!("creating rig lock {}: {e}", path.display()))?;
102 let deadline = Instant::now() + timeout;
103 loop {
104 if file.try_lock_exclusive()? {
105 return Ok(RigLock { file });
106 }
107 if Instant::now() >= deadline {
108 anyhow::bail!(
109 "rig lock {} held by another process for over {timeout:?}",
110 path.display()
111 );
112 }
113 std::thread::sleep(Duration::from_millis(200));
114 }
115 }
116}
117
118impl Drop for RigLock {
119 fn drop(&mut self) {
120 let _ = FileExt::unlock(&self.file);
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn lock_excludes_second_taker_until_dropped() {
130 let path = std::env::temp_dir()
131 .join(format!("banc-rig-lock-test-{}", std::process::id()));
132 let held = RigLock::take(path.clone(), Duration::ZERO).unwrap();
133 let contended = RigLock::take(path.clone(), Duration::ZERO);
134 assert!(contended.is_err(), "second take must fail while lock is held");
135 drop(held);
136 RigLock::take(path.clone(), Duration::ZERO).unwrap();
137 std::fs::remove_file(path).ok();
138 }
139}