Skip to main content

kaish_types/
job.rs

1//! Job identification and status types.
2
3use std::path::PathBuf;
4
5use crate::result::LatchRequest;
6
7/// Unique identifier for a background job.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct JobId(pub u64);
10
11impl std::fmt::Display for JobId {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        write!(f, "{}", self.0)
14    }
15}
16
17/// Status of a background job.
18#[non_exhaustive]
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum JobStatus {
21    /// Job is currently running.
22    Running,
23    /// Job was stopped by a signal (e.g., Ctrl-Z / SIGTSTP).
24    Stopped,
25    /// Job completed successfully.
26    Done,
27    /// Job is blocked on an unfulfilled confirmation latch (exit 2 with a
28    /// stored `LatchRequest` — `rm x &` under `set -o latch`). Distinct from
29    /// `Failed`: the op is *held*, not errored, and can still be fulfilled via
30    /// `Kernel::confirm` with the request surfaced on `JobInfo.latch`.
31    Latched,
32    /// Job failed with an error.
33    Failed,
34}
35
36impl std::fmt::Display for JobStatus {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            JobStatus::Running => write!(f, "Running"),
40            JobStatus::Stopped => write!(f, "Stopped"),
41            JobStatus::Done => write!(f, "Done"),
42            JobStatus::Latched => write!(f, "Latched"),
43            JobStatus::Failed => write!(f, "Failed"),
44        }
45    }
46}
47
48/// Information about a job for listing.
49#[non_exhaustive]
50#[derive(Debug, Clone)]
51pub struct JobInfo {
52    /// Job ID.
53    pub id: JobId,
54    /// Command description.
55    pub command: String,
56    /// Current status.
57    pub status: JobStatus,
58    /// Path to output file (if available).
59    pub output_file: Option<PathBuf>,
60    /// OS process ID (if this is a stopped/foreground process).
61    pub pid: Option<u32>,
62    /// The pending confirmation-latch request when the job is gated
63    /// (`JobStatus::Latched`) — the control-plane surface an embedder reads to
64    /// fulfill a *backgrounded* destructive op via `Kernel::confirm`. `None`
65    /// for any non-latched job. GH #96.
66    pub latch: Option<LatchRequest>,
67}
68
69impl JobInfo {
70    /// Create a `JobInfo` with the required fields; `output_file`/`pid`/`latch`
71    /// default to `None`. Chain the `with_*` setters to fill them in.
72    ///
73    /// `#[non_exhaustive]` blocks struct-literal construction from outside this
74    /// crate — this constructor plus the setters below are the replacement.
75    pub fn new(id: JobId, command: impl Into<String>, status: JobStatus) -> Self {
76        Self {
77            id,
78            command: command.into(),
79            status,
80            output_file: None,
81            pid: None,
82            latch: None,
83        }
84    }
85
86    /// Set the output file path.
87    pub fn with_output_file(mut self, output_file: Option<PathBuf>) -> Self {
88        self.output_file = output_file;
89        self
90    }
91
92    /// Set the OS process ID.
93    pub fn with_pid(mut self, pid: Option<u32>) -> Self {
94        self.pid = pid;
95        self
96    }
97
98    /// Set the pending confirmation-latch request (see [`Self::latch`]).
99    pub fn with_latch(mut self, latch: Option<LatchRequest>) -> Self {
100        self.latch = latch;
101        self
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn new_defaults_optional_fields_to_none() {
111        let info = JobInfo::new(JobId(1), "echo hi", JobStatus::Running);
112        assert_eq!(info.id, JobId(1));
113        assert_eq!(info.command, "echo hi");
114        assert_eq!(info.status, JobStatus::Running);
115        assert!(info.output_file.is_none());
116        assert!(info.pid.is_none());
117        assert!(info.latch.is_none());
118    }
119
120    #[test]
121    fn with_setters_chain_and_override_defaults() {
122        let info = JobInfo::new(JobId(2), "sleep 1", JobStatus::Done)
123            .with_output_file(Some(PathBuf::from("job-output.txt")))
124            .with_pid(Some(1234))
125            .with_latch(None);
126        assert_eq!(info.output_file, Some(PathBuf::from("job-output.txt")));
127        assert_eq!(info.pid, Some(1234));
128        assert!(info.latch.is_none());
129    }
130}