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);
17
18pub enum Acquire {
20 Skip(String),
22 Fail(anyhow::Error),
24}
25
26impl From<anyhow::Error> for Acquire {
27 fn from(e: anyhow::Error) -> Self {
28 Acquire::Fail(e)
29 }
30}
31
32pub struct Rig {
33 pub config: RigConfig,
34 pub base_dir: PathBuf,
36 _lock: RigLock,
37}
38
39impl Rig {
40 pub async fn acquire() -> Result<Rig, Acquire> {
46 let Some(path) = RigConfig::locate()? else {
47 return Err(Acquire::Skip(format!(
48 "no rig: {} not found (set {} or create one to run on hardware)",
49 crate::config::CONFIG_FILE,
50 crate::config::ENV_VAR,
51 )));
52 };
53 let config = RigConfig::load(&path)?;
54 let base_dir = path.parent().unwrap_or(std::path::Path::new(".")).to_path_buf();
55
56 let lock_path = config
57 .rig
58 .lock_file
59 .clone()
60 .map(|p| if p.is_absolute() { p } else { base_dir.join(p) })
61 .unwrap_or_else(|| base_dir.join("target").join("banc.lock"));
62 let timeout = std::env::var("BANC_LOCK_TIMEOUT_SECS")
63 .ok()
64 .and_then(|s| s.parse().ok())
65 .map(Duration::from_secs)
66 .unwrap_or(DEFAULT_LOCK_TIMEOUT);
67 let lock = RigLock::take(lock_path, timeout).await?;
68
69 Ok(Rig { config, base_dir, _lock: lock })
70 }
71
72 pub async fn assistant(&self, name: &str) -> anyhow::Result<Node> {
74 let cfg: &AssistantConfig = self
75 .config
76 .assistant(name)
77 .ok_or_else(|| anyhow::anyhow!("no assistant '{name}' in rig config"))?;
78 Node::connect(cfg).await
79 }
80}
81
82struct RigLock {
86 file: File,
87}
88
89impl RigLock {
90 async fn take(path: PathBuf, timeout: Duration) -> anyhow::Result<Self> {
91 if let Some(parent) = path.parent() {
92 std::fs::create_dir_all(parent)?;
93 }
94 let file = File::create(&path)
95 .map_err(|e| anyhow::anyhow!("creating rig lock {}: {e}", path.display()))?;
96 let deadline = Instant::now() + timeout;
97 loop {
98 if file.try_lock_exclusive()? {
99 return Ok(RigLock { file });
100 }
101 if Instant::now() >= deadline {
102 anyhow::bail!(
103 "rig lock {} held by another process for over {timeout:?}",
104 path.display()
105 );
106 }
107 tokio::time::sleep(Duration::from_millis(200)).await;
108 }
109 }
110}
111
112impl Drop for RigLock {
113 fn drop(&mut self) {
114 let _ = FileExt::unlock(&self.file);
115 }
116}