1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::hash::{BuildHasher, Hasher, RandomState};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(tag = "type")]
19pub enum LogEntry {
20 #[serde(rename = "console")]
22 Console(ConsoleEntry),
23 #[serde(rename = "error")]
25 Error(ErrorEntry),
26 #[serde(rename = "network")]
28 Network(NetworkEntry),
29 #[serde(rename = "app")]
31 App(AppEntry),
32}
33
34impl LogEntry {
35 pub fn id(&self) -> Option<&str> {
37 match self {
38 Self::Console(e) => e.id.as_deref(),
39 Self::Error(e) => e.id.as_deref(),
40 Self::Network(e) => e.id.as_deref(),
41 Self::App(e) => e.id.as_deref(),
42 }
43 }
44
45 pub fn timestamp(&self) -> i64 {
47 match self {
48 Self::Console(e) => e.timestamp,
49 Self::Error(e) => e.timestamp,
50 Self::Network(e) => e.timestamp,
51 Self::App(e) => e.timestamp,
52 }
53 }
54
55 pub fn set_id(&mut self, id: String) {
57 match self {
58 Self::Console(e) => e.id = Some(id),
59 Self::Error(e) => e.id = Some(id),
60 Self::Network(e) => e.id = Some(id),
61 Self::App(e) => e.id = Some(id),
62 }
63 }
64
65 pub fn source(&self) -> &str {
67 match self {
68 Self::Console(e) => &e.source,
69 Self::Error(e) => &e.source,
70 Self::Network(e) => &e.source,
71 Self::App(e) => &e.source,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ConsoleEntry {
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub id: Option<String>,
82 pub level: String,
84 pub args: Vec<serde_json::Value>,
86 pub timestamp: i64,
88 pub source: String,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ErrorEntry {
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub id: Option<String>,
98 pub message: String,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub stack: Option<String>,
103 pub timestamp: i64,
105 pub source: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub url: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub component: Option<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct NetworkEntry {
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub id: Option<String>,
121 pub url: String,
123 pub method: String,
125 pub status: i32,
127 pub duration: f64,
129 pub timestamp: i64,
131 pub failed: bool,
133 pub source: String,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub kind: Option<String>,
138 #[serde(rename = "requestHeaders", skip_serializing_if = "Option::is_none")]
140 pub request_headers: Option<HashMap<String, String>>,
141 #[serde(rename = "responseHeaders", skip_serializing_if = "Option::is_none")]
143 pub response_headers: Option<HashMap<String, String>>,
144 #[serde(rename = "responseBody", skip_serializing_if = "Option::is_none")]
146 pub response_body: Option<String>,
147 #[serde(rename = "connectionId", skip_serializing_if = "Option::is_none")]
149 pub connection_id: Option<String>,
150 #[serde(rename = "messageCount", skip_serializing_if = "Option::is_none")]
152 pub message_count: Option<i32>,
153 #[serde(rename = "wsReadyState", skip_serializing_if = "Option::is_none")]
155 pub ws_ready_state: Option<i32>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct CookieInfo {
161 pub name: String,
163 pub value: String,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub domain: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub path: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub expires: Option<f64>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub secure: Option<bool>,
177 #[serde(rename = "sameSite", skip_serializing_if = "Option::is_none")]
179 pub same_site: Option<String>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct ServiceWorkerInfo {
185 pub scope: String,
187 #[serde(rename = "scriptURL")]
189 pub script_url: String,
190 pub state: String,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct CacheInfo {
197 pub name: String,
199 #[serde(rename = "entryCount")]
201 pub entry_count: i32,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct PermissionInfo {
207 pub name: String,
209 pub state: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct StorageEstimate {
216 pub usage: f64,
218 pub quota: f64,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct AppEntry {
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub id: Option<String>,
228 pub timestamp: i64,
230 pub source: String,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub cookies: Option<Vec<CookieInfo>>,
235 #[serde(rename = "localStorage", skip_serializing_if = "Option::is_none")]
237 pub local_storage: Option<HashMap<String, String>>,
238 #[serde(rename = "sessionStorage", skip_serializing_if = "Option::is_none")]
240 pub session_storage: Option<HashMap<String, String>>,
241 #[serde(rename = "serviceWorkers", skip_serializing_if = "Option::is_none")]
243 pub service_workers: Option<Vec<ServiceWorkerInfo>>,
244 #[serde(rename = "cacheStorage", skip_serializing_if = "Option::is_none")]
246 pub cache_storage: Option<Vec<CacheInfo>>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub permissions: Option<Vec<PermissionInfo>>,
250 #[serde(rename = "storageEstimate", skip_serializing_if = "Option::is_none")]
252 pub storage_estimate: Option<StorageEstimate>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct SessionInfo {
262 #[serde(rename = "sessionId")]
264 pub session_id: String,
265 pub framework: String,
267 pub port: i32,
269 pub pid: u32,
271 #[serde(rename = "startedAt")]
273 pub started_at: i64,
274 #[serde(rename = "socketPath")]
276 pub socket_path: String,
277}
278
279#[derive(Debug, Clone, Default, Serialize, Deserialize)]
281pub struct Filters {
282 #[serde(skip_serializing_if = "Option::is_none")]
284 pub id: Option<String>,
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub ids: Option<Vec<String>>,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub last: Option<f64>,
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub level: Option<String>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub status: Option<f64>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub failed: Option<bool>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub limit: Option<usize>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub source: Option<String>,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub subcommand: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub name: Option<String>,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub key: Option<String>,
315 #[serde(rename = "storageType", skip_serializing_if = "Option::is_none")]
317 pub storage_type: Option<String>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct QueryRequest {
323 pub id: String,
325 pub command: String,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub filters: Option<Filters>,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub data: Option<serde_json::Value>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct QueryResponse {
338 pub id: String,
340 pub ok: bool,
342 pub data: Vec<serde_json::Value>,
344 #[serde(skip_serializing_if = "Option::is_none")]
346 pub session: Option<SessionInfo>,
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub error: Option<String>,
350}
351
352pub fn generate_id() -> String {
358 let s = RandomState::new();
359 let mut h = s.build_hasher();
360 h.write_u128(
361 SystemTime::now()
362 .duration_since(UNIX_EPOCH)
363 .unwrap_or_default()
364 .as_nanos(),
365 );
366 format!("{:06x}", h.finish() & 0xFFFFFF)
367}
368
369pub fn now_millis() -> i64 {
371 SystemTime::now()
372 .duration_since(UNIX_EPOCH)
373 .unwrap_or_default()
374 .as_millis() as i64
375}
376
377pub fn compute_socket_path(cwd: &str) -> String {
382 if cfg!(windows) {
383 use std::collections::hash_map::DefaultHasher;
387 use std::hash::Hash;
388 let mut hasher = DefaultHasher::new();
389 cwd.to_lowercase().hash(&mut hasher);
390 let hash = format!("{:016x}", hasher.finish());
391 format!(r"\\.\pipe\debugger-{}", &hash[..8])
392 } else {
393 format!("{cwd}/.debugger/bridge.sock")
394 }
395}
396
397pub fn create_session(framework: &str, port: i32) -> SessionInfo {
399 let cwd = std::env::current_dir()
400 .unwrap_or_default()
401 .to_string_lossy()
402 .to_string();
403 SessionInfo {
404 session_id: generate_id(),
405 framework: framework.to_string(),
406 port,
407 pid: std::process::id(),
408 started_at: now_millis(),
409 socket_path: compute_socket_path(&cwd),
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn generate_id_is_six_hex_chars() {
419 let id = generate_id();
420 assert_eq!(id.len(), 6);
421 assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
422 }
423
424 #[test]
425 fn console_entry_round_trip() {
426 let entry = LogEntry::Console(ConsoleEntry {
427 id: Some("abc123".to_string()),
428 level: "info".to_string(),
429 args: vec![serde_json::Value::String("hello".to_string())],
430 timestamp: 1700000000000,
431 source: "server".to_string(),
432 });
433 let json = serde_json::to_string(&entry).unwrap();
434 assert!(json.contains(r#""type":"console"#));
435 let parsed: LogEntry = serde_json::from_str(&json).unwrap();
436 assert_eq!(parsed.id(), Some("abc123"));
437 }
438
439 #[test]
440 fn query_request_round_trip() {
441 let req = QueryRequest {
442 id: "req1".to_string(),
443 command: "console".to_string(),
444 filters: Some(Filters {
445 level: Some("error".to_string()),
446 limit: Some(10),
447 ..Default::default()
448 }),
449 data: None,
450 };
451 let json = serde_json::to_string(&req).unwrap();
452 let parsed: QueryRequest = serde_json::from_str(&json).unwrap();
453 assert_eq!(parsed.command, "console");
454 assert_eq!(parsed.filters.as_ref().unwrap().level.as_deref(), Some("error"));
455 }
456
457 #[test]
458 fn now_millis_is_reasonable() {
459 let ms = now_millis();
460 assert!(ms > 1_577_836_800_000);
462 }
463}