process_control/
lib.rs

1//! This crate allows running a process with resource limits, such as a running
2//! time, and the option to terminate it automatically afterward. The latter is
3//! surprisingly difficult to achieve on Unix, since process identifiers can be
4//! arbitrarily reassigned when no longer used. Thus, it would be extremely
5//! easy to inadvertently terminate an unexpected process. This crate protects
6//! against that possibility.
7//!
8//! Methods for setting limits are available on [`ChildExt`], which is
9//! implemented for [`Child`]. They each return a builder of options to
10//! configure how the limit should be applied.
11//!
12//! <div class="warning">
13//!
14//! This crate should not be used for security. There are many ways that a
15//! process can bypass resource limits. The limits are only intended for simple
16//! restriction of harmless processes.
17//!
18//! </div>
19//!
20//! # Features
21//!
22//! These features are optional and can be enabled or disabled in a
23//! "Cargo.toml" file.
24//!
25//! ### Optional Features
26//!
27//! - **parking\_lot** -
28//!   Changes the implementation to use crate [parking\_lot] on targets missing
29//!   some syscalls. This feature will reduce the likelihood of resource
30//!   starvation for those targets.
31//!
32//! # Implementation
33//!
34//! All traits are [sealed], meaning that they can only be implemented by this
35//! crate. Otherwise, backward compatibility would be more difficult to
36//! maintain for new features.
37//!
38//! # Comparable Crates
39//!
40//! - [wait-timeout] -
41//!   Made for a related purpose but does not provide the same functionality.
42//!   Processes cannot be terminated automatically, and there is no counterpart
43//!   of [`ChildExt::controlled_with_output`] to read output while setting a
44//!   timeout. This crate aims to fill in those gaps and simplify the
45//!   implementation, now that [`Receiver::recv_timeout`] exists.
46//!
47//! # Examples
48//!
49//! ```
50//! use std::io;
51//! use std::process::Command;
52//! use std::process::Stdio;
53//! use std::time::Duration;
54//!
55//! use process_control::ChildExt;
56//! use process_control::Control;
57//!
58//! let message = "hello world";
59//! let process = Command::new("echo")
60//!     .arg(message)
61//!     .stdout(Stdio::piped())
62//!     .spawn()?;
63//!
64//! let output = process
65//!     .controlled_with_output()
66//!     .time_limit(Duration::from_secs(1))
67//!     .terminate_for_timeout()
68//!     .wait()?
69//!     .ok_or_else(|| {
70//!         io::Error::new(io::ErrorKind::TimedOut, "Process timed out")
71//!     })?;
72//! assert!(output.status.success());
73//! assert_eq!(message.as_bytes(), &output.stdout[..message.len()]);
74//! #
75//! # Ok::<_, io::Error>(())
76//! ```
77//!
78//! [parking\_lot]: https://crates.io/crates/parking_lot
79//! [`Receiver::recv_timeout`]: ::std::sync::mpsc::Receiver::recv_timeout
80//! [sealed]: https://rust-lang.github.io/api-guidelines/future-proofing.html#c-sealed
81//! [wait-timeout]: https://crates.io/crates/wait-timeout
82
83// Only require a nightly compiler when building documentation for docs.rs.
84// This is a private option that should not be used.
85// https://github.com/rust-lang/docs.rs/issues/147#issuecomment-389544407
86#![cfg_attr(process_control_docs_rs, feature(doc_cfg))]
87#![warn(unused_results)]
88
89use std::fmt;
90use std::fmt::Debug;
91use std::fmt::Display;
92use std::fmt::Formatter;
93use std::io;
94#[cfg(any(doc, unix))]
95use std::os::raw::c_int;
96use std::process;
97use std::process::Child;
98use std::str;
99use std::time::Duration;
100
101mod control;
102
103#[cfg_attr(unix, path = "unix/mod.rs")]
104#[cfg_attr(windows, path = "windows/mod.rs")]
105mod imp;
106
107type WaitResult<T> = io::Result<Option<T>>;
108
109#[rustfmt::skip]
110macro_rules! unix_method {
111    ( $method:ident , $return_type:ty ) => {
112        #[doc = concat!(
113            "Equivalent to [`ExitStatusExt::",
114            stringify!($method),
115            "`][method].
116
117[method]: ::std::os::unix::process::ExitStatusExt::",
118            stringify!($method),
119        )]
120        #[cfg(any(doc, unix))]
121        #[cfg_attr(process_control_docs_rs, doc(cfg(unix)))]
122        #[inline]
123        #[must_use]
124        pub fn $method(&self) -> $return_type {
125            self.inner.$method()
126        }
127    };
128}
129
130/// Equivalent to [`process::ExitStatus`] but allows for greater accuracy.
131#[derive(Copy, Clone, Debug, Eq, PartialEq)]
132#[must_use]
133pub struct ExitStatus {
134    inner: imp::ExitStatus,
135    std: process::ExitStatus,
136}
137
138impl ExitStatus {
139    fn new(inner: imp::ExitStatus, std: process::ExitStatus) -> Self {
140        debug_assert_eq!(inner, std.into());
141        Self { inner, std }
142    }
143
144    /// Equivalent to [`process::ExitStatus::success`].
145    #[inline]
146    #[must_use]
147    pub fn success(self) -> bool {
148        self.inner.success()
149    }
150
151    /// Equivalent to [`process::ExitStatus::code`], but a more accurate value
152    /// will be returned if possible.
153    #[inline]
154    #[must_use]
155    pub fn code(self) -> Option<i64> {
156        self.inner.code().map(Into::into)
157    }
158
159    unix_method!(continued, bool);
160    unix_method!(core_dumped, bool);
161    unix_method!(signal, Option<c_int>);
162    unix_method!(stopped_signal, Option<c_int>);
163
164    /// Converts this structure to a corresponding [`process::ExitStatus`]
165    /// instance.
166    ///
167    /// Since this type can represent more exit codes, it will attempt to
168    /// provide an equivalent representation using the standard library type.
169    /// However, if converted back to this structure, detailed information may
170    /// have been lost.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// # use std::io;
176    /// use std::process::Command;
177    /// use std::time::Duration;
178    ///
179    /// use process_control::ChildExt;
180    /// use process_control::Control;
181    ///
182    /// let exit_status = Command::new("echo")
183    ///     .spawn()?
184    ///     .controlled()
185    ///     .time_limit(Duration::from_secs(1))
186    ///     .terminate_for_timeout()
187    ///     .wait()?
188    ///     .expect("process timed out");
189    /// assert!(exit_status.success());
190    /// assert!(exit_status.into_std_lossy().success());
191    /// #
192    /// # Ok::<_, io::Error>(())
193    /// ```
194    #[inline]
195    #[must_use]
196    pub fn into_std_lossy(self) -> process::ExitStatus {
197        self.std
198    }
199}
200
201impl AsMut<Self> for ExitStatus {
202    #[inline]
203    fn as_mut(&mut self) -> &mut Self {
204        self
205    }
206}
207
208impl AsRef<Self> for ExitStatus {
209    #[inline]
210    fn as_ref(&self) -> &Self {
211        self
212    }
213}
214
215impl Display for ExitStatus {
216    #[inline]
217    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
218        Display::fmt(&self.inner, f)
219    }
220}
221
222impl From<process::ExitStatus> for ExitStatus {
223    #[inline]
224    fn from(value: process::ExitStatus) -> Self {
225        Self::new(value.into(), value)
226    }
227}
228
229/// Equivalent to [`process::Output`] but holds an instance of [`ExitStatus`]
230/// from this crate.
231#[derive(Clone, Eq, PartialEq)]
232#[must_use]
233pub struct Output {
234    /// Equivalent to [`process::Output::status`].
235    pub status: ExitStatus,
236
237    /// Equivalent to [`process::Output::stdout`].
238    pub stdout: Vec<u8>,
239
240    /// Equivalent to [`process::Output::stderr`].
241    pub stderr: Vec<u8>,
242}
243
244impl Output {
245    /// Converts this structure to a corresponding [`process::Output`]
246    /// instance.
247    ///
248    /// For more information, see [`ExitStatus::into_std_lossy`].
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// # use std::io;
254    /// use std::process::Command;
255    /// use std::process::Stdio;
256    /// use std::time::Duration;
257    ///
258    /// use process_control::ChildExt;
259    /// use process_control::Control;
260    ///
261    /// let message = "foobar";
262    /// let output = Command::new("echo")
263    ///     .arg(message)
264    ///     .stdout(Stdio::piped())
265    ///     .spawn()?
266    ///     .controlled_with_output()
267    ///     .time_limit(Duration::from_secs(1))
268    ///     .terminate_for_timeout()
269    ///     .wait()?
270    ///     .expect("process timed out");
271    /// assert!(output.status.success());
272    /// assert_eq!(message.as_bytes(), &output.stdout[..message.len()]);
273    ///
274    /// let output = output.into_std_lossy();
275    /// assert!(output.status.success());
276    /// assert_eq!(message.as_bytes(), &output.stdout[..message.len()]);
277    /// #
278    /// # Ok::<_, io::Error>(())
279    /// ```
280    #[inline]
281    #[must_use]
282    pub fn into_std_lossy(self) -> process::Output {
283        process::Output {
284            status: self.status.into_std_lossy(),
285            stdout: self.stdout,
286            stderr: self.stderr,
287        }
288    }
289}
290
291impl AsMut<ExitStatus> for Output {
292    #[inline]
293    fn as_mut(&mut self) -> &mut ExitStatus {
294        &mut self.status
295    }
296}
297
298impl AsRef<ExitStatus> for Output {
299    #[inline]
300    fn as_ref(&self) -> &ExitStatus {
301        &self.status
302    }
303}
304
305struct DebugBuffer<'a>(&'a [u8]);
306
307impl Debug for DebugBuffer<'_> {
308    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
309        f.write_str("\"")?;
310
311        let mut string = self.0;
312        while !string.is_empty() {
313            let mut invalid = &b""[..];
314            let valid = str::from_utf8(string).unwrap_or_else(|error| {
315                let (valid, string) = string.split_at(error.valid_up_to());
316
317                let invalid_length =
318                    error.error_len().unwrap_or_else(|| string.len());
319                invalid = &string[..invalid_length];
320
321                // SAFETY: This slice was validated to be UTF-8.
322                unsafe { str::from_utf8_unchecked(valid) }
323            });
324
325            Display::fmt(&valid.escape_debug(), f)?;
326            string = &string[valid.len()..];
327
328            for byte in invalid {
329                write!(f, "\\x{:02X}", byte)?;
330            }
331            string = &string[invalid.len()..];
332        }
333
334        f.write_str("\"")
335    }
336}
337
338impl Debug for Output {
339    #[inline]
340    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
341        f.debug_struct("Output")
342            .field("status", &self.status)
343            .field("stdout", &DebugBuffer(&self.stdout))
344            .field("stderr", &DebugBuffer(&self.stderr))
345            .finish()
346    }
347}
348
349impl From<process::Output> for Output {
350    #[inline]
351    fn from(value: process::Output) -> Self {
352        Self {
353            status: value.status.into(),
354            stdout: value.stdout,
355            stderr: value.stderr,
356        }
357    }
358}
359
360impl From<Output> for ExitStatus {
361    #[inline]
362    fn from(value: Output) -> Self {
363        value.status
364    }
365}
366
367/// A function to be called for reads from a specific process pipe ([stdout] or
368/// [stderr]).
369///
370/// When additional bytes are read from the pipe, they are passed to this
371/// function, which determines whether to include them in [`Output`]. The
372/// number of bytes is not guaranteed to be consistent and may not match the
373/// number written at any time by the command on the other side of the stream.
374///
375/// If this function returns `Ok(false)`, the passed output will be discarded
376/// and not included in [`Output`]. Errors will be propagated to
377/// [`Control::wait`]. For more complex cases, where specific portions of read
378/// bytes should be included, this function can return `false` and maintain the
379/// output buffer itself.
380///
381/// # Examples
382///
383/// ```
384/// use std::io;
385/// use std::io::Write;
386/// use std::process::Command;
387/// use std::process::Stdio;
388///
389/// use process_control::ChildExt;
390/// use process_control::Control;
391///
392/// let message = "foobar";
393/// let output = Command::new("echo")
394///     .arg(message)
395///     .stdout(Stdio::piped())
396///     .spawn()?
397///     .controlled_with_output()
398///     // Stream output while collecting it.
399///     .stdout_filter(|x| io::stdout().write_all(x).map(|()| true))
400///     .wait()?
401///     .expect("process timed out");
402/// assert!(output.status.success());
403/// assert_eq!(message.as_bytes(), &output.stdout[..message.len()]);
404/// #
405/// # Ok::<_, io::Error>(())
406/// ```
407///
408/// [stderr]: Control::stderr_filter
409/// [stdout]: Control::stdout_filter
410pub trait PipeFilter:
411    'static + FnMut(&[u8]) -> io::Result<bool> + Send
412{
413}
414
415impl<T> PipeFilter for T where
416    T: 'static + FnMut(&[u8]) -> io::Result<bool> + Send
417{
418}
419
420/// A temporary wrapper for process limits.
421#[attr_alias::eval]
422#[must_use]
423pub trait Control: private::Sealed {
424    /// The type returned by [`wait`].
425    ///
426    /// [`wait`]: Self::wait
427    type Result;
428
429    /// Sets the total virtual memory limit for the process in bytes.
430    ///
431    /// If the process attempts to allocate memory in excess of this limit, the
432    /// allocation will fail. The type of failure will depend on the platform,
433    /// and the process might terminate if it cannot handle it.
434    ///
435    /// Small memory limits are safe, but they might prevent the operating
436    /// system from starting the process.
437    #[attr_alias(memory_limit, cfg(any(doc, *)))]
438    #[attr_alias(memory_limit, cfg_attr(process_control_docs_rs, doc(cfg(*))))]
439    #[must_use]
440    fn memory_limit(self, limit: usize) -> Self;
441
442    /// Sets the total time limit for the process in milliseconds.
443    ///
444    /// A process that exceeds this limit will not be terminated unless
445    /// [`terminate_for_timeout`] is called.
446    ///
447    /// [`terminate_for_timeout`]: Self::terminate_for_timeout
448    #[must_use]
449    fn time_limit(self, limit: Duration) -> Self;
450
451    /// Causes [`wait`] to never suppress an error.
452    ///
453    /// Typically, errors terminating the process will be ignored, as they are
454    /// often less important than the result. However, when this method is
455    /// called, those errors will be returned as well.
456    ///
457    /// [`wait`]: Self::wait
458    #[must_use]
459    fn strict_errors(self) -> Self;
460
461    /// Causes the process to be terminated if it exceeds the time limit.
462    ///
463    /// Process identifier reuse by the system will be mitigated. There should
464    /// never be a scenario that causes an unintended process to be terminated.
465    #[must_use]
466    fn terminate_for_timeout(self) -> Self;
467
468    /// Calls a filter function for each write to [stdout].
469    ///
470    /// For more information, see [`PipeFilter`].
471    ///
472    /// # Panics
473    ///
474    /// Panics if [`Command::stdout`] has not been set to [`Stdio::piped`].
475    ///
476    /// [`Command::stdout`]: ::std::process::Command::stdout
477    /// [`Stdio::piped`]: ::std::process::Stdio::piped
478    /// [stdout]: Output::stdout
479    #[must_use]
480    fn stdout_filter<T>(self, listener: T) -> Self
481    where
482        Self: Control<Result = Output>,
483        T: PipeFilter;
484
485    /// Calls a filter function for each write to [stderr].
486    ///
487    /// For more information, see [`PipeFilter`].
488    ///
489    /// # Panics
490    ///
491    /// Panics if [`Command::stderr`] has not been set to [`Stdio::piped`].
492    ///
493    /// [`Command::stderr`]: ::std::process::Command::stderr
494    /// [`Stdio::piped`]: ::std::process::Stdio::piped
495    /// [stderr]: Output::stderr
496    #[must_use]
497    fn stderr_filter<T>(self, listener: T) -> Self
498    where
499        Self: Control<Result = Output>,
500        T: PipeFilter;
501
502    /// Runs the process to completion, aborting if it exceeds the time limit.
503    ///
504    /// At least one additional thread might be created to wait on the process
505    /// without blocking the current thread.
506    ///
507    /// If the time limit is exceeded before the process finishes, `Ok(None)`
508    /// will be returned. However, the process will not be terminated in that
509    /// case unless [`terminate_for_timeout`] is called beforehand. It is
510    /// recommended to always call that method to allow system resources to be
511    /// freed.
512    ///
513    /// The stdin handle to the process, if it exists, will be closed before
514    /// waiting. Otherwise, the process would assuredly time out when reading
515    /// from that pipe.
516    ///
517    /// This method cannot guarantee that the same [`io::ErrorKind`] variants
518    /// will be returned in the future for the same types of failures. Allowing
519    /// these breakages is required to enable calling [`Child::kill`]
520    /// internally.
521    ///
522    /// [`terminate_for_timeout`]: Self::terminate_for_timeout
523    fn wait(self) -> WaitResult<Self::Result>;
524}
525
526/// Extensions to [`Child`] for easily terminating processes.
527///
528/// For more information, see [the module-level documentation][module].
529///
530/// [module]: self
531pub trait ChildExt: private::Sealed {
532    /// Creates an instance of [`Control`] that yields [`ExitStatus`] for this
533    /// process.
534    ///
535    /// This method parallels [`Child::wait`] but allows setting limits on the
536    /// process.
537    ///
538    /// # Examples
539    ///
540    /// ```
541    /// # use std::io;
542    /// use std::process::Command;
543    /// use std::time::Duration;
544    ///
545    /// use process_control::ChildExt;
546    /// use process_control::Control;
547    ///
548    /// let exit_status = Command::new("echo")
549    ///     .spawn()?
550    ///     .controlled()
551    ///     .time_limit(Duration::from_secs(1))
552    ///     .terminate_for_timeout()
553    ///     .wait()?
554    ///     .expect("process timed out");
555    /// assert!(exit_status.success());
556    /// #
557    /// # Ok::<_, io::Error>(())
558    /// ```
559    #[must_use]
560    fn controlled(&mut self) -> impl Control<Result = ExitStatus> + Debug;
561
562    /// Creates an instance of [`Control`] that yields [`Output`] for this
563    /// process.
564    ///
565    /// This method parallels [`Child::wait_with_output`] but allows setting
566    /// limits on the process.
567    ///
568    /// # Examples
569    ///
570    /// ```
571    /// # use std::io;
572    /// use std::process::Command;
573    /// use std::process::Stdio;
574    /// use std::time::Duration;
575    ///
576    /// use process_control::ChildExt;
577    /// use process_control::Control;
578    ///
579    /// let message = "foobar";
580    /// let output = Command::new("echo")
581    ///     .arg(message)
582    ///     .stdout(Stdio::piped())
583    ///     .spawn()?
584    ///     .controlled_with_output()
585    ///     .time_limit(Duration::from_secs(1))
586    ///     .terminate_for_timeout()
587    ///     .wait()?
588    ///     .expect("process timed out");
589    /// assert!(output.status.success());
590    /// assert_eq!(message.as_bytes(), &output.stdout[..message.len()]);
591    /// #
592    /// # Ok::<_, io::Error>(())
593    /// ```
594    #[must_use]
595    fn controlled_with_output(self) -> impl Control<Result = Output> + Debug;
596}
597
598impl ChildExt for Child {
599    #[inline]
600    fn controlled(&mut self) -> impl Control<Result = ExitStatus> + Debug {
601        control::Buffer::new(self)
602    }
603
604    #[inline]
605    fn controlled_with_output(self) -> impl Control<Result = Output> + Debug {
606        control::Buffer::new(self)
607    }
608}
609
610mod private {
611    use std::process::Child;
612
613    use super::control;
614
615    pub trait Sealed {}
616    impl Sealed for Child {}
617    impl<P> Sealed for control::Buffer<P> where P: control::Process {}
618}