1use serde::{Deserialize, Serialize};
6
7use super::signal;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(tag = "type", rename_all = "snake_case")]
12pub enum FailureReason {
13 ExitCode { code: i32 },
15 Signal { signal: i32 },
17 StartTimeout,
19 StopTimeout,
21 HealthCheckFailed { attempts: u32 },
23 DependencyFailed { service: String },
25 SpawnError { message: String },
27 MissingDependency { dependency: String },
29}
30
31impl FailureReason {
32 pub fn display(&self) -> String {
34 match self {
35 FailureReason::ExitCode { code } => format!("exited with code {}", code),
36 FailureReason::Signal { signal: sig } => {
37 format!("killed by {} ({})", signal::name(*sig), sig)
38 }
39 FailureReason::StartTimeout => "start timeout".to_string(),
40 FailureReason::StopTimeout => "stop timeout".to_string(),
41 FailureReason::HealthCheckFailed { attempts } => {
42 format!("health check failed after {} attempts", attempts)
43 }
44 FailureReason::DependencyFailed { service } => {
45 format!("dependency '{}' failed", service)
46 }
47 FailureReason::SpawnError { message } => format!("spawn error: {}", message),
48 FailureReason::MissingDependency { dependency } => {
49 format!("missing dependency '{}'", dependency)
50 }
51 }
52 }
53}
54
55impl std::fmt::Display for FailureReason {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(f, "{}", self.display())
58 }
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(tag = "state", rename_all = "snake_case")]
64pub enum ServiceState {
65 #[default]
67 Inactive,
68 Blocked { waiting_on: Vec<String> },
70 Starting { pid: u32 },
72 Running { pid: u32 },
74 Stopping { pid: u32 },
76 Exited { exit_code: Option<i32> },
78 Failed { reason: FailureReason },
80}
81
82impl ServiceState {
83 pub fn name(&self) -> &'static str {
85 match self {
86 ServiceState::Inactive => "inactive",
87 ServiceState::Blocked { .. } => "blocked",
88 ServiceState::Starting { .. } => "starting",
89 ServiceState::Running { .. } => "running",
90 ServiceState::Stopping { .. } => "stopping",
91 ServiceState::Exited { .. } => "exited",
92 ServiceState::Failed { .. } => "failed",
93 }
94 }
95
96 pub fn symbol(&self) -> &'static str {
98 match self {
99 ServiceState::Inactive => "[-]",
100 ServiceState::Blocked { .. } => "[?]",
101 ServiceState::Starting { .. } => "[>]",
102 ServiceState::Running { .. } => "[+]",
103 ServiceState::Stopping { .. } => "[!]",
104 ServiceState::Exited { .. } => "[.]",
105 ServiceState::Failed { .. } => "[X]",
106 }
107 }
108
109 pub fn pid(&self) -> Option<u32> {
111 match self {
112 ServiceState::Starting { pid }
113 | ServiceState::Running { pid }
114 | ServiceState::Stopping { pid } => Some(*pid),
115 _ => None,
116 }
117 }
118
119 pub fn is_active(&self) -> bool {
121 matches!(
122 self,
123 ServiceState::Starting { .. }
124 | ServiceState::Running { .. }
125 | ServiceState::Stopping { .. }
126 )
127 }
128
129 pub fn is_satisfied(&self) -> bool {
133 matches!(
134 self,
135 ServiceState::Running { .. } | ServiceState::Exited { exit_code: Some(0) }
136 )
137 }
138
139 pub fn can_attempt_start(&self) -> bool {
143 match self {
144 ServiceState::Inactive | ServiceState::Blocked { .. } | ServiceState::Exited { .. } => {
145 true
146 }
147 ServiceState::Failed { reason } => {
148 !matches!(reason, FailureReason::MissingDependency { .. })
150 }
151 _ => false,
152 }
153 }
154}
155
156impl std::fmt::Display for ServiceState {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 write!(f, "{}", self.name())
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_state_names() {
168 assert_eq!(ServiceState::Inactive.name(), "inactive");
169 assert_eq!(
170 ServiceState::Blocked { waiting_on: vec![] }.name(),
171 "blocked"
172 );
173 assert_eq!(ServiceState::Starting { pid: 1 }.name(), "starting");
174 assert_eq!(ServiceState::Running { pid: 1 }.name(), "running");
175 assert_eq!(ServiceState::Stopping { pid: 1 }.name(), "stopping");
176 assert_eq!(ServiceState::Exited { exit_code: None }.name(), "exited");
177 assert_eq!(
178 ServiceState::Failed {
179 reason: FailureReason::StartTimeout
180 }
181 .name(),
182 "failed"
183 );
184 }
185
186 #[test]
187 fn test_state_symbols() {
188 assert_eq!(ServiceState::Inactive.symbol(), "[-]");
189 assert_eq!(ServiceState::Running { pid: 1 }.symbol(), "[+]");
190 assert_eq!(
191 ServiceState::Failed {
192 reason: FailureReason::StartTimeout
193 }
194 .symbol(),
195 "[X]"
196 );
197 }
198
199 #[test]
200 fn test_pid_extraction() {
201 assert_eq!(ServiceState::Inactive.pid(), None);
202 assert_eq!(ServiceState::Starting { pid: 123 }.pid(), Some(123));
203 assert_eq!(ServiceState::Running { pid: 456 }.pid(), Some(456));
204 assert_eq!(ServiceState::Stopping { pid: 789 }.pid(), Some(789));
205 assert_eq!(ServiceState::Exited { exit_code: Some(0) }.pid(), None);
206 }
207
208 #[test]
209 fn test_is_active() {
210 assert!(!ServiceState::Inactive.is_active());
211 assert!(ServiceState::Starting { pid: 1 }.is_active());
212 assert!(ServiceState::Running { pid: 1 }.is_active());
213 assert!(ServiceState::Stopping { pid: 1 }.is_active());
214 assert!(!ServiceState::Exited { exit_code: None }.is_active());
215 }
216
217 #[test]
218 fn test_is_satisfied() {
219 assert!(!ServiceState::Inactive.is_satisfied());
220 assert!(!ServiceState::Starting { pid: 1 }.is_satisfied());
221 assert!(ServiceState::Running { pid: 1 }.is_satisfied());
222 assert!(!ServiceState::Stopping { pid: 1 }.is_satisfied());
223 }
224
225 #[test]
226 fn test_can_attempt_start() {
227 assert!(ServiceState::Inactive.can_attempt_start());
228 assert!(!ServiceState::Running { pid: 1 }.can_attempt_start());
229 assert!(ServiceState::Exited { exit_code: Some(0) }.can_attempt_start());
230 assert!(
232 ServiceState::Failed {
233 reason: FailureReason::StartTimeout
234 }
235 .can_attempt_start()
236 );
237 assert!(
238 ServiceState::Failed {
239 reason: FailureReason::ExitCode { code: 1 }
240 }
241 .can_attempt_start()
242 );
243 assert!(
245 !ServiceState::Failed {
246 reason: FailureReason::MissingDependency {
247 dependency: "missing".to_string()
248 }
249 }
250 .can_attempt_start()
251 );
252 }
253
254 #[test]
255 fn test_failure_reason_display() {
256 assert_eq!(
257 FailureReason::ExitCode { code: 1 }.display(),
258 "exited with code 1"
259 );
260 assert_eq!(FailureReason::StartTimeout.display(), "start timeout");
261 assert_eq!(
262 FailureReason::DependencyFailed {
263 service: "foo".to_string()
264 }
265 .display(),
266 "dependency 'foo' failed"
267 );
268 }
269
270 #[test]
271 fn test_serialization() {
272 let state = ServiceState::Running { pid: 123 };
273 let json = serde_json::to_string(&state).unwrap();
274 assert!(json.contains("running"));
275 assert!(json.contains("123"));
276
277 let parsed: ServiceState = serde_json::from_str(&json).unwrap();
278 assert_eq!(parsed, state);
279 }
280}