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
53pub fn process_url(name: &str, port: u16, proxy_port: Option<u16>) -> String {
58 match proxy_port {
59 Some(pp) => format!("http://{}.localhost:{}", name, pp),
60 None => format!("http://127.0.0.1:{}", port),
61 }
62}
63
64#[must_use]
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80#[serde(tag = "type")]
81pub enum Request {
82 Run {
83 command: String,
84 name: Option<String>,
85 cwd: Option<String>,
86 #[serde(default)]
87 env: Option<HashMap<String, String>>,
88 #[serde(default)]
89 port: Option<u16>,
90 },
91 Stop {
92 target: String,
93 },
94 StopAll,
95 Restart {
96 target: String,
97 },
98 Status,
99 Logs {
100 target: Option<String>,
101 tail: usize,
102 follow: bool,
103 stderr: bool,
104 all: bool,
105 timeout_secs: Option<u64>,
106 #[serde(default)]
107 lines: Option<usize>,
108 },
109 Wait {
110 target: String,
111 until: Option<String>,
112 regex: bool,
113 exit: bool,
114 timeout_secs: Option<u64>,
115 },
116 Shutdown,
117 EnableProxy {
118 #[serde(default)]
119 proxy_port: Option<u16>,
120 },
121 Hello {
122 version: u32,
123 },
124 #[serde(other)]
126 Unknown,
127}
128
129#[must_use]
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[serde(tag = "type")]
144pub enum Response {
145 Ok {
146 message: String,
147 },
148 RunOk {
149 name: String,
150 id: String,
151 pid: u32,
152 #[serde(default)]
153 port: Option<u16>,
154 #[serde(default)]
155 url: Option<String>,
156 },
157 Status {
158 processes: Vec<ProcessInfo>,
159 },
160 LogLine {
161 process: String,
162 stream: Stream,
163 line: String,
164 },
165 LogEnd,
166 WaitMatch {
167 line: String,
168 },
169 WaitExited {
170 exit_code: Option<i32>,
171 },
172 WaitTimeout,
173 Error {
174 code: ErrorCode,
175 message: String,
176 },
177 Hello {
178 version: u32,
179 },
180 #[serde(other)]
182 Unknown,
183}
184
185#[must_use]
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct ProcessInfo {
188 pub name: String,
189 pub id: String,
190 pub pid: u32,
191 pub state: ProcessState,
192 pub exit_code: Option<i32>,
193 pub uptime_secs: Option<u64>,
194 pub command: String,
195 #[serde(default)]
196 pub port: Option<u16>,
197 #[serde(default)]
198 pub url: Option<String>,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202#[serde(rename_all = "lowercase")]
203pub enum ProcessState {
204 Running,
205 Exited,
206}
207
208#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
209#[serde(rename_all = "lowercase")]
210pub enum Stream {
211 Stdout,
212 Stderr,
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_request_serde_roundtrip() {
221 let requests = vec![
222 Request::Run {
223 command: "echo hi".into(),
224 name: Some("test".into()),
225 cwd: None,
226 env: None,
227 port: None,
228 },
229 Request::Stop {
230 target: "test".into(),
231 },
232 Request::StopAll,
233 Request::Restart {
234 target: "test".into(),
235 },
236 Request::Status,
237 Request::Logs {
238 target: Some("test".into()),
239 tail: 10,
240 follow: true,
241 stderr: false,
242 all: false,
243 timeout_secs: Some(30),
244 lines: None,
245 },
246 Request::Wait {
247 target: "test".into(),
248 until: Some("ready".into()),
249 regex: false,
250 exit: false,
251 timeout_secs: Some(60),
252 },
253 Request::Shutdown,
254 Request::EnableProxy {
255 proxy_port: Some(8080),
256 },
257 Request::Hello {
258 version: PROTOCOL_VERSION,
259 },
260 Request::Unknown,
261 ];
262
263 for req in &requests {
264 let json = serde_json::to_string(req).unwrap();
265 let parsed: Request = serde_json::from_str(&json).unwrap();
266 assert_eq!(&parsed, req);
267 }
268 }
269
270 #[test]
271 fn test_response_serde_roundtrip() {
272 let responses = vec![
273 Response::Ok {
274 message: "done".into(),
275 },
276 Response::RunOk {
277 name: "web".into(),
278 id: "p1".into(),
279 pid: 1234,
280 port: Some(3000),
281 url: Some("http://127.0.0.1:3000".into()),
282 },
283 Response::Status { processes: vec![] },
284 Response::LogLine {
285 process: "web".into(),
286 stream: Stream::Stdout,
287 line: "hello".into(),
288 },
289 Response::LogEnd,
290 Response::WaitMatch {
291 line: "ready".into(),
292 },
293 Response::WaitExited { exit_code: Some(0) },
294 Response::WaitTimeout,
295 Response::Error {
296 code: ErrorCode::General,
297 message: "oops".into(),
298 },
299 Response::Hello {
300 version: PROTOCOL_VERSION,
301 },
302 Response::Unknown,
303 ];
304
305 for resp in &responses {
306 let json = serde_json::to_string(resp).unwrap();
307 let parsed: Response = serde_json::from_str(&json).unwrap();
308 assert_eq!(&parsed, resp);
309 }
310 }
311
312 #[test]
313 fn test_process_info_serde_roundtrip() {
314 let info = ProcessInfo {
315 name: "api".into(),
316 id: "p1".into(),
317 pid: 42,
318 state: ProcessState::Running,
319 exit_code: None,
320 uptime_secs: Some(120),
321 command: "cargo run".into(),
322 port: Some(8080),
323 url: Some("http://127.0.0.1:8080".into()),
324 };
325 let json = serde_json::to_string(&info).unwrap();
326 let parsed: ProcessInfo = serde_json::from_str(&json).unwrap();
327 assert_eq!(parsed, info);
328 }
329
330 #[test]
331 fn test_request_json_format() {
332 let run = Request::Run {
333 command: "ls".into(),
334 name: None,
335 cwd: None,
336 env: None,
337 port: None,
338 };
339 let json = serde_json::to_string(&run).unwrap();
340 assert!(json.contains("\"type\":\"Run\""));
341
342 let stop = Request::Stop { target: "x".into() };
343 let json = serde_json::to_string(&stop).unwrap();
344 assert!(json.contains("\"type\":\"Stop\""));
345
346 let status = Request::Status;
347 let json = serde_json::to_string(&status).unwrap();
348 assert!(json.contains("\"type\":\"Status\""));
349
350 let shutdown = Request::Shutdown;
351 let json = serde_json::to_string(&shutdown).unwrap();
352 assert!(json.contains("\"type\":\"Shutdown\""));
353
354 let stop_all = Request::StopAll;
355 let json = serde_json::to_string(&stop_all).unwrap();
356 assert!(json.contains("\"type\":\"StopAll\""));
357
358 let restart = Request::Restart { target: "x".into() };
359 let json = serde_json::to_string(&restart).unwrap();
360 assert!(json.contains("\"type\":\"Restart\""));
361
362 let enable_proxy = Request::EnableProxy { proxy_port: None };
363 let json = serde_json::to_string(&enable_proxy).unwrap();
364 assert!(json.contains("\"type\":\"EnableProxy\""));
365
366 let hello = Request::Hello { version: 1 };
367 let json = serde_json::to_string(&hello).unwrap();
368 assert!(json.contains("\"type\":\"Hello\""));
369 }
370
371 #[test]
372 fn test_unknown_request_deserialization() {
373 let json = r#"{"type":"FutureRequestType","data":"something"}"#;
375 let parsed: Request = serde_json::from_str(json).unwrap();
376 assert_eq!(parsed, Request::Unknown);
377 }
378
379 #[test]
380 fn test_unknown_response_deserialization() {
381 let json = r#"{"type":"FutureResponseType","data":"something"}"#;
382 let parsed: Response = serde_json::from_str(json).unwrap();
383 assert_eq!(parsed, Response::Unknown);
384 }
385
386 #[test]
387 fn test_error_code_wire_format() {
388 let resp = Response::Error {
389 code: ErrorCode::NotFound,
390 message: "not found".into(),
391 };
392 let json = serde_json::to_string(&resp).unwrap();
393 assert!(json.contains("\"code\":2"));
394
395 let json = r#"{"type":"Error","code":2,"message":"not found"}"#;
397 let parsed: Response = serde_json::from_str(json).unwrap();
398 assert_eq!(
399 parsed,
400 Response::Error {
401 code: ErrorCode::NotFound,
402 message: "not found".into(),
403 }
404 );
405
406 let json = r#"{"type":"Error","code":99,"message":"future error"}"#;
408 let parsed: Response = serde_json::from_str(json).unwrap();
409 if let Response::Error { code, .. } = parsed {
410 assert_eq!(code, ErrorCode::General);
411 } else {
412 panic!("expected Error");
413 }
414 }
415}