1use std::fs::{self, OpenOptions};
10use std::io::Write;
11use std::path::{Path, PathBuf};
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14use serde::{Deserialize, Serialize};
15
16use crate::error::ComputerError;
17use crate::tree::{ElectronEdit, SigNode};
18
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21pub struct Snapshot {
22 pub revision: u64,
24 pub tree: String,
26 pub root: SigNode,
28 pub truncated: bool,
30 pub pid: i32,
32 pub window_id: u32,
34 pub bundle_id: Option<String>,
36 pub electron: Option<ElectronEdit>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub observed: Option<SigNode>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub observed_revision: Option<u64>,
47}
48
49const OWNER_FILE: &str = "owner.json";
50pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(24 * 60 * 60);
52
53pub fn cache_dir() -> PathBuf {
57 cache_root()
58}
59
60pub fn sessions_root() -> PathBuf {
62 cache_root().join("sessions")
63}
64
65fn session_path(session: Option<&str>) -> PathBuf {
66 let name =
67 sanitize_token(session.unwrap_or("")).unwrap_or_else(|| std::process::id().to_string());
68 sessions_root().join(name)
69}
70
71pub fn session_dir(session: Option<&str>) -> Result<PathBuf, ComputerError> {
73 let dir = session_path(session);
74 fs::create_dir_all(&dir).map_err(ComputerError::Io)?;
75 #[cfg(unix)]
76 set_mode(&dir, 0o700);
77 ensure_owner(&dir)?;
78 Ok(dir)
79}
80
81#[derive(Clone, Debug, Serialize, Deserialize)]
82struct SessionOwner {
83 pid: u32,
84 created_unix_ms: u64,
85}
86
87fn ensure_owner(dir: &Path) -> Result<(), ComputerError> {
88 let path = dir.join(OWNER_FILE);
89 if path.exists() {
90 return Ok(());
91 }
92 let owner = SessionOwner {
93 pid: std::process::id(),
94 created_unix_ms: unix_ms(SystemTime::now()),
95 };
96 write_secret_json(&path, &owner)
97}
98
99fn unix_ms(time: SystemTime) -> u64 {
100 time.duration_since(UNIX_EPOCH)
101 .map(|duration| duration.as_millis() as u64)
102 .unwrap_or(0)
103}
104
105fn time_from_unix_ms(ms: u64) -> SystemTime {
106 UNIX_EPOCH + Duration::from_millis(ms)
107}
108
109pub fn load(
111 session: Option<&str>,
112 bundle_id: Option<&str>,
113 pid: i32,
114 window_id: u32,
115) -> Result<Option<Snapshot>, ComputerError> {
116 let path = snapshot_path(session, bundle_id, pid, window_id)?;
117 if !path.exists() {
118 return Ok(None);
119 }
120 let text = fs::read_to_string(&path).map_err(ComputerError::Io)?;
121 serde_json::from_str(&text).map(Some).map_err(|error| {
122 ComputerError::ExecutionFailed(format!(
123 "session cache {} is not readable: {error}",
124 path.display()
125 ))
126 })
127}
128
129const SCREENSHOT_MAP_FILE: &str = "screenshot-map.json";
130
131pub fn store_screenshot_map(
133 session: Option<&str>,
134 map: &crate::image_util::ScreenshotMap,
135) -> Result<(), ComputerError> {
136 let path = session_dir(session)?.join(SCREENSHOT_MAP_FILE);
137 write_secret_json(&path, map)
138}
139
140pub fn load_screenshot_map(
142 session: Option<&str>,
143) -> Result<Option<crate::image_util::ScreenshotMap>, ComputerError> {
144 let path = session_path(session).join(SCREENSHOT_MAP_FILE);
145 if !path.exists() {
146 return Ok(None);
147 }
148 let text = fs::read_to_string(&path).map_err(ComputerError::Io)?;
149 serde_json::from_str(&text).map(Some).map_err(|error| {
150 ComputerError::ExecutionFailed(format!(
151 "screenshot map {} is not readable: {error}",
152 path.display()
153 ))
154 })
155}
156
157pub fn store(session: Option<&str>, snapshot: &Snapshot) -> Result<(), ComputerError> {
159 let path = snapshot_path(
160 session,
161 snapshot.bundle_id.as_deref(),
162 snapshot.pid,
163 snapshot.window_id,
164 )?;
165 let mut snapshot = snapshot.clone();
166 if snapshot.electron.is_none() {
167 if let Some(previous) = read_snapshot(&path)? {
168 snapshot.electron = previous.electron;
169 }
170 }
171 write_json(&path, &snapshot)
172}
173
174pub fn pending_restores(session: Option<&str>) -> Result<Vec<(i32, ElectronEdit)>, ComputerError> {
176 let dir = session_dir(session)?;
177 let mut found = Vec::new();
178 let entries = fs::read_dir(&dir).map_err(ComputerError::Io)?;
179 for entry in entries {
180 let entry = entry.map_err(ComputerError::Io)?;
181 let path = entry.path();
182 if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
183 continue;
184 }
185 if !is_snapshot_file(&path) {
186 continue;
187 }
188 let Some(snapshot) = read_snapshot(&path)? else {
189 continue;
190 };
191 let Some(edit) = snapshot.electron else {
192 continue;
193 };
194 if found.iter().any(|(pid, _)| *pid == snapshot.pid) {
195 continue;
196 }
197 found.push((snapshot.pid, edit));
198 }
199 Ok(found)
200}
201
202pub fn clear_restores(session: Option<&str>, pids: &[i32]) -> Result<(), ComputerError> {
204 let dir = session_dir(session)?;
205 let entries = fs::read_dir(&dir).map_err(ComputerError::Io)?;
206 for entry in entries {
207 let entry = entry.map_err(ComputerError::Io)?;
208 let path = entry.path();
209 if !is_snapshot_file(&path) {
210 continue;
211 }
212 let Some(mut snapshot) = read_snapshot(&path)? else {
213 continue;
214 };
215 if pids.contains(&snapshot.pid) && snapshot.electron.is_some() {
216 snapshot.electron = None;
217 write_json(&path, &snapshot)?;
218 }
219 }
220 Ok(())
221}
222
223fn is_snapshot_file(path: &Path) -> bool {
224 if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
225 return false;
226 }
227 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
228 return false;
229 };
230 name != SCREENSHOT_MAP_FILE && name != OWNER_FILE
231}
232
233#[derive(Clone, Debug)]
235pub struct PruneOptions {
236 pub dry_run: bool,
238 pub ttl: Duration,
240 pub now: SystemTime,
242}
243
244impl Default for PruneOptions {
245 fn default() -> Self {
246 Self {
247 dry_run: false,
248 ttl: DEFAULT_SESSION_TTL,
249 now: SystemTime::now(),
250 }
251 }
252}
253
254#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct PruneReport {
257 pub removed: Vec<String>,
259 pub kept: Vec<String>,
261}
262
263pub fn prune_sessions(
271 root: &Path,
272 options: &PruneOptions,
273 alive: &dyn Fn(u32) -> bool,
274) -> Result<PruneReport, ComputerError> {
275 let mut removed = Vec::new();
276 let mut kept = Vec::new();
277 if !root.exists() {
278 return Ok(PruneReport { removed, kept });
279 }
280 let mut names = Vec::new();
281 for entry in fs::read_dir(root).map_err(ComputerError::Io)? {
282 let entry = entry.map_err(ComputerError::Io)?;
283 let path = entry.path();
284 if !path.is_dir() {
285 continue;
286 }
287 let Some(name) = entry.file_name().into_string().ok() else {
288 continue;
289 };
290 names.push((name, path));
291 }
292 names.sort_by(|left, right| left.0.cmp(&right.0));
293 for (name, path) in names {
294 if should_prune(&path, &name, options, alive)? {
295 if !options.dry_run {
296 fs::remove_dir_all(&path).map_err(ComputerError::Io)?;
297 }
298 removed.push(name);
299 } else {
300 kept.push(name);
301 }
302 }
303 Ok(PruneReport { removed, kept })
304}
305
306pub fn prune_empty_sessions() -> Result<Vec<String>, ComputerError> {
308 let root = sessions_root();
309 if !root.exists() {
310 return Ok(Vec::new());
311 }
312 let mut removed = Vec::new();
313 for entry in fs::read_dir(&root).map_err(ComputerError::Io)? {
314 let entry = entry.map_err(ComputerError::Io)?;
315 let path = entry.path();
316 if !path.is_dir() {
317 continue;
318 }
319 match session_has_content(&path) {
320 Ok(true) => continue,
321 Ok(false) => {}
322 Err(_) => continue,
323 }
324 fs::remove_dir_all(&path).map_err(ComputerError::Io)?;
325 if let Ok(name) = entry.file_name().into_string() {
326 removed.push(name);
327 }
328 }
329 removed.sort();
330 Ok(removed)
331}
332
333pub fn pid_alive(pid: u32) -> bool {
338 if pid == 0 || pid > i32::MAX as u32 {
339 return false;
340 }
341 #[cfg(unix)]
342 {
343 std::process::Command::new("kill")
344 .arg("-0")
345 .arg(pid.to_string())
346 .stdin(std::process::Stdio::null())
347 .stdout(std::process::Stdio::null())
348 .stderr(std::process::Stdio::null())
349 .status()
350 .map(|status| status.success())
351 .unwrap_or(false)
352 }
353 #[cfg(windows)]
354 {
355 let output = std::process::Command::new("tasklist")
356 .args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"])
357 .stdin(std::process::Stdio::null())
358 .stdout(std::process::Stdio::piped())
359 .stderr(std::process::Stdio::null())
360 .output();
361 match output {
362 Ok(output) => {
363 let text = String::from_utf8_lossy(&output.stdout);
364 text.split(',')
365 .any(|field| field.trim().trim_matches('"') == pid.to_string())
366 }
367 Err(_) => false,
368 }
369 }
370}
371
372fn should_prune(
373 path: &Path,
374 name: &str,
375 options: &PruneOptions,
376 alive: &dyn Fn(u32) -> bool,
377) -> Result<bool, ComputerError> {
378 if !session_has_content(path)? {
379 return Ok(true);
380 }
381 if let Some(pid) = owner_pid(path, name) {
382 if alive(pid) {
383 return Ok(false);
384 }
385 }
386 Ok(session_age(path, options.now) >= options.ttl)
387}
388
389fn session_has_content(dir: &Path) -> Result<bool, ComputerError> {
390 let entries = fs::read_dir(dir).map_err(ComputerError::Io)?;
391 for entry in entries {
392 let entry = entry.map_err(ComputerError::Io)?;
393 if entry.file_name() == std::ffi::OsStr::new(OWNER_FILE) {
394 continue;
395 }
396 return Ok(true);
397 }
398 Ok(false)
399}
400
401fn owner_pid(dir: &Path, name: &str) -> Option<u32> {
402 if let Some(owner) = read_owner(dir) {
403 return Some(owner.pid);
404 }
405 name.parse::<u32>().ok()
406}
407
408fn read_owner(dir: &Path) -> Option<SessionOwner> {
409 let text = fs::read_to_string(dir.join(OWNER_FILE)).ok()?;
410 serde_json::from_str(&text).ok()
411}
412
413fn session_age(dir: &Path, now: SystemTime) -> Duration {
414 let created = read_owner(dir)
415 .map(|owner| time_from_unix_ms(owner.created_unix_ms))
416 .or_else(|| dir.metadata().ok().and_then(|meta| meta.modified().ok()));
417 match created {
418 Some(created) => now.duration_since(created).unwrap_or(Duration::ZERO),
419 None => Duration::MAX,
420 }
421}
422
423fn snapshot_path(
424 session: Option<&str>,
425 bundle_id: Option<&str>,
426 pid: i32,
427 window_id: u32,
428) -> Result<PathBuf, ComputerError> {
429 let bundle = sanitize_token(bundle_id.unwrap_or("")).unwrap_or_else(|| "app".into());
430 Ok(session_dir(session)?.join(format!("{bundle}-{pid}-{window_id}.json")))
431}
432
433fn read_snapshot(path: &Path) -> Result<Option<Snapshot>, ComputerError> {
434 if !path.exists() {
435 return Ok(None);
436 }
437 let text = fs::read_to_string(path).map_err(ComputerError::Io)?;
438 if text.trim().is_empty() {
439 return Ok(None);
440 }
441 serde_json::from_str(&text).map(Some).map_err(|error| {
442 ComputerError::ExecutionFailed(format!(
443 "session cache {} is not readable: {error}",
444 path.display()
445 ))
446 })
447}
448
449fn write_json(path: &Path, snapshot: &Snapshot) -> Result<(), ComputerError> {
450 write_secret_json(path, snapshot)
451}
452
453pub fn write_secret_bytes(path: &Path, bytes: &[u8]) -> Result<(), ComputerError> {
455 let mut options = OpenOptions::new();
456 options.write(true).create(true).truncate(true);
457 #[cfg(unix)]
458 {
459 use std::os::unix::fs::OpenOptionsExt;
460 options.mode(0o600);
461 }
462 let mut file = options.open(path).map_err(ComputerError::Io)?;
463 file.write_all(bytes).map_err(ComputerError::Io)?;
464 Ok(())
465}
466
467fn write_secret_json(path: &Path, value: &impl Serialize) -> Result<(), ComputerError> {
468 let text = serde_json::to_string(value).map_err(|error| {
469 ComputerError::ExecutionFailed(format!("session cache encode failed: {error}"))
470 })?;
471 let mut options = OpenOptions::new();
472 options.write(true).create(true).truncate(true);
473 #[cfg(unix)]
474 {
475 use std::os::unix::fs::OpenOptionsExt;
476 options.mode(0o600);
477 }
478 let mut file = options.open(path).map_err(ComputerError::Io)?;
479 file.write_all(text.as_bytes()).map_err(ComputerError::Io)?;
480 Ok(())
481}
482
483fn cache_root() -> PathBuf {
484 if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
485 if !xdg.is_empty() {
486 return PathBuf::from(xdg).join("computer-use");
487 }
488 }
489 #[cfg(test)]
490 {
491 panic!("test used the session cache without XDG_CACHE_HOME");
492 }
493 #[cfg(not(test))]
494 {
495 cache_root_unchecked()
496 }
497}
498
499#[cfg(not(test))]
500fn cache_root_unchecked() -> PathBuf {
501 #[cfg(windows)]
502 {
503 dirs::data_local_dir()
504 .unwrap_or_else(|| PathBuf::from("."))
505 .join("computer-use")
506 .join("cache")
507 }
508 #[cfg(not(windows))]
509 {
510 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
511 home.join(".cache").join("computer-use")
512 }
513}
514
515fn sanitize_token(value: &str) -> Option<String> {
516 let mut out = String::new();
517 for ch in value.chars() {
518 if ch.is_ascii_alphanumeric() || ch == '.' || ch == '_' || ch == '-' {
519 out.push(ch);
520 } else {
521 out.push('_');
522 }
523 }
524 let trimmed = out.trim_matches('_');
525 if trimmed.is_empty() {
526 None
527 } else {
528 Some(trimmed.to_string())
529 }
530}
531
532#[cfg(unix)]
533fn set_mode(path: &Path, mode: u32) {
534 use std::os::unix::fs::PermissionsExt;
535 let _ = fs::set_permissions(path, fs::Permissions::from_mode(mode));
536}
537
538#[cfg(test)]
539pub(crate) static CACHE_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
540
541#[cfg(test)]
542#[allow(unsafe_code)]
543pub(crate) struct CacheEnv {
544 dir: PathBuf,
545 previous: Option<std::ffi::OsString>,
546 _lock: std::sync::MutexGuard<'static, ()>,
547}
548
549#[cfg(test)]
550#[allow(unsafe_code)]
551impl CacheEnv {
552 pub(crate) fn acquire() -> Self {
554 let lock = CACHE_ENV_LOCK
555 .lock()
556 .unwrap_or_else(|poison| poison.into_inner());
557 let dir = std::env::temp_dir().join(format!(
558 "computer-use-cache-{}-{}",
559 std::process::id(),
560 std::time::SystemTime::now()
561 .duration_since(std::time::UNIX_EPOCH)
562 .map(|duration| duration.as_nanos())
563 .unwrap_or(0)
564 ));
565 let _ = fs::remove_dir_all(&dir);
566 fs::create_dir_all(&dir).expect("temp cache dir");
567 let previous = std::env::var_os("XDG_CACHE_HOME");
568 unsafe { std::env::set_var("XDG_CACHE_HOME", &dir) };
570 Self {
571 dir,
572 previous,
573 _lock: lock,
574 }
575 }
576
577 pub(crate) fn dir(&self) -> &Path {
578 &self.dir
579 }
580}
581
582#[cfg(test)]
583#[allow(unsafe_code)]
584impl Drop for CacheEnv {
585 fn drop(&mut self) {
586 unsafe {
588 match &self.previous {
589 Some(value) => std::env::set_var("XDG_CACHE_HOME", value),
590 None => std::env::remove_var("XDG_CACHE_HOME"),
591 }
592 }
593 let _ = fs::remove_dir_all(&self.dir);
594 }
595}
596
597#[cfg(test)]
598#[allow(unsafe_code)]
599mod tests {
600 use super::*;
601 use crate::tree::SigNode;
602
603 fn sample(pid: i32, window: u32, value: &str) -> Snapshot {
604 Snapshot {
605 revision: 7,
606 tree: format!("[0] AXWindow \"{value}\"\n"),
607 root: SigNode {
608 index: 0,
609 role: "AXWindow".into(),
610 identity: value.into(),
611 child_count: 0,
612 value: String::new(),
613 frame: None,
614 secure: false,
615 children: Vec::new(),
616 },
617 truncated: false,
618 pid,
619 window_id: window,
620 bundle_id: Some("com.apple.Notes".into()),
621 electron: None,
622 observed: None,
623 observed_revision: None,
624 }
625 }
626
627 #[test]
628 fn round_trip_under_xdg_cache_home() {
629 let _guard = CACHE_ENV_LOCK.lock().unwrap();
630 let dir = std::env::temp_dir().join(format!(
631 "computer-use-cache-{}-{}",
632 std::process::id(),
633 std::time::SystemTime::now()
634 .duration_since(std::time::UNIX_EPOCH)
635 .map(|duration| duration.as_nanos())
636 .unwrap_or(0)
637 ));
638 let _ = fs::remove_dir_all(&dir);
639 fs::create_dir_all(&dir).unwrap();
640 unsafe { std::env::set_var("XDG_CACHE_HOME", &dir) };
643 let session = Some("unit");
644 let snapshot = sample(12, 34, "Notes");
645 store(session, &snapshot).unwrap();
646 let loaded = load(session, Some("com.apple.Notes"), 12, 34)
647 .unwrap()
648 .expect("stored snapshot");
649 assert_eq!(loaded, snapshot);
650 #[cfg(unix)]
651 {
652 use std::os::unix::fs::PermissionsExt;
653 let path = snapshot_path(session, Some("com.apple.Notes"), 12, 34).unwrap();
654 let mode = fs::metadata(&path).unwrap().permissions().mode();
655 assert_eq!(mode & 0o777, 0o600, "mode {mode:o}");
656 }
657 let mut with_flags = snapshot.clone();
658 with_flags.electron = Some(crate::tree::electron_enable(crate::tree::FlagState {
659 manual: Some(false),
660 enhanced: Some(false),
661 }));
662 with_flags.tree = "[0] AXWindow \"next\"\n".into();
663 store(session, &with_flags).unwrap();
664 let kept = load(session, Some("com.apple.Notes"), 12, 34)
665 .unwrap()
666 .unwrap();
667 assert!(kept.electron.is_some());
668 let pending = pending_restores(session).unwrap();
669 assert_eq!(pending.len(), 1);
670 assert_eq!(pending[0].0, 12);
671 clear_restores(session, &[12]).unwrap();
672 assert!(pending_restores(session).unwrap().is_empty());
673 let before = load(session, Some("com.apple.Notes"), 12, 34)
674 .unwrap()
675 .unwrap();
676 let map = crate::image_util::prepare_shot(
677 &image::RgbaImage::from_pixel(2, 2, image::Rgba([1, 2, 3, 255])),
678 crate::image_util::ShotFit {
679 scale: 2.0,
680 origin_x: 0,
681 origin_y: 0,
682 logical_width: 1,
683 logical_height: 1,
684 max_width: 1440,
685 canvas: None,
686 },
687 )
688 .unwrap()
689 .map;
690 store_screenshot_map(session, &map).unwrap();
691 let after = load(session, Some("com.apple.Notes"), 12, 34)
692 .unwrap()
693 .unwrap();
694 assert_eq!(before, after);
695 let loaded = load_screenshot_map(session).unwrap().expect("map");
696 assert_eq!(loaded.width, map.width);
697 assert!(pending_restores(session).unwrap().is_empty());
698 #[cfg(unix)]
699 {
700 use std::os::unix::fs::PermissionsExt;
701 let map_path = session_dir(session).unwrap().join("screenshot-map.json");
702 let mode = fs::metadata(&map_path).unwrap().permissions().mode();
703 assert_eq!(mode & 0o777, 0o600, "mode {mode:o}");
704 }
705 unsafe { std::env::remove_var("XDG_CACHE_HOME") };
707 let _ = fs::remove_dir_all(&dir);
708 }
709
710 #[test]
711 fn prune_removes_empty_and_stale_dirs_and_keeps_a_live_owner() {
712 let root = std::env::temp_dir().join(format!(
713 "computer-use-prune-{}-{}",
714 std::process::id(),
715 std::time::SystemTime::now()
716 .duration_since(std::time::UNIX_EPOCH)
717 .map(|duration| duration.as_nanos())
718 .unwrap_or(0)
719 ));
720 let _ = fs::remove_dir_all(&root);
721 fs::create_dir_all(&root).unwrap();
722 let now = SystemTime::now();
723 write_session(&root, "empty", 111, now, false);
724 write_session(
725 &root,
726 "live",
727 222,
728 now - Duration::from_secs(10 * 24 * 60 * 60),
729 true,
730 );
731 write_session(&root, "dead-fresh", 333, now, true);
732 write_session(
733 &root,
734 "dead-old",
735 444,
736 now - Duration::from_secs(48 * 60 * 60),
737 true,
738 );
739 let alive = |pid: u32| pid == 222;
740 let options = PruneOptions {
741 dry_run: true,
742 ttl: DEFAULT_SESSION_TTL,
743 now,
744 };
745 let dry = prune_sessions(&root, &options, &alive).unwrap();
746 assert_eq!(
747 dry.removed,
748 vec!["dead-old".to_string(), "empty".to_string()]
749 );
750 assert!(root.join("empty").exists(), "dry run must not delete");
751 assert!(root.join("live").exists());
752 assert!(root.join("dead-fresh").exists());
753
754 let removed = prune_sessions(
755 &root,
756 &PruneOptions {
757 dry_run: false,
758 ttl: DEFAULT_SESSION_TTL,
759 now,
760 },
761 &alive,
762 )
763 .unwrap();
764 assert_eq!(
765 removed.removed,
766 vec!["dead-old".to_string(), "empty".to_string()]
767 );
768 assert_eq!(
769 removed.kept,
770 vec!["dead-fresh".to_string(), "live".to_string()]
771 );
772 assert!(!root.join("empty").exists());
773 assert!(!root.join("dead-old").exists());
774 assert!(root.join("live").join("snapshot.json").exists());
775 assert!(root.join("dead-fresh").join("snapshot.json").exists());
776 let _ = fs::remove_dir_all(&root);
777 }
778
779 fn write_session(root: &Path, name: &str, pid: u32, created: SystemTime, content: bool) {
780 let dir = root.join(name);
781 fs::create_dir_all(&dir).unwrap();
782 let owner = serde_json::json!({
783 "pid": pid,
784 "created_unix_ms": created
785 .duration_since(std::time::UNIX_EPOCH)
786 .unwrap()
787 .as_millis() as u64,
788 });
789 fs::write(dir.join("owner.json"), owner.to_string()).unwrap();
790 if content {
791 fs::write(dir.join("snapshot.json"), "{\"revision\":1}").unwrap();
792 }
793 }
794
795 #[test]
796 fn current_process_is_alive_and_a_huge_pid_is_not() {
797 assert!(pid_alive(std::process::id()));
798 assert!(!pid_alive(u32::MAX));
799 }
800}