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_unregistered(
110 self.command,
111 Some(input),
112 process::OUTPUT_LIMIT,
113 cancellation,
114 )
115 .await
116 }
117
118 pub(crate) async fn tool_output(
120 mut self,
121 input: Vec<u8>,
122 output_limit: usize,
123 cancellation: CancellationToken,
124 ) -> Result<Output, process::Error> {
125 self.command.cwd = self.cwd;
126 process::run_unregistered(self.command, Some(input), output_limit, cancellation).await
127 }
128}
129
130#[derive(
131 Debug,
132 Clone,
133 Copy,
134 PartialEq,
135 Eq,
136 PartialOrd,
137 Ord,
138 serde::Serialize,
139 serde::Deserialize,
140 clap::ValueEnum,
141)]
142#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
143#[value(rename_all = "SCREAMING_SNAKE_CASE")]
144pub enum Verdict {
145 Green,
146 Red,
147}
148
149impl Verdict {
150 pub fn as_str(self) -> &'static str {
151 match self {
152 Self::Green => "GREEN",
153 Self::Red => "RED",
154 }
155 }
156}
157
158impl std::fmt::Display for Verdict {
159 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 formatter.write_str(self.as_str())
161 }
162}
163
164#[derive(Debug)]
165pub struct ReviewResult {
166 pub verdict: Verdict,
167 pub exit_code: i32,
168 pub output: Output,
169}
170
171#[derive(Debug)]
173pub enum Outcome {
174 Completed(ReviewResult),
175 OperationalError(Error),
176}
177
178#[derive(Debug, Error)]
179pub enum Error {
180 #[error("timeoutMs must be between 1 and 2147483647")]
181 InvalidTimeout,
182 #[error("runtime output directory must be outside the review workspace")]
183 OutputInsideWorkspace,
184 #[error("runtime environment preparation failed: {0}")]
185 Environment(#[from] io::Error),
186 #[error(transparent)]
187 Process(#[from] process::Error),
188 #[error("runtime process terminated without a verdict (signal {signal:?})")]
189 AbnormalExit { signal: Option<i32>, output: Output },
190}
191
192pub async fn execute<F, R>(
195 mut command: Command,
196 cancellation: CancellationToken,
197 register: F,
198) -> Outcome
199where
200 F: FnOnce(ChildIdentity) -> R + Send + 'static,
201 R: Future<Output = io::Result<()>> + Send + 'static,
202{
203 command.command.cwd = command.cwd;
204 let scratch = command.directory.join("tmp");
207 let result =
208 process::run_logged(command.command, command.directory, cancellation, register).await;
209 let _ = std::fs::remove_dir_all(scratch);
210 match result {
211 Ok(mut output) => {
212 output.stdout = clean_output(&output.stdout);
213 output.stderr = clean_output(&output.stderr);
214 match crate::platform::process_end(&output.status) {
215 crate::platform::ProcessEnd::Exited(exit_code) => {
216 Outcome::Completed(ReviewResult {
217 verdict: if exit_code == 0 {
218 Verdict::Green
219 } else {
220 Verdict::Red
221 },
222 exit_code,
223 output,
224 })
225 }
226 crate::platform::ProcessEnd::Signaled(signal) => {
227 Outcome::OperationalError(Error::AbnormalExit { signal, output })
228 }
229 }
230 }
231 Err(error) => Outcome::OperationalError(Error::Process(error)),
232 }
233}
234
235pub(crate) use artifactize_tools::result::clean_output;