process_fun/
lib.rs

1//! # process-fun
2//!
3//! A library for easily running Rust functions in separate processes with minimal boilerplate.
4//!
5//! ## Overview
6//!
7//! This crate provides a simple macro-based approach to execute Rust functions in separate processes.
8//! The `#[process]` attribute macro creates an additional version of your function that runs in a
9//! separate process, while keeping the original function unchanged. This allows you to choose between
10//! in-process and out-of-process execution as needed.
11//!
12//! ## Process Execution Model
13//!
14//! When a function marked with `#[process]` is called through its `_process` variant:
15//!
16//! 1. A new process is forked from the current process
17//! 2. A ProcessWrapper is returned which allows:
18//!    - Waiting for completion with optional timeout
19//!    - Automatic process cleanup on timeout or drop
20//!    - Safe result deserialization
21//!
22//! This execution model ensures complete isolation between the parent and child processes,
23//! making it suitable for running potentially risky or resource-intensive operations.
24//!
25//! ## Usage
26//!
27//! ```rust
28//! use process_fun::process;
29//! use serde::{Serialize, Deserialize};
30//! use std::time::Duration;
31//!
32//! #[derive(Serialize, Deserialize, Debug, Clone)]
33//! struct Point {
34//!     x: i32,
35//!     y: i32,
36//! }
37//!
38//! #[process]
39//! pub fn add_points(p1: Point, p2: Point) -> Point {
40//!     Point {
41//!         x: p1.x + p2.x,
42//!         y: p1.y + p2.y,
43//!     }
44//! }
45//!
46//! fn main() {
47//!     let p1 = Point { x: 1, y: 2 };
48//!     let p2 = Point { x: 3, y: 4 };
49//!     
50//!     // Use original function (in-process)
51//!     let result1 = add_points(p1.clone(), p2.clone());
52//!     
53//!     // Use process version with timeout (out-of-process)
54//!     let mut process = add_points_process(p1, p2).unwrap();
55//!     let result2 = process.timeout(Duration::from_secs(5)).unwrap();
56//!     
57//!     assert_eq!(result1.x, result2.x);
58//!     assert_eq!(result1.y, result2.y);
59//! }
60//! ```
61//!
62//! ## Timeout Example
63//!
64//! ```rust
65//! use process_fun::process;
66//! use std::time::Duration;
67//! use std::thread;
68//!
69//! #[process]
70//! fn long_task() -> i32 {
71//!     thread::sleep(Duration::from_secs(10));
72//!     42
73//! }
74//!
75//! fn main() {
76//!     let mut process = long_task_process().unwrap();
77//!     
78//!     // Process will be killed if it exceeds timeout
79//!     match process.timeout(Duration::from_secs(1)) {
80//!         Ok(result) => println!("Task completed: {}", result),
81//!         Err(e) => println!("Task timed out: {}", e)
82//!     }
83//! }
84//! ```
85
86#[allow(unused)]
87use serde::{Deserialize, Serialize};
88
89pub use process_fun_core::*;
90pub use process_fun_macro::process;
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use std::fs;
96    use std::thread;
97    use std::time::Duration;
98
99    #[derive(Serialize, Deserialize, Debug)]
100    pub struct Point {
101        pub x: i32,
102        pub y: i32,
103    }
104
105    #[derive(Serialize, Deserialize, Debug)]
106    pub struct Counter {
107        value: i32,
108    }
109
110    impl Counter {
111        pub fn new(initial: i32) -> Self {
112            Self { value: initial }
113        }
114
115        #[process]
116        pub fn get_value(&self) -> i32 {
117            self.value
118        }
119    }
120
121    #[test]
122    fn test_self_methods() {
123        let counter = Counter::new(5);
124
125        // Test immutable reference
126        let result = counter.get_value_process().unwrap().wait().unwrap();
127        assert_eq!(result, 5);
128    }
129
130    #[process]
131    pub fn add_points(p1: Point, p2: Point) -> Point {
132        Point {
133            x: p1.x + p2.x,
134            y: p1.y + p2.y,
135        }
136    }
137
138    #[test]
139    fn test_process_function() {
140        let p1 = Point { x: 1, y: 2 };
141        let p2 = Point { x: 3, y: 4 };
142
143        let result = add_points_process(p1, p2).unwrap().wait().unwrap();
144        assert_eq!(result.x, 4);
145        assert_eq!(result.y, 6);
146    }
147
148    #[process]
149    fn panicking_function() -> i32 {
150        panic!("This function panics!");
151    }
152
153    #[test]
154    fn test_process_panic() {
155        let result = panicking_function_process().unwrap().wait();
156        assert!(result.is_err(), "Expected error due to panic");
157    }
158
159    #[process]
160    fn slow_but_within_timeout() -> i32 {
161        thread::sleep(Duration::from_millis(500));
162        42
163    }
164
165    #[test]
166    fn test_timeout_success() {
167        let mut process = slow_but_within_timeout_process().unwrap();
168        let result = process.timeout(Duration::from_secs(1));
169        assert!(result.is_ok(), "{}", result.unwrap_err().to_string());
170        assert_eq!(result.unwrap(), 42);
171    }
172
173    #[process]
174    fn write_file_slow() -> bool {
175        // Try to write to a file after sleeping
176        thread::sleep(Duration::from_secs(5));
177        fs::write("test_timeout.txt", "This should not be written").unwrap();
178        true
179    }
180
181    #[test]
182    fn test_timeout_kill() {
183        // Clean up any existing file
184        let _ = fs::remove_file("test_timeout.txt");
185
186        let mut process = write_file_slow_process().unwrap();
187        let result = process.timeout(Duration::from_millis(500));
188
189        // Should timeout
190        assert!(result.is_err());
191
192        // Give a small grace period for the filesystem
193        thread::sleep(Duration::from_secs(5));
194
195        // File should not exist since process was killed
196        let exists = std::path::Path::new("test_timeout.txt").exists();
197
198        // Clean up
199        let _ = fs::remove_file("test_timeout.txt");
200        assert!(!exists, "Process wasn't killed in time - file was created");
201    }
202
203    #[process]
204    fn long_calculation(iterations: u64) -> u64 {
205        let mut sum: u64 = 0;
206        for i in 0..iterations {
207            sum = sum.wrapping_add(i);
208            if i % 1000 == 0 {
209                // Small sleep to make it actually take some time
210                thread::sleep(Duration::from_micros(1));
211            }
212        }
213        sum
214    }
215
216    #[test]
217    fn test_long_calculation() {
218        let iterations = 1_000_000;
219        let mut process = long_calculation_process(iterations).unwrap();
220        let start_time = std::time::Instant::now();
221        let result = process.timeout(Duration::from_secs(5));
222        let elapsed = start_time.elapsed();
223        assert!(
224            result.is_ok(),
225            "Long calculation should complete within timeout"
226        );
227        assert!(
228            elapsed < Duration::from_secs(3),
229            "Long calculation should complete within timeout and return early"
230        );
231        // Verify the result matches in-process calculation
232        let expected = long_calculation(iterations);
233        assert_eq!(result.unwrap(), expected);
234    }
235}