From 64d29e9ee38e9fd54118a8f5d4907ee634de17d3 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Tue, 20 Oct 2026 22:47:18 +0200 Subject: [PATCH 1/5] Restart once for Turbo, OPcache and Xdebug When Xdebug, pcov or tideways has to go, TurboProcessRestarter now hands its -d entries to ProcessRestarter. That writes the ini without those extensions and replaces the process with pcntl_exec(), so the process restarts once, before the console application starts. Before, PHPStan exec'd for Turbo, booted to CommandHelper with Xdebug still loaded, spawned a child without Xdebug, and that child exec'd again for Turbo. Nobody waits for the exec, so the main process removes the ini on shutdown. Without pcntl, CommandHelper still restarts in a child. Co-Authored-By: Claude Opus 5.5 (1M context) --- bin/phpstan | 6 +- src/Process/ProcessRestarter.php | 31 ++++++++ src/Turbo/TurboProcessRestarter.php | 106 ++++++++++++++++++++++++---- 3 files changed, 127 insertions(+), 16 deletions(-) diff --git a/bin/phpstan b/bin/phpstan index 5280bed96aa..134a3f1666b 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -28,12 +28,14 @@ use Symfony\Component\Console\Helper\ProgressBar; require_once __DIR__ . '/../src/Turbo/TurboExtensionEnabler.php'; require_once __DIR__ . '/../src/Turbo/TurboExtensionSelector.php'; require_once __DIR__ . '/../src/Turbo/TurboProcessRestarter.php'; - TurboProcessRestarter::restartIfSuitable($_SERVER['argv']); + TurboProcessRestarter::restartIfSuitable($_SERVER['argv'], null, __DIR__ . '/../vendor/autoload.php'); + TurboProcessRestarter::removeTmpIniOnShutdown(); TurboExtensionEnabler::trustOwnTypesIfSuitable($_SERVER['argv']); $analysisStartTime = microtime(true); - $devOrPharLoader = require_once __DIR__ . '/../vendor/autoload.php'; + // not require_once: the restart above may have loaded it already + $devOrPharLoader = require __DIR__ . '/../vendor/autoload.php'; TurboExtensionEnabler::activateIfCompatible(); require_once __DIR__ . '/../preload.php'; $cwd = getcwd(); diff --git a/src/Process/ProcessRestarter.php b/src/Process/ProcessRestarter.php index ca767701803..b8d8b847d14 100644 --- a/src/Process/ProcessRestarter.php +++ b/src/Process/ProcessRestarter.php @@ -4,11 +4,16 @@ use Composer\XdebugHandler\XdebugHandler; use Override; +use PHPStan\Turbo\TurboProcessRestarter; +use function array_merge; +use function array_slice; use function count; use function extension_loaded; use function file_get_contents; use function file_put_contents; +use function function_exists; use function ini_get; +use function pcntl_exec; use function preg_replace; use const PHP_EOL; @@ -19,6 +24,22 @@ final class ProcessRestarter extends XdebugHandler /** @var array */ private array $extensions = []; + /** @var list|null */ + private ?array $execIniArgs = null; + + /** + * Replaces this process via pcntl_exec() instead of waiting for a child, + * with these `-d` entries on top of the stripped ini. TurboProcessRestarter + * passes its own entries, so one restart covers both of them. + * + * @param list $iniArgs `name=value` entries + */ + public function setExecIniArgs(array $iniArgs): self + { + $this->execIniArgs = $iniArgs; + return $this; + } + #[Override] protected function requiresRestart(bool $default): bool { @@ -50,6 +71,16 @@ protected function restart(array $command): void } } + if ($this->execIniArgs !== null && function_exists('pcntl_exec')) { + $args = []; + foreach (array_merge($this->execIniArgs, [TurboProcessRestarter::RESTART_TMP_INI . '=' . $this->tmpIni]) as $iniArg) { + $args[] = '-d'; + $args[] = $iniArg; + } + pcntl_exec($command[0], array_merge($args, array_slice($command, 1))); + // pcntl_exec() returns only on failure, wait for a child instead + } + parent::restart($command); } diff --git a/src/Turbo/TurboProcessRestarter.php b/src/Turbo/TurboProcessRestarter.php index 8dab17bd1c8..87d4edad4f8 100644 --- a/src/Turbo/TurboProcessRestarter.php +++ b/src/Turbo/TurboProcessRestarter.php @@ -2,18 +2,25 @@ namespace PHPStan\Turbo; +use PHPStan\Process\ProcessRestarter; use function explode; use function extension_loaded; use function function_exists; use function get_cfg_var; +use function getenv; +use function getmypid; use function in_array; use function ini_get; use function is_string; use function max; use function pcntl_exec; use function php_ini_loaded_file; +use function register_shutdown_function; use function strtolower; +use function strtoupper; use function trim; +use function unlink; +use function xdebug_info; use const PHP_BINARY; /** @@ -59,6 +66,9 @@ final class TurboProcessRestarter /** Set by the restart, and by ProcessHelper on spawned workers, so the process never restarts itself */ public const RESTARTED_INI = 'phpstan.restarted'; + /** The ini that ProcessRestarter wrote for the exec, see removeTmpIniOnShutdown() */ + public const RESTART_TMP_INI = 'phpstan.restartTmpIni'; + /** Shared memory reserved (not touched) for the opcode cache, in MB — see resolveOpcacheArgs() for the sizing */ private const OPCACHE_MEMORY_CONSUMPTION_MB_LIMIT = 256; @@ -100,10 +110,18 @@ public static function getRestartExtensionPath(): ?string * Another program that loads PHPStan's phar, Rector for example, passes * the path to that phar: Phar::running() only knows the phar that runs. * + * When Xdebug, pcov or tideways has to go as well, the same exec does it: + * ProcessRestarter writes the ini without them and execs with the entries + * built here, so the process restarts once instead of three times. That + * path needs the Composer autoloader, which is fine for a process about + * to be replaced. $autoloadPath is required for it when given; a caller + * that already loaded its autoloader passes null. $appName is the + * xdebug-handler prefix of the caller's environment variables. + * * @api * @param list $argv */ - public static function restartIfSuitable(array $argv, ?string $pharPath = null): void + public static function restartIfSuitable(array $argv, ?string $pharPath = null, ?string $autoloadPath = null, string $appName = 'phpstan'): void { if (get_cfg_var(self::RESTARTED_INI) !== false) { // already restarted — whatever did not take effect (a binary that @@ -127,12 +145,36 @@ public static function restartIfSuitable(array $argv, ?string $pharPath = null): || !function_exists('posix_kill') ) { // fork mode is impossible here (see ForkParallelChecker) and - // spawned workers get the extension from ProcessHelper already + // spawned workers get the extension from ProcessHelper already; + // CommandHelper restarts without Xdebug in a child process return; } $extensionPath = extension_loaded('phpstan_turbo') ? null : TurboExtensionSelector::findExtension($pharPath); $opcacheArgs = self::getOpcacheArgs(); + + $iniArgs = ['memory_limit=' . ini_get('memory_limit')]; + foreach ($opcacheArgs as $opcacheArg) { + $iniArgs[] = $opcacheArg; + } + if ($extensionPath !== null) { + $iniArgs[] = 'extension=' . $extensionPath; + $iniArgs[] = self::EXTENSION_PATH_INI . '=' . $extensionPath; + } + $iniArgs[] = self::RESTARTED_INI . '=1'; + + if (self::mayNeedExtensionsRemoved($argv, $appName)) { + if ($autoloadPath !== null) { + require_once $autoloadPath; + } + $restarter = new ProcessRestarter($appName); + $restarter->setPersistent(); + $restarter->setExecIniArgs($iniArgs); + $restarter->check(); + // returns when ProcessRestarter decided against a restart + unset($restarter); + } + if ( $extensionPath === null && !self::resolveOpcacheRestartNeeded($opcacheArgs, self::getCurrentIniValues($opcacheArgs)) @@ -146,20 +188,10 @@ public static function restartIfSuitable(array $argv, ?string $pharPath = null): $args[] = '-c'; $args[] = $phpIni; } - $args[] = '-d'; - $args[] = 'memory_limit=' . ini_get('memory_limit'); - foreach ($opcacheArgs as $opcacheArg) { + foreach ($iniArgs as $iniArg) { $args[] = '-d'; - $args[] = $opcacheArg; + $args[] = $iniArg; } - if ($extensionPath !== null) { - $args[] = '-d'; - $args[] = 'extension=' . $extensionPath; - $args[] = '-d'; - $args[] = self::EXTENSION_PATH_INI . '=' . $extensionPath; - } - $args[] = '-d'; - $args[] = self::RESTARTED_INI . '=1'; foreach ($argv as $arg) { $args[] = $arg; } @@ -168,6 +200,52 @@ public static function restartIfSuitable(array $argv, ?string $pharPath = null): // pcntl_exec() returns only on failure — continue as we are } + /** + * Removes the ini that ProcessRestarter wrote for the exec once the main + * process ends. Nobody waits for the exec, so nobody else deletes it. + * Spawned workers read it through PHPRC until then. Forked workers + * inherit the shutdown function, so the pid check skips them. + * + * @api + */ + public static function removeTmpIniOnShutdown(): void + { + $tmpIni = get_cfg_var(self::RESTART_TMP_INI); + if (!is_string($tmpIni) || $tmpIni === '') { + return; + } + + $pid = getmypid(); + register_shutdown_function(static function () use ($tmpIni, $pid): void { + if (getmypid() !== $pid) { + return; + } + + @unlink($tmpIni); + }); + } + + /** + * A cheap pre-check without the autoloader. ProcessRestarter makes the + * real decision, for example about an Xdebug in mode "off". + * + * @param list $argv + */ + private static function mayNeedExtensionsRemoved(array $argv, string $appName): bool + { + if (in_array('--xdebug', $argv, true)) { + return false; + } + if ((bool) getenv(strtoupper($appName) . '_ALLOW_XDEBUG')) { + return false; + } + + // xdebug_info() exists since Xdebug 3.1, older versions count as active + return (extension_loaded('xdebug') && (!function_exists('xdebug_info') || xdebug_info('mode') !== [])) + || (extension_loaded('pcov') && ini_get('pcov.enabled') === '1') + || extension_loaded('tideways'); + } + /** * Whether the OPcache configuration the restart would set differs from * the one in effect — the second reason to restart, independent of turbo: From 12da374404533e595a2e86ecc1362d26a17ca9ec Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Tue, 20 Oct 2026 23:02:17 +0200 Subject: [PATCH 2/5] Remove the restart ini from a watchdog process Nobody waits for the exec, so a forked process takes the role of the waiting parent of a proc_open() restart. It ignores Ctrl-C, outlives the main process and then removes the ini. Before, a shutdown function removed it, which did not run when a signal ended the process. Co-Authored-By: Claude Opus 5.5 (1M context) --- bin/phpstan | 1 - src/Process/ProcessRestarter.php | 55 +++++++++++++++++++++++++++-- src/Turbo/TurboProcessRestarter.php | 31 ---------------- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/bin/phpstan b/bin/phpstan index 134a3f1666b..69a963ed2de 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -29,7 +29,6 @@ use Symfony\Component\Console\Helper\ProgressBar; require_once __DIR__ . '/../src/Turbo/TurboExtensionSelector.php'; require_once __DIR__ . '/../src/Turbo/TurboProcessRestarter.php'; TurboProcessRestarter::restartIfSuitable($_SERVER['argv'], null, __DIR__ . '/../vendor/autoload.php'); - TurboProcessRestarter::removeTmpIniOnShutdown(); TurboExtensionEnabler::trustOwnTypesIfSuitable($_SERVER['argv']); $analysisStartTime = microtime(true); diff --git a/src/Process/ProcessRestarter.php b/src/Process/ProcessRestarter.php index b8d8b847d14..c37e2d214eb 100644 --- a/src/Process/ProcessRestarter.php +++ b/src/Process/ProcessRestarter.php @@ -4,23 +4,39 @@ use Composer\XdebugHandler\XdebugHandler; use Override; -use PHPStan\Turbo\TurboProcessRestarter; use function array_merge; use function array_slice; use function count; use function extension_loaded; +use function fclose; use function file_get_contents; use function file_put_contents; use function function_exists; +use function getmypid; use function ini_get; use function pcntl_exec; +use function pcntl_fork; +use function pcntl_signal; +use function posix_getppid; use function preg_replace; +use function unlink; +use function usleep; use const PHP_EOL; +use const SIG_IGN; +use const SIGHUP; +use const SIGINT; +use const SIGQUIT; +use const SIGTERM; +use const STDERR; +use const STDIN; +use const STDOUT; // see https://github.com/composer/xdebug-handler/issues/174 final class ProcessRestarter extends XdebugHandler { + private const WATCHDOG_POLL_MICROSECONDS = 100_000; + /** @var array */ private array $extensions = []; @@ -40,6 +56,39 @@ public function setExecIniArgs(array $iniArgs): self return $this; } + /** + * Nobody waits for the exec, so a forked process takes the role of the + * waiting parent of a proc_open() restart: it ignores Ctrl-C, outlives the + * main process and then removes the ini. It ends by exec'ing rm, so PHP's + * shutdown sequence (shutdown functions, destructors, Xdebug output) never + * runs in it. + */ + private function startTmpIniWatchdog(): bool + { + $tmpIni = (string) $this->tmpIni; + $parentPid = getmypid(); + $pid = pcntl_fork(); + if ($pid === -1) { + return false; + } + if ($pid > 0) { + return true; + } + + foreach ([SIGINT, SIGTERM, SIGHUP, SIGQUIT] as $signal) { + pcntl_signal($signal, SIG_IGN); + } + fclose(STDIN); + fclose(STDOUT); + fclose(STDERR); + while (posix_getppid() === $parentPid) { + usleep(self::WATCHDOG_POLL_MICROSECONDS); + } + pcntl_exec('/bin/rm', ['-f', $tmpIni]); + @unlink($tmpIni); + exit(0); + } + #[Override] protected function requiresRestart(bool $default): bool { @@ -71,9 +120,9 @@ protected function restart(array $command): void } } - if ($this->execIniArgs !== null && function_exists('pcntl_exec')) { + if ($this->execIniArgs !== null && function_exists('pcntl_exec') && $this->startTmpIniWatchdog()) { $args = []; - foreach (array_merge($this->execIniArgs, [TurboProcessRestarter::RESTART_TMP_INI . '=' . $this->tmpIni]) as $iniArg) { + foreach ($this->execIniArgs as $iniArg) { $args[] = '-d'; $args[] = $iniArg; } diff --git a/src/Turbo/TurboProcessRestarter.php b/src/Turbo/TurboProcessRestarter.php index 87d4edad4f8..98aefca0df6 100644 --- a/src/Turbo/TurboProcessRestarter.php +++ b/src/Turbo/TurboProcessRestarter.php @@ -8,18 +8,15 @@ use function function_exists; use function get_cfg_var; use function getenv; -use function getmypid; use function in_array; use function ini_get; use function is_string; use function max; use function pcntl_exec; use function php_ini_loaded_file; -use function register_shutdown_function; use function strtolower; use function strtoupper; use function trim; -use function unlink; use function xdebug_info; use const PHP_BINARY; @@ -66,9 +63,6 @@ final class TurboProcessRestarter /** Set by the restart, and by ProcessHelper on spawned workers, so the process never restarts itself */ public const RESTARTED_INI = 'phpstan.restarted'; - /** The ini that ProcessRestarter wrote for the exec, see removeTmpIniOnShutdown() */ - public const RESTART_TMP_INI = 'phpstan.restartTmpIni'; - /** Shared memory reserved (not touched) for the opcode cache, in MB — see resolveOpcacheArgs() for the sizing */ private const OPCACHE_MEMORY_CONSUMPTION_MB_LIMIT = 256; @@ -200,31 +194,6 @@ public static function restartIfSuitable(array $argv, ?string $pharPath = null, // pcntl_exec() returns only on failure — continue as we are } - /** - * Removes the ini that ProcessRestarter wrote for the exec once the main - * process ends. Nobody waits for the exec, so nobody else deletes it. - * Spawned workers read it through PHPRC until then. Forked workers - * inherit the shutdown function, so the pid check skips them. - * - * @api - */ - public static function removeTmpIniOnShutdown(): void - { - $tmpIni = get_cfg_var(self::RESTART_TMP_INI); - if (!is_string($tmpIni) || $tmpIni === '') { - return; - } - - $pid = getmypid(); - register_shutdown_function(static function () use ($tmpIni, $pid): void { - if (getmypid() !== $pid) { - return; - } - - @unlink($tmpIni); - }); - } - /** * A cheap pre-check without the autoloader. ProcessRestarter makes the * real decision, for example about an Xdebug in mode "off". From 15836866b8c67bc21a261aa27c62c12f92e1ef94 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Tue, 20 Oct 2026 23:10:36 +0200 Subject: [PATCH 3/5] Print the restart status line on the exec path XdebugHandler prints "Process restarting" under XDEBUG_HANDLER_DEBUG in doRestart(), which the exec skips. The pcov e2e jobs look for that line. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Process/ProcessRestarter.php | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Process/ProcessRestarter.php b/src/Process/ProcessRestarter.php index c37e2d214eb..4338d0d11fc 100644 --- a/src/Process/ProcessRestarter.php +++ b/src/Process/ProcessRestarter.php @@ -12,13 +12,19 @@ use function file_get_contents; use function file_put_contents; use function function_exists; +use function fwrite; +use function getenv; use function getmypid; +use function implode; +use function in_array; use function ini_get; use function pcntl_exec; use function pcntl_fork; use function pcntl_signal; use function posix_getppid; use function preg_replace; +use function sprintf; +use function strtoupper; use function unlink; use function usleep; use const PHP_EOL; @@ -43,6 +49,14 @@ final class ProcessRestarter extends XdebugHandler /** @var list|null */ private ?array $execIniArgs = null; + private string $envAllowXdebugName; + + public function __construct(string $envPrefix) + { + parent::__construct($envPrefix); + $this->envAllowXdebugName = strtoupper($envPrefix) . self::SUFFIX_ALLOW; + } + /** * Replaces this process via pcntl_exec() instead of waiting for a child, * with these `-d` entries on top of the stripped ini. TurboProcessRestarter @@ -56,6 +70,29 @@ public function setExecIniArgs(array $iniArgs): self return $this; } + /** + * Prints the lines that XdebugHandler::doRestart() prints under + * XDEBUG_HANDLER_DEBUG, which the exec skips. Its Status class is internal, + * so the format is repeated here. + * + * @param non-empty-list $command + */ + private function reportRestarting(array $command): void + { + $debug = getenv(self::DEBUG); + if (in_array($debug, [false, ''], true)) { + return; + } + + $lines = [ + sprintf('Process restarting (%s=%s)', $this->envAllowXdebugName, (string) getenv($this->envAllowXdebugName)), + sprintf('Running: [%s]', implode(', ', $command)), + ]; + foreach ($lines as $line) { + fwrite(STDERR, sprintf('xdebug-handler[%d] %s%s', (int) getmypid(), $line, PHP_EOL)); + } + } + /** * Nobody waits for the exec, so a forked process takes the role of the * waiting parent of a proc_open() restart: it ignores Ctrl-C, outlives the @@ -126,6 +163,7 @@ protected function restart(array $command): void $args[] = '-d'; $args[] = $iniArg; } + $this->reportRestarting(array_merge([$command[0]], $args, array_slice($command, 1))); pcntl_exec($command[0], array_merge($args, array_slice($command, 1))); // pcntl_exec() returns only on failure, wait for a child instead } From 91eb784b795c67f92c45fa529c879e5a58955103 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Wed, 21 Oct 2026 08:48:16 +0200 Subject: [PATCH 4/5] Ignore xdebug_info in the dependency analysis when Xdebug is not loaded Co-Authored-By: Claude Opus 5.5 (1M context) --- build/composer-dependency-analyser.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php index ebaec5d9750..7a3e1c77e99 100644 --- a/build/composer-dependency-analyser.php +++ b/build/composer-dependency-analyser.php @@ -33,6 +33,13 @@ $unknownClasses[] = 'PHPStanTurbo\Runtime'; } +$unknownFunctions = []; +if (!function_exists('xdebug_info')) { + // provided by the Xdebug extension, which TurboProcessRestarter checks for; + // conditional for the same reason as PHPStanTurbo\Runtime above + $unknownFunctions[] = 'xdebug_info'; +} + return $config ->addPathToScan(__DIR__ . '/../bin', true) ->ignoreErrorsOnPackages( @@ -44,4 +51,5 @@ ) ->ignoreErrorsOnPackage('phpunit/phpunit', [ErrorType::DEV_DEPENDENCY_IN_PROD]) // prepared test tooling ->ignoreErrorsOnPath(__DIR__ . '/../tests', [ErrorType::UNKNOWN_CLASS, ErrorType::UNKNOWN_FUNCTION, ErrorType::SHADOW_DEPENDENCY]) // to be able to test invalid symbols - ->ignoreUnknownClasses($unknownClasses); + ->ignoreUnknownClasses($unknownClasses) + ->ignoreUnknownFunctions($unknownFunctions); From 772a45b956644b6552788710775bd471e0ce6031 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Wed, 21 Oct 2026 09:43:12 +0200 Subject: [PATCH 5/5] Test that one restart removes the coverage driver and enables OPcache An auto_prepend_file in php.ini writes one line for each start of the main analyse process. With pcov or Xdebug active and OPcache off for the CLI, the process must start twice, and the second start must have OPcache on and the coverage driver gone. Before the merged restarter it started four times. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/e2e-tests.yml | 19 +++++++++++++++++++ e2e/restart-once/phpstan.neon | 4 ++++ e2e/restart-once/start-log.php | 12 ++++++++++++ e2e/restart-once/test.php | 3 +++ 4 files changed, 38 insertions(+) create mode 100644 e2e/restart-once/phpstan.neon create mode 100644 e2e/restart-once/start-log.php create mode 100644 e2e/restart-once/test.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7945c6f431b..37ea9c0bb01 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -2052,6 +2052,25 @@ jobs: ../bashunit assert file_not_contains output.txt "Process restarting" coverage: "pcov" ini-values: "pcov.enabled=0" + - script: | + # One restart removes pcov and turns OPcache on: the main process starts twice, not more. + cd e2e/restart-once + echo "auto_prepend_file=$PWD/start-log.php" | sudo tee -a "$(php -r 'echo php_ini_loaded_file();')" + ../../bin/phpstan analyse --no-progress + cat starts.log + ../bashunit assert equals "2" "$(wc -l < starts.log | tr -d ' ')" + ../bashunit assert equals "opcache=1 pcov=0 xdebug=0" "$(tail -n 1 starts.log)" + coverage: "pcov" + ini-values: "pcov.enabled=1" + - script: | + # One restart removes Xdebug and turns OPcache on: the main process starts twice, not more. + cd e2e/restart-once + echo "auto_prepend_file=$PWD/start-log.php" | sudo tee -a "$(php -r 'echo php_ini_loaded_file();')" + ../../bin/phpstan analyse --no-progress + cat starts.log + ../bashunit assert equals "2" "$(wc -l < starts.log | tr -d ' ')" + ../bashunit assert equals "opcache=1 pcov=0 xdebug=0" "$(tail -n 1 starts.log)" + coverage: "xdebug" steps: - name: Harden the runner (Audit all outbound calls) diff --git a/e2e/restart-once/phpstan.neon b/e2e/restart-once/phpstan.neon new file mode 100644 index 00000000000..6b4b43534ee --- /dev/null +++ b/e2e/restart-once/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 0 + paths: + - test.php diff --git a/e2e/restart-once/start-log.php b/e2e/restart-once/start-log.php new file mode 100644 index 00000000000..c26e572702f --- /dev/null +++ b/e2e/restart-once/start-log.php @@ -0,0 +1,12 @@ +