Skip to main content

rusty_stack/platform/
control_shell.rs

1//! Windows-native control shell using shared Rust contracts.
2//!
3//! Provides a control surface for managing Rusty Stack operations on Windows
4//! without spawning `cmd.exe` or `powershell.exe`. All operations use the
5//! shared Rust core types and platform abstractions.
6//!
7//! # Design Principles
8//!
9//! - **No shell subprocess**: All operations execute through Rust code
10//!   using shared contracts from `core::` and `platform::` modules.
11//! - **Cross-platform**: Compiles on both Linux and Windows. Platform-specific
12//!   operations are cfg-gated.
13//! - **Composable**: Each operation returns structured results that can be
14//!   composed into higher-level workflows.
15//!
16//! # Operations
17//!
18//! - **Status check**: Report system and component status
19//! - **Path translation**: Bridge Windows ↔ WSL2 paths
20//! - **Service management**: List and check health of managed services
21//! - **WSL2 management**: Detect and check WSL2 health
22
23use crate::core::types::BackendMode;
24use crate::platform::path_bridge;
25use crate::platform::service::{self, ServiceConfig, ServiceRegistry};
26use crate::platform::wsl;
27use serde::{Deserialize, Serialize};
28
29// ===========================================================================
30// Control Shell Result Types
31// ===========================================================================
32
33/// Result of a control shell operation.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub struct ControlResult {
36    /// The operation that was performed.
37    pub operation: ControlOperation,
38    /// Whether the operation succeeded.
39    pub success: bool,
40    /// Human-readable message describing the result.
41    pub message: String,
42    /// Additional data as JSON-serializable value.
43    pub data: serde_json::Value,
44}
45
46/// Operations supported by the control shell.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48pub enum ControlOperation {
49    /// Check system status (platform, backend, WSL2).
50    StatusCheck,
51    /// Translate a path between Windows and WSL2 formats.
52    PathTranslate,
53    /// List registered services and their status.
54    ServiceList,
55    /// Check health of all registered services.
56    HealthCheck,
57    /// Detect WSL2 and run health checks.
58    WslCheck,
59}
60
61// ===========================================================================
62// Control Shell
63// ===========================================================================
64
65/// Windows-native control shell for Rusty Stack operations.
66///
67/// All operations use shared Rust contracts — no `cmd.exe` or `powershell.exe`
68/// subprocess is spawned.
69#[derive(Debug, Clone)]
70pub struct ControlShell {
71    /// Current backend mode.
72    backend_mode: BackendMode,
73    /// Service registry for managed services.
74    services: ServiceRegistry,
75}
76
77impl ControlShell {
78    /// Create a new control shell with the given backend mode.
79    pub fn new(backend_mode: BackendMode) -> Self {
80        Self {
81            backend_mode,
82            services: ServiceRegistry::new(),
83        }
84    }
85
86    /// Create a control shell with auto-detected backend mode.
87    pub fn auto() -> Self {
88        Self::new(crate::platform::windows::detect_backend_mode())
89    }
90
91    /// Register a service with the control shell.
92    pub fn register_service(&mut self, config: ServiceConfig) -> Result<(), service::ServiceError> {
93        self.services.register(config)
94    }
95
96    /// Get the current backend mode.
97    pub fn backend_mode(&self) -> BackendMode {
98        self.backend_mode
99    }
100
101    /// Get a reference to the service registry.
102    pub fn services(&self) -> &ServiceRegistry {
103        &self.services
104    }
105
106    // -----------------------------------------------------------------
107    // Operations (no shell subprocess)
108    // -----------------------------------------------------------------
109
110    /// Perform a system status check.
111    ///
112    /// Reports platform, backend mode, and WSL2 status using shared
113    /// Rust contracts. No shell subprocess is spawned.
114    pub fn status_check(&self) -> ControlResult {
115        let wsl_status = wsl::detect_wsl2();
116
117        let data = serde_json::json!({
118            "backend_mode": self.backend_mode,
119            "wsl2": wsl_status,
120        });
121
122        ControlResult {
123            operation: ControlOperation::StatusCheck,
124            success: true,
125            message: format!(
126                "Platform: {:?} | WSL2: {}",
127                self.backend_mode,
128                match &wsl_status {
129                    wsl::WslStatus::NotInstalled => "not installed".to_string(),
130                    wsl::WslStatus::Available { distro, .. } => format!("available ({})", distro),
131                    wsl::WslStatus::Error { message } => format!("error: {}", message),
132                }
133            ),
134            data,
135        }
136    }
137
138    /// Translate a path between Windows and WSL2 formats.
139    ///
140    /// Auto-detects direction based on path format.
141    pub fn translate_path(&self, path: &str) -> ControlResult {
142        let result = path_bridge::translate_path(path);
143
144        let data = serde_json::json!({
145            "original": path,
146            "translated": result.translated,
147            "direction": result.direction,
148        });
149
150        ControlResult {
151            operation: ControlOperation::PathTranslate,
152            success: true,
153            message: format!("{} → {}", path, result.translated),
154            data,
155        }
156    }
157
158    /// List all registered services.
159    ///
160    /// Returns service configurations without starting any processes.
161    pub fn list_services(&self) -> ControlResult {
162        let services: Vec<serde_json::Value> = self
163            .services
164            .all()
165            .iter()
166            .map(|s| {
167                serde_json::json!({
168                    "name": s.name,
169                    "port": s.port,
170                    "enabled": s.enabled,
171                    "address": s.socket_addr().to_string(),
172                    "health_url": s.health_url(),
173                })
174            })
175            .collect();
176
177        let count = services.len();
178        let data = serde_json::json!({ "services": services });
179
180        ControlResult {
181            operation: ControlOperation::ServiceList,
182            success: true,
183            message: format!("{} service(s) registered", count),
184            data,
185        }
186    }
187
188    /// Check health of all registered services.
189    ///
190    /// Returns health check results. Since we don't start actual HTTP servers
191    /// in this module, this returns the health URLs that should be checked.
192    pub fn check_health(&self) -> ControlResult {
193        let urls = self.services.health_urls();
194        let all_local = self.services.all_local_only();
195
196        let checks: Vec<serde_json::Value> = urls
197            .iter()
198            .map(|url| {
199                serde_json::json!({
200                    "url": url,
201                    "local_only": all_local,
202                })
203            })
204            .collect();
205
206        let data = serde_json::json!({
207            "checks": checks,
208            "all_local_only": all_local,
209        });
210
211        ControlResult {
212            operation: ControlOperation::HealthCheck,
213            success: all_local,
214            message: format!(
215                "{} health endpoint(s) — all local-only: {}",
216                checks.len(),
217                all_local
218            ),
219            data,
220        }
221    }
222
223    /// Detect WSL2 and run health checks.
224    ///
225    /// Uses shared Rust contracts for detection and health checking.
226    /// No shell subprocess is spawned for the detection itself.
227    pub fn wsl_check(&self) -> ControlResult {
228        let status = wsl::detect_wsl2();
229        let health = wsl::check_wsl_health(&status);
230
231        let all_healthy = health.iter().all(|h| h.success);
232
233        let data = serde_json::json!({
234            "status": status,
235            "health": health,
236        });
237
238        ControlResult {
239            operation: ControlOperation::WslCheck,
240            success: all_healthy,
241            message: match &status {
242                wsl::WslStatus::NotInstalled => "WSL2 not installed".to_string(),
243                wsl::WslStatus::Available { distro, .. } => {
244                    let healthy_count = health.iter().filter(|h| h.success).count();
245                    format!(
246                        "WSL2 available ({}) — {}/{} checks passed",
247                        distro,
248                        healthy_count,
249                        health.len()
250                    )
251                }
252                wsl::WslStatus::Error { message } => format!("WSL2 error: {}", message),
253            },
254            data,
255        }
256    }
257}
258
259// ===========================================================================
260// Tests
261// ===========================================================================
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    // ================================================================
268    // VAL-WIN-009: Windows shell uses shared Rust contracts
269    // ================================================================
270
271    #[test]
272    fn test_control_shell_status_check() {
273        let shell = ControlShell::auto();
274        let result = shell.status_check();
275        assert_eq!(result.operation, ControlOperation::StatusCheck);
276        assert!(result.success);
277        assert!(!result.message.is_empty());
278    }
279
280    #[test]
281    fn test_control_shell_status_check_has_backend_mode() {
282        let shell = ControlShell::auto();
283        let result = shell.status_check();
284        let data = &result.data;
285        assert!(data.get("backend_mode").is_some());
286    }
287
288    #[test]
289    fn test_control_shell_status_check_has_wsl_info() {
290        let shell = ControlShell::auto();
291        let result = shell.status_check();
292        let data = &result.data;
293        assert!(data.get("wsl2").is_some());
294    }
295
296    #[test]
297    fn test_control_shell_translate_path_windows() {
298        let shell = ControlShell::new(BackendMode::WindowsNative);
299        let result = shell.translate_path(r"C:\Users\alice");
300        assert_eq!(result.operation, ControlOperation::PathTranslate);
301        assert!(result.success);
302        assert!(result.message.contains("/mnt/c/"));
303    }
304
305    #[test]
306    fn test_control_shell_translate_path_linux() {
307        let shell = ControlShell::new(BackendMode::LinuxNative);
308        let result = shell.translate_path("/home/alice");
309        assert_eq!(result.operation, ControlOperation::PathTranslate);
310        assert!(result.success);
311        assert!(result.message.contains("wsl$"));
312    }
313
314    #[test]
315    fn test_control_shell_list_services_empty() {
316        let shell = ControlShell::auto();
317        let result = shell.list_services();
318        assert_eq!(result.operation, ControlOperation::ServiceList);
319        assert!(result.success);
320        assert!(result.message.contains("0 service"));
321    }
322
323    #[test]
324    fn test_control_shell_list_services_with_registered() {
325        let mut shell = ControlShell::auto();
326        shell
327            .register_service(ServiceConfig::new("api", 8080))
328            .unwrap();
329
330        let result = shell.list_services();
331        assert!(result.success);
332        assert!(result.message.contains("1 service"));
333    }
334
335    #[test]
336    fn test_control_shell_health_check_no_shell_subprocess() {
337        let mut shell = ControlShell::auto();
338        shell
339            .register_service(ServiceConfig::new("api", 8080))
340            .unwrap();
341
342        let result = shell.check_health();
343        assert_eq!(result.operation, ControlOperation::HealthCheck);
344        assert!(result.success);
345        // Verify all services are local-only (no 0.0.0.0)
346        let all_local = result
347            .data
348            .get("all_local_only")
349            .unwrap()
350            .as_bool()
351            .unwrap();
352        assert!(all_local);
353    }
354
355    #[test]
356    fn test_control_shell_wsl_check() {
357        let shell = ControlShell::auto();
358        let result = shell.wsl_check();
359        assert_eq!(result.operation, ControlOperation::WslCheck);
360        // Result depends on actual system state
361        // Just verify it returns without error
362        assert!(!result.message.is_empty());
363    }
364
365    #[test]
366    fn test_control_shell_backend_mode() {
367        let shell = ControlShell::new(BackendMode::WindowsNative);
368        assert_eq!(shell.backend_mode(), BackendMode::WindowsNative);
369    }
370
371    // ================================================================
372    // Serde Roundtrips
373    // ================================================================
374
375    #[test]
376    fn test_control_result_serde_roundtrip() {
377        let result = ControlResult {
378            operation: ControlOperation::StatusCheck,
379            success: true,
380            message: "OK".to_string(),
381            data: serde_json::json!({"key": "value"}),
382        };
383        let json = serde_json::to_string(&result).unwrap();
384        let back: ControlResult = serde_json::from_str(&json).unwrap();
385        assert_eq!(result, back);
386    }
387
388    #[test]
389    fn test_control_operation_serde_roundtrip() {
390        for op in [
391            ControlOperation::StatusCheck,
392            ControlOperation::PathTranslate,
393            ControlOperation::ServiceList,
394            ControlOperation::HealthCheck,
395            ControlOperation::WslCheck,
396        ] {
397            let json = serde_json::to_string(&op).unwrap();
398            let back: ControlOperation = serde_json::from_str(&json).unwrap();
399            assert_eq!(op, back);
400        }
401    }
402
403    // ================================================================
404    // Integration: Control Shell uses shared contracts
405    // ================================================================
406
407    #[test]
408    fn test_control_shell_uses_shared_types() {
409        let shell = ControlShell::new(BackendMode::LinuxNative);
410
411        // Status check uses shared BackendMode and WslStatus types
412        let status = shell.status_check();
413        assert!(status.data.get("backend_mode").is_some());
414
415        // Path translation uses shared path_bridge module
416        let translated = shell.translate_path("/home/user");
417        assert!(translated.data.get("translated").is_some());
418
419        // WSL check uses shared wsl module
420        let wsl = shell.wsl_check();
421        assert!(wsl.data.get("status").is_some());
422    }
423
424    #[test]
425    fn test_control_shell_all_operations_succeed() {
426        let mut shell = ControlShell::auto();
427        shell
428            .register_service(ServiceConfig::new("test", 9999))
429            .unwrap();
430
431        // All operations should succeed without spawning any subprocess
432        assert!(shell.status_check().success);
433        assert!(shell.translate_path(r"C:\test").success);
434        assert!(shell.list_services().success);
435        assert!(shell.check_health().success);
436        // wsl_check success depends on system state, just verify no panic
437        let _ = shell.wsl_check();
438    }
439}