use std::net::Ipv4Addr;
use std::process::Command;
#[derive(Clone, Debug)]
pub struct WslInstance {
pub name: String,
pub is_running: bool,
pub wsl_version: u8,
pub ip: Option<Ipv4Addr>,
pub all_ips: Vec<Ipv4Addr>,
pub mac: String,
pub is_default: bool,
pub linux_hostname: String,
pub os_pretty_name: String,
pub os_id: String,
pub os_version: String,
pub kernel: String,
}
pub fn discover() -> Vec<WslInstance> {
let mut instances = discover_verbose();
if instances.is_empty() {
instances = discover_running_only();
}
for inst in &mut instances {
if !inst.is_running {
continue;
}
let (ips, mac) = get_network_info(&inst.name);
if !ips.is_empty() {
inst.all_ips = ips;
inst.ip = inst.all_ips.first().copied();
inst.mac = mac;
} else {
inst.ip = get_wsl_ip_hostname(&inst.name);
if let Some(ip) = inst.ip {
inst.all_ips = vec![ip];
}
}
let (pretty_name, os_id, os_version) = get_os_release(&inst.name);
inst.os_pretty_name = pretty_name;
inst.os_id = os_id;
inst.os_version = os_version;
inst.kernel = get_kernel(&inst.name);
inst.linux_hostname = get_linux_hostname(&inst.name);
}
instances
}
fn discover_verbose() -> Vec<WslInstance> {
let output = Command::new("wsl")
.args(["--list", "--verbose"])
.output();
let output = match output {
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
let stdout = decode_wsl_output(&output.stdout);
let mut instances = Vec::new();
for line in stdout.lines().skip(1) {
let line = line.trim();
if line.is_empty() {
continue;
}
let is_default = line.starts_with('*');
let line = line.trim_start_matches('*').trim();
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
continue;
}
let name = parts[0].to_string();
let state_str = parts.get(1).unwrap_or(&"");
let is_running = state_str.eq_ignore_ascii_case("running");
let wsl_version = parts.last()
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or(0);
instances.push(WslInstance {
name,
is_running,
wsl_version,
ip: None,
all_ips: Vec::new(),
mac: String::new(),
is_default,
linux_hostname: String::new(),
os_pretty_name: String::new(),
os_id: String::new(),
os_version: String::new(),
kernel: String::new(),
});
}
instances
}
fn discover_running_only() -> Vec<WslInstance> {
let output = Command::new("wsl")
.args(["--list", "--running", "--quiet"])
.output();
let output = match output {
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
let stdout = decode_wsl_output(&output.stdout);
stdout.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.map(|name| WslInstance {
name,
is_running: true,
wsl_version: 0,
ip: None,
all_ips: Vec::new(),
mac: String::new(),
is_default: false,
linux_hostname: String::new(),
os_pretty_name: String::new(),
os_id: String::new(),
os_version: String::new(),
kernel: String::new(),
})
.collect()
}
fn get_wsl_ip_hostname(distro: &str) -> Option<Ipv4Addr> {
let output = Command::new("wsl")
.args(["-d", distro, "--", "hostname", "-I"])
.output();
match output {
Ok(o) if o.status.success() => {
let stdout = String::from_utf8_lossy(&o.stdout);
stdout.trim().split_whitespace()
.find_map(|s| s.parse::<Ipv4Addr>().ok())
}
_ => None,
}
}
fn get_os_release(distro: &str) -> (String, String, String) {
let output = Command::new("wsl")
.args(["-d", distro, "--", "cat", "/etc/os-release"])
.output();
let text = match output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
_ => return (String::new(), String::new(), String::new()),
};
let mut pretty_name = String::new();
let mut os_id = String::new();
let mut os_version = String::new();
for line in text.lines() {
if let Some(val) = line.strip_prefix("PRETTY_NAME=") {
pretty_name = val.trim_matches('"').to_string();
} else if let Some(val) = line.strip_prefix("ID=") {
os_id = val.trim_matches('"').to_string();
} else if let Some(val) = line.strip_prefix("VERSION_ID=") {
os_version = val.trim_matches('"').to_string();
}
}
(pretty_name, os_id, os_version)
}
fn get_network_info(distro: &str) -> (Vec<Ipv4Addr>, String) {
let output = Command::new("wsl")
.args(["-d", distro, "--", "ip", "addr", "show", "eth0"])
.output();
let text = match output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).to_string(),
_ => return (Vec::new(), String::new()),
};
let mut ips = Vec::new();
let mut mac = String::new();
for line in text.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("inet ") {
if let Some(cidr) = rest.split_whitespace().next() {
if let Some(ip_str) = cidr.split('/').next() {
if let Ok(ip) = ip_str.parse::<Ipv4Addr>() {
ips.push(ip);
}
}
}
}
if let Some(rest) = trimmed.strip_prefix("link/ether ") {
if let Some(m) = rest.split_whitespace().next() {
mac = m.to_uppercase();
}
}
}
(ips, mac)
}
fn get_kernel(distro: &str) -> String {
let output = Command::new("wsl")
.args(["-d", distro, "--", "uname", "-r"])
.output();
match output {
Ok(o) if o.status.success() => {
String::from_utf8_lossy(&o.stdout).trim().to_string()
}
_ => String::new(),
}
}
fn get_linux_hostname(distro: &str) -> String {
let output = Command::new("wsl")
.args(["-d", distro, "--", "cat", "/etc/hostname"])
.output();
match output {
Ok(o) if o.status.success() => {
String::from_utf8_lossy(&o.stdout).trim().to_string()
}
_ => String::new(),
}
}
fn decode_wsl_output(bytes: &[u8]) -> String {
if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE {
let u16s: Vec<u16> = bytes[2..].chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
return String::from_utf16_lossy(&u16s);
}
if bytes.len() >= 4 && bytes[1] == 0 && bytes[3] == 0 {
let u16s: Vec<u16> = bytes.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
return String::from_utf16_lossy(&u16s);
}
String::from_utf8_lossy(bytes).to_string()
}