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#[derive(Clone, Debug, Serialize, Deserialize)]
17pub enum ProcessKind {
18 Node,
19 Sidecar,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
24pub enum ProcessGroup {
25 Validators1,
26 Validators2,
27 Validators3,
28}
29
30#[derive(Clone, Debug, Serialize, Deserialize)]
32pub enum ProcessStatus {
33 Running,
34 Stopped,
35 Exited,
36 Unknown,
37 Skipped,
38}
39
40#[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#[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}