use anyhow::{Context, anyhow};
use crate::CommandContext;
use crate::context::CookConfig;
use enwiro_sdk::process::ProcessSpec;
use enwiro_sdk::rpc::{EnwiroRpcClient, LaunchResolveParams, LaunchResolveResult, connect};
use std::io::{self, IsTerminal, Write};
use std::os::unix::process::CommandExt;
#[derive(clap::Args)]
#[command(
author,
version,
about = "Run an application/command inside an environment"
)]
pub struct WrapArgs {
pub command_name: String,
pub environment_name: Option<String>,
#[clap(allow_hyphen_values = true, num_args = 0.., last=true)]
child_args: Option<Vec<String>>,
}
pub fn wrap<W: Write>(context: &mut CommandContext<W>, args: WrapArgs) -> anyhow::Result<()> {
let selected_environment =
match context.get_or_cook_environment(&args.environment_name, &CookConfig::default()) {
Ok(env) => Some(env),
Err(e) => {
tracing::warn!(error = %e, "Could not resolve environment");
None
}
};
let environment_path: String = match &selected_environment {
Some(environment) => environment.path.clone(),
None => {
tracing::warn!("No matching environment found, falling back to home directory");
home::home_dir()
.context("Could not determine user home directory")?
.into_os_string()
.into_string()
.map_err(|_| anyhow!("Could not convert home directory path to string"))?
}
};
let environment_name: String = selected_environment
.as_ref()
.map(|environment| environment.name.clone())
.unwrap_or_default();
let child_args: Vec<String> = args.child_args.unwrap_or_default();
let resolved = match resolve_launch_via_daemon(
&environment_name,
&environment_path,
&args.command_name,
&child_args,
io::stdin().is_terminal(),
) {
Ok(resolved) => resolved,
Err(e) => {
let message = e.degraded_launch_message(&args.command_name);
tracing::warn!(error = %message, "daemon launch.resolve failed; launching unwrapped");
eprintln!("{message}");
context.notifier.notify_error(&message);
let err = ProcessSpec::new(args.command_name.clone())
.args(child_args)
.into_command()
.exec();
return Err(anyhow!(err).context(format!("Failed to exec {}", args.command_name)));
}
};
let program = resolved.program;
let mut command = ProcessSpec::new(program.clone())
.args(resolved.args)
.into_command();
command.current_dir(&environment_path);
command.envs(resolved.env_vars);
let err = command.exec();
Err(anyhow!(err).context(format!("Failed to exec {program}")))
}
enum DaemonLaunchError {
Unreachable(String),
Resolve(String),
}
impl DaemonLaunchError {
fn degraded_launch_message(&self, command: &str) -> String {
let cause = match self {
Self::Unreachable(e) => format!("daemon not running ({e})"),
Self::Resolve(e) => format!("daemon error ({e})"),
};
format!("enwiro: {cause}; launching `{command}` unwrapped (no environment, no isolation)")
}
}
fn resolve_launch_via_daemon(
environment_name: &str,
environment_path: &str,
command: &str,
args: &[String],
interactive: bool,
) -> Result<LaunchResolveResult, DaemonLaunchError> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| {
DaemonLaunchError::Unreachable(format!("could not start async runtime: {e}"))
})?;
rt.block_on(async {
let client = connect()
.await
.map_err(|e| DaemonLaunchError::Unreachable(e.to_string()))?;
EnwiroRpcClient::launch_resolve(
&client,
LaunchResolveParams {
env_name: environment_name.to_string(),
env_path: environment_path.to_string(),
command: command.to_string(),
args: args.to_vec(),
interactive,
},
)
.await
.map_err(|e| DaemonLaunchError::Resolve(e.to_string()))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unreachable_is_reported_as_daemon_not_running() {
let msg = DaemonLaunchError::Unreachable("connection refused".to_string())
.degraded_launch_message("nvim");
assert!(msg.contains("daemon not running"), "{msg}");
assert!(msg.contains("connection refused"), "{msg}");
assert!(msg.contains("nvim"), "{msg}");
assert!(!msg.contains("daemon error"), "{msg}");
}
#[test]
fn resolve_error_is_not_reported_as_daemon_not_running() {
let msg =
DaemonLaunchError::Resolve("bad request".to_string()).degraded_launch_message("nvim");
assert!(msg.contains("daemon error"), "{msg}");
assert!(msg.contains("bad request"), "{msg}");
assert!(
!msg.contains("daemon not running"),
"resolve errors must not be mislabelled as a down daemon: {msg}"
);
}
}