aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-07-20 19:28:45 +0900
committernsfisis <nsfisis@gmail.com>2026-07-20 19:29:04 +0900
commit77ff62b1e5333cd05b6c405feb2f42d0d8dcfab8 (patch)
tree5bc6d0372c23a7e0c655699a6dfe1d0954083178 /crates
parentdc0087845355222d1cbf2820289444db3eebc87e (diff)
downloadphp-shirabe-77ff62b1e5333cd05b6c405feb2f42d0d8dcfab8.tar.gz
php-shirabe-77ff62b1e5333cd05b6c405feb2f42d0d8dcfab8.tar.zst
php-shirabe-77ff62b1e5333cd05b6c405feb2f42d0d8dcfab8.zip
feat(php-rpc): attach worker process state to request I/O errors
A dead worker and a live one hitting a framing bug both surface as a raw socket I/O error (e.g. "Broken pipe"), which doesn't say whether the child crashed, was signaled, or is still running. Query the child's exit status via try_wait() and attach it as anyhow::Context so the panic message shows the root cause directly.
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs45
1 files changed, 39 insertions, 6 deletions
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index 9ba02629..d939829a 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -1,5 +1,6 @@
//! Rust-to-PHP RPC over a Unix domain socket. See `docs/dev/php-rpc.md`.
+use anyhow::Context as _;
use shirabe_external_packages::symfony::process::PhpExecutableFinder;
use shirabe_php_shim::PhpMixed;
use std::io::{Read as _, Write as _};
@@ -91,9 +92,11 @@ 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,
+ // Also queried for its exit status when a socket read/write fails, to tell a dead worker
+ // apart from a framing bug. Kept alive for the process lifetime along with 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,
}
@@ -102,8 +105,22 @@ impl Worker {
let mut payload = name.as_bytes().to_vec();
payload.push(0);
payload.extend_from_slice(arg.as_bytes());
- write_frame(&mut self.stream, &payload)?;
- Ok(read_frame(&mut self.stream)?)
+ write_frame(&mut self.stream, &payload).with_context(|| self.worker_state())?;
+ read_frame(&mut self.stream).with_context(|| self.worker_state())
+ }
+
+ /// Describes the PHP worker's current process state, to be attached as `anyhow::Context` to
+ /// an I/O error so a dead worker (crash, OOM kill, ...) can be told apart from a live one
+ /// hitting a framing bug.
+ fn worker_state(&mut self) -> String {
+ match self.child.try_wait() {
+ Ok(Some(status)) => format!("PHP worker process already exited: {status}"),
+ Ok(None) => format!(
+ "PHP worker process (pid {}) is still running",
+ self.child.id()
+ ),
+ Err(wait_err) => format!("failed to check PHP worker process status: {wait_err}"),
+ }
}
}
@@ -165,7 +182,7 @@ fn spawn_worker() -> anyhow::Result<Worker> {
Ok(Worker {
stream,
- _child: child,
+ child,
_tempdir: tempdir,
})
}
@@ -260,6 +277,22 @@ mod tests {
}
#[test]
+ fn request_error_reports_dead_worker_exit_status() {
+ let mut worker = spawn_worker().expect("failed to spawn PHP worker");
+ worker.child.kill().expect("failed to kill PHP worker");
+ worker.child.wait().expect("failed to reap PHP worker");
+
+ let err = worker
+ .request("defined", "PHP_VERSION")
+ .expect_err("request against a dead worker should fail");
+ let message = format!("{err:#}");
+ assert!(
+ message.contains("PHP worker process already exited"),
+ "unexpected error message: {message}"
+ );
+ }
+
+ #[test]
fn rejects_non_numeric_length() {
assert_eq!(parse_serialized_string(b"s:x:\"ab\";"), None);
}