#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
use super::detect::{detect_distros, WslDistro};
use super::shell_config::{install_block, uninstall_block, ShellBlockConfig};
use std::path::{Path, PathBuf};
#[cfg(target_os = "windows")]
use std::time::Duration;
#[cfg(target_os = "windows")]
const WSL_DEP_INSTALL_TIMEOUT: Duration = Duration::from_secs(300);
#[cfg(target_os = "windows")]
const WSL_QUICK_CMD_TIMEOUT: Duration = Duration::from_secs(15);
pub use super::detect::decode_wsl_output;
#[derive(Debug, Clone)]
pub struct LinuxReleaseSpec {
pub repo: String,
pub tag: String,
pub asset_gnu: String,
pub asset_musl: String,
pub binaries: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct WslInstallConfig {
pub app_name: String,
pub shell_block: String,
pub linux_binary_path: Option<PathBuf>,
pub linux_binary_target: Option<String>,
pub auto_install_linux_release: Option<LinuxReleaseSpec>,
pub linux_binaries_to_remove: Vec<String>,
}
#[derive(Debug)]
pub struct DistroResult {
pub distro_name: String,
pub outcome: Result<Vec<String>, String>,
}
pub fn configure_all_distros(config: &WslInstallConfig) -> Vec<DistroResult> {
let distros = detect_distros();
distros
.into_iter()
.map(|distro| {
let name = distro.name.clone();
let outcome = configure_distro(&distro, config);
DistroResult {
distro_name: name,
outcome,
}
})
.collect()
}
pub fn unconfigure_all_distros(config: &WslInstallConfig) -> Vec<DistroResult> {
let distros = detect_distros();
distros
.into_iter()
.map(|distro| {
let name = distro.name.clone();
let outcome = unconfigure_distro(&distro, config);
DistroResult {
distro_name: name,
outcome,
}
})
.collect()
}
#[cfg(target_os = "windows")]
pub fn find_wsl_home(distro: &str) -> Option<PathBuf> {
let linux_home = find_linux_home(distro)?;
if linux_home.is_empty() {
return None;
}
for prefix in &[r"\\wsl$", r"\\wsl.localhost"] {
let win_path = format!(r"{}\{}{}", prefix, distro, linux_home.replace('/', r"\"));
let path = PathBuf::from(&win_path);
if path.exists() {
return Some(path);
}
}
None
}
#[cfg(not(target_os = "windows"))]
pub fn find_wsl_home(_distro: &str) -> Option<PathBuf> {
None
}
#[cfg(target_os = "windows")]
fn find_linux_home(distro: &str) -> Option<String> {
crate::internal::wsl::detect::linux_home(distro)
}
fn configure_distro(distro: &WslDistro, config: &WslInstallConfig) -> Result<Vec<String>, String> {
let home_path = distro
.home_path
.as_ref()
.ok_or_else(|| format!("could not find home directory for {}", distro.name))?;
let mut actions = Vec::new();
#[cfg(target_os = "windows")]
if let (Some(src), Some(target)) = (&config.linux_binary_path, &config.linux_binary_target) {
copy_linux_binary(home_path, src, target, &distro.name, &mut actions)?;
}
let block_config = ShellBlockConfig::new(&config.app_name, &config.shell_block);
inject_shell_configs(home_path, &block_config, &mut actions)?;
#[cfg(target_os = "windows")]
if let Some(release) = config.auto_install_linux_release.as_ref() {
install_linux_release(&distro.name, release, &mut actions)?;
}
Ok(actions)
}
fn unconfigure_distro(
distro: &WslDistro,
config: &WslInstallConfig,
) -> Result<Vec<String>, String> {
let home_path = distro
.home_path
.as_ref()
.ok_or_else(|| format!("could not find home directory for {}", distro.name))?;
let mut actions = Vec::new();
let block_config = ShellBlockConfig::new(&config.app_name, &config.shell_block);
for name in &[".bashrc", ".zshrc", ".profile"] {
let path = home_path.join(name);
if path.exists() {
match uninstall_block(&path, &block_config) {
Ok(crate::internal::wsl::shell_config::UninstallResult::Removed) => {
actions.push(format!("Removed block from {name}"));
}
Ok(crate::internal::wsl::shell_config::UninstallResult::NotPresent) => {}
Err(e) => {
return Err(format!("{name}: {e}"));
}
}
}
}
if let Some(target) = &config.linux_binary_target {
let binary_path = home_path.join(target);
if binary_path.exists() {
std::fs::remove_file(&binary_path).map_err(|e| format!("remove binary: {e}"))?;
actions.push(format!("Removed ~/{target}"));
}
}
#[cfg(target_os = "windows")]
if !config.linux_binaries_to_remove.is_empty() {
remove_linux_release_binaries(&distro.name, &config.linux_binaries_to_remove, &mut actions);
}
#[cfg(target_os = "windows")]
{
let app = &config.app_name;
let runtime_dir = home_path.join(format!(".{app}"));
if runtime_dir.exists() {
remove_runtime_files_in_distro(&distro.name, &runtime_dir, app, &mut actions);
}
}
Ok(actions)
}
#[cfg(target_os = "windows")]
fn remove_linux_release_binaries(
distro_name: &str,
binaries: &[String],
actions: &mut Vec<String>,
) {
let rm_args: Vec<String> = binaries
.iter()
.map(|b| format!("/usr/local/bin/{b}"))
.collect();
let script = format!(
"set -e\nfor b in {}; do sudo rm -f \"$b\" 2>/dev/null || true; done",
rm_args
.iter()
.map(|p| format!("'{p}'"))
.collect::<Vec<_>>()
.join(" ")
);
let mut cmd = std::process::Command::new("wsl");
cmd.args(["-d", distro_name, "-e", "bash", "-c", &script]);
match crate::internal::core::timeout::run_with_timeout(cmd, WSL_QUICK_CMD_TIMEOUT) {
Ok(crate::internal::core::timeout::TimeoutResult::Completed(output))
if output.status.success() =>
{
actions.push(format!(
"Removed {} from /usr/local/bin/",
binaries.join(", ")
));
}
Ok(crate::internal::core::timeout::TimeoutResult::Completed(output)) => {
let stderr = String::from_utf8_lossy(&output.stderr);
actions.push(format!(
"Warning: could not remove binaries from /usr/local/bin/ ({})",
stderr.lines().next().unwrap_or("unknown error")
));
}
Ok(crate::internal::core::timeout::TimeoutResult::TimedOut) => {
actions.push("Warning: timed out removing binaries from /usr/local/bin/".to_string());
}
Err(e) => {
actions.push(format!(
"Warning: could not launch wsl to remove binaries: {e}"
));
}
}
}
#[cfg(target_os = "windows")]
fn remove_runtime_files_in_distro(
distro_name: &str,
runtime_dir: &Path,
app_name: &str,
actions: &mut Vec<String>,
) {
let dir = runtime_dir.display().to_string();
let script = format!(
"set -e\n\
pkill -TERM -x {app_name}-agent 2>/dev/null || true\n\
sleep 0.3\n\
rm -f '{dir}/agent.sock' '{dir}/agent.pid'\n\
rmdir '{dir}' 2>/dev/null || true"
);
let mut cmd = std::process::Command::new("wsl");
cmd.args(["-d", distro_name, "-e", "bash", "-c", &script]);
drop(crate::internal::core::timeout::run_with_timeout(
cmd,
WSL_QUICK_CMD_TIMEOUT,
));
actions.push(format!("Cleaned up runtime files in ~/.{app_name}/"));
}
fn inject_shell_configs(
home_path: &Path,
block_config: &ShellBlockConfig,
actions: &mut Vec<String>,
) -> Result<(), String> {
let mut configured = false;
let bashrc = home_path.join(".bashrc");
if bashrc.exists() {
match install_block(&bashrc, block_config) {
Ok(crate::internal::wsl::shell_config::InstallResult::Installed) => {
actions.push("Updated .bashrc".to_string());
configured = true;
}
Ok(crate::internal::wsl::shell_config::InstallResult::AlreadyPresent) => {
configured = true;
}
Err(e) => return Err(format!(".bashrc: {e}")),
}
}
let zshrc = home_path.join(".zshrc");
if zshrc.exists() {
match install_block(&zshrc, block_config) {
Ok(crate::internal::wsl::shell_config::InstallResult::Installed) => {
actions.push("Updated .zshrc".to_string());
configured = true;
}
Ok(crate::internal::wsl::shell_config::InstallResult::AlreadyPresent) => {
configured = true;
}
Err(e) => return Err(format!(".zshrc: {e}")),
}
}
if !configured {
let profile = home_path.join(".profile");
if profile.exists() {
match install_block(&profile, block_config) {
Ok(crate::internal::wsl::shell_config::InstallResult::Installed) => {
actions.push("Updated .profile".to_string());
}
Ok(crate::internal::wsl::shell_config::InstallResult::AlreadyPresent) => {}
Err(e) => return Err(format!(".profile: {e}")),
}
} else {
match install_block(&bashrc, block_config) {
Ok(_) => {
actions.push("Created .bashrc".to_string());
}
Err(e) => return Err(format!("create .bashrc: {e}")),
}
}
}
Ok(())
}
#[cfg(target_os = "windows")]
fn copy_linux_binary(
home_path: &Path,
src: &Path,
target: &str,
distro_name: &str,
actions: &mut Vec<String>,
) -> Result<(), String> {
let dest = home_path.join(target);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create directory {}: {e}", parent.display()))?;
}
std::fs::copy(src, &dest).map_err(|e| format!("copy binary: {e}"))?;
if let Some(linux_home) = find_linux_home(distro_name) {
let linux_path = format!("{linux_home}/{target}");
let mut cmd = std::process::Command::new("wsl");
cmd.args(["-d", distro_name, "-e", "chmod", "+x", &linux_path]);
drop(crate::internal::core::timeout::run_status_with_timeout(
cmd,
WSL_QUICK_CMD_TIMEOUT,
));
}
actions.push(format!("Installed binary to ~/{target}"));
Ok(())
}
#[cfg(target_os = "windows")]
fn distro_is_glibc(distro_name: &str) -> bool {
let mut wsl = std::process::Command::new("wsl");
wsl.args(["-d", distro_name, "-e", "ldd", "--version"]);
match crate::internal::core::timeout::run_with_timeout(wsl, WSL_QUICK_CMD_TIMEOUT) {
Ok(crate::internal::core::timeout::TimeoutResult::Completed(o)) => {
let stdout = String::from_utf8_lossy(&o.stdout);
let stderr = String::from_utf8_lossy(&o.stderr);
let combined = format!("{stdout}{stderr}");
combined.contains("GNU libc") || combined.contains("Free Software Foundation")
}
_ => false, }
}
#[cfg(target_os = "windows")]
fn install_linux_release(
distro_name: &str,
spec: &LinuxReleaseSpec,
actions: &mut Vec<String>,
) -> Result<(), String> {
let asset = if distro_is_glibc(distro_name) {
&spec.asset_gnu
} else {
&spec.asset_musl
};
let url = format!(
"https://github.com/{}/releases/download/{}/{}",
spec.repo, spec.tag, asset
);
let bins = spec.binaries.join(" ");
let script = format!(
"set -e\n\
work=/tmp/sshenc-install-$$\n\
rm -rf \"$work\"\n\
mkdir -p \"$work\"\n\
cd \"$work\"\n\
trap 'rm -rf \"$work\"' EXIT\n\
curl -fsSL '{url}' -o release.tar.gz\n\
tar xzf release.tar.gz\n\
for b in {bins}; do\n\
sudo cp \"$b\" \"/usr/local/bin/$b.new\"\n\
sudo chmod +x \"/usr/local/bin/$b.new\"\n\
sudo mv \"/usr/local/bin/$b.new\" \"/usr/local/bin/$b\"\n\
done\n\
pkill -KILL -x sshenc-agent 2>/dev/null || true\n"
);
let mut cmd = std::process::Command::new("wsl");
cmd.args(["-d", distro_name, "-e", "bash", "-c", &script]);
match crate::internal::core::timeout::run_with_timeout(cmd, WSL_DEP_INSTALL_TIMEOUT) {
Ok(crate::internal::core::timeout::TimeoutResult::Completed(output))
if output.status.success() =>
{
actions.push(format!(
"Installed {} from {} {}",
spec.binaries.join(", "),
spec.repo,
spec.tag
));
Ok(())
}
Ok(crate::internal::core::timeout::TimeoutResult::TimedOut) => {
actions.push(format!(
"Warning: {} install timed out after {}s",
spec.repo,
WSL_DEP_INSTALL_TIMEOUT.as_secs()
));
Ok(())
}
Ok(crate::internal::core::timeout::TimeoutResult::Completed(output)) => {
let stderr = String::from_utf8_lossy(&output.stderr);
let tail: Vec<&str> = stderr
.lines()
.rev()
.filter(|l| !l.trim().is_empty())
.take(3)
.collect();
let detail: String = tail.into_iter().rev().collect::<Vec<_>>().join(" / ");
let exit = output
.status
.code()
.map(|c| format!("exit {c}"))
.unwrap_or_else(|| "signaled".to_string());
actions.push(format!(
"Warning: failed to install {} from {} ({}: {})",
spec.binaries.join(", "),
url,
exit,
if detail.is_empty() {
"no stderr"
} else {
detail.as_str()
}
));
Ok(())
}
Err(e) => {
actions.push(format!(
"Warning: failed to launch wsl install for {} ({e})",
spec.binaries.join(", "),
));
Ok(())
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic, let_underscore_drop)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
fn test_dir(name: &str) -> PathBuf {
let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
let dir =
std::env::temp_dir().join(format!("enclaveapp-wsl-install-test-{pid}-{id}-{name}"));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn test_inject_shell_configs_bashrc() {
let dir = test_dir("inject-bashrc");
std::fs::write(dir.join(".bashrc"), "# existing\n").unwrap();
let config = ShellBlockConfig::new("testapp", "export FOO=bar");
let mut actions = Vec::new();
inject_shell_configs(&dir, &config, &mut actions).unwrap();
assert!(!actions.is_empty());
let content = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
assert!(content.contains("BEGIN testapp managed block"));
assert!(content.contains("export FOO=bar"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_inject_shell_configs_zshrc() {
let dir = test_dir("inject-zshrc");
std::fs::write(dir.join(".zshrc"), "# zsh config\n").unwrap();
let config = ShellBlockConfig::new("testapp", "export BAR=baz");
let mut actions = Vec::new();
inject_shell_configs(&dir, &config, &mut actions).unwrap();
assert!(actions.iter().any(|a| a.contains(".zshrc")));
let content = std::fs::read_to_string(dir.join(".zshrc")).unwrap();
assert!(content.contains("export BAR=baz"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_inject_shell_configs_both() {
let dir = test_dir("inject-both");
std::fs::write(dir.join(".bashrc"), "# bash\n").unwrap();
std::fs::write(dir.join(".zshrc"), "# zsh\n").unwrap();
let config = ShellBlockConfig::new("testapp", "export X=1");
let mut actions = Vec::new();
inject_shell_configs(&dir, &config, &mut actions).unwrap();
assert!(actions.iter().any(|a| a.contains(".bashrc")));
assert!(actions.iter().any(|a| a.contains(".zshrc")));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_inject_shell_configs_fallback_profile() {
let dir = test_dir("inject-profile");
std::fs::write(dir.join(".profile"), "# profile\n").unwrap();
let config = ShellBlockConfig::new("testapp", "export Y=2");
let mut actions = Vec::new();
inject_shell_configs(&dir, &config, &mut actions).unwrap();
assert!(actions.iter().any(|a| a.contains(".profile")));
let content = std::fs::read_to_string(dir.join(".profile")).unwrap();
assert!(content.contains("export Y=2"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_inject_shell_configs_creates_bashrc() {
let dir = test_dir("inject-create");
let config = ShellBlockConfig::new("testapp", "export Z=3");
let mut actions = Vec::new();
inject_shell_configs(&dir, &config, &mut actions).unwrap();
assert!(actions.iter().any(|a| a.contains(".bashrc")));
let content = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
assert!(content.contains("export Z=3"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_inject_idempotent() {
let dir = test_dir("inject-idempotent");
std::fs::write(dir.join(".bashrc"), "# existing\n").unwrap();
let config = ShellBlockConfig::new("testapp", "export A=1");
let mut actions1 = Vec::new();
inject_shell_configs(&dir, &config, &mut actions1).unwrap();
let content1 = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
let mut actions2 = Vec::new();
inject_shell_configs(&dir, &config, &mut actions2).unwrap();
let content2 = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
assert_eq!(content1, content2);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_unconfigure_distro_removes_blocks() {
let dir = test_dir("unconfigure");
std::fs::write(dir.join(".bashrc"), "# before\n").unwrap();
let block_config = ShellBlockConfig::new("testapp", "export Q=1");
install_block(dir.join(".bashrc").as_path(), &block_config).unwrap();
let distro = WslDistro {
name: "TestDistro".to_string(),
home_path: Some(dir.clone()),
};
let config = WslInstallConfig {
app_name: "testapp".to_string(),
shell_block: "export Q=1".to_string(),
auto_install_linux_release: None,
linux_binary_path: None,
linux_binary_target: None,
linux_binaries_to_remove: vec![],
};
let result = unconfigure_distro(&distro, &config).unwrap();
assert!(result.iter().any(|a| a.contains("Removed")));
let content = std::fs::read_to_string(dir.join(".bashrc")).unwrap();
assert!(!content.contains("BEGIN testapp managed block"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_decode_wsl_output_utf8() {
let input = b"Ubuntu\nDebian\n";
let result = decode_wsl_output(input);
assert_eq!(result, "Ubuntu\nDebian\n");
}
#[test]
fn test_decode_wsl_output_utf16le_bom() {
let mut bytes = vec![0xFF_u8, 0xFE]; for ch in "Hi\n".encode_utf16() {
bytes.extend_from_slice(&ch.to_le_bytes());
}
let result = decode_wsl_output(&bytes);
assert_eq!(result, "Hi\n");
}
#[test]
fn test_find_wsl_home_non_windows() {
#[cfg(not(target_os = "windows"))]
assert!(find_wsl_home("Ubuntu").is_none());
}
#[test]
fn test_distro_result_debug() {
let result = DistroResult {
distro_name: "Ubuntu".to_string(),
outcome: Ok(vec!["Updated .bashrc".to_string()]),
};
let debug_str = format!("{result:?}");
assert!(debug_str.contains("Ubuntu"));
}
#[test]
fn test_wsl_install_config_clone() {
let config = WslInstallConfig {
app_name: "test".to_string(),
shell_block: "# test".to_string(),
auto_install_linux_release: None,
linux_binary_path: None,
linux_binary_target: None,
linux_binaries_to_remove: vec![],
};
let cloned = config.clone();
assert_eq!(cloned.app_name, config.app_name);
assert_eq!(cloned.shell_block, config.shell_block);
}
#[test]
fn test_decode_wsl_output_real_utf16le_bom() {
let text = "Ubuntu\r\n";
let mut bytes = vec![0xFF_u8, 0xFE]; for ch in text.encode_utf16() {
bytes.extend_from_slice(&ch.to_le_bytes());
}
let result = decode_wsl_output(&bytes);
assert_eq!(result, "Ubuntu\r\n");
}
#[test]
fn test_decode_wsl_output_plain_utf8() {
let input = b"Debian GNU/Linux\n";
let result = decode_wsl_output(input);
assert_eq!(result, "Debian GNU/Linux\n");
}
#[test]
fn test_configure_distro_creates_backup_like_file() {
let dir = test_dir("configure-backup");
let bashrc = dir.join(".bashrc");
std::fs::write(&bashrc, "# original content\nexport PATH=/usr/bin\n").unwrap();
let original_content = std::fs::read_to_string(&bashrc).unwrap();
let distro = WslDistro {
name: "TestDistro".to_string(),
home_path: Some(dir.clone()),
};
let config = WslInstallConfig {
app_name: "testapp".to_string(),
shell_block: "export TEST=1".to_string(),
auto_install_linux_release: None,
linux_binary_path: None,
linux_binary_target: None,
linux_binaries_to_remove: vec![],
};
let result = configure_distro(&distro, &config).unwrap();
assert!(!result.is_empty());
let new_content = std::fs::read_to_string(&bashrc).unwrap();
assert!(new_content.contains("BEGIN testapp managed block"));
assert!(new_content.contains(&original_content.trim_end().to_string()));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_unconfigure_distro_removes_block_but_keeps_content() {
let dir = test_dir("unconfigure-keep");
let bashrc = dir.join(".bashrc");
std::fs::write(&bashrc, "# my config\nexport FOO=bar\n").unwrap();
let block_config = ShellBlockConfig::new("testapp", "export Q=1");
install_block(bashrc.as_path(), &block_config).unwrap();
let content = std::fs::read_to_string(&bashrc).unwrap();
assert!(content.contains("BEGIN testapp managed block"));
let distro = WslDistro {
name: "TestDistro".to_string(),
home_path: Some(dir.clone()),
};
let config = WslInstallConfig {
app_name: "testapp".to_string(),
shell_block: "export Q=1".to_string(),
auto_install_linux_release: None,
linux_binary_path: None,
linux_binary_target: None,
linux_binaries_to_remove: vec![],
};
let result = unconfigure_distro(&distro, &config).unwrap();
assert!(result.iter().any(|a| a.contains("Removed")));
let final_content = std::fs::read_to_string(&bashrc).unwrap();
assert!(!final_content.contains("BEGIN testapp managed block"));
assert!(final_content.contains("# my config"));
assert!(final_content.contains("export FOO=bar"));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn test_decode_wsl_output_utf16le_multiple_lines() {
let text = "Ubuntu\nDebian\n";
let mut bytes = vec![0xFF_u8, 0xFE];
for ch in text.encode_utf16() {
bytes.extend_from_slice(&ch.to_le_bytes());
}
let result = decode_wsl_output(&bytes);
assert_eq!(result, "Ubuntu\nDebian\n");
}
#[test]
fn test_decode_wsl_output_empty_utf8() {
let result = decode_wsl_output(b"");
assert_eq!(result, "");
}
}