casper_devnet/
state.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::ffi::OsStr;
4use std::path::{Path, PathBuf};
5use std::sync::{
6    Arc,
7    atomic::{AtomicBool, AtomicU32},
8};
9use std::time::{SystemTime, UNIX_EPOCH};
10use time::OffsetDateTime;
11use tokio::fs as tokio_fs;
12
13pub const STATE_FILE_NAME: &str = "state.json";
14
15/// Process classification used in logs and reporting.
16#[derive(Clone, Debug, Serialize, Deserialize)]
17pub enum ProcessKind {
18    Node,
19    Sidecar,
20}
21
22/// Logical process grouping (kept for parity with NCTL UX).
23#[derive(Clone, Debug, Serialize, Deserialize)]
24pub enum ProcessGroup {
25    Validators1,
26    Validators2,
27    Validators3,
28}
29
30/// Runtime status of a tracked process.
31#[derive(Clone, Debug, Serialize, Deserialize)]
32pub enum ProcessStatus {
33    Running,
34    Stopped,
35    Exited,
36    Unknown,
37    Skipped,
38}
39
40/// Persisted record of a process and its lifecycle details.
41#[derive(Clone, Debug, Serialize, Deserialize)]
42pub struct ProcessRecord {
43    pub id: String,
44    pub node_id: u32,
45    pub kind: ProcessKind,
46    pub group: ProcessGroup,
47    pub command: String,
48    pub args: Vec<String>,
49    pub cwd: String,
50    pub pid: Option<u32>,
51    #[serde(skip)]
52    pub pid_handle: Option<Arc<AtomicU32>>,
53    #[serde(skip)]
54    pub shutdown_handle: Option<Arc<AtomicBool>>,
55    pub stdout_path: String,
56    pub stderr_path: String,
57    #[serde(with = "time::serde::rfc3339::option")]
58    pub started_at: Option<OffsetDateTime>,
59    #[serde(with = "time::serde::rfc3339::option")]
60    pub stopped_at: Option<OffsetDateTime>,
61    pub exit_code: Option<i32>,
62    pub exit_signal: Option<i32>,
63    pub last_status: ProcessStatus,
64}
65
66/// State snapshot stored for process bookkeeping.
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct State {
69    #[serde(with = "time::serde::rfc3339")]
70    pub created_at: OffsetDateTime,
71    #[serde(with = "time::serde::rfc3339")]
72    pub updated_at: OffsetDateTime,
73    pub last_block_height: Option<u64>,
74    pub processes: Vec<ProcessRecord>,
75    #[serde(skip)]
76    path: PathBuf,
77}
78
79impl State {
80    pub async fn new(path: PathBuf) -> Result<Self> {
81        let now = OffsetDateTime::now_utc();
82        let state = Self {
83            created_at: now,
84            updated_at: now,
85            last_block_height: None,
86            processes: Vec::new(),
87            path,
88        };
89        state.persist().await?;
90        Ok(state)
91    }
92
93    pub async fn touch(&mut self) -> Result<()> {
94        self.updated_at = OffsetDateTime::now_utc();
95        self.persist().await
96    }
97
98    async fn persist(&self) -> Result<()> {
99        ensure_parent(&self.path).await?;
100        let contents = serde_json::to_string_pretty(self)?;
101        write_atomic(&self.path, contents).await
102    }
103}
104
105async fn ensure_parent(path: &Path) -> Result<()> {
106    if let Some(parent) = path.parent() {
107        tokio_fs::create_dir_all(parent).await?;
108    }
109    Ok(())
110}
111
112async fn write_atomic(path: &Path, contents: String) -> Result<()> {
113    let base_name = path
114        .file_name()
115        .unwrap_or_else(|| OsStr::new(STATE_FILE_NAME))
116        .to_string_lossy();
117    let suffix = SystemTime::now()
118        .duration_since(UNIX_EPOCH)
119        .map(|duration| duration.as_nanos())
120        .unwrap_or(0);
121    let tmp_dir = tempfile::Builder::new()
122        .prefix("casper-devnet-state")
123        .tempdir()?;
124    let tmp_name = format!("{}.{}.{}.tmp", base_name, std::process::id(), suffix);
125    let tmp_path = tmp_dir.path().join(tmp_name);
126    tokio_fs::write(&tmp_path, contents).await?;
127    if let Err(err) = tokio_fs::rename(&tmp_path, path).await {
128        let _ = tokio_fs::remove_file(&tmp_path).await;
129        return Err(err.into());
130    }
131    Ok(())
132}