calloop_subproc/
lib.rs

1//! Subprocess handling event source for the [Calloop event loop][calloop].
2//!
3//! [calloop]: https://crates.io/crates/calloop
4//!
5//! Calloop is built around the concept of *event sources* eg. timers, channels,
6//! file descriptors themselves, which generate events and call a callback for
7//! them. This crate provides two kinds of event sources that let you manage
8//! subprocess:
9//! - a [chain], which runs a series of commands and generates a success/failure
10//!   event
11//! - a [listener], which runs a single command and generates an event based on
12//!   lines of output
13//!
14//! [chain]: crate::SubprocChain
15//! [listener]: crate::SubprocListen
16//!
17//! # Error handling
18//!
19//! Errors in this crate are classified into two high-level kinds, as is the
20//! [custom for Calloop][calloop-errors].
21//!
22//! [calloop-errors]: https://smithay.github.io/calloop/ch02-06-errors.html
23//!
24//! [`LaunchError`] might be generated by an event source's [`process_events()`]
25//! method when a critical error is encountered. This generally means that the
26//! event source itself can't continue sensibly and should be removed from the
27//! event loop.
28//!
29//! The [`ErrorEvent`] type is concerned with errors of the subprocess it's
30//! managing, including the command not being found or requiring permissions
31//! that the process doesn't have. These will be contained in the type of event
32//! that is given to the callback.
33//!
34//! The main way that these are different is: if there's a problem using
35//! whatever underlying mechanism we use to spawn a subprocess or get results
36//! back from it, then a [`LaunchError`] is produced then and there. But if the
37//! underlying mechanics for setting up a subprocess succeed, and it's just that
38//! the command contains a typo and can't be found, then it's perfectly possible
39//! to generate an event for that. It will just happen to contain the [IO error]
40//! you'd expect when trying to run the command directly.
41//!
42//! [IO error]: std::io::Error
43//!
44//! # Releasing file descriptors
45//!
46//! To avoid "holding on" to file descriptors (and just the memory associated
47//! with the event source itself), you **must** honour the
48//! [`calloop::PostAction`] returned from [`process_events()`] and remove the
49//! event source if requested.
50//!
51//! This is automatically done for top level event sources ie. those added by
52//! [`insert_source()`]. If you use it as part of a composed event source, you
53//! must either manage reregistration yourself, or wrap the source with
54//! [`TransientSource`](calloop::transient::TransientSource).
55//!
56//! [`insert_source()`]: calloop::LoopHandle::insert_source()
57//!
58//! # Example
59//!
60//! This runs `ls -a` and prints the events received.
61//!
62//! ```
63//! use calloop::{EventLoop, LoopSignal};
64//! use calloop_subproc::{Command, SubprocListen};
65//!
66//! let ls_cmd = Command::new("ls").with_args(["-a"]);
67//! let listener = SubprocListen::new(ls_cmd).unwrap();
68//!
69//! let mut event_loop: EventLoop<LoopSignal> = EventLoop::try_new().unwrap();
70//!
71//! event_loop
72//!     .handle()
73//!     .insert_source(listener, |event, _, stopper| {
74//!         // What kind of event did we get this time?
75//!         let msg = match event {
76//!             // The subprocess was just started.
77//!             calloop_subproc::ListenEvent::Start => "Subprocess started".to_owned(),
78//!             // We got a line of output from the subprocess.
79//!             calloop_subproc::ListenEvent::Line(line) => format!("Output: {}", line),
80//!             // The subprocess ended.
81//!             calloop_subproc::ListenEvent::End(res) => {
82//!                 // Since the subprocess ended, we want to stop the loop.
83//!                 stopper.stop();
84//!
85//!                 // Show why the subprocess ended.
86//!                 match res {
87//!                     Ok(()) => "Subprocess completed".to_owned(),
88//!                     Err(error) => format!("Subprocess error: {:?}", error),
89//!                 }
90//!             }
91//!         };
92//!
93//!         // Print our formatted event.
94//!         println!("{}", msg);
95//!
96//!         // This callback must return true if the subprocess should be
97//!         // killed. We want it to run to completion, so we return false.
98//!         false
99//!     })
100//!     .unwrap();
101//!
102//! event_loop
103//!     .run(None, &mut event_loop.get_signal(), |_| {})
104//!     .unwrap();
105//! ```
106//!
107//! [`process_events()`]: calloop::EventSource::process_events
108
109#![forbid(unsafe_code)]
110
111mod chain;
112mod listen;
113
114pub use chain::SubprocChain;
115pub use listen::{ListenEvent, SubprocListen};
116use log::*;
117use std::ffi::OsString;
118use std::fmt;
119
120/// This is the error type that our event source's callback might receive. If
121/// something went wrong, it could be for one of two reasons:
122///   - the subprocess returned a non-zero status: `SubprocError(status)`
123///   - there was an IO error handling the subprocess: `IoError(ioerror)`
124#[derive(thiserror::Error, Debug)]
125pub enum ErrorEvent {
126    #[error("subprocess returned non-zero status: {0}")]
127    SubprocError(async_process::ExitStatus),
128
129    #[error("IO error managing subprocess")]
130    IoError(#[from] std::io::Error),
131}
132
133/// This is the error type that might be returned from the methods on the event
134/// source itself. In general they are more "critical" than [`ErrorEvent`] and
135/// mean that the event source is unable to continue.
136#[derive(thiserror::Error, Debug)]
137#[error("error processing subprocess events")]
138pub enum LaunchError {
139    CalloopExecutorError(#[from] calloop::futures::ExecutorError),
140    CalloopChannelError(#[from] calloop::channel::ChannelError),
141    IoError(#[from] std::io::Error),
142}
143
144type SrcResult = core::result::Result<(), ErrorEvent>;
145
146/// There's a lot of common manipulation of what we think of as a command. This
147/// type factors out some of that, specifically:
148/// - conversion from an arbitrary sequence of strings
149/// - conversion to async_process's builder type
150/// - debugging representation
151#[derive(Clone)]
152pub struct Command {
153    /// The actual command to run.
154    command: OsString,
155    /// Optional arguments to the command.
156    args: Vec<OsString>,
157}
158
159impl Command {
160    /// Constructs a new `Command` from a program or command name.
161    pub fn new<T: Into<OsString>>(command: T) -> Self {
162        Self {
163            command: command.into(),
164            args: Vec::new(),
165        }
166    }
167
168    /// Adds a sequence of arguments to the command. The trait bound for `T` is:
169    ///
170    /// ```none,actually-rust-but-see-https://github.com/rust-lang/rust/issues/63193
171    /// T where
172    ///     T: IntoIterator,
173    ///     T::Item: Into<OsString>
174    /// ```
175    ///
176    /// ...and can be understood as:
177    /// - `T` is anything that can turn into an iterator
178    /// - the iterator items are anything that can turn into an `OsString`
179    ///
180    /// Commonly this might be a `Vec<String>` or just `[&str]`.
181    pub fn with_args<T>(self, args: T) -> Self
182    where
183        T: IntoIterator,
184        T::Item: Into<OsString>,
185    {
186        Self {
187            args: args.into_iter().map(Into::into).collect(),
188            ..self
189        }
190    }
191}
192
193impl From<Command> for async_process::Command {
194    /// Converts our `Command` type to an `async_process::Command`.
195    fn from(command: Command) -> Self {
196        let mut async_command = async_process::Command::new(command.command);
197        async_command.args(command.args);
198        async_command
199    }
200}
201
202impl fmt::Debug for Command {
203    /// Formats the command as it would appear on the command line. No fancy
204    /// escaping or anything is done, this is purely for reading in the logs.
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        f.write_fmt(format_args!(
207            "{} {}",
208            self.command.to_string_lossy(),
209            self.args
210                .iter()
211                .map(|s| s.to_string_lossy())
212                .collect::<Vec<_>>()
213                .join(" ")
214        ))
215    }
216}
217
218/// Running a command can result in either `Ok(())` or the error outcomes
219/// described above (see `SrcResult`). The `command` is actually just a
220/// `String`: the debug representation of the `Command`. This exists just so we
221/// can log what command failed and why.
222#[derive(Debug)]
223struct CommandResult {
224    command: String,
225    result: SrcResult,
226}
227
228#[cfg(test)]
229mod test {
230    use std::str::FromStr;
231
232    use super::*;
233
234    #[test]
235    fn test_cmd() {
236        let command = Command::new("/bin/program").with_args(["one", "B 2", "3"]);
237
238        assert_eq!(command.command, OsString::from_str("/bin/program").unwrap());
239        assert_eq!(
240            command.args,
241            vec![
242                OsString::from_str("one").unwrap(),
243                OsString::from_str("B 2").unwrap(),
244                OsString::from_str("3").unwrap(),
245            ]
246        );
247    }
248}