1use std::collections::{HashMap, VecDeque};
8use std::sync::RwLock;
9use std::sync::atomic::AtomicBool;
10use std::time::{Duration, Instant};
11
12use serde::Serialize;
13
14#[derive(Debug, Clone, Serialize)]
16pub struct CommandTimingStats {
17 pub command: String,
19 pub count: u64,
21 pub min_ms: f64,
23 pub max_ms: f64,
25 pub avg_ms: f64,
27 pub p95_ms: f64,
29 pub total_ms: f64,
31}
32
33#[derive(Debug, Default)]
35pub struct TimingSamples {
36 pub samples: Vec<Duration>,
38}
39
40impl TimingSamples {
41 pub fn record(&mut self, duration: Duration) {
43 self.samples.push(duration);
44 }
45
46 #[must_use]
48 pub fn stats(&self, command: &str) -> CommandTimingStats {
49 if self.samples.is_empty() {
50 return CommandTimingStats {
51 command: command.to_string(),
52 count: 0,
53 min_ms: 0.0,
54 max_ms: 0.0,
55 avg_ms: 0.0,
56 p95_ms: 0.0,
57 total_ms: 0.0,
58 };
59 }
60 let mut sorted: Vec<f64> = self
61 .samples
62 .iter()
63 .map(|d| d.as_secs_f64() * 1000.0)
64 .collect();
65 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
66
67 let count = sorted.len() as u64;
68 let total: f64 = sorted.iter().sum();
69 let min = sorted[0];
70 let max = sorted[sorted.len() - 1];
71 let avg = total / sorted.len() as f64;
72 let p95_idx = ((sorted.len() as f64) * 0.95).ceil() as usize;
73 let p95 = sorted[p95_idx.min(sorted.len() - 1)];
74
75 CommandTimingStats {
76 command: command.to_string(),
77 count,
78 min_ms: (min * 100.0).round() / 100.0,
79 max_ms: (max * 100.0).round() / 100.0,
80 avg_ms: (avg * 100.0).round() / 100.0,
81 p95_ms: (p95 * 100.0).round() / 100.0,
82 total_ms: (total * 100.0).round() / 100.0,
83 }
84 }
85}
86
87pub struct CommandTimings {
89 inner: RwLock<HashMap<String, TimingSamples>>,
90}
91
92impl CommandTimings {
93 #[must_use]
95 pub fn new() -> Self {
96 Self {
97 inner: RwLock::new(HashMap::new()),
98 }
99 }
100
101 pub fn record(&self, command: &str, duration: Duration) {
103 let mut map = self
104 .inner
105 .write()
106 .unwrap_or_else(std::sync::PoisonError::into_inner);
107 map.entry(command.to_string()).or_default().record(duration);
108 }
109
110 #[must_use]
112 pub fn all_stats(&self) -> Vec<CommandTimingStats> {
113 let map = self
114 .inner
115 .read()
116 .unwrap_or_else(std::sync::PoisonError::into_inner);
117 let mut stats: Vec<CommandTimingStats> =
118 map.iter().map(|(name, s)| s.stats(name)).collect();
119 stats.sort_by(|a, b| {
120 b.total_ms
121 .partial_cmp(&a.total_ms)
122 .unwrap_or(std::cmp::Ordering::Equal)
123 });
124 stats
125 }
126
127 #[must_use]
129 pub fn stats_for(&self, command: &str) -> Option<CommandTimingStats> {
130 let map = self
131 .inner
132 .read()
133 .unwrap_or_else(std::sync::PoisonError::into_inner);
134 map.get(command).map(|s| s.stats(command))
135 }
136
137 pub fn clear(&self) {
139 let mut map = self
140 .inner
141 .write()
142 .unwrap_or_else(std::sync::PoisonError::into_inner);
143 map.clear();
144 }
145}
146
147impl Default for CommandTimings {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153#[derive(Debug, Clone, Serialize)]
157pub enum FaultType {
158 Delay {
160 delay_ms: u64,
162 },
163 Error {
165 message: String,
167 },
168 Drop,
170 Corrupt,
172}
173
174#[derive(Debug, Clone, Serialize)]
176pub struct FaultConfig {
177 pub command: String,
179 pub fault_type: FaultType,
181 pub trigger_count: u64,
183 pub max_triggers: u64,
185 #[serde(skip)]
187 pub created_at: Instant,
188}
189
190pub const FAULT_TTL: Duration = Duration::from_secs(900); impl FaultConfig {
195 #[must_use]
198 pub fn should_trigger_at(&self, now: Instant) -> bool {
199 if now.saturating_duration_since(self.created_at) >= FAULT_TTL {
200 return false;
201 }
202 self.max_triggers == 0 || self.trigger_count < self.max_triggers
203 }
204
205 #[must_use]
207 pub fn should_trigger(&self) -> bool {
208 self.should_trigger_at(Instant::now())
209 }
210}
211
212pub struct FaultRegistry {
214 inner: RwLock<HashMap<String, FaultConfig>>,
215}
216
217impl FaultRegistry {
218 #[must_use]
220 pub fn new() -> Self {
221 Self {
222 inner: RwLock::new(HashMap::new()),
223 }
224 }
225
226 pub fn inject(&self, config: FaultConfig) {
228 let mut map = self
229 .inner
230 .write()
231 .unwrap_or_else(std::sync::PoisonError::into_inner);
232 map.insert(config.command.clone(), config);
233 }
234
235 pub fn check_and_trigger(&self, command: &str) -> Option<FaultType> {
238 let mut map = self
239 .inner
240 .write()
241 .unwrap_or_else(std::sync::PoisonError::into_inner);
242 if let Some(config) = map.get_mut(command)
243 && config.should_trigger()
244 {
245 config.trigger_count += 1;
246 return Some(config.fault_type.clone());
247 }
248 None
249 }
250
251 #[must_use]
253 pub fn list(&self) -> Vec<FaultConfig> {
254 let map = self
255 .inner
256 .read()
257 .unwrap_or_else(std::sync::PoisonError::into_inner);
258 map.values().cloned().collect()
259 }
260
261 pub fn clear(&self, command: &str) -> bool {
263 let mut map = self
264 .inner
265 .write()
266 .unwrap_or_else(std::sync::PoisonError::into_inner);
267 map.remove(command).is_some()
268 }
269
270 pub fn clear_all(&self) -> usize {
272 let mut map = self
273 .inner
274 .write()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 let count = map.len();
277 map.clear();
278 count
279 }
280}
281
282impl Default for FaultRegistry {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288#[derive(Debug, Clone, Serialize, PartialEq)]
292pub enum JsonShape {
293 Null,
295 Bool,
297 Number,
299 String,
301 Array(Box<Self>),
303 Object(HashMap<String, Self>),
305}
306
307impl JsonShape {
308 #[must_use]
310 pub fn from_value(value: &serde_json::Value) -> Self {
311 match value {
312 serde_json::Value::Null => Self::Null,
313 serde_json::Value::Bool(_) => Self::Bool,
314 serde_json::Value::Number(_) => Self::Number,
315 serde_json::Value::String(_) => Self::String,
316 serde_json::Value::Array(arr) => {
317 let elem = arr.first().map_or(Self::Null, Self::from_value);
318 Self::Array(Box::new(elem))
319 }
320 serde_json::Value::Object(obj) => {
321 let fields: HashMap<String, Self> = obj
322 .iter()
323 .map(|(k, v)| (k.clone(), Self::from_value(v)))
324 .collect();
325 Self::Object(fields)
326 }
327 }
328 }
329
330 #[must_use]
332 pub fn type_name(&self) -> &'static str {
333 match self {
334 Self::Null => "null",
335 Self::Bool => "bool",
336 Self::Number => "number",
337 Self::String => "string",
338 Self::Array(_) => "array",
339 Self::Object(_) => "object",
340 }
341 }
342}
343
344#[derive(Debug, Clone, Serialize)]
346pub struct ContractBaseline {
347 pub command: String,
349 pub args: serde_json::Value,
351 pub shape: JsonShape,
353 pub sample: String,
355 pub recorded_at: String,
357}
358
359#[derive(Debug, Clone, Serialize)]
361pub struct ContractDrift {
362 pub command: String,
364 pub new_fields: Vec<String>,
366 pub removed_fields: Vec<String>,
368 pub type_changes: Vec<TypeChange>,
370 pub shape_matches: bool,
372}
373
374#[derive(Debug, Clone, Serialize)]
376pub struct TypeChange {
377 pub path: String,
379 pub baseline_type: String,
381 pub current_type: String,
383}
384
385#[must_use]
387pub fn diff_shapes(baseline: &JsonShape, current: &JsonShape, prefix: &str) -> ContractDrift {
388 let mut new_fields = Vec::new();
389 let mut removed_fields = Vec::new();
390 let mut type_changes = Vec::new();
391
392 diff_shapes_inner(
393 baseline,
394 current,
395 prefix,
396 &mut new_fields,
397 &mut removed_fields,
398 &mut type_changes,
399 );
400
401 let shape_matches =
402 new_fields.is_empty() && removed_fields.is_empty() && type_changes.is_empty();
403 ContractDrift {
404 command: prefix.to_string(),
405 new_fields,
406 removed_fields,
407 type_changes,
408 shape_matches,
409 }
410}
411
412fn diff_shapes_inner(
413 baseline: &JsonShape,
414 current: &JsonShape,
415 prefix: &str,
416 new_fields: &mut Vec<String>,
417 removed_fields: &mut Vec<String>,
418 type_changes: &mut Vec<TypeChange>,
419) {
420 match (baseline, current) {
421 (JsonShape::Object(b_fields), JsonShape::Object(c_fields)) => {
422 for (key, b_shape) in b_fields {
423 let path = if prefix.is_empty() {
424 key.clone()
425 } else {
426 format!("{prefix}.{key}")
427 };
428 if let Some(c_shape) = c_fields.get(key) {
429 diff_shapes_inner(
430 b_shape,
431 c_shape,
432 &path,
433 new_fields,
434 removed_fields,
435 type_changes,
436 );
437 } else {
438 removed_fields.push(path);
439 }
440 }
441 for key in c_fields.keys() {
442 if !b_fields.contains_key(key) {
443 let path = if prefix.is_empty() {
444 key.clone()
445 } else {
446 format!("{prefix}.{key}")
447 };
448 new_fields.push(path);
449 }
450 }
451 }
452 (JsonShape::Array(b_elem), JsonShape::Array(c_elem)) => {
453 let path = format!("{prefix}[]");
454 diff_shapes_inner(
455 b_elem,
456 c_elem,
457 &path,
458 new_fields,
459 removed_fields,
460 type_changes,
461 );
462 }
463 (b, c) if b.type_name() != c.type_name() => {
464 type_changes.push(TypeChange {
465 path: prefix.to_string(),
466 baseline_type: b.type_name().to_string(),
467 current_type: c.type_name().to_string(),
468 });
469 }
470 _ => {}
471 }
472}
473
474pub struct ContractStore {
476 inner: RwLock<HashMap<String, ContractBaseline>>,
477}
478
479impl ContractStore {
480 #[must_use]
482 pub fn new() -> Self {
483 Self {
484 inner: RwLock::new(HashMap::new()),
485 }
486 }
487
488 pub fn record(&self, baseline: ContractBaseline) {
490 let mut map = self
491 .inner
492 .write()
493 .unwrap_or_else(std::sync::PoisonError::into_inner);
494 map.insert(baseline.command.clone(), baseline);
495 }
496
497 #[must_use]
499 pub fn get(&self, command: &str) -> Option<ContractBaseline> {
500 let map = self
501 .inner
502 .read()
503 .unwrap_or_else(std::sync::PoisonError::into_inner);
504 map.get(command).cloned()
505 }
506
507 #[must_use]
509 pub fn all(&self) -> Vec<ContractBaseline> {
510 let map = self
511 .inner
512 .read()
513 .unwrap_or_else(std::sync::PoisonError::into_inner);
514 map.values().cloned().collect()
515 }
516
517 pub fn clear(&self) -> usize {
519 let mut map = self
520 .inner
521 .write()
522 .unwrap_or_else(std::sync::PoisonError::into_inner);
523 let count = map.len();
524 map.clear();
525 count
526 }
527}
528
529impl Default for ContractStore {
530 fn default() -> Self {
531 Self::new()
532 }
533}
534
535#[derive(Debug, Clone, Serialize)]
539pub struct StartupPhase {
540 pub name: String,
542 pub duration_ms: f64,
544 pub cumulative_ms: f64,
546}
547
548pub struct StartupTimeline {
550 start: Instant,
551 phases: RwLock<Vec<(String, Instant)>>,
552}
553
554impl StartupTimeline {
555 #[must_use]
557 pub fn new() -> Self {
558 Self {
559 start: Instant::now(),
560 phases: RwLock::new(Vec::new()),
561 }
562 }
563
564 pub fn mark(&self, name: &str) {
566 let mut phases = self
567 .phases
568 .write()
569 .unwrap_or_else(std::sync::PoisonError::into_inner);
570 phases.push((name.to_string(), Instant::now()));
571 }
572
573 #[must_use]
575 pub fn report(&self) -> Vec<StartupPhase> {
576 let phases = self
577 .phases
578 .read()
579 .unwrap_or_else(std::sync::PoisonError::into_inner);
580 let mut result = Vec::new();
581 let mut prev = self.start;
582
583 for (name, instant) in phases.iter() {
584 let duration = instant.duration_since(prev);
585 let cumulative = instant.duration_since(self.start);
586 result.push(StartupPhase {
587 name: name.clone(),
588 duration_ms: (duration.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
589 cumulative_ms: (cumulative.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
590 });
591 prev = *instant;
592 }
593 result
594 }
595
596 #[must_use]
598 pub fn total_ms(&self) -> f64 {
599 let phases = self
600 .phases
601 .read()
602 .unwrap_or_else(std::sync::PoisonError::into_inner);
603 if let Some((_, last)) = phases.last() {
604 (last.duration_since(self.start).as_secs_f64() * 1000.0 * 100.0).round() / 100.0
605 } else {
606 0.0
607 }
608 }
609}
610
611impl Default for StartupTimeline {
612 fn default() -> Self {
613 Self::new()
614 }
615}
616
617#[derive(Debug, Clone, Serialize)]
621pub struct CapturedTauriEvent {
622 pub name: String,
624 pub payload: String,
626 pub timestamp: String,
628}
629
630const DEFAULT_EVENT_BUS_CAPACITY: usize = 1000;
631
632#[derive(Clone)]
634pub struct EventBusMonitor {
635 inner: std::sync::Arc<RwLock<VecDeque<CapturedTauriEvent>>>,
636 capacity: usize,
637}
638
639impl EventBusMonitor {
640 #[must_use]
642 pub fn new(capacity: usize) -> Self {
643 Self {
644 inner: std::sync::Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
645 capacity,
646 }
647 }
648
649 pub fn push(&self, event: CapturedTauriEvent) {
651 let mut buf = self
652 .inner
653 .write()
654 .unwrap_or_else(std::sync::PoisonError::into_inner);
655 if buf.len() >= self.capacity {
656 buf.pop_front();
657 }
658 buf.push_back(event);
659 }
660
661 #[must_use]
663 pub fn events(&self) -> Vec<CapturedTauriEvent> {
664 self.inner
665 .read()
666 .unwrap_or_else(std::sync::PoisonError::into_inner)
667 .iter()
668 .cloned()
669 .collect()
670 }
671
672 #[must_use]
674 pub fn len(&self) -> usize {
675 self.inner
676 .read()
677 .unwrap_or_else(std::sync::PoisonError::into_inner)
678 .len()
679 }
680
681 #[must_use]
683 pub fn is_empty(&self) -> bool {
684 self.len() == 0
685 }
686
687 pub fn clear(&self) -> usize {
689 let mut buf = self
690 .inner
691 .write()
692 .unwrap_or_else(std::sync::PoisonError::into_inner);
693 let count = buf.len();
694 buf.clear();
695 count
696 }
697}
698
699impl Default for EventBusMonitor {
700 fn default() -> Self {
701 Self::new(DEFAULT_EVENT_BUS_CAPACITY)
702 }
703}
704
705#[derive(Debug, Clone, Serialize)]
709pub struct TrackedTaskInfo {
710 pub name: String,
712 pub spawned_at: String,
714 pub is_finished: bool,
716 pub uptime_secs: u64,
718}
719
720struct TrackedTaskEntry {
721 name: String,
722 spawned_at: Instant,
723 spawned_at_wall: String,
724 finished: std::sync::Arc<AtomicBool>,
725}
726
727pub struct TaskTracker {
729 tasks: RwLock<Vec<TrackedTaskEntry>>,
730}
731
732impl TaskTracker {
733 #[must_use]
735 pub fn new() -> Self {
736 Self {
737 tasks: RwLock::new(Vec::new()),
738 }
739 }
740
741 pub fn track(&self, name: &str) -> std::sync::Arc<AtomicBool> {
743 let finished = std::sync::Arc::new(AtomicBool::new(false));
744 let entry = TrackedTaskEntry {
745 name: name.to_string(),
746 spawned_at: Instant::now(),
747 spawned_at_wall: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
748 finished: finished.clone(),
749 };
750 self.tasks
751 .write()
752 .unwrap_or_else(std::sync::PoisonError::into_inner)
753 .push(entry);
754 finished
755 }
756
757 #[must_use]
759 pub fn list(&self) -> Vec<TrackedTaskInfo> {
760 let tasks = self
761 .tasks
762 .read()
763 .unwrap_or_else(std::sync::PoisonError::into_inner);
764 tasks
765 .iter()
766 .map(|t| TrackedTaskInfo {
767 name: t.name.clone(),
768 spawned_at: t.spawned_at_wall.clone(),
769 is_finished: t.finished.load(std::sync::atomic::Ordering::Relaxed),
770 uptime_secs: t.spawned_at.elapsed().as_secs(),
771 })
772 .collect()
773 }
774
775 #[must_use]
777 pub fn active_count(&self) -> usize {
778 let tasks = self
779 .tasks
780 .read()
781 .unwrap_or_else(std::sync::PoisonError::into_inner);
782 tasks
783 .iter()
784 .filter(|t| !t.finished.load(std::sync::atomic::Ordering::Relaxed))
785 .count()
786 }
787}
788
789impl Default for TaskTracker {
790 fn default() -> Self {
791 Self::new()
792 }
793}
794
795#[derive(Debug, Clone, Serialize)]
799pub struct ChildProcessInfo {
800 pub pid: u32,
802 pub ppid: u32,
804 pub name: String,
806 pub memory_bytes: Option<u64>,
808}
809
810#[must_use]
817pub fn enumerate_child_processes() -> Vec<ChildProcessInfo> {
818 let my_pid = std::process::id();
819
820 #[cfg(windows)]
821 {
822 enumerate_children_windows(my_pid)
823 }
824
825 #[cfg(target_os = "linux")]
826 {
827 enumerate_children_linux(my_pid)
828 }
829
830 #[cfg(target_os = "macos")]
831 {
832 enumerate_children_macos(my_pid)
833 }
834
835 #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
836 {
837 let _ = my_pid;
838 Vec::new()
839 }
840}
841
842#[cfg(windows)]
843#[allow(unsafe_code)]
844fn enumerate_children_windows(parent_pid: u32) -> Vec<ChildProcessInfo> {
845 use windows::Win32::Foundation::CloseHandle;
846 use windows::Win32::System::Diagnostics::ToolHelp::{
847 CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS,
848 };
849
850 let mut children = Vec::new();
851
852 unsafe {
857 let Ok(snapshot) = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) else {
858 return children;
859 };
860
861 let mut entry: PROCESSENTRY32 = std::mem::zeroed();
862 entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
863
864 if Process32First(snapshot, &mut entry).is_ok() {
865 loop {
866 if entry.th32ParentProcessID == parent_pid && entry.th32ProcessID != parent_pid {
867 let name_bytes: Vec<u8> = entry
868 .szExeFile
869 .iter()
870 .take_while(|&&b| b != 0)
871 .map(|&b| b as u8)
872 .collect();
873 let name = String::from_utf8_lossy(&name_bytes).to_string();
874
875 let memory_bytes = get_process_memory_windows(entry.th32ProcessID);
876
877 children.push(ChildProcessInfo {
878 pid: entry.th32ProcessID,
879 ppid: entry.th32ParentProcessID,
880 name,
881 memory_bytes,
882 });
883 }
884
885 if Process32Next(snapshot, &mut entry).is_err() {
886 break;
887 }
888 }
889 }
890
891 let _ = CloseHandle(snapshot);
892 }
893
894 children
895}
896
897#[cfg(windows)]
898#[allow(unsafe_code)]
899fn get_process_memory_windows(pid: u32) -> Option<u64> {
900 use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
901 use windows::Win32::System::Threading::{
902 OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ,
903 };
904
905 unsafe {
909 let process = OpenProcess(
910 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
911 false,
912 pid,
913 )
914 .ok()?;
915
916 let mut counters: PROCESS_MEMORY_COUNTERS = std::mem::zeroed();
917 counters.cb = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
918
919 if GetProcessMemoryInfo(process, &mut counters, counters.cb).is_ok() {
920 Some(counters.WorkingSetSize as u64)
921 } else {
922 None
923 }
924 }
925}
926
927#[cfg(target_os = "linux")]
928fn enumerate_children_linux(parent_pid: u32) -> Vec<ChildProcessInfo> {
929 let mut children = Vec::new();
930 let Ok(entries) = std::fs::read_dir("/proc") else {
931 return children;
932 };
933
934 for entry in entries.flatten() {
935 let file_name = entry.file_name();
936 let Some(pid_str) = file_name.to_str() else {
937 continue;
938 };
939 let Ok(pid) = pid_str.parse::<u32>() else {
940 continue;
941 };
942
943 let status_path = format!("/proc/{pid}/status");
944 let Ok(status) = std::fs::read_to_string(&status_path) else {
945 continue;
946 };
947
948 let mut ppid: Option<u32> = None;
949 let mut name = String::new();
950 let mut vm_rss_kb: u64 = 0;
951
952 for line in status.lines() {
953 if let Some(v) = line.strip_prefix("PPid:\t") {
954 ppid = v.trim().parse().ok();
955 } else if let Some(v) = line.strip_prefix("Name:\t") {
956 name = v.trim().to_string();
957 } else if let Some(v) = line.strip_prefix("VmRSS:") {
958 vm_rss_kb = v
959 .split_whitespace()
960 .next()
961 .and_then(|n| n.parse().ok())
962 .unwrap_or(0);
963 }
964 }
965
966 if ppid == Some(parent_pid) {
967 children.push(ChildProcessInfo {
968 pid,
969 ppid: parent_pid,
970 name,
971 memory_bytes: if vm_rss_kb > 0 {
972 Some(vm_rss_kb * 1024)
973 } else {
974 None
975 },
976 });
977 }
978 }
979
980 children
981}
982
983#[cfg(target_os = "macos")]
984#[allow(unsafe_code)]
985fn enumerate_children_macos(parent_pid: u32) -> Vec<ChildProcessInfo> {
986 use std::mem;
987
988 unsafe extern "C" {
989 fn proc_listchildpids(ppid: i32, buffer: *mut i32, buffersize: i32) -> i32;
990 fn proc_pidinfo(pid: i32, flavor: i32, arg: u64, buffer: *mut u8, buffersize: i32) -> i32;
991 fn proc_name(pid: i32, buffer: *mut u8, buffersize: u32) -> i32;
992 }
993
994 const PROC_PIDTASKINFO: i32 = 4;
995
996 #[repr(C)]
997 struct ProcTaskInfo {
998 pti_virtual_size: u64,
999 pti_resident_size: u64,
1000 pti_total_user: u64,
1001 pti_total_system: u64,
1002 pti_threads_user: u64,
1003 pti_threads_system: u64,
1004 pti_policy: i32,
1005 pti_faults: i32,
1006 pti_pageins: i32,
1007 pti_cow_faults: i32,
1008 pti_messages_sent: i32,
1009 pti_messages_received: i32,
1010 pti_syscalls_mach: i32,
1011 pti_syscalls_unix: i32,
1012 pti_csw: i32,
1013 pti_threadnum: i32,
1014 pti_numrunning: i32,
1015 pti_priority: i32,
1016 }
1017
1018 let mut children = Vec::new();
1019
1020 unsafe {
1024 let ppid = parent_pid as i32;
1025 let mut cap = 256usize;
1032 let (pids, n) = loop {
1033 let mut pids = vec![0i32; cap];
1034 let buf_size = (cap * mem::size_of::<i32>()) as i32;
1035 let actual = proc_listchildpids(ppid, pids.as_mut_ptr(), buf_size);
1036 if actual <= 0 {
1037 return children;
1038 }
1039 let count = actual as usize;
1040 if count < cap || cap >= 65536 {
1043 break (pids, count.min(cap));
1044 }
1045 cap = (count + 16).max(cap * 2);
1046 };
1047 for &pid in &pids[..n] {
1048 if pid <= 0 {
1049 continue;
1050 }
1051
1052 let mut name_buf = [0u8; 256];
1053 let name_len = proc_name(pid, name_buf.as_mut_ptr(), 256);
1054 let name = if name_len > 0 {
1055 String::from_utf8_lossy(&name_buf[..name_len as usize]).to_string()
1056 } else {
1057 String::from("<unknown>")
1058 };
1059
1060 let mut task_info: ProcTaskInfo = mem::zeroed();
1061 let info_size = mem::size_of::<ProcTaskInfo>() as i32;
1062 let ret = proc_pidinfo(
1063 pid,
1064 PROC_PIDTASKINFO,
1065 0,
1066 &mut task_info as *mut _ as *mut u8,
1067 info_size,
1068 );
1069
1070 let memory_bytes = if ret == info_size {
1071 Some(task_info.pti_resident_size)
1072 } else {
1073 None
1074 };
1075
1076 children.push(ChildProcessInfo {
1077 pid: pid as u32,
1078 ppid: parent_pid,
1079 name,
1080 memory_bytes,
1081 });
1082 }
1083 }
1084
1085 children
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use super::*;
1091
1092 #[test]
1093 fn event_bus_push_and_read() {
1094 let bus = EventBusMonitor::new(3);
1095 assert!(bus.is_empty());
1096 bus.push(CapturedTauriEvent {
1097 name: "test".to_string(),
1098 payload: "{}".to_string(),
1099 timestamp: "2026-01-01T00:00:00Z".to_string(),
1100 });
1101 assert_eq!(bus.len(), 1);
1102 assert_eq!(bus.events()[0].name, "test");
1103 }
1104
1105 #[test]
1106 fn event_bus_ring_buffer_eviction() {
1107 let bus = EventBusMonitor::new(2);
1108 for i in 0..5 {
1109 bus.push(CapturedTauriEvent {
1110 name: format!("event_{i}"),
1111 payload: String::new(),
1112 timestamp: String::new(),
1113 });
1114 }
1115 assert_eq!(bus.len(), 2);
1116 assert_eq!(bus.events()[0].name, "event_3");
1117 assert_eq!(bus.events()[1].name, "event_4");
1118 }
1119
1120 #[test]
1121 fn event_bus_clear() {
1122 let bus = EventBusMonitor::new(10);
1123 bus.push(CapturedTauriEvent {
1124 name: "a".to_string(),
1125 payload: String::new(),
1126 timestamp: String::new(),
1127 });
1128 assert_eq!(bus.clear(), 1);
1129 assert!(bus.is_empty());
1130 }
1131
1132 #[test]
1133 fn task_tracker_lifecycle() {
1134 let tracker = TaskTracker::new();
1135 let flag = tracker.track("mcp_server");
1136 let tasks = tracker.list();
1137 assert_eq!(tasks.len(), 1);
1138 assert_eq!(tasks[0].name, "mcp_server");
1139 assert!(!tasks[0].is_finished);
1140 assert_eq!(tracker.active_count(), 1);
1141
1142 flag.store(true, std::sync::atomic::Ordering::Relaxed);
1143 let tasks = tracker.list();
1144 assert!(tasks[0].is_finished);
1145 assert_eq!(tracker.active_count(), 0);
1146 }
1147
1148 #[test]
1149 fn timing_samples_basic() {
1150 let mut samples = TimingSamples::default();
1151 samples.record(Duration::from_millis(10));
1152 samples.record(Duration::from_millis(20));
1153 samples.record(Duration::from_millis(30));
1154 let stats = samples.stats("test_cmd");
1155 assert_eq!(stats.count, 3);
1156 assert!((stats.min_ms - 10.0).abs() < 1.0);
1157 assert!((stats.max_ms - 30.0).abs() < 1.0);
1158 assert!((stats.avg_ms - 20.0).abs() < 1.0);
1159 }
1160
1161 #[test]
1162 fn timing_samples_empty() {
1163 let samples = TimingSamples::default();
1164 let stats = samples.stats("empty");
1165 assert_eq!(stats.count, 0);
1166 assert_eq!(stats.min_ms, 0.0);
1167 }
1168
1169 #[test]
1170 fn command_timings_thread_safe() {
1171 let timings = CommandTimings::new();
1172 timings.record("cmd_a", Duration::from_millis(5));
1173 timings.record("cmd_a", Duration::from_millis(15));
1174 timings.record("cmd_b", Duration::from_millis(100));
1175
1176 let all = timings.all_stats();
1177 assert_eq!(all.len(), 2);
1178 assert_eq!(all[0].command, "cmd_b");
1179
1180 let a = timings.stats_for("cmd_a").unwrap();
1181 assert_eq!(a.count, 2);
1182 }
1183
1184 #[test]
1185 fn fault_registry_lifecycle() {
1186 let registry = FaultRegistry::new();
1187 registry.inject(FaultConfig {
1188 command: "slow_cmd".to_string(),
1189 fault_type: FaultType::Delay { delay_ms: 500 },
1190 trigger_count: 0,
1191 max_triggers: 2,
1192 created_at: Instant::now(),
1193 });
1194
1195 assert!(registry.check_and_trigger("slow_cmd").is_some());
1196 assert!(registry.check_and_trigger("slow_cmd").is_some());
1197 assert!(registry.check_and_trigger("slow_cmd").is_none());
1198
1199 assert_eq!(registry.list().len(), 1);
1200 assert!(registry.clear("slow_cmd"));
1201 assert_eq!(registry.list().len(), 0);
1202 }
1203
1204 #[test]
1205 fn fault_registry_unlimited() {
1206 let registry = FaultRegistry::new();
1207 registry.inject(FaultConfig {
1208 command: "always_fail".to_string(),
1209 fault_type: FaultType::Error {
1210 message: "injected".to_string(),
1211 },
1212 trigger_count: 0,
1213 max_triggers: 0,
1214 created_at: Instant::now(),
1215 });
1216
1217 for _ in 0..100 {
1218 assert!(registry.check_and_trigger("always_fail").is_some());
1219 }
1220 }
1221
1222 #[test]
1223 fn fault_expires_after_ttl() {
1224 let cfg = FaultConfig {
1225 command: "x".to_string(),
1226 fault_type: FaultType::Error {
1227 message: "e".to_string(),
1228 },
1229 trigger_count: 0,
1230 max_triggers: 0, created_at: Instant::now(),
1232 };
1233 assert!(cfg.should_trigger_at(cfg.created_at));
1235 assert!(!cfg.should_trigger_at(cfg.created_at + FAULT_TTL + Duration::from_secs(1)));
1236 }
1237
1238 #[test]
1239 fn json_shape_extraction() {
1240 let value = serde_json::json!({
1241 "name": "test",
1242 "count": 42,
1243 "active": true,
1244 "items": [{"id": 1}],
1245 "meta": null
1246 });
1247 let shape = JsonShape::from_value(&value);
1248 match &shape {
1249 JsonShape::Object(fields) => {
1250 assert_eq!(fields.len(), 5);
1251 assert_eq!(*fields.get("name").unwrap(), JsonShape::String);
1252 assert_eq!(*fields.get("count").unwrap(), JsonShape::Number);
1253 assert_eq!(*fields.get("active").unwrap(), JsonShape::Bool);
1254 assert_eq!(*fields.get("meta").unwrap(), JsonShape::Null);
1255 }
1256 _ => panic!("expected object"),
1257 }
1258 }
1259
1260 #[test]
1261 fn contract_diff_detects_changes() {
1262 let baseline = serde_json::json!({"name": "old", "count": 1});
1263 let current = serde_json::json!({"name": "new", "count": "not_a_number", "extra": true});
1264
1265 let b_shape = JsonShape::from_value(&baseline);
1266 let c_shape = JsonShape::from_value(¤t);
1267 let drift = diff_shapes(&b_shape, &c_shape, "test_cmd");
1268
1269 assert!(!drift.shape_matches);
1270 assert_eq!(drift.new_fields, vec!["test_cmd.extra"]);
1271 assert_eq!(drift.type_changes.len(), 1);
1272 assert_eq!(drift.type_changes[0].path, "test_cmd.count");
1273 }
1274
1275 #[test]
1276 fn contract_store_crud() {
1277 let store = ContractStore::new();
1278 let baseline = ContractBaseline {
1279 command: "get_user".to_string(),
1280 args: serde_json::json!({}),
1281 shape: JsonShape::Object(HashMap::new()),
1282 sample: "{}".to_string(),
1283 recorded_at: "2026-05-26".to_string(),
1284 };
1285 store.record(baseline);
1286 assert!(store.get("get_user").is_some());
1287 assert_eq!(store.all().len(), 1);
1288 assert_eq!(store.clear(), 1);
1289 assert!(store.get("get_user").is_none());
1290 }
1291
1292 #[test]
1293 fn startup_timeline_records_phases() {
1294 let timeline = StartupTimeline::new();
1295 std::thread::sleep(Duration::from_millis(5));
1296 timeline.mark("phase_1");
1297 std::thread::sleep(Duration::from_millis(5));
1298 timeline.mark("phase_2");
1299
1300 let report = timeline.report();
1301 assert_eq!(report.len(), 2);
1302 assert_eq!(report[0].name, "phase_1");
1303 assert!(report[1].cumulative_ms >= report[0].cumulative_ms);
1304 assert!(timeline.total_ms() > 0.0);
1305 }
1306
1307 #[test]
1308 fn enumerate_child_processes_returns_vec() {
1309 let children = enumerate_child_processes();
1310 for child in &children {
1313 assert_ne!(child.pid, 0, "child PID should be non-zero");
1314 assert_eq!(
1315 child.ppid,
1316 std::process::id(),
1317 "parent PID should match current process"
1318 );
1319 assert!(!child.name.is_empty(), "child name should not be empty");
1320 }
1321 }
1322
1323 #[test]
1324 fn enumerate_child_processes_with_spawned_child() {
1325 let child = std::process::Command::new(if cfg!(windows) { "cmd.exe" } else { "sleep" })
1327 .args(if cfg!(windows) {
1328 &["/c", "timeout /t 10 /nobreak >nul"][..]
1329 } else {
1330 &["10"][..]
1331 })
1332 .spawn();
1333
1334 if let Ok(mut child_proc) = child {
1335 let children = enumerate_child_processes();
1336 assert!(
1337 !children.is_empty(),
1338 "should find at least one child process"
1339 );
1340
1341 let found = children.iter().any(|c| c.pid == child_proc.id());
1342 assert!(
1343 found,
1344 "spawned child (PID {}) should appear in enumeration",
1345 child_proc.id()
1346 );
1347
1348 let _ = child_proc.kill();
1349 let _ = child_proc.wait();
1350 }
1351 }
1352
1353 #[test]
1354 fn child_process_info_serializes() {
1355 let info = ChildProcessInfo {
1356 pid: 1234,
1357 ppid: 5678,
1358 name: "test-sidecar".to_string(),
1359 memory_bytes: Some(1_048_576),
1360 };
1361 let json = serde_json::to_value(&info).unwrap();
1362 assert_eq!(json["pid"], 1234);
1363 assert_eq!(json["ppid"], 5678);
1364 assert_eq!(json["name"], "test-sidecar");
1365 assert_eq!(json["memory_bytes"], 1_048_576);
1366 }
1367
1368 #[test]
1369 fn child_process_info_serializes_no_memory() {
1370 let info = ChildProcessInfo {
1371 pid: 42,
1372 ppid: 1,
1373 name: "zombie".to_string(),
1374 memory_bytes: None,
1375 };
1376 let json = serde_json::to_value(&info).unwrap();
1377 assert!(json["memory_bytes"].is_null());
1378 }
1379}