powerpack_detach/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! Provides a way to background a process from an Alfred workflow.
//!
//! It does this by forking the process and closing the stdin/stdout/stderr file
//! descriptors for the child process. This makes sure Alfred does not block on
//! the process. Calling [`spawn`] does the following:
//!
//! - In the parent:
//!   - Returns immediately.
//! - In the child:
//!   - Detaches the stdin/stdout/stderr file descriptors.
//!   - Sets up a panic hook that logs an error on panic.
//!   - Executes the given function.
//!   - Exit the process.
//!
//! ### 💡 Note
//!
//! Depending on your Alfred workflow settings Alfred might execute your
//! workflow many times in a short space of time. It can be useful to make sure
//! only one child process is running at a time by first acquiring a file mutex
//! in the spawned function.
//!
//! # Examples
//!
//! ```no-compile
//! powerpack::detach::spawn(|| {
//!
//!     // some expensive operation that shouldn't block Alfred
//!     //
//!     // e.g. fetch and cache a remote resource
//!
//! }).expect("forked child process");
//! ```

use std::io;
use std::panic;
use std::process;

#[derive(Debug, Clone, Copy)]
enum Fork {
    Parent,
    Child,
}

/// Fork the current process.
fn fork() -> io::Result<Fork> {
    // SAFETY: We are handling the error correctly.
    let r = unsafe { libc::fork() };
    handle_err(r).map(|r| match r {
        0 => Fork::Child,
        _ => Fork::Parent,
    })
}

/// Close the standard file descriptors.
fn close_std_fds() -> io::Result<()> {
    // SAFETY: We are handling the error correctly.
    handle_err(unsafe { libc::close(libc::STDOUT_FILENO) })?;
    handle_err(unsafe { libc::close(libc::STDERR_FILENO) })?;
    handle_err(unsafe { libc::close(libc::STDIN_FILENO) })?;
    Ok(())
}

fn handle_err(res: i32) -> io::Result<i32> {
    match res {
        -1 => Err(io::Error::last_os_error()),
        r => Ok(r),
    }
}

#[allow(deprecated)]
fn panic_hook(info: &panic::PanicInfo<'_>) {
    let msg = match info.payload().downcast_ref::<&'static str>() {
        Some(s) => *s,
        None => match info.payload().downcast_ref::<String>() {
            Some(s) => &s[..],
            None => "Box<Any>",
        },
    };
    log::error!("child panicked at '{}', {}", msg, info.location().unwrap());
}

/// Execute a function in a child process.
///
/// See the [crate] level documentation for more.
pub fn spawn<F>(f: F) -> io::Result<()>
where
    F: FnOnce(),
{
    match fork()? {
        Fork::Parent => Ok(()),
        Fork::Child => match exec_child(f) {
            Ok(()) => {
                process::exit(0);
            }
            Err(err) => {
                log::error!("{:#}", err);
                process::exit(1);
            }
        },
    }
}

fn exec_child<F>(f: F) -> io::Result<()>
where
    F: FnOnce(),
{
    close_std_fds()?;
    panic::set_hook(Box::new(panic_hook));
    f();
    Ok(())
}