1use std::fmt;
9use std::time::Duration;
10
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13
14use crate::daemon::Daemon;
15use crate::platform::Platform;
16use crate::types::{DaemonId, DaemonStatus, Signal};
17
18#[derive(Debug, thiserror::Error)]
24pub enum PlatformError {
25 #[error("operation not supported on {platform}: {operation}")]
27 NotSupported {
28 platform: Platform,
30 operation: String,
32 },
33
34 #[error("spawn failed: {0}")]
36 SpawnFailed(String),
37
38 #[error("signal failed: {0}")]
40 SignalFailed(String),
41
42 #[error("status query failed: {0}")]
44 StatusFailed(String),
45
46 #[error("tracer attachment failed: {0}")]
48 TracerFailed(String),
49
50 #[error("daemon not found: {0}")]
52 NotFound(String),
53
54 #[error("invalid state: {0}")]
56 InvalidState(String),
57
58 #[error("operation timed out after {0:?}")]
60 Timeout(Duration),
61
62 #[error("I/O error: {0}")]
64 Io(#[from] std::io::Error),
65
66 #[error("configuration error: {0}")]
68 Config(String),
69
70 #[error("resource limit exceeded: {0}")]
72 ResourceLimit(String),
73
74 #[error("permission denied: {0}")]
76 PermissionDenied(String),
77}
78
79impl PlatformError {
80 #[must_use]
82 pub fn not_supported(platform: Platform, operation: impl Into<String>) -> Self {
83 Self::NotSupported {
84 platform,
85 operation: operation.into(),
86 }
87 }
88
89 #[must_use]
91 pub fn spawn_failed(msg: impl Into<String>) -> Self {
92 Self::SpawnFailed(msg.into())
93 }
94
95 #[must_use]
97 pub fn signal_failed(msg: impl Into<String>) -> Self {
98 Self::SignalFailed(msg.into())
99 }
100
101 #[must_use]
103 pub fn status_failed(msg: impl Into<String>) -> Self {
104 Self::StatusFailed(msg.into())
105 }
106
107 #[must_use]
109 pub fn tracer_failed(msg: impl Into<String>) -> Self {
110 Self::TracerFailed(msg.into())
111 }
112
113 #[must_use]
115 pub const fn is_not_supported(&self) -> bool {
116 matches!(self, Self::NotSupported { .. })
117 }
118
119 #[must_use]
121 pub const fn is_recoverable(&self) -> bool {
122 matches!(
123 self,
124 Self::Timeout(_) | Self::ResourceLimit(_) | Self::InvalidState(_)
125 )
126 }
127}
128
129pub type PlatformResult<T> = std::result::Result<T, PlatformError>;
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct DaemonHandle {
146 id: DaemonId,
148 platform: Platform,
150 handle_data: HandleData,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub enum HandleData {
157 Systemd {
159 unit_name: String,
161 },
162 Launchd {
164 label: String,
166 },
167 Container {
169 runtime: String,
171 container_id: String,
173 },
174 Pepita {
176 vm_id: String,
178 vsock_cid: u32,
180 },
181 Wos {
183 pid: u32,
185 },
186 Native {
188 pid: u32,
190 },
191}
192
193impl DaemonHandle {
194 #[must_use]
196 pub fn systemd(id: DaemonId, unit_name: impl Into<String>) -> Self {
197 Self {
198 id,
199 platform: Platform::Linux,
200 handle_data: HandleData::Systemd {
201 unit_name: unit_name.into(),
202 },
203 }
204 }
205
206 #[must_use]
208 pub fn launchd(id: DaemonId, label: impl Into<String>) -> Self {
209 Self {
210 id,
211 platform: Platform::MacOS,
212 handle_data: HandleData::Launchd {
213 label: label.into(),
214 },
215 }
216 }
217
218 #[must_use]
220 pub fn container(
221 id: DaemonId,
222 runtime: impl Into<String>,
223 container_id: impl Into<String>,
224 ) -> Self {
225 Self {
226 id,
227 platform: Platform::Container,
228 handle_data: HandleData::Container {
229 runtime: runtime.into(),
230 container_id: container_id.into(),
231 },
232 }
233 }
234
235 #[must_use]
237 pub fn pepita(id: DaemonId, vm_id: impl Into<String>, vsock_cid: u32) -> Self {
238 Self {
239 id,
240 platform: Platform::PepitaMicroVM,
241 handle_data: HandleData::Pepita {
242 vm_id: vm_id.into(),
243 vsock_cid,
244 },
245 }
246 }
247
248 #[must_use]
250 pub fn wos(id: DaemonId, pid: u32) -> Self {
251 Self {
252 id,
253 platform: Platform::Wos,
254 handle_data: HandleData::Wos { pid },
255 }
256 }
257
258 #[must_use]
260 pub fn native(id: DaemonId, pid: u32) -> Self {
261 Self {
262 id,
263 platform: Platform::Native,
264 handle_data: HandleData::Native { pid },
265 }
266 }
267
268 #[must_use]
270 pub const fn id(&self) -> DaemonId {
271 self.id
272 }
273
274 #[must_use]
276 pub const fn platform(&self) -> Platform {
277 self.platform
278 }
279
280 #[must_use]
282 pub const fn handle_data(&self) -> &HandleData {
283 &self.handle_data
284 }
285
286 #[must_use]
288 pub fn systemd_unit(&self) -> Option<&str> {
289 match &self.handle_data {
290 HandleData::Systemd { unit_name } => Some(unit_name),
291 _ => None,
292 }
293 }
294
295 #[must_use]
297 pub fn launchd_label(&self) -> Option<&str> {
298 match &self.handle_data {
299 HandleData::Launchd { label } => Some(label),
300 _ => None,
301 }
302 }
303
304 #[must_use]
306 pub fn container_id(&self) -> Option<&str> {
307 match &self.handle_data {
308 HandleData::Container { container_id, .. } => Some(container_id),
309 _ => None,
310 }
311 }
312
313 #[must_use]
315 pub fn pid(&self) -> Option<u32> {
316 match &self.handle_data {
317 HandleData::Wos { pid } | HandleData::Native { pid } => Some(*pid),
318 _ => None,
319 }
320 }
321
322 #[must_use]
324 pub fn vsock_cid(&self) -> Option<u32> {
325 match &self.handle_data {
326 HandleData::Pepita { vsock_cid, .. } => Some(*vsock_cid),
327 _ => None,
328 }
329 }
330}
331
332impl fmt::Display for DaemonHandle {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 match &self.handle_data {
335 HandleData::Systemd { unit_name } => write!(f, "systemd:{}", unit_name),
336 HandleData::Launchd { label } => write!(f, "launchd:{}", label),
337 HandleData::Container {
338 runtime,
339 container_id,
340 } => {
341 write!(f, "{}:{}", runtime, container_id)
342 }
343 HandleData::Pepita { vm_id, vsock_cid } => {
344 write!(f, "pepita:{}@cid{}", vm_id, vsock_cid)
345 }
346 HandleData::Wos { pid } => write!(f, "wos:pid{}", pid),
347 HandleData::Native { pid } => write!(f, "native:pid{}", pid),
348 }
349 }
350}
351
352#[derive(Debug, Clone)]
361pub struct TracerHandle {
362 daemon_id: DaemonId,
364 tracer_type: TracerType,
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum TracerType {
371 Ptrace,
373 Ebpf,
375 RemoteVsock,
377 Simulated,
379}
380
381impl TracerHandle {
382 #[must_use]
384 pub fn ptrace(daemon_id: DaemonId) -> Self {
385 Self {
386 daemon_id,
387 tracer_type: TracerType::Ptrace,
388 }
389 }
390
391 #[must_use]
393 pub fn ebpf(daemon_id: DaemonId) -> Self {
394 Self {
395 daemon_id,
396 tracer_type: TracerType::Ebpf,
397 }
398 }
399
400 #[must_use]
402 pub fn remote_vsock(daemon_id: DaemonId) -> Self {
403 Self {
404 daemon_id,
405 tracer_type: TracerType::RemoteVsock,
406 }
407 }
408
409 #[must_use]
411 pub fn simulated(daemon_id: DaemonId) -> Self {
412 Self {
413 daemon_id,
414 tracer_type: TracerType::Simulated,
415 }
416 }
417
418 #[must_use]
420 pub const fn daemon_id(&self) -> DaemonId {
421 self.daemon_id
422 }
423
424 #[must_use]
426 pub const fn tracer_type(&self) -> TracerType {
427 self.tracer_type
428 }
429}
430
431#[async_trait]
444pub trait PlatformAdapter: Send + Sync {
445 fn platform(&self) -> Platform;
447
448 async fn spawn(&self, daemon: Box<dyn Daemon>) -> PlatformResult<DaemonHandle>;
453
454 async fn signal(&self, handle: &DaemonHandle, sig: Signal) -> PlatformResult<()>;
459
460 async fn status(&self, handle: &DaemonHandle) -> PlatformResult<DaemonStatus>;
465
466 async fn attach_tracer(&self, handle: &DaemonHandle) -> PlatformResult<TracerHandle>;
471
472 async fn stop(&self, handle: &DaemonHandle, timeout: Duration) -> PlatformResult<()> {
479 self.signal(handle, Signal::Term).await?;
480
481 let start = std::time::Instant::now();
482 while start.elapsed() < timeout {
483 if let Ok(status) = self.status(handle).await
484 && status.is_terminal()
485 {
486 return Ok(());
487 }
488 tokio::time::sleep(Duration::from_millis(100)).await;
489 }
490
491 Err(PlatformError::Timeout(timeout))
492 }
493
494 async fn kill(&self, handle: &DaemonHandle) -> PlatformResult<()> {
501 self.signal(handle, Signal::Kill).await
502 }
503
504 async fn pause(&self, handle: &DaemonHandle) -> PlatformResult<()> {
511 self.signal(handle, Signal::Stop).await
512 }
513
514 async fn resume(&self, handle: &DaemonHandle) -> PlatformResult<()> {
521 self.signal(handle, Signal::Cont).await
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
534 fn test_platform_error_not_supported() {
535 let err = PlatformError::not_supported(Platform::MacOS, "cgroups");
536 assert!(err.is_not_supported());
537 assert!(!err.is_recoverable());
538 assert!(err.to_string().contains("macos"));
539 assert!(err.to_string().contains("cgroups"));
540 }
541
542 #[test]
543 fn test_platform_error_spawn_failed() {
544 let err = PlatformError::spawn_failed("binary not found");
545 assert!(!err.is_not_supported());
546 assert!(err.to_string().contains("spawn"));
547 assert!(err.to_string().contains("binary not found"));
548 }
549
550 #[test]
551 fn test_platform_error_timeout_recoverable() {
552 let err = PlatformError::Timeout(Duration::from_secs(30));
553 assert!(err.is_recoverable());
554 }
555
556 #[test]
557 fn test_platform_error_resource_limit_recoverable() {
558 let err = PlatformError::ResourceLimit("memory".into());
559 assert!(err.is_recoverable());
560 }
561
562 #[test]
567 fn test_daemon_handle_systemd() {
568 let id = DaemonId::new();
569 let handle = DaemonHandle::systemd(id, "test.service");
570
571 assert_eq!(handle.id(), id);
572 assert_eq!(handle.platform(), Platform::Linux);
573 assert_eq!(handle.systemd_unit(), Some("test.service"));
574 assert_eq!(handle.launchd_label(), None);
575 assert!(handle.to_string().contains("systemd:test.service"));
576 }
577
578 #[test]
579 fn test_daemon_handle_launchd() {
580 let id = DaemonId::new();
581 let handle = DaemonHandle::launchd(id, "com.example.daemon");
582
583 assert_eq!(handle.platform(), Platform::MacOS);
584 assert_eq!(handle.launchd_label(), Some("com.example.daemon"));
585 assert!(handle.to_string().contains("launchd:com.example.daemon"));
586 }
587
588 #[test]
589 fn test_daemon_handle_container() {
590 let id = DaemonId::new();
591 let handle = DaemonHandle::container(id, "docker", "abc123");
592
593 assert_eq!(handle.platform(), Platform::Container);
594 assert_eq!(handle.container_id(), Some("abc123"));
595 assert!(handle.to_string().contains("docker:abc123"));
596 }
597
598 #[test]
599 fn test_daemon_handle_pepita() {
600 let id = DaemonId::new();
601 let handle = DaemonHandle::pepita(id, "vm-1234", 3);
602
603 assert_eq!(handle.platform(), Platform::PepitaMicroVM);
604 assert_eq!(handle.vsock_cid(), Some(3));
605 assert!(handle.to_string().contains("pepita"));
606 assert!(handle.to_string().contains("cid3"));
607 }
608
609 #[test]
610 fn test_daemon_handle_wos() {
611 let id = DaemonId::new();
612 let handle = DaemonHandle::wos(id, 42);
613
614 assert_eq!(handle.platform(), Platform::Wos);
615 assert_eq!(handle.pid(), Some(42));
616 assert!(handle.to_string().contains("wos:pid42"));
617 }
618
619 #[test]
620 fn test_daemon_handle_native() {
621 let id = DaemonId::new();
622 let handle = DaemonHandle::native(id, 12345);
623
624 assert_eq!(handle.platform(), Platform::Native);
625 assert_eq!(handle.pid(), Some(12345));
626 assert!(handle.to_string().contains("native:pid12345"));
627 }
628
629 #[test]
630 fn test_daemon_handle_serialize_roundtrip() {
631 let id = DaemonId::new();
632 let handle = DaemonHandle::systemd(id, "test.service");
633
634 let json = serde_json::to_string(&handle).unwrap();
635 let deserialized: DaemonHandle = serde_json::from_str(&json).unwrap();
636
637 assert_eq!(deserialized.id(), id);
638 assert_eq!(deserialized.platform(), Platform::Linux);
639 assert_eq!(deserialized.systemd_unit(), Some("test.service"));
640 }
641
642 #[test]
647 fn test_tracer_handle_ptrace() {
648 let id = DaemonId::new();
649 let tracer = TracerHandle::ptrace(id);
650
651 assert_eq!(tracer.daemon_id(), id);
652 assert_eq!(tracer.tracer_type(), TracerType::Ptrace);
653 }
654
655 #[test]
656 fn test_tracer_handle_ebpf() {
657 let id = DaemonId::new();
658 let tracer = TracerHandle::ebpf(id);
659
660 assert_eq!(tracer.tracer_type(), TracerType::Ebpf);
661 }
662
663 #[test]
664 fn test_tracer_handle_remote_vsock() {
665 let id = DaemonId::new();
666 let tracer = TracerHandle::remote_vsock(id);
667
668 assert_eq!(tracer.tracer_type(), TracerType::RemoteVsock);
669 }
670
671 #[test]
672 fn test_tracer_handle_simulated() {
673 let id = DaemonId::new();
674 let tracer = TracerHandle::simulated(id);
675
676 assert_eq!(tracer.tracer_type(), TracerType::Simulated);
677 }
678
679 #[test]
680 fn test_tracer_type_equality() {
681 assert_eq!(TracerType::Ptrace, TracerType::Ptrace);
682 assert_ne!(TracerType::Ptrace, TracerType::Ebpf);
683 }
684
685 #[test]
690 fn test_handle_data_all_variants() {
691 let variants = vec![
693 HandleData::Systemd {
694 unit_name: "test".into(),
695 },
696 HandleData::Launchd {
697 label: "test".into(),
698 },
699 HandleData::Container {
700 runtime: "docker".into(),
701 container_id: "abc".into(),
702 },
703 HandleData::Pepita {
704 vm_id: "vm".into(),
705 vsock_cid: 1,
706 },
707 HandleData::Wos { pid: 1 },
708 HandleData::Native { pid: 1 },
709 ];
710
711 for variant in variants {
712 let _ = format!("{:?}", variant);
714 }
715 }
716}