Skip to main content

banc_host/
rig.rs

1//! Rig acquisition: locate the topology config, take the cross-process lock,
2//! hand out fixtures. One `Rig` per process; access to the hardware is
3//! exclusive for as long as it lives (the pattern is embedded-test-stand's
4//! lock-owned-by-fixture, extended with a file lock because nextest runs one
5//! process per test).
6
7use 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
14/// How long a process waits for another test process to release the rig
15/// before giving up. Override with BANC_LOCK_TIMEOUT_SECS.
16const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(300);
17
18/// Why `Rig::acquire` did not return a rig.
19pub enum Acquire {
20    /// No rig on this machine — report the test as ignored, with this reason.
21    Skip(String),
22    /// A rig is configured but broken/contended — report a failure.
23    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    /// Directory the config file lives in; relative paths resolve from here.
35    pub base_dir: PathBuf,
36    _lock: RigLock,
37}
38
39impl Rig {
40    /// Locate + parse the config and take the cross-process lock.
41    ///
42    /// Missing config => `Acquire::Skip` (honest self-skip). Malformed
43    /// config or lock timeout => `Acquire::Fail` (a rig machine that cannot
44    /// run its suite is a failure, not a skip).
45    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    /// Connect to a configured assistant by name.
73    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
82/// Advisory file lock serializing rig access across processes. Held for the
83/// life of the `Rig`; a poisoned/stale holder is handled by the OS releasing
84/// the lock when that process dies.
85struct 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}