duende_core/
platform.rs

1//! Platform detection and abstraction.
2//!
3//! # Toyota Way: Poka-Yoke (ポカヨケ)
4//! Fail to safest option during platform detection.
5//!
6//! # Detection Order
7//! 1. WOS: Check for WASM runtime markers
8//! 2. pepita: Check for virtio devices
9//! 3. Container: Check for /.dockerenv or cgroup markers
10//! 4. Linux: Check for systemd
11//! 5. macOS: Check for launchd
12//! 6. Fallback: Native process
13
14use serde::{Deserialize, Serialize};
15use std::path::Path;
16
17/// Supported platforms for daemon execution.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum Platform {
20    /// Linux with systemd.
21    Linux,
22    /// macOS with launchd.
23    MacOS,
24    /// Docker/OCI container.
25    Container,
26    /// pepita MicroVM.
27    PepitaMicroVM,
28    /// WOS (WebAssembly Operating System).
29    Wos,
30    /// Native process (fallback).
31    Native,
32}
33
34impl Platform {
35    /// Returns true if this platform supports process isolation.
36    #[must_use]
37    pub const fn supports_isolation(&self) -> bool {
38        matches!(self, Self::Container | Self::PepitaMicroVM | Self::Wos)
39    }
40
41    /// Returns true if this platform supports resource limits via cgroups.
42    #[must_use]
43    pub const fn supports_cgroups(&self) -> bool {
44        matches!(self, Self::Linux | Self::Container)
45    }
46
47    /// Returns true if this platform supports systemd-style unit management.
48    #[must_use]
49    pub const fn supports_systemd(&self) -> bool {
50        matches!(self, Self::Linux)
51    }
52
53    /// Returns true if this platform supports launchd-style plist management.
54    #[must_use]
55    pub const fn supports_launchd(&self) -> bool {
56        matches!(self, Self::MacOS)
57    }
58
59    /// Returns the platform name as a static string.
60    #[must_use]
61    pub const fn name(&self) -> &'static str {
62        match self {
63            Self::Linux => "linux",
64            Self::MacOS => "macos",
65            Self::Container => "container",
66            Self::PepitaMicroVM => "pepita",
67            Self::Wos => "wos",
68            Self::Native => "native",
69        }
70    }
71}
72
73impl std::fmt::Display for Platform {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}", self.name())
76    }
77}
78
79/// Auto-detect current platform with fallback chain.
80///
81/// # Detection Order (Poka-Yoke: fail to safest option)
82/// 1. WOS: Check for WASM runtime markers
83/// 2. pepita: Check for virtio devices
84/// 3. Container: Check for /.dockerenv or cgroup markers
85/// 4. Linux: Check for systemd
86/// 5. macOS: Check for launchd
87/// 6. Fallback: Native process
88#[must_use]
89pub fn detect_platform() -> Platform {
90    // 1. WOS detection (WASM target or env var)
91    if cfg!(target_arch = "wasm32") || std::env::var("WOS_KERNEL").is_ok() {
92        return Platform::Wos;
93    }
94
95    // 2. pepita MicroVM detection
96    if is_pepita_vm() {
97        return Platform::PepitaMicroVM;
98    }
99
100    // 3. Container detection
101    if is_container() {
102        return Platform::Container;
103    }
104
105    // 4. Linux with systemd
106    #[cfg(target_os = "linux")]
107    if is_systemd_available() {
108        return Platform::Linux;
109    }
110
111    // 5. macOS with launchd
112    #[cfg(target_os = "macos")]
113    {
114        return Platform::MacOS;
115    }
116
117    // 6. Fallback to native
118    Platform::Native
119}
120
121/// Check if running inside a pepita MicroVM.
122fn is_pepita_vm() -> bool {
123    // Check for pepita-specific markers
124    if std::env::var("PEPITA_VM").is_ok() {
125        return true;
126    }
127
128    // Check for virtio-ports device (pepita uses vsock)
129    Path::new("/dev/virtio-ports").exists()
130}
131
132/// Check if running inside a container.
133fn is_container() -> bool {
134    // Docker marker file
135    if Path::new("/.dockerenv").exists() {
136        return true;
137    }
138
139    // Check cgroup for container indicators
140    if let Ok(cgroup) = std::fs::read_to_string("/proc/1/cgroup")
141        && (cgroup.contains("docker")
142            || cgroup.contains("containerd")
143            || cgroup.contains("kubepods")
144            || cgroup.contains("lxc"))
145    {
146        return true;
147    }
148
149    // Check for container runtime env vars
150    if std::env::var("KUBERNETES_SERVICE_HOST").is_ok() {
151        return true;
152    }
153
154    false
155}
156
157/// Check if systemd is available on Linux.
158#[cfg(target_os = "linux")]
159fn is_systemd_available() -> bool {
160    Path::new("/run/systemd/system").exists()
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    // =========================================================================
168    // Platform enum tests
169    // =========================================================================
170
171    #[test]
172    fn test_platform_display() {
173        assert_eq!(Platform::Linux.to_string(), "linux");
174        assert_eq!(Platform::MacOS.to_string(), "macos");
175        assert_eq!(Platform::Container.to_string(), "container");
176        assert_eq!(Platform::PepitaMicroVM.to_string(), "pepita");
177        assert_eq!(Platform::Wos.to_string(), "wos");
178        assert_eq!(Platform::Native.to_string(), "native");
179    }
180
181    #[test]
182    fn test_platform_name() {
183        assert_eq!(Platform::Linux.name(), "linux");
184        assert_eq!(Platform::MacOS.name(), "macos");
185        assert_eq!(Platform::Container.name(), "container");
186        assert_eq!(Platform::PepitaMicroVM.name(), "pepita");
187        assert_eq!(Platform::Wos.name(), "wos");
188        assert_eq!(Platform::Native.name(), "native");
189    }
190
191    #[test]
192    fn test_platform_supports_isolation() {
193        assert!(!Platform::Linux.supports_isolation());
194        assert!(!Platform::MacOS.supports_isolation());
195        assert!(Platform::Container.supports_isolation());
196        assert!(Platform::PepitaMicroVM.supports_isolation());
197        assert!(Platform::Wos.supports_isolation());
198        assert!(!Platform::Native.supports_isolation());
199    }
200
201    #[test]
202    fn test_platform_supports_cgroups() {
203        assert!(Platform::Linux.supports_cgroups());
204        assert!(!Platform::MacOS.supports_cgroups());
205        assert!(Platform::Container.supports_cgroups());
206        assert!(!Platform::PepitaMicroVM.supports_cgroups());
207        assert!(!Platform::Wos.supports_cgroups());
208        assert!(!Platform::Native.supports_cgroups());
209    }
210
211    #[test]
212    fn test_platform_supports_systemd() {
213        assert!(Platform::Linux.supports_systemd());
214        assert!(!Platform::MacOS.supports_systemd());
215        assert!(!Platform::Container.supports_systemd());
216        assert!(!Platform::PepitaMicroVM.supports_systemd());
217        assert!(!Platform::Wos.supports_systemd());
218        assert!(!Platform::Native.supports_systemd());
219    }
220
221    #[test]
222    fn test_platform_supports_launchd() {
223        assert!(!Platform::Linux.supports_launchd());
224        assert!(Platform::MacOS.supports_launchd());
225        assert!(!Platform::Container.supports_launchd());
226        assert!(!Platform::PepitaMicroVM.supports_launchd());
227        assert!(!Platform::Wos.supports_launchd());
228        assert!(!Platform::Native.supports_launchd());
229    }
230
231    #[test]
232    fn test_platform_equality() {
233        assert_eq!(Platform::Linux, Platform::Linux);
234        assert_ne!(Platform::Linux, Platform::MacOS);
235    }
236
237    #[test]
238    fn test_platform_clone() {
239        let p1 = Platform::Container;
240        let p2 = p1;
241        assert_eq!(p1, p2);
242    }
243
244    #[test]
245    fn test_platform_debug() {
246        let debug = format!("{:?}", Platform::Linux);
247        assert!(debug.contains("Linux"));
248    }
249
250    // =========================================================================
251    // Detection tests
252    // =========================================================================
253
254    #[test]
255    fn test_detect_platform_returns_valid() {
256        // detect_platform should always return a valid Platform
257        let platform = detect_platform();
258        // Verify it's one of the valid variants
259        let valid = matches!(
260            platform,
261            Platform::Linux
262                | Platform::MacOS
263                | Platform::Container
264                | Platform::PepitaMicroVM
265                | Platform::Wos
266                | Platform::Native
267        );
268        assert!(valid);
269    }
270
271    #[test]
272    fn test_is_container_false_on_host() {
273        // On most dev machines, this should be false
274        // (unless running tests in container)
275        let result = is_container();
276        // Just verify it doesn't panic
277        let _ = result;
278    }
279
280    #[test]
281    fn test_is_pepita_vm_false_on_host() {
282        // On most dev machines, this should be false
283        let result = is_pepita_vm();
284        // Just verify it doesn't panic
285        let _ = result;
286    }
287
288    // =========================================================================
289    // Serialization tests
290    // =========================================================================
291
292    #[test]
293    fn test_platform_serialize() {
294        let json = serde_json::to_string(&Platform::Linux).unwrap();
295        assert!(json.contains("Linux"));
296    }
297
298    #[test]
299    fn test_platform_deserialize() {
300        let platform: Platform = serde_json::from_str("\"Linux\"").unwrap();
301        assert_eq!(platform, Platform::Linux);
302    }
303
304    #[test]
305    fn test_platform_roundtrip() {
306        for platform in [
307            Platform::Linux,
308            Platform::MacOS,
309            Platform::Container,
310            Platform::PepitaMicroVM,
311            Platform::Wos,
312            Platform::Native,
313        ] {
314            let json = serde_json::to_string(&platform).unwrap();
315            let deserialized: Platform = serde_json::from_str(&json).unwrap();
316            assert_eq!(platform, deserialized);
317        }
318    }
319
320    // =========================================================================
321    // Hash tests (for HashMap usage)
322    // =========================================================================
323
324    #[test]
325    fn test_platform_hash() {
326        use std::collections::HashSet;
327
328        let mut set = HashSet::new();
329        set.insert(Platform::Linux);
330        set.insert(Platform::MacOS);
331        set.insert(Platform::Linux); // Duplicate
332
333        assert_eq!(set.len(), 2);
334        assert!(set.contains(&Platform::Linux));
335        assert!(set.contains(&Platform::MacOS));
336        assert!(!set.contains(&Platform::Container));
337    }
338}