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