blob: 1783818a864a1e3cc25202ba7b67a14c275597e4 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::tail::Tail;
use serde_json::{Value, json};
use std::{path::PathBuf, process::ExitStatus};
#[derive(Debug)]
pub enum Termination {
Timeout,
Cancelled,
}
#[derive(Debug)]
pub enum Outcome {
Exited(ExitStatus),
Interrupted {
reason: Termination,
/// Whether the first native termination signal was accepted.
signal_applied: bool,
},
}
/// Final native exit evidence and bounded output, after process and pipe cleanup.
#[derive(Debug)]
pub struct Captured {
pub outcome: Outcome,
pub stdout: String,
pub stderr: String,
pub stdout_truncated: bool,
pub stderr_truncated: bool,
}
impl Captured {
/// Present the final native evidence using the shell tool's common format.
pub fn into_output(self, cwd: PathBuf, command: String) -> Value {
render(cwd, command, self)
}
pub(crate) fn new(outcome: Outcome, mut stdout: Tail, mut stderr: Tail) -> Self {
Self {
outcome,
stdout: stdout.text(),
stderr: stderr.text(),
stdout_truncated: stdout.truncated,
stderr_truncated: stderr.truncated,
}
}
}
pub(crate) fn render(cwd: PathBuf, command: String, captured: Captured) -> Value {
#[cfg(windows)]
let cwd = dunce::simplified(&cwd);
let (status, code, failure) = match captured.outcome {
Outcome::Exited(exit) if exit.success() => ("completed", Some(0), None),
Outcome::Exited(exit) => ("failed", exit.code(), Some(failure(exit))),
Outcome::Interrupted {
reason: Termination::Timeout,
..
} => ("timed_out", Some(124), None),
Outcome::Interrupted {
reason: Termination::Cancelled,
..
} => ("cancelled", Some(130), None),
};
let mut result = json!({"kind":"terminal","cwd":cwd,"cmd":command,"status":status,
"output":{"mode":"pipes","stdout":captured.stdout,"stderr":captured.stderr,
"stdoutTruncated":captured.stdout_truncated,"stderrTruncated":captured.stderr_truncated,"redacted":false}});
if let Some(code) = code {
result["exitCode"] = json!(code);
}
if let Some(failure) = failure {
result["failureMessage"] = json!(failure);
}
result
}
fn failure(exit: ExitStatus) -> String {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = exit.signal() {
return format!("Command terminated by signal {signal}");
}
}
format!("Command exited with code {}", exit.code().unwrap_or(-1))
}