powerpack_detach/
lib.rs

1//! Provides a way to background a process from an Alfred workflow.
2//!
3//! It does this by forking the process and closing the stdin/stdout/stderr file
4//! descriptors for the child process. This makes sure Alfred does not block on
5//! the process. Calling [`spawn`] does the following:
6//!
7//! - In the parent:
8//!   - Returns immediately.
9//! - In the child:
10//!   - Detaches the stdin/stdout/stderr file descriptors.
11//!   - Sets up a panic hook that logs an error on panic.
12//!   - Executes the given function.
13//!   - Exit the process.
14//!
15//! ### 💡 Note
16//!
17//! Depending on your Alfred workflow settings Alfred might execute your
18//! workflow many times in a short space of time. It can be useful to make sure
19//! only one child process is running at a time by first acquiring a file mutex
20//! in the spawned function.
21//!
22//! # Examples
23//!
24//! ```no-compile
25//! powerpack::detach::spawn(|| {
26//!
27//!     // some expensive operation that shouldn't block Alfred
28//!     //
29//!     // e.g. fetch and cache a remote resource
30//!
31//! }).expect("forked child process");
32//! ```
33
34use std::error::Error;
35use std::fmt::Write;
36use std::io;
37use std::panic;
38use std::process;
39
40#[derive(Debug, Clone, Copy)]
41enum Fork {
42    Parent,
43    Child,
44}
45
46/// Fork the current process.
47fn fork() -> io::Result<Fork> {
48    // SAFETY: We are handling the error correctly.
49    let r = unsafe { libc::fork() };
50    handle_err(r).map(|r| match r {
51        0 => Fork::Child,
52        _ => Fork::Parent,
53    })
54}
55
56/// Close the standard file descriptors.
57fn close_std_fds() -> io::Result<()> {
58    // SAFETY: We are handling the error correctly.
59    handle_err(unsafe { libc::close(libc::STDOUT_FILENO) })?;
60    handle_err(unsafe { libc::close(libc::STDERR_FILENO) })?;
61    handle_err(unsafe { libc::close(libc::STDIN_FILENO) })?;
62    Ok(())
63}
64
65fn handle_err(res: i32) -> io::Result<i32> {
66    match res {
67        -1 => Err(io::Error::last_os_error()),
68        r => Ok(r),
69    }
70}
71
72#[allow(deprecated)]
73fn panic_hook(info: &panic::PanicInfo<'_>) {
74    let msg = match info.payload().downcast_ref::<&'static str>() {
75        Some(s) => *s,
76        None => match info.payload().downcast_ref::<String>() {
77            Some(s) => &s[..],
78            None => "Box<Any>",
79        },
80    };
81    log::error!("child panicked at '{}', {}", msg, info.location().unwrap());
82}
83
84/// Execute a function in a child process.
85///
86/// See the [crate] level documentation for more.
87pub fn spawn<F>(f: F) -> io::Result<()>
88where
89    F: FnOnce(),
90{
91    match fork()? {
92        Fork::Parent => Ok(()),
93        Fork::Child => match exec_child(f) {
94            Ok(()) => {
95                process::exit(0);
96            }
97            Err(err) => {
98                log::error!("{}", format_err(&err));
99                process::exit(1);
100            }
101        },
102    }
103}
104
105fn exec_child<F>(f: F) -> io::Result<()>
106where
107    F: FnOnce(),
108{
109    close_std_fds()?;
110    panic::set_hook(Box::new(panic_hook));
111    f();
112    Ok(())
113}
114
115#[doc(hidden)]
116pub fn format_err(err: &(dyn Error + 'static)) -> String {
117    let mut out = err.to_string();
118    let mut tmp = String::new();
119    let mut source = err.source();
120    while let Some(err) = source {
121        match write!(&mut tmp, "{err}") {
122            Ok(_) => {
123                if !out.contains(&tmp) {
124                    out.push_str(": ");
125                    out.push_str(&tmp);
126                }
127            }
128            Err(_) => {
129                out.push_str(": <unknown>");
130            }
131        }
132        source = err.source();
133        tmp.clear();
134    }
135    out
136}