use std::io::{IsTerminal, Write};
use clap::{CommandFactory, Parser, Subcommand};
use microsandbox_cli::{
commands::{
copy, create, exec, image, inspect, install, list, logs, metrics, ps, pull, registry,
remove, run, self_cmd, snapshot, start, stop, uninstall, volume,
},
log_args::{self, LogArgs},
sandbox_cmd::{self, SandboxArgs},
};
const TOP_LEVEL_COMMAND_GROUPS: &[CommandGroup] = &[
CommandGroup {
heading: "Sandboxes",
commands: &[
"run", "create", "start", "stop", "list", "status", "metrics", "remove", "exec",
"copy", "logs", "ssh", "inspect",
],
},
CommandGroup {
heading: "Images",
commands: &["image", "pull", "load", "save", "registry"],
},
CommandGroup {
heading: "Storage",
commands: &["volume", "snapshot"],
},
CommandGroup {
heading: "Installation",
commands: &["install", "uninstall", "self"],
},
];
#[derive(Parser)]
#[command(
name = "msb",
version,
about = format!("Microsandbox CLI v{}", env!("CARGO_PKG_VERSION")),
styles = microsandbox_cli::styles::styles()
)]
struct Cli {
#[arg(long, global = true)]
tree: bool,
#[command(flatten)]
logs: LogArgs,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
#[command(hide = true)]
Sandbox(Box<SandboxArgs>),
Run(run::RunArgs),
Create(create::CreateArgs),
Start(start::StartArgs),
Stop(stop::StopArgs),
#[command(visible_alias = "ls")]
List(list::ListArgs),
#[command(name = "status", visible_alias = "ps")]
Status(ps::PsArgs),
Metrics(metrics::MetricsArgs),
#[command(visible_alias = "rm")]
Remove(remove::RemoveArgs),
Exec(exec::ExecArgs),
#[command(visible_alias = "cp")]
Copy(copy::CopyArgs),
Logs(logs::LogsArgs),
Image(image::ImageArgs),
Pull(pull::PullArgs),
Load(image::ImageLoadArgs),
Save(image::ImageSaveArgs),
Registry(registry::RegistryArgs),
#[cfg(feature = "ssh")]
Ssh(microsandbox_cli::commands::ssh::SshArgs),
#[command(hide = true)]
Images(image::ImageListArgs),
#[command(hide = true)]
Rmi(image::ImageRemoveArgs),
Inspect(inspect::InspectArgs),
#[command(visible_alias = "vol")]
Volume(volume::VolumeArgs),
#[command(visible_alias = "snap")]
Snapshot(snapshot::SnapshotArgs),
Install(install::InstallArgs),
Uninstall(uninstall::UninstallArgs),
#[command(name = "self")]
Self_(self_cmd::SelfArgs),
}
struct CommandGroup {
heading: &'static str,
commands: &'static [&'static str],
}
#[derive(Clone)]
struct CommandHelpLine {
name: String,
help: String,
}
struct HelpStyles {
enabled: bool,
}
impl HelpStyles {
fn detect() -> Self {
Self {
enabled: std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none(),
}
}
fn header(&self, value: &str) -> String {
if !self.enabled {
return value.to_string();
}
format!("\x1b[1;33m{value}\x1b[0m")
}
fn literal(&self, value: &str) -> String {
if !self.enabled {
return value.to_string();
}
format!("\x1b[1;34m{value}\x1b[0m")
}
fn style_default_help_fragment(&self, value: &str) -> String {
if !self.enabled {
return value.to_string();
}
value.replacen("Usage:", &self.header("Usage:"), 1)
}
fn style_aliases(&self, value: &str) -> String {
if !self.enabled {
return value.to_string();
}
let Some((help, aliases)) = value.split_once(" [aliases: ") else {
return value.to_string();
};
let Some(aliases) = aliases.strip_suffix(']') else {
return value.to_string();
};
format!("{help} [aliases: {}]", self.literal(aliases))
}
}
fn main() {
microsandbox_cli::ui::install_panic_hook();
if std::env::var("MSB_PATH").is_err()
&& let Ok(exe) = std::env::current_exe()
{
unsafe { std::env::set_var("MSB_PATH", &exe) };
}
if let Some(tree) = microsandbox_cli::tree::try_show_tree(&Cli::command()) {
println!("{tree}");
return;
}
if try_show_grouped_top_level_help() {
return;
}
let cli = Cli::parse();
let log_level = cli.logs.selected_level();
let exit_code = match cli.command {
Commands::Sandbox(args) => {
let sandbox_level = log_level.or(Some(microsandbox_runtime::logging::LogLevel::Info));
log_args::init_tracing(sandbox_level, false);
sandbox_cmd::run(*args, log_level); }
command => {
let ansi = std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none();
log_args::init_tracing(log_level, ansi);
match run_async_command_anyhow(command, log_level) {
Ok(()) => 0,
Err(e) => render_anyhow_error(&e),
}
}
};
if exit_code != 0 {
std::process::exit(exit_code);
}
}
fn try_show_grouped_top_level_help() -> bool {
if !is_top_level_help_request() {
return false;
}
print!("{}", render_grouped_top_level_help());
std::io::stdout()
.flush()
.expect("flushing grouped help should not fail");
true
}
fn is_top_level_help_request() -> bool {
let args: Vec<_> = std::env::args_os().skip(1).collect();
if args.is_empty() {
return true;
}
let mut saw_help = false;
for arg in args {
let Some(arg) = arg.to_str() else {
return false;
};
match arg {
"-h" | "--help" => saw_help = true,
"--error" | "--warn" | "--info" | "--debug" | "--trace" => {}
_ => return false,
}
}
saw_help
}
fn render_grouped_top_level_help() -> String {
let mut cmd = Cli::command();
let styles = HelpStyles::detect();
let mut help = Vec::new();
cmd.write_help(&mut help)
.expect("writing clap help into memory should not fail");
let default_help = String::from_utf8(help).expect("clap help should be valid UTF-8");
let Some((prefix, _)) = default_help.split_once("\nCommands:\n") else {
return default_help;
};
let Some((_, suffix)) = default_help.split_once("\nOptions:\n") else {
return default_help;
};
let mut output = String::new();
output.push_str(&styles.style_default_help_fragment(prefix));
output.push('\n');
output.push_str(&render_grouped_commands(&cmd, &styles));
output.push('\n');
output.push_str(&styles.header("Options:"));
output.push('\n');
output.push_str(&styles.style_default_help_fragment(suffix));
output
}
fn render_grouped_commands(cmd: &clap::Command, styles: &HelpStyles) -> String {
let lines = visible_command_help_lines(cmd, styles);
let name_width = lines.iter().map(|line| line.name.len()).max().unwrap_or(0);
let mut output = String::new();
let mut rendered_commands = Vec::new();
for (group_index, group) in TOP_LEVEL_COMMAND_GROUPS.iter().enumerate() {
if group_index > 0 {
output.push('\n');
}
output.push_str(&styles.header(&format!("{}:", group.heading)));
output.push('\n');
for command in group.commands {
if let Some(line) = lines.iter().find(|line| line.name == *command) {
output.push_str(&format_command_help_line(line, name_width, styles));
rendered_commands.push(line.name.as_str());
}
}
}
let mut other_lines: Vec<_> = lines
.iter()
.filter(|line| !rendered_commands.contains(&line.name.as_str()))
.cloned()
.collect();
if !other_lines.iter().any(|line| line.name == "help") {
other_lines.push(CommandHelpLine {
name: "help".to_string(),
help: "Print this message or the help of the given subcommand(s)".to_string(),
});
}
output.push('\n');
output.push_str(&styles.header("Other:"));
output.push('\n');
for line in &other_lines {
output.push_str(&format_command_help_line(line, name_width, styles));
}
output
}
fn visible_command_help_lines(cmd: &clap::Command, styles: &HelpStyles) -> Vec<CommandHelpLine> {
cmd.get_subcommands()
.filter(|command| !command.is_hide_set())
.map(|command| {
let aliases: Vec<_> = command.get_visible_aliases().collect();
let mut help = command
.get_about()
.map(ToString::to_string)
.unwrap_or_default();
if !aliases.is_empty() {
help.push_str(&format!(" [aliases: {}]", aliases.join(", ")));
}
CommandHelpLine {
name: command.get_name().to_string(),
help: styles.style_aliases(&help),
}
})
.collect()
}
fn format_command_help_line(
line: &CommandHelpLine,
name_width: usize,
styles: &HelpStyles,
) -> String {
let padded_name = format!("{:<width$}", line.name, width = name_width);
format!(
" {name} {help}\n",
name = styles.literal(&padded_name),
help = line.help
)
}
fn render_anyhow_error(err: &anyhow::Error) -> i32 {
if let Some((name, boot_err)) = find_boot_start_in_chain(err) {
microsandbox_cli::boot_error_render::render(&name, &boot_err);
return 1;
}
if find_unsupported_feature_in_chain(err) {
microsandbox_cli::ui::error_with_lines(
"this sandbox's runtime is too old for the requested feature",
&[
microsandbox_cli::ui::ErrorLine::Cause(
"the sandbox was started by an older microsandbox runtime",
),
microsandbox_cli::ui::ErrorLine::Hint("exec and shell still work"),
microsandbox_cli::ui::ErrorLine::Hint(
"restart the sandbox to update its runtime, then retry",
),
],
);
return 1;
}
if let Some(failed) = find_exec_failed_in_chain(err) {
let cmd = extract_quoted_token_str(&err.to_string())
.or_else(|| extract_quoted_token_str(&failed.message))
.unwrap_or_else(|| "<unknown>".into());
microsandbox_cli::exec_error_render::render(&cmd, &failed);
return microsandbox_cli::exec_error_render::exit_code_for(failed.kind);
}
microsandbox_cli::ui::error(&err.to_string());
1
}
fn find_boot_start_in_chain(
err: &anyhow::Error,
) -> Option<(String, microsandbox_runtime::boot_error::BootError)> {
for cause in err.chain() {
if let Some(microsandbox::MicrosandboxError::BootStart { name, err: b }) =
cause.downcast_ref::<microsandbox::MicrosandboxError>()
{
return Some((name.clone(), b.clone()));
}
}
None
}
fn find_exec_failed_in_chain(
err: &anyhow::Error,
) -> Option<microsandbox_protocol::exec::ExecFailed> {
for cause in err.chain() {
if let Some(microsandbox::MicrosandboxError::ExecFailed(payload)) =
cause.downcast_ref::<microsandbox::MicrosandboxError>()
{
return Some(payload.clone());
}
}
None
}
fn find_unsupported_feature_in_chain(err: &anyhow::Error) -> bool {
for cause in err.chain() {
if let Some(microsandbox::MicrosandboxError::AgentClient(
microsandbox::AgentClientError::UnsupportedOperation { .. },
)) = cause.downcast_ref::<microsandbox::MicrosandboxError>()
{
return true;
}
}
false
}
fn extract_quoted_token_str(s: &str) -> Option<String> {
let start = s.find('"')? + 1;
let rest = &s[start..];
let end = rest.find('"')?;
let name = &rest[..end];
if name.is_empty() {
return None;
}
Some(name.to_string())
}
fn run_async_command_anyhow(
command: Commands,
_log_level: Option<microsandbox::LogLevel>,
) -> anyhow::Result<()> {
let worker_threads = std::thread::available_parallelism()
.map(|count| count.get().clamp(4, 8))
.unwrap_or(4);
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(worker_threads)
.enable_all()
.build()?;
runtime.block_on(async move {
microsandbox::sandbox::spawn_reaper();
match command {
Commands::Sandbox(_) => unreachable!("handled before Tokio starts"),
Commands::Run(args) => run::run(args).await,
Commands::Create(args) => create::run(args).await,
Commands::Start(args) => start::run(args).await,
Commands::Stop(args) => stop::run(args).await,
Commands::List(args) => list::run(args).await,
Commands::Status(args) => ps::run(args).await,
Commands::Metrics(args) => metrics::run(args).await,
Commands::Remove(args) => remove::run(args).await,
Commands::Exec(args) => exec::run(args).await,
Commands::Copy(args) => copy::run(args).await,
Commands::Logs(args) => logs::run(args).await,
Commands::Image(args) => image::run(args).await,
Commands::Pull(args) => image::run_pull(args).await,
Commands::Load(args) => image::run_load(args).await,
Commands::Save(args) => image::run_save(args).await,
Commands::Registry(args) => registry::run(args).await,
#[cfg(feature = "ssh")]
Commands::Ssh(args) => microsandbox_cli::commands::ssh::run(args).await,
Commands::Images(args) => image::run_list(args).await,
Commands::Rmi(args) => image::run_remove(args).await,
Commands::Inspect(args) => inspect::run(args).await,
Commands::Volume(args) => volume::run(args).await,
Commands::Snapshot(args) => snapshot::run(args).await,
Commands::Install(args) => install::run(args).await,
Commands::Uninstall(args) => uninstall::run(args).await,
Commands::Self_(args) => self_cmd::run(args).await,
}
})
}