Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Binary/Build/
ServiceRegistry.rs

1//! # Service Registry Module
2//!
3//! ## RESPONSIBILITIES
4//!
5//! - Track mapping from land:// domain names to local HTTP service ports
6//! - Provide thread-safe access using Arc<RwLock<>>
7//! - Support service registration and lookup
8//! - Enable health checks for registered services
9//!
10//! ## ARCHITECTURAL ROLE
11//!
12//! The ServiceRegistry provides the bridge between land:// URIs and local
13//! services:
14//!
15//! ```text
16//! land://code.land.playform.cloud/path ──► ServiceRegistry ──► http://127.0.0.1:PORT/path
17//! ```
18//!
19//! ## THREAD SAFETY
20//!
21//! - Uses Arc<RwLock<HashMap<String, LocalService>>> for concurrent access
22//! - Multiple readers allowed concurrently
23//! - Writers lock exclusively
24//!
25//! ## USAGE
26//!
27//! ```rust
28//! let registry = ServiceRegistry::new();
29//! registry.register("code.land.playform.cloud".to_string(), 8080, Some("/health".to_string()));
30//!
31//! let service = registry.lookup("code.land.playform.cloud").unwrap();
32//! assert_eq!(service.port, 8080);
33//! ```
34
35use std::{
36	collections::HashMap,
37	sync::{Arc, RwLock},
38};
39
40use http::{Request as HttpRequest, Response as HttpResponse, header};
41use tokio::{
42	io::{AsyncReadExt, AsyncWriteExt},
43	net::TcpStream,
44};
45
46use crate::dev_log;
47
48/// Represents a local HTTP/HTTPS service registered with the land:// scheme
49///
50/// # Fields
51///
52/// - `name`: Domain name (e.g., "code.land.playform.cloud")
53/// - `port`: Local port where the service is listening
54/// - `tls_port`: Optional TLS port for HTTPS (defaults to port + 1000 if not
55///   specified)
56/// - `use_tls`: Whether the service uses HTTPS
57/// - `health_check_path`: Optional path for health check endpoint
58#[derive(Debug, Clone)]
59pub struct LocalService {
60	pub name:String,
61
62	pub port:u16,
63
64	pub tls_port:Option<u16>,
65
66	pub use_tls:bool,
67
68	pub health_check_path:Option<String>,
69}
70
71impl LocalService {
72	/// Get the appropriate port based on TLS configuration
73	pub fn get_port(&self) -> u16 {
74		if self.use_tls {
75			self.tls_port.unwrap_or_else(|| self.port + 1000)
76		} else {
77			self.port
78		}
79	}
80}
81
82/// Registry for tracking local HTTP/HTTPS services
83///
84/// Provides thread-safe methods to register and lookup services by domain name.
85/// Supports both HTTP and HTTPS protocols with automatic TLS certificate
86/// provisioning.
87#[derive(Clone)]
88pub struct ServiceRegistry {
89	/// Inner storage using `Arc<RwLock>` for thread-safe concurrent access
90	services:Arc<RwLock<HashMap<String, LocalService>>>,
91
92	/// Optional certificate manager for HTTPS support
93	cert_manager:Option<std::sync::Arc<std::sync::Mutex<super::CertificateManager::CertificateManager>>>,
94}
95
96impl ServiceRegistry {
97	/// Create a new ServiceRegistry instance
98	///
99	/// Returns an empty registry ready to accept service registrations.
100	pub fn new() -> Self {
101		dev_log!("lifecycle", "[ServiceRegistry] Creating new ServiceRegistry");
102
103		Self { services:Arc::new(RwLock::new(HashMap::new())), cert_manager:None }
104	}
105
106	/// Create a new ServiceRegistry instance with TLS support
107	///
108	/// # Parameters
109	///
110	/// * `cert_manager` - Certificate manager for provisioning TLS certificates
111	///
112	/// Returns a registry ready to accept both HTTP and HTTPS service
113	/// registrations.
114	pub fn with_tls(
115		cert_manager:std::sync::Arc<std::sync::Mutex<super::CertificateManager::CertificateManager>>,
116	) -> Self {
117		dev_log!("lifecycle", "[ServiceRegistry] Creating new ServiceRegistry with TLS support");
118
119		Self { services:Arc::new(RwLock::new(HashMap::new())), cert_manager:Some(cert_manager) }
120	}
121
122	/// Register a local HTTP service
123	///
124	/// # Parameters
125	///
126	/// - `name`: Domain name (e.g., "code.land.playform.cloud")
127	/// - `port`: Local port where the service is listening
128	/// - `health_check_path`: Optional path for health check endpoint (e.g.,
129	///   "/health")
130	///
131	/// # Example
132	///
133	/// ```rust
134	/// registry.register("code.land.playform.cloud".to_string(), 8080, Some("/health".to_string()));
135	/// ```
136	pub fn register(&self, name:String, port:u16, health_check_path:Option<String>) {
137		self.register_with_options(name, port, None, false, health_check_path);
138	}
139
140	/// Register a local service with TLS options
141	///
142	/// # Parameters
143	///
144	/// - `name`: Domain name (e.g., "code.land.playform.cloud")
145	/// - `port`: Local HTTP port
146	/// - `tls_port`: Optional TLS port (defaults to port + 1000)
147	/// - `use_tls`: Whether to enable HTTPS
148	/// - `health_check_path`: Optional path for health check endpoint
149	///
150	/// # Example
151	///
152	/// ```rust
153	/// // Register with TLS enabled
154	/// registry.register_with_options(
155	/// 	"code.land.playform.cloud".to_string(),
156	/// 	8080,
157	/// 	None, // Use default TLS port (9080)
158	/// 	true,
159	/// 	Some("/health".to_string()),
160	/// );
161	/// ```
162	pub fn register_with_options(
163		&self,
164
165		name:String,
166
167		port:u16,
168
169		tls_port:Option<u16>,
170
171		use_tls:bool,
172
173		health_check_path:Option<String>,
174	) {
175		dev_log!(
176			"lifecycle",
177			"[ServiceRegistry] Registering service: {} -> HTTP:{}, TLS:{}, use_tls:{}",
178			name,
179			port,
180			tls_port.unwrap_or(port + 1000),
181			use_tls
182		);
183
184		let service = LocalService { name:name.clone(), port, tls_port, use_tls, health_check_path };
185
186		// Pre-provision TLS certificate if needed
187		if use_tls {
188			if let Some(cert_manager) = &self.cert_manager {
189				// NOTE: TLS certificate is generated on-demand when needed
190				dev_log!("lifecycle", "[ServiceRegistry] TLS will be provisioned on-demand for {}", name);
191			} else {
192				dev_log!(
193					"lifecycle",
194					"warn: [ServiceRegistry] Service {} requested TLS but no certificate manager available",
195					name
196				);
197			}
198		}
199
200		if let Ok(mut services) = self.services.write() {
201			// Check if service already exists
202			if services.contains_key(&name) {
203				dev_log!(
204					"lifecycle",
205					"warn: [ServiceRegistry] Service {} already registered, overwriting",
206					name
207				);
208			}
209
210			services.insert(name.clone(), service);
211
212			dev_log!("lifecycle", "[ServiceRegistry] Service {} registered successfully", name);
213		} else {
214			dev_log!(
215				"lifecycle",
216				"error: [ServiceRegistry] Failed to acquire write lock for registration"
217			);
218		}
219	}
220
221	/// Look up a service by domain name
222	///
223	/// # Parameters
224	///
225	/// - `name`: Domain name to look up
226	///
227	/// # Returns
228	///
229	/// - `Some(LocalService)` if found
230	/// - `None` if not registered
231	///
232	/// # Example
233	///
234	/// ```rust
235	/// let service = registry.lookup("code.land.playform.cloud");
236	/// if let Some(svc) = service {
237	/// 	println!("Service running on port {}", svc.port);
238	/// }
239	/// ```
240	pub fn lookup(&self, name:&str) -> Option<LocalService> {
241		dev_log!("lifecycle", "[ServiceRegistry] Looking up service: {}", name);
242
243		if let Ok(services) = self.services.read() {
244			let service = services.get(name).cloned();
245
246			if service.is_some() {
247				dev_log!("lifecycle", "[ServiceRegistry] Service {} found", name);
248			} else {
249				dev_log!("lifecycle", "[ServiceRegistry] Service {} not found", name);
250			}
251
252			service
253		} else {
254			dev_log!("lifecycle", "error: [ServiceRegistry] Failed to acquire read lock for lookup");
255
256			None
257		}
258	}
259
260	/// Get all registered services
261	///
262	/// # Returns
263	///
264	/// A vector of all registered LocalService instances
265	pub fn all_services(&self) -> Vec<LocalService> {
266		if let Ok(services) = self.services.read() {
267			services.values().cloned().collect()
268		} else {
269			dev_log!(
270				"lifecycle",
271				"error: [ServiceRegistry] Failed to acquire read lock for all_services"
272			);
273
274			Vec::new()
275		}
276	}
277
278	/// Perform a health check on a registered service
279	///
280	/// # Parameters
281	///
282	/// - `name`: Domain name of the service to check
283	///
284	/// # Returns
285	///
286	/// - `Ok(true)` if service is healthy and responding
287	/// - `Ok(false)` if service is not healthy
288	/// - `Err` if service not found or health check fails
289	pub async fn health_check(&self, name:&str) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
290		let service = self.lookup(name).ok_or_else(|| format!("Service {} not found", name))?;
291
292		let health_path = service.health_check_path.as_deref().unwrap_or("/health");
293
294		let addr = format!("127.0.0.1:{}", service.port);
295
296		dev_log!(
297			"lifecycle",
298			"[ServiceRegistry] Performing health check for {} at {}:{}",
299			name,
300			addr,
301			health_path
302		);
303
304		// Try to connect to the service
305		match TcpStream::connect(&addr).await {
306			Ok(mut stream) => {
307				// Send simple HTTP GET request
308				let request = format!("GET {} HTTP/1.1\r\nHost: 127.0.0.1:{}\r\n\r\n", health_path, service.port);
309
310				match stream.write_all(request.as_bytes()).await {
311					Ok(_) => {
312						// Try to read response
313						let mut buffer = [0u8; 1024];
314
315						match stream.read(&mut buffer).await {
316							Ok(n) => {
317								let response = String::from_utf8_lossy(&buffer[..n]);
318
319								let is_healthy = response.contains("HTTP/1.1 200") || response.contains("HTTP/1.0 200");
320
321								if is_healthy {
322									dev_log!("lifecycle", "[ServiceRegistry] Service {} is healthy", name);
323								} else {
324									dev_log!(
325										"lifecycle",
326										"warn: [ServiceRegistry] Service {} health check failed: not 200",
327										name
328									);
329								}
330
331								Ok(is_healthy)
332							},
333
334							Err(e) => {
335								dev_log!(
336									"lifecycle",
337									"warn: [ServiceRegistry] Service {} health check failed to read: {}",
338									name,
339									e
340								);
341
342								Ok(false)
343							},
344						}
345					},
346
347					Err(e) => {
348						dev_log!(
349							"lifecycle",
350							"warn: [ServiceRegistry] Service {} health check failed to write: {}",
351							name,
352							e
353						);
354
355						Ok(false)
356					},
357				}
358			},
359
360			Err(e) => {
361				dev_log!(
362					"lifecycle",
363					"warn: [ServiceRegistry] Service {} health check failed to connect: {}",
364					name,
365					e
366				);
367
368				Ok(false)
369			},
370		}
371	}
372
373	/// Remove a service from the registry
374	///
375	/// # Parameters
376	///
377	/// - `name`: Domain name of the service to remove
378	///
379	/// # Returns
380	///
381	/// - `Some(LocalService)` if service was removed
382	/// - `None` if service was not found
383	pub fn unregister(&self, name:&str) -> Option<LocalService> {
384		dev_log!("lifecycle", "[ServiceRegistry] Unregistering service: {}", name);
385
386		if let Ok(mut services) = self.services.write() {
387			services.remove(name)
388		} else {
389			dev_log!(
390				"lifecycle",
391				"error: [ServiceRegistry] Failed to acquire write lock for unregistration"
392			);
393
394			None
395		}
396	}
397
398	/// Get TLS configuration for a service (if available)
399	///
400	/// # Parameters
401	///
402	/// - `name`: Domain name of the service
403	///
404	/// # Returns
405	///
406	/// - `Some(Arc<ServerConfig>)` if service uses TLS and certificate manager
407	///   is available
408	/// - `None` if service doesn't use TLS or certificate manager is not
409	///   configured
410	pub async fn get_tls_config(&self, name:&str) -> Option<std::sync::Arc<rustls::ServerConfig>> {
411		let service = self.lookup(name)?;
412
413		if !service.use_tls {
414			return None;
415		}
416
417		let cert_manager = self.cert_manager.as_ref()?;
418
419		let manager = cert_manager
420			.lock()
421			.map_err(|e| {
422				dev_log!("lifecycle", "error: [ServiceRegistry] Failed to acquire lock: {}", e);
423			})
424			.ok()?;
425
426		manager.build_server_config(name).await.ok()
427	}
428
429	/// Check if a service uses TLS
430	///
431	/// # Parameters
432	///
433	/// - `name`: Domain name of the service
434	///
435	/// # Returns
436	///
437	/// `true` if the service is configured to use TLS, `false` otherwise
438	pub fn uses_tls(&self, name:&str) -> bool { self.lookup(name).map(|s| s.use_tls).unwrap_or(false) }
439}
440
441impl Default for ServiceRegistry {
442	fn default() -> Self { Self::new() }
443}
444
445#[cfg(test)]
446mod tests {
447
448	use super::*;
449
450	#[test]
451	fn test_register_and_lookup() {
452		let registry = ServiceRegistry::new();
453
454		registry.register("test.service.land".to_string(), 8080, Some("/health".to_string()));
455
456		let service = registry.lookup("test.service.land").unwrap();
457
458		assert_eq!(service.name, "test.service.land");
459
460		assert_eq!(service.port, 8080);
461
462		assert_eq!(service.health_check_path, Some("/health".to_string()));
463	}
464
465	#[test]
466	fn test_lookup_nonexistent() {
467		let registry = ServiceRegistry::new();
468
469		let service = registry.lookup("nonexistent.service.land");
470
471		assert!(service.is_none());
472	}
473
474	#[test]
475	fn test_all_services() {
476		let registry = ServiceRegistry::new();
477
478		registry.register("service1.land".to_string(), 8080, None);
479
480		registry.register("service2.land".to_string(), 8081, None);
481
482		let services = registry.all_services();
483
484		assert_eq!(services.len(), 2);
485	}
486
487	#[test]
488	fn test_unregister() {
489		let registry = ServiceRegistry::new();
490
491		registry.register("test.service.land".to_string(), 8080, None);
492
493		assert!(registry.lookup("test.service.land").is_some());
494
495		registry.unregister("test.service.land");
496
497		assert!(registry.lookup("test.service.land").is_none());
498	}
499
500	#[test]
501	fn test_overwrite_registration() {
502		let registry = ServiceRegistry::new();
503
504		registry.register("test.service.land".to_string(), 8080, None);
505
506		registry.register("test.service.land".to_string(), 9090, None);
507
508		let service = registry.lookup("test.service.land").unwrap();
509
510		assert_eq!(service.port, 9090);
511	}
512
513	#[test]
514	fn test_tls_service() {
515		let registry = ServiceRegistry::new();
516
517		registry.register_with_options(
518			"secure.service.land".to_string(),
519			8080,
520			Some(8443),
521			true,
522			Some("/health".to_string()),
523		);
524
525		let service = registry.lookup("secure.service.land").unwrap();
526
527		assert_eq!(service.name, "secure.service.land");
528
529		assert_eq!(service.port, 8080);
530
531		assert_eq!(service.tls_port, Some(8443));
532
533		assert_eq!(service.use_tls, true);
534
535		assert_eq!(service.get_port(), 8443);
536	}
537
538	#[test]
539	fn test_default_tls_port() {
540		let registry = ServiceRegistry::new();
541
542		registry.register_with_options(
543			"secure.service.land".to_string(),
544			8080,
545			None, // Use default TLS port (8080 + 1000 = 9080)
546			true,
547			None,
548		);
549
550		let service = registry.lookup("secure.service.land").unwrap();
551
552		assert_eq!(service.tls_port, None); // Explicitly None
553
554		assert_eq!(service.get_port(), 9080); // But get_port() returns default
555	}
556}