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
190impl FaultConfig {
191 #[must_use]
193 pub fn should_trigger(&self) -> bool {
194 self.max_triggers == 0 || self.trigger_count < self.max_triggers
195 }
196}
197
198pub struct FaultRegistry {
200 inner: RwLock<HashMap<String, FaultConfig>>,
201}
202
203impl FaultRegistry {
204 #[must_use]
206 pub fn new() -> Self {
207 Self {
208 inner: RwLock::new(HashMap::new()),
209 }
210 }
211
212 pub fn inject(&self, config: FaultConfig) {
214 let mut map = self
215 .inner
216 .write()
217 .unwrap_or_else(std::sync::PoisonError::into_inner);
218 map.insert(config.command.clone(), config);
219 }
220
221 pub fn check_and_trigger(&self, command: &str) -> Option<FaultType> {
224 let mut map = self
225 .inner
226 .write()
227 .unwrap_or_else(std::sync::PoisonError::into_inner);
228 if let Some(config) = map.get_mut(command)
229 && config.should_trigger()
230 {
231 config.trigger_count += 1;
232 return Some(config.fault_type.clone());
233 }
234 None
235 }
236
237 #[must_use]
239 pub fn list(&self) -> Vec<FaultConfig> {
240 let map = self
241 .inner
242 .read()
243 .unwrap_or_else(std::sync::PoisonError::into_inner);
244 map.values().cloned().collect()
245 }
246
247 pub fn clear(&self, command: &str) -> bool {
249 let mut map = self
250 .inner
251 .write()
252 .unwrap_or_else(std::sync::PoisonError::into_inner);
253 map.remove(command).is_some()
254 }
255
256 pub fn clear_all(&self) -> usize {
258 let mut map = self
259 .inner
260 .write()
261 .unwrap_or_else(std::sync::PoisonError::into_inner);
262 let count = map.len();
263 map.clear();
264 count
265 }
266}
267
268impl Default for FaultRegistry {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274#[derive(Debug, Clone, Serialize, PartialEq)]
278pub enum JsonShape {
279 Null,
281 Bool,
283 Number,
285 String,
287 Array(Box<Self>),
289 Object(HashMap<String, Self>),
291}
292
293impl JsonShape {
294 #[must_use]
296 pub fn from_value(value: &serde_json::Value) -> Self {
297 match value {
298 serde_json::Value::Null => Self::Null,
299 serde_json::Value::Bool(_) => Self::Bool,
300 serde_json::Value::Number(_) => Self::Number,
301 serde_json::Value::String(_) => Self::String,
302 serde_json::Value::Array(arr) => {
303 let elem = arr.first().map_or(Self::Null, Self::from_value);
304 Self::Array(Box::new(elem))
305 }
306 serde_json::Value::Object(obj) => {
307 let fields: HashMap<String, Self> = obj
308 .iter()
309 .map(|(k, v)| (k.clone(), Self::from_value(v)))
310 .collect();
311 Self::Object(fields)
312 }
313 }
314 }
315
316 #[must_use]
318 pub fn type_name(&self) -> &'static str {
319 match self {
320 Self::Null => "null",
321 Self::Bool => "bool",
322 Self::Number => "number",
323 Self::String => "string",
324 Self::Array(_) => "array",
325 Self::Object(_) => "object",
326 }
327 }
328}
329
330#[derive(Debug, Clone, Serialize)]
332pub struct ContractBaseline {
333 pub command: String,
335 pub args: serde_json::Value,
337 pub shape: JsonShape,
339 pub sample: String,
341 pub recorded_at: String,
343}
344
345#[derive(Debug, Clone, Serialize)]
347pub struct ContractDrift {
348 pub command: String,
350 pub new_fields: Vec<String>,
352 pub removed_fields: Vec<String>,
354 pub type_changes: Vec<TypeChange>,
356 pub shape_matches: bool,
358}
359
360#[derive(Debug, Clone, Serialize)]
362pub struct TypeChange {
363 pub path: String,
365 pub baseline_type: String,
367 pub current_type: String,
369}
370
371#[must_use]
373pub fn diff_shapes(baseline: &JsonShape, current: &JsonShape, prefix: &str) -> ContractDrift {
374 let mut new_fields = Vec::new();
375 let mut removed_fields = Vec::new();
376 let mut type_changes = Vec::new();
377
378 diff_shapes_inner(
379 baseline,
380 current,
381 prefix,
382 &mut new_fields,
383 &mut removed_fields,
384 &mut type_changes,
385 );
386
387 let shape_matches =
388 new_fields.is_empty() && removed_fields.is_empty() && type_changes.is_empty();
389 ContractDrift {
390 command: prefix.to_string(),
391 new_fields,
392 removed_fields,
393 type_changes,
394 shape_matches,
395 }
396}
397
398fn diff_shapes_inner(
399 baseline: &JsonShape,
400 current: &JsonShape,
401 prefix: &str,
402 new_fields: &mut Vec<String>,
403 removed_fields: &mut Vec<String>,
404 type_changes: &mut Vec<TypeChange>,
405) {
406 match (baseline, current) {
407 (JsonShape::Object(b_fields), JsonShape::Object(c_fields)) => {
408 for (key, b_shape) in b_fields {
409 let path = if prefix.is_empty() {
410 key.clone()
411 } else {
412 format!("{prefix}.{key}")
413 };
414 if let Some(c_shape) = c_fields.get(key) {
415 diff_shapes_inner(
416 b_shape,
417 c_shape,
418 &path,
419 new_fields,
420 removed_fields,
421 type_changes,
422 );
423 } else {
424 removed_fields.push(path);
425 }
426 }
427 for key in c_fields.keys() {
428 if !b_fields.contains_key(key) {
429 let path = if prefix.is_empty() {
430 key.clone()
431 } else {
432 format!("{prefix}.{key}")
433 };
434 new_fields.push(path);
435 }
436 }
437 }
438 (JsonShape::Array(b_elem), JsonShape::Array(c_elem)) => {
439 let path = format!("{prefix}[]");
440 diff_shapes_inner(
441 b_elem,
442 c_elem,
443 &path,
444 new_fields,
445 removed_fields,
446 type_changes,
447 );
448 }
449 (b, c) if b.type_name() != c.type_name() => {
450 type_changes.push(TypeChange {
451 path: prefix.to_string(),
452 baseline_type: b.type_name().to_string(),
453 current_type: c.type_name().to_string(),
454 });
455 }
456 _ => {}
457 }
458}
459
460pub struct ContractStore {
462 inner: RwLock<HashMap<String, ContractBaseline>>,
463}
464
465impl ContractStore {
466 #[must_use]
468 pub fn new() -> Self {
469 Self {
470 inner: RwLock::new(HashMap::new()),
471 }
472 }
473
474 pub fn record(&self, baseline: ContractBaseline) {
476 let mut map = self
477 .inner
478 .write()
479 .unwrap_or_else(std::sync::PoisonError::into_inner);
480 map.insert(baseline.command.clone(), baseline);
481 }
482
483 #[must_use]
485 pub fn get(&self, command: &str) -> Option<ContractBaseline> {
486 let map = self
487 .inner
488 .read()
489 .unwrap_or_else(std::sync::PoisonError::into_inner);
490 map.get(command).cloned()
491 }
492
493 #[must_use]
495 pub fn all(&self) -> Vec<ContractBaseline> {
496 let map = self
497 .inner
498 .read()
499 .unwrap_or_else(std::sync::PoisonError::into_inner);
500 map.values().cloned().collect()
501 }
502
503 pub fn clear(&self) -> usize {
505 let mut map = self
506 .inner
507 .write()
508 .unwrap_or_else(std::sync::PoisonError::into_inner);
509 let count = map.len();
510 map.clear();
511 count
512 }
513}
514
515impl Default for ContractStore {
516 fn default() -> Self {
517 Self::new()
518 }
519}
520
521#[derive(Debug, Clone, Serialize)]
525pub struct StartupPhase {
526 pub name: String,
528 pub duration_ms: f64,
530 pub cumulative_ms: f64,
532}
533
534pub struct StartupTimeline {
536 start: Instant,
537 phases: RwLock<Vec<(String, Instant)>>,
538}
539
540impl StartupTimeline {
541 #[must_use]
543 pub fn new() -> Self {
544 Self {
545 start: Instant::now(),
546 phases: RwLock::new(Vec::new()),
547 }
548 }
549
550 pub fn mark(&self, name: &str) {
552 let mut phases = self
553 .phases
554 .write()
555 .unwrap_or_else(std::sync::PoisonError::into_inner);
556 phases.push((name.to_string(), Instant::now()));
557 }
558
559 #[must_use]
561 pub fn report(&self) -> Vec<StartupPhase> {
562 let phases = self
563 .phases
564 .read()
565 .unwrap_or_else(std::sync::PoisonError::into_inner);
566 let mut result = Vec::new();
567 let mut prev = self.start;
568
569 for (name, instant) in phases.iter() {
570 let duration = instant.duration_since(prev);
571 let cumulative = instant.duration_since(self.start);
572 result.push(StartupPhase {
573 name: name.clone(),
574 duration_ms: (duration.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
575 cumulative_ms: (cumulative.as_secs_f64() * 1000.0 * 100.0).round() / 100.0,
576 });
577 prev = *instant;
578 }
579 result
580 }
581
582 #[must_use]
584 pub fn total_ms(&self) -> f64 {
585 let phases = self
586 .phases
587 .read()
588 .unwrap_or_else(std::sync::PoisonError::into_inner);
589 if let Some((_, last)) = phases.last() {
590 (last.duration_since(self.start).as_secs_f64() * 1000.0 * 100.0).round() / 100.0
591 } else {
592 0.0
593 }
594 }
595}
596
597impl Default for StartupTimeline {
598 fn default() -> Self {
599 Self::new()
600 }
601}
602
603#[derive(Debug, Clone, Serialize)]
607pub struct CapturedTauriEvent {
608 pub name: String,
610 pub payload: String,
612 pub timestamp: String,
614}
615
616const DEFAULT_EVENT_BUS_CAPACITY: usize = 1000;
617
618#[derive(Clone)]
620pub struct EventBusMonitor {
621 inner: std::sync::Arc<RwLock<VecDeque<CapturedTauriEvent>>>,
622 capacity: usize,
623}
624
625impl EventBusMonitor {
626 #[must_use]
628 pub fn new(capacity: usize) -> Self {
629 Self {
630 inner: std::sync::Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
631 capacity,
632 }
633 }
634
635 pub fn push(&self, event: CapturedTauriEvent) {
637 let mut buf = self
638 .inner
639 .write()
640 .unwrap_or_else(std::sync::PoisonError::into_inner);
641 if buf.len() >= self.capacity {
642 buf.pop_front();
643 }
644 buf.push_back(event);
645 }
646
647 #[must_use]
649 pub fn events(&self) -> Vec<CapturedTauriEvent> {
650 self.inner
651 .read()
652 .unwrap_or_else(std::sync::PoisonError::into_inner)
653 .iter()
654 .cloned()
655 .collect()
656 }
657
658 #[must_use]
660 pub fn len(&self) -> usize {
661 self.inner
662 .read()
663 .unwrap_or_else(std::sync::PoisonError::into_inner)
664 .len()
665 }
666
667 #[must_use]
669 pub fn is_empty(&self) -> bool {
670 self.len() == 0
671 }
672
673 pub fn clear(&self) -> usize {
675 let mut buf = self
676 .inner
677 .write()
678 .unwrap_or_else(std::sync::PoisonError::into_inner);
679 let count = buf.len();
680 buf.clear();
681 count
682 }
683}
684
685impl Default for EventBusMonitor {
686 fn default() -> Self {
687 Self::new(DEFAULT_EVENT_BUS_CAPACITY)
688 }
689}
690
691#[derive(Debug, Clone, Serialize)]
695pub struct TrackedTaskInfo {
696 pub name: String,
698 pub spawned_at: String,
700 pub is_finished: bool,
702 pub uptime_secs: u64,
704}
705
706struct TrackedTaskEntry {
707 name: String,
708 spawned_at: Instant,
709 spawned_at_wall: String,
710 finished: std::sync::Arc<AtomicBool>,
711}
712
713pub struct TaskTracker {
715 tasks: RwLock<Vec<TrackedTaskEntry>>,
716}
717
718impl TaskTracker {
719 #[must_use]
721 pub fn new() -> Self {
722 Self {
723 tasks: RwLock::new(Vec::new()),
724 }
725 }
726
727 pub fn track(&self, name: &str) -> std::sync::Arc<AtomicBool> {
729 let finished = std::sync::Arc::new(AtomicBool::new(false));
730 let entry = TrackedTaskEntry {
731 name: name.to_string(),
732 spawned_at: Instant::now(),
733 spawned_at_wall: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
734 finished: finished.clone(),
735 };
736 self.tasks
737 .write()
738 .unwrap_or_else(std::sync::PoisonError::into_inner)
739 .push(entry);
740 finished
741 }
742
743 #[must_use]
745 pub fn list(&self) -> Vec<TrackedTaskInfo> {
746 let tasks = self
747 .tasks
748 .read()
749 .unwrap_or_else(std::sync::PoisonError::into_inner);
750 tasks
751 .iter()
752 .map(|t| TrackedTaskInfo {
753 name: t.name.clone(),
754 spawned_at: t.spawned_at_wall.clone(),
755 is_finished: t.finished.load(std::sync::atomic::Ordering::Relaxed),
756 uptime_secs: t.spawned_at.elapsed().as_secs(),
757 })
758 .collect()
759 }
760
761 #[must_use]
763 pub fn active_count(&self) -> usize {
764 let tasks = self
765 .tasks
766 .read()
767 .unwrap_or_else(std::sync::PoisonError::into_inner);
768 tasks
769 .iter()
770 .filter(|t| !t.finished.load(std::sync::atomic::Ordering::Relaxed))
771 .count()
772 }
773}
774
775impl Default for TaskTracker {
776 fn default() -> Self {
777 Self::new()
778 }
779}
780
781#[derive(Debug, Clone, Serialize)]
785pub struct ChildProcessInfo {
786 pub pid: u32,
788 pub ppid: u32,
790 pub name: String,
792 pub memory_bytes: Option<u64>,
794}
795
796#[must_use]
803pub fn enumerate_child_processes() -> Vec<ChildProcessInfo> {
804 let my_pid = std::process::id();
805
806 #[cfg(windows)]
807 {
808 enumerate_children_windows(my_pid)
809 }
810
811 #[cfg(target_os = "linux")]
812 {
813 enumerate_children_linux(my_pid)
814 }
815
816 #[cfg(target_os = "macos")]
817 {
818 enumerate_children_macos(my_pid)
819 }
820
821 #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
822 {
823 let _ = my_pid;
824 Vec::new()
825 }
826}
827
828#[cfg(windows)]
829#[allow(unsafe_code)]
830fn enumerate_children_windows(parent_pid: u32) -> Vec<ChildProcessInfo> {
831 use windows::Win32::Foundation::CloseHandle;
832 use windows::Win32::System::Diagnostics::ToolHelp::{
833 CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS,
834 };
835
836 let mut children = Vec::new();
837
838 unsafe {
843 let Ok(snapshot) = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) else {
844 return children;
845 };
846
847 let mut entry: PROCESSENTRY32 = std::mem::zeroed();
848 entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
849
850 if Process32First(snapshot, &mut entry).is_ok() {
851 loop {
852 if entry.th32ParentProcessID == parent_pid && entry.th32ProcessID != parent_pid {
853 let name_bytes: Vec<u8> = entry
854 .szExeFile
855 .iter()
856 .take_while(|&&b| b != 0)
857 .map(|&b| b as u8)
858 .collect();
859 let name = String::from_utf8_lossy(&name_bytes).to_string();
860
861 let memory_bytes = get_process_memory_windows(entry.th32ProcessID);
862
863 children.push(ChildProcessInfo {
864 pid: entry.th32ProcessID,
865 ppid: entry.th32ParentProcessID,
866 name,
867 memory_bytes,
868 });
869 }
870
871 if Process32Next(snapshot, &mut entry).is_err() {
872 break;
873 }
874 }
875 }
876
877 let _ = CloseHandle(snapshot);
878 }
879
880 children
881}
882
883#[cfg(windows)]
884#[allow(unsafe_code)]
885fn get_process_memory_windows(pid: u32) -> Option<u64> {
886 use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
887 use windows::Win32::System::Threading::{
888 OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ,
889 };
890
891 unsafe {
895 let process = OpenProcess(
896 PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
897 false,
898 pid,
899 )
900 .ok()?;
901
902 let mut counters: PROCESS_MEMORY_COUNTERS = std::mem::zeroed();
903 counters.cb = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
904
905 if GetProcessMemoryInfo(process, &mut counters, counters.cb).is_ok() {
906 Some(counters.WorkingSetSize as u64)
907 } else {
908 None
909 }
910 }
911}
912
913#[cfg(target_os = "linux")]
914fn enumerate_children_linux(parent_pid: u32) -> Vec<ChildProcessInfo> {
915 let mut children = Vec::new();
916 let Ok(entries) = std::fs::read_dir("/proc") else {
917 return children;
918 };
919
920 for entry in entries.flatten() {
921 let file_name = entry.file_name();
922 let Some(pid_str) = file_name.to_str() else {
923 continue;
924 };
925 let Ok(pid) = pid_str.parse::<u32>() else {
926 continue;
927 };
928
929 let status_path = format!("/proc/{pid}/status");
930 let Ok(status) = std::fs::read_to_string(&status_path) else {
931 continue;
932 };
933
934 let mut ppid: Option<u32> = None;
935 let mut name = String::new();
936 let mut vm_rss_kb: u64 = 0;
937
938 for line in status.lines() {
939 if let Some(v) = line.strip_prefix("PPid:\t") {
940 ppid = v.trim().parse().ok();
941 } else if let Some(v) = line.strip_prefix("Name:\t") {
942 name = v.trim().to_string();
943 } else if let Some(v) = line.strip_prefix("VmRSS:") {
944 vm_rss_kb = v
945 .split_whitespace()
946 .next()
947 .and_then(|n| n.parse().ok())
948 .unwrap_or(0);
949 }
950 }
951
952 if ppid == Some(parent_pid) {
953 children.push(ChildProcessInfo {
954 pid,
955 ppid: parent_pid,
956 name,
957 memory_bytes: if vm_rss_kb > 0 {
958 Some(vm_rss_kb * 1024)
959 } else {
960 None
961 },
962 });
963 }
964 }
965
966 children
967}
968
969#[cfg(target_os = "macos")]
970#[allow(unsafe_code)]
971fn enumerate_children_macos(parent_pid: u32) -> Vec<ChildProcessInfo> {
972 use std::mem;
973
974 unsafe extern "C" {
975 fn proc_listchildpids(ppid: i32, buffer: *mut i32, buffersize: i32) -> i32;
976 fn proc_pidinfo(pid: i32, flavor: i32, arg: u64, buffer: *mut u8, buffersize: i32) -> i32;
977 fn proc_name(pid: i32, buffer: *mut u8, buffersize: u32) -> i32;
978 }
979
980 const PROC_PIDTASKINFO: i32 = 4;
981
982 #[repr(C)]
983 struct ProcTaskInfo {
984 pti_virtual_size: u64,
985 pti_resident_size: u64,
986 pti_total_user: u64,
987 pti_total_system: u64,
988 pti_threads_user: u64,
989 pti_threads_system: u64,
990 pti_policy: i32,
991 pti_faults: i32,
992 pti_pageins: i32,
993 pti_cow_faults: i32,
994 pti_messages_sent: i32,
995 pti_messages_received: i32,
996 pti_syscalls_mach: i32,
997 pti_syscalls_unix: i32,
998 pti_csw: i32,
999 pti_threadnum: i32,
1000 pti_numrunning: i32,
1001 pti_priority: i32,
1002 }
1003
1004 let mut children = Vec::new();
1005
1006 unsafe {
1010 let ppid = parent_pid as i32;
1011 let count = proc_listchildpids(ppid, std::ptr::null_mut(), 0);
1012 if count <= 0 {
1013 return children;
1014 }
1015
1016 let mut pids = vec![0i32; count as usize];
1017 let buf_size = (count as usize * mem::size_of::<i32>()) as i32;
1018 let actual = proc_listchildpids(ppid, pids.as_mut_ptr(), buf_size);
1019 if actual <= 0 {
1020 return children;
1021 }
1022
1023 let n = actual as usize / mem::size_of::<i32>();
1024 for &pid in &pids[..n] {
1025 if pid <= 0 {
1026 continue;
1027 }
1028
1029 let mut name_buf = [0u8; 256];
1030 let name_len = proc_name(pid, name_buf.as_mut_ptr(), 256);
1031 let name = if name_len > 0 {
1032 String::from_utf8_lossy(&name_buf[..name_len as usize]).to_string()
1033 } else {
1034 String::from("<unknown>")
1035 };
1036
1037 let mut task_info: ProcTaskInfo = mem::zeroed();
1038 let info_size = mem::size_of::<ProcTaskInfo>() as i32;
1039 let ret = proc_pidinfo(
1040 pid,
1041 PROC_PIDTASKINFO,
1042 0,
1043 &mut task_info as *mut _ as *mut u8,
1044 info_size,
1045 );
1046
1047 let memory_bytes = if ret == info_size {
1048 Some(task_info.pti_resident_size)
1049 } else {
1050 None
1051 };
1052
1053 children.push(ChildProcessInfo {
1054 pid: pid as u32,
1055 ppid: parent_pid,
1056 name,
1057 memory_bytes,
1058 });
1059 }
1060 }
1061
1062 children
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068
1069 #[test]
1070 fn event_bus_push_and_read() {
1071 let bus = EventBusMonitor::new(3);
1072 assert!(bus.is_empty());
1073 bus.push(CapturedTauriEvent {
1074 name: "test".to_string(),
1075 payload: "{}".to_string(),
1076 timestamp: "2026-01-01T00:00:00Z".to_string(),
1077 });
1078 assert_eq!(bus.len(), 1);
1079 assert_eq!(bus.events()[0].name, "test");
1080 }
1081
1082 #[test]
1083 fn event_bus_ring_buffer_eviction() {
1084 let bus = EventBusMonitor::new(2);
1085 for i in 0..5 {
1086 bus.push(CapturedTauriEvent {
1087 name: format!("event_{i}"),
1088 payload: String::new(),
1089 timestamp: String::new(),
1090 });
1091 }
1092 assert_eq!(bus.len(), 2);
1093 assert_eq!(bus.events()[0].name, "event_3");
1094 assert_eq!(bus.events()[1].name, "event_4");
1095 }
1096
1097 #[test]
1098 fn event_bus_clear() {
1099 let bus = EventBusMonitor::new(10);
1100 bus.push(CapturedTauriEvent {
1101 name: "a".to_string(),
1102 payload: String::new(),
1103 timestamp: String::new(),
1104 });
1105 assert_eq!(bus.clear(), 1);
1106 assert!(bus.is_empty());
1107 }
1108
1109 #[test]
1110 fn task_tracker_lifecycle() {
1111 let tracker = TaskTracker::new();
1112 let flag = tracker.track("mcp_server");
1113 let tasks = tracker.list();
1114 assert_eq!(tasks.len(), 1);
1115 assert_eq!(tasks[0].name, "mcp_server");
1116 assert!(!tasks[0].is_finished);
1117 assert_eq!(tracker.active_count(), 1);
1118
1119 flag.store(true, std::sync::atomic::Ordering::Relaxed);
1120 let tasks = tracker.list();
1121 assert!(tasks[0].is_finished);
1122 assert_eq!(tracker.active_count(), 0);
1123 }
1124
1125 #[test]
1126 fn timing_samples_basic() {
1127 let mut samples = TimingSamples::default();
1128 samples.record(Duration::from_millis(10));
1129 samples.record(Duration::from_millis(20));
1130 samples.record(Duration::from_millis(30));
1131 let stats = samples.stats("test_cmd");
1132 assert_eq!(stats.count, 3);
1133 assert!((stats.min_ms - 10.0).abs() < 1.0);
1134 assert!((stats.max_ms - 30.0).abs() < 1.0);
1135 assert!((stats.avg_ms - 20.0).abs() < 1.0);
1136 }
1137
1138 #[test]
1139 fn timing_samples_empty() {
1140 let samples = TimingSamples::default();
1141 let stats = samples.stats("empty");
1142 assert_eq!(stats.count, 0);
1143 assert_eq!(stats.min_ms, 0.0);
1144 }
1145
1146 #[test]
1147 fn command_timings_thread_safe() {
1148 let timings = CommandTimings::new();
1149 timings.record("cmd_a", Duration::from_millis(5));
1150 timings.record("cmd_a", Duration::from_millis(15));
1151 timings.record("cmd_b", Duration::from_millis(100));
1152
1153 let all = timings.all_stats();
1154 assert_eq!(all.len(), 2);
1155 assert_eq!(all[0].command, "cmd_b");
1156
1157 let a = timings.stats_for("cmd_a").unwrap();
1158 assert_eq!(a.count, 2);
1159 }
1160
1161 #[test]
1162 fn fault_registry_lifecycle() {
1163 let registry = FaultRegistry::new();
1164 registry.inject(FaultConfig {
1165 command: "slow_cmd".to_string(),
1166 fault_type: FaultType::Delay { delay_ms: 500 },
1167 trigger_count: 0,
1168 max_triggers: 2,
1169 created_at: Instant::now(),
1170 });
1171
1172 assert!(registry.check_and_trigger("slow_cmd").is_some());
1173 assert!(registry.check_and_trigger("slow_cmd").is_some());
1174 assert!(registry.check_and_trigger("slow_cmd").is_none());
1175
1176 assert_eq!(registry.list().len(), 1);
1177 assert!(registry.clear("slow_cmd"));
1178 assert_eq!(registry.list().len(), 0);
1179 }
1180
1181 #[test]
1182 fn fault_registry_unlimited() {
1183 let registry = FaultRegistry::new();
1184 registry.inject(FaultConfig {
1185 command: "always_fail".to_string(),
1186 fault_type: FaultType::Error {
1187 message: "injected".to_string(),
1188 },
1189 trigger_count: 0,
1190 max_triggers: 0,
1191 created_at: Instant::now(),
1192 });
1193
1194 for _ in 0..100 {
1195 assert!(registry.check_and_trigger("always_fail").is_some());
1196 }
1197 }
1198
1199 #[test]
1200 fn json_shape_extraction() {
1201 let value = serde_json::json!({
1202 "name": "test",
1203 "count": 42,
1204 "active": true,
1205 "items": [{"id": 1}],
1206 "meta": null
1207 });
1208 let shape = JsonShape::from_value(&value);
1209 match &shape {
1210 JsonShape::Object(fields) => {
1211 assert_eq!(fields.len(), 5);
1212 assert_eq!(*fields.get("name").unwrap(), JsonShape::String);
1213 assert_eq!(*fields.get("count").unwrap(), JsonShape::Number);
1214 assert_eq!(*fields.get("active").unwrap(), JsonShape::Bool);
1215 assert_eq!(*fields.get("meta").unwrap(), JsonShape::Null);
1216 }
1217 _ => panic!("expected object"),
1218 }
1219 }
1220
1221 #[test]
1222 fn contract_diff_detects_changes() {
1223 let baseline = serde_json::json!({"name": "old", "count": 1});
1224 let current = serde_json::json!({"name": "new", "count": "not_a_number", "extra": true});
1225
1226 let b_shape = JsonShape::from_value(&baseline);
1227 let c_shape = JsonShape::from_value(¤t);
1228 let drift = diff_shapes(&b_shape, &c_shape, "test_cmd");
1229
1230 assert!(!drift.shape_matches);
1231 assert_eq!(drift.new_fields, vec!["test_cmd.extra"]);
1232 assert_eq!(drift.type_changes.len(), 1);
1233 assert_eq!(drift.type_changes[0].path, "test_cmd.count");
1234 }
1235
1236 #[test]
1237 fn contract_store_crud() {
1238 let store = ContractStore::new();
1239 let baseline = ContractBaseline {
1240 command: "get_user".to_string(),
1241 args: serde_json::json!({}),
1242 shape: JsonShape::Object(HashMap::new()),
1243 sample: "{}".to_string(),
1244 recorded_at: "2026-05-26".to_string(),
1245 };
1246 store.record(baseline);
1247 assert!(store.get("get_user").is_some());
1248 assert_eq!(store.all().len(), 1);
1249 assert_eq!(store.clear(), 1);
1250 assert!(store.get("get_user").is_none());
1251 }
1252
1253 #[test]
1254 fn startup_timeline_records_phases() {
1255 let timeline = StartupTimeline::new();
1256 std::thread::sleep(Duration::from_millis(5));
1257 timeline.mark("phase_1");
1258 std::thread::sleep(Duration::from_millis(5));
1259 timeline.mark("phase_2");
1260
1261 let report = timeline.report();
1262 assert_eq!(report.len(), 2);
1263 assert_eq!(report[0].name, "phase_1");
1264 assert!(report[1].cumulative_ms >= report[0].cumulative_ms);
1265 assert!(timeline.total_ms() > 0.0);
1266 }
1267
1268 #[test]
1269 fn enumerate_child_processes_returns_vec() {
1270 let children = enumerate_child_processes();
1271 for child in &children {
1274 assert_ne!(child.pid, 0, "child PID should be non-zero");
1275 assert_eq!(
1276 child.ppid,
1277 std::process::id(),
1278 "parent PID should match current process"
1279 );
1280 assert!(!child.name.is_empty(), "child name should not be empty");
1281 }
1282 }
1283
1284 #[test]
1285 fn enumerate_child_processes_with_spawned_child() {
1286 let child = std::process::Command::new(if cfg!(windows) { "cmd.exe" } else { "sleep" })
1288 .args(if cfg!(windows) {
1289 &["/c", "timeout /t 10 /nobreak >nul"][..]
1290 } else {
1291 &["10"][..]
1292 })
1293 .spawn();
1294
1295 if let Ok(mut child_proc) = child {
1296 let children = enumerate_child_processes();
1297 assert!(
1298 !children.is_empty(),
1299 "should find at least one child process"
1300 );
1301
1302 let found = children.iter().any(|c| c.pid == child_proc.id());
1303 assert!(
1304 found,
1305 "spawned child (PID {}) should appear in enumeration",
1306 child_proc.id()
1307 );
1308
1309 let _ = child_proc.kill();
1310 let _ = child_proc.wait();
1311 }
1312 }
1313
1314 #[test]
1315 fn child_process_info_serializes() {
1316 let info = ChildProcessInfo {
1317 pid: 1234,
1318 ppid: 5678,
1319 name: "test-sidecar".to_string(),
1320 memory_bytes: Some(1_048_576),
1321 };
1322 let json = serde_json::to_value(&info).unwrap();
1323 assert_eq!(json["pid"], 1234);
1324 assert_eq!(json["ppid"], 5678);
1325 assert_eq!(json["name"], "test-sidecar");
1326 assert_eq!(json["memory_bytes"], 1_048_576);
1327 }
1328
1329 #[test]
1330 fn child_process_info_serializes_no_memory() {
1331 let info = ChildProcessInfo {
1332 pid: 42,
1333 ppid: 1,
1334 name: "zombie".to_string(),
1335 memory_bytes: None,
1336 };
1337 let json = serde_json::to_value(&info).unwrap();
1338 assert!(json["memory_bytes"].is_null());
1339 }
1340}