Skip to main content

mvm_runtime/
shell.rs

1use anyhow::{Context, Result};
2use std::path::Path;
3use std::process::{Command, Output, Stdio};
4
5use crate::config::VM_NAME;
6use mvm_core::platform;
7
8/// Run a command on the host, capturing output.
9pub fn run_host(cmd: &str, args: &[&str]) -> Result<Output> {
10    Command::new(cmd)
11        .args(args)
12        .output()
13        .with_context(|| format!("Failed to run: {} {}", cmd, args.join(" ")))
14}
15
16/// Run a command on the host, inheriting stdio (visible to user).
17pub fn run_host_visible(cmd: &str, args: &[&str]) -> Result<()> {
18    let status = Command::new(cmd)
19        .args(args)
20        .stdin(Stdio::inherit())
21        .stdout(Stdio::inherit())
22        .stderr(Stdio::inherit())
23        .status()
24        .with_context(|| format!("Failed to run: {} {}", cmd, args.join(" ")))?;
25
26    if !status.success() {
27        anyhow::bail!(
28            "Command failed (exit {}): {} {}",
29            status.code().unwrap_or(-1),
30            cmd,
31            args.join(" ")
32        );
33    }
34    Ok(())
35}
36
37/// Run a bash script in the Linux execution environment, capturing output.
38///
39/// On native Linux with KVM: runs `bash -c` directly on the host.
40/// On macOS or Linux without KVM: runs via `limactl shell` inside a Lima VM.
41pub fn run_on_vm(vm_name: &str, script: &str) -> Result<Output> {
42    if let Some(output) = crate::shell_mock::intercept(script) {
43        return Ok(output);
44    }
45
46    if platform::current().needs_lima() {
47        Command::new("limactl")
48            .args(["shell", vm_name, "bash", "-c", script])
49            .output()
50            .with_context(|| format!("Failed to run command in Lima VM '{}'", vm_name))
51    } else {
52        Command::new("bash")
53            .args(["-c", script])
54            .output()
55            .with_context(|| "Failed to run command on host")
56    }
57}
58
59/// Run a bash script in the Linux execution environment, with output visible to user.
60pub fn run_on_vm_visible(vm_name: &str, script: &str) -> Result<()> {
61    let status = if platform::current().needs_lima() {
62        Command::new("limactl")
63            .args(["shell", vm_name, "bash", "-c", script])
64            .stdin(Stdio::inherit())
65            .stdout(Stdio::inherit())
66            .stderr(Stdio::inherit())
67            .status()
68            .with_context(|| format!("Failed to run command in Lima VM '{}'", vm_name))?
69    } else {
70        Command::new("bash")
71            .args(["-c", script])
72            .stdin(Stdio::inherit())
73            .stdout(Stdio::inherit())
74            .stderr(Stdio::inherit())
75            .status()
76            .with_context(|| "Failed to run command on host")?
77    };
78
79    if !status.success() {
80        if platform::current().needs_lima() {
81            anyhow::bail!(
82                "Command failed in Lima VM '{}' (exit {})",
83                vm_name,
84                status.code().unwrap_or(-1)
85            );
86        } else {
87            anyhow::bail!("Command failed (exit {})", status.code().unwrap_or(-1));
88        }
89    }
90    Ok(())
91}
92
93/// Run a bash script in the Linux execution environment, returning stdout as String.
94pub fn run_on_vm_stdout(vm_name: &str, script: &str) -> Result<String> {
95    let output = run_on_vm(vm_name, script)?;
96    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
97}
98
99/// Run a bash script in the default execution environment, capturing output.
100pub fn run_in_vm(script: &str) -> Result<Output> {
101    run_on_vm(VM_NAME, script)
102}
103
104/// Run a bash script in the default execution environment, with output visible to user.
105pub fn run_in_vm_visible(script: &str) -> Result<()> {
106    run_on_vm_visible(VM_NAME, script)
107}
108
109/// Run a bash script in the default execution environment, returning stdout as String.
110pub fn run_in_vm_stdout(script: &str) -> Result<String> {
111    run_on_vm_stdout(VM_NAME, script)
112}
113
114/// Heuristic: are we currently executing inside a Lima guest VM?
115/// Checks common Lima environment markers.
116pub fn inside_lima() -> bool {
117    std::env::var("LIMA_INSTANCE").is_ok()
118        || Path::new("/etc/lima-boot.conf").exists()
119        || Path::new("/run/lima-guestagent.sock").exists()
120}
121
122/// Replace the current process with an interactive command (for SSH/TTY).
123/// Uses Unix exec() — the Rust process is fully replaced, no return on success.
124/// Note: This is safe because all arguments are passed as an array, not via shell interpolation.
125#[cfg(unix)]
126pub fn replace_process(cmd: &str, args: &[&str]) -> Result<()> {
127    use std::os::unix::process::CommandExt;
128
129    let err = Command::new(cmd)
130        .args(args)
131        .stdin(Stdio::inherit())
132        .stdout(Stdio::inherit())
133        .stderr(Stdio::inherit())
134        .exec();
135
136    // exec() only returns on error
137    Err(err).with_context(|| format!("Failed to exec: {} {}", cmd, args.join(" ")))
138}