1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13pub const MAX_MESSAGE_SIZE: usize = 1024 * 1024; #[must_use]
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33#[serde(tag = "type")]
34pub enum Request {
35 Run {
36 command: String,
37 name: Option<String>,
38 cwd: Option<String>,
39 #[serde(default)]
40 env: Option<HashMap<String, String>>,
41 #[serde(default)]
42 port: Option<u16>,
43 },
44 Stop {
45 target: String,
46 },
47 StopAll,
48 Restart {
49 target: String,
50 },
51 Status,
52 Logs {
53 target: Option<String>,
54 tail: usize,
55 follow: bool,
56 stderr: bool,
57 all: bool,
58 timeout_secs: Option<u64>,
59 #[serde(default)]
60 lines: Option<usize>,
61 },
62 Wait {
63 target: String,
64 until: Option<String>,
65 regex: bool,
66 exit: bool,
67 timeout_secs: Option<u64>,
68 },
69 Shutdown,
70 EnableProxy {
71 #[serde(default)]
72 proxy_port: Option<u16>,
73 },
74}
75
76#[must_use]
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(tag = "type")]
91pub enum Response {
92 Ok {
93 message: String,
94 },
95 RunOk {
96 name: String,
97 id: String,
98 pid: u32,
99 #[serde(default)]
100 port: Option<u16>,
101 #[serde(default)]
102 url: Option<String>,
103 },
104 Status {
105 processes: Vec<ProcessInfo>,
106 },
107 LogLine {
108 process: String,
109 stream: Stream,
110 line: String,
111 },
112 LogEnd,
113 WaitMatch {
114 line: String,
115 },
116 WaitExited {
117 exit_code: Option<i32>,
118 },
119 WaitTimeout,
120 Error {
121 code: i32,
122 message: String,
123 },
124}
125
126#[must_use]
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct ProcessInfo {
129 pub name: String,
130 pub id: String,
131 pub pid: u32,
132 pub state: ProcessState,
133 pub exit_code: Option<i32>,
134 pub uptime_secs: Option<u64>,
135 pub command: String,
136 #[serde(default)]
137 pub port: Option<u16>,
138 #[serde(default)]
139 pub url: Option<String>,
140}
141
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[serde(rename_all = "lowercase")]
144pub enum ProcessState {
145 Running,
146 Exited,
147}
148
149#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
150#[serde(rename_all = "lowercase")]
151pub enum Stream {
152 Stdout,
153 Stderr,
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_request_serde_roundtrip() {
162 let requests = vec![
163 Request::Run {
164 command: "echo hi".into(),
165 name: Some("test".into()),
166 cwd: None,
167 env: None,
168 port: None,
169 },
170 Request::Stop {
171 target: "test".into(),
172 },
173 Request::StopAll,
174 Request::Restart {
175 target: "test".into(),
176 },
177 Request::Status,
178 Request::Logs {
179 target: Some("test".into()),
180 tail: 10,
181 follow: true,
182 stderr: false,
183 all: false,
184 timeout_secs: Some(30),
185 lines: None,
186 },
187 Request::Wait {
188 target: "test".into(),
189 until: Some("ready".into()),
190 regex: false,
191 exit: false,
192 timeout_secs: Some(60),
193 },
194 Request::Shutdown,
195 Request::EnableProxy {
196 proxy_port: Some(8080),
197 },
198 ];
199
200 for req in &requests {
201 let json = serde_json::to_string(req).unwrap();
202 let parsed: Request = serde_json::from_str(&json).unwrap();
203 assert_eq!(&parsed, req);
204 }
205 }
206
207 #[test]
208 fn test_response_serde_roundtrip() {
209 let responses = vec![
210 Response::Ok {
211 message: "done".into(),
212 },
213 Response::RunOk {
214 name: "web".into(),
215 id: "p1".into(),
216 pid: 1234,
217 port: Some(3000),
218 url: Some("http://127.0.0.1:3000".into()),
219 },
220 Response::Status { processes: vec![] },
221 Response::LogLine {
222 process: "web".into(),
223 stream: Stream::Stdout,
224 line: "hello".into(),
225 },
226 Response::LogEnd,
227 Response::WaitMatch {
228 line: "ready".into(),
229 },
230 Response::WaitExited { exit_code: Some(0) },
231 Response::WaitTimeout,
232 Response::Error {
233 code: 1,
234 message: "oops".into(),
235 },
236 ];
237
238 for resp in &responses {
239 let json = serde_json::to_string(resp).unwrap();
240 let parsed: Response = serde_json::from_str(&json).unwrap();
241 assert_eq!(&parsed, resp);
242 }
243 }
244
245 #[test]
246 fn test_process_info_serde_roundtrip() {
247 let info = ProcessInfo {
248 name: "api".into(),
249 id: "p1".into(),
250 pid: 42,
251 state: ProcessState::Running,
252 exit_code: None,
253 uptime_secs: Some(120),
254 command: "cargo run".into(),
255 port: Some(8080),
256 url: Some("http://127.0.0.1:8080".into()),
257 };
258 let json = serde_json::to_string(&info).unwrap();
259 let parsed: ProcessInfo = serde_json::from_str(&json).unwrap();
260 assert_eq!(parsed, info);
261 }
262
263 #[test]
264 fn test_request_json_format() {
265 let run = Request::Run {
266 command: "ls".into(),
267 name: None,
268 cwd: None,
269 env: None,
270 port: None,
271 };
272 let json = serde_json::to_string(&run).unwrap();
273 assert!(json.contains("\"type\":\"Run\""));
274
275 let stop = Request::Stop { target: "x".into() };
276 let json = serde_json::to_string(&stop).unwrap();
277 assert!(json.contains("\"type\":\"Stop\""));
278
279 let status = Request::Status;
280 let json = serde_json::to_string(&status).unwrap();
281 assert!(json.contains("\"type\":\"Status\""));
282
283 let shutdown = Request::Shutdown;
284 let json = serde_json::to_string(&shutdown).unwrap();
285 assert!(json.contains("\"type\":\"Shutdown\""));
286
287 let stop_all = Request::StopAll;
288 let json = serde_json::to_string(&stop_all).unwrap();
289 assert!(json.contains("\"type\":\"StopAll\""));
290
291 let restart = Request::Restart { target: "x".into() };
292 let json = serde_json::to_string(&restart).unwrap();
293 assert!(json.contains("\"type\":\"Restart\""));
294
295 let enable_proxy = Request::EnableProxy { proxy_port: None };
296 let json = serde_json::to_string(&enable_proxy).unwrap();
297 assert!(json.contains("\"type\":\"EnableProxy\""));
298 }
299}