CHANGELOG.md000064400000002424144760121370006364 0ustar00## [Unreleased] ## [1.1.0] - 2018-04-11 * Added: `getRestartSettings` method for calling PHP processes in a restart process. * Added: API definition and @internal class annotations. * Added: protected `requiresRestart` method for extending classes. * Added: `setMainScript` method for applications that change the working directory. * Changed: private `tmpIni` variable to protected for extending classes. * Fixed: environment variables not available in $_SERVER when restored in the restart. * Fixed: relative path problems caused by Phar::interceptFileFuncs - [composer/xdebug-handler#46](https://github.com/composer/xdebug-handler/issues/46). * Fixed: incorrect handling when script file cannot be found. ## [1.0.0] - 2018-03-08 * Added: PSR3 logging for optional status output. * Added: existing ini settings are merged to catch command-line overrides. * Added: code, tests and other artefacts to decouple from Composer. * Break: the following class was renamed: - `Composer\XdebugHandler` -> `Composer\XdebugHandler\XdebugHandler` [Unreleased]: https://github.com/composer/xdebug-handler/compare/1.1.0...HEAD [1.1.0]: https://github.com/composer/xdebug-handler/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/composer/xdebug-handler/compare/d66f0d15cb57...1.0.0 LICENSE000064400000002051144760121370005554 0ustar00MIT License Copyright (c) 2017 Composer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. README.md000064400000017061144760121370006035 0ustar00# composer/xdebug-handler [![packagist](https://img.shields.io/packagist/v/composer/xdebug-handler.svg)](https://packagist.org/packages/composer/xdebug-handler) [![linux build](https://img.shields.io/travis/composer/xdebug-handler/master.svg?label=linux+build)](https://travis-ci.org/composer/xdebug-handler) [![windows build](https://img.shields.io/appveyor/ci/Seldaek/xdebug-handler/master.svg?label=windows+build)](https://ci.appveyor.com/project/Seldaek/xdebug-handler) ![license](https://img.shields.io/github/license/composer/xdebug-handler.svg) ![php](https://img.shields.io/packagist/php-v/composer/xdebug-handler.svg?colorB=8892BF&label=php) Restart a CLI process without loading the xdebug extension. Originally written as part of [composer/composer](https://github.com/composer/composer), now extracted and made available as a stand-alone library. ## Installation Install the latest version with: ```bash $ composer require composer/xdebug-handler ``` ## Requirements * PHP 5.3.2 minimum, although functionality is disabled below PHP 5.4.0. Using the latest PHP version is highly recommended. ## Basic Usage ```php use Composer\XdebugHandler\XdebugHandler; $xdebug = new XdebugHandler('myapp'); $xdebug->check(); unset($xdebug); ``` The constructor takes two parameters: #### _$envPrefix_ This is used to create distinct environment variables and is upper-cased and prepended to default base values. The above example enables the use of: - `MYAPP_ALLOW_XDEBUG=1` to override automatic restart and allow xdebug - `MYAPP_ORIGINAL_INIS` to obtain ini file locations in a restarted process #### _$colorOption_ This optional value is added to the restart command-line and is needed to force color output in a piped child process. Only long-options are supported, for example `--ansi` `--colors=always` etc. If the original command-line contains an argument that pattern-matches this value, for example `--no-ansi` `--colors=never`, then _$colorOption_ is ignored. Do not use this parameter if the input handler cannot cope with an option as the last argument. ## Advanced Usage ### How it works A temporary ini file is created from the loaded (and scanned) ini files, with any references to the xdebug extension commented out. Current ini settings are merged, so that settings made on the command-line or by the application are included. * `MYAPP_ALLOW_XDEBUG` is set with internal data to flag and use in the restart. * If any scanned ini files were used, `PHP_INI_SCAN_DIR` is set to an empty string. This tells PHP not to scan for additional inis. * The temporary ini is added to the command-line with the `-c` option. * The application is restarted in a new process using `passthru`. * `MYAPP_ALLOW_XDEBUG` is unset. * `PHP_INI_SCAN_DIR` is restored to its original value (if it was changed). * The application runs and exits. * The main process exits with the exit code from the restarted process. ### Ini files If the application does anything with ini files, then functions like `php_ini_loaded_file` and `php_ini_scanned_files` will not work correctly in a restarted process. To make the original locations available, they are saved to the environment variable suffixed `_ORIGINAL_INIS`. This is a path-separated string comprising the location returned from `php_ini_loaded_file`, which could be empty, followed by locations parsed from calling `php_ini_scanned_files`. A static helper method `XdebugHandler::getAllIniFiles` is provided to access these values in a single array, regardless of whether the process has been restarted or not. ```php use Composer\XdebugHandler\XdebugHandler; $files = XdebugHandler::getAllIniFiles(); // $files[0] always exists, it could be an empty string $loadedIni = array_shift($files); $scannedInis = $files; ``` ### Restarted process Other static helper methods provide information about the current process, which may or may not have been restarted: * `XdebugHandler::getSkippedVersion` - the xdebug version string that was skipped by the restart, or an empty value. * `XdebugHandler::getRestartSettings` - an array of settings to use with PHP sub-processes, or null. ```php use Composer\XdebugHandler\XdebugHandler; $version = XdebugHandler::getSkippedVersion(); // $version: '2.6.0' (for example), or an empty string $settings = XdebugHandler::getRestartSettings(); /** * $settings: array (if the current process was restarted, * or called with the settings from a previous restart), or null * * 'tmpIni' => the temporary ini file used in the restart (string) * 'scannedInis' => if there were any scanned inis (bool) * 'scanDir' => the original PHP_INI_SCAN_DIR value (false|string) * 'inis' => the original inis from getAllIniFiles (array) * 'skipped' => the skipped version from getSkippedVersion (string) */ ``` #### Sub-processes Calling a PHP process from a restarted process will result in xdebug being loaded in that process, or another restart if xdebug-handler is implemented. The `XdebugHandler::getRestartSettings()` method is provided so that an application can call a PHP process with the same settings that were used in a restart: * If `scannedInis` is true, set `PHP_INI_SCAN_DIR` to an empty string. * Add `tmpIni`to the command-line with the `-c` option. * Run the process. * If xdebug-handler is implemented, its internal settings are synced and `PHP_INI_SCAN_DIR` is restored to its original value (if it was changed). * If `PHP_INI_SCAN_DIR` was changed, restore it using `scanDir`. This solution is not without its pitfalls. In addition to the ini files issue outlined above, `PHP_INI_SCAN_DIR` is not restored in the sub-process (unless xdebug-hander is implemented). This will cause problems if it was changed and the sub-process calls another PHP process. However, an application can safely spawn itself, or other scripts that it controls, or other applications that implement xdebug-handler. ### Output The `setLogger` method enables the output of status messages to an external PSR3 logger. ```php use Composer\XdebugHandler\XdebugHandler; $xdebug = new XdebugHandler('myapp'); // Provide a PSR3 logger $xdebug->setLogger($myLogger); ``` All messages are reported with either `DEBUG` or `WARNING` log levels. For example: ``` // Restart overridden DEBUG Checking MYAPP_ALLOW_XDEBUG DEBUG The xdebug extension is loaded (2.5.0) DEBUG No restart (MYAPP_ALLOW_XDEBUG=1) // Failed restart DEBUG Checking MYAPP_ALLOW_XDEBUG DEBUG The xdebug extension is loaded (2.5.0) WARNING No restart (Unable to create temporary ini file) ``` ### Main script The process will not be restarted if the location of the main script is inaccessible. This may occur if the working directory has been changed and can be fixed by using the `setMainScript` method. ```php // Save the full path to the invoked script $mainScript = realpath($_SERVER['argv'][0]); ... use Composer\XdebugHandler\XdebugHandler; $xdebug = new XdebugHandler('myapp'); $xdebug->setMainScript($mainScript); ``` ### Extending the library The API is defined by classes and their accessible elements that are not annotated as @internal. The main class has two protected methods that can be overriden to provide additional functionality: #### _requiresRestart($isLoaded)_ By default the process will restart if xdebug is loaded. Overriding this allows an application to decide. This method is only called if `MYAPP_ALLOW_XDEBUG` is empty. #### _restart($command)_ An application can hook into this to access the temporary ini file, its location given in the `tmpIni` property. ## License composer/xdebug-handler is licensed under the MIT License, see the LICENSE file for details. composer.json000064400000001620144760121370007272 0ustar00{ "name": "composer/xdebug-handler", "description": "Restarts a process without xdebug.", "type": "library", "license": "MIT", "keywords": [ "xdebug", "performance" ], "authors": [ { "name": "John Stevenson", "email": "john-stevenson@blueyonder.co.uk" } ], "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues" }, "require": { "php": "^5.3.2 || ^7.0", "psr/log": "^1.0" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" }, "autoload": { "psr-4": { "Composer\\XdebugHandler\\": "src" } }, "autoload-dev": { "psr-4": { "Composer\\XdebugHandler\\": "tests" } }, "scripts": { "test": "phpunit" } } src/Process.php000064400000010740144760121370007471 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\XdebugHandler; /** * Provides utility functions to prepare a child process command-line and set * environment variables in that process. * * @author John Stevenson * @internal */ class Process { /** * Returns the process arguments, appending a color option if required * * A color option is needed because child process output is piped. * * @param array $args Command line arguments * @param string $colorOption The long option to force color output * * @return array */ public static function addColorOption(array $args, $colorOption) { if (!$colorOption || in_array($colorOption, $args) || !preg_match('/^--([a-z]+$)|(^--[a-z]+=)/', $colorOption, $matches)) { return $args; } if (isset($matches[2])) { // Handle --color(s)= options. Note args[0] is the script name if ($index = array_search($matches[2].'auto', $args)) { $args[$index] = $colorOption; return $args; } elseif (preg_grep('/^'.$matches[2].'/', $args)) { return $args; } } elseif (in_array('--no-'.$matches[1], $args)) { return $args; } $args[] = $colorOption; return $args; } /** * Escapes a string to be used as a shell argument. * * From https://github.com/johnstevenson/winbox-args * MIT Licensed (c) John Stevenson * * @param string $arg The argument to be escaped * @param bool $meta Additionally escape cmd.exe meta characters * @param bool $module The argument is the module to invoke * * @return string The escaped argument */ public static function escape($arg, $meta = true, $module = false) { if (!defined('PHP_WINDOWS_VERSION_BUILD')) { return escapeshellarg($arg); } $quote = strpbrk($arg, " \t") !== false || $arg === ''; $arg = preg_replace('/(\\\\*)"/', '$1$1\\"', $arg, -1, $dquotes); if ($meta) { $meta = $dquotes || preg_match('/%[^%]+%/', $arg); if (!$meta) { $quote = $quote || strpbrk($arg, '^&|<>()') !== false; } elseif ($module && !$dquotes && $quote) { $meta = false; } } if ($quote) { $arg = preg_replace('/(\\\\*)$/', '$1$1', $arg); $arg = '"'.$arg.'"'; } if ($meta) { $arg = preg_replace('/(["^&|<>()%])/', '^$1', $arg); } return $arg; } /** * Returns true if the output stream supports colors * * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo * terminals via named pipes, so we can only check the environment. * * @param mixed $output A valid CLI output stream * * @return bool */ public static function supportsColor($output) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { return (function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support($output)) || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); } if (function_exists('stream_isatty')) { return stream_isatty($output); } elseif (function_exists('posix_isatty')) { return posix_isatty($output); } $stat = fstat($output); // Check if formatted mode is S_IFCHR return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; } /** * Makes putenv environment changes available in $_SERVER * * @param string $name * @param string|false $value A false value unsets the variable * * @return bool Whether the environment variable was set */ public static function setEnv($name, $value = false) { $unset = false === $value; if (!putenv($unset ? $name : $name.'='.$value)) { return false; } if ($unset) { unset($_SERVER[$name]); } else { $_SERVER[$name] = $value; } return true; } } src/Status.php000064400000007270144760121370007342 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\XdebugHandler; use Composer\XdebugHandler\Process; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** * @author John Stevenson * @internal */ class Status { const ENV_RESTART = 'XDEBUG_HANDLER_RESTART'; const CHECK = 'Check'; const ERROR = 'Error'; const INFO = 'Info'; const NORESTART = 'NoRestart'; const RESTART = 'Restart'; const RESTARTING = 'Restarting'; const RESTARTED = 'Restarted'; private $envAllowXdebug; private $loaded; private $logger; private $time; /** * Constructor * * @param LoggerInterface $logger * @param string $envAllowXdebug Prefixed _ALLOW_XDEBUG name */ public function __construct(LoggerInterface $logger, $envAllowXdebug) { $start = getenv(self::ENV_RESTART); Process::setEnv(self::ENV_RESTART); $this->time = $start ? round((microtime(true) - $start) * 1000) : 0; $this->logger = $logger; $this->envAllowXdebug = $envAllowXdebug; } /** * Calls a handler method to report a message * * @param string $op The handler constant * @param null|string $data Data required by the handler */ public function report($op, $data) { $func = array($this, 'report'.$op); call_user_func($func, $data); } /** * Sends a status message to the logger * * @param string $text * @param string $level */ private function output($text, $level = null) { $this->logger->log($level ?: LogLevel::DEBUG, $text); } private function reportCheck($loaded) { $this->loaded = $loaded; $this->output('Checking '.$this->envAllowXdebug); } private function reportError($error) { $this->output(sprintf('No restart (%s)', $error), LogLevel::WARNING); } private function reportInfo($info) { $this->output($info); } private function reportNoRestart() { $this->output($this->getLoadedMessage()); if ($this->loaded) { $text = sprintf('No restart (%s)', $this->getEnvAllow()); if (!getenv($this->envAllowXdebug)) { $text .= ' Allowed by application'; } $this->output($text); } } private function reportRestart() { $this->output($this->getLoadedMessage()); Process::setEnv(self::ENV_RESTART, (string) microtime(true)); } private function reportRestarted() { $loaded = $this->getLoadedMessage(); $text = sprintf('Restarted (%d ms). %s', $this->time, $loaded); $level = $this->loaded ? LogLevel::WARNING : null; $this->output($text, $level); } private function reportRestarting($command) { $text = sprintf('Process restarting (%s)', $this->getEnvAllow()); $this->output($text); $text = 'Running '.$command; $this->output($text); } /** * Returns the _ALLOW_XDEBUG environment variable as name=value * * @return string */ private function getEnvAllow() { return $this->envAllowXdebug.'='.getenv($this->envAllowXdebug); } /** * Returns the xdebug status and version * * @return string */ private function getLoadedMessage() { $loaded = $this->loaded ? sprintf('loaded (%s)', $this->loaded) : 'not loaded'; return 'The xdebug extension is '.$loaded; } } src/XdebugHandler.php000064400000036155144760121370010577 0ustar00 * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\XdebugHandler; use Psr\Log\LoggerInterface; /** * @author John Stevenson */ class XdebugHandler { const SUFFIX_ALLOW = '_ALLOW_XDEBUG'; const SUFFIX_INIS = '_ORIGINAL_INIS'; const RESTART_ID = 'internal'; const RESTART_SETTINGS = 'XDEBUG_HANDLER_SETTINGS'; /** @var string|null */ protected $tmpIni; private static $inRestart; private static $name; private static $skipped; private $cli; private $colorOption; private $envAllowXdebug; private $envOriginalInis; private $loaded; private $script; /** @var Status|null */ private $statusWriter; /** * Constructor * * The $envPrefix is used to create distinct environment variables. It is * uppercased and prepended to the default base values. For example 'myapp' * would result in MYAPP_ALLOW_XDEBUG and MYAPP_ORIGINAL_INIS. * * @param string $envPrefix Value used in environment variables * @param string $colorOption Command-line long option to force color output * @throws \RuntimeException If a parameter is invalid */ public function __construct($envPrefix, $colorOption = '') { if (!is_string($envPrefix) || empty($envPrefix) || !is_string($colorOption)) { throw new \RuntimeException('Invalid constructor parameter'); } self::$name = strtoupper($envPrefix); $this->envAllowXdebug = self::$name.self::SUFFIX_ALLOW; $this->envOriginalInis = self::$name.self::SUFFIX_INIS; $this->colorOption = $colorOption; $this->cli = PHP_SAPI === 'cli'; if (extension_loaded('xdebug')) { $ext = new \ReflectionExtension('xdebug'); $this->loaded = $ext->getVersion() ?: 'unknown'; } } /** * Activates status message output to a PSR3 logger * * @param LoggerInterface $logger */ public function setLogger(LoggerInterface $logger) { $this->statusWriter = new Status($logger, $this->envAllowXdebug); } /** * Sets the main script location if the working directory has changed * * @param string $script */ public function setMainScript($script) { $this->script = $script; } /** * Checks if xdebug is loaded and the process needs to be restarted * * If so, then a tmp ini is created with the xdebug ini entry commented out. * If scanned inis have been loaded, these are combined into the tmp ini * and PHP_INI_SCAN_DIR is set to an empty value. Current ini locations are * are stored in MYAPP_ORIGINAL_INIS (where 'MYAPP' is the prefix passed in the * constructor) for use in the restarted process. * * This behaviour can be disabled by setting the MYAPP_ALLOW_XDEBUG * environment variable to 1. This variable is used internally so that the * restarted process is created only once and PHP_INI_SCAN_DIR can be * restored to its original value. */ public function check() { $this->notify(Status::CHECK, $this->loaded); $envArgs = explode('|', (string) getenv($this->envAllowXdebug), 4); if (empty($envArgs[0]) && $this->requiresRestart((bool) $this->loaded)) { // Restart required $this->notify(Status::RESTART); if ($this->prepareRestart()) { $command = $this->getCommand(); $this->notify(Status::RESTARTING, $command); $this->restart($command); } return; } if (self::RESTART_ID === $envArgs[0] && count($envArgs) >= 3) { // Restarting, so unset environment variable and extract saved values $this->notify(Status::RESTARTED); Process::setEnv($this->envAllowXdebug); self::$inRestart = true; $version = $envArgs[1]; $scannedInis = (bool) $envArgs[2]; if (!$this->loaded) { // Skipped version is only set if xdebug is not loaded self::$skipped = $version; } if ($scannedInis) { // Scan dir will have been changed, so restore it if (isset($envArgs[3])) { Process::setEnv('PHP_INI_SCAN_DIR', $envArgs[3]); } else { Process::setEnv('PHP_INI_SCAN_DIR'); } } // Put restart settings in the environment $this->setEnvRestartSettings($scannedInis); return; } $this->notify(Status::NORESTART); if ($settings = self::getRestartSettings()) { // Called with existing settings, so sync our settings $this->syncSettings($settings); } } /** * Returns an array of php.ini locations with at least one entry * * The equivalent of calling php_ini_loaded_file then php_ini_scanned_files. * The loaded ini location is the first entry and may be empty. * * @return array */ public static function getAllIniFiles() { if (!empty(self::$name)) { $env = getenv(self::$name.self::SUFFIX_INIS); if (false !== $env) { return explode(PATH_SEPARATOR, $env); } } $paths = array((string) php_ini_loaded_file()); if ($scanned = php_ini_scanned_files()) { $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); } return $paths; } /** * Returns an array of restart settings or null * * Settings will be available if the current process was restarted, or * called with the settings from an existing restart. * * @return array|null */ public static function getRestartSettings() { $envArgs = explode('|', (string) getenv(self::RESTART_SETTINGS), 5); if (count($envArgs) === 5) { if (!self::$inRestart && php_ini_loaded_file() !== $envArgs[0] && php_ini_scanned_files()) { return; } return array( 'tmpIni' => $envArgs[0], 'scannedInis' => (bool) $envArgs[1], 'scanDir' => '!' !== $envArgs[2] ? $envArgs[2] : false, 'inis' => explode(',', $envArgs[3]), 'skipped' => $envArgs[4], ); } } /** * Returns the xdebug version that triggered a successful restart * * @return string */ public static function getSkippedVersion() { return (string) self::$skipped; } /** * Returns true if xdebug is loaded, or as directed by an extending class * * @param bool $isLoaded Whether xdebug is loaded * * @return bool */ protected function requiresRestart($isLoaded) { return $isLoaded; } /** * Allows an extending class to access the tmpIni * * @param string $command */ protected function restart($command) { $this->doRestart($command); } /** * Executes the restarted command then deletes the tmp ini * * @param string $command */ private function doRestart($command) { passthru($command, $exitCode); $this->notify(Status::INFO, 'Restarted process exited '.$exitCode); if (!empty($this->tmpIni)) { @unlink($this->tmpIni); } exit($exitCode); } /** * Returns true if everything was written for the restart * * If any of the following fails (however unlikely) we must return false to * stop potential recursion: * - tmp ini file creation * - environment variable creation * * @return bool */ private function prepareRestart() { $error = ''; $iniFiles = self::getAllIniFiles(); $scannedInis = count($iniFiles) > 1; $scanDir = getenv('PHP_INI_SCAN_DIR'); if (!$this->cli) { $error = 'Unsupported SAPI: '.PHP_SAPI; } elseif (!defined('PHP_BINARY')) { $error = 'PHP version is too old: '.PHP_VERSION; } elseif (!$this->checkMainScript()) { $error = 'Unable to access main script: '.$this->script; } elseif (!$this->writeTmpIni($iniFiles)) { $error = 'Unable to create temporary ini file'; } elseif (!$this->setEnvironment($scannedInis, $scanDir, $iniFiles)) { $error = 'Unable to set environment variables'; } if ($error) { $this->notify(Status::ERROR, $error); } return empty($error); } /** * Returns true if the tmp ini file was written * * The filename is passed as the -c option when the process restarts. * * @param array $iniFiles All ini files used in the current process * * @return bool */ private function writeTmpIni(array $iniFiles) { if (!$this->tmpIni = tempnam(sys_get_temp_dir(), '')) { return false; } // $iniFiles has at least one item and it may be empty if (empty($iniFiles[0])) { array_shift($iniFiles); } $content = ''; $regex = '/^\s*(zend_extension\s*=.*xdebug.*)$/mi'; foreach ($iniFiles as $file) { $data = preg_replace($regex, ';$1', file_get_contents($file)); $content .= $data.PHP_EOL; } $loaded = ini_get_all(null, false); $config = parse_ini_string($content); $content .= $this->mergeLoadedConfig($loaded, $config); // Work-around for https://bugs.php.net/bug.php?id=75932 $content .= 'opcache.enable_cli=0'.PHP_EOL; return @file_put_contents($this->tmpIni, $content); } /** * Returns the restart command line * * @return string */ private function getCommand() { $args = array_slice($_SERVER['argv'], 1); if (defined('STDOUT') && Process::supportsColor(STDOUT)) { $args = Process::addColorOption($args, $this->colorOption); } $executable = array(PHP_BINARY, '-c', $this->tmpIni, $this->script); $args = array_merge($executable, $args); $cmd = Process::escape(array_shift($args), true, true); foreach ($args as $arg) { $cmd .= ' '.Process::escape($arg); } return $cmd; } /** * Returns true if the restart environment variables were set * * No need to update $_SERVER since this is set in the restarted process. * * @param bool $scannedInis Whether there were scanned ini files * @param false|string $scanDir PHP_INI_SCAN_DIR environment variable * @param array $iniFiles All ini files used in the current process * * @return bool */ private function setEnvironment($scannedInis, $scanDir, array $iniFiles) { // Set scan dir env to an empty value if there were scanned ini files if ($scannedInis && !putenv('PHP_INI_SCAN_DIR=')) { return false; } // Make original inis available to restarted process if (!putenv($this->envOriginalInis.'='.implode(PATH_SEPARATOR, $iniFiles))) { return false; } // Flag restarted process and save values for it to use $envArgs = array( self::RESTART_ID, $this->loaded, (int) $scannedInis, ); if ($scannedInis && false !== $scanDir) { // Only add original scan dir if it was set $envArgs[] = $scanDir; } return putenv($this->envAllowXdebug.'='.implode('|', $envArgs)); } /** * Logs status messages * * @param string $op Status handler constant * @param null|string $data Optional data */ private function notify($op, $data = null) { if ($this->statusWriter) { $this->statusWriter->report($op, $data); } } /** * Returns default or changed settings for the tmp ini * * Ini settings can be passed on the command line using the -d option. To * preserve these, all loaded settings that are either not present or * different from those in the ini files are added at the end of the tmp ini. * * @param array $loadedConfig All current ini settings * @param array $iniConfig Settings from user ini files * * @return string */ private function mergeLoadedConfig(array $loadedConfig, array $iniConfig) { $content = ''; foreach ($loadedConfig as $name => $value) { // Values will either be null, string or array (HHVM only) if (!is_string($value) || strpos($name, 'xdebug') === 0) { continue; } if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { // Based on main -d option handling in php-src/sapi/cli/php_cli.c if ($value && !ctype_alnum($value)) { $value = '"'.str_replace('"', '\\"', $value).'"'; } $content .= $name.'='.$value.PHP_EOL; } } return $content; } /** * Returns true if the script name can be used * * @return bool */ private function checkMainScript() { if (null === $this->script) { $this->script = $_SERVER['argv'][0]; } if (file_exists($this->script)) { return true; } // Phar::interceptFileFuncs causes issues if ($archive = \Phar::running(false)) { $this->script = $archive; return true; } if (in_array($this->script, array('php://stdin', 'Standard input code'))) { $this->script === '--'; return true; } return false; } /** * Adds restart settings to the environment * * @param bool $scannedInis */ private function setEnvRestartSettings($scannedInis) { $scanDir = getenv('PHP_INI_SCAN_DIR'); $settings = array( php_ini_loaded_file(), (int) $scannedInis, false === $scanDir ? '!' : $scanDir, implode(',', self::getAllIniFiles()), self::$skipped, ); Process::setEnv(self::RESTART_SETTINGS, implode('|', $settings)); } /** * Syncs settings and the environment if called with existing settings * * @param array $settings */ private function syncSettings(array $settings) { if ($settings['scannedInis']) { Process::setEnv('PHP_INI_SCAN_DIR', $settings['scanDir']); } if (false === getenv($this->envOriginalInis)) { // Called by another app, so make original inis available Process::setEnv($this->envOriginalInis, implode('|', $settings['inis'])); } self::$skipped = $settings['skipped']; $this->notify(Status::INFO, 'Process called with existing restart settings'); } }