1use std::collections::VecDeque;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Condvar, Mutex};
6use std::time::{Duration, SystemTime};
7
8use running_process_platform_internal::platform::process::{
9 exact_trace_capability, ExactTraceEvent, ExactTraceEventKind, NonInvasiveObservationGrade,
10 TraceOriginArtifact,
11};
12
13const DEFAULT_RETAINED_MATCHES: usize = 256;
14
15#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
16pub enum ObservationPolicy {
17 #[default]
18 NonInvasive,
19 AllowTracing,
20 RequireExact,
21}
22
23impl ObservationPolicy {
24 pub fn as_str(self) -> &'static str {
25 match self {
26 Self::NonInvasive => "non_invasive",
27 Self::AllowTracing => "allow_tracing",
28 Self::RequireExact => "require_exact",
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum ObservationGrade {
35 ExactTrace,
36 ExactEvent,
37 KernelNotification,
38 KernelHintReconciled,
39 SnapshotInferred,
40}
41
42impl ObservationGrade {
43 pub fn as_str(self) -> &'static str {
44 match self {
45 Self::ExactTrace => "exact_trace",
46 Self::ExactEvent => "exact_event",
47 Self::KernelNotification => "kernel_notification",
48 Self::KernelHintReconciled => "kernel_hint_reconciled",
49 Self::SnapshotInferred => "snapshot_inferred",
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum StackCapture {
56 OriginPreferred,
57 OriginRequired,
58 OwnerAllThreads,
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct StackDump {
63 pub capture: StackCapture,
64 pub directory: Option<PathBuf>,
65 pub symbolize_immediately: bool,
66}
67
68impl Default for StackDump {
69 fn default() -> Self {
70 Self {
71 capture: StackCapture::OriginPreferred,
72 directory: None,
73 symbolize_immediately: false,
74 }
75 }
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
79enum WatchSelector {
80 Spawn,
81 Exec {
82 basename: Option<String>,
83 path: Option<PathBuf>,
84 },
85 Exit {
86 code: Option<i32>,
87 signal: Option<i32>,
88 basename: Option<String>,
89 failure_only: bool,
90 },
91}
92
93#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct ProcessWatch {
95 selector: WatchSelector,
96 pub dump: Option<StackDump>,
97 pub limit: Option<usize>,
98 pub cooldown: Duration,
99 pub label: String,
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct ProcessWatchConfigurationError(pub String);
104
105impl std::fmt::Display for ProcessWatchConfigurationError {
106 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 formatter.write_str(&self.0)
108 }
109}
110
111impl std::error::Error for ProcessWatchConfigurationError {}
112
113impl ProcessWatch {
114 pub fn on_spawn(
115 dump: Option<StackDump>,
116 limit: Option<usize>,
117 cooldown: Duration,
118 label: impl Into<String>,
119 ) -> Result<Self, ProcessWatchConfigurationError> {
120 Self::new(WatchSelector::Spawn, dump, limit, cooldown, label)
121 }
122
123 pub fn on_exec(
124 basename: Option<String>,
125 path: Option<PathBuf>,
126 dump: Option<StackDump>,
127 limit: Option<usize>,
128 cooldown: Duration,
129 label: impl Into<String>,
130 ) -> Result<Self, ProcessWatchConfigurationError> {
131 if basename.is_some() && path.is_some() {
132 return Err(ProcessWatchConfigurationError(
133 "on_exec accepts basename or path, not both".to_owned(),
134 ));
135 }
136 if basename.as_ref().is_some_and(|name| name.is_empty()) {
137 return Err(ProcessWatchConfigurationError(
138 "basename must not be empty".to_owned(),
139 ));
140 }
141 Self::new(
142 WatchSelector::Exec { basename, path },
143 dump,
144 limit,
145 cooldown,
146 label,
147 )
148 }
149
150 pub fn on_exit(
151 code: Option<i32>,
152 signal: Option<i32>,
153 basename: Option<String>,
154 dump: Option<StackDump>,
155 limit: Option<usize>,
156 cooldown: Duration,
157 label: impl Into<String>,
158 ) -> Result<Self, ProcessWatchConfigurationError> {
159 if code.is_some() && signal.is_some() {
160 return Err(ProcessWatchConfigurationError(
161 "on_exit accepts code or signal, not both".to_owned(),
162 ));
163 }
164 Self::new(
165 WatchSelector::Exit {
166 code,
167 signal,
168 basename,
169 failure_only: false,
170 },
171 dump,
172 limit,
173 cooldown,
174 label,
175 )
176 }
177
178 pub fn on_failure(
179 basename: Option<String>,
180 dump: Option<StackDump>,
181 limit: Option<usize>,
182 cooldown: Duration,
183 label: impl Into<String>,
184 ) -> Result<Self, ProcessWatchConfigurationError> {
185 Self::new(
186 WatchSelector::Exit {
187 code: None,
188 signal: None,
189 basename,
190 failure_only: true,
191 },
192 dump,
193 limit,
194 cooldown,
195 label,
196 )
197 }
198
199 fn new(
200 selector: WatchSelector,
201 dump: Option<StackDump>,
202 limit: Option<usize>,
203 cooldown: Duration,
204 label: impl Into<String>,
205 ) -> Result<Self, ProcessWatchConfigurationError> {
206 let label = label.into();
207 if label.trim().is_empty() {
208 return Err(ProcessWatchConfigurationError(
209 "watch label must not be empty".to_owned(),
210 ));
211 }
212 if limit == Some(0) {
213 return Err(ProcessWatchConfigurationError(
214 "watch limit must be positive or None".to_owned(),
215 ));
216 }
217 if dump
218 .as_ref()
219 .is_some_and(|request| request.symbolize_immediately)
220 {
221 return Err(ProcessWatchConfigurationError(
222 "immediate remote symbolization is not implemented; use deferred artifacts"
223 .to_owned(),
224 ));
225 }
226 if dump
227 .as_ref()
228 .is_some_and(|request| request.capture == StackCapture::OwnerAllThreads)
229 {
230 return Err(ProcessWatchConfigurationError(
231 "owner all-thread event-time capture is not implemented".to_owned(),
232 ));
233 }
234 Ok(Self {
235 selector,
236 dump,
237 limit,
238 cooldown,
239 label,
240 })
241 }
242
243 fn non_invasive_unsupported_requirement(&self) -> Option<&'static str> {
244 if self
245 .dump
246 .as_ref()
247 .is_some_and(|dump| dump.capture != StackCapture::OriginPreferred)
248 {
249 return Some("the selected stack capture provenance is unavailable non-invasively");
250 }
251 match &self.selector {
252 WatchSelector::Exit {
253 code,
254 signal,
255 basename,
256 failure_only,
257 } if code.is_some() || signal.is_some() || basename.is_some() || *failure_only => Some(
258 "the exit selector needs status or executable fields this backend cannot provide",
259 ),
260 _ => None,
261 }
262 }
263}
264
265#[derive(Clone, Debug, Eq, PartialEq)]
266pub struct ProcessIdentity {
267 pub pid: u32,
268 pub start_key: Option<u64>,
269}
270
271#[derive(Clone, Copy, Debug, Eq, PartialEq)]
272pub enum ProcessEventKind {
273 Spawn,
274 Exec,
275 Exit,
276 Loss,
277}
278
279#[derive(Clone, Debug)]
280pub struct ProcessEvent {
281 pub kind: ProcessEventKind,
282 pub process: ProcessIdentity,
283 pub parent: Option<ProcessIdentity>,
284 pub timestamp: SystemTime,
285 pub executable: Option<PathBuf>,
286 pub argv: Option<Vec<String>>,
287 pub exit_code: Option<i32>,
288 pub signal: Option<i32>,
289 pub raw_exit_status: Option<i64>,
290 pub backend: &'static str,
291 pub observation_grade: ObservationGrade,
292 pub coverage_complete: bool,
293 pub loss_detected: bool,
294}
295
296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297pub enum CaptureSource {
298 RemoteSpawningThread,
299 ManagedSpawnBoundary,
300 OwnerEventTimeSnapshot,
301 None,
302}
303
304impl CaptureSource {
305 pub fn as_str(self) -> &'static str {
306 match self {
307 Self::RemoteSpawningThread => "remote_spawning_thread",
308 Self::ManagedSpawnBoundary => "managed_spawn_boundary",
309 Self::OwnerEventTimeSnapshot => "owner_event_time_snapshot",
310 Self::None => "none",
311 }
312 }
313}
314
315#[derive(Clone, Debug)]
316pub struct DumpResult {
317 pub capture_source: CaptureSource,
318 pub artifacts: Vec<PathBuf>,
319 pub symbolized: bool,
320 pub error: Option<String>,
321}
322
323#[derive(Clone, Debug)]
324pub struct ProcessWatchMatch {
325 pub sequence: u64,
326 pub watch: ProcessWatch,
327 pub event: ProcessEvent,
328 pub dump: Option<DumpResult>,
329}
330
331#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct ProcessWatchGap {
333 pub first_missing: u64,
334 pub last_missing: u64,
335}
336
337#[derive(Clone, Debug)]
338pub struct ProcessWatchLoss {
339 pub sequence: u64,
340 pub event: ProcessEvent,
341 pub reason: String,
342}
343
344#[derive(Clone, Debug)]
345pub enum ProcessWatchRead {
346 Match(Box<ProcessWatchMatch>),
347 Loss(Box<ProcessWatchLoss>),
348 Gap(ProcessWatchGap),
349 Timeout,
350 Eof,
351}
352
353#[derive(Clone, Debug, Eq, PartialEq)]
354pub struct ProcessObservationCapabilities {
355 pub exact_available: bool,
356 pub exact_backend: &'static str,
357 pub reason: &'static str,
358 pub non_invasive_backend: &'static str,
359 pub non_invasive_grade: ObservationGrade,
360}
361
362impl ProcessObservationCapabilities {
363 pub fn current() -> Self {
364 let capability = exact_trace_capability();
365 Self {
366 exact_available: capability.available,
367 exact_backend: capability.backend,
368 reason: capability.reason,
369 non_invasive_backend: capability.non_invasive_backend,
370 non_invasive_grade: match capability.non_invasive_grade {
371 NonInvasiveObservationGrade::KernelNotification => {
372 ObservationGrade::KernelNotification
373 }
374 NonInvasiveObservationGrade::KernelHintReconciled => {
375 ObservationGrade::KernelHintReconciled
376 }
377 NonInvasiveObservationGrade::SnapshotInferred => ObservationGrade::SnapshotInferred,
378 },
379 }
380 }
381}
382
383#[derive(Clone, Debug, Eq, PartialEq)]
384pub struct ProcessObservation {
385 pub backend: &'static str,
386 pub grade: ObservationGrade,
387 pub fallback_reason: Option<String>,
388}
389
390#[derive(Clone, Debug, Eq, PartialEq)]
391pub struct ProcessObservationError(pub String);
392
393impl std::fmt::Display for ProcessObservationError {
394 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 formatter.write_str(&self.0)
396 }
397}
398
399impl std::error::Error for ProcessObservationError {}
400
401struct WatchRuntime {
402 watch: ProcessWatch,
403 matched: usize,
404 last_match: Option<SystemTime>,
405}
406
407struct LogState {
408 entries: VecDeque<ProcessWatchRecord>,
409 first_sequence: u64,
410 next_sequence: u64,
411 closed: bool,
412 coverage_complete: bool,
413}
414
415#[derive(Clone, Debug)]
416enum ProcessWatchRecord {
417 Match(ProcessWatchMatch),
418 Loss(ProcessWatchLoss),
419}
420
421struct SharedLog {
422 state: Mutex<LogState>,
423 wake: Condvar,
424}
425
426struct PendingMatch {
427 watch: ProcessWatch,
428 event: ProcessEvent,
429 dump_request: Option<StackDump>,
430 native: ExactTraceEvent,
431}
432
433enum PendingDelivery {
434 Match(Box<PendingMatch>),
435 Loss { event: ProcessEvent, reason: String },
436}
437
438struct PendingOverflow {
439 event: ProcessEvent,
440 reason: String,
441 additional_dropped: usize,
442 native_loss_reasons: Vec<String>,
443}
444
445impl PendingDelivery {
446 fn into_overflow(self) -> PendingOverflow {
447 match self {
448 Self::Match(pending) => PendingOverflow {
449 event: pending.event,
450 reason: "process-watch delivery queue overflow".to_owned(),
451 additional_dropped: 0,
452 native_loss_reasons: Vec::new(),
453 },
454 Self::Loss { event, reason } => PendingOverflow {
455 event,
456 reason: "process-watch delivery queue overflow".to_owned(),
457 additional_dropped: 0,
458 native_loss_reasons: vec![reason],
459 },
460 }
461 }
462
463 fn merge_into(self, overflow: &mut PendingOverflow) {
464 overflow.additional_dropped = overflow.additional_dropped.saturating_add(1);
465 if let Self::Loss { reason, .. } = self {
466 if !overflow.native_loss_reasons.contains(&reason) {
467 overflow.native_loss_reasons.push(reason);
468 }
469 }
470 }
471}
472
473pub(crate) struct ProcessWatchEmitter {
474 watches: Mutex<Vec<WatchRuntime>>,
475 log: Arc<SharedLog>,
476 observation: ProcessObservation,
477 descendant_stop:
478 Arc<running_process_platform_internal::platform::process::DescendantMonitorStop>,
479 exact_delivery_active: std::sync::atomic::AtomicBool,
480 delivery_tx: std::sync::mpsc::SyncSender<PendingDelivery>,
481 delivery_overflow: Arc<Mutex<Option<PendingOverflow>>>,
482 delivery_closing: Arc<std::sync::atomic::AtomicBool>,
483}
484
485impl ProcessWatchEmitter {
486 pub(crate) fn new(
487 watches: Vec<ProcessWatch>,
488 policy: ObservationPolicy,
489 ) -> Result<(Arc<Self>, ProcessWatchSubscriber), ProcessObservationError> {
490 let capabilities = ProcessObservationCapabilities::current();
491 if policy == ObservationPolicy::RequireExact && !capabilities.exact_available {
492 return Err(ProcessObservationError(format!(
493 "exact process observation is unavailable: {}",
494 capabilities.reason
495 )));
496 }
497 let exact = policy != ObservationPolicy::NonInvasive && capabilities.exact_available;
498 if !exact {
499 for watch in &watches {
500 if let Some(requirement) = watch.non_invasive_unsupported_requirement() {
501 return Err(ProcessObservationError(format!(
502 "process watch '{}': {requirement}; select an exact tracing backend",
503 watch.label
504 )));
505 }
506 }
507 }
508 let observation = if exact {
509 ProcessObservation {
510 backend: capabilities.exact_backend,
511 grade: ObservationGrade::ExactTrace,
512 fallback_reason: None,
513 }
514 } else {
515 let backend = capabilities.non_invasive_backend;
516 let grade = capabilities.non_invasive_grade;
517 ProcessObservation {
518 backend,
519 grade,
520 fallback_reason: (policy == ObservationPolicy::AllowTracing
521 && !capabilities.exact_available)
522 .then(|| capabilities.reason.to_owned()),
523 }
524 };
525 let log = Arc::new(SharedLog {
526 state: Mutex::new(LogState {
527 entries: VecDeque::new(),
528 first_sequence: 1,
529 next_sequence: 1,
530 closed: false,
531 coverage_complete: true,
532 }),
533 wake: Condvar::new(),
534 });
535 let (delivery_tx, delivery_rx) = std::sync::mpsc::sync_channel(DEFAULT_RETAINED_MATCHES);
536 let delivery_closing = Arc::new(std::sync::atomic::AtomicBool::new(false));
537 let delivery_overflow = Arc::new(Mutex::new(None));
538 let worker_log = Arc::clone(&log);
539 let worker_closing = Arc::clone(&delivery_closing);
540 let worker_overflow = Arc::clone(&delivery_overflow);
541 std::thread::Builder::new()
542 .name("rp-watch-writer".to_owned())
543 .spawn(move || {
544 delivery_loop(delivery_rx, worker_log, worker_closing, worker_overflow);
545 })
546 .map_err(|error| {
547 ProcessObservationError(format!("spawn process-watch artifact writer: {error}"))
548 })?;
549 let emitter = Arc::new(Self {
550 watches: Mutex::new(
551 watches
552 .into_iter()
553 .map(|watch| WatchRuntime {
554 watch,
555 matched: 0,
556 last_match: None,
557 })
558 .collect(),
559 ),
560 log: Arc::clone(&log),
561 observation: observation.clone(),
562 descendant_stop: Arc::new(
563 running_process_platform_internal::platform::process::DescendantMonitorStop::new(),
564 ),
565 exact_delivery_active: std::sync::atomic::AtomicBool::new(false),
566 delivery_tx,
567 delivery_overflow,
568 delivery_closing,
569 });
570 Ok((emitter, ProcessWatchSubscriber { log, observation }))
571 }
572
573 pub(crate) fn uses_exact_trace(&self) -> bool {
574 self.observation.grade == ObservationGrade::ExactTrace
575 }
576
577 pub(crate) fn emit_exact(&self, native: ExactTraceEvent) {
578 if self
579 .exact_delivery_active
580 .compare_exchange(
581 false,
582 true,
583 std::sync::atomic::Ordering::AcqRel,
584 std::sync::atomic::Ordering::Acquire,
585 )
586 .is_err()
587 {
588 let mut log = self.log.state.lock().unwrap_or_else(|e| e.into_inner());
589 log.coverage_complete = false;
590 return;
591 }
592 struct DeliveryGuard<'a>(&'a std::sync::atomic::AtomicBool);
593 impl Drop for DeliveryGuard<'_> {
594 fn drop(&mut self) {
595 self.0.store(false, std::sync::atomic::Ordering::Release);
596 }
597 }
598 let _delivery_guard = DeliveryGuard(&self.exact_delivery_active);
599 if let ExactTraceEventKind::Loss { reason } = &native.kind {
600 let mut log = self.log.state.lock().unwrap_or_else(|e| e.into_inner());
601 log.coverage_complete = false;
602 drop(log);
603 let event = event_from_exact(&native, self.observation.clone(), false);
604 self.queue_delivery(PendingDelivery::Loss {
605 event,
606 reason: reason.clone(),
607 });
608 return;
609 }
610 let event = event_from_exact(&native, self.observation.clone(), self.coverage_complete());
611 let now = SystemTime::now();
612 let mut watches = self.watches.lock().unwrap_or_else(|e| e.into_inner());
613 for runtime in &mut *watches {
614 if runtime
615 .watch
616 .limit
617 .is_some_and(|limit| runtime.matched >= limit)
618 || !selector_matches(&runtime.watch.selector, &event)
619 || runtime.last_match.is_some_and(|last| {
620 now.duration_since(last).unwrap_or_default() < runtime.watch.cooldown
621 })
622 {
623 continue;
624 }
625 runtime.matched += 1;
626 runtime.last_match = Some(now);
627 let pending = PendingMatch {
628 watch: runtime.watch.clone(),
629 event: event.clone(),
630 dump_request: runtime.watch.dump.clone(),
631 native: native.clone(),
632 };
633 self.queue_delivery(PendingDelivery::Match(Box::new(pending)));
634 }
635 }
636
637 pub(crate) fn emit_inferred(&self, pid: u32, started: bool) {
638 let command_line = super::read_process_cmdline(pid).ok();
639 let executable = command_line
640 .as_deref()
641 .and_then(|line| line.split_ascii_whitespace().next())
642 .filter(|value| !value.is_empty())
643 .map(PathBuf::from);
644 let event = ProcessEvent {
645 kind: if started {
646 ProcessEventKind::Spawn
647 } else {
648 ProcessEventKind::Exit
649 },
650 process: ProcessIdentity {
651 pid,
652 start_key: None,
653 },
654 parent: None,
655 timestamp: SystemTime::now(),
656 executable,
657 argv: command_line.map(|line| vec![line]),
658 exit_code: None,
659 signal: None,
660 raw_exit_status: None,
661 backend: self.observation.backend,
662 observation_grade: self.observation.grade,
663 coverage_complete: false,
664 loss_detected: false,
665 };
666 self.emit_inferred_event(event.clone());
667 if started {
668 self.emit_inferred_event(ProcessEvent {
669 kind: ProcessEventKind::Exec,
670 ..event
671 });
672 }
673 }
674
675 fn emit_inferred_event(&self, event: ProcessEvent) {
676 let mut watches = self.watches.lock().unwrap_or_else(|e| e.into_inner());
677 let now = SystemTime::now();
678 for runtime in &mut *watches {
679 if runtime
680 .watch
681 .limit
682 .is_some_and(|limit| runtime.matched >= limit)
683 || !selector_matches(&runtime.watch.selector, &event)
684 || runtime.last_match.is_some_and(|last| {
685 now.duration_since(last).unwrap_or_default() < runtime.watch.cooldown
686 })
687 {
688 continue;
689 }
690 runtime.matched += 1;
691 runtime.last_match = Some(now);
692 let dump = runtime.watch.dump.as_ref().map(|request| DumpResult {
693 capture_source: CaptureSource::None,
694 artifacts: Vec::new(),
695 symbolized: false,
696 error: Some(match request.capture {
697 StackCapture::OwnerAllThreads => {
698 "owner all-thread capture is unavailable from this backend".to_owned()
699 }
700 _ => "origin capture requires an exact trace backend".to_owned(),
701 }),
702 });
703 self.push(runtime.watch.clone(), event.clone(), dump);
704 }
705 }
706
707 pub(crate) fn descendant_stop(
708 &self,
709 ) -> Arc<running_process_platform_internal::platform::process::DescendantMonitorStop> {
710 Arc::clone(&self.descendant_stop)
711 }
712
713 pub(crate) fn close(&self) {
714 self.descendant_stop.stop();
715 self.finish_delivery();
716 }
717
718 pub(crate) fn finish_delivery(&self) {
719 self.delivery_closing
720 .store(true, std::sync::atomic::Ordering::Release);
721 self.log.wake.notify_all();
722 }
723
724 fn coverage_complete(&self) -> bool {
725 self.log
726 .state
727 .lock()
728 .unwrap_or_else(|e| e.into_inner())
729 .coverage_complete
730 }
731
732 fn push(&self, watch: ProcessWatch, event: ProcessEvent, dump: Option<DumpResult>) {
733 push_match(&self.log, watch, event, dump);
734 }
735
736 fn queue_delivery(&self, delivery: PendingDelivery) {
737 let mut overflow = self
742 .delivery_overflow
743 .lock()
744 .unwrap_or_else(|error| error.into_inner());
745 if let Some(pending) = overflow.as_mut() {
746 delivery.merge_into(pending);
747 self.mark_coverage_incomplete();
748 return;
749 }
750 match self.delivery_tx.try_send(delivery) {
751 Ok(()) => {}
752 Err(std::sync::mpsc::TrySendError::Full(delivery)) => {
753 *overflow = Some(delivery.into_overflow());
754 self.mark_coverage_incomplete();
755 }
756 Err(std::sync::mpsc::TrySendError::Disconnected(delivery)) => {
757 self.mark_coverage_incomplete();
758 let pending = delivery.into_overflow();
759 push_loss(&self.log, pending.event, pending.reason);
760 }
761 }
762 }
763
764 fn mark_coverage_incomplete(&self) {
765 self.log
766 .state
767 .lock()
768 .unwrap_or_else(|error| error.into_inner())
769 .coverage_complete = false;
770 }
771}
772
773fn delivery_loop(
774 receiver: std::sync::mpsc::Receiver<PendingDelivery>,
775 log: Arc<SharedLog>,
776 closing: Arc<std::sync::atomic::AtomicBool>,
777 overflow: Arc<Mutex<Option<PendingOverflow>>>,
778) {
779 loop {
780 match receiver.recv_timeout(Duration::from_millis(10)) {
781 Ok(PendingDelivery::Match(pending)) => {
782 let pending = *pending;
783 let dump = pending
784 .dump_request
785 .as_ref()
786 .map(|request| write_dump(request, &pending.watch.label, &pending.native));
787 push_match(&log, pending.watch, pending.event, dump);
788 }
789 Ok(PendingDelivery::Loss { event, reason }) => {
790 push_loss(&log, event, reason);
791 }
792 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
793 let pending = overflow
794 .lock()
795 .unwrap_or_else(|error| error.into_inner())
796 .take();
797 if let Some(mut pending) = pending {
798 if pending.additional_dropped != 0 {
799 pending.reason.push_str(&format!(
800 "; {} additional deliveries dropped",
801 pending.additional_dropped
802 ));
803 }
804 for reason in pending.native_loss_reasons {
805 pending.reason.push_str("; native trace loss: ");
806 pending.reason.push_str(&reason);
807 }
808 push_loss(&log, pending.event, pending.reason);
809 }
810 if closing.load(std::sync::atomic::Ordering::Acquire) {
811 let mut state = log.state.lock().unwrap_or_else(|e| e.into_inner());
812 state.closed = true;
813 log.wake.notify_all();
814 return;
815 }
816 }
817 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
818 let mut state = log.state.lock().unwrap_or_else(|e| e.into_inner());
819 state.closed = true;
820 log.wake.notify_all();
821 return;
822 }
823 }
824 }
825}
826
827fn push_loss(log: &SharedLog, event: ProcessEvent, reason: String) {
828 let mut state = log.state.lock().unwrap_or_else(|e| e.into_inner());
829 if state.closed {
830 return;
831 }
832 let sequence = state.next_sequence;
833 state.next_sequence += 1;
834 state
835 .entries
836 .push_back(ProcessWatchRecord::Loss(ProcessWatchLoss {
837 sequence,
838 event,
839 reason,
840 }));
841 trim_log(&mut state);
842 log.wake.notify_all();
843}
844
845fn push_match(log: &SharedLog, watch: ProcessWatch, event: ProcessEvent, dump: Option<DumpResult>) {
846 let mut state = log.state.lock().unwrap_or_else(|e| e.into_inner());
847 if state.closed {
848 return;
849 }
850 let sequence = state.next_sequence;
851 state.next_sequence += 1;
852 state
853 .entries
854 .push_back(ProcessWatchRecord::Match(ProcessWatchMatch {
855 sequence,
856 watch,
857 event,
858 dump,
859 }));
860 trim_log(&mut state);
861 log.wake.notify_all();
862}
863
864fn trim_log(log: &mut LogState) {
865 while log.entries.len() > DEFAULT_RETAINED_MATCHES {
866 log.entries.pop_front();
867 log.first_sequence += 1;
868 }
869}
870
871pub struct ProcessWatchSubscriber {
872 log: Arc<SharedLog>,
873 observation: ProcessObservation,
874}
875
876impl ProcessWatchSubscriber {
877 pub fn observation(&self) -> &ProcessObservation {
878 &self.observation
879 }
880
881 pub fn snapshot(&self) -> Vec<ProcessWatchMatch> {
882 self.log
883 .state
884 .lock()
885 .unwrap_or_else(|e| e.into_inner())
886 .entries
887 .iter()
888 .filter_map(|record| match record {
889 ProcessWatchRecord::Match(item) => Some(item.clone()),
890 ProcessWatchRecord::Loss(_) => None,
891 })
892 .collect()
893 }
894
895 pub fn cursor(&self) -> ProcessWatchCursor {
896 let next_sequence = self
897 .log
898 .state
899 .lock()
900 .unwrap_or_else(|e| e.into_inner())
901 .first_sequence;
902 ProcessWatchCursor {
903 log: Arc::clone(&self.log),
904 next_sequence,
905 }
906 }
907}
908
909pub struct ProcessWatchCursor {
910 log: Arc<SharedLog>,
911 next_sequence: u64,
912}
913
914impl ProcessWatchCursor {
915 pub fn read_next(&mut self, timeout: Option<Duration>) -> ProcessWatchRead {
916 let mut state = self.log.state.lock().unwrap_or_else(|e| e.into_inner());
917 let deadline = timeout.map(|duration| std::time::Instant::now() + duration);
918 loop {
919 if self.next_sequence < state.first_sequence {
920 let gap = ProcessWatchGap {
921 first_missing: self.next_sequence,
922 last_missing: state.first_sequence - 1,
923 };
924 self.next_sequence = state.first_sequence;
925 return ProcessWatchRead::Gap(gap);
926 }
927 if self.next_sequence < state.next_sequence {
928 let index = (self.next_sequence - state.first_sequence) as usize;
929 let item = state.entries[index].clone();
930 self.next_sequence += 1;
931 return match item {
932 ProcessWatchRecord::Match(item) => ProcessWatchRead::Match(Box::new(item)),
933 ProcessWatchRecord::Loss(item) => ProcessWatchRead::Loss(Box::new(item)),
934 };
935 }
936 if state.closed {
937 return ProcessWatchRead::Eof;
938 }
939 state = if let Some(deadline) = deadline {
940 let Some(remaining) = deadline.checked_duration_since(std::time::Instant::now())
941 else {
942 return ProcessWatchRead::Timeout;
943 };
944 let (state, result) = self
945 .log
946 .wake
947 .wait_timeout(state, remaining)
948 .unwrap_or_else(|e| e.into_inner());
949 if result.timed_out() {
950 return ProcessWatchRead::Timeout;
951 }
952 state
953 } else {
954 self.log.wake.wait(state).unwrap_or_else(|e| e.into_inner())
955 };
956 }
957 }
958}
959
960fn event_from_exact(
961 native: &ExactTraceEvent,
962 observation: ProcessObservation,
963 coverage_complete: bool,
964) -> ProcessEvent {
965 let (kind, exit_code, signal, raw_exit_status, loss_detected) = match &native.kind {
966 ExactTraceEventKind::Spawn => (ProcessEventKind::Spawn, None, None, None, false),
967 ExactTraceEventKind::Exec => (ProcessEventKind::Exec, None, None, None, false),
968 ExactTraceEventKind::Exit {
969 exit_code,
970 signal,
971 raw_status,
972 } => (
973 ProcessEventKind::Exit,
974 *exit_code,
975 *signal,
976 Some(*raw_status),
977 false,
978 ),
979 ExactTraceEventKind::Loss { .. } => (ProcessEventKind::Loss, None, None, None, true),
980 };
981 ProcessEvent {
982 kind,
983 process: ProcessIdentity {
984 pid: native.pid,
985 start_key: native.start_key,
986 },
987 parent: native.parent_pid.map(|pid| ProcessIdentity {
988 pid,
989 start_key: native.parent_start_key,
990 }),
991 timestamp: native.timestamp,
992 executable: native.executable.clone(),
993 argv: native.argv.as_ref().map(|args| {
994 args.iter()
995 .map(|arg| arg.to_string_lossy().into_owned())
996 .collect()
997 }),
998 exit_code,
999 signal,
1000 raw_exit_status,
1001 backend: observation.backend,
1002 observation_grade: observation.grade,
1003 coverage_complete,
1004 loss_detected,
1005 }
1006}
1007
1008fn selector_matches(selector: &WatchSelector, event: &ProcessEvent) -> bool {
1009 match selector {
1010 WatchSelector::Spawn => event.kind == ProcessEventKind::Spawn,
1011 WatchSelector::Exec { basename, path } => {
1012 event.kind == ProcessEventKind::Exec
1013 && executable_matches(
1014 event.executable.as_deref(),
1015 basename.as_deref(),
1016 path.as_deref(),
1017 )
1018 }
1019 WatchSelector::Exit {
1020 code,
1021 signal,
1022 basename,
1023 failure_only,
1024 } => {
1025 event.kind == ProcessEventKind::Exit
1026 && executable_matches(event.executable.as_deref(), basename.as_deref(), None)
1027 && code.is_none_or(|wanted| exit_code_matches(wanted, event.exit_code))
1028 && signal.is_none_or(|wanted| event.signal == Some(wanted))
1029 && (!failure_only
1030 || event.signal.is_some()
1031 || event.exit_code.is_some_and(|c| c != 0))
1032 }
1033 }
1034}
1035
1036fn executable_matches(
1037 executable: Option<&Path>,
1038 basename: Option<&str>,
1039 exact_path: Option<&Path>,
1040) -> bool {
1041 if let Some(expected) = exact_path {
1042 return executable == Some(expected);
1043 }
1044 if let Some(expected) = basename {
1045 return executable
1046 .and_then(Path::file_name)
1047 .and_then(|name| name.to_str())
1048 == Some(expected);
1049 }
1050 true
1051}
1052
1053fn exit_code_matches(requested: i32, observed: Option<i32>) -> bool {
1054 if requested == -1 {
1055 return observed == Some(255) || observed == Some(-1);
1056 }
1057 observed == Some(requested)
1058}
1059
1060fn write_dump(request: &StackDump, label: &str, event: &ExactTraceEvent) -> DumpResult {
1061 if request.capture == StackCapture::OwnerAllThreads {
1062 return DumpResult {
1063 capture_source: CaptureSource::None,
1064 artifacts: Vec::new(),
1065 symbolized: false,
1066 error: Some("owner all-thread capture is unavailable for this event".to_owned()),
1067 };
1068 }
1069 let Some(origin) = event.origin.as_ref() else {
1070 return DumpResult {
1071 capture_source: CaptureSource::None,
1072 artifacts: Vec::new(),
1073 symbolized: false,
1074 error: Some(match request.capture {
1075 StackCapture::OwnerAllThreads => {
1076 "owner all-thread capture is unavailable for this event".to_owned()
1077 }
1078 _ => "spawning-thread origin capture is unavailable for this event".to_owned(),
1079 }),
1080 };
1081 };
1082 let directory = request
1083 .directory
1084 .clone()
1085 .unwrap_or_else(|| std::env::temp_dir().join("running-process-watch"));
1086 if let Err(error) = std::fs::create_dir_all(&directory) {
1087 return dump_error(error);
1088 }
1089 let safe_label: String = label
1090 .chars()
1091 .map(|character| {
1092 if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
1093 character
1094 } else {
1095 '_'
1096 }
1097 })
1098 .collect();
1099 let path = directory.join(format!(
1100 "{safe_label}-{}-{}.rpstack",
1101 event.pid, event.sequence
1102 ));
1103 let bytes = render_origin(origin);
1104 if let Err(error) = std::fs::write(&path, bytes) {
1105 return dump_error(error);
1106 }
1107 DumpResult {
1108 capture_source: CaptureSource::RemoteSpawningThread,
1109 artifacts: vec![path],
1110 symbolized: false,
1111 error: request.symbolize_immediately.then(|| {
1112 "immediate remote symbolization is unavailable; retained a deferred raw artifact"
1113 .to_owned()
1114 }),
1115 }
1116}
1117
1118fn dump_error(error: std::io::Error) -> DumpResult {
1119 DumpResult {
1120 capture_source: CaptureSource::None,
1121 artifacts: Vec::new(),
1122 symbolized: false,
1123 error: Some(error.to_string()),
1124 }
1125}
1126
1127fn render_origin(origin: &TraceOriginArtifact) -> Vec<u8> {
1128 let mut text = format!(
1129 "format=running-process-origin-v2\norigin_pid={}\nthread_id={}\narchitecture={}\nregister_format={}\norigin_executable={:?}\nstack_pointer={:?}\ninstruction_pointer={:?}\nregister_bytes={}\nstack_bytes={}\nstack_truncated={}\nmodule_map_bytes={}\nmodule_map_truncated={}\nregisters=",
1130 origin.origin_pid,
1131 origin.thread_id,
1132 origin.architecture,
1133 origin.register_format,
1134 origin.executable,
1135 origin.stack_pointer,
1136 origin.instruction_pointer,
1137 origin.registers.len(),
1138 origin.stack.len(),
1139 origin.truncated,
1140 origin.module_map.len(),
1141 origin.module_map_truncated,
1142 );
1143 push_hex(&mut text, &origin.registers);
1144 text.push_str("\nstack=");
1145 push_hex(&mut text, &origin.stack);
1146 text.push_str("\nmodule_map=");
1147 push_hex(&mut text, &origin.module_map);
1148 text.push('\n');
1149 text.into_bytes()
1150}
1151
1152fn push_hex(output: &mut String, bytes: &[u8]) {
1153 use std::fmt::Write;
1154 for byte in bytes {
1155 let _ = write!(output, "{byte:02x}");
1156 }
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161 use super::*;
1162
1163 #[test]
1164 fn minus_one_matches_unix_truncated_status() {
1165 assert!(exit_code_matches(-1, Some(255)));
1166 assert!(exit_code_matches(-1, Some(-1)));
1167 assert!(!exit_code_matches(-1, Some(254)));
1168 assert!(!exit_code_matches(-1, None));
1169 }
1170
1171 #[test]
1172 fn ambiguous_exec_selector_is_rejected() {
1173 assert!(ProcessWatch::on_exec(
1174 Some("soldr".to_owned()),
1175 Some(PathBuf::from("/usr/bin/soldr")),
1176 None,
1177 Some(1),
1178 Duration::ZERO,
1179 "recursive-soldr",
1180 )
1181 .is_err());
1182 }
1183
1184 #[test]
1185 fn bounded_log_reports_an_explicit_cursor_gap() {
1186 let watch = ProcessWatch::on_spawn(None, None, Duration::ZERO, "all-spawns").unwrap();
1187 let (emitter, subscriber) =
1188 ProcessWatchEmitter::new(vec![watch], ObservationPolicy::NonInvasive).unwrap();
1189 let mut cursor = subscriber.cursor();
1190 for pid in 1..=(DEFAULT_RETAINED_MATCHES as u32 + 7) {
1191 emitter.emit_inferred(pid, true);
1192 }
1193 let ProcessWatchRead::Gap(gap) = cursor.read_next(Some(Duration::ZERO)) else {
1194 panic!("expected an explicit cursor gap");
1195 };
1196 assert_eq!(gap.first_missing, 1);
1197 assert_eq!(gap.last_missing, 7);
1198 }
1199
1200 #[test]
1201 fn limit_and_cooldown_bound_matching() {
1202 let limited = ProcessWatch::on_spawn(None, Some(2), Duration::ZERO, "limited").unwrap();
1203 let cooled = ProcessWatch::on_spawn(None, None, Duration::from_secs(60), "cooled").unwrap();
1204 let (emitter, subscriber) =
1205 ProcessWatchEmitter::new(vec![limited, cooled], ObservationPolicy::NonInvasive)
1206 .unwrap();
1207 for pid in 1..=4 {
1208 emitter.emit_inferred(pid, true);
1209 }
1210 let matches = subscriber.snapshot();
1211 assert_eq!(
1212 matches
1213 .iter()
1214 .filter(|item| item.watch.label == "limited")
1215 .count(),
1216 2
1217 );
1218 assert_eq!(
1219 matches
1220 .iter()
1221 .filter(|item| item.watch.label == "cooled")
1222 .count(),
1223 1
1224 );
1225 }
1226
1227 #[test]
1228 fn native_loss_is_delivered_without_a_matching_selector() {
1229 let watch = ProcessWatch::on_spawn(None, Some(1), Duration::ZERO, "spawn").unwrap();
1230 let (emitter, subscriber) =
1231 ProcessWatchEmitter::new(vec![watch], ObservationPolicy::NonInvasive).unwrap();
1232 let mut cursor = subscriber.cursor();
1233 emitter.emit_exact(ExactTraceEvent {
1234 sequence: 7,
1235 pid: 42,
1236 parent_pid: None,
1237 parent_start_key: None,
1238 start_key: Some(11),
1239 timestamp: SystemTime::now(),
1240 kind: ExactTraceEventKind::Loss {
1241 reason: "resume failed".to_owned(),
1242 },
1243 executable: None,
1244 argv: None,
1245 origin: None,
1246 });
1247 let ProcessWatchRead::Loss(loss) = cursor.read_next(Some(Duration::from_secs(1))) else {
1248 panic!("expected an explicit loss record");
1249 };
1250 assert_eq!(loss.reason, "resume failed");
1251 assert!(loss.event.loss_detected);
1252 assert!(!loss.event.coverage_complete);
1253 }
1254}