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=pipe`, `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    let (task_master_fd, task_child_fd) = create_socketpair()?;
38    let (ctrl_master_fd, ctrl_child_fd) = create_socketpair()?;
39
40    let task_child_raw = task_child_fd.as_raw_fd();
41    let ctrl_child_raw = ctrl_child_fd.as_raw_fd();
42
43    let mut cmd = Command::new(php);
44    cmd.arg(script)
45        .env("FOLK_RUNTIME", "pipe")
46        .env("FOLK_TASK_FD", TASK_FD.to_string())
47        .env("FOLK_CONTROL_FD", CONTROL_FD.to_string())
48        .stdin(std::process::Stdio::null())
49        .stdout(std::process::Stdio::piped())
50        .stderr(std::process::Stdio::piped());
51
52    // SAFETY: we only call async-signal-safe libc functions and use only
53    // captured primitive integers (no heap allocation, no Rust drop).
54    unsafe {
55        cmd.pre_exec(move || {
56            if libc::dup2(task_child_raw, TASK_FD) < 0 {
57                return Err(std::io::Error::last_os_error());
58            }
59            if libc::dup2(ctrl_child_raw, CONTROL_FD) < 0 {
60                return Err(std::io::Error::last_os_error());
61            }
62
63            if task_child_raw != TASK_FD {
64                libc::close(task_child_raw);
65            }
66            if ctrl_child_raw != CONTROL_FD {
67                libc::close(ctrl_child_raw);
68            }
69
70            libc::fcntl(TASK_FD, libc::F_SETFD, 0);
71            libc::fcntl(CONTROL_FD, libc::F_SETFD, 0);
72
73            Ok(())
74        });
75    }
76
77    let child = cmd.spawn()?;
78
79    drop(task_child_fd);
80    drop(ctrl_child_fd);
81
82    let task_master = {
83        let std_sock =
84            unsafe { std::os::unix::net::UnixStream::from_raw_fd(task_master_fd.into_raw_fd()) };
85        std_sock.set_nonblocking(true)?;
86        UnixStream::from_std(std_sock)?
87    };
88
89    let control_master = {
90        let std_sock =
91            unsafe { std::os::unix::net::UnixStream::from_raw_fd(ctrl_master_fd.into_raw_fd()) };
92        std_sock.set_nonblocking(true)?;
93        UnixStream::from_std(std_sock)?
94    };
95
96    debug!(pid = ?child.id(), "worker spawned");
97
98    Ok(SpawnedWorker {
99        child,
100        task_master,
101        control_master,
102    })
103}