1use std::sync::Arc;
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::time::Instant;
4
5use crate::common::mutex_lock_or_recover;
6use crate::terminal::PtyError;
7
8use crate::daemon::domain::{
9 ConsoleInput, ConsoleOutput, ErrorsInput, ErrorsOutput, HealthInput, HealthOutput,
10 MetricsInput, MetricsOutput, PtyReadInput, PtyReadOutput, PtyWriteInput, PtyWriteOutput,
11 TraceInput, TraceOutput,
12};
13use crate::daemon::error::SessionError;
14use crate::daemon::metrics::DaemonMetrics;
15use crate::daemon::repository::SessionRepository;
16
17pub trait TraceUseCase: Send + Sync {
18 fn execute(&self, input: TraceInput) -> Result<TraceOutput, SessionError>;
19}
20
21pub struct TraceUseCaseImpl<R: SessionRepository> {
22 repository: Arc<R>,
23}
24
25impl<R: SessionRepository> TraceUseCaseImpl<R> {
26 pub fn new(repository: Arc<R>) -> Self {
27 Self { repository }
28 }
29}
30
31impl<R: SessionRepository> TraceUseCase for TraceUseCaseImpl<R> {
32 fn execute(&self, input: TraceInput) -> Result<TraceOutput, SessionError> {
33 let session = self.repository.resolve(input.session_id.as_deref())?;
34 let session_guard = mutex_lock_or_recover(&session);
35
36 let count = if input.count == 0 { 1000 } else { input.count };
37 let entries = session_guard.get_trace_entries(count);
38
39 Ok(TraceOutput {
40 tracing: true,
41 entries,
42 })
43 }
44}
45
46pub trait ConsoleUseCase: Send + Sync {
47 fn execute(&self, input: ConsoleInput) -> Result<ConsoleOutput, SessionError>;
48}
49
50pub struct ConsoleUseCaseImpl<R: SessionRepository> {
51 repository: Arc<R>,
52}
53
54impl<R: SessionRepository> ConsoleUseCaseImpl<R> {
55 pub fn new(repository: Arc<R>) -> Self {
56 Self { repository }
57 }
58}
59
60impl<R: SessionRepository> ConsoleUseCase for ConsoleUseCaseImpl<R> {
61 fn execute(&self, input: ConsoleInput) -> Result<ConsoleOutput, SessionError> {
62 let session = self.repository.resolve(input.session_id.as_deref())?;
63 let mut session_guard = mutex_lock_or_recover(&session);
64
65 let _ = session_guard.update();
67
68 let screen_text = session_guard.screen_text();
69 let lines: Vec<String> = screen_text.lines().map(String::from).collect();
70
71 Ok(ConsoleOutput { lines })
72 }
73}
74
75pub trait ErrorsUseCase: Send + Sync {
76 fn execute(&self, input: ErrorsInput) -> Result<ErrorsOutput, SessionError>;
77}
78
79pub struct ErrorsUseCaseImpl<R: SessionRepository> {
80 repository: Arc<R>,
81}
82
83impl<R: SessionRepository> ErrorsUseCaseImpl<R> {
84 pub fn new(repository: Arc<R>) -> Self {
85 Self { repository }
86 }
87}
88
89impl<R: SessionRepository> ErrorsUseCase for ErrorsUseCaseImpl<R> {
90 fn execute(&self, input: ErrorsInput) -> Result<ErrorsOutput, SessionError> {
91 let session = self.repository.resolve(input.session_id.as_deref())?;
92 let session_guard = mutex_lock_or_recover(&session);
93
94 let count = if input.count == 0 { 1000 } else { input.count };
95 let errors = session_guard.get_errors(count);
96
97 Ok(ErrorsOutput {
98 total_count: errors.len(),
99 errors,
100 })
101 }
102}
103
104pub trait PtyReadUseCase: Send + Sync {
105 fn execute(&self, input: PtyReadInput) -> Result<PtyReadOutput, SessionError>;
106}
107
108pub struct PtyReadUseCaseImpl<R: SessionRepository> {
109 repository: Arc<R>,
110}
111
112impl<R: SessionRepository> PtyReadUseCaseImpl<R> {
113 pub fn new(repository: Arc<R>) -> Self {
114 Self { repository }
115 }
116}
117
118impl<R: SessionRepository> PtyReadUseCase for PtyReadUseCaseImpl<R> {
119 fn execute(&self, input: PtyReadInput) -> Result<PtyReadOutput, SessionError> {
120 let session = self.repository.resolve(input.session_id.as_deref())?;
121 let session_guard = mutex_lock_or_recover(&session);
122
123 let max_bytes = if input.max_bytes == 0 {
124 4096
125 } else {
126 input.max_bytes
127 };
128 let mut buf = vec![0u8; max_bytes];
129
130 match session_guard.pty_try_read(&mut buf, 100) {
131 Ok(bytes_read) => {
132 buf.truncate(bytes_read);
133 let data = String::from_utf8_lossy(&buf).to_string();
134 Ok(PtyReadOutput {
135 session_id: session_guard.id.clone(),
136 data,
137 bytes_read,
138 })
139 }
140 Err(e) => Err(SessionError::Pty(PtyError::Read(e.to_string()))),
141 }
142 }
143}
144
145pub trait PtyWriteUseCase: Send + Sync {
146 fn execute(&self, input: PtyWriteInput) -> Result<PtyWriteOutput, SessionError>;
147}
148
149pub struct PtyWriteUseCaseImpl<R: SessionRepository> {
150 repository: Arc<R>,
151}
152
153impl<R: SessionRepository> PtyWriteUseCaseImpl<R> {
154 pub fn new(repository: Arc<R>) -> Self {
155 Self { repository }
156 }
157}
158
159impl<R: SessionRepository> PtyWriteUseCase for PtyWriteUseCaseImpl<R> {
160 fn execute(&self, input: PtyWriteInput) -> Result<PtyWriteOutput, SessionError> {
161 let session = self.repository.resolve(input.session_id.as_deref())?;
162 let session_guard = mutex_lock_or_recover(&session);
163
164 match session_guard.pty_write(input.data.as_bytes()) {
165 Ok(()) => Ok(PtyWriteOutput {
166 session_id: session_guard.id.clone(),
167 bytes_written: input.data.len(),
168 success: true,
169 }),
170 Err(e) => Err(SessionError::Pty(PtyError::Write(e.to_string()))),
171 }
172 }
173}
174
175pub trait HealthUseCase: Send + Sync {
176 fn execute(&self, input: HealthInput) -> Result<HealthOutput, SessionError>;
177}
178
179pub struct HealthUseCaseImpl<R: SessionRepository> {
180 repository: Arc<R>,
181 metrics: Arc<DaemonMetrics>,
182 start_time: Instant,
183 active_connections: Arc<AtomicUsize>,
184}
185
186impl<R: SessionRepository> HealthUseCaseImpl<R> {
187 pub fn new(
188 repository: Arc<R>,
189 metrics: Arc<DaemonMetrics>,
190 start_time: Instant,
191 active_connections: Arc<AtomicUsize>,
192 ) -> Self {
193 Self {
194 repository,
195 metrics,
196 start_time,
197 active_connections,
198 }
199 }
200}
201
202impl<R: SessionRepository> HealthUseCase for HealthUseCaseImpl<R> {
203 fn execute(&self, _input: HealthInput) -> Result<HealthOutput, SessionError> {
204 Ok(HealthOutput {
205 status: "healthy".to_string(),
206 pid: std::process::id(),
207 uptime_ms: self.start_time.elapsed().as_millis() as u64,
208 session_count: self.repository.session_count(),
209 version: env!("CARGO_PKG_VERSION").to_string(),
210 active_connections: self.active_connections.load(Ordering::Relaxed),
211 total_requests: self.metrics.requests(),
212 error_count: self.metrics.errors(),
213 })
214 }
215}
216
217pub trait MetricsUseCase: Send + Sync {
218 fn execute(&self, input: MetricsInput) -> Result<MetricsOutput, SessionError>;
219}
220
221pub struct MetricsUseCaseImpl<R: SessionRepository> {
222 repository: Arc<R>,
223 metrics: Arc<DaemonMetrics>,
224 start_time: Instant,
225 active_connections: Arc<AtomicUsize>,
226}
227
228impl<R: SessionRepository> MetricsUseCaseImpl<R> {
229 pub fn new(
230 repository: Arc<R>,
231 metrics: Arc<DaemonMetrics>,
232 start_time: Instant,
233 active_connections: Arc<AtomicUsize>,
234 ) -> Self {
235 Self {
236 repository,
237 metrics,
238 start_time,
239 active_connections,
240 }
241 }
242}
243
244impl<R: SessionRepository> MetricsUseCase for MetricsUseCaseImpl<R> {
245 fn execute(&self, _input: MetricsInput) -> Result<MetricsOutput, SessionError> {
246 Ok(MetricsOutput {
247 requests_total: self.metrics.requests(),
248 errors_total: self.metrics.errors(),
249 lock_timeouts: self.metrics.lock_timeouts(),
250 poison_recoveries: self.metrics.poison_recoveries(),
251 uptime_ms: self.start_time.elapsed().as_millis() as u64,
252 active_connections: self.active_connections.load(Ordering::Relaxed),
253 session_count: self.repository.session_count(),
254 })
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 use crate::daemon::domain::SessionId;
263 use crate::daemon::test_support::{MockError, MockSessionRepository};
264
265 #[test]
266 fn test_health_usecase_returns_correct_output() {
267 let repo = Arc::new(
268 MockSessionRepository::builder()
269 .with_session_count(5)
270 .build(),
271 );
272 let metrics = Arc::new(DaemonMetrics::new());
273 metrics.record_request();
274 metrics.record_request();
275 metrics.record_error();
276
277 let active_connections = Arc::new(AtomicUsize::new(3));
278 let start_time = Instant::now();
279
280 let usecase = HealthUseCaseImpl::new(repo, metrics, start_time, active_connections);
281
282 let output = usecase.execute(HealthInput).unwrap();
283
284 assert_eq!(output.status, "healthy");
285 assert_eq!(output.session_count, 5);
286 assert_eq!(output.active_connections, 3);
287 assert_eq!(output.total_requests, 2);
288 assert_eq!(output.error_count, 1);
289 assert!(!output.version.is_empty());
290 }
291
292 #[test]
293 fn test_metrics_usecase_returns_correct_output() {
294 let repo = Arc::new(
295 MockSessionRepository::builder()
296 .with_session_count(2)
297 .build(),
298 );
299 let metrics = Arc::new(DaemonMetrics::new());
300 metrics.record_request();
301 metrics.record_lock_timeout();
302 metrics.record_poison_recovery();
303
304 let active_connections = Arc::new(AtomicUsize::new(1));
305 let start_time = Instant::now();
306
307 let usecase = MetricsUseCaseImpl::new(repo, metrics, start_time, active_connections);
308
309 let output = usecase.execute(MetricsInput).unwrap();
310
311 assert_eq!(output.requests_total, 1);
312 assert_eq!(output.errors_total, 0);
313 assert_eq!(output.lock_timeouts, 1);
314 assert_eq!(output.poison_recoveries, 1);
315 assert_eq!(output.active_connections, 1);
316 assert_eq!(output.session_count, 2);
317 }
318
319 #[test]
324 fn test_trace_usecase_returns_error_when_no_active_session() {
325 let repo = Arc::new(MockSessionRepository::new());
326 let usecase = TraceUseCaseImpl::new(repo);
327
328 let input = TraceInput {
329 session_id: None,
330 start: false,
331 stop: false,
332 count: 100,
333 };
334
335 let result = usecase.execute(input);
336 assert!(matches!(result, Err(SessionError::NoActiveSession)));
337 }
338
339 #[test]
340 fn test_trace_usecase_returns_error_when_session_not_found() {
341 let repo = Arc::new(
342 MockSessionRepository::builder()
343 .with_resolve_error(MockError::NotFound("missing".to_string()))
344 .build(),
345 );
346 let usecase = TraceUseCaseImpl::new(repo);
347
348 let input = TraceInput {
349 session_id: Some(SessionId::new("missing")),
350 start: true,
351 stop: false,
352 count: 50,
353 };
354
355 let result = usecase.execute(input);
356 assert!(matches!(result, Err(SessionError::NotFound(_))));
357 }
358
359 #[test]
364 fn test_console_usecase_returns_error_when_no_active_session() {
365 let repo = Arc::new(MockSessionRepository::new());
366 let usecase = ConsoleUseCaseImpl::new(repo);
367
368 let input = ConsoleInput {
369 session_id: None,
370 count: 100,
371 clear: false,
372 };
373
374 let result = usecase.execute(input);
375 assert!(matches!(result, Err(SessionError::NoActiveSession)));
376 }
377
378 #[test]
379 fn test_console_usecase_returns_error_when_session_not_found() {
380 let repo = Arc::new(
381 MockSessionRepository::builder()
382 .with_resolve_error(MockError::NotFound("missing".to_string()))
383 .build(),
384 );
385 let usecase = ConsoleUseCaseImpl::new(repo);
386
387 let input = ConsoleInput {
388 session_id: Some(SessionId::new("missing")),
389 count: 50,
390 clear: true,
391 };
392
393 let result = usecase.execute(input);
394 assert!(matches!(result, Err(SessionError::NotFound(_))));
395 }
396
397 #[test]
402 fn test_errors_usecase_returns_error_when_no_active_session() {
403 let repo = Arc::new(MockSessionRepository::new());
404 let usecase = ErrorsUseCaseImpl::new(repo);
405
406 let input = ErrorsInput {
407 session_id: None,
408 count: 100,
409 clear: false,
410 };
411
412 let result = usecase.execute(input);
413 assert!(matches!(result, Err(SessionError::NoActiveSession)));
414 }
415
416 #[test]
417 fn test_errors_usecase_returns_error_when_session_not_found() {
418 let repo = Arc::new(
419 MockSessionRepository::builder()
420 .with_resolve_error(MockError::NotFound("missing".to_string()))
421 .build(),
422 );
423 let usecase = ErrorsUseCaseImpl::new(repo);
424
425 let input = ErrorsInput {
426 session_id: Some(SessionId::new("missing")),
427 count: 50,
428 clear: false,
429 };
430
431 let result = usecase.execute(input);
432 assert!(matches!(result, Err(SessionError::NotFound(_))));
433 }
434
435 #[test]
440 fn test_pty_read_usecase_returns_error_when_no_active_session() {
441 let repo = Arc::new(MockSessionRepository::new());
442 let usecase = PtyReadUseCaseImpl::new(repo);
443
444 let input = PtyReadInput {
445 session_id: None,
446 max_bytes: 4096,
447 };
448
449 let result = usecase.execute(input);
450 assert!(matches!(result, Err(SessionError::NoActiveSession)));
451 }
452
453 #[test]
454 fn test_pty_read_usecase_returns_error_when_session_not_found() {
455 let repo = Arc::new(
456 MockSessionRepository::builder()
457 .with_resolve_error(MockError::NotFound("missing".to_string()))
458 .build(),
459 );
460 let usecase = PtyReadUseCaseImpl::new(repo);
461
462 let input = PtyReadInput {
463 session_id: Some(SessionId::new("missing")),
464 max_bytes: 1024,
465 };
466
467 let result = usecase.execute(input);
468 assert!(matches!(result, Err(SessionError::NotFound(_))));
469 }
470
471 #[test]
476 fn test_pty_write_usecase_returns_error_when_no_active_session() {
477 let repo = Arc::new(MockSessionRepository::new());
478 let usecase = PtyWriteUseCaseImpl::new(repo);
479
480 let input = PtyWriteInput {
481 session_id: None,
482 data: "hello".to_string(),
483 };
484
485 let result = usecase.execute(input);
486 assert!(matches!(result, Err(SessionError::NoActiveSession)));
487 }
488
489 #[test]
490 fn test_pty_write_usecase_returns_error_when_session_not_found() {
491 let repo = Arc::new(
492 MockSessionRepository::builder()
493 .with_resolve_error(MockError::NotFound("missing".to_string()))
494 .build(),
495 );
496 let usecase = PtyWriteUseCaseImpl::new(repo);
497
498 let input = PtyWriteInput {
499 session_id: Some(SessionId::new("missing")),
500 data: "test data".to_string(),
501 };
502
503 let result = usecase.execute(input);
504 assert!(matches!(result, Err(SessionError::NotFound(_))));
505 }
506}