Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 97 additions & 14 deletions src/cli/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,13 @@ pub struct StartArgs {

#[derive(Args)]
pub struct StopArgs {
/// VM name
pub name: String,
/// VM name (required unless --all is used)
#[arg(required_unless_present = "all")]
pub name: Option<String>,

/// Stop all running VMs
#[arg(long, conflicts_with = "name")]
pub all: bool,

/// Force stop (SIGKILL)
#[arg(long)]
Expand Down Expand Up @@ -181,8 +186,13 @@ pub struct UpdateConfigArgs {

#[derive(Args)]
pub struct DeleteArgs {
/// VM name
pub name: String,
/// VM name (required unless --all is used)
#[arg(required_unless_present = "all")]
pub name: Option<String>,

/// Delete all VMs
#[arg(long, conflicts_with = "name")]
pub all: bool,

/// Force delete (kill if running)
#[arg(long)]
Expand Down Expand Up @@ -777,15 +787,20 @@ fn start(args: &StartArgs, state_dir: &Path) -> anyhow::Result<()> {
/// if --force) → wait for exit → SIGKILL if still alive → clean up network +
/// socket → update metadata.
fn stop(args: &StopArgs, state_dir: &Path) -> anyhow::Result<()> {
if args.all {
return stop_all(args.force, state_dir);
}

let name = args.name.as_deref().unwrap();
let store = StateStore::new(state_dir.to_path_buf());

// Load and validate VM state.
let mut metadata = vm::load(&store, &args.name)?;
let mut metadata = vm::load(&store, name)?;
match metadata.status {
VmStatus::Running | VmStatus::Paused => {}
_ => {
return Err(Error::VmWrongState {
name: args.name.clone(),
name: name.to_string(),
actual: metadata.status.to_string(),
expected: "running or paused".to_string(),
}
Expand All @@ -797,9 +812,9 @@ fn stop(args: &StopArgs, state_dir: &Path) -> anyhow::Result<()> {
anyhow::anyhow!(
"vm '{}' is {} but has no PID — state may be corrupted\n\
Hint: try 'ember vm delete --force {}' and recreate the VM",
args.name,
name,
metadata.status,
args.name
name
)
})?;

Expand All @@ -810,7 +825,7 @@ fn stop(args: &StopArgs, state_dir: &Path) -> anyhow::Result<()> {
println!("Force-stopping VM (pid {pid})...");
Vm::force_stop(&metadata)?;
} else {
println!("Stopping VM '{}'...", args.name);
println!("Stopping VM '{}'...", name);
Vm::stop(&metadata)?;
}

Expand All @@ -824,7 +839,36 @@ fn stop(args: &StopArgs, state_dir: &Path) -> anyhow::Result<()> {
metadata.network = None;
vm::save(&store, &metadata)?;

println!("VM '{}' stopped.", args.name);
println!("VM '{}' stopped.", name);
Ok(())
}

/// Stop all running/paused VMs.
fn stop_all(force: bool, state_dir: &Path) -> anyhow::Result<()> {
let store = StateStore::new(state_dir.to_path_buf());
let vms = vm::list(&store)?;
let targets: Vec<_> = vms
.iter()
.filter(|v| matches!(v.status, VmStatus::Running | VmStatus::Paused))
.collect();

if targets.is_empty() {
println!("No running VMs to stop.");
return Ok(());
}

println!("Stopping {} VMs...", targets.len());
for metadata in &targets {
let stop_args = StopArgs {
name: Some(metadata.name.clone()),
all: false,
force,
};
if let Err(e) = stop(&stop_args, state_dir) {
eprintln!("warning: failed to stop '{}': {}", metadata.name, e);
}
}

Ok(())
}

Expand Down Expand Up @@ -1025,16 +1069,21 @@ fn update_config(args: &UpdateConfigArgs, state_dir: &Path) -> anyhow::Result<()
///
/// Each cleanup step is idempotent — continues if the resource is already gone.
fn delete(args: &DeleteArgs, state_dir: &Path) -> anyhow::Result<()> {
if args.all {
return delete_all(args.force, state_dir);
}

let name = args.name.as_deref().unwrap();
let store = StateStore::new(state_dir.to_path_buf());

// Load VM metadata (must exist).
let metadata = vm::load(&store, &args.name)?;
let metadata = vm::load(&store, name)?;

// If the VM is running or paused, require --force.
if matches!(metadata.status, VmStatus::Running | VmStatus::Paused) && !args.force {
anyhow::bail!(
"vm '{}' is {} — stop it first or use --force",
args.name,
name,
metadata.status
);
}
Expand All @@ -1043,13 +1092,13 @@ fn delete(args: &DeleteArgs, state_dir: &Path) -> anyhow::Result<()> {
// On macOS/APFS this always returns empty — forks are independent.
let config: GlobalConfig = store.read(&store.config_path())?;
let storage = Storage::new(&config);
let dependents = storage.storage_dependents(&args.name)?;
let dependents = storage.storage_dependents(name)?;
if !dependents.is_empty() {
if !args.force {
anyhow::bail!(
"vm '{}' has dependent forks: {}\n\
Delete them first, or use --force to cascade-delete all dependents.",
args.name,
name,
dependents.join(", ")
);
}
Expand All @@ -1066,6 +1115,40 @@ fn delete(args: &DeleteArgs, state_dir: &Path) -> anyhow::Result<()> {
Ok(())
}

/// Delete all VMs.
fn delete_all(force: bool, state_dir: &Path) -> anyhow::Result<()> {
let store = StateStore::new(state_dir.to_path_buf());
let vms = vm::list(&store)?;

if vms.is_empty() {
println!("No VMs to delete.");
return Ok(());
}

if !force {
let running = vms
.iter()
.any(|v| matches!(v.status, VmStatus::Running | VmStatus::Paused));
if running {
anyhow::bail!("some VMs are still running — use --force to stop and delete them");
}
}

println!("Deleting {} VMs...", vms.len());
for metadata in &vms {
let delete_args = DeleteArgs {
name: Some(metadata.name.clone()),
all: false,
force,
};
if let Err(e) = delete(&delete_args, state_dir) {
eprintln!("warning: failed to delete '{}': {}", metadata.name, e);
}
}

Ok(())
}

/// Force-delete a VM: kill process, clean up network, destroy storage, remove state.
///
/// Idempotent — each cleanup step continues if the resource is already gone.
Expand Down
Loading