use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::config::{Result, TestbedError};
use crate::providers::ProviderId;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VmState {
pub profile_name: String,
pub disk_path: String,
pub pid: Option<u32>,
pub provider_id: ProviderId,
pub provider_internal_id: String,
pub monitor_socket: String,
pub ssh_port: u16,
pub winrm_port: Option<u16>,
pub rdp_port: Option<u16>,
pub vnc_port: u16,
pub bootstrapped: bool,
pub created_at: String,
}
pub fn save(state: &VmState) -> Result<()> {
let path = state_path(&state.profile_name);
let json = serde_json::to_string_pretty(state).map_err(|e| TestbedError::Qcow2Error {
message: format!("serializing state: {e}"),
})?;
std::fs::write(&path, json).map_err(|e| TestbedError::Qcow2Error {
message: format!("writing state to {path:?}: {e}"),
})?;
Ok(())
}
pub fn load(name: &str) -> Result<VmState> {
let path = state_path(name);
if !path.exists() {
return Err(TestbedError::VmNotRunning {
name: name.to_string(),
});
}
let json = std::fs::read_to_string(&path).map_err(|e| TestbedError::Qcow2Error {
message: format!("reading state from {path:?}: {e}"),
})?;
let mut state: VmState = serde_json::from_str(&json).map_err(|e| TestbedError::Qcow2Error {
message: format!("parsing state: {e}"),
})?;
if let Some(pid) = state.pid
&& !is_process_alive(pid) {
state.pid = None; save(&state)?;
}
Ok(state)
}
pub fn exists(name: &str) -> bool {
state_path(name).exists()
}
pub fn delete(name: &str) -> Result<()> {
let path = state_path(name);
if path.exists() {
std::fs::remove_file(&path).map_err(|e| TestbedError::Qcow2Error {
message: format!("deleting state {path:?}: {e}"),
})?;
}
Ok(())
}
pub fn list() -> Result<Vec<(String, VmState)>> {
let dir = state_dir();
if !dir.exists() {
return Ok(Vec::new());
}
let mut vms = Vec::new();
for entry in std::fs::read_dir(&dir).map_err(|e| TestbedError::Qcow2Error {
message: format!("reading state dir {dir:?}: {e}"),
})? {
let entry = entry.map_err(|e| TestbedError::Qcow2Error {
message: format!("reading state entry: {e}"),
})?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("json")
&& let Ok(json) = std::fs::read_to_string(&path)
&& let Ok(state) = serde_json::from_str::<VmState>(&json) {
vms.push((state.profile_name.clone(), state));
}
}
Ok(vms)
}
pub fn is_running(name: &str) -> bool {
if let Ok(state) = load(name)
&& let Some(pid) = state.pid {
return is_process_alive(pid);
}
false
}
pub fn from_runtime(
profile_name: &str,
disk_path: &std::path::Path,
provider_id: ProviderId,
provider_internal_id: &str,
ssh_port: u16,
winrm_port: Option<u16>,
rdp_port: Option<u16>,
vnc_port: u16,
bootstrapped: bool,
) -> VmState {
VmState {
profile_name: profile_name.to_string(),
disk_path: disk_path.to_string_lossy().to_string(),
pid: None, provider_id,
provider_internal_id: provider_internal_id.to_string(),
monitor_socket: String::new(),
ssh_port,
winrm_port,
rdp_port,
vnc_port,
bootstrapped,
created_at: format!("{}T{}",
chrono::Utc::now().date_naive(),
chrono::Utc::now().time()
),
}
}
pub fn from_qemu(
profile_name: &str,
disk_path: &std::path::Path,
pid: u32,
monitor_path: &std::path::Path,
ssh_port: u16,
winrm_port: Option<u16>,
rdp_port: Option<u16>,
vnc_port: u16,
bootstrapped: bool,
) -> VmState {
VmState {
profile_name: profile_name.to_string(),
disk_path: disk_path.to_string_lossy().to_string(),
pid: Some(pid),
provider_id: crate::providers::ProviderId::Qemu,
provider_internal_id: pid.to_string(),
monitor_socket: monitor_path.to_string_lossy().to_string(),
ssh_port,
winrm_port,
rdp_port,
vnc_port,
bootstrapped,
created_at: format!("{}T{}",
chrono::Utc::now().date_naive(),
chrono::Utc::now().time()
),
}
}
fn state_path(name: &str) -> PathBuf {
state_dir().join(format!("{name}.json"))
}
pub fn state_dir() -> PathBuf {
crate::config::state_dir()
}
pub fn is_process_alive(pid: u32) -> bool {
#[cfg(unix)]
{
unsafe { libc::kill(pid as i32, 0) == 0 }
}
#[cfg(not(unix))]
{
let _ = pid;
false
}
}