From 1de6ba5c6735e42b5f462c0b7489bcf172ccdc08 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Wed, 1 Jul 2026 01:33:38 +0900 Subject: feat(php-rpc): query real PHP version and binary for --version Add a minimal shirabe-php-rpc crate that spawns the system PHP as a child process and asks it for runtime information over a Unix domain socket, then use it to fill the `--version` PHP line with the real \PHP_VERSION and \PHP_BINARY instead of fixed placeholder values. See docs/dev/php-rpc.md for the design and scope. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/shirabe-php-rpc/src/lib.rs | 109 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 crates/shirabe-php-rpc/src/lib.rs (limited to 'crates/shirabe-php-rpc/src/lib.rs') diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs new file mode 100644 index 0000000..f08a79a --- /dev/null +++ b/crates/shirabe-php-rpc/src/lib.rs @@ -0,0 +1,109 @@ +//! Rust-to-PHP RPC over a Unix domain socket. See `docs/dev/php-rpc.md`. + +use shirabe_external_packages::symfony::process::PhpExecutableFinder; +use std::io::{Read, Write}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::sync::{LazyLock, Mutex}; +use std::time::{Duration, Instant}; + +/// PHP `\PHP_VERSION`. +pub fn get_php_version() -> String { + call("get_php_version").unwrap_or_default() +} + +/// PHP `\PHP_BINARY`. +pub fn get_php_binary() -> String { + call("get_php_binary").unwrap_or_default() +} + +const GLUE_SCRIPT: &str = include_str!("../php/worker.php"); + +struct Worker { + stream: UnixStream, + // Kept alive for the process lifetime: the child interpreter and the temp dir holding the socket + // and glue script. Neither is dropped because the worker lives in a never-dropped static. + _child: std::process::Child, + _tempdir: tempfile::TempDir, +} + +impl Worker { + fn request(&mut self, name: &str) -> Option { + write_frame(&mut self.stream, name.as_bytes()).ok()?; + let payload = read_frame(&mut self.stream).ok()?; + parse_serialized_string(&payload) + } +} + +static WORKER: LazyLock>> = LazyLock::new(|| Mutex::new(spawn_worker())); + +fn call(name: &str) -> Option { + let mut guard = WORKER.lock().ok()?; + guard.as_mut()?.request(name) +} + +fn spawn_worker() -> Option { + let php = PhpExecutableFinder::new().find(false)?; + + let tempdir = tempfile::tempdir().ok()?; + let socket_path = tempdir.path().join("rpc.sock"); + let script_path = tempdir.path().join("worker.php"); + std::fs::write(&script_path, GLUE_SCRIPT).ok()?; + + // Bind before spawning so the socket exists when the child connects. + let listener = UnixListener::bind(&socket_path).ok()?; + listener.set_nonblocking(true).ok()?; + + let child = std::process::Command::new(&php) + .arg(&script_path) + .arg(&socket_path) + .spawn() + .ok()?; + + // Poll for the child's connection with a bounded deadline so a child that never connects does + // not hang the caller. + let deadline = Instant::now() + Duration::from_secs(10); + let stream = loop { + match listener.accept() { + Ok((stream, _)) => break stream, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + if Instant::now() >= deadline { + return None; + } + std::thread::sleep(Duration::from_millis(5)); + } + Err(_) => return None, + } + }; + stream.set_nonblocking(false).ok()?; + + Some(Worker { + stream, + _child: child, + _tempdir: tempdir, + }) +} + +fn write_frame(stream: &mut UnixStream, payload: &[u8]) -> std::io::Result<()> { + stream.write_all(&(payload.len() as u64).to_le_bytes())?; + stream.write_all(payload)?; + stream.flush() +} + +fn read_frame(stream: &mut UnixStream) -> std::io::Result> { + let mut header = [0u8; 8]; + stream.read_exact(&mut header)?; + let len = u64::from_le_bytes(header) as usize; + let mut payload = vec![0u8; len]; + stream.read_exact(&mut payload)?; + Ok(payload) +} + +/// Parse the `s::"";` form; only strings are needed here, so other forms are rejected. +fn parse_serialized_string(payload: &[u8]) -> Option { + let rest = payload.strip_prefix(b"s:")?; + let colon = rest.iter().position(|&b| b == b':')?; + let len: usize = std::str::from_utf8(&rest[..colon]).ok()?.parse().ok()?; + let after = rest.get(colon + 1..)?; + let bytes = after.strip_prefix(b"\"")?.get(..len)?; + Some(String::from_utf8_lossy(bytes).into_owned()) +} -- cgit v1.3.1