use std::fs::File; use std::io::Read; use std::time::Duration; use reqwest::multipart::{Form, Part}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; /// WASM management port on ESP32 nodes. const WASM_PORT: u16 = 8033; /// Request timeout for WASM operations. const WASM_TIMEOUT_SECS: u64 = 30; /// List WASM modules loaded on a specific node. #[tauri::command] pub async fn wasm_list(node_ip: String) -> Result, String> { let client = reqwest::Client::builder() .timeout(Duration::from_secs(WASM_TIMEOUT_SECS)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let url = format!("http://{}:{}/wasm/list", node_ip, WASM_PORT); let response = client .get(&url) .send() .await .map_err(|e| format!("Failed to connect to node: {}", e))?; if !response.status().is_success() { return Err(format!("Node returned HTTP {}", response.status())); } let modules: Vec = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; Ok(modules) } /// Upload a WASM module to a node. /// /// Protocol: /// 1. Read WASM file and calculate SHA-256 /// 2. POST multipart/form-data to http://:8033/wasm/upload /// 3. Module is automatically validated on node side /// 4. Return assigned module ID #[tauri::command] pub async fn wasm_upload( node_ip: String, wasm_path: String, module_name: Option, auto_start: Option, ) -> Result { // Read WASM file let mut file = File::open(&wasm_path).map_err(|e| format!("Cannot read WASM file: {}", e))?; let mut wasm_data = Vec::new(); file.read_to_end(&mut wasm_data) .map_err(|e| format!("Failed to read WASM file: {}", e))?; let wasm_size = wasm_data.len(); // Validate WASM magic bytes if wasm_data.len() < 4 || &wasm_data[0..4] != b"\0asm" { return Err("Invalid WASM file: missing magic bytes".into()); } // Calculate SHA-256 let mut hasher = Sha256::new(); hasher.update(&wasm_data); let wasm_hash = hex::encode(hasher.finalize()); // Extract filename for module name let name = module_name.unwrap_or_else(|| { std::path::Path::new(&wasm_path) .file_stem() .and_then(|s| s.to_str()) .unwrap_or("module") .to_string() }); // Build HTTP client let client = reqwest::Client::builder() .timeout(Duration::from_secs(WASM_TIMEOUT_SECS)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; // Build multipart form let wasm_part = Part::bytes(wasm_data) .file_name(format!("{}.wasm", name)) .mime_str("application/wasm") .map_err(|e| format!("Failed to create multipart: {}", e))?; let form = Form::new() .part("wasm", wasm_part) .text("name", name.clone()) .text("sha256", wasm_hash.clone()) .text("size", wasm_size.to_string()) .text("auto_start", auto_start.unwrap_or(false).to_string()); // Send request let url = format!("http://{}:{}/wasm/upload", node_ip, WASM_PORT); let response = client .post(&url) .multipart(form) .send() .await .map_err(|e| format!("WASM upload failed: {}", e))?; let status = response.status(); if !status.is_success() { let body = response.text().await.unwrap_or_default(); return Err(format!("WASM upload failed with HTTP {}: {}", status, body)); } // Parse response for module ID let upload_response: WasmUploadResponse = response .json() .await .map_err(|e| format!("Failed to parse upload response: {}", e))?; Ok(WasmUploadResult { success: true, module_id: upload_response.module_id, message: format!( "Module '{}' uploaded successfully ({} bytes)", name, wasm_size ), sha256: Some(wasm_hash), }) } /// Start, stop, or unload a WASM module on a node. /// /// Actions: /// - "start": Start module execution /// - "stop": Pause module execution /// - "unload": Remove module from memory /// - "restart": Stop then start #[tauri::command] pub async fn wasm_control( node_ip: String, module_id: String, action: String, ) -> Result { // Validate action let valid_actions = ["start", "stop", "unload", "restart"]; if !valid_actions.contains(&action.as_str()) { return Err(format!( "Invalid action '{}'. Valid actions: {:?}", action, valid_actions )); } let client = reqwest::Client::builder() .timeout(Duration::from_secs(WASM_TIMEOUT_SECS)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let url = format!( "http://{}:{}/wasm/{}/{}", node_ip, WASM_PORT, module_id, action ); let response = client .post(&url) .send() .await .map_err(|e| format!("WASM control failed: {}", e))?; let status = response.status(); if !status.is_success() { let body = response.text().await.unwrap_or_default(); return Err(format!( "WASM {} failed with HTTP {}: {}", action, status, body )); } Ok(WasmControlResult { success: true, module_id, action, message: "Operation completed successfully".into(), }) } /// Get detailed info about a specific WASM module. #[tauri::command] pub async fn wasm_info(node_ip: String, module_id: String) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(WASM_TIMEOUT_SECS)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let url = format!("http://{}:{}/wasm/{}", node_ip, WASM_PORT, module_id); let response = client .get(&url) .send() .await .map_err(|e| format!("Failed to get module info: {}", e))?; if !response.status().is_success() { return Err(format!("Module not found or HTTP {}", response.status())); } let detail: WasmModuleDetail = response .json() .await .map_err(|e| format!("Failed to parse module info: {}", e))?; Ok(detail) } /// Get WASM runtime statistics from a node. #[tauri::command] pub async fn wasm_stats(node_ip: String) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(WASM_TIMEOUT_SECS)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let url = format!("http://{}:{}/wasm/stats", node_ip, WASM_PORT); let response = client .get(&url) .send() .await .map_err(|e| format!("Failed to get WASM stats: {}", e))?; if !response.status().is_success() { return Err(format!("HTTP {}", response.status())); } let stats: WasmRuntimeStats = response .json() .await .map_err(|e| format!("Failed to parse stats: {}", e))?; Ok(stats) } /// Check if node supports WASM modules. #[tauri::command] pub async fn check_wasm_support(node_ip: String) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(5)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let url = format!("http://{}:{}/wasm/info", node_ip, WASM_PORT); match client.get(&url).send().await { Ok(response) => { if response.status().is_success() { let body = response.text().await.unwrap_or_default(); // Try to parse as JSON let info = serde_json::from_str::(&body).ok(); Ok(WasmSupportInfo { supported: true, max_modules: info .as_ref() .and_then(|v| v.get("max_modules").and_then(|v| v.as_u64())) .map(|v| v as u8), memory_limit_kb: info .as_ref() .and_then(|v| v.get("memory_limit_kb").and_then(|v| v.as_u64())) .map(|v| v as u32), verify_signatures: info .as_ref() .and_then(|v| v.get("verify_signatures").and_then(|v| v.as_bool())) .unwrap_or(false), }) } else if response.status() == reqwest::StatusCode::NOT_FOUND { Ok(WasmSupportInfo { supported: false, max_modules: None, memory_limit_kb: None, verify_signatures: false, }) } else { Err(format!("HTTP {}", response.status())) } } Err(_) => Ok(WasmSupportInfo { supported: false, max_modules: None, memory_limit_kb: None, verify_signatures: false, }), } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmModuleInfo { pub id: String, pub name: String, pub size_bytes: u64, pub status: String, pub sha256: Option, pub loaded_at: Option, pub memory_used_kb: Option, pub cpu_usage_pct: Option, pub exec_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmModuleDetail { pub id: String, pub name: String, pub size_bytes: u64, pub status: String, pub sha256: String, pub loaded_at: String, pub memory_used_kb: u32, pub exports: Vec, pub imports: Vec, pub execution_count: u64, pub last_error: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] struct WasmUploadResponse { pub module_id: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmUploadResult { pub success: bool, pub module_id: String, pub message: String, pub sha256: Option, } #[derive(Debug, Clone, Serialize)] pub struct WasmControlResult { pub success: bool, pub module_id: String, pub action: String, pub message: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmRuntimeStats { pub total_modules: u8, pub running_modules: u8, pub memory_used_kb: u32, pub memory_limit_kb: u32, pub total_executions: u64, pub errors: u64, } #[derive(Debug, Clone, Serialize)] pub struct WasmSupportInfo { pub supported: bool, pub max_modules: Option, pub memory_limit_kb: Option, pub verify_signatures: bool, } #[cfg(test)] mod tests { use super::*; #[test] fn test_wasm_magic_bytes() { let valid_wasm = b"\0asm\x01\x00\x00\x00"; assert_eq!(&valid_wasm[0..4], b"\0asm"); let invalid = b"not wasm"; assert_ne!(&invalid[0..4], b"\0asm"); } #[test] fn test_wasm_module_info() { let info = WasmModuleInfo { id: "mod-1".into(), name: "test".into(), size_bytes: 1024, status: "running".into(), sha256: Some("abc123".into()), loaded_at: Some("2024-01-01T00:00:00Z".into()), memory_used_kb: Some(128), cpu_usage_pct: Some(5.2), exec_count: Some(42), }; assert_eq!(info.id, "mod-1"); assert_eq!(info.size_bytes, 1024); assert_eq!(info.memory_used_kb, Some(128)); } }