1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::OnceLock;
9
10use crate::impl_serializable;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct RuntimeEnvironment {
15 pub library_version: String,
17 pub library: String,
19 pub platform: String,
21 pub runtime: String,
23 pub runtime_version: String,
25 pub architecture: String,
27 pub os: String,
29 pub compiler: String,
31 pub target: String,
33 pub env_vars: HashMap<String, String>,
35 pub features: Vec<String>,
37}
38
39impl RuntimeEnvironment {
40 pub fn new() -> Self {
42 Self {
43 library_version: crate::VERSION.to_string(),
44 library: "ferriclink-core".to_string(),
45 platform: get_platform_info(),
46 runtime: "rust".to_string(),
47 runtime_version: get_rust_version(),
48 architecture: get_architecture(),
49 os: get_os_info(),
50 compiler: get_compiler_info(),
51 target: get_target_triple(),
52 env_vars: get_relevant_env_vars(),
53 features: get_enabled_features(),
54 }
55 }
56
57 pub fn summary(&self) -> String {
59 format!(
60 "FerricLink {} on {} {} ({})",
61 self.library_version, self.os, self.architecture, self.runtime_version
62 )
63 }
64
65 pub fn has_feature(&self, feature: &str) -> bool {
67 self.features.contains(&feature.to_string())
68 }
69
70 pub fn get_env_var(&self, key: &str) -> Option<&String> {
72 self.env_vars.get(key)
73 }
74
75 pub fn is_debug(&self) -> bool {
77 cfg!(debug_assertions)
78 }
79
80 pub fn is_release(&self) -> bool {
82 !self.is_debug()
83 }
84
85 pub fn memory_info(&self) -> Option<MemoryInfo> {
87 get_memory_info()
88 }
89}
90
91impl Default for RuntimeEnvironment {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97impl_serializable!(
98 RuntimeEnvironment,
99 ["ferriclink", "env", "runtime_environment"]
100);
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104pub struct MemoryInfo {
105 pub total_memory: Option<u64>,
107 pub available_memory: Option<u64>,
109 pub process_memory: Option<u64>,
111}
112
113impl_serializable!(MemoryInfo, ["ferriclink", "env", "memory_info"]);
114
115static RUNTIME_ENV: OnceLock<RuntimeEnvironment> = OnceLock::new();
117
118pub fn get_runtime_environment() -> &'static RuntimeEnvironment {
127 RUNTIME_ENV.get_or_init(RuntimeEnvironment::new)
128}
129
130pub fn get_fresh_runtime_environment() -> RuntimeEnvironment {
132 RuntimeEnvironment::new()
133}
134
135fn get_platform_info() -> String {
137 format!("{} {}", get_os_info(), get_architecture())
138}
139
140fn get_os_info() -> String {
142 #[cfg(target_os = "linux")]
143 {
144 "Linux".to_string()
145 }
146 #[cfg(target_os = "macos")]
147 {
148 "macOS".to_string()
149 }
150 #[cfg(target_os = "windows")]
151 {
152 "Windows".to_string()
153 }
154 #[cfg(target_os = "freebsd")]
155 {
156 "FreeBSD".to_string()
157 }
158 #[cfg(target_os = "openbsd")]
159 {
160 "OpenBSD".to_string()
161 }
162 #[cfg(target_os = "netbsd")]
163 {
164 "NetBSD".to_string()
165 }
166 #[cfg(not(any(
167 target_os = "linux",
168 target_os = "macos",
169 target_os = "windows",
170 target_os = "freebsd",
171 target_os = "openbsd",
172 target_os = "netbsd"
173 )))]
174 {
175 "Unknown".to_string()
176 }
177}
178
179fn get_architecture() -> String {
181 #[cfg(target_arch = "x86_64")]
182 {
183 "x86_64".to_string()
184 }
185 #[cfg(target_arch = "x86")]
186 {
187 "x86".to_string()
188 }
189 #[cfg(target_arch = "aarch64")]
190 {
191 "aarch64".to_string()
192 }
193 #[cfg(target_arch = "arm")]
194 {
195 "arm".to_string()
196 }
197 #[cfg(target_arch = "riscv64")]
198 {
199 "riscv64".to_string()
200 }
201 #[cfg(target_arch = "powerpc64")]
202 {
203 "powerpc64".to_string()
204 }
205 #[cfg(not(any(
206 target_arch = "x86_64",
207 target_arch = "x86",
208 target_arch = "aarch64",
209 target_arch = "arm",
210 target_arch = "riscv64",
211 target_arch = "powerpc64"
212 )))]
213 {
214 "unknown".to_string()
215 }
216}
217
218fn get_rust_version() -> String {
220 std::env::var("RUSTC_VERSION")
221 .or_else(|_| std::env::var("RUSTC_VERSION_MAJOR").map(|v| format!("{v}.0.0")))
222 .unwrap_or_else(|_| "unknown".to_string())
223}
224
225fn get_compiler_info() -> String {
227 let rustc_version = get_rust_version();
228 if rustc_version == "unknown" {
229 "rustc (version unknown)".to_string()
230 } else {
231 format!("rustc {rustc_version}")
232 }
233}
234
235fn get_target_triple() -> String {
237 std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_string())
238}
239
240fn get_relevant_env_vars() -> HashMap<String, String> {
242 let mut env_vars = HashMap::new();
243
244 let relevant_vars = [
246 "RUST_LOG",
247 "RUST_BACKTRACE",
248 "CARGO_PKG_NAME",
249 "CARGO_PKG_VERSION",
250 "PATH",
251 "HOME",
252 "USER",
253 "SHELL",
254 "TERM",
255 "LANG",
256 "LC_ALL",
257 "TZ",
258 ];
259
260 for var in &relevant_vars {
261 if let Ok(value) = std::env::var(var) {
262 env_vars.insert(var.to_string(), value);
263 }
264 }
265
266 for (key, value) in std::env::vars() {
268 if key.starts_with("FERRICLINK_") || key.starts_with("FERRIC_") {
269 env_vars.insert(key, value);
270 }
271 }
272
273 env_vars
274}
275
276fn get_enabled_features() -> Vec<String> {
278 let mut features = Vec::new();
279
280 #[cfg(feature = "http")]
282 features.push("http".to_string());
283
284 #[cfg(feature = "validation")]
285 features.push("validation".to_string());
286
287 #[cfg(feature = "all")]
288 features.push("all".to_string());
289
290 if cfg!(debug_assertions) {
292 features.push("debug".to_string());
293 } else {
294 features.push("release".to_string());
295 }
296
297 if cfg!(target_pointer_width = "64") {
299 features.push("64bit".to_string());
300 } else if cfg!(target_pointer_width = "32") {
301 features.push("32bit".to_string());
302 }
303
304 features
305}
306
307fn get_memory_info() -> Option<MemoryInfo> {
309 #[cfg(target_os = "linux")]
310 {
311 get_linux_memory_info()
312 }
313 #[cfg(target_os = "macos")]
314 {
315 get_macos_memory_info()
316 }
317 #[cfg(target_os = "windows")]
318 {
319 get_windows_memory_info()
320 }
321 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
322 {
323 None
324 }
325}
326
327#[cfg(target_os = "linux")]
328fn get_linux_memory_info() -> Option<MemoryInfo> {
329 use std::fs;
330
331 let meminfo = fs::read_to_string("/proc/meminfo").ok()?;
333 let mut total_memory = None;
334 let mut available_memory = None;
335
336 for line in meminfo.lines() {
337 if line.starts_with("MemTotal:") {
338 if let Some(value) = line.split_whitespace().nth(1) {
339 total_memory = value.parse::<u64>().ok().map(|kb| kb * 1024);
340 }
341 } else if line.starts_with("MemAvailable:") {
342 if let Some(value) = line.split_whitespace().nth(1) {
343 available_memory = value.parse::<u64>().ok().map(|kb| kb * 1024);
344 }
345 }
346 }
347
348 let process_memory = get_process_memory_usage();
350
351 Some(MemoryInfo {
352 total_memory,
353 available_memory,
354 process_memory,
355 })
356}
357
358#[cfg(target_os = "macos")]
359fn get_macos_memory_info() -> Option<MemoryInfo> {
360 let process_memory = get_process_memory_usage();
363
364 Some(MemoryInfo {
365 total_memory: None,
366 available_memory: None,
367 process_memory,
368 })
369}
370
371#[cfg(target_os = "windows")]
372fn get_windows_memory_info() -> Option<MemoryInfo> {
373 let process_memory = get_process_memory_usage();
376
377 Some(MemoryInfo {
378 total_memory: None,
379 available_memory: None,
380 process_memory,
381 })
382}
383
384fn get_process_memory_usage() -> Option<u64> {
386 None
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_runtime_environment_creation() {
397 let env = RuntimeEnvironment::new();
398 assert_eq!(env.library, "ferriclink-core");
399 assert_eq!(env.runtime, "rust");
400 assert!(!env.library_version.is_empty());
401 assert!(!env.platform.is_empty());
402 }
403
404 #[test]
405 fn test_runtime_environment_caching() {
406 let env1 = get_runtime_environment();
407 let env2 = get_runtime_environment();
408 assert!(std::ptr::eq(env1, env2));
409 }
410
411 #[test]
412 fn test_runtime_environment_summary() {
413 let env = RuntimeEnvironment::new();
414 let summary = env.summary();
415 assert!(summary.contains("FerricLink"));
416 assert!(summary.contains(&env.library_version));
417 }
418
419 #[test]
420 fn test_feature_detection() {
421 let env = RuntimeEnvironment::new();
422 assert!(env.has_feature("debug") || env.has_feature("release"));
424 }
425
426 #[test]
427 fn test_environment_variables() {
428 let env = RuntimeEnvironment::new();
429 assert!(!env.env_vars.is_empty());
431 }
432
433 #[test]
434 fn test_serialization() {
435 let env = RuntimeEnvironment::new();
436 let serialized = serde_json::to_string(&env).unwrap();
437 let deserialized: RuntimeEnvironment = serde_json::from_str(&serialized).unwrap();
438 assert_eq!(env, deserialized);
439 }
440}