rash_core 2.18.3

Declarative shell scripting using Rust native bindings
Documentation
use std::collections::HashMap;
use std::process::Child;
use std::sync::{Arc, LazyLock, Mutex};
use std::time::{Duration, Instant};

use serde::{Deserialize, Serialize};

pub type JobId = u64;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum JobStatus {
    Pending,
    Running,
    Finished,
    Failed,
}

#[derive(Debug, Clone)]
pub struct JobInfo {
    pub status: JobStatus,
    pub output: Option<String>,
    pub error: Option<String>,
    pub changed: bool,
    pub elapsed: Duration,
}

#[derive(Debug)]
pub struct Job {
    pub id: JobId,
    pub status: JobStatus,
    pub started_at: Instant,
    pub timeout: Option<Duration>,
    pub child: Option<Child>,
    pub output: Option<String>,
    pub error: Option<String>,
    pub changed: bool,
}

impl Job {
    pub fn new(id: JobId, timeout: Option<Duration>, child: Child) -> Self {
        Job {
            id,
            status: JobStatus::Running,
            started_at: Instant::now(),
            timeout,
            child: Some(child),
            output: None,
            error: None,
            changed: false,
        }
    }

    pub fn is_timed_out(&self) -> bool {
        if let Some(timeout) = self.timeout {
            self.started_at.elapsed() > timeout
        } else {
            false
        }
    }

    pub fn elapsed(&self) -> Duration {
        self.started_at.elapsed()
    }
}

#[derive(Debug, Default)]
pub struct JobRegistry {
    jobs: HashMap<JobId, Job>,
    next_id: JobId,
}

impl JobRegistry {
    pub fn new() -> Self {
        JobRegistry {
            jobs: HashMap::new(),
            next_id: 1,
        }
    }

    pub fn register(&mut self, timeout: Option<Duration>, child: Child) -> JobId {
        let id = self.next_id;
        self.next_id += 1;
        let job = Job::new(id, timeout, child);
        self.jobs.insert(id, job);
        id
    }

    pub fn get(&self, id: JobId) -> Option<&Job> {
        self.jobs.get(&id)
    }

    pub fn get_mut(&mut self, id: JobId) -> Option<&mut Job> {
        self.jobs.get_mut(&id)
    }

    pub fn remove(&mut self, id: JobId) -> Option<Job> {
        self.jobs.remove(&id)
    }

    pub fn contains(&self, id: JobId) -> bool {
        self.jobs.contains_key(&id)
    }

    pub fn list(&self) -> Vec<JobId> {
        self.jobs.keys().copied().collect()
    }
}

pub static JOBS: LazyLock<Arc<Mutex<JobRegistry>>> =
    LazyLock::new(|| Arc::new(Mutex::new(JobRegistry::new())));

pub fn register_job(timeout: Option<Duration>, child: Child) -> JobId {
    let mut registry = JOBS.lock().expect("Failed to lock job registry");
    registry.register(timeout, child)
}

pub fn get_job(id: JobId) -> Option<JobStatus> {
    let registry = JOBS.lock().expect("Failed to lock job registry");
    registry.get(id).map(|j| j.status.clone())
}

pub fn get_job_info(id: JobId) -> Option<JobInfo> {
    let registry = JOBS.lock().expect("Failed to lock job registry");
    registry.get(id).map(|j| JobInfo {
        status: j.status.clone(),
        output: j.output.clone(),
        error: j.error.clone(),
        changed: j.changed,
        elapsed: j.elapsed(),
    })
}

pub fn update_job_status(
    id: JobId,
    status: JobStatus,
    output: Option<String>,
    error: Option<String>,
    changed: bool,
) -> bool {
    let mut registry = JOBS.lock().expect("Failed to lock job registry");
    if let Some(job) = registry.get_mut(id) {
        job.status = status;
        job.output = output;
        job.error = error;
        job.changed = changed;
        job.child = None;
        true
    } else {
        false
    }
}

pub fn job_exists(id: JobId) -> bool {
    let registry = JOBS.lock().expect("Failed to lock job registry");
    registry.contains(id)
}

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

    #[test]
    fn test_job_registry() {
        let registry = JobRegistry::new();
        assert!(registry.list().is_empty());
    }
}