netget/scripting/
environment.rs1use super::types::ScriptLanguage;
4use std::sync::OnceLock;
5use std::time::Duration;
6use tracing::warn;
7#[cfg(not(target_arch = "wasm32"))]
8use tracing::{debug, info};
9
10const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
23
24static DETECTED: OnceLock<ScriptingEnvironment> = OnceLock::new();
30
31#[derive(Debug, Clone, Default)]
33pub struct ScriptingEnvironment {
34 pub python: Option<String>,
36
37 pub javascript: Option<String>,
39
40 pub go: Option<String>,
42
43 pub perl: Option<String>,
45}
46
47impl ScriptingEnvironment {
48 pub fn detect() -> Self {
55 DETECTED.get_or_init(Self::detect_uncached).clone()
56 }
57
58 pub fn detect_uncached() -> Self {
60 #[cfg(target_arch = "wasm32")]
61 {
62 Self::default()
63 }
64 #[cfg(not(target_arch = "wasm32"))]
65 {
66 Self::detect_native()
67 }
68 }
69
70 #[cfg(not(target_arch = "wasm32"))]
71 fn detect_native() -> Self {
72 debug!("Detecting scripting environments...");
73 let (python, javascript, go, perl) = std::thread::scope(|scope| {
75 let python = scope.spawn(Self::detect_python);
76 let javascript = scope.spawn(Self::detect_javascript);
77 let go = scope.spawn(Self::detect_go);
78 let perl = scope.spawn(Self::detect_perl);
79 (
80 python.join().unwrap_or_default(),
81 javascript.join().unwrap_or_default(),
82 go.join().unwrap_or_default(),
83 perl.join().unwrap_or_default(),
84 )
85 });
86
87 info!("Scripting environment detection:");
88 if let Some(ref ver) = python {
89 info!(" Python: {} ✓", ver);
90 } else {
91 info!(" Python: not available");
92 }
93 if let Some(ref ver) = javascript {
94 info!(" Node.js: {} ✓", ver);
95 } else {
96 info!(" Node.js: not available");
97 }
98 if let Some(ref ver) = go {
99 info!(" Go: {} ✓", ver);
100 } else {
101 info!(" Go: not available");
102 }
103 if let Some(ref ver) = perl {
104 info!(" Perl: {} ✓", ver);
105 } else {
106 info!(" Perl: not available");
107 }
108
109 Self {
110 python,
111 javascript,
112 go,
113 perl,
114 }
115 }
116
117 pub fn probe(program: &str, args: &[&str]) -> Option<String> {
123 Self::probe_with_timeout(program, args, PROBE_TIMEOUT)
124 }
125
126 pub fn probe_with_timeout(program: &str, args: &[&str], timeout: Duration) -> Option<String> {
128 #[cfg(target_arch = "wasm32")]
129 {
130 let _ = (program, args, timeout);
131 None
132 }
133 #[cfg(not(target_arch = "wasm32"))]
134 {
135 let program = program.to_owned();
136 let args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
137 std::thread::Builder::new()
140 .name("netget-runtime-probe".into())
141 .spawn(move || {
142 let runtime = tokio::runtime::Builder::new_current_thread()
143 .enable_all()
144 .build()
145 .ok()?;
146 runtime.block_on(async move {
147 use super::process_io::{read_bounded, ProcessGroup};
148 use std::process::Stdio;
149 let mut command = tokio::process::Command::new(&program);
150 command
151 .args(&args)
152 .stdin(Stdio::null())
153 .stdout(Stdio::piped())
154 .stderr(Stdio::piped())
155 .kill_on_drop(true);
156 ProcessGroup::configure(&mut command);
157 let mut child = command.spawn().ok()?;
158 let group = ProcessGroup::new(&child).ok()?;
159 let stdout = child.stdout.take()?;
160 let stderr = child.stderr.take()?;
161 let outcome = tokio::time::timeout(timeout, async {
162 tokio::try_join!(
163 read_bounded(stdout, 64 * 1024),
164 read_bounded(stderr, 64 * 1024),
165 child.wait()
166 )
167 })
168 .await;
169 group.kill();
170 let _ = child.start_kill();
171 let _ = tokio::time::timeout(Duration::from_secs(1), child.wait()).await;
172 let (stdout, stderr, status) = outcome.ok()?.ok()?;
173 if !status.success() {
174 return None;
175 }
176 let output = if stdout.is_empty() { stderr } else { stdout };
177 let text = String::from_utf8_lossy(&output).trim().to_string();
178 (!text.is_empty()).then_some(text)
179 })
180 })
181 .ok()?
182 .join()
183 .ok()
184 .flatten()
185 }
186 }
187
188 #[cfg(not(target_arch = "wasm32"))]
189 fn detect_python() -> Option<String> {
190 Self::probe("python3", &["--version"]).inspect(|v| debug!("python3 detected: {v}"))
191 }
192
193 #[cfg(not(target_arch = "wasm32"))]
195 fn detect_javascript() -> Option<String> {
196 Self::probe("node", &["--version"]).inspect(|v| debug!("node detected: {v}"))
197 }
198
199 #[cfg(not(target_arch = "wasm32"))]
201 fn detect_go() -> Option<String> {
202 Self::probe("go", &["version"]).inspect(|v| debug!("go detected: {v}"))
203 }
204
205 #[cfg(not(target_arch = "wasm32"))]
207 fn detect_perl() -> Option<String> {
208 Self::probe("perl", &["--version"]).inspect(|v| debug!("perl detected: {v}"))
209 }
210
211 pub fn is_available(&self, language: ScriptLanguage) -> bool {
213 match language {
214 ScriptLanguage::Python => self.python.is_some(),
215 ScriptLanguage::JavaScript => self.javascript.is_some(),
216 ScriptLanguage::Go => self.go.is_some(),
217 ScriptLanguage::Perl => self.perl.is_some(),
218 }
219 }
220
221 pub fn get_version(&self, language: ScriptLanguage) -> Option<&str> {
223 match language {
224 ScriptLanguage::Python => self.python.as_deref(),
225 ScriptLanguage::JavaScript => self.javascript.as_deref(),
226 ScriptLanguage::Go => self.go.as_deref(),
227 ScriptLanguage::Perl => self.perl.as_deref(),
228 }
229 }
230
231 pub fn format_available(&self) -> String {
233 let mut parts = Vec::new();
234
235 if let Some(ref ver) = self.python {
236 parts.push(format!("Python ({})", ver));
237 }
238 if let Some(ref ver) = self.javascript {
239 parts.push(format!("Node.js ({})", ver));
240 }
241 if let Some(ref ver) = self.go {
242 parts.push(format!("Go ({})", ver));
243 }
244 if let Some(ref ver) = self.perl {
245 parts.push(format!("Perl ({})", ver));
246 }
247
248 if parts.is_empty() {
249 "None".to_string()
250 } else {
251 parts.join(", ")
252 }
253 }
254
255 pub fn warn_if_unavailable(&self, language: ScriptLanguage) {
257 if !self.is_available(language) {
258 warn!(
259 "{} is not available on this system. Scripts using {} will fail.",
260 language.as_str(),
261 language.as_str()
262 );
263 }
264 }
265}