use std::sync::mpsc::{Receiver, Sender};
use std::time::{SystemTime, UNIX_EPOCH};
mod cmdline;
pub use cmdline::read_process_cmdline;
mod file_handles;
pub use file_handles::read_process_file_handles;
#[cfg(target_os = "linux")]
pub(crate) mod descendants_linux;
#[cfg(target_os = "macos")]
pub(crate) mod descendants_macos;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TraceScope {
LaunchedProcessTree,
SystemWide,
}
impl TraceScope {
pub const ALL: [TraceScope; 2] = [TraceScope::LaunchedProcessTree, TraceScope::SystemWide];
pub fn as_str(self) -> &'static str {
match self {
TraceScope::LaunchedProcessTree => "launched-process-tree",
TraceScope::SystemWide => "system-wide",
}
}
}
#[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 {
scope: TraceScope,
categories: Vec<CategoryCapability>,
}
fn detect_file_backend(scope: TraceScope) -> (CapabilitySupport, &'static str, &'static str) {
match scope {
TraceScope::SystemWide => {
#[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",
)
}
}
TraceScope::LaunchedProcessTree => {
#[cfg(target_os = "linux")]
{
(
CapabilitySupport::Partial,
"proc-fd-snapshot",
"Linux /proc/<pid>/fd/* snapshot via read_process_file_handles (#539 slice 6 follow-up; no streaming file events)",
)
}
#[cfg(target_os = "windows")]
{
(
CapabilitySupport::Partial,
"nt-handle-snapshot",
"Windows NtQuerySystemInformation + DuplicateHandle + NtQueryObject snapshot via read_process_file_handles (#539 slice 4; no streaming file events)",
)
}
#[cfg(target_os = "macos")]
{
(
CapabilitySupport::Partial,
"proc-pidinfo",
"macOS proc_pidinfo(PROC_PIDLISTFDS) snapshot via read_process_file_handles (#539 slice 8 follow-up; no streaming file events)",
)
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
(
CapabilitySupport::Unavailable,
"none",
"#539: no launched-process-tree file backend planned for this OS",
)
}
}
}
}
fn detect_network_backend(scope: TraceScope) -> (CapabilitySupport, &'static str, &'static str) {
match scope {
TraceScope::SystemWide => {
#[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",
)
}
}
TraceScope::LaunchedProcessTree => (
CapabilitySupport::Unavailable,
"none",
"#539: no-admin per-child network backend deferred to a follow-up issue",
),
}
}
fn detect_process_backend(scope: TraceScope) -> (CapabilitySupport, &'static str, &'static str) {
match scope {
TraceScope::SystemWide => {
#[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",
)
}
}
TraceScope::LaunchedProcessTree => {
#[cfg(target_os = "linux")]
{
(
CapabilitySupport::Supported,
"subreaper-proc-poll",
"Linux PR_SET_CHILD_SUBREAPER + /proc descendant polling (#539 slice 5)",
)
}
#[cfg(target_os = "windows")]
{
(
CapabilitySupport::Supported,
"job-object-iocp",
"Windows Job Object IOCP descendant lifecycle (#539 slice 2)",
)
}
#[cfg(target_os = "macos")]
{
(
CapabilitySupport::Supported,
"sysctl-proc-poll",
"macOS sysctl(KERN_PROC_ALL) descendant polling (#539 slice 7)",
)
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
(
CapabilitySupport::Unavailable,
"none",
"#539: no launched-process-tree process backend planned for this OS",
)
}
}
}
}
impl ObserverCapabilities {
pub fn negotiate() -> Self {
Self::negotiate_for_scope(TraceScope::SystemWide)
}
pub fn negotiate_for_scope(scope: TraceScope) -> 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(scope);
CategoryCapability {
category,
support,
backend,
reason,
}
}
EventCategory::Network => {
let (support, backend, reason) = detect_network_backend(scope);
CategoryCapability {
category,
support,
backend,
reason,
}
}
EventCategory::Process => {
let (support, backend, reason) = detect_process_backend(scope);
CategoryCapability {
category,
support,
backend,
reason,
}
}
})
.collect();
Self { scope, categories }
}
pub fn scope(&self) -> TraceScope {
self.scope
}
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 = format!("observer capabilities (scope={}):\n", self.scope.as_str());
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,
},
DescendantStarted,
DescendantExited,
FileOpen {
path: std::path::PathBuf,
flags: u32,
},
FileWrite {
path: std::path::PathBuf,
byte_count: u64,
},
FileClose {
path: std::path::PathBuf,
},
FileUnlink {
path: std::path::PathBuf,
},
FileRename {
from: std::path::PathBuf,
to: std::path::PathBuf,
},
}
impl ObserverEventKind {
pub fn as_str(&self) -> &'static str {
match self {
ObserverEventKind::Started => "started",
ObserverEventKind::Exited { .. } => "exited",
ObserverEventKind::DescendantStarted => "descendant-started",
ObserverEventKind::DescendantExited => "descendant-exited",
ObserverEventKind::FileOpen { .. } => "file-open",
ObserverEventKind::FileWrite { .. } => "file-write",
ObserverEventKind::FileClose { .. } => "file-close",
ObserverEventKind::FileUnlink { .. } => "file-unlink",
ObserverEventKind::FileRename { .. } => "file-rename",
}
}
}
#[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,
));
}
#[allow(dead_code)]
pub(crate) fn descendant_sink(&self) -> Option<Sender<ObserverEvent>> {
if self.config.observes(EventCategory::Process) {
Some(self.tx.clone())
} else {
None
}
}
}
#[cfg(test)]
mod tests;