use super::{DeploymentMode, EngineType};
use std::ffi::{OsStr, OsString};
use std::time::Duration;
#[derive(Clone, Debug)]
pub struct DetectedEngine {
pub engine_type: EngineType,
pub endpoint: String,
pub deployment_mode: DeploymentMode,
}
const ENGINE_BINARIES: &[(&str, EngineType, &str)] =
&[("vllm", EngineType::Vllm, "http://localhost:8000")];
pub async fn detect_engines(
sys: &sysinfo::System,
client: &reqwest::Client,
) -> Vec<DetectedEngine> {
let mut candidates = detect_by_process(sys);
let docker_candidates = detect_docker_engines().await;
for dc in docker_candidates {
if let Some(existing) = candidates
.iter_mut()
.find(|c| c.engine_type == dc.engine_type)
{
existing.endpoint = dc.endpoint;
existing.deployment_mode = dc.deployment_mode;
} else {
candidates.push(dc);
}
}
let mut verified = Vec::new();
for candidate in candidates {
if probe_engine(client, &candidate).await {
verified.push(candidate);
}
}
verified
}
fn detect_by_process(sys: &sysinfo::System) -> Vec<DetectedEngine> {
let mut detected = Vec::new();
for &(binary, ref engine_type, default_endpoint) in ENGINE_BINARIES {
let mut procs: Vec<_> = sys.processes_by_name(OsStr::new(binary)).collect();
if procs.is_empty() {
let vllm_procs: Vec<_> = sys
.processes()
.values()
.filter(|p| {
p.cmd().iter().any(|arg| {
arg.to_str()
.map(|s| {
s.contains("vllm.entrypoints")
|| s.ends_with("/vllm")
|| s == "vllm"
})
.unwrap_or(false)
})
})
.collect();
procs = vllm_procs;
}
if !procs.is_empty() {
let endpoint = procs
.iter()
.filter(|p| !p.cmd().is_empty())
.filter_map(|p| parse_endpoint_from_args(p.cmd(), default_endpoint))
.next()
.unwrap_or_else(|| default_endpoint.to_string());
detected.push(DetectedEngine {
engine_type: engine_type.clone(),
endpoint,
deployment_mode: DeploymentMode::Native,
});
}
}
detected
}
fn parse_endpoint_from_args(args: &[OsString], default_endpoint: &str) -> Option<String> {
let args: Vec<String> = args
.iter()
.filter_map(|a| a.to_str().map(String::from))
.collect();
let mut host: Option<&str> = None;
let mut port: Option<&str> = None;
let mut i = 0;
while i < args.len() {
if args[i] == "--port" {
if let Some(val) = args.get(i + 1) {
port = Some(val.as_str());
i += 2;
continue;
}
} else if let Some(val) = args[i].strip_prefix("--port=") {
port = Some(val);
} else if args[i] == "--host" {
if let Some(val) = args.get(i + 1) {
host = Some(val.as_str());
i += 2;
continue;
}
} else if let Some(val) = args[i].strip_prefix("--host=") {
host = Some(val);
}
i += 1;
}
if port.is_some() || host.is_some() {
let h = host.unwrap_or("localhost");
let h = if h == "0.0.0.0" { "localhost" } else { h };
let p = port.unwrap_or("8000");
Some(format!("http://{}:{}", h, p))
} else {
Some(default_endpoint.to_string())
}
}
#[cfg(target_os = "linux")]
pub async fn detect_docker_engines() -> Vec<DetectedEngine> {
use bollard::query_parameters::{ListContainersOptions, TopOptionsBuilder};
use bollard::Docker;
let docker = match Docker::connect_with_local_defaults() {
Ok(d) => d,
Err(e) => {
tracing::debug!(
"Docker not available for engine detection (permission denied?): {}",
e
);
return vec![];
}
};
let opts = ListContainersOptions {
all: false, ..Default::default()
};
let containers = match docker.list_containers(Some(opts)).await {
Ok(c) => c,
Err(e) => {
tracing::debug!("Failed to list Docker containers: {}", e);
return vec![];
}
};
let mut detected = Vec::new();
for container in &containers {
let image = container
.image
.as_deref()
.unwrap_or_default()
.to_lowercase();
let command = container
.command
.as_deref()
.unwrap_or_default()
.to_lowercase();
let names = container
.names
.as_ref()
.map(|n| n.join(" ").to_lowercase())
.unwrap_or_default();
let is_vllm = image.contains("vllm") || command.contains("vllm") || names.contains("vllm");
if !is_vllm {
continue;
}
let mapped_port = container
.ports
.as_ref()
.and_then(|ports| ports.iter().find_map(|p| p.public_port));
let cmd_port =
parse_port_from_command_str(container.command.as_deref().unwrap_or_default());
let port = mapped_port.map(|p| p.to_string()).or(cmd_port);
let port = match port {
Some(p) => Some(p),
None => {
let container_id = container.id.as_deref().unwrap_or_default();
if container_id.is_empty() {
None
} else {
let top_opts = TopOptionsBuilder::default().ps_args("-eo pid,args").build();
match docker.top_processes(container_id, Some(top_opts)).await {
Ok(top) => {
top.processes.as_ref().and_then(|procs| {
for row in procs {
let line = row.join(" ");
if let Some(p) = parse_port_from_command_str(&line) {
tracing::debug!(
"Docker top: found port {} in: {}",
p,
line
);
return Some(p);
}
}
None
})
}
Err(e) => {
tracing::debug!("docker top failed for {}: {}", container_id, e);
None
}
}
}
}
};
if let Some(p) = port {
let endpoint = format!("http://localhost:{}", p);
tracing::debug!(
"Docker vLLM candidate: image={}, port={}, endpoint={}",
container.image.as_deref().unwrap_or("?"),
p,
endpoint
);
detected.push(DetectedEngine {
engine_type: EngineType::Vllm,
endpoint,
deployment_mode: DeploymentMode::Docker,
});
} else {
tracing::debug!(
"Docker vLLM container found (image={}) but could not determine port",
container.image.as_deref().unwrap_or("?"),
);
}
}
detected
}
#[cfg(target_os = "linux")]
fn parse_port_from_command_str(cmd: &str) -> Option<String> {
let parts: Vec<&str> = cmd.split_whitespace().collect();
for (i, part) in parts.iter().enumerate() {
if *part == "--port" {
if let Some(val) = parts.get(i + 1) {
if val.parse::<u16>().is_ok() {
return Some(val.to_string());
}
}
} else if let Some(val) = part.strip_prefix("--port=") {
if val.parse::<u16>().is_ok() {
return Some(val.to_string());
}
}
}
None
}
#[cfg(not(target_os = "linux"))]
pub async fn detect_docker_engines() -> Vec<DetectedEngine> {
tracing::debug!("Docker engine detection stubbed on non-Linux");
vec![]
}
async fn probe_engine(client: &reqwest::Client, candidate: &DetectedEngine) -> bool {
let timeout = Duration::from_secs(2);
match candidate.engine_type {
EngineType::Vllm => {
client
.get(format!("{}/health", candidate.endpoint))
.timeout(timeout)
.send()
.await
.map(|r| r.status().is_success())
.unwrap_or(false)
}
}
}