use std::ffi::OsString;
use std::io;
use std::process::ExitStatus;
use std::time::Duration;
use adk_sandbox::ProcessBackend;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
use tokio::process::{Child, Command};
use crate::EnvironmentPolicy;
pub(crate) fn allowlisted_host_env(policy: &EnvironmentPolicy) -> Vec<(String, OsString)> {
match policy {
EnvironmentPolicy::None => Vec::new(),
EnvironmentPolicy::AllowList(names) => names
.iter()
.filter_map(|name| std::env::var_os(name).map(|value| (name.clone(), value)))
.collect(),
}
}
pub(crate) fn binary_env(policy: &EnvironmentPolicy) -> Vec<(String, OsString)> {
let environment = allowlisted_host_env(policy);
#[cfg(windows)]
let mut environment = environment;
#[cfg(windows)]
if !environment.iter().any(|(key, _)| key.eq_ignore_ascii_case("SystemRoot"))
&& let Some(value) = std::env::var_os("SystemRoot")
{
environment.push(("SystemRoot".to_string(), value));
}
environment
}
pub(crate) fn rustc_command(rustc_path: &str) -> Command {
let mut command = Command::new(rustc_path);
command.env_clear().envs(ProcessBackend::toolchain_env()).kill_on_drop(true);
#[cfg(windows)]
command.arg("-Clinker=rust-lld");
command
}
#[derive(Debug, Default)]
pub(crate) struct CapturedStream {
pub(crate) bytes: Vec<u8>,
pub(crate) truncated: bool,
}
#[derive(Debug)]
pub(crate) struct CompletedProcess {
pub(crate) status: ExitStatus,
pub(crate) stdout: CapturedStream,
pub(crate) stderr: CapturedStream,
}
pub(crate) async fn run_to_completion(
mut child: Child,
stdin: Option<Vec<u8>>,
timeout: Duration,
stdout_cap: usize,
stderr_cap: usize,
) -> io::Result<Option<CompletedProcess>> {
let stdin_pipe = child.stdin.take();
let stdout_pipe = child.stdout.take();
let stderr_pipe = child.stderr.take();
let feed_stdin = async move {
if let (Some(mut pipe), Some(bytes)) = (stdin_pipe, stdin) {
match pipe.write_all(&bytes).await {
Err(error) if error.kind() == io::ErrorKind::BrokenPipe => {}
result => result?,
}
}
Ok::<(), io::Error>(())
};
let run = async {
let (fed, stdout, stderr, status) = tokio::join!(
feed_stdin,
read_capped(stdout_pipe, stdout_cap),
read_capped(stderr_pipe, stderr_cap),
child.wait(),
);
fed?;
Ok(CompletedProcess { status: status?, stdout: stdout?, stderr: stderr? })
};
let outcome = tokio::time::timeout(timeout, run).await;
match outcome {
Ok(result) => result.map(Some),
Err(_) => {
let _ = child.start_kill();
Ok(None)
}
}
}
async fn read_capped<R>(pipe: Option<R>, cap: usize) -> io::Result<CapturedStream>
where
R: AsyncRead + Unpin,
{
let mut captured = CapturedStream::default();
let Some(mut pipe) = pipe else {
return Ok(captured);
};
let mut chunk = [0u8; 8192];
loop {
let read = pipe.read(&mut chunk).await?;
if read == 0 {
return Ok(captured);
}
let take = cap.saturating_sub(captured.bytes.len()).min(read);
captured.bytes.extend_from_slice(&chunk[..take]);
captured.truncated |= take < read;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Stdio;
use std::time::Instant;
#[test]
fn no_environment_policy_exposes_nothing() {
assert!(allowlisted_host_env(&EnvironmentPolicy::None).is_empty());
}
#[test]
fn allowlist_exposes_only_named_variables_the_host_sets() {
let policy = EnvironmentPolicy::AllowList(vec![
"PATH".to_string(),
"ADK_CODE_VARIABLE_THE_HOST_NEVER_SETS".to_string(),
]);
let expected: Vec<(String, OsString)> =
std::env::var_os("PATH").map(|path| ("PATH".to_string(), path)).into_iter().collect();
assert_eq!(allowlisted_host_env(&policy), expected);
}
#[test]
fn rustc_command_starts_from_a_cleared_environment() {
let command = rustc_command("rustc");
#[cfg(unix)]
assert!(format!("{:?}", command.as_std()).starts_with("env -i "), "{command:?}");
let explicit: Vec<String> = command
.as_std()
.get_envs()
.map(|(key, _)| key.to_string_lossy().into_owned())
.collect();
let toolchain: Vec<String> =
ProcessBackend::toolchain_env().into_iter().map(|(key, _)| key).collect();
assert_eq!(explicit.len(), toolchain.len());
assert!(explicit.iter().all(|key| toolchain.contains(key)), "{explicit:?}");
}
#[cfg(unix)]
#[tokio::test]
async fn unread_stdin_does_not_outlive_the_deadline() {
let child = Command::new("sleep")
.arg("30")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.expect("spawn sleep");
let started = Instant::now();
let outcome = run_to_completion(
child,
Some(vec![b'x'; 8 * 1024 * 1024]),
Duration::from_millis(300),
1024,
1024,
)
.await
.expect("run");
assert!(outcome.is_none(), "expected the deadline to pass");
assert!(started.elapsed() < Duration::from_secs(5), "took {:?}", started.elapsed());
}
#[cfg(unix)]
#[tokio::test]
async fn stdin_and_output_are_pumped_concurrently() {
let child = Command::new("sh")
.arg("-c")
.arg("head -c 1000000 /dev/zero; cat > /dev/null; echo done >&2")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.expect("spawn sh");
let completed = run_to_completion(
child,
Some(vec![b'x'; 1_000_000]),
Duration::from_secs(20),
4096,
4096,
)
.await
.expect("run")
.expect("must complete before the deadline");
assert!(completed.status.success());
assert_eq!(completed.stdout.bytes.len(), 4096);
assert!(completed.stdout.truncated);
assert_eq!(completed.stderr.bytes, b"done\n");
assert!(!completed.stderr.truncated);
}
}