use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::ast::{AndOrList, CommandList, Program};
use crate::embed::{DeferredWorkDetail, DeferredWorkKind, TraceEvent};
use super::ShellState;
fn timestamp_rfc3339_utc() -> Option<String> {
let mut now = libc::timeval {
tv_sec: 0,
tv_usec: 0,
};
if unsafe { libc::gettimeofday(&mut now, std::ptr::null_mut()) } != 0 {
return None;
}
let mut tm = unsafe { std::mem::zeroed::<libc::tm>() };
if unsafe { libc::gmtime_r(&now.tv_sec, &mut tm) }.is_null() {
return None;
}
let format = b"%Y-%m-%dT%H:%M:%SZ\0";
let mut buf = [0_u8; 32];
let written = unsafe {
libc::strftime(
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
format.as_ptr() as *const libc::c_char,
&tm,
)
};
if written == 0 {
None
} else {
Some(String::from_utf8_lossy(&buf[..written]).into_owned())
}
}
fn trim_trailing_newlines(text: &str) -> &str {
text.trim_end_matches('\n')
}
pub(super) fn observability_enabled(state: &ShellState) -> bool {
state.has_outcome_capture()
}
fn fallback_uuid_bytes() -> [u8; 16] {
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
let counter = NEXT_ID.fetch_add(1, Ordering::Relaxed) as u128;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut value = now ^ (counter << 32) ^ (std::process::id() as u128);
let mut bytes = [0_u8; 16];
for byte in &mut bytes {
*byte = value as u8;
value = value.rotate_left(9) ^ 0xa5a5_a5a5_a5a5_a5a5_u128;
}
bytes
}
struct EventDetails<'a> {
kind: TraceEventKind,
command_id: &'a str,
raw_command: Option<&'a str>,
canonical_command: Option<&'a str>,
status: Option<i32>,
job_id: Option<u32>,
background_pid: Option<u32>,
work_id: Option<&'a str>,
work_kind: Option<DeferredWorkKind>,
work_detail: Option<&'a DeferredWorkDetail>,
}
#[derive(Clone, Copy)]
enum TraceEventKind {
RunStarted,
RunFinished,
BackgroundJobStarted,
BackgroundJobFinished,
DeferredWorkStarted,
DeferredWorkFinished,
}
pub(super) fn new_command_id() -> String {
let mut bytes = [0_u8; 16];
let filled =
unsafe { libc::getentropy(bytes.as_mut_ptr() as *mut libc::c_void, bytes.len()) } == 0;
if !filled {
bytes = fallback_uuid_bytes();
}
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0],
bytes[1],
bytes[2],
bytes[3],
bytes[4],
bytes[5],
bytes[6],
bytes[7],
bytes[8],
bytes[9],
bytes[10],
bytes[11],
bytes[12],
bytes[13],
bytes[14],
bytes[15]
)
}
fn emit_event(state: &ShellState, details: EventDetails<'_>) {
if !state.has_outcome_capture() {
return;
}
let raw_command = details.raw_command.map(trim_trailing_newlines);
let canonical_command = details.canonical_command.map(trim_trailing_newlines);
if matches!(raw_command, Some(raw) if raw.trim().is_empty())
&& matches!(canonical_command, Some(canonical) if canonical.trim().is_empty())
{
return;
}
let Some(timestamp) = timestamp_rfc3339_utc() else {
return;
};
let pid = std::process::id();
let event = match details.kind {
TraceEventKind::RunStarted => TraceEvent::RunStarted {
timestamp,
pid,
command_id: details.command_id.to_string(),
raw_command: raw_command.map(ToString::to_string),
canonical_command: canonical_command.map(ToString::to_string),
},
TraceEventKind::RunFinished => TraceEvent::RunFinished {
timestamp,
pid,
command_id: details.command_id.to_string(),
status: details.status.unwrap_or_default(),
},
TraceEventKind::BackgroundJobStarted => TraceEvent::BackgroundJobStarted {
timestamp,
pid,
command_id: details.command_id.to_string(),
raw_command: raw_command.map(ToString::to_string),
canonical_command: canonical_command.map(ToString::to_string),
job_id: details.job_id.unwrap_or_default(),
background_pid: details.background_pid,
},
TraceEventKind::BackgroundJobFinished => TraceEvent::BackgroundJobFinished {
timestamp,
pid,
command_id: details.command_id.to_string(),
status: details.status.unwrap_or_default(),
job_id: details.job_id.unwrap_or_default(),
background_pid: details.background_pid,
},
TraceEventKind::DeferredWorkStarted | TraceEventKind::DeferredWorkFinished => {
let (Some(work_id), Some(kind), Some(detail)) =
(details.work_id, details.work_kind, details.work_detail)
else {
return;
};
match details.kind {
TraceEventKind::DeferredWorkStarted => TraceEvent::DeferredWorkStarted {
timestamp,
pid,
command_id: details.command_id.to_string(),
work_id: work_id.to_string(),
kind,
detail: detail.clone(),
},
TraceEventKind::DeferredWorkFinished => TraceEvent::DeferredWorkFinished {
timestamp,
pid,
command_id: details.command_id.to_string(),
work_id: work_id.to_string(),
kind,
detail: detail.clone(),
status: details.status.unwrap_or_default(),
},
TraceEventKind::RunStarted
| TraceEventKind::RunFinished
| TraceEventKind::BackgroundJobStarted
| TraceEventKind::BackgroundJobFinished => unreachable!(),
}
}
};
state.record_trace_event(event);
}
pub(super) fn canonical_program_text(program: &Program) -> String {
trim_trailing_newlines(&program.to_canonical()).to_string()
}
pub(super) fn canonical_command_list_text(and_or_list: &AndOrList, ampersand: bool) -> String {
let program = Program::new(vec![CommandList::new(
and_or_list.clone(),
ampersand,
Default::default(),
)]);
canonical_program_text(&program)
}
pub(super) fn emit_program_start(
state: &ShellState,
command_id: &str,
raw_command: Option<&str>,
canonical_command: &str,
) {
emit_event(
state,
EventDetails {
kind: TraceEventKind::RunStarted,
command_id,
raw_command,
canonical_command: Some(canonical_command),
status: None,
job_id: None,
background_pid: None,
work_id: None,
work_kind: None,
work_detail: None,
},
);
}
pub(super) fn emit_program_finish(state: &ShellState, command_id: &str, status: i32) {
emit_event(
state,
EventDetails {
kind: TraceEventKind::RunFinished,
command_id,
raw_command: None,
canonical_command: None,
status: Some(status),
job_id: None,
background_pid: None,
work_id: None,
work_kind: None,
work_detail: None,
},
);
}
pub(super) fn emit_background_start(
state: &ShellState,
command_id: &str,
raw_command: Option<&str>,
canonical_command: &str,
job_id: u32,
background_pid: Option<u32>,
) {
emit_event(
state,
EventDetails {
kind: TraceEventKind::BackgroundJobStarted,
command_id,
raw_command,
canonical_command: Some(canonical_command),
status: None,
job_id: Some(job_id),
background_pid,
work_id: None,
work_kind: None,
work_detail: None,
},
);
}
pub(super) fn emit_background_finish(
state: &ShellState,
command_id: &str,
job_id: u32,
background_pid: Option<u32>,
status: i32,
) {
emit_event(
state,
EventDetails {
kind: TraceEventKind::BackgroundJobFinished,
command_id,
raw_command: None,
canonical_command: None,
status: Some(status),
job_id: Some(job_id),
background_pid,
work_id: None,
work_kind: None,
work_detail: None,
},
);
}
pub(super) fn emit_deferred_work_start(
state: &ShellState,
work_id: &str,
work_kind: DeferredWorkKind,
_work_detail_summary: &str,
work_detail: &DeferredWorkDetail,
) {
let Some(command_id) = state.active_command_id() else {
return;
};
emit_event(
state,
EventDetails {
kind: TraceEventKind::DeferredWorkStarted,
command_id,
raw_command: None,
canonical_command: None,
status: None,
job_id: None,
background_pid: None,
work_id: Some(work_id),
work_kind: Some(work_kind),
work_detail: Some(work_detail),
},
);
}
pub(super) fn emit_deferred_work_finish(
state: &ShellState,
work_id: &str,
work_kind: DeferredWorkKind,
_work_detail_summary: &str,
work_detail: &DeferredWorkDetail,
status: i32,
) {
let Some(command_id) = state.active_command_id() else {
return;
};
emit_event(
state,
EventDetails {
kind: TraceEventKind::DeferredWorkFinished,
command_id,
raw_command: None,
canonical_command: None,
status: Some(status),
job_id: None,
background_pid: None,
work_id: Some(work_id),
work_kind: Some(work_kind),
work_detail: Some(work_detail),
},
);
}