1use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(tag = "platform", rename_all = "snake_case")]
10pub enum ProcessStart {
11 Linux { ticks: u64 },
12 Darwin { seconds: u64, microseconds: u32 },
13}
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct ProcessIdentity {
17 pub boot_id: String,
18 pub start: ProcessStart,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum ProcessObservation {
23 Present(ProcessIdentity),
24 Absent,
25 Unknown,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum OwnerPresence {
30 Live,
31 Absent,
32 Unknown,
33}
34
35impl ProcessIdentity {
36 pub fn is_valid(&self) -> bool {
37 uuid::Uuid::parse_str(&self.boot_id).is_ok_and(|uuid| uuid.to_string() == self.boot_id)
38 && match self.start {
39 ProcessStart::Linux { .. } => true,
40 ProcessStart::Darwin {
41 seconds,
42 microseconds,
43 } => seconds > 0 && microseconds < 1_000_000,
44 }
45 }
46
47 pub fn to_record(&self) -> Option<String> {
50 self.is_valid().then(|| match self.start {
51 ProcessStart::Linux { ticks } => format!("{}:{ticks}", self.boot_id),
52 ProcessStart::Darwin {
53 seconds,
54 microseconds,
55 } => format!("{}:darwin:{seconds}:{microseconds}", self.boot_id),
56 })
57 }
58
59 pub fn from_record(record: &str) -> Option<Self> {
60 let (boot, marker) = record.split_once(':')?;
61 let start = if let Some(time) = marker.strip_prefix("darwin:") {
62 let (seconds, microseconds) = time.split_once(':')?;
63 ProcessStart::Darwin {
64 seconds: decimal(seconds)?,
65 microseconds: u32::try_from(decimal(microseconds)?).ok()?,
66 }
67 } else {
68 ProcessStart::Linux {
69 ticks: decimal(marker)?,
70 }
71 };
72 let identity = Self {
73 boot_id: boot.into(),
74 start,
75 };
76 identity.is_valid().then_some(identity)
77 }
78
79 pub fn presence(&self, observation: &ProcessObservation) -> OwnerPresence {
80 if !self.is_valid() {
81 return OwnerPresence::Unknown;
82 }
83 match observation {
84 ProcessObservation::Absent => OwnerPresence::Absent,
85 ProcessObservation::Unknown => OwnerPresence::Unknown,
86 ProcessObservation::Present(current) => {
87 if !current.is_valid()
88 || std::mem::discriminant(&self.start) != std::mem::discriminant(¤t.start)
89 {
90 return OwnerPresence::Unknown;
91 }
92 if self == current {
93 OwnerPresence::Live
94 } else {
95 OwnerPresence::Absent
96 }
97 }
98 }
99 }
100}
101
102fn decimal(value: &str) -> Option<u64> {
103 (!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
104 .then(|| value.parse().ok())
105 .flatten()
106}
107
108pub fn owner_presence(pid: u32, expected: Option<&ProcessIdentity>) -> OwnerPresence {
111 if pid <= 1 || i32::try_from(pid).is_err() {
112 return OwnerPresence::Unknown;
113 }
114 observed_owner_presence(expected, &observe_process(pid))
115}
116
117fn observed_owner_presence(
118 expected: Option<&ProcessIdentity>,
119 observed: &ProcessObservation,
120) -> OwnerPresence {
121 if *observed == ProcessObservation::Absent {
122 return OwnerPresence::Absent;
123 }
124 expected.map_or(OwnerPresence::Unknown, |identity| {
125 identity.presence(observed)
126 })
127}
128
129pub fn current_process_identity() -> Option<ProcessIdentity> {
132 let observation = observe_process(std::process::id());
133 match observation {
134 ProcessObservation::Present(identity) => Some(identity),
135 _ => None,
136 }
137}
138
139#[cfg(target_os = "linux")]
140fn boot_identity() -> Option<String> {
141 let boot = std::fs::read_to_string("/proc/sys/kernel/random/boot_id").ok()?;
142 let boot = boot.trim();
143 uuid::Uuid::parse_str(boot).ok()?;
144 Some(boot.into())
145}
146
147#[cfg(target_os = "macos")]
148fn boot_identity() -> Option<String> {
149 static BOOT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
150 if let Some(boot) = BOOT.get() {
151 return Some(boot.clone());
152 }
153 let output = std::process::Command::new("/usr/sbin/sysctl")
154 .args(["-n", "kern.bootsessionuuid"])
155 .output()
156 .ok()?;
157 if !output.status.success() {
158 return None;
159 }
160 let text = std::str::from_utf8(&output.stdout).ok()?.trim();
161 let boot = uuid::Uuid::parse_str(text).ok()?.to_string();
162 let _ = BOOT.set(boot.clone());
164 Some(boot)
165}
166
167#[cfg(target_os = "linux")]
168pub fn observe_process(pid: u32) -> ProcessObservation {
169 if pid <= 1 || i32::try_from(pid).is_err() {
170 return ProcessObservation::Unknown;
171 }
172 let namespace_matches = match (
177 std::fs::read_to_string("/proc/self/stat"),
178 std::fs::read_to_string("/proc/self/status"),
179 ) {
180 (Ok(stat), Ok(status)) => linux_namespace_matches(std::process::id(), &stat, &status),
181 _ => false,
182 };
183 if !namespace_matches {
184 return ProcessObservation::Unknown;
185 }
186 let Some(boot) = boot_identity() else {
187 return ProcessObservation::Unknown;
188 };
189 observe_linux_process(
190 &std::path::PathBuf::from(format!("/proc/{pid}")),
191 pid,
192 &boot,
193 )
194}
195
196#[cfg(target_os = "linux")]
197fn observe_linux_process(process: &std::path::Path, pid: u32, boot: &str) -> ProcessObservation {
198 match std::fs::read_to_string(process.join("stat")) {
199 Ok(stat) => {
200 let observed = linux_stat(&stat, pid, boot);
201 if observed != ProcessObservation::Unknown {
202 return observed;
203 }
204 let Some(leader) = parse_linux_stat(&stat, pid) else {
205 return ProcessObservation::Unknown;
206 };
207 observe_linux_threads(process, pid, boot, leader)
208 }
209 Err(error) if error.kind() == std::io::ErrorKind::NotFound => kernel_absence(pid),
212 Err(_) => ProcessObservation::Unknown,
213 }
214}
215
216#[cfg(any(target_os = "linux", test))]
217fn linux_namespace_matches(pid: u32, stat: &str, status: &str) -> bool {
218 if parse_linux_stat(stat, pid).is_none() {
219 return false;
220 }
221 let mut records = status
225 .lines()
226 .filter_map(|line| line.strip_prefix("NSpid:"));
227 let Some(record) = records.next() else {
228 return false;
229 };
230 if records.next().is_some() {
231 return false;
232 }
233 let mut ids = record.split_whitespace();
234 ids.next().and_then(decimal) == Some(u64::from(pid)) && ids.next().is_none()
235}
236
237#[cfg(any(target_os = "linux", target_os = "macos"))]
240fn kernel_absence(pid: u32) -> ProcessObservation {
241 let Some(pid) = i32::try_from(pid)
242 .ok()
243 .filter(|pid| *pid > 1)
244 .and_then(rustix::process::Pid::from_raw)
245 else {
246 return ProcessObservation::Unknown;
247 };
248 match rustix::process::test_kill_process(pid) {
249 Err(rustix::io::Errno::SRCH) => ProcessObservation::Absent,
250 _ => ProcessObservation::Unknown,
251 }
252}
253
254#[cfg(any(target_os = "linux", test))]
255#[derive(Clone, Copy)]
256struct LinuxTaskStat {
257 executing: bool,
258 threads: u32,
259 ticks: u64,
260}
261
262#[cfg(any(target_os = "linux", test))]
263fn parse_linux_stat(stat: &str, pid: u32) -> Option<LinuxTaskStat> {
264 let (prefix, rest) = stat.rsplit_once(") ")?;
265 if prefix.split_once(" (").and_then(|(pid, _)| decimal(pid)) != Some(u64::from(pid)) {
266 return None;
267 }
268 let mut fields = rest.split_whitespace();
269 let executing = match fields.next()? {
270 "Z" | "X" | "x" => false,
271 "R" | "S" | "D" | "T" | "t" | "K" | "W" | "P" | "I" => true,
272 _ => return None,
273 };
274 let threads = u32::try_from(fields.nth(16).and_then(decimal)?).ok()?;
277 let ticks = fields.nth(1).and_then(decimal)?;
278 (threads > 0).then_some(LinuxTaskStat {
279 executing,
280 threads,
281 ticks,
282 })
283}
284
285#[cfg(any(target_os = "linux", test))]
286fn linux_stat(stat: &str, pid: u32, boot: &str) -> ProcessObservation {
287 let Some(task) = parse_linux_stat(stat, pid) else {
288 return ProcessObservation::Unknown;
289 };
290 if task.executing {
291 ProcessObservation::Present(ProcessIdentity {
292 boot_id: boot.into(),
293 start: ProcessStart::Linux { ticks: task.ticks },
294 })
295 } else if task.threads == 1 {
296 ProcessObservation::Absent
297 } else {
298 ProcessObservation::Unknown
301 }
302}
303
304#[cfg(target_os = "linux")]
308fn observe_linux_threads(
309 process: &std::path::Path,
310 pid: u32,
311 boot: &str,
312 leader: LinuxTaskStat,
313) -> ProcessObservation {
314 const MAX_THREAD_OBSERVATIONS: usize = 4096;
315 let Ok(entries) = std::fs::read_dir(process.join("task")) else {
316 return ProcessObservation::Unknown;
317 };
318 for (index, entry) in entries.enumerate() {
319 if index >= MAX_THREAD_OBSERVATIONS {
320 return ProcessObservation::Unknown;
321 }
322 let Ok(entry) = entry else {
323 return ProcessObservation::Unknown;
324 };
325 let Some(tid) = entry
326 .file_name()
327 .to_str()
328 .and_then(decimal)
329 .and_then(|tid| u32::try_from(tid).ok())
330 .filter(|tid| *tid > 0 && i32::try_from(*tid).is_ok())
331 else {
332 return ProcessObservation::Unknown;
333 };
334 let Ok(stat) = std::fs::read_to_string(entry.path().join("stat")) else {
335 return ProcessObservation::Unknown;
336 };
337 let Some(task) = parse_linux_stat(&stat, tid) else {
338 return ProcessObservation::Unknown;
339 };
340 if task.executing {
341 let confirmed = std::fs::read_to_string(process.join("stat"))
344 .ok()
345 .and_then(|stat| parse_linux_stat(&stat, pid));
346 return if confirmed.is_some_and(|current| current.ticks == leader.ticks) {
347 ProcessObservation::Present(ProcessIdentity {
348 boot_id: boot.into(),
349 start: ProcessStart::Linux {
350 ticks: leader.ticks,
351 },
352 })
353 } else {
354 ProcessObservation::Unknown
355 };
356 }
357 }
358 ProcessObservation::Unknown
359}
360
361#[cfg(target_os = "macos")]
362pub fn observe_process(pid: u32) -> ProcessObservation {
363 if pid <= 1 || i32::try_from(pid).is_err() {
364 return ProcessObservation::Unknown;
365 }
366 let Some(boot) = boot_identity() else {
367 return ProcessObservation::Unknown;
368 };
369 match libproc::proc_pid::pidinfo::<libproc::bsd_info::BSDInfo>(pid as i32, 0) {
370 Ok(info) => darwin_info(
371 pid,
372 info.pbi_pid,
373 info.pbi_status,
374 info.pbi_start_tvsec,
375 info.pbi_start_tvusec,
376 &boot,
377 ),
378 Err(_) => {
379 kernel_absence(pid)
382 }
383 }
384}
385
386#[cfg(any(target_os = "macos", test))]
387fn darwin_info(
388 requested: u32,
389 actual: u32,
390 state: u32,
391 seconds: u64,
392 microseconds: u64,
393 boot: &str,
394) -> ProcessObservation {
395 if actual != requested || seconds == 0 || microseconds >= 1_000_000 {
396 return ProcessObservation::Unknown;
397 }
398 match state {
401 5 => ProcessObservation::Absent,
402 1..=4 => ProcessObservation::Present(ProcessIdentity {
403 boot_id: boot.into(),
404 start: ProcessStart::Darwin {
405 seconds,
406 microseconds: microseconds as u32,
407 },
408 }),
409 _ => ProcessObservation::Unknown,
410 }
411}
412
413#[cfg(not(any(target_os = "linux", target_os = "macos")))]
414pub fn observe_process(_pid: u32) -> ProcessObservation {
415 ProcessObservation::Unknown
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 const BOOT: &str = "3f1c2a9e-5b7d-4e2a-9c1f-0a1b2c3d4e5f";
423
424 #[test]
425 fn native_precision_survives_records_and_rejects_second_resolution_history() {
426 let linux = format!("{BOOT}:123456789");
427 let darwin = format!("{BOOT}:darwin:1791280000:123456");
428 for record in [&linux, &darwin] {
429 let identity = ProcessIdentity::from_record(record).unwrap();
430 assert_eq!(identity.to_record().as_ref(), Some(record));
431 assert_eq!(
432 serde_json::from_str::<ProcessIdentity>(&serde_json::to_string(&identity).unwrap())
433 .unwrap(),
434 identity
435 );
436 }
437 for record in [
438 format!("{BOOT}:Mon Sep 28 10:48:17 2026"),
439 format!("{BOOT}:darwin:1791280000:1000000"),
440 format!("{BOOT}:darwin:0:123"),
441 format!("{BOOT}:darwin:1791280000:123:4"),
442 format!("{BOOT}:+123"),
443 "unverified-boot:123".into(),
444 format!("{}:123", BOOT.to_uppercase()),
445 ] {
446 assert!(ProcessIdentity::from_record(&record).is_none(), "{record}");
447 }
448 }
449
450 #[test]
451 fn matching_distinguishes_microseconds_reboots_and_unknown_observation() {
452 let owner =
453 ProcessIdentity::from_record(&format!("{BOOT}:darwin:1791280000:123456")).unwrap();
454 assert_eq!(
455 owner.presence(&ProcessObservation::Present(owner.clone())),
456 OwnerPresence::Live
457 );
458 let mut reused = owner.clone();
459 reused.start = ProcessStart::Darwin {
460 seconds: 1791280000,
461 microseconds: 123457,
462 };
463 assert_eq!(
464 owner.presence(&ProcessObservation::Present(reused)),
465 OwnerPresence::Absent
466 );
467 let mut rebooted = owner.clone();
468 rebooted.boot_id = "00000000-0000-0000-0000-000000000001".into();
469 assert_eq!(
470 owner.presence(&ProcessObservation::Present(rebooted)),
471 OwnerPresence::Absent
472 );
473 assert_eq!(
474 owner.presence(&ProcessObservation::Unknown),
475 OwnerPresence::Unknown
476 );
477 assert_eq!(
478 owner.presence(&ProcessObservation::Absent),
479 OwnerPresence::Absent
480 );
481 let linux = ProcessIdentity::from_record(&format!("{BOOT}:123")).unwrap();
482 assert_eq!(
483 owner.presence(&ProcessObservation::Present(linux)),
484 OwnerPresence::Unknown
485 );
486 }
487
488 #[test]
489 fn absent_pid_is_independent_proof_but_unknown_or_legacy_birth_cannot_adopt_a_live_pid() {
490 let current =
491 ProcessIdentity::from_record(&format!("{BOOT}:darwin:1791280000:123456")).unwrap();
492 let legacy = ProcessIdentity::from_record(&format!("{BOOT}:Mon Sep 28 10:48:17 2026"));
493 assert!(legacy.is_none());
494 for expected in [None, legacy.as_ref(), Some(¤t)] {
495 assert_eq!(
496 observed_owner_presence(expected, &ProcessObservation::Absent),
497 OwnerPresence::Absent
498 );
499 assert_eq!(
500 observed_owner_presence(expected, &ProcessObservation::Unknown),
501 OwnerPresence::Unknown
502 );
503 }
504 assert_eq!(
505 observed_owner_presence(None, &ProcessObservation::Present(current)),
506 OwnerPresence::Unknown
507 );
508 }
509
510 #[test]
511 fn proc_stat_handles_command_parentheses_and_refuses_wrong_pid_namespace() {
512 let stat = |state| linux_record(123, state, "1", 42);
513 let expected = ProcessIdentity::from_record(&format!("{BOOT}:42")).unwrap();
514 assert_eq!(
515 expected.presence(&linux_stat(&stat("S"), 123, BOOT)),
516 OwnerPresence::Live
517 );
518 for state in ["Z", "X", "x"] {
519 assert_eq!(
520 linux_stat(&stat(state), 123, BOOT),
521 ProcessObservation::Absent
522 );
523 }
524 assert_eq!(
525 linux_stat(&stat("S"), 456, BOOT),
526 ProcessObservation::Unknown
527 );
528 assert_eq!(
529 linux_stat(&stat("?"), 123, BOOT),
530 ProcessObservation::Unknown
531 );
532 assert_eq!(
533 linux_stat("truncated", 123, BOOT),
534 ProcessObservation::Unknown
535 );
536 }
537
538 fn linux_record(pid: u32, state: &str, threads: &str, ticks: u64) -> String {
539 format!(
540 "{pid} (rch (wrapper)) {state} {} {threads} 0 {ticks}",
541 vec!["0"; 16].join(" ")
542 )
543 }
544
545 #[test]
546 fn namespace_validation_rejects_coincident_and_ambiguous_ids() {
547 let stat = linux_record(123, "S", "1", 42);
548 assert!(linux_namespace_matches(
549 123,
550 &stat,
551 "Name:\trch\nNSpid:\t123\n"
552 ));
553 for status in [
554 "NSpid:\t123\t123\n",
555 "NSpid:\t123\t7\n",
556 "NSpid:\t999\t123\n",
557 "NSpid:\t999\n",
558 "NSpid:\t123\nNSpid:\t123\n",
559 "NSpid:\n",
560 "NSpid:\t+123\n",
561 "NSpid:\t123 invalid\n",
562 "Name:\trch\n",
563 "",
564 ] {
565 assert!(!linux_namespace_matches(123, &stat, status), "{status:?}");
566 }
567 assert!(!linux_namespace_matches(456, &stat, "NSpid:\t456\n"));
568 assert!(!linux_namespace_matches(123, "truncated", "NSpid:\t123\n"));
569 }
570
571 #[cfg(target_os = "linux")]
572 #[test]
573 fn a_missing_proc_entry_cannot_release_a_live_owner() {
574 let hidden = tempfile::tempdir().unwrap();
575 let pid = std::process::id();
576 let expected = current_process_identity().expect("native observer identity");
577 let observed = observe_linux_process(hidden.path(), pid, &expected.boot_id);
580 assert_eq!(observed, ProcessObservation::Unknown);
581 assert_eq!(expected.presence(&observed), OwnerPresence::Unknown);
582 assert_eq!(
583 observed_owner_presence(None, &observed),
584 OwnerPresence::Unknown
585 );
586 }
587
588 #[cfg(any(target_os = "linux", target_os = "macos"))]
589 #[test]
590 fn signal_free_probe_never_supplies_a_live_birth_identity() {
591 assert_eq!(
592 kernel_absence(std::process::id()),
593 ProcessObservation::Unknown
594 );
595 for invalid in [0, 1, u32::MAX] {
596 assert_eq!(kernel_absence(invalid), ProcessObservation::Unknown);
597 }
598 let mut child = std::process::Command::new("/bin/sleep")
599 .arg("20")
600 .spawn()
601 .unwrap();
602 let observed = kernel_absence(child.id());
603 child.kill().unwrap();
604 child.wait().unwrap();
605 assert_eq!(observed, ProcessObservation::Unknown);
606 assert_eq!(kernel_absence(child.id()), ProcessObservation::Absent);
607 }
608
609 #[test]
610 fn a_terminal_leader_with_other_threads_is_not_an_absent_owner() {
611 for state in ["Z", "X", "x"] {
612 assert_eq!(
613 linux_stat(&linux_record(123, state, "2", 42), 123, BOOT),
614 ProcessObservation::Unknown
615 );
616 }
617 for threads in ["0", "-1", "invalid", "4294967296"] {
618 for state in ["Z", "S"] {
619 assert_eq!(
620 linux_stat(&linux_record(123, state, threads, 42), 123, BOOT),
621 ProcessObservation::Unknown
622 );
623 }
624 }
625 }
626
627 #[cfg(target_os = "linux")]
628 fn thread_fixture() -> (tempfile::TempDir, LinuxTaskStat) {
629 let directory = tempfile::tempdir().unwrap();
630 let stat = linux_record(123, "Z", "2", 42);
631 std::fs::write(directory.path().join("stat"), &stat).unwrap();
632 let leader = parse_linux_stat(&stat, 123).unwrap();
633 (directory, leader)
634 }
635
636 #[cfg(target_os = "linux")]
637 fn write_task(process: &std::path::Path, tid: u32, state: &str) {
638 let directory = process.join("task").join(tid.to_string());
639 std::fs::create_dir_all(&directory).unwrap();
640 std::fs::write(directory.join("stat"), linux_record(tid, state, "2", 900)).unwrap();
641 }
642
643 #[cfg(target_os = "linux")]
644 #[test]
645 fn surviving_thread_retains_the_group_leaders_birth_identity() {
646 let (directory, leader) = thread_fixture();
647 write_task(directory.path(), 456, "S");
648 let observed = observe_linux_threads(directory.path(), 123, BOOT, leader);
649 let expected = ProcessIdentity::from_record(&format!("{BOOT}:42")).unwrap();
650 assert_eq!(observed, ProcessObservation::Present(expected.clone()));
651 assert_eq!(expected.presence(&observed), OwnerPresence::Live);
652 assert_eq!(
653 observed_owner_presence(None, &observed),
654 OwnerPresence::Unknown
655 );
656 }
657
658 #[cfg(target_os = "linux")]
659 #[test]
660 fn incomplete_and_dead_task_inventories_never_authorize_release() {
661 let (directory, leader) = thread_fixture();
662 let observe = || observe_linux_threads(directory.path(), 123, BOOT, leader);
663 assert_eq!(observe(), ProcessObservation::Unknown);
664 std::fs::create_dir(directory.path().join("task")).unwrap();
665 assert_eq!(observe(), ProcessObservation::Unknown);
666 write_task(directory.path(), 123, "Z");
667 assert_eq!(observe(), ProcessObservation::Unknown);
668 write_task(directory.path(), 456, "Z");
669 assert_eq!(observe(), ProcessObservation::Unknown);
670 std::fs::write(directory.path().join("task/456/stat"), "truncated").unwrap();
671 assert_eq!(observe(), ProcessObservation::Unknown);
672 }
673
674 #[cfg(target_os = "linux")]
675 #[test]
676 fn a_reused_leader_cannot_inherit_a_threads_liveness_evidence() {
677 let (directory, leader) = thread_fixture();
678 write_task(directory.path(), 456, "S");
679 std::fs::write(
680 directory.path().join("stat"),
681 linux_record(123, "S", "2", 43),
682 )
683 .unwrap();
684 assert_eq!(
685 observe_linux_threads(directory.path(), 123, BOOT, leader),
686 ProcessObservation::Unknown
687 );
688 }
689
690 #[test]
691 fn darwin_kernel_records_validate_pid_status_and_fractional_time() {
692 assert!(matches!(
693 darwin_info(42, 42, 2, 1791280000, 123456, BOOT),
694 ProcessObservation::Present(_)
695 ));
696 assert_eq!(
697 darwin_info(42, 42, 5, 1791280000, 123456, BOOT),
698 ProcessObservation::Absent
699 );
700 for (pid, state, seconds, micros) in [
701 (43, 2, 1791280000, 123456),
702 (42, 99, 1791280000, 123456),
703 (42, 2, 0, 1),
704 (42, 2, 1791280000, 1000000),
705 ] {
706 assert_eq!(
707 darwin_info(42, pid, state, seconds, micros, BOOT),
708 ProcessObservation::Unknown
709 );
710 }
711 }
712
713 #[cfg(target_os = "macos")]
714 #[test]
715 fn darwin_native_birth_is_stable_and_a_reaped_child_is_absent() {
716 let own = current_process_identity().expect("native own process identity");
717 assert!(matches!(own.start, ProcessStart::Darwin { .. }));
718 assert_eq!(current_process_identity(), Some(own));
719 let mut child = std::process::Command::new("/bin/sleep")
720 .arg("30")
721 .spawn()
722 .unwrap();
723 let observed = observe_process(child.id());
724 child.kill().unwrap();
725 child.wait().unwrap();
726 assert!(
727 matches!(observed, ProcessObservation::Present(_)),
728 "{observed:?}"
729 );
730 assert_eq!(observe_process(child.id()), ProcessObservation::Absent);
731 }
732}