use std::path::Path;
use std::process::ExitStatus;
use std::time::Duration;
use sentinel_core::capture::{CaptureConfig, CaptureStats};
const EXIT_CAPTURE_FAILED: i32 = 1;
const EXIT_INCOMPLETE: i32 = 2;
pub async fn cmd_capture(
output: &Path,
listen_address: String,
port_grpc: u16,
port_http: u16,
max_file_size_mb: u64,
grace_ms: u64,
command: &[String],
) -> i32 {
let cfg = CaptureConfig {
listen_addr: listen_address,
port_grpc,
port_http,
output: output.to_path_buf(),
max_file_bytes: capped_file_bytes(max_file_size_mb),
grace: Duration::from_millis(grace_ms),
};
let (result, command_code) = match command {
[] => (sentinel_core::capture::run_until_signal(&cfg).await, None),
[program, args @ ..] => run_wrapped(&cfg, program, args).await,
};
let stats = match result {
Ok(stats) => stats,
Err(e) => {
eprintln!("Capture error: {e}");
return command_code
.filter(|c| *c != 0)
.unwrap_or(EXIT_CAPTURE_FAILED);
}
};
report(&cfg, &stats);
if let Some(code) = command_code
&& code != 0
{
return code;
}
if stats.is_incomplete() {
return EXIT_INCOMPLETE;
}
0
}
async fn run_wrapped(
cfg: &CaptureConfig,
program: &str,
args: &[String],
) -> (
Result<CaptureStats, sentinel_core::capture::CaptureError>,
Option<i32>,
) {
let capture = match sentinel_core::capture::start(cfg).await {
Ok(capture) => capture,
Err(e) => return (Err(e), None),
};
let mut cmd = tokio::process::Command::new(program);
cmd.args(args);
#[cfg(unix)]
cmd.process_group(0);
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => {
eprintln!("Capture error: cannot run {program}: {e}");
let _ = capture.finish().await;
return (Ok(CaptureStats::default()), Some(EXIT_CAPTURE_FAILED));
}
};
let code = tokio::select! {
waited = child.wait() => match waited {
Ok(status) => Some(exit_code_of(status)),
Err(e) => {
eprintln!("Capture error: waiting on {program} failed: {e}");
Some(EXIT_CAPTURE_FAILED)
}
},
() = sentinel_core::capture::shutdown_signal() => {
eprintln!("Capture: stopping on signal, terminating {program}");
terminate_tree(&mut child).await;
Some(EXIT_CAPTURE_FAILED)
}
};
(capture.finish().await, code.or(Some(0)))
}
#[cfg(unix)]
const TERM_GRACE: Duration = Duration::from_secs(5);
async fn terminate_tree(child: &mut tokio::process::Child) {
#[cfg(unix)]
if let Some(pid) = child.id() {
let pgid = pid.cast_signed();
unsafe {
libc::killpg(pgid, libc::SIGTERM);
}
if tokio::time::timeout(TERM_GRACE, child.wait()).await.is_ok() {
return;
}
eprintln!("Capture: command did not stop on SIGTERM, killing it");
unsafe {
libc::killpg(pgid, libc::SIGKILL);
}
}
let _ = child.kill().await;
}
fn capped_file_bytes(requested_mb: u64) -> u64 {
let requested = requested_mb.saturating_mul(1024 * 1024);
let cap = crate::limits::MAX_BATCH_INPUT_BYTES as u64;
if requested > cap {
eprintln!(
"Capture: --max-file-size {requested_mb} MiB exceeds the {} MiB \
analyze can read, capping there.",
cap / (1024 * 1024)
);
return cap;
}
requested
}
fn exit_code_of(status: ExitStatus) -> i32 {
if let Some(code) = status.code() {
return code;
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = status.signal() {
return 128 + signal;
}
}
EXIT_CAPTURE_FAILED
}
fn report(cfg: &CaptureConfig, stats: &CaptureStats) {
if stats.requests == 0 && !stats.is_incomplete() {
eprintln!(
"Capture: no traces received. Is the application exporting to \
{}:{} (gRPC) or {}:{} (HTTP)?",
cfg.listen_addr, cfg.port_grpc, cfg.listen_addr, cfg.port_http
);
return;
}
eprintln!(
"Capture: {} spans in {} requests written to {}",
stats.spans,
stats.requests,
cfg.output.display()
);
if stats.truncated {
eprintln!(
"Capture: size limit reached, {} is incomplete. Raise \
--max-file-size or narrow the test scope.",
cfg.output.display()
);
}
if stats.rejected_backpressure > 0 {
eprintln!(
"Capture: {} requests could not be queued and were refused, {} is \
incomplete. The exporter was faster than the writer.",
stats.rejected_backpressure,
cfg.output.display()
);
}
if stats.rejected_unusable > 0 {
eprintln!(
"Capture: {} requests were refused as unusable, {} is incomplete. \
Is the exporter sending OTLP protobuf? Set \
OTEL_EXPORTER_OTLP_PROTOCOL to grpc (port {}) or http/protobuf \
(port {}).",
stats.rejected_unusable,
cfg.output.display(),
cfg.port_grpc,
cfg.port_http
);
}
}