Skip to main content

folk_runtime_pipe/
spawn.rs

1//! PHP worker process spawn with FD inheritance.
2
3use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd};
4
5use anyhow::Result;
6use tokio::net::UnixStream;
7use tokio::process::Command;
8use tracing::debug;
9
10use crate::socket::create_socketpair;
11
12/// File descriptor numbers in the child process.
13pub const TASK_FD: libc::c_int = 3;
14pub const CONTROL_FD: libc::c_int = 4;
15
16/// What we get back from a successful spawn.
17pub struct SpawnedWorker {
18    pub child: tokio::process::Child,
19    pub task_master: UnixStream,
20    pub control_master: UnixStream,
21}
22
23/// Spawn a PHP worker process.
24///
25/// Sets `FOLK_RUNTIME`, `FOLK_TASK_FD=3`, `FOLK_CONTROL_FD=4` in the
26/// environment. The child receives a connected Unix socket on each of FD 3
27/// and FD 4.
28///
29/// # Safety
30///
31/// Calls `pre_exec` which runs `libc::dup2` and `libc::close` in the
32/// child after fork but before exec. This is safe here because:
33/// - We only use async-signal-safe functions (`dup2`, `close`, `fcntl`).
34/// - We do not allocate or use Rust data structures in the callback.
35#[allow(unsafe_code)]
36pub fn spawn_worker(php: &str, script: &str) -> Result<SpawnedWorker> {
37    spawn_worker_with_runtime(php, script, "pipe")
38}
39
40/// Like [`spawn_worker`] but with a custom `FOLK_RUNTIME` value.
41#[allow(unsafe_code)]
42pub fn spawn_worker_with_runtime(php: &str, script: &str, runtime: &str) -> Result<SpawnedWorker> {
43    let (task_master_fd, task_child_fd) = create_socketpair()?;
44    let (ctrl_master_fd, ctrl_child_fd) = create_socketpair()?;
45
46    let task_child_raw = task_child_fd.as_raw_fd();
47    let ctrl_child_raw = ctrl_child_fd.as_raw_fd();
48
49    let mut cmd = Command::new(php);
50    cmd.arg(script)
51        .env("FOLK_RUNTIME", runtime)
52        .env("FOLK_TASK_FD", TASK_FD.to_string())
53        .env("FOLK_CONTROL_FD", CONTROL_FD.to_string())
54        .stdin(std::process::Stdio::null())
55        .stdout(std::process::Stdio::piped())
56        .stderr(std::process::Stdio::inherit());
57
58    // SAFETY: we only call async-signal-safe libc functions and use only
59    // captured primitive integers (no heap allocation, no Rust drop).
60    unsafe {
61        cmd.pre_exec(move || {
62            if libc::dup2(task_child_raw, TASK_FD) < 0 {
63                return Err(std::io::Error::last_os_error());
64            }
65            if libc::dup2(ctrl_child_raw, CONTROL_FD) < 0 {
66                return Err(std::io::Error::last_os_error());
67            }
68
69            if task_child_raw != TASK_FD {
70                libc::close(task_child_raw);
71            }
72            if ctrl_child_raw != CONTROL_FD {
73                libc::close(ctrl_child_raw);
74            }
75
76            libc::fcntl(TASK_FD, libc::F_SETFD, 0);
77            libc::fcntl(CONTROL_FD, libc::F_SETFD, 0);
78
79            Ok(())
80        });
81    }
82
83    let child = cmd.spawn()?;
84
85    drop(task_child_fd);
86    drop(ctrl_child_fd);
87
88    let task_master = {
89        let std_sock =
90            unsafe { std::os::unix::net::UnixStream::from_raw_fd(task_master_fd.into_raw_fd()) };
91        std_sock.set_nonblocking(true)?;
92        UnixStream::from_std(std_sock)?
93    };
94
95    let control_master = {
96        let std_sock =
97            unsafe { std::os::unix::net::UnixStream::from_raw_fd(ctrl_master_fd.into_raw_fd()) };
98        std_sock.set_nonblocking(true)?;
99        UnixStream::from_std(std_sock)?
100    };
101
102    debug!(pid = ?child.id(), "worker spawned");
103
104    Ok(SpawnedWorker {
105        child,
106        task_master,
107        control_master,
108    })
109}