use std::io::{IsTerminal, Write};
use clap::Args;
use microsandbox::sandbox::Sandbox;
use microsandbox::sandbox::{AttachOptionsBuilder, ExecOptionsBuilder, ExecOutput};
use super::common::{SandboxOpts, apply_sandbox_opts};
use crate::ui;
#[derive(Debug, Args)]
pub struct RunArgs {
pub image: String,
#[arg(short, long)]
pub detach: bool,
#[arg(last = true)]
pub command: Vec<String>,
#[command(flatten)]
pub sandbox: SandboxOpts,
}
pub async fn run(args: RunArgs) -> anyhow::Result<()> {
let is_named = args.sandbox.name.is_some();
let name = args.sandbox.name.clone().unwrap_or_else(ui::generate_name);
if is_named && !args.sandbox.replace && Sandbox::get(&name).await.is_ok() {
return run_existing(name, args).await;
}
run_new(name, is_named, args).await
}
async fn run_existing(name: String, args: RunArgs) -> anyhow::Result<()> {
if args.sandbox.has_creation_flags() {
ui::warn(&format!(
"sandbox '{name}' already exists; image and resource flags ignored (use --replace to recreate)"
));
}
let sandbox = super::resolve_and_start(&name, args.sandbox.quiet).await?;
if args.detach {
sandbox.detach().await;
return Ok(());
}
let interactive = std::io::stdin().is_terminal();
let result: anyhow::Result<i32> = async {
let (cmd, cmd_args) = resolve_command(sandbox.config(), args.command, interactive)?;
match cmd {
Some(cmd) => exec_in_sandbox(&sandbox, &cmd, cmd_args, interactive).await,
None => Ok(0),
}
}
.await;
super::maybe_stop(&sandbox).await;
handle_exit(result?)
}
async fn run_new(name: String, is_named: bool, args: RunArgs) -> anyhow::Result<()> {
let builder = Sandbox::builder(&name).image(args.image.as_str());
let builder = apply_sandbox_opts(builder, &args.sandbox)?;
let (mut progress, task) = if args.detach {
builder.create_detached_with_pull_progress()?
} else {
builder.create_with_pull_progress()?
};
let mut display = if args.sandbox.quiet {
ui::PullProgressDisplay::quiet(&args.image)
} else {
ui::PullProgressDisplay::new(&args.image)
};
while let Some(event) = progress.recv().await {
display.handle_event(event);
}
display.finish();
let sandbox = task
.await
.map_err(|e| anyhow::anyhow!("create task panicked: {e}"))??;
if args.detach {
sandbox.detach().await;
if !is_named {
println!("{name}");
}
return Ok(());
}
let interactive = std::io::stdin().is_terminal();
let (cmd, cmd_args) = resolve_command(sandbox.config(), args.command, interactive)?;
let (cmd, cmd_args) = match (cmd, cmd_args) {
(Some(cmd), args) => (cmd, args),
(None, _) => {
if let Err(e) = sandbox.stop_and_wait().await {
ui::warn(&format!("failed to stop sandbox: {e}"));
}
if !is_named {
let _ = sandbox.remove_persisted().await;
}
return Ok(());
}
};
let result = exec_in_sandbox(&sandbox, &cmd, cmd_args, interactive).await;
if let Err(e) = sandbox.stop_and_wait().await {
ui::warn(&format!("failed to stop sandbox: {e}"));
}
if !is_named {
let _ = sandbox.remove_persisted().await;
}
handle_exit(result?)
}
fn resolve_command(
config: µsandbox::sandbox::SandboxConfig,
user_command: Vec<String>,
interactive: bool,
) -> anyhow::Result<(Option<String>, Vec<String>)> {
if !user_command.is_empty() {
match &config.entrypoint {
Some(ep) if !ep.is_empty() => {
let bin = ep[0].clone();
let args = ep[1..].iter().cloned().chain(user_command).collect();
Ok((Some(bin), args))
}
_ => {
let mut parts = user_command;
let cmd = parts.remove(0);
Ok((Some(cmd), parts))
}
}
} else if let Some((cmd, cmd_args)) = resolve_image_command(config) {
Ok((Some(cmd), cmd_args))
} else if interactive {
let shell = config.shell.as_deref().unwrap_or("/bin/sh");
Ok((Some(shell.to_string()), vec![]))
} else {
ui::warn("no command provided and stdin is not a terminal");
Ok((None, vec![]))
}
}
async fn exec_in_sandbox(
sandbox: &Sandbox,
cmd: &str,
cmd_args: Vec<String>,
interactive: bool,
) -> anyhow::Result<i32> {
if interactive {
Ok(sandbox
.attach(cmd, |a: AttachOptionsBuilder| a.args(cmd_args))
.await?)
} else {
let output: ExecOutput = sandbox
.exec(cmd, |e: ExecOptionsBuilder| e.args(cmd_args))
.await?;
std::io::stdout().write_all(output.stdout_bytes())?;
std::io::stderr().write_all(output.stderr_bytes())?;
Ok(if output.status().success {
0
} else {
output.status().code
})
}
}
fn handle_exit(exit_code: i32) -> anyhow::Result<()> {
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
fn resolve_image_command(
config: µsandbox::sandbox::SandboxConfig,
) -> Option<(String, Vec<String>)> {
match (&config.entrypoint, &config.cmd) {
(Some(ep), cmd) if !ep.is_empty() => {
let bin = ep[0].clone();
let args = ep[1..]
.iter()
.chain(cmd.iter().flatten())
.cloned()
.collect();
Some((bin, args))
}
(_, Some(cmd)) if !cmd.is_empty() => {
let bin = cmd[0].clone();
let args = cmd[1..].to_vec();
Some((bin, args))
}
_ => None,
}
}