mxsh 0.1.0

Embeddable POSIX-style shell parser and runtime
Documentation
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),
        },
    );
}