duende_core/
adapter.rs

1//! Platform adapter abstraction for daemon lifecycle management.
2//!
3//! # Toyota Way: Standardized Work (標準作業)
4//! Every platform adapter follows the same contract, enabling
5//! predictable daemon behavior across Linux, macOS, containers,
6//! pepita microVMs, and WOS.
7
8use 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// =============================================================================
19// PlatformError
20// =============================================================================
21
22/// Error type for platform adapter operations.
23#[derive(Debug, thiserror::Error)]
24pub enum PlatformError {
25    /// Platform operation not supported.
26    #[error("operation not supported on {platform}: {operation}")]
27    NotSupported {
28        /// The platform.
29        platform: Platform,
30        /// The operation that was attempted.
31        operation: String,
32    },
33
34    /// Failed to spawn daemon.
35    #[error("spawn failed: {0}")]
36    SpawnFailed(String),
37
38    /// Failed to signal daemon.
39    #[error("signal failed: {0}")]
40    SignalFailed(String),
41
42    /// Failed to query status.
43    #[error("status query failed: {0}")]
44    StatusFailed(String),
45
46    /// Failed to attach tracer.
47    #[error("tracer attachment failed: {0}")]
48    TracerFailed(String),
49
50    /// Daemon not found.
51    #[error("daemon not found: {0}")]
52    NotFound(String),
53
54    /// Invalid state for operation.
55    #[error("invalid state: {0}")]
56    InvalidState(String),
57
58    /// Timeout during operation.
59    #[error("operation timed out after {0:?}")]
60    Timeout(Duration),
61
62    /// I/O error.
63    #[error("I/O error: {0}")]
64    Io(#[from] std::io::Error),
65
66    /// Configuration error.
67    #[error("configuration error: {0}")]
68    Config(String),
69
70    /// Resource limit exceeded.
71    #[error("resource limit exceeded: {0}")]
72    ResourceLimit(String),
73
74    /// Permission denied.
75    #[error("permission denied: {0}")]
76    PermissionDenied(String),
77}
78
79impl PlatformError {
80    /// Creates a "not supported" error.
81    #[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    /// Creates a spawn failed error.
90    #[must_use]
91    pub fn spawn_failed(msg: impl Into<String>) -> Self {
92        Self::SpawnFailed(msg.into())
93    }
94
95    /// Creates a signal failed error.
96    #[must_use]
97    pub fn signal_failed(msg: impl Into<String>) -> Self {
98        Self::SignalFailed(msg.into())
99    }
100
101    /// Creates a status query failed error.
102    #[must_use]
103    pub fn status_failed(msg: impl Into<String>) -> Self {
104        Self::StatusFailed(msg.into())
105    }
106
107    /// Creates a tracer attachment failed error.
108    #[must_use]
109    pub fn tracer_failed(msg: impl Into<String>) -> Self {
110        Self::TracerFailed(msg.into())
111    }
112
113    /// Returns true if this error indicates the operation is not supported.
114    #[must_use]
115    pub const fn is_not_supported(&self) -> bool {
116        matches!(self, Self::NotSupported { .. })
117    }
118
119    /// Returns true if this error is recoverable.
120    #[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
129/// Result type for platform operations.
130pub type PlatformResult<T> = std::result::Result<T, PlatformError>;
131
132// =============================================================================
133// DaemonHandle
134// =============================================================================
135
136/// Handle to a running daemon instance.
137///
138/// The handle type varies by platform:
139/// - Linux/systemd: Unit name
140/// - macOS/launchd: Service label
141/// - Container: Container ID
142/// - pepita: VM ID + vsock transport
143/// - WOS: Process ID
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct DaemonHandle {
146    /// Daemon ID.
147    id: DaemonId,
148    /// Platform this daemon is running on.
149    platform: Platform,
150    /// Platform-specific handle data.
151    handle_data: HandleData,
152}
153
154/// Platform-specific handle data.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub enum HandleData {
157    /// Linux systemd unit name.
158    Systemd {
159        /// Unit name (e.g., "my-daemon.service").
160        unit_name: String,
161    },
162    /// macOS launchd service label.
163    Launchd {
164        /// Service label (e.g., "com.example.my-daemon").
165        label: String,
166    },
167    /// Container ID.
168    Container {
169        /// Container runtime (docker, podman, containerd).
170        runtime: String,
171        /// Container ID.
172        container_id: String,
173    },
174    /// pepita MicroVM.
175    Pepita {
176        /// VM ID.
177        vm_id: String,
178        /// vsock CID.
179        vsock_cid: u32,
180    },
181    /// WOS process.
182    Wos {
183        /// Process ID.
184        pid: u32,
185    },
186    /// Native process.
187    Native {
188        /// Process ID.
189        pid: u32,
190    },
191}
192
193impl DaemonHandle {
194    /// Creates a systemd handle.
195    #[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    /// Creates a launchd handle.
207    #[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    /// Creates a container handle.
219    #[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    /// Creates a pepita handle.
236    #[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    /// Creates a WOS handle.
249    #[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    /// Creates a native process handle.
259    #[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    /// Returns the daemon ID.
269    #[must_use]
270    pub const fn id(&self) -> DaemonId {
271        self.id
272    }
273
274    /// Returns the platform.
275    #[must_use]
276    pub const fn platform(&self) -> Platform {
277        self.platform
278    }
279
280    /// Returns the handle data.
281    #[must_use]
282    pub const fn handle_data(&self) -> &HandleData {
283        &self.handle_data
284    }
285
286    /// Returns the systemd unit name, if applicable.
287    #[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    /// Returns the launchd label, if applicable.
296    #[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    /// Returns the container ID, if applicable.
305    #[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    /// Returns the process ID, if applicable.
314    #[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    /// Returns the vsock CID, if applicable (pepita).
323    #[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// =============================================================================
353// TracerHandle
354// =============================================================================
355
356/// Handle to an attached tracer (renacer integration).
357///
358/// # Toyota Way: Genchi Genbutsu (現地現物)
359/// Direct observation of daemon behavior via syscall tracing.
360#[derive(Debug, Clone)]
361pub struct TracerHandle {
362    /// Daemon being traced.
363    daemon_id: DaemonId,
364    /// Tracer type.
365    tracer_type: TracerType,
366}
367
368/// Type of tracer attachment.
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum TracerType {
371    /// Local ptrace-based tracer.
372    Ptrace,
373    /// eBPF-based tracer.
374    Ebpf,
375    /// Remote tracer via vsock (pepita).
376    RemoteVsock,
377    /// Simulated tracer (WOS).
378    Simulated,
379}
380
381impl TracerHandle {
382    /// Creates a ptrace-based tracer handle.
383    #[must_use]
384    pub fn ptrace(daemon_id: DaemonId) -> Self {
385        Self {
386            daemon_id,
387            tracer_type: TracerType::Ptrace,
388        }
389    }
390
391    /// Creates an eBPF-based tracer handle.
392    #[must_use]
393    pub fn ebpf(daemon_id: DaemonId) -> Self {
394        Self {
395            daemon_id,
396            tracer_type: TracerType::Ebpf,
397        }
398    }
399
400    /// Creates a remote vsock-based tracer handle.
401    #[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    /// Creates a simulated tracer handle.
410    #[must_use]
411    pub fn simulated(daemon_id: DaemonId) -> Self {
412        Self {
413            daemon_id,
414            tracer_type: TracerType::Simulated,
415        }
416    }
417
418    /// Returns the daemon ID being traced.
419    #[must_use]
420    pub const fn daemon_id(&self) -> DaemonId {
421        self.daemon_id
422    }
423
424    /// Returns the tracer type.
425    #[must_use]
426    pub const fn tracer_type(&self) -> TracerType {
427        self.tracer_type
428    }
429}
430
431// =============================================================================
432// PlatformAdapter trait
433// =============================================================================
434
435/// Platform-specific daemon adapter.
436///
437/// # Toyota Way: Standardized Work
438/// Every platform implements the same lifecycle contract:
439/// - spawn: Create and start daemon
440/// - signal: Send signal to daemon
441/// - status: Query daemon status
442/// - attach_tracer: Attach renacer tracer
443#[async_trait]
444pub trait PlatformAdapter: Send + Sync {
445    /// Returns the platform identifier.
446    fn platform(&self) -> Platform;
447
448    /// Spawns a daemon on this platform.
449    ///
450    /// # Errors
451    /// Returns an error if the daemon cannot be spawned.
452    async fn spawn(&self, daemon: Box<dyn Daemon>) -> PlatformResult<DaemonHandle>;
453
454    /// Sends a signal to a daemon.
455    ///
456    /// # Errors
457    /// Returns an error if the signal cannot be delivered.
458    async fn signal(&self, handle: &DaemonHandle, sig: Signal) -> PlatformResult<()>;
459
460    /// Queries daemon status.
461    ///
462    /// # Errors
463    /// Returns an error if the status cannot be determined.
464    async fn status(&self, handle: &DaemonHandle) -> PlatformResult<DaemonStatus>;
465
466    /// Attaches a tracer to a running daemon.
467    ///
468    /// # Errors
469    /// Returns an error if the tracer cannot be attached.
470    async fn attach_tracer(&self, handle: &DaemonHandle) -> PlatformResult<TracerHandle>;
471
472    /// Gracefully stops a daemon.
473    ///
474    /// Sends SIGTERM and waits for termination up to the timeout.
475    ///
476    /// # Errors
477    /// Returns an error if the daemon cannot be stopped.
478    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    /// Forcefully kills a daemon.
495    ///
496    /// Sends SIGKILL immediately.
497    ///
498    /// # Errors
499    /// Returns an error if the daemon cannot be killed.
500    async fn kill(&self, handle: &DaemonHandle) -> PlatformResult<()> {
501        self.signal(handle, Signal::Kill).await
502    }
503
504    /// Pauses a daemon.
505    ///
506    /// Sends SIGSTOP.
507    ///
508    /// # Errors
509    /// Returns an error if the daemon cannot be paused.
510    async fn pause(&self, handle: &DaemonHandle) -> PlatformResult<()> {
511        self.signal(handle, Signal::Stop).await
512    }
513
514    /// Resumes a paused daemon.
515    ///
516    /// Sends SIGCONT.
517    ///
518    /// # Errors
519    /// Returns an error if the daemon cannot be resumed.
520    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    // =========================================================================
530    // PlatformError tests
531    // =========================================================================
532
533    #[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    // =========================================================================
563    // DaemonHandle tests
564    // =========================================================================
565
566    #[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    // =========================================================================
643    // TracerHandle tests
644    // =========================================================================
645
646    #[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    // =========================================================================
686    // HandleData tests
687    // =========================================================================
688
689    #[test]
690    fn test_handle_data_all_variants() {
691        // Test that all variants can be created and matched
692        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            // Verify each variant can be debug-printed
713            let _ = format!("{:?}", variant);
714        }
715    }
716}