aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-rpc/Cargo.toml11
-rw-r--r--crates/shirabe-php-rpc/php/worker.php37
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs109
-rw-r--r--crates/shirabe/Cargo.toml1
-rw-r--r--crates/shirabe/src/console/application.rs18
5 files changed, 167 insertions, 9 deletions
diff --git a/crates/shirabe-php-rpc/Cargo.toml b/crates/shirabe-php-rpc/Cargo.toml
new file mode 100644
index 0000000..692dda9
--- /dev/null
+++ b/crates/shirabe-php-rpc/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "shirabe-php-rpc"
+version.workspace = true
+edition.workspace = true
+
+[dependencies]
+shirabe-external-packages.workspace = true
+tempfile.workspace = true
+
+[lints]
+workspace = true
diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php
new file mode 100644
index 0000000..214817a
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/worker.php
@@ -0,0 +1,37 @@
+<?php
+
+// PHP glue worker. See docs/dev/php-rpc.md.
+
+$client = @stream_socket_client('unix://' . $argv[1], $errno, $errstr);
+if ($client === false) {
+ exit(1);
+}
+$dispatch = [
+ 'get_php_version' => static fn() => PHP_VERSION,
+ 'get_php_binary' => static fn() => PHP_BINARY,
+];
+$read_exact = static function ($conn, int $len): ?string {
+ $buf = '';
+ while (strlen($buf) < $len) {
+ $chunk = fread($conn, $len - strlen($buf));
+ if ($chunk === false || $chunk === '') {
+ return null;
+ }
+ $buf .= $chunk;
+ }
+ return $buf;
+};
+while (true) {
+ $header = $read_exact($client, 8);
+ if ($header === null) {
+ break;
+ }
+ $len = unpack('P', $header)[1];
+ $name = $len === 0 ? '' : $read_exact($client, $len);
+ if ($name === null) {
+ break;
+ }
+ $result = isset($dispatch[$name]) ? ($dispatch[$name])() : null;
+ $payload = serialize($result);
+ fwrite($client, pack('P', strlen($payload)) . $payload);
+}
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<String> {
+ write_frame(&mut self.stream, name.as_bytes()).ok()?;
+ let payload = read_frame(&mut self.stream).ok()?;
+ parse_serialized_string(&payload)
+ }
+}
+
+static WORKER: LazyLock<Mutex<Option<Worker>>> = LazyLock::new(|| Mutex::new(spawn_worker()));
+
+fn call(name: &str) -> Option<String> {
+ let mut guard = WORKER.lock().ok()?;
+ guard.as_mut()?.request(name)
+}
+
+fn spawn_worker() -> Option<Worker> {
+ 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<Vec<u8>> {
+ 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:<len>:"<bytes>";` form; only strings are needed here, so other forms are rejected.
+fn parse_serialized_string(payload: &[u8]) -> Option<String> {
+ 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())
+}
diff --git a/crates/shirabe/Cargo.toml b/crates/shirabe/Cargo.toml
index 632b659..7ec9db8 100644
--- a/crates/shirabe/Cargo.toml
+++ b/crates/shirabe/Cargo.toml
@@ -7,6 +7,7 @@ edition.workspace = true
shirabe-class-map-generator.workspace = true
shirabe-external-packages.workspace = true
shirabe-metadata-minifier.workspace = true
+shirabe-php-rpc.workspace = true
shirabe-php-shim.workspace = true
shirabe-semver.workspace = true
shirabe-spdx-licenses.workspace = true
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index 077ea0a..a3b4df7 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -93,14 +93,13 @@ use shirabe_external_packages::symfony::console::style::symfony_style::SymfonySt
use shirabe_external_packages::symfony::console::terminal::Terminal;
use shirabe_external_packages::symfony::process::exception::ProcessTimedOutException;
use shirabe_php_shim::{
- LogicException as ShimLogicException, PHP_BINARY, PHP_VERSION, PHP_VERSION_ID, PhpMixed,
- RuntimeException, bin2hex, chdir, date_default_timezone_get, date_default_timezone_set,
- defined, dirname, disk_free_space, extension_loaded, file_exists, file_get_contents,
- file_put_contents, function_exists, getcwd, getmypid, glob, in_array, ini_set, is_array,
- is_dir, is_file, is_string, is_subclass_of, json_decode, memory_get_peak_usage,
- memory_get_usage, microtime, php_uname, posix_getuid, random_bytes, realpath,
- restore_error_handler, round, str_contains, str_replace, strpos, strtoupper, sys_get_temp_dir,
- time, unlink,
+ LogicException as ShimLogicException, PHP_VERSION, PHP_VERSION_ID, PhpMixed, RuntimeException,
+ bin2hex, chdir, date_default_timezone_get, date_default_timezone_set, defined, dirname,
+ disk_free_space, extension_loaded, file_exists, file_get_contents, file_put_contents,
+ function_exists, getcwd, getmypid, glob, in_array, ini_set, is_array, is_dir, is_file,
+ is_string, is_subclass_of, json_decode, memory_get_peak_usage, memory_get_usage, microtime,
+ php_uname, posix_getuid, random_bytes, realpath, restore_error_handler, round, str_contains,
+ str_replace, strpos, strtoupper, sys_get_temp_dir, time, unlink,
};
/// The PHP `Composer\Console\Application` and `Symfony\Component\Console\Application` are
@@ -2492,7 +2491,8 @@ impl ApplicationHandle {
{
io.write_error(&format!(
"<info>PHP</info> version <comment>{}</comment> ({})",
- PHP_VERSION, PHP_BINARY,
+ shirabe_php_rpc::get_php_version(),
+ shirabe_php_rpc::get_php_binary(),
));
io.write_error(
"Run the \"diagnose\" command to get more detailed diagnostics output.",