Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2576,6 +2576,17 @@ jobs:
fi
php check.php restarted-for=xdebug,opcache xdebug-armed=no xdebug-loaded=yes processes=2+ starts=2

# xdebug armed in php.ini, disarmed with `php -d xdebug.mode=off`:
# nothing to restart for, and the spawned workers do not inherit the
# -d flag - ProcessHelper has to pass the switch on, or they run with
# the php.ini's xdebug.mode
- operating-system: "windows-latest"
coverage: "xdebug"
extensions: ""
script: |
php -d xdebug.mode=off ../../bin/phpstan analyse --no-progress
php check.php restarted-for= xdebug-armed=no xdebug-loaded=yes processes=2+ starts=1

# XDEBUG_MODE overrides xdebug.mode, so flagging the ini alone would
# be silently overridable - the row exports the env var too
- operating-system: "ubuntu-latest"
Expand Down
3 changes: 2 additions & 1 deletion src/Parallel/ForkParallelChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\Diagnose\DiagnoseExtension;
use PHPStan\Process\ProcessHelper;
use PHPStan\Process\Restart\OpcacheSettings;
use PHPStan\Process\Restart\RestartExtensionRegistry;
use PHPStan\Turbo\TurboExtensionEnabler;
use function function_exists;
use function getmypid;
Expand Down Expand Up @@ -72,7 +73,7 @@ public function print(Output $output): void
// (see ProcessHelper); the extension path is on the turbo lines
$parentPid = getmypid();
$output->writeLineFormatted('Worker -d entries:');
foreach (ProcessHelper::resolveWorkerIniEntries(OpcacheSettings::getIniEntries(), PHP_OS_FAMILY, $parentPid === false ? 0 : $parentPid, 1) as $iniEntry) {
foreach (ProcessHelper::resolveWorkerIniEntries(OpcacheSettings::getIniEntries(), RestartExtensionRegistry::getStartupSwitches(), PHP_OS_FAMILY, $parentPid === false ? 0 : $parentPid, 1) as $iniEntry) {
$output->writeLineFormatted(sprintf(
' %s%s',
$iniEntry,
Expand Down
18 changes: 15 additions & 3 deletions src/Process/ProcessHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Command\AnalyseCommand;
use PHPStan\Process\Restart\OpcacheSettings;
use PHPStan\Process\Restart\RestartExtensionRegistry;
use PHPStan\Process\Restart\RestartManager;
use PHPStan\Turbo\TurboExtensionSelector;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -76,9 +77,10 @@ public static function getWorkerCommand(

$parentPid = getmypid();
self::$spawnedWorkerCount++;
foreach (self::resolveWorkerIniEntries(OpcacheSettings::getIniEntries(), PHP_OS_FAMILY, $parentPid === false ? 0 : $parentPid, self::$spawnedWorkerCount) as $iniEntry) {
foreach (self::resolveWorkerIniEntries(OpcacheSettings::getIniEntries(), RestartExtensionRegistry::getStartupSwitches(), PHP_OS_FAMILY, $parentPid === false ? 0 : $parentPid, self::$spawnedWorkerCount) as $iniEntry) {
$processCommandArray[] = '-d';
$processCommandArray[] = $iniEntry;
// xdebug.mode can be a list with spaces in it
$processCommandArray[] = escapeshellarg($iniEntry);
}

foreach ([$mainScript, $commandName] as $arg) {
Expand Down Expand Up @@ -154,18 +156,28 @@ public static function getWorkerCommand(
* model spawned workers have elsewhere, where the segment is an anonymous
* mapping private to the process.
*
* The startup switches of xdebug, pcov and uopz are repeated as the
* spawning process has them. Where it got them through `-d` - `php -d
* xdebug.mode=off` with xdebug armed in the php.ini - it had nothing to
* restart for, and the worker would otherwise run with the php.ini's
* armed extension.
*
* The restart marker closes the loop: the worker's configuration is
* decided here, so the worker must not restart itself.
*
* @param list<string> $opcacheArgs OpcacheSettings::getIniEntries() of the spawning process
* @param array<string, string> $startupSwitches RestartExtensionRegistry::getStartupSwitches() of the spawning process
* @return list<string>
*/
public static function resolveWorkerIniEntries(array $opcacheArgs, string $osFamily, int $parentPid, int $workerNumber): array
public static function resolveWorkerIniEntries(array $opcacheArgs, array $startupSwitches, string $osFamily, int $parentPid, int $workerNumber): array
{
$entries = $opcacheArgs;
if ($entries !== [] && $osFamily === 'Windows') {
$entries[] = sprintf('opcache.cache_id=phpstan-%d-%d', $parentPid, $workerNumber);
}
foreach ($startupSwitches as $name => $value) {
$entries[] = sprintf('%s=%s', $name, $value);
}
$entries[] = RestartManager::RESTARTED_INI . '=1';

return $entries;
Expand Down
34 changes: 25 additions & 9 deletions src/Process/Restart/RestartExtensionRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,7 @@ final class RestartExtensionRegistry
*/
public static function collectReasons(bool $allowXdebug, ?string $pharPath = null): array
{
$iniSettings = [];
foreach (['xdebug.mode', 'pcov.enabled', 'uopz.disable'] as $name) {
$value = ini_get($name);
if ($value === false) {
continue;
}

$iniSettings[$name] = $value;
}
$iniSettings = self::getStartupSwitches();

// XDEBUG_MODE wins over the ini file, so the effective mode - not the
// configured one - is what decides. Resolved here rather than in
Expand All @@ -120,6 +112,30 @@ public static function collectReasons(bool $allowXdebug, ?string $pharPath = nul
);
}

/**
* The startup switches of the flag rows (xdebug.mode, pcov.enabled,
* uopz.disable) as the current process has them, for each extension that
* is loaded. A switch set with `-d` - `php -d xdebug.mode=off` - leaves
* nothing to restart for, and is not inherited by spawned workers either,
* so ProcessHelper passes these on.
*
* @return array<string, string>
*/
public static function getStartupSwitches(): array
{
$iniSettings = [];
foreach (['xdebug.mode', 'pcov.enabled', 'uopz.disable'] as $name) {
$value = ini_get($name);
if ($value === false) {
continue;
}

$iniSettings[$name] = $value;
}

return $iniSettings;
}

/**
* @param list<string> $loadedExtensions
* @param array<string, string> $iniSettings
Expand Down
7 changes: 7 additions & 0 deletions src/Process/Restart/RestartManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,13 @@ private static function getPassedThroughArguments(): array
$arguments[] = '-d';
$arguments[] = 'memory_limit=' . ini_get('memory_limit');

// `php -d xdebug.mode=off` left nothing to restart for, and the
// restarted process must not get the php.ini's armed xdebug back
foreach (RestartExtensionRegistry::getStartupSwitches() as $name => $value) {
$arguments[] = '-d';
$arguments[] = $name . '=' . $value;
}

return $arguments;
}

Expand Down
36 changes: 24 additions & 12 deletions tests/PHPStan/Process/ProcessHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Process;

use PHPStan\Process\Restart\OpcacheSettings;
use PHPStan\Process\Restart\RestartExtensionRegistry;
use PHPStan\Process\Restart\RestartManager;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
Expand All @@ -19,26 +20,33 @@ final class ProcessHelperTest extends TestCase
{

/**
* @return iterable<string, array{list<string>, string, list<string>}>
* @return iterable<string, array{list<string>, array<string, string>, string, list<string>}>
*/
public static function dataResolveWorkerIniEntries(): iterable
{
$opcacheArgs = ['opcache.enable=1', 'opcache.enable_cli=1'];
yield 'linux' => [$opcacheArgs, 'Linux', ['opcache.enable=1', 'opcache.enable_cli=1', 'phpstan.restarted=1']];
yield 'darwin' => [$opcacheArgs, 'Darwin', ['opcache.enable=1', 'opcache.enable_cli=1', 'phpstan.restarted=1']];
yield 'windows' => [$opcacheArgs, 'Windows', ['opcache.enable=1', 'opcache.enable_cli=1', 'opcache.cache_id=phpstan-4242-7', 'phpstan.restarted=1']];
yield 'linux without OPcache' => [[], 'Linux', ['phpstan.restarted=1']];
yield 'windows without OPcache' => [[], 'Windows', ['phpstan.restarted=1']];
yield 'linux' => [$opcacheArgs, [], 'Linux', ['opcache.enable=1', 'opcache.enable_cli=1', 'phpstan.restarted=1']];
yield 'darwin' => [$opcacheArgs, [], 'Darwin', ['opcache.enable=1', 'opcache.enable_cli=1', 'phpstan.restarted=1']];
yield 'windows' => [$opcacheArgs, [], 'Windows', ['opcache.enable=1', 'opcache.enable_cli=1', 'opcache.cache_id=phpstan-4242-7', 'phpstan.restarted=1']];
yield 'linux without OPcache' => [[], [], 'Linux', ['phpstan.restarted=1']];
yield 'windows without OPcache' => [[], [], 'Windows', ['phpstan.restarted=1']];
yield 'windows with xdebug and pcov switched off by -d' => [
$opcacheArgs,
['xdebug.mode' => 'off', 'pcov.enabled' => '0'],
'Windows',
['opcache.enable=1', 'opcache.enable_cli=1', 'opcache.cache_id=phpstan-4242-7', 'xdebug.mode=off', 'pcov.enabled=0', 'phpstan.restarted=1'],
];
}

/**
* @param list<string> $opcacheArgs
* @param array<string, string> $startupSwitches
* @param list<string> $expected
*/
#[DataProvider('dataResolveWorkerIniEntries')]
public function testResolveWorkerIniEntries(array $opcacheArgs, string $osFamily, array $expected): void
public function testResolveWorkerIniEntries(array $opcacheArgs, array $startupSwitches, string $osFamily, array $expected): void
{
$this->assertSame($expected, ProcessHelper::resolveWorkerIniEntries($opcacheArgs, $osFamily, 4242, 7));
$this->assertSame($expected, ProcessHelper::resolveWorkerIniEntries($opcacheArgs, $startupSwitches, $osFamily, 4242, 7));
}

public function testWorkerCommandCarriesTheIniEntries(): void
Expand All @@ -48,12 +56,16 @@ public function testWorkerCommandCarriesTheIniEntries(): void

$opcacheArgs = OpcacheSettings::getIniEntries();
foreach ($opcacheArgs as $opcacheArg) {
$this->assertStringContainsString(sprintf(' -d %s ', $opcacheArg), $command);
$this->assertStringContainsString(sprintf(' -d %s ', escapeshellarg($opcacheArg)), $command);
}

foreach (RestartExtensionRegistry::getStartupSwitches() as $name => $value) {
$this->assertStringContainsString(sprintf(' -d %s ', escapeshellarg($name . '=' . $value)), $command);
}

// the restart marker is the last -d, right before the script
$this->assertStringContainsString(
sprintf(' -d %s=1 %s %s ', RestartManager::RESTARTED_INI, escapeshellarg('bin/phpstan'), escapeshellarg('worker')),
sprintf(' -d %s %s %s ', escapeshellarg(RestartManager::RESTARTED_INI . '=1'), escapeshellarg('bin/phpstan'), escapeshellarg('worker')),
$command,
);

Expand All @@ -63,8 +75,8 @@ public function testWorkerCommandCarriesTheIniEntries(): void
}

// every Windows worker gets its own OPcache instance
$this->assertSame(1, preg_match('~ -d opcache\.cache_id=(phpstan-\d+-\d+) ~', $command, $matches));
$this->assertSame(1, preg_match('~ -d opcache\.cache_id=(phpstan-\d+-\d+) ~', $commandAgain, $matchesAgain));
$this->assertSame(1, preg_match('~ -d [\'"]?opcache\.cache_id=(phpstan-\d+-\d+)[\'"]? ~', $command, $matches));
$this->assertSame(1, preg_match('~ -d [\'"]?opcache\.cache_id=(phpstan-\d+-\d+)[\'"]? ~', $commandAgain, $matchesAgain));
$this->assertNotSame($matches[1], $matchesAgain[1]);
}

Expand Down
Loading