use crate::{context::Context, utils::spawn_command};
use fastcgi_client::{Client, Params, Request};
use libc::{SIGTERM, atexit, kill, pid_t};
use log::debug;
use std::{
borrow::Cow,
fs,
net::TcpListener,
path::Path,
process::Child,
sync::{Mutex, Once, OnceLock},
time::Duration,
};
use tempfile::NamedTempFile;
use tokio::{io, net::TcpStream};
static FPM_HANDLE: OnceLock<FpmHandle> = OnceLock::new();
pub struct FpmHandle {
fpm_child: Child,
fpm_conf_file: Mutex<Option<NamedTempFile>>,
port: u16,
}
impl FpmHandle {
fn find_available_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.expect("Failed to bind to an available port")
.local_addr()
.expect("Failed to get local address")
.port()
}
pub fn setup(lib_path: impl AsRef<Path>, log_path: impl AsRef<Path>) -> &'static FpmHandle {
if FPM_HANDLE.get().is_some() {
panic!("FPM_HANDLE has set");
}
let lib_path = lib_path.as_ref().to_owned();
let port = Self::find_available_port();
let context = Context::get_global();
let php_fpm = context.find_php_fpm().unwrap();
let fpm_conf_file = context.create_tmp_fpm_conf_file(port, log_path.as_ref());
let argv = [
&*php_fpm,
"-F",
"-n",
"-d",
&format!("extension={}", lib_path.display()),
"-y",
&fpm_conf_file.path().display().to_string(),
];
debug!(argv:% = argv.join(" "), port:% = port; "setup php-fpm");
let child = spawn_command(&argv, Some(Duration::from_secs(3)));
let log = fs::read_to_string(log_path.as_ref()).unwrap();
debug!(log:%; "php-fpm log");
let handle = FpmHandle {
fpm_child: child,
fpm_conf_file: Mutex::new(Some(fpm_conf_file)),
port,
};
static TEARDOWN: Once = Once::new();
TEARDOWN.call_once(|| unsafe {
atexit(teardown);
});
if FPM_HANDLE.set(handle).is_err() {
panic!("FPM_HANDLE has set");
}
FPM_HANDLE.get().unwrap()
}
pub async fn test_fpm_request(
&self, method: &str, root: impl AsRef<Path>, request_uri: &str,
content_type: Option<String>, body: Option<Vec<u8>>,
) {
let root = root.as_ref();
let script_name = request_uri.split('?').next().unwrap();
let mut tmp = root.to_path_buf();
tmp.push(script_name.trim_start_matches('/'));
let script_filename = tmp.as_path().to_str().unwrap();
let stream = TcpStream::connect(("127.0.0.1", self.port)).await.unwrap();
let local_addr = stream.local_addr().unwrap();
let peer_addr = stream.peer_addr().unwrap();
let local_ip = local_addr.ip().to_string();
let local_port = local_addr.port();
let peer_ip = peer_addr.ip().to_string();
let peer_port = peer_addr.port();
let client = Client::new(stream);
let mut params = Params::default()
.request_method(method)
.script_name(request_uri)
.script_filename(script_filename)
.request_uri(request_uri)
.document_uri(script_name)
.remote_addr(&local_ip)
.remote_port(local_port)
.server_addr(&peer_ip)
.server_port(peer_port)
.server_name("phper-test");
if let Some(content_type) = &content_type {
params = params.content_type(content_type);
}
if let Some(body) = &body {
params = params.content_length(body.len());
}
let response = if let Some(body) = body {
client
.execute_once(Request::new(params, body.as_ref()))
.await
} else {
client
.execute_once(Request::new(params, &mut io::empty()))
.await
};
let output = response.unwrap();
let stdout = output.stdout.unwrap_or_default();
let stderr = output.stderr.unwrap_or_default();
let no_error = stderr.is_empty();
let f = |out| {
let out = String::from_utf8_lossy(out);
if out.is_empty() {
Cow::Borrowed("<empty>")
} else {
out
}
};
debug!(uri:% = request_uri, stdout:% = f(&stdout), stderr:% = f(&stderr); "test php request");
assert!(no_error, "request not success: {}", request_uri);
}
}
extern "C" fn teardown() {
unsafe {
let fpm_handle = FPM_HANDLE.get().unwrap();
drop(fpm_handle.fpm_conf_file.lock().unwrap().take());
let id = fpm_handle.fpm_child.id();
kill(id as pid_t, SIGTERM);
}
}