use std::path::Path;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
const EXIT_STDIO_GRACE_MS: u64 = 100;
const WINDOWS_SHELL_COMMANDS: &[&str] = &["npm", "npx", "pnpm", "yarn", "yarnpkg", "corepack"];
pub fn should_use_windows_shell(command: &str) -> bool {
if !cfg!(target_os = "windows") {
return false;
}
let command_name = Path::new(command)
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_lowercase())
.unwrap_or_default();
command_name.ends_with(".cmd")
|| command_name.ends_with(".bat")
|| WINDOWS_SHELL_COMMANDS.contains(&command_name.as_str())
}
pub async fn wait_for_child_process(child: &mut Child) -> std::io::Result<Option<i32>> {
match child.wait().await {
Ok(status) => {
tokio::time::sleep(std::time::Duration::from_millis(EXIT_STDIO_GRACE_MS)).await;
Ok(status.code())
}
Err(e) => Err(e),
}
}
pub async fn spawn_with_signal(
program: &str,
args: &[&str],
) -> std::io::Result<Child> {
let mut cmd = Command::new(program);
#[cfg(target_os = "windows")]
{
if should_use_windows_shell(program) {
cmd = Command::new("cmd.exe");
let mut all_args = vec!["/c".to_string(), program.to_string()];
all_args.extend(args.iter().map(|s| s.to_string()));
cmd.args(&all_args);
} else {
cmd.args(args);
}
}
#[cfg(not(target_os = "windows"))]
{
cmd.args(args);
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
cmd.stdin(Stdio::null());
cmd.kill_on_drop(true);
cmd.spawn()
}
pub async fn run_command(program: &str, args: &[&str]) -> std::io::Result<CommandOutput> {
let mut child = spawn_with_signal(program, args).await?;
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let mut stdout_output = String::new();
let mut stderr_output = String::new();
if let Some(stdout) = stdout {
let mut reader = BufReader::new(stdout).lines();
while let Ok(Some(line)) = reader.next_line().await {
stdout_output.push_str(&line);
stdout_output.push('\n');
}
}
if let Some(stderr) = stderr {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
stderr_output.push_str(&line);
stderr_output.push('\n');
}
}
let exit_code = wait_for_child_process(&mut child).await?;
Ok(CommandOutput {
stdout: stdout_output.trim().to_string(),
stderr: stderr_output.trim().to_string(),
exit_code,
})
}
#[derive(Debug, Clone)]
pub struct CommandOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: Option<i32>,
}
impl CommandOutput {
pub fn is_success(&self) -> bool {
self.exit_code == Some(0)
}
pub fn exit_code_or(&self, default: i32) -> i32 {
self.exit_code.unwrap_or(default)
}
}
pub async fn run_shell(command: &str) -> std::io::Result<CommandOutput> {
#[cfg(target_os = "windows")]
{
run_command("cmd.exe", &["/c", command]).await
}
#[cfg(not(target_os = "windows"))]
{
run_command("sh", &["-c", command]).await
}
}
pub async fn run_shell_capture(command: &str) -> std::io::Result<String> {
let output = run_shell(command).await?;
Ok(output.stdout)
}
pub async fn run_capture(program: &str, args: &[&str]) -> std::io::Result<String> {
let output = run_command(program, args).await?;
Ok(output.stdout)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_use_windows_shell() {
#[cfg(target_os = "windows")]
{
assert!(should_use_windows_shell("npm"));
assert!(should_use_windows_shell("pnpm"));
assert!(should_use_windows_shell("yarn"));
assert!(should_use_windows_shell("npx.cmd"));
assert!(!should_use_windows_shell("rustc"));
}
#[cfg(not(target_os = "windows"))]
{
assert!(!should_use_windows_shell("npm"));
assert!(!should_use_windows_shell("pnpm"));
}
}
#[tokio::test]
async fn test_spawn_with_signal() {
#[cfg(target_os = "windows")]
let program = "cmd.exe";
#[cfg(target_os = "windows")]
let args = vec!["/c", "echo", "hello"];
#[cfg(not(target_os = "windows"))]
let program = "echo";
#[cfg(not(target_os = "windows"))]
let args = vec!["hello"];
let mut child = spawn_with_signal(program, &args).await.unwrap();
let exit_code = wait_for_child_process(&mut child).await.unwrap();
#[cfg(target_os = "windows")]
assert_eq!(exit_code, Some(0));
#[cfg(not(target_os = "windows"))]
assert_eq!(exit_code, Some(0));
}
#[tokio::test]
async fn test_run_command() {
#[cfg(target_os = "windows")]
let (program, args) = ("cmd.exe", vec!["/c", "echo", "test"]);
#[cfg(not(target_os = "windows"))]
let (program, args) = ("echo", vec!["test"]);
let output = run_command(program, &args).await.unwrap();
assert!(output.is_success());
}
#[tokio::test]
async fn test_run_shell() {
#[cfg(target_os = "windows")]
let output = run_shell("echo hello").await.unwrap();
#[cfg(target_os = "windows")]
assert!(output.stdout.contains("hello"));
#[cfg(not(target_os = "windows"))]
let output = run_shell("echo hello").await.unwrap();
#[cfg(not(target_os = "windows"))]
assert!(output.stdout.contains("hello"));
}
}