use std::sync::mpsc::{Receiver, Sender};
use std::time::{SystemTime, UNIX_EPOCH};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EventCategory {
Lifecycle,
File,
Network,
Process,
}
impl EventCategory {
pub const ALL: [EventCategory; 4] = [
EventCategory::Lifecycle,
EventCategory::File,
EventCategory::Network,
EventCategory::Process,
];
pub fn as_str(self) -> &'static str {
match self {
EventCategory::Lifecycle => "lifecycle",
EventCategory::File => "file",
EventCategory::Network => "network",
EventCategory::Process => "process",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapabilitySupport {
Supported,
Partial,
Unavailable,
}
impl CapabilitySupport {
pub fn as_str(self) -> &'static str {
match self {
CapabilitySupport::Supported => "supported",
CapabilitySupport::Partial => "partial",
CapabilitySupport::Unavailable => "unavailable",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CategoryCapability {
pub category: EventCategory,
pub support: CapabilitySupport,
pub backend: &'static str,
pub reason: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObserverCapabilities {
categories: Vec<CategoryCapability>,
}
fn detect_file_backend() -> (CapabilitySupport, &'static str, &'static str) {
#[cfg(target_os = "linux")]
{
(
CapabilitySupport::Unavailable,
"seccomp-user-notify",
"Phase 3: Linux seccomp user-notify file backend not yet implemented",
)
}
#[cfg(target_os = "windows")]
{
(
CapabilitySupport::Unavailable,
"etw",
"Phase 3: Windows ETW file backend not yet implemented",
)
}
#[cfg(target_os = "macos")]
{
(
CapabilitySupport::Unavailable,
"kqueue",
"Phase 3: macOS kqueue/EndpointSecurity file backend not yet implemented (entitlement-gated)",
)
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
(
CapabilitySupport::Unavailable,
"none",
"Phase 3: no file backend planned for this OS",
)
}
}
fn detect_network_backend() -> (CapabilitySupport, &'static str, &'static str) {
#[cfg(target_os = "linux")]
{
(
CapabilitySupport::Unavailable,
"ebpf",
"Phase 3: Linux eBPF network backend not yet implemented",
)
}
#[cfg(target_os = "windows")]
{
(
CapabilitySupport::Unavailable,
"etw",
"Phase 3: Windows ETW network backend not yet implemented",
)
}
#[cfg(target_os = "macos")]
{
(
CapabilitySupport::Unavailable,
"endpoint-security",
"Phase 3: macOS EndpointSecurity network backend not yet implemented (entitlement-gated)",
)
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
(
CapabilitySupport::Unavailable,
"none",
"Phase 3: no network backend planned for this OS",
)
}
}
fn detect_process_backend() -> (CapabilitySupport, &'static str, &'static str) {
#[cfg(target_os = "linux")]
{
(
CapabilitySupport::Unavailable,
"seccomp-user-notify",
"Phase 3: Linux seccomp user-notify process backend not yet implemented",
)
}
#[cfg(target_os = "windows")]
{
(
CapabilitySupport::Unavailable,
"etw",
"Phase 3: Windows ETW process backend not yet implemented",
)
}
#[cfg(target_os = "macos")]
{
(
CapabilitySupport::Unavailable,
"endpoint-security",
"Phase 3: macOS EndpointSecurity process backend not yet implemented (entitlement-gated)",
)
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
(
CapabilitySupport::Unavailable,
"none",
"Phase 3: no process backend planned for this OS",
)
}
}
impl ObserverCapabilities {
pub fn negotiate() -> Self {
let categories = EventCategory::ALL
.iter()
.map(|&category| match category {
EventCategory::Lifecycle => CategoryCapability {
category,
support: CapabilitySupport::Supported,
backend: "portable-lifecycle",
reason: "started/exited emitted from the crate spawn and reap path",
},
EventCategory::File => {
let (support, backend, reason) = detect_file_backend();
CategoryCapability {
category,
support,
backend,
reason,
}
}
EventCategory::Network => {
let (support, backend, reason) = detect_network_backend();
CategoryCapability {
category,
support,
backend,
reason,
}
}
EventCategory::Process => {
let (support, backend, reason) = detect_process_backend();
CategoryCapability {
category,
support,
backend,
reason,
}
}
})
.collect();
Self { categories }
}
pub fn categories(&self) -> &[CategoryCapability] {
&self.categories
}
pub fn category(&self, category: EventCategory) -> &CategoryCapability {
self.categories
.iter()
.find(|entry| entry.category == category)
.expect("ObserverCapabilities always contains every EventCategory")
}
pub fn support(&self, category: EventCategory) -> CapabilitySupport {
self.category(category).support
}
pub fn is_supported(&self, category: EventCategory) -> bool {
self.support(category) == CapabilitySupport::Supported
}
pub fn to_table_rows(&self) -> Vec<[String; 4]> {
self.categories
.iter()
.map(|entry| {
[
entry.category.as_str().to_string(),
entry.support.as_str().to_string(),
entry.backend.to_string(),
entry.reason.to_string(),
]
})
.collect()
}
pub fn render_summary(&self) -> String {
let rows = self.to_table_rows();
let mut widths = [0usize; 3];
for row in &rows {
for (i, cell) in row[..3].iter().enumerate() {
widths[i] = widths[i].max(cell.len());
}
}
let mut out = String::from("observer capabilities:\n");
for row in &rows {
out.push_str(&format!(
" {cat:<cw$} {sup:<sw$} {bk:<bw$} {reason}\n",
cat = row[0],
sup = row[1],
bk = row[2],
reason = row[3],
cw = widths[0],
sw = widths[1],
bw = widths[2],
));
}
out
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ObserverEventKind {
Started,
Exited {
exit_code: i32,
},
}
impl ObserverEventKind {
pub fn as_str(&self) -> &'static str {
match self {
ObserverEventKind::Started => "started",
ObserverEventKind::Exited { .. } => "exited",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObserverEvent {
pub category: EventCategory,
pub kind: ObserverEventKind,
pub pid: u32,
pub timestamp_ms: u128,
}
impl ObserverEvent {
fn now(category: EventCategory, kind: ObserverEventKind, pid: u32) -> Self {
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
Self {
category,
kind,
pid,
timestamp_ms,
}
}
pub fn new_now(category: EventCategory, kind: ObserverEventKind, pid: u32) -> Self {
Self::now(category, kind, pid)
}
}
#[derive(Debug, Clone)]
pub struct ObserverConfig {
categories: Vec<EventCategory>,
}
impl ObserverConfig {
pub fn lifecycle() -> Self {
Self {
categories: vec![EventCategory::Lifecycle],
}
}
pub fn with_categories(categories: impl IntoIterator<Item = EventCategory>) -> Self {
Self {
categories: categories.into_iter().collect(),
}
}
pub fn observes(&self, category: EventCategory) -> bool {
self.categories.contains(&category)
}
pub fn categories(&self) -> &[EventCategory] {
&self.categories
}
}
pub struct ObserverSubscriber {
rx: Receiver<ObserverEvent>,
}
impl ObserverSubscriber {
pub(crate) fn from_receiver(rx: Receiver<ObserverEvent>) -> Self {
Self { rx }
}
pub fn recv(&self) -> Option<ObserverEvent> {
self.rx.recv().ok()
}
pub fn try_recv(&self) -> Option<ObserverEvent> {
self.rx.try_recv().ok()
}
pub fn drain(&self) -> Vec<ObserverEvent> {
let mut events = Vec::new();
while let Ok(event) = self.rx.try_recv() {
events.push(event);
}
events
}
pub fn receiver(&self) -> &Receiver<ObserverEvent> {
&self.rx
}
}
pub(crate) struct ObserverEmitter {
config: ObserverConfig,
tx: Sender<ObserverEvent>,
}
impl ObserverEmitter {
pub(crate) fn new(config: ObserverConfig) -> (Self, ObserverSubscriber) {
let (tx, rx) = std::sync::mpsc::channel();
(Self { config, tx }, ObserverSubscriber { rx })
}
pub(crate) fn emit_started(&self, pid: u32) {
if !self.config.observes(EventCategory::Lifecycle) {
return;
}
let _ = self.tx.send(ObserverEvent::now(
EventCategory::Lifecycle,
ObserverEventKind::Started,
pid,
));
}
pub(crate) fn emit_exited(&self, pid: u32, exit_code: i32) {
if !self.config.observes(EventCategory::Lifecycle) {
return;
}
let _ = self.tx.send(ObserverEvent::now(
EventCategory::Lifecycle,
ObserverEventKind::Exited { exit_code },
pid,
));
}
}
#[cfg(test)]
mod tests;