diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-10 00:57:19 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-10 02:35:55 +0900 |
| commit | bbf33b836026cbe9fb9e7c76291dcb133b7144e2 (patch) | |
| tree | 8b0437c321467bb8151cacb17c2414874f342443 | |
| parent | 177edc22cac6475db85cba9e0d19e6440aef64bb (diff) | |
| download | php-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>
| -rw-r--r-- | Cargo.lock | 5 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | LICENSE.md | 2 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/LICENSE (renamed from crates/shirabe-xdebug-handler/LICENSE) | 0 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/php/worker.php | 5 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/src/lib.rs | 62 | ||||
| -rw-r--r-- | crates/shirabe-php-rpc/src/xdebug.rs | 35 | ||||
| -rw-r--r-- | crates/shirabe-xdebug-handler/Cargo.toml | 11 | ||||
| -rw-r--r-- | crates/shirabe-xdebug-handler/src/lib.rs | 3 | ||||
| -rw-r--r-- | crates/shirabe-xdebug-handler/src/xdebug_handler.rs | 32 | ||||
| -rw-r--r-- | crates/shirabe/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/shirabe/src/command/diagnose_command.rs | 3 | ||||
| -rw-r--r-- | crates/shirabe/src/console/application.rs | 3 | ||||
| -rw-r--r-- | crates/shirabe/src/main.rs | 10 | ||||
| -rw-r--r-- | crates/shirabe/src/repository/platform_repository.rs | 20 | ||||
| -rw-r--r-- | crates/shirabe/src/util/ini_helper.rs | 8 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/ini_helper_test.rs | 109 | ||||
| -rw-r--r-- | docs/dev/php-rpc.md | 3 | ||||
| -rw-r--r-- | docs/dev/xdebug.md | 28 | ||||
| -rw-r--r-- | docs/known-incompatibilities.md | 7 |
20 files changed, 155 insertions, 193 deletions
@@ -2110,7 +2110,6 @@ dependencies = [ "shirabe-symfony-filesystem", "shirabe-symfony-finder", "shirabe-symfony-process", - "shirabe-xdebug-handler", "tempfile", "tokio", "tracing", @@ -2273,10 +2272,6 @@ dependencies = [ ] [[package]] -name = "shirabe-xdebug-handler" -version = "0.0.1" - -[[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -27,7 +27,6 @@ shirabe-symfony-filesystem = { path = "crates/shirabe-symfony-filesystem" } shirabe-symfony-finder = { path = "crates/shirabe-symfony-finder" } shirabe-symfony-process = { path = "crates/shirabe-symfony-process" } shirabe-symfony-string = { path = "crates/shirabe-symfony-string" } -shirabe-xdebug-handler = { path = "crates/shirabe-xdebug-handler" } anyhow = "1.0.102" async-trait = "0.1.89" base64 = "0.22.1" @@ -31,6 +31,7 @@ license of the package it is ported from: | [`shirabe-class-map-generator`](crates/shirabe-class-map-generator/LICENSE) | composer/class-map-generator | | [`shirabe-metadata-minifier`](crates/shirabe-metadata-minifier/LICENSE) | composer/metadata-minifier | | [`shirabe-pcre`](crates/shirabe-pcre/LICENSE) | composer/pcre | +| [`shirabe-php-rpc`](crates/shirabe-php-rpc/LICENSE) | composer/xdebug-handler | | [`shirabe-seld-json-lint`](crates/shirabe-seld-json-lint/LICENSE) | seld/jsonlint | | [`shirabe-seld-signal`](crates/shirabe-seld-signal/LICENSE) | seld/signal-handler | | [`shirabe-semver`](crates/shirabe-semver/LICENSE) | composer/semver | @@ -40,7 +41,6 @@ license of the package it is ported from: | [`shirabe-symfony-finder`](crates/shirabe-symfony-finder/LICENSE) | symfony/finder | | [`shirabe-symfony-process`](crates/shirabe-symfony-process/LICENSE) | symfony/process | | [`shirabe-symfony-string`](crates/shirabe-symfony-string/LICENSE) | symfony/string | -| [`shirabe-xdebug-handler`](crates/shirabe-xdebug-handler/LICENSE) | composer/xdebug-handler | ## PHP diff --git a/crates/shirabe-xdebug-handler/LICENSE b/crates/shirabe-php-rpc/LICENSE index 963618a1..963618a1 100644 --- a/crates/shirabe-xdebug-handler/LICENSE +++ b/crates/shirabe-php-rpc/LICENSE diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php index 578c30d0..5946a52f 100644 --- a/crates/shirabe-php-rpc/php/worker.php +++ b/crates/shirabe-php-rpc/php/worker.php @@ -526,8 +526,8 @@ final class ShirabePlatformRuntime } } -// Port of Composer\XdebugHandler\XdebugHandler::setXdebugDetails(), which the diagnose payload -// reports as `xdebug_active`. +// Port of Composer\XdebugHandler\XdebugHandler::setXdebugDetails(), which the `xdebug_active` +// query and the diagnose payload both report. $xdebug_active = static function (): bool { if (!extension_loaded('xdebug')) { return false; @@ -562,6 +562,7 @@ $xdebug_active = static function (): bool { ShirabeRpcRuntime::$dispatch = [ 'constant' => static fn($args) => defined($args[0]) ? constant($args[0]) : null, + 'xdebug_active' => static fn($args) => $xdebug_active(), 'get_all_ini_files' => static function ($args) { $paths = [(string) php_ini_loaded_file()]; $scanned = php_ini_scanned_files(); 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") +} diff --git a/crates/shirabe-xdebug-handler/Cargo.toml b/crates/shirabe-xdebug-handler/Cargo.toml deleted file mode 100644 index 3deda4ac..00000000 --- a/crates/shirabe-xdebug-handler/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "shirabe-xdebug-handler" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -description = "A Rust port of composer/xdebug-handler" -repository.workspace = true -license.workspace = true - -[lints] -workspace = true diff --git a/crates/shirabe-xdebug-handler/src/lib.rs b/crates/shirabe-xdebug-handler/src/lib.rs deleted file mode 100644 index c95f237d..00000000 --- a/crates/shirabe-xdebug-handler/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod xdebug_handler; - -pub use xdebug_handler::*; diff --git a/crates/shirabe-xdebug-handler/src/xdebug_handler.rs b/crates/shirabe-xdebug-handler/src/xdebug_handler.rs deleted file mode 100644 index 6ccedc6a..00000000 --- a/crates/shirabe-xdebug-handler/src/xdebug_handler.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! ref: composer/vendor/composer/xdebug-handler/src/XdebugHandler.php - -#[derive(Debug)] -pub struct XdebugHandler; - -impl XdebugHandler { - pub fn is_xdebug_active() -> bool { - // TODO(php-runtime) - false - } - - pub fn get_skipped_version() -> Option<String> { - // TODO(php-runtime) - // The restart-to-disable-xdebug mechanism is not ported (`is_xdebug_active` is - // hardcoded `false`), so a restart never happens and `self::$skipped` stays at - // its PHP default of `""`. - Some(String::new()) - } - - pub fn get_all_ini_files() -> Vec<String> { - // TODO(php-runtime) - // No XdebugHandler is ever constructed (`self::$name` stays null), because the - // `new XdebugHandler('Composer'); $xdebug->check();` bootstrap in `bin/composer` is not - // ported (see the TODO(phase-c) at the top of shirabe's main.rs), so the - // COMPOSER_ORIGINAL_INIS env-var branch is unreachable here. - // - // Callers that need the real PHP runtime's ini files (php_ini_loaded_file() / - // php_ini_scanned_files()) query shirabe_php_rpc directly instead of going through this - // stub; see IniHelper::get_all in the shirabe crate. - vec![String::new()] - } -} diff --git a/crates/shirabe/Cargo.toml b/crates/shirabe/Cargo.toml index ccc2ca8a..6d29c886 100644 --- a/crates/shirabe/Cargo.toml +++ b/crates/shirabe/Cargo.toml @@ -22,7 +22,6 @@ shirabe-symfony-console.workspace = true shirabe-symfony-filesystem.workspace = true shirabe-symfony-finder.workspace = true shirabe-symfony-process.workspace = true -shirabe-xdebug-handler.workspace = true anyhow.workspace = true async-trait.workspace = true base64.workspace = true diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs index 2af2acfc..5094f06a 100644 --- a/crates/shirabe/src/command/diagnose_command.rs +++ b/crates/shirabe/src/command/diagnose_command.rs @@ -887,9 +887,6 @@ impl DiagnoseCommand { if filter_var_boolean(diagnostics.ini_get("xdebug.profiler_enabled").unwrap_or("")) { warnings.insert("xdebug_profile".to_string(), PhpMixed::Bool(true)); } else if diagnostics.xdebug_active { - // PHP: XdebugHandler::isXdebugActive(). As with IniHelper::get_all, the port of that - // method in shirabe_xdebug_handler is a stub, so the real runtime is queried through - // the diagnose payload instead. warnings.insert("xdebug_loaded".to_string(), PhpMixed::Bool(true)); } diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index cc456c3c..8fed08e9 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -98,7 +98,6 @@ use shirabe_symfony_console::style::style_interface::StyleInterface; use shirabe_symfony_console::style::symfony_style::SymfonyStyle; use shirabe_symfony_console::terminal::Terminal; use shirabe_symfony_process::exception::ProcessTimedOutException; -use shirabe_xdebug_handler::XdebugHandler; /// The PHP `Composer\Console\Application` and `Symfony\Component\Console\Application` are /// flattened into a single struct. Methods that are overridden by subclass and called via @@ -2275,7 +2274,7 @@ impl ApplicationHandle { io.write_error(&format!("<warning>Composer supports PHP 7.2.5 and above, you will most likely encounter problems with your PHP {}. Upgrading is strongly recommended but you can use Composer 2.2.x LTS as a fallback.</warning>", PHP_VERSION)); } - if XdebugHandler::is_xdebug_active() + if shirabe_php_rpc::xdebug::is_xdebug_active() && Platform::get_env("COMPOSER_DISABLE_XDEBUG_WARN").is_none() { io.write_error("<warning>Composer is operating slower than normal because you have Xdebug enabled. See https://getcomposer.org/xdebug</warning>"); diff --git a/crates/shirabe/src/main.rs b/crates/shirabe/src/main.rs index dcf33803..f8b64c46 100644 --- a/crates/shirabe/src/main.rs +++ b/crates/shirabe/src/main.rs @@ -29,18 +29,14 @@ fn init_tracing() { } fn main() { - // TODO(phase-c): PHP: `$xdebug = new XdebugHandler('Composer'); $xdebug->check(); unset($xdebug);` - // (restart the process without Xdebug loaded, for performance) is not ported. Since no - // XdebugHandler is ever constructed, `self::$name` never gets set, so the - // COMPOSER_ORIGINAL_INIS-env-var branch of XdebugHandler::getAllIniFiles() (see - // shirabe-xdebug-handler's xdebug_handler.rs and shirabe/src/util/ini_helper.rs) is - // unreachable in this port, not merely unexercised by current tests. - // Take the $_ENV / $_SERVER snapshots before any putenv() mutates the real environment. // See `docs/dev/env-vars-porting.md` for details. std::sync::LazyLock::force(&PHP_ENV); std::sync::LazyLock::force(&PHP_SERVER); + // Composer calls XdebugHandler::check() here to restart without Xdebug, which is intentionally + // unported. See `docs/dev/xdebug.md`. + // The single process-wide tokio Runtime. `shirabe::run` and everything under it // (Command::execute and friends) is still synchronous top to bottom; entering the runtime // here (rather than driving `run` via `.block_on`) just makes it ambiently available via diff --git a/crates/shirabe/src/repository/platform_repository.rs b/crates/shirabe/src/repository/platform_repository.rs index 7e123151..3e1f35ec 100644 --- a/crates/shirabe/src/repository/platform_repository.rs +++ b/crates/shirabe/src/repository/platform_repository.rs @@ -20,11 +20,10 @@ use shirabe_pcre::{CaptureKey, Preg}; use shirabe_php_rpc::PlatformInfo; use shirabe_php_shim::{ InvalidArgumentException, PhpMixed, UnexpectedValueException, array_map_str_fn, - array_slice_strs, explode, get_class, implode, in_array_strict, is_string, php_regex, - str_replace, str_starts_with, strpos, strtolower, var_export, + array_slice_strs, explode, get_class, implode, is_string, php_regex, str_replace, + str_starts_with, strpos, strtolower, var_export, }; use shirabe_semver::constraint::SimpleConstraint; -use shirabe_xdebug_handler::XdebugHandler; use std::sync::{LazyLock, Mutex}; static LAST_SEEN_PLATFORM_PHP: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None)); @@ -303,17 +302,10 @@ impl PlatformRepository { } // Check for Xdebug in a restarted process - if !in_array_strict( - "xdebug".to_string(), - &loaded_extensions - .iter() - .map(|s| PhpMixed::String(s.clone())) - .collect::<Vec<_>>(), - ) && let Some(xdebug_pretty_version) = XdebugHandler::get_skipped_version() - && !xdebug_pretty_version.is_empty() - { - self.add_extension("xdebug", &xdebug_pretty_version)?; - } + // PHP re-adds the extension `XdebugHandler::getSkippedVersion()` names, since its restart + // runs on an ini file with Xdebug commented out. The worker is instead started with + // `xdebug.mode=off` (see docs/dev/xdebug.md), which leaves the extension loaded, so it is + // already among $loadedExtensions. // Another quick loop, just for possible libraries // Doing it this way to know that functions or constants exist before diff --git a/crates/shirabe/src/util/ini_helper.rs b/crates/shirabe/src/util/ini_helper.rs index 4ff9a5e2..08f5c816 100644 --- a/crates/shirabe/src/util/ini_helper.rs +++ b/crates/shirabe/src/util/ini_helper.rs @@ -4,10 +4,12 @@ pub struct IniHelper; impl IniHelper { /// Returns an array of php.ini locations with at least one entry. + /// + /// PHP asks `XdebugHandler::getAllIniFiles()`, which answers from `COMPOSER_ORIGINAL_INIS` + /// when it is set, because a restarted process runs on a generated ini file and has to name + /// the ones it replaced. Nothing here restarts PHP, so the worker always runs on the machine's + /// own ini files and the variable is not consulted. pub fn get_all() -> Vec<String> { - // PHP: XdebugHandler::getAllIniFiles(). shirabe_xdebug_handler::XdebugHandler's port - // of that method is a stub that returns the PHP default, so query the real PHP - // runtime's loaded/scanned ini files here instead. shirabe_php_rpc::get_all_ini_files() } diff --git a/crates/shirabe/tests/util/ini_helper_test.rs b/crates/shirabe/tests/util/ini_helper_test.rs index 90015169..f9ef3219 100644 --- a/crates/shirabe/tests/util/ini_helper_test.rs +++ b/crates/shirabe/tests/util/ini_helper_test.rs @@ -1,104 +1,7 @@ //! ref: composer/tests/Composer/Test/Util/IniHelperTest.php - -use shirabe::util::ini_helper::IniHelper; -use shirabe::util::platform::Platform; -use shirabe_php_shim::{getenv, putenv}; - -#[allow(dead_code)] -fn set_up() -> TearDown { - // Register our name with XdebugHandler. - // The PHP test constructs `new XdebugHandler('composer')` so that - // getAllIniFiles() reads the COMPOSER_ORIGINAL_INIS env var. The Rust - // XdebugHandler is a unit struct with no name-registration API, so this - // step is a no-op here. - // Save current state - let env_original = - getenv("COMPOSER_ORIGINAL_INIS").map(|value| value.to_string_lossy().into_owned()); - TearDown { env_original } -} - -#[allow(dead_code)] -fn tear_down(env_original: &Option<String>) { - // Restore original state - if let Some(env_original) = env_original { - unsafe { putenv("COMPOSER_ORIGINAL_INIS", env_original) }; - } else { - Platform::clear_env("COMPOSER_ORIGINAL_INIS"); - } -} - -#[allow(dead_code)] -struct TearDown { - env_original: Option<String>, -} - -impl Drop for TearDown { - fn drop(&mut self) { - tear_down(&self.env_original); - } -} - -fn set_env(paths: &[&str]) { - unsafe { - putenv( - "COMPOSER_ORIGINAL_INIS", - std::env::join_paths(paths).unwrap(), - ) - }; -} - -#[test] -#[ignore = "XdebugHandler is never constructed with a name (the bin/composer restart-without-Xdebug \ -bootstrap is unported, see TODO(phase-c) in shirabe's main.rs), so IniHelper::get_all() queries \ -the real PHP runtime directly instead of consulting COMPOSER_ORIGINAL_INIS"] -fn test_with_no_ini() { - let paths = [""]; - - set_env(&paths); - assert!(IniHelper::get_message().contains("does not exist")); - assert_eq!( - paths.iter().map(|s| s.to_string()).collect::<Vec<_>>(), - IniHelper::get_all() - ); -} - -#[test] -#[ignore = "XdebugHandler is never constructed with a name (the bin/composer restart-without-Xdebug \ -bootstrap is unported, see TODO(phase-c) in shirabe's main.rs), so IniHelper::get_all() queries \ -the real PHP runtime directly instead of consulting COMPOSER_ORIGINAL_INIS"] -fn test_with_loaded_ini_only() { - let paths = ["loaded.ini"]; - - set_env(&paths); - assert!(IniHelper::get_message().contains("loaded.ini")); -} - -#[test] -#[ignore = "XdebugHandler is never constructed with a name (the bin/composer restart-without-Xdebug \ -bootstrap is unported, see TODO(phase-c) in shirabe's main.rs), so IniHelper::get_all() queries \ -the real PHP runtime directly instead of consulting COMPOSER_ORIGINAL_INIS"] -fn test_with_loaded_ini_and_additional() { - let paths = ["loaded.ini", "one.ini", "two.ini"]; - - set_env(&paths); - assert!(IniHelper::get_message().contains("multiple ini files")); - assert_eq!( - paths.iter().map(|s| s.to_string()).collect::<Vec<_>>(), - IniHelper::get_all() - ); -} - -#[test] -#[ignore = "XdebugHandler is never constructed with a name (the bin/composer restart-without-Xdebug \ -bootstrap is unported, see TODO(phase-c) in shirabe's main.rs), so IniHelper::get_all() queries \ -the real PHP runtime directly instead of consulting COMPOSER_ORIGINAL_INIS"] -fn test_without_loaded_ini_and_additional() { - let paths = ["", "one.ini", "two.ini"]; - - set_env(&paths); - assert!(IniHelper::get_message().contains("multiple ini files")); - assert_eq!( - paths.iter().map(|s| s.to_string()).collect::<Vec<_>>(), - IniHelper::get_all() - ); -} +//! +//! All four cases of this test feed IniHelper made-up ini paths through COMPOSER_ORIGINAL_INIS. +//! That variable exists so a process restarted without Xdebug can name the ini files it replaced; +//! nothing here restarts PHP, so IniHelper::get_all() reports the ini files the PHP worker loaded +//! and never reads it (see docs/dev/xdebug.md). With no way to feed the helper, none of the cases +//! are ported. diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md index 314386d8..8418db88 100644 --- a/docs/dev/php-rpc.md +++ b/docs/dev/php-rpc.md @@ -11,7 +11,8 @@ domain socket. There is exactly one child process per Shirabe process, shared by The existing `PhpExecutableFinder` class resolves the PHP binary. The child is started with `-d serialize_precision=-1` so the wire codec's float formatting is pinned to the default PHP -behavior. +behavior, and with `-d xdebug.mode=off` unless `COMPOSER_ALLOW_XDEBUG` asks for Xdebug to stay +(see `xdebug.md`). ## Transport diff --git a/docs/dev/xdebug.md b/docs/dev/xdebug.md new file mode 100644 index 00000000..be198cca --- /dev/null +++ b/docs/dev/xdebug.md @@ -0,0 +1,28 @@ +# Xdebug + +Composer restarts itself without Xdebug loaded, because Xdebug makes PHP +several times slower. Shirabe is written in Rust, where Xdebug does not exist, +but it invokes a PHP command for plugins, scripts and platform queries. +When Shirabe spawns a PHP worker, Xdebug is disabled as Composer does. + +The PHP worker is spawned with the `-d xdebug.mode=off` flag and the +`XDEBUG_MODE=off` environment variable. The way to disable Xdebug in Shirabe is +different from Composer: Composer restarts its own process with a temporary +INI file, where Xdebug extension is disabled. The difference probably does not +matter, both for users and for plugin authors. + +## Enable Xdebug in Shirabe's PHP worker + +It is the same as Composer: setting `COMPOSER_ALLOW_XDEBUG` to 1 makes Shirabe +leave Xdebug enabled. + +``` +$ COMPOSER_ALLOW_XDEBUG=1 shirabe install +``` + +## Xdebug 2 support + +Shirabe does not try to disable Xdebug version 2 because Xdebug 2 has no +`xdebug.mode` or `XDEBUG_MODE`, while Composer disables Xdebug 2 too. The +performance penalty seems to be small as Shirabe's CPU-heavy workloads are +written in Rust. diff --git a/docs/known-incompatibilities.md b/docs/known-incompatibilities.md index 99c0e322..daaa8bcd 100644 --- a/docs/known-incompatibilities.md +++ b/docs/known-incompatibilities.md @@ -32,6 +32,13 @@ The following are intentionally left unchanged for ecosystem compatibility: TODO: a CLI flag or an environment variable to force Shirabe to use compatible paths. +## Xdebug + +Composer disables Xdebug on startup. Shirabe emulates it, but the exact +behavior is slightly different. See [docs/dev/xdebug.md](./dev/xdebug.md) +for details. + + ## Plugins ### Reflection |
