1mod environment;
5
6use std::{
7 ffi::OsString,
8 future::Future,
9 io,
10 path::{Path, PathBuf},
11 time::Duration,
12};
13
14use thiserror::Error;
15use tokio_util::sync::CancellationToken;
16
17use crate::process::{self, ChildIdentity, Output};
18
19pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
22pub const MAX_TIMEOUT: Duration = Duration::from_millis(crate::config::validation::MAX_TIMEOUT_MS);
24
25#[derive(Debug)]
29pub struct Command {
30 pub cwd: PathBuf,
32 command: process::Command,
33 directory: PathBuf,
34}
35
36impl Command {
37 pub fn prepare(
41 program: OsString,
42 args: Vec<OsString>,
43 workspace: &Path,
44 run_dir: &Path,
45 timeout: Option<Duration>,
46 ) -> Result<Self, Error> {
47 let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT);
48 if timeout.is_zero() || timeout > MAX_TIMEOUT {
49 return Err(Error::InvalidTimeout);
50 }
51 let (cwd, directory, env) = environment::prepare(workspace, run_dir)?;
52 Ok(Self {
53 command: process::Command {
54 program,
55 args,
56 cwd: cwd.clone(),
57 env,
58 timeout,
59 },
60 cwd,
61 directory,
62 })
63 }
64
65 pub(crate) fn session_environment(&mut self, directory: &Path) {
66 crate::platform::environment::set(
67 &mut self.command.env,
68 "ARTIFACTIZE_SESSION_DIR",
69 directory,
70 );
71 }
72
73 pub(crate) fn inherit_session(
75 &mut self,
76 environment: &std::collections::BTreeMap<OsString, OsString>,
77 ) {
78 if let Some(value) =
79 crate::platform::environment::lookup(environment, "ARTIFACTIZE_SESSION_DIR")
80 {
81 crate::platform::environment::set(
82 &mut self.command.env,
83 "ARTIFACTIZE_SESSION_DIR",
84 value,
85 );
86 }
87 }
88
89 pub(crate) fn into_process(mut self) -> process::Command {
90 self.command.cwd = self.cwd;
91 self.command
92 }
93
94 pub fn directory(&self) -> &Path {
95 &self.directory
96 }
97
98 pub fn timeout(&self) -> Duration {
99 self.command.timeout
100 }
101
102 pub(crate) async fn output(
104 mut self,
105 input: Vec<u8>,
106 cancellation: CancellationToken,
107 ) -> Result<Output, process::Error> {
108 self.command.cwd = self.cwd;
109 process::run_with_input(self.command, Some(input), cancellation, |_| async {
110 Ok(())
111 })
112 .await
113 }
114
115 pub(crate) async fn tool_output(
117 mut self,
118 input: Vec<u8>,
119 output_limit: usize,
120 cancellation: CancellationToken,
121 ) -> Result<Output, process::Error> {
122 self.command.cwd = self.cwd;
123 process::run_with_input_limit(
124 self.command,
125 Some(input),
126 output_limit,
127 cancellation,
128 |_| async { Ok(()) },
129 )
130 .await
131 }
132}
133
134#[derive(
135 Debug,
136 Clone,
137 Copy,
138 PartialEq,
139 Eq,
140 PartialOrd,
141 Ord,
142 serde::Serialize,
143 serde::Deserialize,
144 clap::ValueEnum,
145)]
146#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
147#[value(rename_all = "SCREAMING_SNAKE_CASE")]
148pub enum Verdict {
149 Green,
150 Red,
151}
152
153impl Verdict {
154 pub fn as_str(self) -> &'static str {
155 match self {
156 Self::Green => "GREEN",
157 Self::Red => "RED",
158 }
159 }
160}
161
162impl std::fmt::Display for Verdict {
163 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 formatter.write_str(self.as_str())
165 }
166}
167
168#[derive(Debug)]
169pub struct ReviewResult {
170 pub verdict: Verdict,
171 pub exit_code: i32,
172 pub output: Output,
173}
174
175#[derive(Debug)]
177pub enum Outcome {
178 Completed(ReviewResult),
179 OperationalError(Error),
180}
181
182#[derive(Debug, Error)]
183pub enum Error {
184 #[error("timeoutMs must be between 1 and 2147483647")]
185 InvalidTimeout,
186 #[error("runtime output directory must be outside the review workspace")]
187 OutputInsideWorkspace,
188 #[error("runtime environment preparation failed: {0}")]
189 Environment(#[from] io::Error),
190 #[error(transparent)]
191 Process(#[from] process::Error),
192 #[error("runtime process terminated without a verdict (signal {signal:?})")]
193 AbnormalExit { signal: Option<i32>, output: Output },
194}
195
196pub async fn execute<F, R>(
199 mut command: Command,
200 cancellation: CancellationToken,
201 register: F,
202) -> Outcome
203where
204 F: FnOnce(ChildIdentity) -> R + Send + 'static,
205 R: Future<Output = io::Result<()>> + Send + 'static,
206{
207 command.command.cwd = command.cwd;
208 let scratch = command.directory.join("tmp");
211 let result =
212 process::run_logged(command.command, command.directory, cancellation, register).await;
213 let _ = std::fs::remove_dir_all(scratch);
214 match result {
215 Ok(mut output) => {
216 output.stdout = clean_output(&output.stdout);
217 output.stderr = clean_output(&output.stderr);
218 match crate::platform::process_end(&output.status) {
219 crate::platform::ProcessEnd::Exited(exit_code) => {
220 Outcome::Completed(ReviewResult {
221 verdict: if exit_code == 0 {
222 Verdict::Green
223 } else {
224 Verdict::Red
225 },
226 exit_code,
227 output,
228 })
229 }
230 crate::platform::ProcessEnd::Signaled(signal) => {
231 Outcome::OperationalError(Error::AbnormalExit { signal, output })
232 }
233 }
234 }
235 Err(error) => Outcome::OperationalError(Error::Process(error)),
236 }
237}
238
239pub(crate) use artifactize_tools::result::clean_output;