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}