Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/phpstan
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use Symfony\Component\Console\Helper\ProgressBar;

define('__PHPSTAN_RUNNING__', true);

require_once __DIR__ . '/../src/Process/InheritedPhpConfig.php';
require_once __DIR__ . '/../src/Turbo/TurboExtensionEnabler.php';
require_once __DIR__ . '/../src/Turbo/TurboExtensionSelector.php';
require_once __DIR__ . '/../src/Turbo/TurboProcessRestarter.php';
Expand Down
11 changes: 10 additions & 1 deletion src/Command/BisectCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Command\Bisect\BinarySearch;
use PHPStan\File\FileReader;
use PHPStan\Internal\HttpClientFactory;
use PHPStan\Process\InheritedPhpConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
Expand All @@ -19,6 +20,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;
use function array_filter;
use function array_map;
use function array_merge;
use function array_values;
use function chmod;
Expand Down Expand Up @@ -416,9 +418,16 @@ public function buildAnalyseArgs(InputInterface $input): string

private function runAnalysis(string $pharPath, string $analyseArgs): int
{
// every bisect step is a full analysis of its own, and a child process
// inherits nothing of our command line - without this each of them
// would run with an Xdebug the user turned off for us, see
// InheritedPhpConfig
$phpArgs = implode(' ', array_map(static fn (string $arg): string => escapeshellarg($arg), InheritedPhpConfig::getArgs()));

$command = sprintf(
'%s %s analyse %s',
'%s %s %s analyse %s',
escapeshellarg(PHP_BINARY),
$phpArgs,
escapeshellarg($pharPath),
$analyseArgs,
);
Expand Down
9 changes: 8 additions & 1 deletion src/Command/FixerApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use PHPStan\Parallel\ForkParallelChecker;
use PHPStan\PhpDoc\StubFilesProvider;
use PHPStan\Process\ForkedProcessPromise;
use PHPStan\Process\InheritedPhpConfig;
use PHPStan\Process\ProcessCanceledException;
use PHPStan\Process\ProcessCrashedException;
use PHPStan\Process\ProcessHelper;
Expand All @@ -43,12 +44,14 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function array_map;
use function array_merge;
use function count;
use function defined;
use function escapeshellarg;
use function get_class;
use function http_build_query;
use function implode;
use function ini_get;
use function is_file;
use function parse_url;
Expand Down Expand Up @@ -302,7 +305,11 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc
}
}

return new Process(sprintf('%s -d memory_limit=%s %s --port %d', escapeshellarg(PHP_BINARY), escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), env: $env, fds: []);
// the PHPStan Pro process is a child like a worker is - it inherits
// nothing of our command line either, see InheritedPhpConfig
$phpArgs = implode(' ', array_map(static fn (string $arg): string => escapeshellarg($arg), InheritedPhpConfig::getArgs()));

return new Process(sprintf('%s %s -d memory_limit=%s %s --port %d', escapeshellarg(PHP_BINARY), $phpArgs, escapeshellarg(ini_get('memory_limit')), escapeshellarg($pharPath), $serverPort), env: $env, fds: []);
}

/**
Expand Down
8 changes: 6 additions & 2 deletions src/Parallel/ForkParallelChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
use PHPStan\Command\Output;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Diagnose\DiagnoseExtension;
use PHPStan\Process\InheritedPhpConfig;
use PHPStan\Process\ProcessHelper;
use PHPStan\Turbo\TurboExtensionEnabler;
use PHPStan\Turbo\TurboProcessRestarter;
use function function_exists;
use function getmypid;
use function implode;
use function opcache_get_status;
use function sprintf;
use function str_starts_with;
Expand Down Expand Up @@ -68,8 +70,10 @@ public function print(Output $output): void
$output->writeLineFormatted('Mechanism: spawn (react/child-process)');
$output->writeLineFormatted(sprintf('Reason fork not used: %s', $reason));

// what a spawned worker's command line adds on top of the php.ini
// (see ProcessHelper); the extension path is on the turbo lines
// what a spawned worker's command line spells out for it (see
// ProcessHelper); the extension path is on the turbo lines
$output->writeLineFormatted(sprintf('Worker php options: %s', implode(' ', InheritedPhpConfig::getArgs())));

$parentPid = getmypid();
$output->writeLineFormatted('Worker -d entries:');
foreach (ProcessHelper::resolveWorkerIniEntries(TurboProcessRestarter::getOpcacheArgs(), PHP_OS_FAMILY, $parentPid === false ? 0 : $parentPid, 1) as $iniEntry) {
Expand Down
102 changes: 102 additions & 0 deletions src/Process/InheritedPhpConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php declare(strict_types = 1);

namespace PHPStan\Process;

use function get_cfg_var;
use function ini_get;
use function is_string;
use function php_ini_loaded_file;
use function php_ini_scanned_files;
use function sys_get_temp_dir;
use function trim;

/**
* The PHP command-line options a child PHP process needs so that it runs with
* the PHP configuration of the process starting it - the spawned workers of
* ProcessHelper, the re-executed main process of TurboProcessRestarter, and
* the PHPStan Pro process of FixerApplication.
*
* The environment carries over to a child on its own, so PHPRC and
* PHP_INI_SCAN_DIR - what composer/xdebug-handler sets up for a persistent
* restart - need nothing from here. The command line carries over to nothing,
* and repeating just `-c`, as the worker command used to, reproduces the main
* php.ini and nothing else:
*
* - The ini scan directory is read again. That matches the spawning process
* when it read it too, and contradicts it when it did not - `php -n`, or a
* `-c` pointing elsewhere, then gives the worker every extension and setting
* the main process was deliberately started without.
* - `-d` entries are dropped. `php -d xdebug.mode=off vendor/bin/phpstan` turns
* Xdebug off for the main process alone: xdebug-handler sees an inactive
* Xdebug and rightly does not restart, so no PHPRC is set up either, and every
* worker loads Xdebug again from the scan directory in whatever mode the ini
* says - the analysis runs under an active Xdebug, several times slower, with
* nothing on screen saying so
* (https://github.com/phpstan/phpstan/issues/15189).
*
* PHP does not record which directives came from the command line, so `-d`
* entries cannot be repeated as a group - only the two that matter to a PHPStan
* process are: xdebug.mode, whose value decides how fast the whole run is, and
* sys_temp_dir, which decides where the result cache lives.
*/
final class InheritedPhpConfig
{

/**
* @return list<string>
*/
public static function getArgs(): array
{
return self::resolveArgs(php_ini_loaded_file(), php_ini_scanned_files(), sys_get_temp_dir(), self::getXdebugMode());
}

/**
* The xdebug.mode in effect, or false when nothing set it.
*
* ini_get() answers only for a loaded Xdebug - for any other PHP the
* directive is not registered and only the raw ini entry exists, which is
* what a child loading Xdebug when we do not would be configured by.
*/
private static function getXdebugMode(): string|false
{
$mode = ini_get('xdebug.mode');
if ($mode !== false) {
return $mode;
}

$mode = get_cfg_var('xdebug.mode');

return is_string($mode) ? $mode : false;
}

/**
* @param string|false $loadedIniFile php_ini_loaded_file() of the spawning process
* @param string|false $scannedIniFiles php_ini_scanned_files() of the spawning process
* @param string $tempDir sys_get_temp_dir() of the spawning process
* @param string|false $xdebugMode see getXdebugMode()
* @return list<string>
*/
public static function resolveArgs(string|false $loadedIniFile, string|false $scannedIniFiles, string $tempDir, string|false $xdebugMode): array
{
$args = [];
if ($scannedIniFiles === false || trim($scannedIniFiles) === '') {
// -n only suppresses the scan directory here: an explicit -c is
// still honored next to it, the way xdebug-handler restarts
$args[] = '-n';
}
if ($loadedIniFile !== false && $loadedIniFile !== '') {
$args[] = '-c';
$args[] = $loadedIniFile;
}
$args[] = '-d';
// quote value so PHP will parse it as a string when the path contains a bitwise operator like ~
$args[] = "sys_temp_dir='" . $tempDir . "'";
if ($xdebugMode !== false) {
$args[] = '-d';
$args[] = 'xdebug.mode=' . $xdebugMode;
}

return $args;
}

}
25 changes: 9 additions & 16 deletions src/Process/ProcessHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
use function implode;
use function ini_get;
use function is_bool;
use function php_ini_loaded_file;
use function sprintf;
use function sys_get_temp_dir;
use const PHP_BINARY;
use const PHP_OS_FAMILY;

Expand All @@ -23,11 +21,11 @@
* and SpawnedProcessPromise).
*
* Besides the worker command and its options it spells out the PHP
* configuration the worker runs with. The php.ini is inherited through
* `-c`, but command-line `-d` entries are not, so whatever the spawning
* process got that way - the turbo extension and the OPcache setup of the
* TurboProcessRestarter restart - is repeated here; see
* resolveWorkerIniEntries() for the set and the reasoning.
* configuration the worker runs with. Nothing of a command line is inherited
* by a child process, so whatever the spawning process got that way is
* repeated here: the php.ini situation it runs with (InheritedPhpConfig), and
* the turbo extension and the OPcache setup of the TurboProcessRestarter
* restart - see resolveWorkerIniEntries() for that set and the reasoning.
*/
final class ProcessHelper
{
Expand All @@ -46,15 +44,10 @@ public static function getWorkerCommand(
InputInterface $input,
): string
{
$phpIni = php_ini_loaded_file();
$phpCmd = $phpIni === false ? escapeshellarg(PHP_BINARY) : sprintf('%s -c %s', escapeshellarg(PHP_BINARY), escapeshellarg($phpIni));

$processCommandArray = [
$phpCmd,
'-d',
// quote value so PHP will parse it as a string when the path contains a bitwise operator like ~
'sys_temp_dir=' . escapeshellarg("'" . sys_get_temp_dir() . "'"),
];
$processCommandArray = [escapeshellarg(PHP_BINARY)];
foreach (InheritedPhpConfig::getArgs() as $inheritedArg) {
$processCommandArray[] = escapeshellarg($inheritedArg);
}

if ($input->getOption('memory-limit') === null) {
$processCommandArray[] = '-d';
Expand Down
42 changes: 32 additions & 10 deletions src/Turbo/TurboProcessRestarter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Turbo;

use PHPStan\Process\InheritedPhpConfig;
use function explode;
use function extension_loaded;
use function function_exists;
Expand All @@ -11,7 +12,6 @@
use function is_string;
use function max;
use function pcntl_exec;
use function php_ini_loaded_file;
use function strtolower;
use function trim;
use const PHP_BINARY;
Expand Down Expand Up @@ -129,14 +129,37 @@ public static function restartIfSuitable(array $argv): void
return;
}

$args = [];
$phpIni = php_ini_loaded_file();
if ($phpIni !== false) {
$args[] = '-c';
$args[] = $phpIni;
}
pcntl_exec(PHP_BINARY, self::resolveRestartArgs(
InheritedPhpConfig::getArgs(),
$opcacheArgs,
$extensionPath,
ini_get('memory_limit'),
$argv,
));
// pcntl_exec() returns only on failure — continue as we are
}

/**
* The whole command line of the restarted process, php options first.
*
* The restart replaces the process, so everything the current command line
* gave it and a child process does not inherit has to be spelled out again
* - the php.ini situation and the Xdebug mode of InheritedPhpConfig just as
* much as the OPcache setup this restart exists for. Without the former,
* `php -d xdebug.mode=off vendor/bin/phpstan` restarted into a process with
* Xdebug active again, which xdebug-handler then had to restart a second
* time.
*
* @param list<string> $inheritedArgs InheritedPhpConfig::getArgs()
* @param list<string> $opcacheArgs getOpcacheArgs()
* @param list<string> $argv the current $_SERVER['argv'], php options already stripped from it
* @return list<string>
*/
public static function resolveRestartArgs(array $inheritedArgs, array $opcacheArgs, ?string $extensionPath, string $memoryLimit, array $argv): array
{
$args = $inheritedArgs;
$args[] = '-d';
$args[] = 'memory_limit=' . ini_get('memory_limit');
$args[] = 'memory_limit=' . $memoryLimit;
foreach ($opcacheArgs as $opcacheArg) {
$args[] = '-d';
$args[] = $opcacheArg;
Expand All @@ -153,8 +176,7 @@ public static function restartIfSuitable(array $argv): void
$args[] = $arg;
}

pcntl_exec(PHP_BINARY, $args);
// pcntl_exec() returns only on failure — continue as we are
return $args;
}

/**
Expand Down
Loading
Loading