1use std::collections::BTreeMap;
17
18use kernel::{AxError, Payload, ToolOutcome};
19use serde_json::{Map, Value};
20
21use crate::backlog::{BacklogId, Exit, Finished};
22
23mod ceiling;
24use ceiling::memory_ceiling;
25pub(super) use ceiling::with_ceiling;
26
27fn ending(result: &mut Map<String, Value>, exit: Exit) {
35 match exit {
36 Exit::Ended { code } => {
37 result.insert(
38 "exit_code".to_owned(),
39 Value::Number(i64::from(code).into()),
40 );
41 }
42 Exit::Signalled => {
43 result.insert(
44 "outcome".to_owned(),
45 Value::String(exit.as_str().to_owned()),
46 );
47 result.insert(
48 "detail".to_owned(),
49 Value::String(
50 "a signal stopped this command, so it returned no code; `halt` is \
51 what usually sends one"
52 .to_owned(),
53 ),
54 );
55 }
56 Exit::Unknown { why } => {
57 result.insert(
58 "outcome".to_owned(),
59 Value::String(exit.as_str().to_owned()),
60 );
61 result.insert(
62 "detail".to_owned(),
63 Value::String(why.sentence().to_owned()),
64 );
65 }
66 }
67}
68
69pub(super) fn backgrounded(id: &BacklogId, what: &str, arm: &str) -> Result<ToolOutcome, AxError> {
75 let mut result = Map::new();
76 result.insert("arm".to_owned(), Value::String(arm.to_owned()));
77 result.insert(
78 "outcome".to_owned(),
79 Value::String("backgrounded".to_owned()),
80 );
81 result.insert("handle".to_owned(), Value::String(id.to_string()));
82 result.insert("what".to_owned(), Value::String(what.to_owned()));
83 result.insert(
84 "detail".to_owned(),
85 Value::String(
86 "still running; do not wait for it - carry on, and its result arrives at the end \
87 of a later tool result"
88 .to_owned(),
89 ),
90 );
91 Ok(ToolOutcome {
92 result: Payload::new(result)?,
93 attachments: Vec::new(),
94 })
95}
96
97pub(super) fn with_backlog(
106 outcome: ToolOutcome,
107 done: Vec<Finished>,
108) -> Result<ToolOutcome, AxError> {
109 if done.is_empty() {
110 return Ok(outcome);
111 }
112 let mut result = outcome.result.as_map().clone();
113 let rows = done
114 .into_iter()
115 .map(|member| {
116 let mut row = Map::new();
117 row.insert("handle".to_owned(), Value::String(member.id.to_string()));
118 row.insert("what".to_owned(), Value::String(member.what));
119 ending(&mut row, member.exit);
120 memory_ceiling(&mut row, member.ceiling);
121 row.insert("stdout".to_owned(), Value::String(member.stdout));
122 row.insert("stderr".to_owned(), Value::String(member.stderr));
123 Value::Object(row)
124 })
125 .collect();
126 result.insert("background".to_owned(), Value::Array(rows));
127 Ok(ToolOutcome {
128 result: Payload::new(result)?,
129 attachments: Vec::new(),
130 })
131}
132
133pub(super) fn settled(
134 stdout: &str,
135 stderr: &str,
136 exit: Exit,
137 arm: &str,
138) -> Result<ToolOutcome, AxError> {
139 let mut result = Map::new();
140 result.insert("arm".to_owned(), Value::String(arm.to_owned()));
141 result.insert("stdout".to_owned(), Value::String(stdout.to_owned()));
142 result.insert("stderr".to_owned(), Value::String(stderr.to_owned()));
143 ending(&mut result, exit);
144 Ok(ToolOutcome {
145 result: Payload::new(result)?,
146 attachments: Vec::new(),
147 })
148}
149
150pub(super) fn with_environment(
155 outcome: ToolOutcome,
156 inherited: &BTreeMap<String, String>,
157) -> Result<ToolOutcome, AxError> {
158 let mut result = outcome.result.as_map().clone();
159 result.insert(
160 "env".to_owned(),
161 Value::Array(
162 inherited
163 .keys()
164 .map(|name| Value::String(name.clone()))
165 .collect(),
166 ),
167 );
168 Ok(ToolOutcome {
169 result: Payload::new(result)?,
170 attachments: Vec::new(),
171 })
172}
173
174pub(super) fn with_interpreter(
176 outcome: ToolOutcome,
177 interpreter: &str,
178) -> Result<ToolOutcome, AxError> {
179 let mut result = outcome.result.as_map().clone();
180 result.insert(
181 INTERPRETER.to_owned(),
182 Value::String(interpreter.to_owned()),
183 );
184 Ok(ToolOutcome {
185 result: Payload::new(result)?,
186 attachments: Vec::new(),
187 })
188}
189
190const INTERPRETER: &str = "interpreter";
191
192const BEFORE_INTERPRETERS: &str = "system";
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
198pub enum FailureClass {
199 CommandNotFound,
201 Syntax,
203 Encoding,
205}
206
207enum Family {
209 Cmd,
210 Pwsh,
211 Posix,
212 Unnamed,
214}
215
216impl Family {
217 fn of(interpreter: &str) -> Family {
218 match interpreter {
219 "cmd" => Family::Cmd,
220 "pwsh" | "powershell" => Family::Pwsh,
221 BEFORE_INTERPRETERS => Family::Unnamed,
222 _ => Family::Posix,
223 }
224 }
225}
226
227impl FailureClass {
228 #[must_use]
230 pub fn as_str(self) -> &'static str {
231 match self {
232 FailureClass::CommandNotFound => "command_not_found",
233 FailureClass::Syntax => "syntax",
234 FailureClass::Encoding => "encoding",
235 }
236 }
237
238 #[must_use]
247 pub fn of(interpreter: &str, exit_code: i64, stdout: &str, stderr: &str) -> Option<Self> {
248 if exit_code == 0 {
249 return None;
250 }
251 let family = Family::of(interpreter);
252 let not_found = match family {
253 Family::Cmd => exit_code == 9009,
254 Family::Posix => exit_code == 127,
255 Family::Pwsh => stderr.contains("CommandNotFoundException"),
256 Family::Unnamed => exit_code == 9009 || exit_code == 127,
257 };
258 let parser = matches!(family, Family::Pwsh) && stderr.contains("ParserError");
259 let cmd_text = stderr.contains("was unexpected at this time.")
260 || stderr.contains("The syntax of the command is incorrect.");
261 let posix_text = exit_code == 2 && stderr.contains("syntax error");
262 let syntax_text = match family {
263 Family::Cmd => cmd_text,
264 Family::Posix => posix_text,
265 Family::Pwsh => false,
266 Family::Unnamed => cmd_text || posix_text,
267 };
268 if not_found {
269 Some(FailureClass::CommandNotFound)
270 } else if parser {
271 Some(FailureClass::Syntax)
272 } else if stdout.contains('\u{FFFD}') || stderr.contains('\u{FFFD}') {
273 Some(FailureClass::Encoding)
274 } else if syntax_text {
275 Some(FailureClass::Syntax)
276 } else {
277 None
278 }
279 }
280}
281
282#[derive(Debug, Clone, Default, PartialEq, Eq)]
284pub struct ShellCount {
285 pub calls: u64,
287 pub failures: BTreeMap<FailureClass, u64>,
289}
290
291#[derive(Debug, Clone, Default, PartialEq, Eq)]
294pub struct ShellTally {
295 by_interpreter: BTreeMap<String, ShellCount>,
296}
297
298impl ShellTally {
299 pub fn absorb(&mut self, result: &Map<String, Value>) {
304 if result.get("arm").and_then(Value::as_str) != Some("shell") {
305 return;
306 }
307 let Some(code) = result.get("exit_code").and_then(Value::as_i64) else {
308 return;
309 };
310 let text = |key: &str| result.get(key).and_then(Value::as_str).unwrap_or("");
311 let interpreter = result
312 .get(INTERPRETER)
313 .and_then(Value::as_str)
314 .unwrap_or(BEFORE_INTERPRETERS);
315 let count = self
316 .by_interpreter
317 .entry(interpreter.to_owned())
318 .or_default();
319 count.calls = count.calls.saturating_add(1);
320 if let Some(class) = FailureClass::of(interpreter, code, text("stdout"), text("stderr")) {
321 let failed = count.failures.entry(class).or_default();
322 *failed = failed.saturating_add(1);
323 }
324 }
325
326 pub fn interpreters(&self) -> impl Iterator<Item = (&str, &ShellCount)> {
328 self.by_interpreter
329 .iter()
330 .map(|(name, count)| (name.as_str(), count))
331 }
332}
333
334pub(super) fn exceptional(
335 stdout: &[u8],
336 stderr: &[u8],
337 kind: &str,
338 detail: Option<String>,
339) -> Result<ToolOutcome, AxError> {
340 let mut result = Map::new();
341 result.insert("arm".to_owned(), Value::String("python".to_owned()));
342 result.insert(
343 "stdout".to_owned(),
344 Value::String(String::from_utf8_lossy(stdout).into_owned()),
345 );
346 result.insert(
347 "stderr".to_owned(),
348 Value::String(String::from_utf8_lossy(stderr).into_owned()),
349 );
350 result.insert("outcome".to_owned(), Value::String(kind.to_owned()));
351 if let Some(detail) = detail {
352 result.insert("detail".to_owned(), Value::String(detail));
353 }
354 Ok(ToolOutcome {
355 result: Payload::new(result)?,
356 attachments: Vec::new(),
357 })
358}