use crate::config::KopiConfig;
use crate::error::{KopiError, Result};
use crate::version::VersionRequest;
use log::{debug, info, warn};
use std::io::{self, Write};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, PartialEq)]
pub enum InstallationResult {
Installed,
UserDeclined,
AutoInstallDisabled,
}
pub struct AutoInstaller<'a> {
config: &'a KopiConfig,
}
impl<'a> AutoInstaller<'a> {
pub fn new(config: &'a KopiConfig) -> Self {
Self { config }
}
pub fn should_auto_install(&self) -> bool {
self.config.auto_install.enabled
}
pub fn prompt_user(&self, version_spec: &str) -> Result<bool> {
if !self.config.auto_install.prompt {
return Ok(true);
}
print!("JDK {version_spec} is not installed. Would you like to install it now? [Y/n] ");
io::stdout()
.flush()
.map_err(|e| KopiError::SystemError(e.to_string()))?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|e| KopiError::SystemError(e.to_string()))?;
let response = input.trim().to_lowercase();
Ok(response.is_empty() || response == "y" || response == "yes")
}
pub fn install_jdk(&self, version_request: &VersionRequest) -> Result<()> {
let version_spec = if let Some(dist) = &version_request.distribution {
format!("{}@{}", dist, version_request.version_pattern)
} else {
version_request.version_pattern.clone()
};
info!("Auto-installing JDK: {version_spec}");
let kopi_path = self.find_kopi_binary()?;
let mut cmd = std::process::Command::new(&kopi_path);
cmd.arg("install").arg(&version_spec);
let timeout_secs = self.config.auto_install.timeout_secs;
debug!("Auto-install timeout: {timeout_secs} seconds");
match self.execute_with_timeout(cmd, Duration::from_secs(timeout_secs)) {
Ok(status) if status.success() => {
info!("Successfully auto-installed {version_spec}");
Ok(())
}
Ok(status) => {
warn!("Auto-install failed with status: {status:?}");
Err(KopiError::SystemError(format!(
"Failed to install {version_spec}: command exited with status {status:?}"
)))
}
Err(e) => {
warn!("Auto-install error: {e}");
Err(e)
}
}
}
pub fn prompt_and_install(
&self,
version_request: &VersionRequest,
) -> Result<InstallationResult> {
if !self.should_auto_install() {
return Ok(InstallationResult::AutoInstallDisabled);
}
let user_approved = self.prompt_user(&version_request.version_pattern)?;
if user_approved {
println!("Installing JDK...");
self.install_jdk(version_request)?;
Ok(InstallationResult::Installed)
} else {
println!("Skipping installation.");
println!("You can install this JDK later with:");
println!(" kopi install {}", version_request.version_pattern);
Ok(InstallationResult::UserDeclined)
}
}
pub(crate) fn execute_with_timeout(
&self,
mut cmd: std::process::Command,
timeout: Duration,
) -> Result<std::process::ExitStatus> {
use std::thread;
use std::time::Instant;
let start = Instant::now();
let mut child = cmd
.spawn()
.map_err(|e| KopiError::SystemError(format!("Failed to spawn command: {e}")))?;
loop {
match child.try_wait() {
Ok(Some(status)) => return Ok(status),
Ok(None) => {
if start.elapsed() >= timeout {
let _ = child.kill();
return Err(KopiError::SystemError(format!(
"Installation timed out after {} seconds",
timeout.as_secs()
)));
}
thread::sleep(Duration::from_millis(100));
}
Err(e) => {
return Err(KopiError::SystemError(format!(
"Failed to wait for command: {e}"
)));
}
}
}
}
fn find_kopi_binary(&self) -> Result<PathBuf> {
let mut searched_paths = Vec::new();
let kopi_name = crate::platform::kopi_binary_name();
if let Ok(current_exe) = std::env::current_exe()
&& let Some(parent) = current_exe.parent()
{
#[cfg(target_os = "windows")]
{
if let Ok(shims_dir) = self.config.shims_dir()
&& parent == shims_dir
&& let Ok(bin_dir) = self.config.bin_dir()
{
let kopi_bin_path = bin_dir.join(kopi_name);
searched_paths.push(kopi_bin_path.display().to_string());
if kopi_bin_path.exists() {
return Ok(kopi_bin_path);
}
}
}
let kopi_path = parent.join(kopi_name);
searched_paths.push(kopi_path.display().to_string());
if kopi_path.exists() {
return Ok(kopi_path);
}
}
searched_paths.push("PATH".to_string());
if let Ok(kopi_in_path) = which::which(kopi_name) {
return Ok(kopi_in_path);
}
Err(KopiError::KopiNotFound {
searched_paths,
is_auto_install_context: true,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_config() -> KopiConfig {
let temp_dir = TempDir::new().unwrap();
unsafe {
std::env::remove_var("KOPI_STORAGE_MIN_DISK_SPACE_MB");
std::env::remove_var("KOPI_AUTO_INSTALL_TIMEOUT_SECS");
std::env::remove_var("KOPI_AUTO_INSTALL_ENABLED");
std::env::remove_var("KOPI_CACHE_TTL_HOURS");
}
let mut config = KopiConfig::new(temp_dir.path().to_path_buf()).unwrap();
config.auto_install.enabled = true;
config.auto_install.prompt = false;
config.auto_install.timeout_secs = 30;
config
}
#[test]
fn test_should_auto_install() {
let config = create_test_config();
let installer = AutoInstaller::new(&config);
assert!(installer.should_auto_install());
let mut config2 = config.clone();
config2.auto_install.enabled = false;
let installer2 = AutoInstaller::new(&config2);
assert!(!installer2.should_auto_install());
}
#[test]
fn test_prompt_user_no_prompt() {
let config = create_test_config();
let installer = AutoInstaller::new(&config);
assert!(installer.prompt_user("temurin@21").unwrap());
}
#[test]
fn test_find_kopi_binary_not_found() {
let config = create_test_config();
let installer = AutoInstaller::new(&config);
let result = installer.find_kopi_binary();
if let Err(e) = result {
match e {
KopiError::KopiNotFound {
searched_paths,
is_auto_install_context,
} => {
assert!(!searched_paths.is_empty());
assert!(is_auto_install_context);
}
_ => panic!("Expected KopiNotFound error"),
}
}
}
#[test]
fn test_execute_with_timeout() {
let config = create_test_config();
let installer = AutoInstaller::new(&config);
#[cfg(unix)]
let cmd = std::process::Command::new("true");
#[cfg(windows)]
let cmd = {
let mut c = std::process::Command::new("cmd");
c.args(["/c", "exit 0"]);
c
};
let result = installer.execute_with_timeout(cmd, Duration::from_secs(5));
assert!(result.is_ok());
assert!(result.unwrap().success());
}
#[test]
#[cfg(unix)]
fn test_execute_with_timeout_exceeds() {
let config = create_test_config();
let installer = AutoInstaller::new(&config);
let mut cmd = std::process::Command::new("sleep");
cmd.arg("10");
let result = installer.execute_with_timeout(cmd, Duration::from_secs(1));
assert!(result.is_err());
match result {
Err(KopiError::SystemError(msg)) => {
assert!(msg.contains("timed out") || msg.contains("Command execution timed out"));
}
_ => panic!("Expected timeout error"),
}
}
}