8000 feat(minimal2,minimald): default to native minimald on Linux; --minvmd to opt into the VM by norrietaylor · Pull Request #571 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
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
68 changes: 68 additions & 0 deletions crates/minimal2/src/autospawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! Linux (KVM); on any other target it is a no-op.

use std::io;
use std::path::Path;
#[cfg(any(target_os = "macos", target_os = "linux"))]
use std::process::Command;
#[cfg(any(target_os = "macos", target_os = "linux"))]
Expand Down Expand Up @@ -135,6 +136,73 @@ pub fn ensure_minvmd_running() -> io::Result<()> {
Ok(())
}

/// Ensure the daemon the CLI will talk to is running, spawning it if needed.
///
/// Backend selection:
/// - macOS: always via minvmd (libkrun/Hypervisor.framework; there is no native
/// macOS minimald). `use_minvmd`/`minimal_dir` are ignored.
/// - Linux: native minimald on the host (DM2) by default; the minvmd microVM
/// (DM1) when `use_minvmd` is set.
#[cfg(target_os = "macos")]
pub fn ensure_daemon_running(use_minvmd: bool, minimal_dir: Option<&Path>) -> io::Result<()> {
let _ = (use_minvmd, minimal_dir);
ensure_minvmd_running()
}

#[cfg(target_os = "linux")]
pub fn ensure_daemon_running(use_minvmd: bool, minimal_dir: Option<&Path>) -> io::Result<()> {
if use_minvmd {
return ensure_minvmd_running();
}
let sock = crate::client::resolve_socket_path(minimal_dir, false)
.map_err(|e| io::Error::other(format!("resolving native minimald socket path: {e}")))?;
ensure_minimald_running(&sock, minimal_dir)
}

/// On targets without an auto-spawn backend, this is a no-op.
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub fn ensure_daemon_running(use_minvmd: bool, minimal_dir: Option<&Path>) -> io::Result<()> {
let _ = (use_minvmd, minimal_dir);
Ok(())
}

/// Linux native (DM2): if the SSH socket is not already accepting connections,
/// spawn `minimald run --detach --instance-num 0`. minimald daemonizes itself
/// (setsid) and only returns once it is listening, so this fail-fasts on a
/// non-zero exit.
#[cfg(target_os = "linux")]
fn ensure_minimald_running(socket_path: &Path, minimal_dir: Option<&Path>) -> io::Result<()> {
use std::os::unix::net::UnixStream;

if UnixStream::connect(socket_path).is_ok() {
tracing::debug!("native minimald already running (socket connectable)");
return Ok(());
}

tracing::info!("spawning native minimald run --detach");
// Forward the state-dir override so the spawned daemon binds the same socket
// `resolve_socket_path` resolved above; otherwise minimald would default to
// `$XDG_STATE_HOME/minimal` and the CLI would later connect to the override
// socket and fail. minimald's flag is `--minimal-state-dir`.
let mut cmd = Command::new("minimald");
if let Some(dir) = minimal_dir {
cmd.arg("--minimal-state-dir").arg(dir);
}
let output = cmd
.args(["run", "--detach", "--instance-num", "0"])
.output()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
.map_err(|e| io::Error::new(e.kind(), format!("failed to spawn minimald: {e}")))?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(io::Error::other(format!(
"minimald run --detach failed: {stderr}"
)));
}
tracing::info!("native minimald spawned successfully");
Ok(())
}

#[cfg(test)]
#[cfg(any(target_os = "macos", target_os = "linux"))]
mod tests {
Expand Down
34 changes: 32 additions & 2 deletions crates/minimal2/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,32 @@ impl Client {
/// socket created by the minvmd host daemon).
///
/// If `--minimal-dir` is set, use `<minimal_dir>/providers/local-0/ssh.sock`
/// on Linux, or `<minimal_dir>/minimald.sock` on macOS.
/// on Linux (native), or `<minimal_dir>/minimald.sock` on macOS / Linux+minvmd.
///
/// `use_minvmd` selects the backend on Linux: `false` (default) resolves the
/// native minimald UDS; `true` resolves the minvmd host-UDS bridge. On macOS the
/// bridge is the only backend, so `use_minvmd` is ignored.
pub fn resolve_socket_path(
minimal_dir_override: Option<&std::path::Path>,
use_minvmd: bool,
) -> std::io::Result<std::path::PathBuf> {
#[cfg(target_os = "macos")]
let _ = use_minvmd;
if let Some(dir) = minimal_dir_override {
#[cfg(target_os = "linux")]
return Ok(dir.join("providers/local-0/ssh.sock"));
return Ok(if use_minvmd {
dir.join("minimald.sock")
} else {
dir.join("providers/local-0/ssh.sock")
});
#[cfg(target_os = "macos")]
return Ok(dir.join("minimald.sock"));
}
#[cfg(target_os = "linux")]
{
if use_minvmd {
return minvmd::sock::resolve_uds_path();
}
let state = dirs::state_dir()
.or_else(|| dirs::home_dir().map(|h| h.join(".local/state")))
.ok_or_else(|| {
Expand All @@ -164,3 +178,19 @@ pub fn resolve_socket_path(
#[cfg(target_os = "macos")]
minvmd::sock::resolve_uds_path()
}

#[cfg(test)]
#[cfg(target_os = "linux")]
mod tests {
use super::resolve_socket_path;
use std::path::Path;

#[test]
fn linux_socket_path_honors_backend_with_override() {
let dir = Path::new("/tmp/minimal-test");
let native = resolve_socket_path(Some(dir), false).unwrap();
let bridged = resolve_socket_path(Some(dir), true).unwrap();
assert!(native.ends_with("providers/local-0/ssh.sock"), "{native:?}");
assert!(bridged.ends_with("minimald.sock"), "{bridged:?}");
}
}
43 changes: 24 additions & 19 deletions crates/minimal2/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ pub struct GlobalArgs {
/// Override the base directory used for operations (default: ~/.cache/minimal)
#[arg(long)]
minimal_dir: Option<PathBuf>,
/// Linux: run minimald inside the minvmd microVM (DM1) instead of natively
/// on the host (DM2, the default). No effect on macOS, where minvmd is the
/// only backend.
#[arg(long, global = true)]
minvmd: bool,
}

#[derive(Debug, Args)]
Expand Down Expand Up @@ -269,7 +274,7 @@ async fn main() -> Result<(), ()> {

/// Connect to the daemon, resolving the socket path from global args.
async fn connect_daemon(global: &GlobalArgs) -> Result<client::Client, ()> {
let sock = client::resolve_socket_path(global.minimal_dir.as_deref())
let sock = client::resolve_socket_path(global.minimal_dir.as_deref(), global.minvmd)
.map_err(|e| eprintln!("Failed to resolve daemon socket path: {e}"))?;

client::Client::connect(&sock)
Expand Down Expand Up @@ -302,8 +307,8 @@ async fn cmd_proxy(args: ProxyArgs) -> Result<(), ()> {

/// List sessions via the `ListSessions` RPC.
async fn cmd_ls(global: &GlobalArgs, args: LsArgs) -> Result<(), ()> {
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {e}");
if let Err(e) = autospawn::ensure_daemon_running(global.minvmd, global.minimal_dir.as_deref()) {
eprintln!("Failed to ensure the minimald daemon is running: {e}");
return Err(());
}

Expand Down Expand Up @@ -367,8 +372,8 @@ async fn cmd_ls(global: &GlobalArgs, args: LsArgs) -> Result<(), ()> {

/// Create a new session via the `CreateSession` RPC.
async fn cmd_activate(global: &GlobalArgs, args: ActivateArgs) -> Result<(), ()> {
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {e}");
if let Err(e) = autospawn::ensure_daemon_running(global.minvmd, global.minimal_dir.as_deref()) {
eprintln!("Failed to ensure the minimald daemon is running: {e}");
return Err(());
}

Expand Down Expand Up @@ -434,12 +439,12 @@ fn shell_quote(s: &str) -> String {
/// shell out to `ssh` — the daemon's shell_request handler mints a PTY-backed
/// session shell, and ssh handles termios/PTY management for us.
async fn cmd_attach(global: &GlobalArgs, args: AttachArgs) -> Result<(), ()> {
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {e}");
if let Err(e) = autospawn::ensure_daemon_running(global.minvmd, global.minimal_dir.as_deref()) {
eprintln!("Failed to ensure the minimald daemon is running: {e}");
return Err(());
}

let sock = client::resolve_socket_path(global.minimal_dir.as_deref())
let sock = client::resolve_socket_path(global.minimal_dir.as_deref(), global.minvmd)
.map_err(|e| eprintln!("Failed to resolve daemon socket path: {e}"))?;

// Resolve the session: if it looks like a UUID, query by ID; otherwise by name.
Expand Down Expand Up @@ -511,8 +516,8 @@ async fn cmd_attach(global: &GlobalArgs, args: AttachArgs) -> Result<(), ()> {

/// Print the effective networking policy for a session as JSON.
async fn cmd_session_policy(global: &GlobalArgs, args: PolicyArgs) -> Result<(), ()> {
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {e}");
if let Err(e) = autospawn::ensure_daemon_running(global.minvmd, global.minimal_dir.as_deref()) {
eprintln!("Failed to ensure the minimald daemon is running: {e}");
return Err(());
}

Expand Down Expand Up @@ -559,8 +564,8 @@ fn mesh_enrolment_path(global: &GlobalArgs) -> Result<PathBuf, ()> {
/// Show this minimald's WireGuard mesh status (R4.6): own public key, the
/// switch subnets it advertises, and each peer's last handshake.
async fn cmd_mesh_status(global: &GlobalArgs) -> Result<(), ()> {
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {e}");
if let Err(e) = autospawn::ensure_daemon_running(global.minvmd, global.minimal_dir.as_deref()) {
eprintln!("Failed to ensure the minimald daemon is running: {e}");
return Err(());
}

Expand Down Expand Up @@ -666,8 +671,8 @@ fn cmd_mesh_leave(global: &GlobalArgs) -> Result<(), ()> {

/// Destroy (terminate) a session.
async fn cmd_destroy(global: &GlobalArgs, args: DestroyArgs) -> Result<(), ()> {
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {e}");
if let Err(e) = autospawn::ensure_daemon_running(global.minvmd, global.minimal_dir.as_deref()) {
eprintln!("Failed to ensure the minimald daemon is running: {e}");
return Err(());
}

Expand Down Expand Up @@ -723,12 +728,12 @@ async fn cmd_destroy(global: &GlobalArgs, args: DestroyArgs) -> Result<(), ()> {
/// The `-N` flag keeps the tunnel alive without opening an interactive
/// shell.
async fn cmd_ssh_forward(global: &GlobalArgs, args: SshForwardArgs) -> Result<(), ()> {
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {e}");
if let Err(e) = autospawn::ensure_daemon_running(global.minvmd, global.minimal_dir.as_deref()) {
eprintln!("Failed to ensure the minimald daemon is running: {e}");
return Err(());
}

let sock = client::resolve_socket_path(global.minimal_dir.as_deref())
let sock = client::resolve_socket_path(global.minimal_dir.as_deref(), global.minvmd)
.map_err(|e| eprintln!("Failed to resolve daemon socket path: {e}"))?;

// Look up the session to validate it exists and to obtain its UUID for the
Expand Down Expand Up @@ -817,8 +822,8 @@ async fn cmd_ssh_forward(global: &GlobalArgs, args: SshForwardArgs) -> Result<()
/// sign the certificate with its internal CA, and return both. The cert, key,
/// and CA cert are written to `<cert_dir>/{client.pem,client.key,ca.pem}`.
async fn cmd_login(global: &GlobalArgs, args: LoginArgs) -> Result<(), ()> {
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {e}");
if let Err(e) = autospawn::ensure_daemon_running(global.minvmd, global.minimal_dir.as_deref()) {
eprintln!("Failed to ensure the minimald daemon is running: {e}");
return Err(());
}

Expand Down
73 changes: 73 additions & 0 deletions crates/minimald/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ pub struct ListenArgs {
#[arg(long)]
#[clap(hide = true)]
mount_rootfs: Option<String>,

/// Daemonize: spawn minimald in a new session (setsid) and return once the
/// SSH socket accepts connections, or an 8s timeout elapses. Used by the
/// `minimal` CLI to auto-start a native (DM2) daemon on Linux.
#[arg(long, default_value_t = false)]
detach: bool,
}

/// An error at the top level of minimald.
Expand Down Expand Up @@ -211,6 +217,56 @@ fn main() -> Result<(), MainError> {
runtime.block_on(async_main())
}

/// Re-exec this binary in a new session (`setsid`) with `--detach` stripped, so
/// the child runs the foreground server fully detached from the caller's
/// controlling terminal, then poll the SSH socket until it accepts connections.
/// Mirrors `minvmd run --detach`.
fn spawn_detached(cli: &Cli) -> Result<(), MainError> {
use std::os::unix::process::CommandExt as _;
use std::time::{Duration, Instant};

const DETACH_TIMEOUT_SECS: u64 = 8;

let exe = std::env::current_exe().map_err(|e| MainError::IO(e, "resolving current exe"))?;
let args: Vec<String> = std::env::args()
.skip(1)
.filter(|a| a != "--detach")
.collect();
let mut cmd = std::process::Command::new(&exe);
cmd.args(&args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
// SAFETY: setsid() is async-signal-safe. In the child it starts a new
// session so the daemon outlives the CLI and is unaffected by SIGHUP when
// the invoking shell exits.
unsafe {
cmd.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
cmd.spawn()
.map_err(|e| MainError::IO(e, "spawning detached minimald"))?;

let sock = cli.listen_on();
let sock_path = std::path::Path::new(sock.as_utf8_path().as_str());
let deadline = Instant::now() + Duration::from_secs(DETACH_TIMEOUT_SECS);
loop {
if std::os::unix::net::UnixStream::connect(sock_path).is_ok() {
return Ok(());
}
if Instant::now() >= deadline {
return Err(MainError::Other(format!(
"detached minimald did not start listening on {sock} within {DETACH_TIMEOUT_SECS}s"
)));
}
std::thread::sleep(Duration::from_millis(100));
}
}

async fn async_main() -> Result<(), MainError> {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
EnvFilter::new("info")
Expand All @@ -234,6 +290,7 @@ async fn async_main() -> Result<(), MainError> {
vsock: true,
mount_dev: true,
mount_rootfs: Some("/dev/vda".to_string()),
detach: false,
}),
global_args: GlobalArgs {
minimal_state_dir: Some(DaemonAbsPath::try_new("/run/minimal").unwrap().into()),
Expand All @@ -258,6 +315,22 @@ async fn async_main() -> Result<(), MainError> {

let listen_args = cli.listen_args().unwrap();

// Daemonize before doing any work: re-exec ourselves in a new session and
// wait until the SSH socket is accepting connections, then return so the
// caller (the `minimal` CLI autospawn) gets a clean ready/timeout result.
if listen_args.detach {
// `spawn_detached` polls `cli.listen_on()` (a UDS) for readiness, but a
// `--vsock` child binds the vsock listener instead, so the UDS never
// appears and the parent would always hit the 8s timeout while leaving a
// detached child running. Reject the combination up front.
if listen_args.vsock {
return Err(MainError::Other(
"--detach is only supported for Unix-socket listeners (not --vsock)".to_string(),
));
}
return spawn_detached(&cli);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

// Handle setup specific to operating in a micro-vm.
use minimald::guest;
if listen_args.mount_dev {
Expand Down
Loading
0