use std::path::Path;
use color_eyre::{Result, eyre};
use clap::ArgMatches;
use super::session::{discover_vm_session, is_process_alive, cleanup_vm_session_files, vm_session_file_path};
pub fn cmd_vm_stop(_args: &ArgMatches) -> Result<()> {
let cfg = crate::models::config();
let env_name = cfg.common.env_name.clone();
let _env_root = if cfg.common.env_root.is_empty() {
crate::dirs::get_env_root(env_name.clone())?
} else {
std::path::PathBuf::from(&cfg.common.env_root)
};
let session = discover_vm_session(&env_name)?
.ok_or_else(|| eyre::eyre!("No VM running for {}", env_name))?;
log::info!("Stopping VM for {} (PID {}, backend={})", env_name, session.daemon_pid, session.backend);
if let Err(e) = send_shutdown_to_guest(&session.socket_path, &session.backend) {
log::warn!("Failed to send shutdown to guest: {}", e);
}
#[cfg(unix)]
{
use nix::sys::signal::{kill, Signal};
let pid = nix::unistd::Pid::from_raw(session.daemon_pid as i32);
let _ = kill(pid, Signal::SIGTERM);
}
#[cfg(windows)]
{
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, false, session.daemon_pid);
if let Ok(h) = handle {
let _ = TerminateProcess(h, 1);
let _ = CloseHandle(h);
}
}
}
let start = std::time::Instant::now();
while start.elapsed() < std::time::Duration::from_millis(500) {
if !is_process_alive(session.daemon_pid) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
#[cfg(unix)]
if is_process_alive(session.daemon_pid) {
use nix::sys::signal::{kill, Signal};
let pid = nix::unistd::Pid::from_raw(session.daemon_pid as i32);
let _ = kill(pid, Signal::SIGKILL);
std::thread::sleep(std::time::Duration::from_millis(50));
}
#[cfg(windows)]
if is_process_alive(session.daemon_pid) {
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, false, session.daemon_pid);
if let Ok(h) = handle {
let _ = TerminateProcess(h, 1);
let _ = CloseHandle(h);
}
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
let session_file = vm_session_file_path(&env_name);
cleanup_vm_session_files(&session_file, &session.socket_path);
println!("VM stopped for {}", env_name);
Ok(())
}
fn send_shutdown_to_guest(socket_path: &Path, _backend: &str) -> Result<()> {
#[cfg(target_os = "linux")]
{
let socket_str = socket_path.to_string_lossy();
if socket_str.starts_with("vsock:") {
send_shutdown_via_vsock(&socket_str)?;
return Ok(());
}
}
#[cfg(all(target_os = "linux", feature = "libkrun"))]
{
use std::io::Write;
let mut stream = std::os::unix::net::UnixStream::connect(socket_path)?;
let request = super::client::build_command_request(
&[crate::run::VM_SESSION_DONE_CMD.to_string()],
crate::models::IoMode::Stream,
None,
None,
);
let request_json = serde_json::Value::Object(request);
writeln!(stream, "{}", request_json)?;
log::debug!("Sent {} to guest vm_daemon via Unix socket", crate::run::VM_SESSION_DONE_CMD);
Ok(())
}
#[cfg(all(windows, feature = "libkrun"))]
{
use std::io::Write;
let mut stream = crate::libkrun::bridge::connect_vsock_bridge(socket_path, crate::libkrun::bridge::VSOCK_BRIDGE_MAX_RETRIES)?;
let request = serde_json::json!({
"command": [crate::run::VM_SESSION_DONE_CMD],
"cwd": null,
"env": {},
"stdin": "",
"pty": false,
});
writeln!(stream, "{}", request)?;
log::debug!("Sent {} to guest vm_daemon via named pipe", crate::run::VM_SESSION_DONE_CMD);
Ok(())
}
#[cfg(all(target_os = "macos", feature = "libkrun"))]
{
use std::io::Write;
let mut stream = std::os::unix::net::UnixStream::connect(socket_path)?;
let request = serde_json::json!({
"command": [crate::run::VM_SESSION_DONE_CMD],
"cwd": null,
"env": {},
"stdin": "",
"pty": false,
});
writeln!(stream, "{}", request)?;
log::debug!("Sent {} to guest vm_daemon via Unix socket", crate::run::VM_SESSION_DONE_CMD);
Ok(())
}
#[cfg(not(feature = "libkrun"))]
{
let socket_str = socket_path.to_string_lossy();
log::warn!("Unknown socket type for shutdown: {}", socket_str);
Ok(())
}
}
#[cfg(target_os = "linux")]
fn send_shutdown_via_vsock(socket_str: &str) -> Result<()> {
use nix::sys::socket::{socket, connect, AddressFamily, SockType, SockFlag, VsockAddr};
use std::os::fd::{IntoRawFd, FromRawFd};
let cid: u32 = socket_str
.strip_prefix("vsock:")
.and_then(|s| s.parse().ok())
.ok_or_else(|| eyre::eyre!("Invalid vsock address: {}", socket_str))?;
const VM_DAEMON_PORT: u32 = 10000;
log::debug!("Sending shutdown via vsock to CID {} port {}", cid, VM_DAEMON_PORT);
let fd = socket(
AddressFamily::Vsock,
SockType::Stream,
SockFlag::SOCK_CLOEXEC,
None,
).map_err(|e| eyre::eyre!("Failed to create vsock socket: {}", e))?;
let raw_fd = fd.into_raw_fd();
let addr = VsockAddr::new(cid, VM_DAEMON_PORT);
connect(raw_fd, &addr)
.map_err(|e| eyre::eyre!("Failed to connect to vsock CID {} port {}: {}", cid, VM_DAEMON_PORT, e))?;
use std::io::Write;
let mut stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(raw_fd) };
let request = super::client::build_command_request(
&[crate::run::VM_SESSION_DONE_CMD.to_string()],
crate::models::IoMode::Stream,
None,
None,
);
let request_json = serde_json::Value::Object(request);
writeln!(stream, "{}", request_json)?;
log::debug!("Sent {} to guest vm_daemon via vsock", crate::run::VM_SESSION_DONE_CMD);
let _ = stream.shutdown(std::net::Shutdown::Both);
Ok(())
}