use std::io;
use std::process::Stdio;
use tokio::process::Command;
use super::stdio::ChildStdio;
use super::tokens::expand_proxy_tokens;
use crate::error::AnvilError;
pub(crate) fn spawn_proxy_command(
template: &str,
host: &str,
port: u16,
user: &str,
alias: &str,
) -> Result<ChildStdio, AnvilError> {
let expanded = expand_proxy_tokens(template, host, port, user, alias);
log::debug!("ProxyCommand: spawning `{expanded}`");
let mut cmd = if cfg!(windows) {
let mut c = Command::new("cmd");
c.arg("/C").arg(&expanded);
c
} else {
let mut c = Command::new("sh");
c.arg("-c").arg(&expanded);
c
};
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let child = cmd.spawn().map_err(|e| {
AnvilError::invalid_config(format!(
"ProxyCommand: failed to spawn shell for `{expanded}`: {e}",
))
})?;
ChildStdio::new(child).map_err(|e: io::Error| {
AnvilError::invalid_config(format!(
"ProxyCommand: failed to capture stdio for `{expanded}`: {e}",
))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "hangs in CI mac/linux runners; see proxy::stdio comment. Run with --ignored locally."]
async fn spawns_through_shell_with_token_expansion() {
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
if cfg!(windows) {
return;
}
let mut io_pair = spawn_proxy_command(
"echo host=%h port=%p user=%r alias=%n",
"github.com",
22,
"git",
"gh",
)
.expect("spawn");
io_pair.shutdown().await.expect("shutdown stdin");
let mut out = String::new();
io_pair.read_to_string(&mut out).await.expect("read");
assert_eq!(out.trim(), "host=github.com port=22 user=git alias=gh");
}
#[tokio::test]
#[ignore = "spawns a child via sh -c; pair with the round-trip test for local iteration."]
async fn shell_unavailable_surfaces_clear_error() {
if cfg!(windows) {
return;
}
let _ = spawn_proxy_command("/path/that/should/not/exist/binary", "h", 22, "u", "n")
.expect("spawn returns Ok even when the inner command fails");
}
}