use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use clap::ArgMatches;
use crate::bootstrap::BootstrapLogger;
use crate::config::{get_profile, DisplayMode, GuestOs};
use crate::providers::{default_provider, ResolvedPorts};
use crate::state;
use crate::winrm::WinRM;
type BoxedError = Box<dyn std::error::Error + Send + Sync>;
fn resolve_profile(name: &str) -> std::result::Result<crate::config::VmProfile, BoxedError> {
get_profile(name).map(|p| p.clone()).map_err(|e| Box::new(e) as BoxedError)
}
fn ensure_image(profile: &crate::config::VmProfile) -> Result<(), BoxedError> {
let cache_path = profile.image_cache_path();
if cache_path.exists() {
return Ok(());
}
eprintln!(" Image not cached, importing...");
let provider = default_provider()?;
let path = provider.ensure_image(profile)
.map_err(|e| format!("Failed to import image: {e}"))?;
eprintln!(" Image imported at {}", path.display());
Ok(())
}
fn ensure_vm_running(profile: &crate::config::VmProfile) -> Result<ResolvedPorts, BoxedError> {
if state::is_running(&profile.name) {
let vm_state = state::load(&profile.name)
.expect("is_running returned true but load failed");
eprintln!(" VM '{}' already running (PID {:?})", profile.name, vm_state.pid);
return Ok(ResolvedPorts {
ssh_port: vm_state.ssh_port,
winrm_port: vm_state.winrm_port,
rdp_port: vm_state.rdp_port,
vnc_port: vm_state.vnc_port,
});
}
eprintln!(" VM '{}' not running, launching...", profile.name);
let provider = default_provider()?;
let handle = provider.launch(profile, DisplayMode::Headless)
.map_err(|e| format!("Failed to launch VM: {e}"))?;
let ports = handle.resolved_ports.clone();
drop(handle);
eprintln!(" VM launched: SSH={}, VNC={}, WinRM={:?}",
ports.ssh_port, ports.vnc_port, ports.winrm_port);
Ok(ports)
}
fn ensure_bootstrapped(
profile: &crate::config::VmProfile,
ports: &ResolvedPorts,
) -> Result<(), BoxedError> {
if crate::bootstrap::is_bootstrapped(profile) {
eprintln!(" VM already bootstrapped, skipping.");
return Ok(());
}
eprintln!(" Bootstrapping VM...");
let logger = Arc::new(
BootstrapLogger::new(&profile.name)
.map_err(|e| format!("Failed to create bootstrap logger: {e}"))?,
);
match profile.os {
GuestOs::Linux | GuestOs::MacOS => {
let mut session = crate::ssh::connect_from_port(ports.ssh_port, &profile.user, profile.os)
.map_err(|e| format!("SSH connection failed: {e}"))?;
crate::bootstrap::bootstrap(profile, &mut session, &logger, None)?;
}
GuestOs::Windows => {
bootstrap_windows(profile, &logger, ports)?;
}
}
if let Ok(mut state) = state::load(&profile.name) {
state.bootstrapped = true;
let _ = state::save(&state);
}
eprintln!(" Bootstrap complete.");
Ok(())
}
fn bootstrap_windows(
profile: &crate::config::VmProfile,
logger: &BootstrapLogger,
ports: &ResolvedPorts,
) -> Result<(), BoxedError> {
use crate::winrm::elevated::ProgressCallback;
use std::time::Instant;
let winrm_port = ports.winrm_port
.ok_or_else(|| format!("No WinRM port configured for '{}'", profile.name))?;
eprintln!(" Waiting for WinRM on port {winrm_port}...");
let start = Instant::now();
let timeout = Duration::from_secs(300);
loop {
if start.elapsed() > timeout {
return Err(format!("Timed out waiting for WinRM after {:?}", timeout).into());
}
let winrm = WinRM::new("127.0.0.1", winrm_port, profile.user, profile.pass);
if winrm.ping() {
break;
}
std::thread::sleep(Duration::from_secs(5));
}
eprintln!(" WinRM ready");
let winrm = WinRM::new("127.0.0.1", winrm_port, profile.user, profile.pass);
let progress_cb: ProgressCallback<'_> = Some(&|msg: &str| {
eprintln!(" {}", msg);
});
crate::bootstrap::windows::bootstrap_windows_winrm_phase(profile, &winrm, logger, progress_cb)?;
eprintln!(" Waiting for SSH on port {}...", ports.ssh_port);
let start = Instant::now();
let timeout = Duration::from_secs(120);
loop {
if start.elapsed() > timeout {
return Err("Timed out waiting for SSH".into());
}
if crate::ssh::connect_from_port(ports.ssh_port, &profile.user, profile.os).is_ok() {
break;
}
std::thread::sleep(Duration::from_secs(5));
}
eprintln!(" SSH ready");
let mut session = crate::ssh::connect_from_port(ports.ssh_port, &profile.user, profile.os)
.map_err(|e| format!("SSH connection failed: {e}"))?;
crate::bootstrap::windows::bootstrap_windows_ssh_phase(profile, &winrm, &mut session, logger, progress_cb, false)?;
Ok(())
}
pub fn cmd_build(args: &ArgMatches) -> std::result::Result<(), BoxedError> {
let name = args.get_one::<String>("profile").unwrap();
let project_dir = args.get_one::<String>("project").unwrap();
let _target_override = args.get_one::<String>("target");
let no_artifacts = args.get_flag("no-artifacts");
let profile = resolve_profile(name)?;
let project_path = PathBuf::from(project_dir);
if !project_path.exists() {
return Err(format!("Project directory does not exist: {}", project_path.display()).into());
}
if !project_path.is_dir() {
return Err(format!("Project path is not a directory: {}", project_path.display()).into());
}
ensure_image(&profile)?;
let ports = ensure_vm_running(&profile)?;
ensure_bootstrapped(&profile, &ports)?;
eprintln!("Connecting to VM via SSH...");
let mut session = crate::ssh::connect_from_port(ports.ssh_port, &profile.user, profile.os)
.map_err(|e| format!("SSH connection failed: {e}"))?;
eprintln!("Building project in VM...");
eprintln!(" Project: {}", project_path.display());
let artifact_dir = match crate::build::build_in_vm(&profile, &mut session, &project_path) {
Ok(dir) => dir,
Err(crate::config::TestbedError::BuildFailed { target, code }) => {
eprintln!("\nBuild failed for target {} (exit code: {})", target, code);
eprintln!("Check logs with: testbed logs {} --errors", name);
return Err(format!("Build failed with exit code {}", code).into());
}
Err(e) => {
return Err(format!("Build error: {e}").into());
}
};
if no_artifacts {
eprintln!("\nBuild complete. Artifacts left in VM (--no-artifacts specified).");
} else {
eprintln!("\nBuild complete. Artifacts retrieved to: {}", artifact_dir.display());
if let Ok(entries) = std::fs::read_dir(&artifact_dir) {
eprintln!("\nArtifacts:");
for entry in entries.flatten() {
if let Ok(meta) = entry.metadata() {
let size_mb = meta.len() as f64 / 1_048_576.0;
eprintln!(" {:.2} MB {}", size_mb, entry.file_name().to_string_lossy());
}
}
}
}
Ok(())
}