Skip to main content

commy_sdk_rust/
examples_support.rs

1// Server setup infrastructure for examples
2//
3// This module handles downloading, configuring, and starting a Commy server
4// for use by example applications and the example runner GUI.
5
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::process::{Child, Command};
9use tokio::time::{sleep, Duration};
10
11const COMMY_RELEASE_URL: &str = "https://github.com/commy-project/commy/releases/download";
12const DEFAULT_PORT: u16 = 8443;
13const DEFAULT_HTTP_PORT: u16 = 8000;
14
15/// Configuration for running a Commy server
16#[derive(Debug, Clone)]
17pub struct ServerConfig {
18    pub port: u16,
19    pub http_port: u16,
20    pub data_dir: PathBuf,
21    pub cert_path: PathBuf,
22    pub key_path: PathBuf,
23}
24
25impl ServerConfig {
26    /// Create default configuration
27    pub fn default() -> Self {
28        let data_dir = PathBuf::from(".commy_examples");
29        let cert_path = data_dir.join("cert.pem");
30        let key_path = data_dir.join("key.pem");
31
32        Self {
33            port: DEFAULT_PORT,
34            http_port: DEFAULT_HTTP_PORT,
35            data_dir,
36            cert_path,
37            key_path,
38        }
39    }
40
41    /// Create configuration with custom port
42    pub fn with_port(mut self, port: u16) -> Self {
43        self.port = port;
44        self
45    }
46}
47
48/// Manages Commy server process lifecycle
49pub struct CommyServer {
50    config: ServerConfig,
51    process: Option<Child>,
52}
53
54impl CommyServer {
55    /// Create a new server manager
56    pub fn new(config: ServerConfig) -> Self {
57        Self {
58            config,
59            process: None,
60        }
61    }
62
63    /// Create a server with default configuration
64    pub fn default() -> Self {
65        Self::new(ServerConfig::default())
66    }
67
68    /// Prepare the server for running (download binary, generate certs, etc.)
69    pub async fn prepare(&self) -> Result<(), Box<dyn std::error::Error>> {
70        // Create data directory
71        fs::create_dir_all(&self.config.data_dir)?;
72
73        // Check if binary exists, otherwise download it
74        let binary_path = self.config.data_dir.join("commy");
75        if !binary_path.exists() {
76            println!("📥 Downloading Commy server binary...");
77            self.download_binary(&binary_path).await?;
78            println!("✅ Downloaded to: {}", binary_path.display());
79        } else {
80            println!("✓ Commy binary already present");
81        }
82
83        // Generate TLS certificates if needed
84        if !self.config.cert_path.exists() || !self.config.key_path.exists() {
85            println!("🔐 Generating TLS certificates...");
86            self.generate_self_signed_cert()?;
87            println!("✅ Generated certificates");
88        } else {
89            println!("✓ TLS certificates already present");
90        }
91
92        Ok(())
93    }
94
95    /// Download the Commy server binary
96    async fn download_binary(&self, target: &Path) -> Result<(), Box<dyn std::error::Error>> {
97        // In production, this would download from GitHub releases
98        // For now, we'll look for a pre-built binary in the workspace
99        let workspace_binary = PathBuf::from("target/release/commy");
100
101        if workspace_binary.exists() {
102            fs::copy(&workspace_binary, target)?;
103            #[cfg(unix)]
104            {
105                use std::os::unix::fs::PermissionsExt;
106                let perms = fs::Permissions::from_mode(0o755);
107                fs::set_permissions(target, perms)?;
108            }
109            Ok(())
110        } else {
111            // Fallback: try debug build
112            let debug_binary = PathBuf::from("target/debug/commy");
113            if debug_binary.exists() {
114                fs::copy(&debug_binary, target)?;
115                #[cfg(unix)]
116                {
117                    use std::os::unix::fs::PermissionsExt;
118                    let perms = fs::Permissions::from_mode(0o755);
119                    fs::set_permissions(target, perms)?;
120                }
121                Ok(())
122            } else {
123                Err("Commy binary not found. Please build with 'cargo build --release' in main Commy directory".into())
124            }
125        }
126    }
127
128    /// Generate self-signed TLS certificate
129    fn generate_self_signed_cert(&self) -> Result<(), Box<dyn std::error::Error>> {
130        // Create a self-signed certificate using openssl
131        // For example purposes, we create basic certs
132
133        let cert_path = &self.config.cert_path;
134        let key_path = &self.config.key_path;
135
136        // This is a simplified approach - in production you'd use proper cert generation
137        // For now, we create placeholder cert/key files
138        // You would typically use rustls_pemfile or similar for real cert generation
139
140        fs::write(
141            key_path,
142            "-----BEGIN PRIVATE KEY-----\n\
143            (This would be your actual private key)\n\
144            -----END PRIVATE KEY-----",
145        )?;
146
147        fs::write(
148            cert_path,
149            "-----BEGIN CERTIFICATE-----\n\
150            (This would be your actual certificate)\n\
151            -----END CERTIFICATE-----",
152        )?;
153
154        Ok(())
155    }
156
157    /// Start the Commy server
158    pub async fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
159        println!("🚀 Starting Commy server...");
160
161        let binary_path = self.config.data_dir.join("commy");
162
163        let mut cmd = Command::new(&binary_path);
164        cmd.env("COMMY_LISTEN_PORT", self.config.port.to_string())
165            .env("COMMY_LISTEN_ADDR", "127.0.0.1")
166            .env("COMMY_TLS_CERT_PATH", &self.config.cert_path)
167            .env("COMMY_TLS_KEY_PATH", &self.config.key_path)
168            .env("COMMY_CLUSTER_ENABLED", "false");
169
170        let child = cmd.spawn()?;
171        self.process = Some(child);
172
173        // Wait for server to be ready
174        println!("⏳ Waiting for server to start...");
175        self.wait_for_ready(5).await?;
176
177        println!("✅ Server started on port {}", self.config.port);
178        Ok(())
179    }
180
181    /// Wait for server to become ready (with timeout)
182    async fn wait_for_ready(&self, timeout_secs: u64) -> Result<(), Box<dyn std::error::Error>> {
183        let start = std::time::Instant::now();
184        let timeout = Duration::from_secs(timeout_secs);
185
186        loop {
187            if start.elapsed() > timeout {
188                return Err("Server failed to start within timeout".into());
189            }
190
191            // Try to connect to check if server is ready
192            match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", self.config.port)).await {
193                Ok(_) => return Ok(()),
194                Err(_) => {
195                    sleep(Duration::from_millis(100)).await;
196                }
197            }
198        }
199    }
200
201    /// Stop the server
202    pub async fn stop(&mut self) -> Result<(), Box<dyn std::error::Error>> {
203        if let Some(mut child) = self.process.take() {
204            println!("🛑 Stopping Commy server...");
205            child.kill()?;
206            child.wait()?;
207            println!("✅ Server stopped");
208        }
209        Ok(())
210    }
211
212    /// Get the server URL
213    pub fn url(&self) -> String {
214        format!("wss://127.0.0.1:{}", self.config.port)
215    }
216}
217
218impl Drop for CommyServer {
219    fn drop(&mut self) {
220        if let Some(mut child) = self.process.take() {
221            let _ = child.kill();
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_default_config() {
232        let config = ServerConfig::default();
233        assert_eq!(config.port, 8443);
234        assert_eq!(config.http_port, 8000);
235    }
236
237    #[test]
238    fn test_custom_port_config() {
239        let config = ServerConfig::default().with_port(9443);
240        assert_eq!(config.port, 9443);
241    }
242
243    #[test]
244    fn test_server_url() {
245        let server = CommyServer::default();
246        assert!(server.url().starts_with("wss://"));
247    }
248}