Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TASK: Separate Booting\Scripts and CLI subrequest calls #3372

Open
wants to merge 1 commit into
base: 9.0
Choose a base branch
from
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
4 changes: 2 additions & 2 deletions Neos.Flow/Classes/Command/ServerCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Cli\CommandController;
use Neos\Flow\Core\Booting\Scripts;
use Neos\Flow\Core\PhpCliCommandHandler;

/**
* Command controller for starting the development-server
Expand Down Expand Up @@ -43,7 +43,7 @@ class ServerCommandController extends CommandController
*/
public function runCommand(string $host = '127.0.0.1', int $port = 8081)
{
$command = Scripts::buildPhpCommand($this->settings);
$command = PhpCliCommandHandler::buildAndValidatePhpCommand($this->settings);

$address = sprintf('%s:%s', $host, $port);
$command .= ' -S ' . escapeshellarg($address) . ' -t ' . escapeshellarg(FLOW_PATH_WEB) . ' ' . escapeshellarg(FLOW_PATH_FLOW . '/Scripts/PhpDevelopmentServerRouter.php');
Expand Down
314 changes: 29 additions & 285 deletions Neos.Flow/Classes/Core/Booting/Scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
use Neos\Flow\Configuration\Loader\SettingsLoader;
use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException;
use Neos\Flow\Configuration\Source\YamlSource;
use Neos\Flow\Core\Booting\Exception\SubProcessException;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Core\ClassLoader;
use Neos\Flow\Core\LockManager as CoreLockManager;
use Neos\Flow\Core\PhpCliCommandHandler;
use Neos\Flow\Core\ProxyClassLoader;
use Neos\Flow\Error\Debugger;
use Neos\Flow\Error\ErrorHandler;
Expand All @@ -48,6 +50,7 @@
use Neos\Flow\ResourceManagement\Streams\StreamWrapperAdapter;
use Neos\Flow\SignalSlot\Dispatcher;
use Neos\Flow\Utility\Environment;
use Neos\Utility\Exception\FilesException;
use Neos\Utility\Files;
use Neos\Utility\OpcodeCacheHelper;
use Neos\Flow\Exception as FlowException;
Expand All @@ -61,9 +64,6 @@
*/
class Scripts
{
/** @var string|null */
protected static $builtPhpCommand = null;

/**
* Initializes the Class Loader
*
Expand Down Expand Up @@ -405,7 +405,7 @@ public static function initializeProxyClasses(Bootstrap $bootstrap)
// The compile sub command will only be run if the code cache is completely empty:
OpcodeCacheHelper::clearAllActive(FLOW_PATH_CONFIGURATION);
OpcodeCacheHelper::clearAllActive(FLOW_PATH_DATA);
self::executeCommand('neos.flow:core:compile', $settings);
PhpCliCommandHandler::executeCommand('neos.flow:core:compile', $settings);
if (isset($settings['persistence']['doctrine']['enable']) && $settings['persistence']['doctrine']['enable'] === true) {
self::compileDoctrineProxies($bootstrap);
}
Expand Down Expand Up @@ -666,7 +666,7 @@ protected static function compileDoctrineProxies(Bootstrap $bootstrap)
$logger = $bootstrap->getEarlyInstance(PsrLoggerFactoryInterface::class)->get('systemLogger');
$coreCache->set('doctrineSetupRunning', 'White Russian', [], 60);
$logger->debug('Compiling Doctrine proxies');
self::executeCommand('neos.flow:doctrine:compileproxies', $settings);
PhpCliCommandHandler::executeCommand('neos.flow:doctrine:compileproxies', $settings);
$coreCache->remove('doctrineSetupRunning');
$objectConfigurationCache->set('doctrineProxyCodeUpToDate', true);
}
Expand All @@ -683,53 +683,34 @@ public static function initializeResources(Bootstrap $bootstrap)
StreamWrapperAdapter::initializeStreamWrapper($bootstrap->getObjectManager());
}

/**
* Check if the old fallback classloader should be used.
*
* The old class loader is used only in a testing context.
*
* @param Bootstrap $bootstrap
* @return bool
*/
protected static function useClassLoader(Bootstrap $bootstrap)
{
return $bootstrap->getContext()->isTesting();
}

/**
* Executes the given command as a sub-request to the Flow CLI system.
*
* @param string $commandIdentifier E.g. neos.flow:cache:flush
* @param array $settings The Neos.Flow settings
* @param array<string, mixed> $settings The Neos.Flow settings
* @param boolean $outputResults Echo the commands output on success
* @param array $commandArguments Command arguments
* @param array<string, string> $commandArguments Command arguments
* @return true Legacy return value. Will always be true. A failure is expressed as a thrown exception
* @throws Exception\SubProcessException The execution of the sub process failed
* @api
* @throws SubProcessException The execution of the sub process failed
* @throws FilesException
* @deprecated Use {@see PhpCliCommandHandler::executeCommand()}
*/
public static function executeCommand(string $commandIdentifier, array $settings, bool $outputResults = true, array $commandArguments = []): bool
{
$command = self::buildSubprocessCommand($commandIdentifier, $settings, $commandArguments);
// Output errors in response
$command .= ' 2>&1';
$output = [];
exec($command, $output, $result);
if ($result !== 0) {
if (count($output) > 0) {
$exceptionMessage = implode(PHP_EOL, $output);
} else {
$exceptionMessage = sprintf('Execution of subprocess failed with exit code %d without any further output. (Please check your PHP error log for possible Fatal errors)', $result);

// If the command is too long, it'll just produce /usr/bin/php: Argument list too long but this will be invisible
// If anything else goes wrong, it may as well not produce any $output, but might do so when run on an interactive
// shell. Thus we dump the command next to the exception dumps.
$exceptionMessage .= ' Try to run the command manually, to hopefully get some hint on the actual error.';
if (!file_exists(FLOW_PATH_DATA . 'Logs/Exceptions')) {
Files::createDirectoryRecursively(FLOW_PATH_DATA . 'Logs/Exceptions');
}
if (file_exists(FLOW_PATH_DATA . 'Logs/Exceptions') && is_dir(FLOW_PATH_DATA . 'Logs/Exceptions') && is_writable(FLOW_PATH_DATA . 'Logs/Exceptions')) {
// Logs the command string `php ./flow foo:bar` inside `Logs/Exceptions/123-command.txt`
$referenceCode = date('YmdHis', $_SERVER['REQUEST_TIME']) . substr(md5(rand()), 0, 6);
$errorDumpPathAndFilename = FLOW_PATH_DATA . 'Logs/Exceptions/' . $referenceCode . '-command.txt';
file_put_contents($errorDumpPathAndFilename, $command);
$exceptionMessage .= sprintf(' It has been stored in: %s', basename($errorDumpPathAndFilename));
} else {
$exceptionMessage .= sprintf(' (could not write command into %s because the directory could not be created or is not writable.)', FLOW_PATH_DATA . 'Logs/Exceptions/');
}
}
throw new Exception\SubProcessException($exceptionMessage, 1355480641);
}
if ($outputResults) {
echo implode(PHP_EOL, $output);
}
// Legacy return value
PhpCliCommandHandler::executeCommand($commandIdentifier, $settings, $outputResults, $commandArguments);
return true;
}

Expand All @@ -739,250 +720,13 @@ public static function executeCommand(string $commandIdentifier, array $settings
* Note: As the command execution is done in a separate thread potential exceptions or failures will *not* be reported
*
* @param string $commandIdentifier E.g. neos.flow:cache:flush
* @param array $settings The Neos.Flow settings
* @param array $commandArguments Command arguments
* @param array $settings<string, mixed> The Neos.Flow settings
* @param array<string, string> $commandArguments Command arguments
* @return void
* @api
*/
public static function executeCommandAsync(string $commandIdentifier, array $settings, array $commandArguments = [])
{
$command = self::buildSubprocessCommand($commandIdentifier, $settings, $commandArguments);
if (DIRECTORY_SEPARATOR === '/') {
exec($command . ' > /dev/null 2>/dev/null &');
} else {
pclose(popen('START /B CMD /S /C "' . $command . '" > NUL 2> NUL &', 'r'));
}
}

/**
* @param string $commandIdentifier E.g. neos.flow:cache:flush
* @param array $settings The Neos.Flow settings
* @param array $commandArguments Command arguments
* @return string A command line command ready for being exec()uted
*/
protected static function buildSubprocessCommand(string $commandIdentifier, array $settings, array $commandArguments = []): string
{
$command = self::buildPhpCommand($settings);

if (isset($settings['core']['subRequestIniEntries']) && is_array($settings['core']['subRequestIniEntries'])) {
foreach ($settings['core']['subRequestIniEntries'] as $entry => $value) {
$command .= ' -d ' . escapeshellarg($entry);
if (trim($value) !== '') {
$command .= '=' . escapeshellarg(trim($value));
}
}
}

$escapedArguments = '';
foreach ($commandArguments as $argument => $argumentValue) {
$argumentValue = trim($argumentValue);
$escapedArguments .= ' ' . escapeshellarg('--' . trim($argument)) . ($argumentValue !== '' ? '=' . escapeshellarg($argumentValue) : '');
}

$command .= sprintf(' %s %s %s', escapeshellarg(FLOW_PATH_FLOW . 'Scripts/flow.php'), escapeshellarg($commandIdentifier), trim($escapedArguments));

return trim($command);
}

/**
* @param array $settings The Neos.Flow settings
* @return string A command line command for PHP, which can be extended and then exec()uted
* @throws Exception\SubProcessException in case the phpBinaryPathAndFilename is incorrect
*/
public static function buildPhpCommand(array $settings): string
{
if (isset(static::$builtPhpCommand)) {
return static::$builtPhpCommand;
}

$subRequestEnvironmentVariables = [
'FLOW_ROOTPATH' => FLOW_PATH_ROOT,
'FLOW_PATH_TEMPORARY_BASE' => FLOW_PATH_TEMPORARY_BASE,
'FLOW_CONTEXT' => $settings['core']['context']
];
if (isset($settings['core']['subRequestEnvironmentVariables'])) {
$subRequestEnvironmentVariables = array_merge($subRequestEnvironmentVariables, $settings['core']['subRequestEnvironmentVariables']);
}

static::ensureCLISubrequestsUseCurrentlyRunningPhpBinary($settings['core']['phpBinaryPathAndFilename']);

$command = '';
foreach ($subRequestEnvironmentVariables as $argumentKey => $argumentValue) {
if (DIRECTORY_SEPARATOR === '/') {
$command .= sprintf('%s=%s ', $argumentKey, escapeshellarg($argumentValue));
} else {
// SET does not parse out quotes, hence we need escapeshellcmd here instead
$command .= sprintf('SET %s=%s&', $argumentKey, escapeshellcmd($argumentValue));
}
}
if (DIRECTORY_SEPARATOR === '/') {
$phpBinaryPathAndFilename = '"' . escapeshellcmd(Files::getUnixStylePath($settings['core']['phpBinaryPathAndFilename'])) . '"';
} else {
$phpBinaryPathAndFilename = escapeshellarg(Files::getUnixStylePath($settings['core']['phpBinaryPathAndFilename']));
}
$command .= $phpBinaryPathAndFilename;
if (!isset($settings['core']['subRequestPhpIniPathAndFilename']) || $settings['core']['subRequestPhpIniPathAndFilename'] !== false) {
if (!isset($settings['core']['subRequestPhpIniPathAndFilename'])) {
$useIniFile = php_ini_loaded_file();
} else {
$useIniFile = $settings['core']['subRequestPhpIniPathAndFilename'];
}
$command .= ' -c ' . escapeshellarg($useIniFile);
}

static::ensureWebSubrequestsUseCurrentlyRunningPhpVersion($command);

return static::$builtPhpCommand = $command;
}

/**
* Compares the realpath of the configured PHP binary (if any) with the one flow was called with in a CLI request.
* This avoids config errors where users forget to set Neos.Flow.core.phpBinaryPathAndFilename in CLI.
*
* @param string $phpBinaryPathAndFilename
* @throws Exception\SubProcessException in case the php binary doesn't exist / is a different one for the current cli request
* @deprecated Use {@see PhpCliCommandHandler::executeCommandAsync()}
*/
protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpBinaryPathAndFilename)
public static function executeCommandAsync(string $commandIdentifier, array $settings, array $commandArguments = []): void
{
// Do nothing for non-CLI requests
if (PHP_SAPI !== 'cli') {
return;
}

// Ensure the actual PHP binary is known before checking if it is correct.
if (!$phpBinaryPathAndFilename || strlen($phpBinaryPathAndFilename) === 0) {
throw new Exception\SubProcessException('"Neos.Flow.core.phpBinaryPathAndFilename" is not set.', 1689676816060);
}

$command = [];
if (PHP_OS_FAMILY !== 'Windows') {
// Handle possible fast cgi: send empty stdin to close possible fast cgi server
//
// in case the phpBinaryPathAndFilename points to a fast cgi php binary we will get caught in an endless process
// the fast cgi will expect input from the stdin and otherwise continue listening
// to close the stdin we send an empty string
// related https://bugs.php.net/bug.php?id=71209
$command[] = 'echo "" | ';
}
$command[] = $phpBinaryPathAndFilename;
$command[] = <<<'EOF'
-r "echo realpath(PHP_BINARY);"
EOF;
$command[] = '2>&1'; // Output errors in response

// Try to resolve which binary file PHP is pointing to
$output = [];
exec(join(' ', $command), $output, $result);

if ($result === 0 && count($output) === 1) {
// Resolve any wrapper
$configuredPhpBinaryPathAndFilename = $output[0];
} else {
// Resolve any symlinks that the configured php might be pointing to
$configuredPhpBinaryPathAndFilename = realpath($phpBinaryPathAndFilename);
}

// if the configured PHP binary is empty here, the file does not exist.
if ($configuredPhpBinaryPathAndFilename === false || strlen($configuredPhpBinaryPathAndFilename) === 0) {
throw new Exception\SubProcessException(
sprintf('The configured PHP binary "%s" via setting the "Neos.Flow.core.phpBinaryPathAndFilename" doesnt exist.', $phpBinaryPathAndFilename),
1689676923331
);
}

// stfu to avoid possible open_basedir restriction https://github.com/neos/flow-development-collection/pull/2491
$realPhpBinary = @realpath(PHP_BINARY);
if ($realPhpBinary === false) {
// bypass with exec open_basedir restriction
$output = [];
exec(PHP_BINARY . ' -r "echo realpath(PHP_BINARY);"', $output);
$realPhpBinary = $output[0];
}
if (strcmp($realPhpBinary, $configuredPhpBinaryPathAndFilename) !== 0) {
throw new Exception\SubProcessException(sprintf(
'You are running the Flow CLI with a PHP binary different from the one Flow is configured to use internally. ' .
'Flow has been run with "%s", while the PHP version Flow is configured to use for subrequests is "%s". Make sure to configure Flow to ' .
'use the same PHP binary by setting the "Neos.Flow.core.phpBinaryPathAndFilename" configuration option to "%s". Flush the ' .
'caches by removing the folder Data/Temporary before running ./flow again.',
$realPhpBinary,
$configuredPhpBinaryPathAndFilename,
$realPhpBinary
), 1536303119);
}
}

/**
* Compares the actual version of the configured PHP binary (if any) with the one flow was called with in a non-CLI request.
* This avoids config errors where users forget to set Neos.Flow.core.phpBinaryPathAndFilename in connection with a web
* server.
*
* @param string $phpCommand the completely build php string that is used to execute subrequests
* @throws Exception\SubProcessException in case the php binary doesn't exist, or is not suitable for cli usage, or its version doesn't match
*/
protected static function ensureWebSubrequestsUseCurrentlyRunningPhpVersion($phpCommand)
{
// Do nothing for CLI requests
if (PHP_SAPI === 'cli') {
return;
}

$command = [];
if (PHP_OS_FAMILY !== 'Windows') {
// Handle possible fast cgi: send empty stdin to close possible fast cgi server
//
// in case the phpBinaryPathAndFilename points to a fast cgi php binary we will get caught in an endless process
// the fast cgi will expect input from the stdin and otherwise continue listening
// to close the stdin we send an empty string
// related https://bugs.php.net/bug.php?id=71209
$command[] = 'echo "" | ';
}
$command[] = $phpCommand;
$command[] = <<<'EOF'
-r "echo json_encode(['sapi' => PHP_SAPI, 'version' => PHP_VERSION]);"
EOF;
$command[] = '2>&1'; // Output errors in response

exec(join(' ', $command), $output, $result);

$phpInformation = json_decode($output[0] ?? '{}', true) ?: [];

if ($result !== 0 || ($phpInformation['sapi'] ?? null) !== 'cli') {
throw new Exception\SubProcessException(sprintf('PHP binary might not exist or is not suitable for cli usage. Command `%s` didnt succeed.', $phpCommand), 1689676967447);
}

/**
* Checks if two (php) versions equal by comparing major and minor.
* Differences in the patch level will be ignored.
*
* versionsAlmostEqual(8.1.0, 8.1.1) === true
*/
$versionsAlmostEqual = function (string $oneVersion, string $otherVersion): bool {
return array_slice(explode('.', $oneVersion), 0, 2) === array_slice(explode('.', $otherVersion), 0, 2);
};

if (!$versionsAlmostEqual($phpInformation['version'], PHP_VERSION)) {
throw new FlowException(sprintf(
'You are executing Neos/Flow with a PHP version different from the one Flow is configured to use internally. ' .
'Flow is running with with PHP "%s", while the PHP version Flow is configured to use for subrequests is "%s". Make sure to configure Flow to ' .
'use the same PHP version by setting the "Neos.Flow.core.phpBinaryPathAndFilename" configuration option to a PHP-CLI binary of the version ' .
'%s. Flush the caches by removing the folder Data/Temporary before executing Flow/Neos again.',
PHP_VERSION,
$phpInformation['version'],
PHP_VERSION
), 1536563428);
}
}

/**
* Check if the old fallback classloader should be used.
*
* The old class loader is used only in a testing context.
*
* @param Bootstrap $bootstrap
* @return bool
*/
protected static function useClassLoader(Bootstrap $bootstrap)
{
return $bootstrap->getContext()->isTesting();
PhpCliCommandHandler::executeCommandAsync($commandIdentifier, $settings, $commandArguments);
}
}
Loading
Loading