use super::types::ScriptLanguage;
use std::sync::OnceLock;
use std::time::Duration;
use tracing::warn;
#[cfg(not(target_arch = "wasm32"))]
use tracing::{debug, info};
const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
static DETECTED: OnceLock<ScriptingEnvironment> = OnceLock::new();
#[derive(Debug, Clone, Default)]
pub struct ScriptingEnvironment {
pub python: Option<String>,
pub javascript: Option<String>,
pub go: Option<String>,
pub perl: Option<String>,
}
impl ScriptingEnvironment {
pub fn detect() -> Self {
DETECTED.get_or_init(Self::detect_uncached).clone()
}
pub fn detect_uncached() -> Self {
#[cfg(target_arch = "wasm32")]
{
Self::default()
}
#[cfg(not(target_arch = "wasm32"))]
{
Self::detect_native()
}
}
#[cfg(not(target_arch = "wasm32"))]
fn detect_native() -> Self {
debug!("Detecting scripting environments...");
let (python, javascript, go, perl) = std::thread::scope(|scope| {
let python = scope.spawn(Self::detect_python);
let javascript = scope.spawn(Self::detect_javascript);
let go = scope.spawn(Self::detect_go);
let perl = scope.spawn(Self::detect_perl);
(
python.join().unwrap_or_default(),
javascript.join().unwrap_or_default(),
go.join().unwrap_or_default(),
perl.join().unwrap_or_default(),
)
});
info!("Scripting environment detection:");
if let Some(ref ver) = python {
info!(" Python: {} ✓", ver);
} else {
info!(" Python: not available");
}
if let Some(ref ver) = javascript {
info!(" Node.js: {} ✓", ver);
} else {
info!(" Node.js: not available");
}
if let Some(ref ver) = go {
info!(" Go: {} ✓", ver);
} else {
info!(" Go: not available");
}
if let Some(ref ver) = perl {
info!(" Perl: {} ✓", ver);
} else {
info!(" Perl: not available");
}
Self {
python,
javascript,
go,
perl,
}
}
pub fn probe(program: &str, args: &[&str]) -> Option<String> {
Self::probe_with_timeout(program, args, PROBE_TIMEOUT)
}
pub fn probe_with_timeout(program: &str, args: &[&str], timeout: Duration) -> Option<String> {
#[cfg(target_arch = "wasm32")]
{
let _ = (program, args, timeout);
None
}
#[cfg(not(target_arch = "wasm32"))]
{
let program = program.to_owned();
let args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
std::thread::Builder::new()
.name("netget-runtime-probe".into())
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.ok()?;
runtime.block_on(async move {
use super::process_io::{read_bounded, ProcessGroup};
use std::process::Stdio;
let mut command = tokio::process::Command::new(&program);
command
.args(&args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
ProcessGroup::configure(&mut command);
let mut child = command.spawn().ok()?;
let group = ProcessGroup::new(&child).ok()?;
let stdout = child.stdout.take()?;
let stderr = child.stderr.take()?;
let outcome = tokio::time::timeout(timeout, async {
tokio::try_join!(
read_bounded(stdout, 64 * 1024),
read_bounded(stderr, 64 * 1024),
child.wait()
)
})
.await;
group.kill();
let _ = child.start_kill();
let _ = tokio::time::timeout(Duration::from_secs(1), child.wait()).await;
let (stdout, stderr, status) = outcome.ok()?.ok()?;
if !status.success() {
return None;
}
let output = if stdout.is_empty() { stderr } else { stdout };
let text = String::from_utf8_lossy(&output).trim().to_string();
(!text.is_empty()).then_some(text)
})
})
.ok()?
.join()
.ok()
.flatten()
}
}
#[cfg(not(target_arch = "wasm32"))]
fn detect_python() -> Option<String> {
Self::probe("python3", &["--version"]).inspect(|v| debug!("python3 detected: {v}"))
}
#[cfg(not(target_arch = "wasm32"))]
fn detect_javascript() -> Option<String> {
Self::probe("node", &["--version"]).inspect(|v| debug!("node detected: {v}"))
}
#[cfg(not(target_arch = "wasm32"))]
fn detect_go() -> Option<String> {
Self::probe("go", &["version"]).inspect(|v| debug!("go detected: {v}"))
}
#[cfg(not(target_arch = "wasm32"))]
fn detect_perl() -> Option<String> {
Self::probe("perl", &["--version"]).inspect(|v| debug!("perl detected: {v}"))
}
pub fn is_available(&self, language: ScriptLanguage) -> bool {
match language {
ScriptLanguage::Python => self.python.is_some(),
ScriptLanguage::JavaScript => self.javascript.is_some(),
ScriptLanguage::Go => self.go.is_some(),
ScriptLanguage::Perl => self.perl.is_some(),
}
}
pub fn get_version(&self, language: ScriptLanguage) -> Option<&str> {
match language {
ScriptLanguage::Python => self.python.as_deref(),
ScriptLanguage::JavaScript => self.javascript.as_deref(),
ScriptLanguage::Go => self.go.as_deref(),
ScriptLanguage::Perl => self.perl.as_deref(),
}
}
pub fn format_available(&self) -> String {
let mut parts = Vec::new();
if let Some(ref ver) = self.python {
parts.push(format!("Python ({})", ver));
}
if let Some(ref ver) = self.javascript {
parts.push(format!("Node.js ({})", ver));
}
if let Some(ref ver) = self.go {
parts.push(format!("Go ({})", ver));
}
if let Some(ref ver) = self.perl {
parts.push(format!("Perl ({})", ver));
}
if parts.is_empty() {
"None".to_string()
} else {
parts.join(", ")
}
}
pub fn warn_if_unavailable(&self, language: ScriptLanguage) {
if !self.is_available(language) {
warn!(
"{} is not available on this system. Scripts using {} will fail.",
language.as_str(),
language.as_str()
);
}
}
}