aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-10 00:57:19 +0900
committernsfisis <nsfisis@gmail.com>2026-08-10 02:35:55 +0900
commitbbf33b836026cbe9fb9e7c76291dcb133b7144e2 (patch)
tree8b0437c321467bb8151cacb17c2414874f342443 /crates/shirabe-php-rpc/src
parent177edc22cac6475db85cba9e0d19e6440aef64bb (diff)
downloadphp-shirabe-bbf33b836026cbe9fb9e7c76291dcb133b7144e2.tar.gz
php-shirabe-bbf33b836026cbe9fb9e7c76291dcb133b7144e2.tar.zst
php-shirabe-bbf33b836026cbe9fb9e7c76291dcb133b7144e2.zip
feat(xdebug): switch Xdebug off in the PHP worker
Composer restarts itself with Xdebug unloaded because Xdebug makes PHP several times slower. Xdebug is never loaded into this process, so what needs dealing with is the PHP worker: it is spawned with `-d xdebug.mode=off` and `XDEBUG_MODE=off` (Xdebug reads the environment variable first and lets it override every ini setting), which makes its module init return before it installs any executor, compile, error or opcode hook. Rewriting the ini files and re-executing, the way xdebug-handler does, would additionally cover Xdebug 2, which hooks unconditionally and has no equivalent setting. That is not worth its machinery here: Xdebug 2 caps out at PHP 7.4, while every PHP version Composer supports can run Xdebug 3. What remains of XdebugHandler is small enough to live beside the worker it governs, so its crate is gone and its callers inline it. isXdebugActive answers false without asking PHP whenever the worker is switched off, so a command that needs no PHP does not spawn one just for the Xdebug warning; diagnose reports what the worker measures instead, which still surfaces an Xdebug that ignores the setting. PlatformRepository has no unloaded extension to restore, since switching the mode off leaves it loaded. COMPOSER_ORIGINAL_INIS is neither written nor read: it exists so a restarted process can name the ini files it replaced, and IniHelper can report the worker's own. IniHelperTest injects through that variable, so none of its cases are ported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-rpc/src')
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs62
-rw-r--r--crates/shirabe-php-rpc/src/xdebug.rs35
2 files changed, 93 insertions, 4 deletions
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index a4742664..1b165cfa 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -3,6 +3,7 @@
pub mod frame;
pub mod session;
pub mod value;
+pub mod xdebug;
pub use value::{PhpClassHandle, PhpObjHandle, PhpObject, PluginValue, RustObjHandle};
@@ -536,14 +537,23 @@ pub fn phpversion(extension: &str) -> Option<String> {
}
}
-/// `Composer\XdebugHandler\XdebugHandler::getAllIniFiles()` (minus the `self::$name` branch,
-/// which is unreachable since this port never constructs an XdebugHandler): `[(string)
+/// What `Composer\XdebugHandler\XdebugHandler::getAllIniFiles()` measures: `[(string)
/// php_ini_loaded_file()]` merged with the trimmed, comma-split `php_ini_scanned_files()` list
-/// when scanning is active.
+/// when scanning is active. The worker runs on the ini files of the machine, so these are the
+/// user's own.
pub fn get_all_ini_files() -> Vec<String> {
string_list(call("get_all_ini_files", ""), "get_all_ini_files")
}
+/// `Composer\XdebugHandler\XdebugHandler::isXdebugActive()` as measured in the worker, which is
+/// the process Xdebug is loaded into.
+pub(crate) fn xdebug_active() -> bool {
+ match call("xdebug_active", "") {
+ PhpMixed::Bool(b) => b,
+ other => panic!("PHP RPC: `xdebug_active` did not return a bool: {other:?}"),
+ }
+}
+
fn string_list(value: PhpMixed, name: &str) -> Vec<String> {
match value {
PhpMixed::List(items) => items
@@ -1070,7 +1080,16 @@ fn spawn_worker() -> anyhow::Result<Worker> {
// supported) serialize_precision; pin the child to it in case a distro php.ini overrides
// the default.
.arg("-d")
- .arg("serialize_precision=-1")
+ .arg("serialize_precision=-1");
+ if xdebug::switches_xdebug_off() {
+ // The environment variable takes precedence over every ini setting, so switching the
+ // mode off takes both. See `docs/dev/xdebug.md`.
+ command
+ .arg("-d")
+ .arg("xdebug.mode=off")
+ .env("XDEBUG_MODE", "off");
+ }
+ command
.arg(&script_path)
.arg(WORKER_SOCKET_FD.to_string())
.arg(&stubs_dir);
@@ -1147,6 +1166,41 @@ mod tests {
}
#[test]
+ fn worker_starts_with_xdebug_switched_off() {
+ if PhpExecutableFinder::new().find(false).is_none() {
+ // No PHP in this environment; the worker cannot start.
+ return;
+ }
+
+ let mut worker = spawn_worker().expect("failed to spawn PHP worker");
+
+ // The `-d xdebug.mode=off` half is invisible from PHP while the extension is not loaded
+ // (an unregistered ini entry is not readable), so only the environment half — the one
+ // that overrides every ini setting — can be asserted here.
+ frame::write_frame(
+ &mut worker.stream,
+ &Frame::CallFunction {
+ corr_id: 1,
+ function_name: "getenv".to_string(),
+ args: vec![PluginValue::string("XDEBUG_MODE")],
+ out_param_positions: Vec::new(),
+ },
+ )
+ .expect("failed to ask the worker for its Xdebug mode");
+ let reply = frame::read_frame(&mut worker.stream).expect("failed to read the worker reply");
+ match reply {
+ Frame::Return { value, .. } => assert_eq!(
+ value.to_php_mixed().expect("unusable reply"),
+ PhpMixed::String("off".to_string())
+ ),
+ other => panic!("unexpected reply: {other:?}"),
+ }
+
+ worker.child.kill().expect("failed to kill PHP worker");
+ worker.child.wait().expect("failed to reap PHP worker");
+ }
+
+ #[test]
fn queries_string_lists_when_php_available() {
if PhpExecutableFinder::new().find(false).is_none() {
// No PHP in this environment; the worker cannot start.
diff --git a/crates/shirabe-php-rpc/src/xdebug.rs b/crates/shirabe-php-rpc/src/xdebug.rs
new file mode 100644
index 00000000..6ed048cf
--- /dev/null
+++ b/crates/shirabe-php-rpc/src/xdebug.rs
@@ -0,0 +1,35 @@
+//! ref: composer/vendor/composer/xdebug-handler/src/XdebugHandler.php
+//!
+//! Keeping Xdebug out of the PHP worker, which is where Composer's restart of itself lands in
+//! this port. See `docs/dev/xdebug.md`.
+
+use shirabe_php_shim::getenv;
+
+/// `XdebugHandler::$name.XdebugHandler::SUFFIX_ALLOW`, where the name is the uppercased prefix of
+/// the one construction Composer makes: `new XdebugHandler('Composer')` in `bin/composer`.
+const ALLOW: &str = "COMPOSER_ALLOW_XDEBUG";
+
+/// PHP: `XdebugHandler::isXdebugActive()`. Whether Xdebug is loaded and running in an active mode.
+///
+/// Answered without asking PHP whenever the worker is started with the mode switched off, since
+/// that settles the question for every Xdebug that honours the setting — Xdebug 2, which has no
+/// such setting, is reported inactive while it is not. `diagnose` reports what the worker measures
+/// instead.
+pub fn is_xdebug_active() -> bool {
+ if switches_xdebug_off() {
+ return false;
+ }
+
+ crate::xdebug_active()
+}
+
+/// Whether the worker is started with Xdebug switched off, which is this port's stand-in for
+/// `XdebugHandler::check()` restarting the process. The answer is a property of the environment
+/// alone, so it holds whether or not the worker has been spawned yet.
+pub(crate) fn switches_xdebug_off() -> bool {
+ // PHP: `!((bool) explode('|', getenv($this->envAllowXdebug))[0])`, where the pipe-separated
+ // form is the handoff to a process xdebug-handler restarted. Nothing restarts here, so what
+ // is left is PHP's truthiness of the value.
+ let allow_xdebug = getenv(ALLOW).unwrap_or_default();
+ matches!(allow_xdebug.to_string_lossy().as_ref(), "" | "0")
+}