mod environment;
use std::{
ffi::OsString,
future::Future,
io,
path::{Path, PathBuf},
time::Duration,
};
use thiserror::Error;
use tokio_util::sync::CancellationToken;
use crate::process::{self, ChildIdentity, Output};
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub const MAX_TIMEOUT: Duration = Duration::from_millis(crate::config::validation::MAX_TIMEOUT_MS);
#[derive(Debug)]
pub struct Command {
pub cwd: PathBuf,
command: process::Command,
directory: PathBuf,
}
impl Command {
pub fn prepare(
program: OsString,
args: Vec<OsString>,
workspace: &Path,
run_dir: &Path,
timeout: Option<Duration>,
) -> Result<Self, Error> {
let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT);
if timeout.is_zero() || timeout > MAX_TIMEOUT {
return Err(Error::InvalidTimeout);
}
let (cwd, directory, env) = environment::prepare(workspace, run_dir)?;
Ok(Self {
command: process::Command {
program,
args,
cwd: cwd.clone(),
env,
timeout,
},
cwd,
directory,
})
}
pub(crate) fn session_environment(&mut self, directory: &Path) {
crate::platform::environment::set(
&mut self.command.env,
"ARTIFACTIZE_SESSION_DIR",
directory,
);
}
pub(crate) fn inherit_session(
&mut self,
environment: &std::collections::BTreeMap<OsString, OsString>,
) {
if let Some(value) =
crate::platform::environment::lookup(environment, "ARTIFACTIZE_SESSION_DIR")
{
crate::platform::environment::set(
&mut self.command.env,
"ARTIFACTIZE_SESSION_DIR",
value,
);
}
}
pub(crate) fn into_process(mut self) -> process::Command {
self.command.cwd = self.cwd;
self.command
}
pub fn directory(&self) -> &Path {
&self.directory
}
pub fn timeout(&self) -> Duration {
self.command.timeout
}
pub(crate) async fn output(
mut self,
input: Vec<u8>,
cancellation: CancellationToken,
) -> Result<Output, process::Error> {
self.command.cwd = self.cwd;
process::run_with_input(self.command, Some(input), cancellation, |_| async {
Ok(())
})
.await
}
pub(crate) async fn tool_output(
mut self,
input: Vec<u8>,
output_limit: usize,
cancellation: CancellationToken,
) -> Result<Output, process::Error> {
self.command.cwd = self.cwd;
process::run_with_input_limit(
self.command,
Some(input),
output_limit,
cancellation,
|_| async { Ok(()) },
)
.await
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
serde::Serialize,
serde::Deserialize,
clap::ValueEnum,
)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[value(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Verdict {
Green,
Red,
}
impl Verdict {
pub fn as_str(self) -> &'static str {
match self {
Self::Green => "GREEN",
Self::Red => "RED",
}
}
}
impl std::fmt::Display for Verdict {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug)]
pub struct ReviewResult {
pub verdict: Verdict,
pub exit_code: i32,
pub output: Output,
}
#[derive(Debug)]
pub enum Outcome {
Completed(ReviewResult),
OperationalError(Error),
}
#[derive(Debug, Error)]
pub enum Error {
#[error("timeoutMs must be between 1 and 2147483647")]
InvalidTimeout,
#[error("runtime output directory must be outside the review workspace")]
OutputInsideWorkspace,
#[error("runtime environment preparation failed: {0}")]
Environment(#[from] io::Error),
#[error(transparent)]
Process(#[from] process::Error),
#[error("runtime process terminated without a verdict (signal {signal:?})")]
AbnormalExit { signal: Option<i32>, output: Output },
}
pub async fn execute<F, R>(
mut command: Command,
cancellation: CancellationToken,
register: F,
) -> Outcome
where
F: FnOnce(ChildIdentity) -> R + Send + 'static,
R: Future<Output = io::Result<()>> + Send + 'static,
{
command.command.cwd = command.cwd;
let scratch = command.directory.join("tmp");
let result =
process::run_logged(command.command, command.directory, cancellation, register).await;
let _ = std::fs::remove_dir_all(scratch);
match result {
Ok(mut output) => {
output.stdout = clean_output(&output.stdout);
output.stderr = clean_output(&output.stderr);
match crate::platform::process_end(&output.status) {
crate::platform::ProcessEnd::Exited(exit_code) => {
Outcome::Completed(ReviewResult {
verdict: if exit_code == 0 {
Verdict::Green
} else {
Verdict::Red
},
exit_code,
output,
})
}
crate::platform::ProcessEnd::Signaled(signal) => {
Outcome::OperationalError(Error::AbnormalExit { signal, output })
}
}
}
Err(error) => Outcome::OperationalError(Error::Process(error)),
}
}
pub(crate) use artifactize_tools::result::clean_output;