use std::path::Path;
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tracing::{debug, instrument};
use crate::isolate::IsolateError;
use crate::isolate::box_manager::IsolateBox;
use crate::isolate::command::IsolateCommand;
use crate::isolate::meta::MetaFile;
use crate::types::ExecutionResult;
async fn run_isolate_command(
args: Vec<String>,
meta_path: &Path,
) -> Result<(std::process::Output, MetaFile), IsolateError> {
let program = args
.first()
.ok_or_else(|| IsolateError::CommandFailed("empty command arguments".to_string()))?;
let output = Command::new(program)
.args(&args[1..])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.map_err(IsolateError::SpawnFailed)?;
let meta = if meta_path.exists() {
MetaFile::load(meta_path).await?
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(IsolateError::CommandFailed(stderr.to_string()));
};
Ok((output, meta))
}
#[instrument(skip(sandbox, stdin_data))]
pub async fn run_batch(
sandbox: &IsolateBox,
command: IsolateCommand,
stdin_data: Option<&[u8]>,
) -> Result<ExecutionResult, IsolateError> {
let meta_path = sandbox.file_path("meta.txt")?;
let stdout_host_path = sandbox.file_path("stdout.txt")?;
let stderr_host_path = sandbox.file_path("stderr.txt")?;
let stdin_sandbox_path = sandbox.sandbox_path("stdin.txt")?;
let stdout_sandbox_path = sandbox.sandbox_path("stdout.txt")?;
let stderr_sandbox_path = sandbox.sandbox_path("stderr.txt")?;
if let Some(data) = stdin_data {
sandbox.write_file("stdin.txt", data).await?;
} else {
sandbox.write_file("stdin.txt", b"").await?;
}
let command = command
.meta_file(&meta_path)
.stdin(&stdin_sandbox_path)
.stdout(&stdout_sandbox_path)
.stderr(&stderr_sandbox_path);
let args = command.build();
debug!(?args, "running isolate command");
let (_output, meta) = run_isolate_command(args, &meta_path).await?;
let mut result = meta.to_execution_result();
if stdout_host_path.exists() {
result.stdout = Some(tokio::fs::read(&stdout_host_path).await?);
}
if stderr_host_path.exists() {
result.stderr = Some(tokio::fs::read(&stderr_host_path).await?);
}
debug!(
status = ?result.status,
time = result.time,
memory = result.memory,
"execution complete"
);
Ok(result)
}
#[instrument(skip(sandbox))]
pub async fn run_with_output(
sandbox: &IsolateBox,
command: IsolateCommand,
) -> Result<(ExecutionResult, String), IsolateError> {
let meta_path = sandbox.file_path("meta.txt")?;
let stdout_host_path = sandbox.file_path("compile_stdout.txt")?;
let stderr_host_path = sandbox.file_path("compile_stderr.txt")?;
let stdin_sandbox_path = sandbox.sandbox_path("compile_stdin.txt")?;
let stdout_sandbox_path = sandbox.sandbox_path("compile_stdout.txt")?;
let stderr_sandbox_path = sandbox.sandbox_path("compile_stderr.txt")?;
sandbox.write_file("compile_stdin.txt", b"").await?;
let command = command
.meta_file(&meta_path)
.stdin(&stdin_sandbox_path)
.stdout(&stdout_sandbox_path)
.stderr(&stderr_sandbox_path);
let args = command.build();
debug!(?args, "running compile command");
let (_output, meta) = run_isolate_command(args, &meta_path).await?;
let result = meta.to_execution_result();
let mut compiler_output = String::new();
if stdout_host_path.exists() {
let stdout = tokio::fs::read_to_string(&stdout_host_path).await?;
compiler_output.push_str(&stdout);
}
if stderr_host_path.exists() {
let stderr = tokio::fs::read_to_string(&stderr_host_path).await?;
if !compiler_output.is_empty() && !stderr.is_empty() {
compiler_output.push('\n');
}
compiler_output.push_str(&stderr);
}
Ok((result, compiler_output))
}
#[derive(Debug)]
pub struct IsolateProcess {
child: tokio::process::Child,
stdin: Option<tokio::process::ChildStdin>,
stdout: Option<tokio::process::ChildStdout>,
stderr: Option<tokio::process::ChildStderr>,
meta_path: std::path::PathBuf,
}
impl IsolateProcess {
#[instrument(skip(sandbox))]
pub async fn spawn(
sandbox: &IsolateBox,
command: IsolateCommand,
) -> Result<Self, IsolateError> {
let meta_path = sandbox.file_path("interactive_meta.txt")?;
let command = command.meta_file(&meta_path);
let args = command.build();
debug!(?args, "spawning interactive isolate process");
let program = args
.first()
.ok_or_else(|| IsolateError::CommandFailed("empty command arguments".to_string()))?;
let mut child = Command::new(program)
.args(&args[1..])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(IsolateError::SpawnFailed)?;
let stdin = child.stdin.take();
let stdout = child.stdout.take();
let stderr = child.stderr.take();
Ok(Self {
child,
stdin,
stdout,
stderr,
meta_path,
})
}
pub async fn write(&mut self, data: &[u8]) -> Result<(), IsolateError> {
if let Some(ref mut stdin) = self.stdin {
stdin.write_all(data).await?;
stdin.flush().await?;
Ok(())
} else {
Err(IsolateError::StdinClosed)
}
}
pub fn close_stdin(&mut self) {
self.stdin = None;
}
pub fn stdout(&mut self) -> Option<&mut tokio::process::ChildStdout> {
self.stdout.as_mut()
}
pub fn stderr(&mut self) -> Option<&mut tokio::process::ChildStderr> {
self.stderr.as_mut()
}
pub fn take_stdout(&mut self) -> Option<tokio::process::ChildStdout> {
self.stdout.take()
}
pub fn take_stderr(&mut self) -> Option<tokio::process::ChildStderr> {
self.stderr.take()
}
pub async fn wait(mut self) -> Result<ExecutionResult, IsolateError> {
self.stdin = None;
let _ = self.child.wait().await?;
let meta = if self.meta_path.exists() {
MetaFile::load(&self.meta_path).await?
} else {
return Err(IsolateError::CommandFailed(
"no meta file produced".to_string(),
));
};
Ok(meta.to_execution_result())
}
pub async fn kill(&mut self) -> Result<(), IsolateError> {
self.child.kill().await?;
Ok(())
}
pub fn try_wait(&mut self) -> Result<Option<()>, IsolateError> {
match self.child.try_wait()? {
Some(_) => Ok(Some(())),
None => Ok(None),
}
}
}