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