1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13pub const MAX_MESSAGE_SIZE: usize = 1024 * 1024; pub const PROTOCOL_VERSION: u32 = 1;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(into = "i32", from = "i32")]
27pub enum ErrorCode {
28 General = 1,
29 NotFound = 2,
30}
31
32impl ErrorCode {
33 pub fn exit_code(self) -> i32 {
34 self as i32
35 }
36}
37
38impl From<i32> for ErrorCode {
39 fn from(v: i32) -> Self {
40 match v {
41 2 => Self::NotFound,
42 _ => Self::General,
43 }
44 }
45}
46
47impl From<ErrorCode> for i32 {
48 fn from(c: ErrorCode) -> i32 {
49 c as i32
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct RestartPolicy {
56 pub mode: RestartMode,
57 pub max_restarts: Option<u32>,
58 pub restart_delay_ms: u64,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
63#[serde(rename_all = "kebab-case")]
64pub enum RestartMode {
65 Always,
66 OnFailure,
67 Never,
68}
69
70impl RestartMode {
71 pub fn parse(s: &str) -> Self {
73 match s {
74 "always" => Self::Always,
75 "on-failure" => Self::OnFailure,
76 _ => Self::Never,
77 }
78 }
79
80 pub fn should_restart(self, exit_code: Option<i32>) -> bool {
82 match self {
83 Self::Never => false,
84 Self::Always => true,
85 Self::OnFailure => exit_code != Some(0),
86 }
87 }
88}
89
90impl RestartPolicy {
91 pub fn from_args(mode: &str, max_restarts: Option<u32>, restart_delay: Option<u64>) -> Self {
93 Self {
94 mode: RestartMode::parse(mode),
95 max_restarts,
96 restart_delay_ms: restart_delay.unwrap_or(1000),
97 }
98 }
99}
100
101impl WatchConfig {
102 pub fn from_args(paths: Vec<String>, ignore: Vec<String>) -> Option<Self> {
104 if paths.is_empty() {
105 None
106 } else {
107 Some(Self {
108 paths,
109 ignore: if ignore.is_empty() {
110 None
111 } else {
112 Some(ignore)
113 },
114 })
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct WatchConfig {
122 pub paths: Vec<String>,
123 #[serde(default)]
124 pub ignore: Option<Vec<String>>,
125}
126
127pub fn process_url(name: &str, port: u16, proxy_port: Option<u16>) -> String {
132 match proxy_port {
133 Some(pp) => format!("http://{}.localhost:{}", name, pp),
134 None => format!("http://127.0.0.1:{}", port),
135 }
136}
137
138#[must_use]
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
154#[serde(tag = "type")]
155pub enum Request {
156 Run {
157 command: String,
158 name: Option<String>,
159 cwd: Option<String>,
160 #[serde(default)]
161 env: Option<HashMap<String, String>>,
162 #[serde(default)]
163 port: Option<u16>,
164 #[serde(default)]
165 restart: Option<RestartPolicy>,
166 #[serde(default)]
167 watch: Option<WatchConfig>,
168 },
169 Stop {
170 target: String,
171 },
172 StopAll,
173 Restart {
174 target: String,
175 },
176 Status,
177 Logs {
178 target: Option<String>,
179 tail: usize,
180 follow: bool,
181 stderr: bool,
182 all: bool,
183 timeout_secs: Option<u64>,
184 #[serde(default)]
185 lines: Option<usize>,
186 },
187 Wait {
188 target: String,
189 until: Option<String>,
190 regex: bool,
191 exit: bool,
192 timeout_secs: Option<u64>,
193 },
194 Shutdown,
195 EnableProxy {
196 #[serde(default)]
197 proxy_port: Option<u16>,
198 },
199 Hello {
200 version: u32,
201 },
202 #[serde(other)]
204 Unknown,
205}
206
207#[must_use]
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221#[serde(tag = "type")]
222pub enum Response {
223 Ok {
224 message: String,
225 },
226 RunOk {
227 name: String,
228 id: String,
229 pid: u32,
230 #[serde(default)]
231 port: Option<u16>,
232 #[serde(default)]
233 url: Option<String>,
234 },
235 Status {
236 processes: Vec<ProcessInfo>,
237 },
238 LogLine {
239 process: String,
240 stream: Stream,
241 line: String,
242 },
243 LogEnd,
244 WaitMatch {
245 line: String,
246 },
247 WaitExited {
248 exit_code: Option<i32>,
249 },
250 WaitTimeout,
251 Error {
252 code: ErrorCode,
253 message: String,
254 },
255 Hello {
256 version: u32,
257 },
258 #[serde(other)]
260 Unknown,
261}
262
263#[must_use]
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
265pub struct ProcessInfo {
266 pub name: String,
267 pub id: String,
268 pub pid: u32,
269 pub state: ProcessState,
270 pub exit_code: Option<i32>,
271 pub uptime_secs: Option<u64>,
272 pub command: String,
273 #[serde(default)]
274 pub port: Option<u16>,
275 #[serde(default)]
276 pub url: Option<String>,
277 #[serde(default)]
278 pub restart_count: Option<u32>,
279 #[serde(default)]
280 pub max_restarts: Option<u32>,
281 #[serde(default)]
282 pub restart_policy: Option<String>,
283 #[serde(default)]
284 pub watched: Option<bool>,
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize)]
288#[serde(rename_all = "lowercase")]
289pub enum ProcessState {
290 Running,
291 Exited,
292 Failed,
293 Unknown,
294}
295
296impl std::fmt::Display for ProcessState {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 match self {
299 Self::Running => write!(f, "running"),
300 Self::Exited => write!(f, "exited"),
301 Self::Failed => write!(f, "FAILED"),
302 Self::Unknown => write!(f, "unknown"),
303 }
304 }
305}
306
307impl<'de> serde::Deserialize<'de> for ProcessState {
308 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
309 where
310 D: serde::Deserializer<'de>,
311 {
312 let s = String::deserialize(deserializer)?;
313 Ok(match s.as_str() {
314 "running" => Self::Running,
315 "exited" => Self::Exited,
316 "failed" => Self::Failed,
317 _ => Self::Unknown,
318 })
319 }
320}
321
322#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
323#[serde(rename_all = "lowercase")]
324pub enum Stream {
325 Stdout,
326 Stderr,
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_request_serde_roundtrip() {
335 let requests = vec![
336 Request::Run {
337 command: "echo hi".into(),
338 name: Some("test".into()),
339 cwd: None,
340 env: None,
341 port: None,
342 restart: None,
343 watch: None,
344 },
345 Request::Stop {
346 target: "test".into(),
347 },
348 Request::StopAll,
349 Request::Restart {
350 target: "test".into(),
351 },
352 Request::Status,
353 Request::Logs {
354 target: Some("test".into()),
355 tail: 10,
356 follow: true,
357 stderr: false,
358 all: false,
359 timeout_secs: Some(30),
360 lines: None,
361 },
362 Request::Wait {
363 target: "test".into(),
364 until: Some("ready".into()),
365 regex: false,
366 exit: false,
367 timeout_secs: Some(60),
368 },
369 Request::Shutdown,
370 Request::EnableProxy {
371 proxy_port: Some(8080),
372 },
373 Request::Hello {
374 version: PROTOCOL_VERSION,
375 },
376 Request::Unknown,
377 ];
378
379 for req in &requests {
380 let json = serde_json::to_string(req).unwrap();
381 let parsed: Request = serde_json::from_str(&json).unwrap();
382 assert_eq!(&parsed, req);
383 }
384 }
385
386 #[test]
387 fn test_response_serde_roundtrip() {
388 let responses = vec![
389 Response::Ok {
390 message: "done".into(),
391 },
392 Response::RunOk {
393 name: "web".into(),
394 id: "p1".into(),
395 pid: 1234,
396 port: Some(3000),
397 url: Some("http://127.0.0.1:3000".into()),
398 },
399 Response::Status { processes: vec![] },
400 Response::LogLine {
401 process: "web".into(),
402 stream: Stream::Stdout,
403 line: "hello".into(),
404 },
405 Response::LogEnd,
406 Response::WaitMatch {
407 line: "ready".into(),
408 },
409 Response::WaitExited { exit_code: Some(0) },
410 Response::WaitTimeout,
411 Response::Error {
412 code: ErrorCode::General,
413 message: "oops".into(),
414 },
415 Response::Hello {
416 version: PROTOCOL_VERSION,
417 },
418 Response::Unknown,
419 ];
420
421 for resp in &responses {
422 let json = serde_json::to_string(resp).unwrap();
423 let parsed: Response = serde_json::from_str(&json).unwrap();
424 assert_eq!(&parsed, resp);
425 }
426 }
427
428 #[test]
429 fn test_process_info_serde_roundtrip() {
430 let info = ProcessInfo {
431 name: "api".into(),
432 id: "p1".into(),
433 pid: 42,
434 state: ProcessState::Running,
435 exit_code: None,
436 uptime_secs: Some(120),
437 command: "cargo run".into(),
438 port: Some(8080),
439 url: Some("http://127.0.0.1:8080".into()),
440 restart_count: None,
441 max_restarts: None,
442 restart_policy: None,
443 watched: None,
444 };
445 let json = serde_json::to_string(&info).unwrap();
446 let parsed: ProcessInfo = serde_json::from_str(&json).unwrap();
447 assert_eq!(parsed, info);
448 }
449
450 #[test]
451 fn test_request_json_format() {
452 let run = Request::Run {
453 command: "ls".into(),
454 name: None,
455 cwd: None,
456 env: None,
457 port: None,
458 restart: None,
459 watch: None,
460 };
461 let json = serde_json::to_string(&run).unwrap();
462 assert!(json.contains("\"type\":\"Run\""));
463
464 let stop = Request::Stop { target: "x".into() };
465 let json = serde_json::to_string(&stop).unwrap();
466 assert!(json.contains("\"type\":\"Stop\""));
467
468 let status = Request::Status;
469 let json = serde_json::to_string(&status).unwrap();
470 assert!(json.contains("\"type\":\"Status\""));
471
472 let shutdown = Request::Shutdown;
473 let json = serde_json::to_string(&shutdown).unwrap();
474 assert!(json.contains("\"type\":\"Shutdown\""));
475
476 let stop_all = Request::StopAll;
477 let json = serde_json::to_string(&stop_all).unwrap();
478 assert!(json.contains("\"type\":\"StopAll\""));
479
480 let restart = Request::Restart { target: "x".into() };
481 let json = serde_json::to_string(&restart).unwrap();
482 assert!(json.contains("\"type\":\"Restart\""));
483
484 let enable_proxy = Request::EnableProxy { proxy_port: None };
485 let json = serde_json::to_string(&enable_proxy).unwrap();
486 assert!(json.contains("\"type\":\"EnableProxy\""));
487
488 let hello = Request::Hello { version: 1 };
489 let json = serde_json::to_string(&hello).unwrap();
490 assert!(json.contains("\"type\":\"Hello\""));
491 }
492
493 #[test]
494 fn test_unknown_request_deserialization() {
495 let json = r#"{"type":"FutureRequestType","data":"something"}"#;
497 let parsed: Request = serde_json::from_str(json).unwrap();
498 assert_eq!(parsed, Request::Unknown);
499 }
500
501 #[test]
502 fn test_unknown_response_deserialization() {
503 let json = r#"{"type":"FutureResponseType","data":"something"}"#;
504 let parsed: Response = serde_json::from_str(json).unwrap();
505 assert_eq!(parsed, Response::Unknown);
506 }
507
508 #[test]
509 fn test_error_code_wire_format() {
510 let resp = Response::Error {
511 code: ErrorCode::NotFound,
512 message: "not found".into(),
513 };
514 let json = serde_json::to_string(&resp).unwrap();
515 assert!(json.contains("\"code\":2"));
516
517 let json = r#"{"type":"Error","code":2,"message":"not found"}"#;
519 let parsed: Response = serde_json::from_str(json).unwrap();
520 assert_eq!(
521 parsed,
522 Response::Error {
523 code: ErrorCode::NotFound,
524 message: "not found".into(),
525 }
526 );
527
528 let json = r#"{"type":"Error","code":99,"message":"future error"}"#;
530 let parsed: Response = serde_json::from_str(json).unwrap();
531 if let Response::Error { code, .. } = parsed {
532 assert_eq!(code, ErrorCode::General);
533 } else {
534 panic!("expected Error");
535 }
536 }
537}