dx-driven 0.1.0

Professional AI-assisted development orchestrator with binary-first architecture
Documentation
use crate::{DrivenError, Result};
use serde::{Deserialize, Serialize};
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

const LOCK_FILE: &str = ".driven-lane-pass.lock";
const LOCK_SCHEMA: &str = "driven.lane_pass.lock.v1";
const DEFAULT_STALE_AFTER_SECONDS: u64 = 30 * 60;
static LOCK_SEQUENCE: AtomicU64 = AtomicU64::new(1);

#[derive(Debug)]
pub(super) struct StateLock {
    path: PathBuf,
    lock_id: String,
}

impl StateLock {
    pub(super) fn acquire(state_dir: &Path) -> Result<Self> {
        let path = state_dir.join(LOCK_FILE);
        match Self::create(&path) {
            Ok(lock) => Ok(lock),
            Err(AcquireLockError::Io(error))
                if error.kind() == std::io::ErrorKind::AlreadyExists =>
            {
                Self::recover_or_block_at(path, unix_seconds()?)
            }
            Err(AcquireLockError::Io(error)) => Err(DrivenError::Io(error)),
            Err(AcquireLockError::Driven(error)) => Err(error),
        }
    }

    fn recover_or_block_at(path: PathBuf, now: u64) -> Result<Self> {
        match LockMetadata::read(&path) {
            Ok(metadata) => {
                if metadata.is_stale(now) {
                    Self::remove_lock_if_metadata_matches(&path, &metadata)?;
                    return Self::create(&path).map_err(AcquireLockError::into_driven);
                }

                Err(DrivenError::Validation(format!(
                    "lane/pass state is locked at {} by pid {} since {} (stale after {} seconds)",
                    path.display(),
                    metadata.owner_pid,
                    metadata.acquired_unix_seconds,
                    metadata.stale_after_seconds
                )))
            }
            Err(error) => {
                let modified = modified_unix_seconds(&path)?;
                if modified
                    .checked_add(DEFAULT_STALE_AFTER_SECONDS)
                    .is_some_and(|expires_at| expires_at <= now)
                {
                    Self::remove_unreadable_lock_if_modified_matches(&path, modified)?;
                    return Self::create(&path).map_err(AcquireLockError::into_driven);
                }

                Err(DrivenError::Validation(format!(
                    "{}; lock file modified at {} and will be treated as stale after {} seconds",
                    error, modified, DEFAULT_STALE_AFTER_SECONDS
                )))
            }
        }
    }

    fn remove_lock_if_metadata_matches(path: &Path, observed: &LockMetadata) -> Result<()> {
        let current = LockMetadata::read(path)?;
        if current.lock_id != observed.lock_id
            || current.owner_pid != observed.owner_pid
            || current.acquired_unix_seconds != observed.acquired_unix_seconds
        {
            return Err(DrivenError::Validation(format!(
                "lane/pass lock at {} was replaced during stale recovery",
                path.display()
            )));
        }
        fs::remove_file(path).map_err(DrivenError::Io)
    }

    fn remove_unreadable_lock_if_modified_matches(
        path: &Path,
        observed_modified: u64,
    ) -> Result<()> {
        let current_modified = modified_unix_seconds(path)?;
        if current_modified != observed_modified {
            return Err(DrivenError::Validation(format!(
                "lane/pass lock at {} changed during stale recovery",
                path.display()
            )));
        }
        fs::remove_file(path).map_err(DrivenError::Io)
    }

    fn create(path: &Path) -> std::result::Result<Self, AcquireLockError> {
        let mut file = OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(path)
            .map_err(AcquireLockError::Io)?;
        let metadata = LockMetadata::new(unix_seconds().map_err(AcquireLockError::Driven)?);
        let lock_id = metadata.lock_id.clone();
        let content = serde_json::to_string_pretty(&metadata)
            .map(|mut value| {
                value.push('\n');
                value
            })
            .map_err(|e| {
                AcquireLockError::Driven(DrivenError::Format(format!(
                    "failed to render lane/pass lock metadata: {}",
                    e
                )))
            })?;
        if let Err(error) = file.write_all(content.as_bytes()) {
            let _ = fs::remove_file(path);
            return Err(AcquireLockError::Io(error));
        }
        Ok(Self {
            path: path.to_path_buf(),
            lock_id,
        })
    }
}

impl Drop for StateLock {
    fn drop(&mut self) {
        if LockMetadata::read(&self.path)
            .ok()
            .is_some_and(|metadata| metadata.lock_id == self.lock_id)
        {
            let _ = fs::remove_file(&self.path);
        }
    }
}

#[derive(Debug)]
enum AcquireLockError {
    Io(std::io::Error),
    Driven(DrivenError),
}

impl AcquireLockError {
    fn into_driven(self) -> DrivenError {
        match self {
            Self::Io(error) => DrivenError::Io(error),
            Self::Driven(error) => error,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct LockMetadata {
    schema: String,
    #[serde(default)]
    lock_id: String,
    owner_pid: u32,
    acquired_unix_seconds: u64,
    stale_after_seconds: u64,
}

impl LockMetadata {
    fn new(acquired_unix_seconds: u64) -> Self {
        Self {
            schema: LOCK_SCHEMA.to_string(),
            lock_id: format!(
                "{}-{}-{}",
                std::process::id(),
                acquired_unix_seconds,
                LOCK_SEQUENCE.fetch_add(1, Ordering::Relaxed)
            ),
            owner_pid: std::process::id(),
            acquired_unix_seconds,
            stale_after_seconds: DEFAULT_STALE_AFTER_SECONDS,
        }
    }

    fn read(path: &Path) -> Result<Self> {
        let content = fs::read_to_string(path).map_err(DrivenError::Io)?;
        let metadata: Self = serde_json::from_str(&content).map_err(|e| {
            DrivenError::Validation(format!(
                "lane/pass state is locked at {}; lock metadata is unreadable: {}",
                path.display(),
                e
            ))
        })?;
        if metadata.schema != LOCK_SCHEMA {
            return Err(DrivenError::Validation(format!(
                "lane/pass state is locked at {}; unsupported lock schema {}",
                path.display(),
                metadata.schema
            )));
        }
        Ok(metadata)
    }

    fn is_stale(&self, now: u64) -> bool {
        self.acquired_unix_seconds
            .checked_add(self.stale_after_seconds)
            .is_some_and(|expires_at| expires_at <= now)
    }
}

fn unix_seconds() -> Result<u64> {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .map_err(|e| DrivenError::Validation(format!("system clock is before Unix epoch: {}", e)))
}

fn modified_unix_seconds(path: &Path) -> Result<u64> {
    fs::metadata(path)
        .and_then(|metadata| metadata.modified())
        .map_err(DrivenError::Io)?
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .map_err(|e| DrivenError::Validation(format!("lock mtime is before Unix epoch: {}", e)))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn dropping_lock_preserves_replaced_lock_file() {
        let temp = tempfile::tempdir().unwrap();
        let lock = StateLock::acquire(temp.path()).unwrap();
        let replacement = LockMetadata {
            schema: LOCK_SCHEMA.to_string(),
            lock_id: "replacement-owner".to_string(),
            owner_pid: 4242,
            acquired_unix_seconds: unix_seconds().unwrap(),
            stale_after_seconds: DEFAULT_STALE_AFTER_SECONDS,
        };
        fs::write(
            temp.path().join(LOCK_FILE),
            serde_json::to_string_pretty(&replacement).unwrap(),
        )
        .unwrap();

        drop(lock);

        let current = LockMetadata::read(&temp.path().join(LOCK_FILE)).unwrap();
        assert_eq!(current.lock_id, "replacement-owner");
    }

    #[test]
    fn stale_unreadable_lock_metadata_can_be_recovered() {
        let temp = tempfile::tempdir().unwrap();
        let lock_path = temp.path().join(LOCK_FILE);
        fs::write(&lock_path, "{").unwrap();

        let lock =
            StateLock::recover_or_block_at(lock_path.clone(), unix_seconds().unwrap() + 3600)
                .unwrap();

        assert_eq!(lock.path, lock_path);
    }

    #[test]
    fn stale_recovery_refuses_to_remove_replaced_lock_file() {
        let temp = tempfile::tempdir().unwrap();
        let lock_path = temp.path().join(LOCK_FILE);
        let stale = LockMetadata {
            schema: LOCK_SCHEMA.to_string(),
            lock_id: "stale-owner".to_string(),
            owner_pid: 1,
            acquired_unix_seconds: 1,
            stale_after_seconds: 1,
        };
        fs::write(&lock_path, serde_json::to_string_pretty(&stale).unwrap()).unwrap();
        let observed = LockMetadata::read(&lock_path).unwrap();
        let replacement = LockMetadata {
            schema: LOCK_SCHEMA.to_string(),
            lock_id: "replacement-owner".to_string(),
            owner_pid: 4242,
            acquired_unix_seconds: unix_seconds().unwrap(),
            stale_after_seconds: DEFAULT_STALE_AFTER_SECONDS,
        };
        fs::write(
            &lock_path,
            serde_json::to_string_pretty(&replacement).unwrap(),
        )
        .unwrap();

        let err = StateLock::remove_lock_if_metadata_matches(&lock_path, &observed).unwrap_err();

        assert!(err.to_string().contains("replaced"));
        assert_eq!(
            LockMetadata::read(&lock_path).unwrap().lock_id,
            "replacement-owner"
        );
    }
}