diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 8b20142f4..4346c8116 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -28,4 +28,7 @@ */polyfills/Stringable.php */polyfills/WeakMap.php + + */tests/ElasticApmTests/ComponentTests/WordPress/mock_src/*.php + */tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/*.php diff --git a/phpstan.neon b/phpstan.neon index 72356fe70..7d58c1f03 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,6 +10,8 @@ parameters: excludePaths: - tests/polyfills/Stringable.php - tests/polyfills/WeakMap.php + - tests/ElasticApmTests/ComponentTests/WordPress/mock_src/*.php + - tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/*.php ignoreErrors: # diff --git a/src/ElasticApm/Impl/AutoInstrument/AutoInstrumentationBase.php b/src/ElasticApm/Impl/AutoInstrument/AutoInstrumentationBase.php index 54fbf06f9..918bf8c23 100644 --- a/src/ElasticApm/Impl/AutoInstrument/AutoInstrumentationBase.php +++ b/src/ElasticApm/Impl/AutoInstrument/AutoInstrumentationBase.php @@ -55,6 +55,14 @@ public function isEnabled(?string &$reason = null): bool return false; } + $isUserlandCodeInstrumentationEnabled = $this->tracer->getConfig()->astProcessEnabled(); + if ($this->requiresUserlandCodeInstrumentation() && (!$isUserlandCodeInstrumentationEnabled)) { + $reason = 'Instrumentation ' . $this->name() . ' needs userland code instrumentation' + . ' but AST-process is the only currently supported mechanism to instrument userland code and it is DISABLED' + . ' (via ' . OptionNames::AST_PROCESS_ENABLED . ' configuration option)'; + return false; + } + $disabledInstrumentationsMatcher = $this->tracer->getConfig()->disableInstrumentations(); if ($disabledInstrumentationsMatcher === null) { return true; @@ -85,6 +93,14 @@ public function requiresAttachContextToExternalObjects(): bool return false; } + /** + * @return bool + */ + public function requiresUserlandCodeInstrumentation(): bool + { + return false; + } + /** * @return string[] */ diff --git a/src/ElasticApm/Impl/AutoInstrument/Autoloader.php b/src/ElasticApm/Impl/AutoInstrument/Autoloader.php index 620e60186..0e8d864c0 100644 --- a/src/ElasticApm/Impl/AutoInstrument/Autoloader.php +++ b/src/ElasticApm/Impl/AutoInstrument/Autoloader.php @@ -83,7 +83,25 @@ public static function autoloadCodeForClass(string $fqClassName): void __LINE__, __FUNCTION__ ); + + /** + * elastic_apm_* functions are provided by the elastic_apm extension + * + * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection + * @phpstan-ignore-next-line + */ + \elastic_apm_before_loading_agent_php_code(); + require $classSrcFileAbsolute; + + /** + * elastic_apm_* functions are provided by the elastic_apm extension + * + * @noinspection PhpFullyQualifiedNameUsageInspection, PhpUndefinedFunctionInspection + * @phpstan-ignore-next-line + */ + \elastic_apm_after_loading_agent_php_code(); + BootstrapStageLogger::logTrace( "After require `$classSrcFileAbsolute' ...", __LINE__, diff --git a/src/ElasticApm/Impl/AutoInstrument/BuiltinPlugin.php b/src/ElasticApm/Impl/AutoInstrument/BuiltinPlugin.php index a0d3fa45d..6284b7cdc 100644 --- a/src/ElasticApm/Impl/AutoInstrument/BuiltinPlugin.php +++ b/src/ElasticApm/Impl/AutoInstrument/BuiltinPlugin.php @@ -32,6 +32,9 @@ */ final class BuiltinPlugin extends PluginBase { + /** @var ?WordPressAutoInstrumentation */ + private $wordPressAutoInstrumentationIfEnabled; + public function __construct(Tracer $tracer) { parent::__construct( @@ -42,10 +45,21 @@ public function __construct(Tracer $tracer) new MySQLiAutoInstrumentation($tracer), ] ); + + $wordPressAutoInstrumentation = new WordPressAutoInstrumentation($tracer); + $this->wordPressAutoInstrumentationIfEnabled = + $this->checkIfInstrumentationEnabled($wordPressAutoInstrumentation) + ? $wordPressAutoInstrumentation + : null; } public function getDescription(): string { return 'BUILT-IN'; } + + public function getWordPressAutoInstrumentationIfEnabled(): ?WordPressAutoInstrumentation + { + return $this->wordPressAutoInstrumentationIfEnabled; + } } diff --git a/src/ElasticApm/Impl/AutoInstrument/InstrumentationNames.php b/src/ElasticApm/Impl/AutoInstrument/InstrumentationNames.php index 938532455..18dcaf42c 100644 --- a/src/ElasticApm/Impl/AutoInstrument/InstrumentationNames.php +++ b/src/ElasticApm/Impl/AutoInstrument/InstrumentationNames.php @@ -37,4 +37,5 @@ final class InstrumentationNames public const CURL = 'curl'; public const PDO = 'pdo'; public const MYSQLI = 'mysqli'; + public const WORDPRESS = 'wordpress'; } diff --git a/src/ElasticApm/Impl/AutoInstrument/InterceptionManager.php b/src/ElasticApm/Impl/AutoInstrument/InterceptionManager.php index f7a1f95a2..6f795d796 100644 --- a/src/ElasticApm/Impl/AutoInstrument/InterceptionManager.php +++ b/src/ElasticApm/Impl/AutoInstrument/InterceptionManager.php @@ -27,6 +27,7 @@ use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Tracer; use Elastic\Apm\Impl\Util\ArrayUtil; +use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\DbgUtil; use Throwable; @@ -43,6 +44,9 @@ final class InterceptionManager /** @var Logger */ private $logger; + /** @var BuiltinPlugin */ + private $builtinPlugin; + /** @var int|null */ private $interceptedCallInProgressRegistrationId = null; @@ -64,20 +68,12 @@ public function __construct(Tracer $tracer) private function loadPlugins(Tracer $tracer): void { + $this->builtinPlugin = new BuiltinPlugin($tracer); $registerCtx = new RegistrationContext(); - $this->loadPluginsImpl($tracer, $registerCtx); - - $this->interceptedCallRegistrations = $registerCtx->interceptedCallRegistrations; - } - - private function loadPluginsImpl(Tracer $tracer, RegistrationContext $registerCtx): void - { - $builtinPlugin = new BuiltinPlugin($tracer); $registerCtx->dbgCurrentPluginIndex = 0; - $registerCtx->dbgCurrentPluginDesc = $builtinPlugin->getDescription(); - $builtinPlugin->register($registerCtx); - - // self::loadConfiguredPlugins(); + $registerCtx->dbgCurrentPluginDesc = $this->builtinPlugin->getDescription(); + $this->builtinPlugin->register($registerCtx); + $this->interceptedCallRegistrations = $registerCtx->interceptedCallRegistrations; } /** @@ -101,8 +97,9 @@ public function internalFuncCallPreHook( 'interceptedCallArgs' => $this->logger->possiblySecuritySensitive($interceptedCallArgs), ] ); - ($loggerProxy = $localLogger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log('Entered'); + $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__); + + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); $interceptRegistration = ArrayUtil::getValueIfKeyExistsElse($interceptRegistrationId, $this->interceptedCallRegistrations, null); @@ -113,8 +110,7 @@ public function internalFuncCallPreHook( } $localLogger->addContext('interceptRegistration', $interceptRegistration); - ($loggerProxy = $localLogger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log('Calling preHook...'); + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling preHook...'); try { $preHookRetVal = ($interceptRegistration->preHook)($thisObj, $interceptedCallArgs); } catch (Throwable $throwable) { @@ -133,8 +129,7 @@ public function internalFuncCallPreHook( $this->interceptedCallInProgressPreHookRetVal = $preHookRetVal; } - ($loggerProxy = $localLogger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log('preHook completed successfully', ['shouldCallPostHook' => $shouldCallPostHook]); + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'preHook completed successfully', ['shouldCallPostHook' => $shouldCallPostHook]); return $shouldCallPostHook; } @@ -149,8 +144,14 @@ public function internalFuncCallPostHook( bool $hasExitedByException, $returnValueOrThrown ): void { - ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->log('Entered'); + $localLogger = $this->logger->inherit()->addAllContext( + [ + 'interceptRegistrationId' => $this->interceptedCallInProgressRegistrationId, + 'interceptRegistration' => $this->interceptedCallInProgressRegistration, + ] + ); + $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__); + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); if ($this->interceptedCallInProgressRegistrationId === null) { ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) @@ -160,28 +161,92 @@ public function internalFuncCallPostHook( assert($this->interceptedCallInProgressRegistration !== null); assert($this->interceptedCallInProgressPreHookRetVal !== null); - $localLogger = $this->logger->inherit()->addAllContext( - [ - 'interceptRegistrationId' => $this->interceptedCallInProgressRegistrationId, - 'interceptRegistration' => $this->interceptedCallInProgressRegistration, - ] - ); - + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling postHook...'); try { ($this->interceptedCallInProgressPreHookRetVal)( $numberOfStackFramesToSkip + 1, $hasExitedByException, $returnValueOrThrown ); + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'postHook completed without throwing'); } catch (Throwable $throwable) { ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) - && $loggerProxy->logThrowable( - $throwable, - 'postHook has let a Throwable to escape' - ); + && $loggerProxy->logThrowable($throwable, 'postHook has thrown'); } $this->interceptedCallInProgressRegistrationId = null; $this->interceptedCallInProgressPreHookRetVal = null; } + + public function astInstrumentationDirectCall(string $method): void + { + $localLogger = $this->logger->inherit()->addAllContext(['method' => $method]); + + $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__); + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); + + $wordPressAutoInstrumIfEnabled = $this->builtinPlugin->getWordPressAutoInstrumentationIfEnabled(); + if ($wordPressAutoInstrumIfEnabled === null) { + static $loggedOnce = false; + if (!$loggedOnce) { + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'WordPress instrumentation is DISABLED'); + $loggedOnce = true; + } + return; + } + + static $dbgImplFuncDesc = null; + if ($dbgImplFuncDesc === null) { + $dbgImplFuncDesc = ClassNameUtil::fqToShort(WordPressAutoInstrumentation::class) . '->directCall'; + } + /** @var string $dbgImplFuncDesc */ + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling ' . $dbgImplFuncDesc . '...'); + try { + $wordPressAutoInstrumIfEnabled->directCall($method); + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, $dbgImplFuncDesc . ' completed without throwing'); + } catch (Throwable $throwable) { + ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->logThrowable($throwable, $dbgImplFuncDesc . ' has thrown'); + } + } + + /** + * @param ?string $instrumentedClassFullName + * @param string $instrumentedFunction + * @param mixed[] $capturedArgs + * + * @return null|callable(?Throwable $thrown, mixed $returnValue): void + */ + public function astInstrumentationPreHook(?string $instrumentedClassFullName, string $instrumentedFunction, array $capturedArgs): ?callable + { + $localLogger = $this->logger->inherit()->addAllContext(['instrumentedClassFullName' => $instrumentedClassFullName]); + + $loggerProxyTrace = $localLogger->ifTraceLevelEnabledNoLine(__FUNCTION__); + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); + + $wordPressAutoInstrumIfEnabled = $this->builtinPlugin->getWordPressAutoInstrumentationIfEnabled(); + if ($wordPressAutoInstrumIfEnabled === null) { + static $loggedOnce = false; + if (!$loggedOnce) { + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'WordPress instrumentation is DISABLED'); + $loggedOnce = true; + } + return null; + } + + static $dbgImplFuncDesc = null; + if ($dbgImplFuncDesc === null) { + $dbgImplFuncDesc = ClassNameUtil::fqToShort(WordPressAutoInstrumentation::class) . '->preHook'; + } + /** @var string $dbgImplFuncDesc */ + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Calling ' . $dbgImplFuncDesc . '...'); + try { + $retVal = $wordPressAutoInstrumIfEnabled->preHook($instrumentedClassFullName, $instrumentedFunction, $capturedArgs); + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, $dbgImplFuncDesc . ' completed without throwing', ['retVal == null' => ($retVal == null)]); + return $retVal; + } catch (Throwable $throwable) { + ($loggerProxy = $localLogger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->logThrowable($throwable, $dbgImplFuncDesc . ' has thrown'); + return null; + } + } } diff --git a/src/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php b/src/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php index ca37610b2..d5043bf3f 100644 --- a/src/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php +++ b/src/ElasticApm/Impl/AutoInstrument/PhpPartFacade.php @@ -515,4 +515,36 @@ private static function buildTracer(): ?Tracer public static function emptyMethod(): void { } + + /** + * Calls to this method are inserted by AST instrumentation. + * See src/ext/WordPress_instrumentation.c + * + * @noinspection PhpUnused + * + * @param ?string $instrumentedClassFullName + * @param string $instrumentedFunction + * @param mixed[] $capturedArgs + * + * @return null|callable(?Throwable $thrown, mixed $returnValue): void + */ + public static function astInstrumentationPreHook(?string $instrumentedClassFullName, string $instrumentedFunction, array $capturedArgs): ?callable + { + return (($interceptionManager = self::singletonInstance()->interceptionManager) !== null) + ? $interceptionManager->astInstrumentationPreHook($instrumentedClassFullName, $instrumentedFunction, $capturedArgs) + : null; + } + + /** + * Calls to this method are inserted by AST instrumentation. + * See src/ext/WordPress_instrumentation.c + * + * @noinspection PhpUnused + */ + public static function astInstrumentationDirectCall(string $method): void + { + if (($interceptionManager = self::singletonInstance()->interceptionManager) !== null) { + $interceptionManager->astInstrumentationDirectCall($method); + } + } } diff --git a/src/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php b/src/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php index 7a50fddeb..fbda2d549 100644 --- a/src/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php +++ b/src/ElasticApm/Impl/AutoInstrument/Util/AutoInstrumentationUtil.php @@ -59,18 +59,55 @@ public static function buildSpanNameFromCall(?string $className, string $funcNam return ($className === null) ? $funcName : ($className . '->' . $funcName); } - public static function beginCurrentSpan(string $name, string $type, ?string $subtype = null, ?string $action = null): SpanInterface + private static function processNewSpan(SpanInterface $span): void { - $span = ElasticApm::getCurrentTransaction()->beginCurrentSpan($name, $type, $subtype, $action); - if ($span instanceof Span) { // Mark all spans created by auto-instrumentation as compressible $span->setCompressible(true); } + } + + public static function beginCurrentSpan(string $name, string $type, ?string $subtype = null, ?string $action = null): SpanInterface + { + $span = ElasticApm::getCurrentTransaction()->beginCurrentSpan($name, $type, $subtype, $action); + + self::processNewSpan($span); return $span; } + /** + * @template T + * + * @param string $name New span's name + * @param string $type New span's type + * @param Closure(SpanInterface $newSpan): T $callback + * @param string|null $subtype New span's subtype + * @param string|null $action New span's action + * @param float|null $timestamp Start time of the new span + * + * @return T The return value of $callback + */ + public static function captureCurrentSpan(string $name, string $type, Closure $callback, ?string $subtype = null, ?string $action = null, ?float $timestamp = null) + { + return ElasticApm::getCurrentTransaction()->captureCurrentSpan( + $name, + $type, + /** + * @param SpanInterface $newSpan + * + * @return T The return value of $callback + */ + function (SpanInterface $newSpan) use ($callback) { + self::processNewSpan($newSpan); + return $callback($newSpan); + }, + $subtype, + $action, + $timestamp + ); + } + /** * @param int $numberOfStackFramesToSkip * @param SpanInterface $span @@ -264,6 +301,19 @@ public function verifyInstanceOf(string $expectedClass, $actualValue, ?string $d return true; } + /** + * @param mixed $actualValue + * @param bool $shouldCheckSyntaxOnly + * @param ?string $dbgParamName + * + * @return bool + */ + public function verifyIsCallable($actualValue, bool $shouldCheckSyntaxOnly, ?string $dbgParamName = null): bool + { + $isCallable = is_callable($actualValue, $shouldCheckSyntaxOnly); + return $this->verifyType($isCallable, 'callable', $actualValue, $dbgParamName); + } + /** * @param int $expectedMinArgsCount * @param mixed[] $interceptedCallArgs @@ -287,4 +337,28 @@ public function verifyMinArgsCount(int $expectedMinArgsCount, array $intercepted ); return false; } + + /** + * @param int $expectedArgsCount + * @param mixed[] $interceptedCallArgs + * + * @return bool + */ + public function verifyExactArgsCount(int $expectedArgsCount, array $interceptedCallArgs): bool + { + if (count($interceptedCallArgs) === $expectedArgsCount) { + return true; + } + + ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log( + 'Actual number of arguments does not equal the expected number', + [ + 'expected number of arguments' => $expectedArgsCount, + 'actual number of arguments' => count($interceptedCallArgs), + 'actual arguments' => $this->logger->possiblySecuritySensitive($interceptedCallArgs), + ] + ); + return false; + } } diff --git a/src/ElasticApm/Impl/AutoInstrument/WordPressAutoInstrumentation.php b/src/ElasticApm/Impl/AutoInstrument/WordPressAutoInstrumentation.php new file mode 100644 index 000000000..a13a2e852 --- /dev/null +++ b/src/ElasticApm/Impl/AutoInstrument/WordPressAutoInstrumentation.php @@ -0,0 +1,530 @@ + self::CALLBACK_GROUP_KIND_PLUGIN, + self::WORDPRESS_MU_PLUGINS_SUBDIR_SUBPATH => self::CALLBACK_GROUP_KIND_PLUGIN, + self::WORDPRESS_THEMES_SUBDIR_SUBPATH => self::CALLBACK_GROUP_KIND_THEME, + ]; + + /** + * \ELASTIC_APM_* constants are provided by the elastic_apm extension + * + * @phpstan-ignore-next-line + */ + private const DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS = \ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS; + + /** @var LoggerFactory */ + private $loggerFactory; + + /** @var Logger */ + private $logger; + + /** @var AutoInstrumentationUtil */ + private $util; + + /** @var bool */ + private $isInFailedMode = false; + + /** @var bool */ + private $isReadyToWrapFilterCallbacks = false; + + public function __construct(Tracer $tracer) + { + parent::__construct($tracer); + + $this->loggerFactory = $tracer->loggerFactory(); + $this->logger = $this->loggerFactory->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__)->addContext('this', $this); + + $this->util = new AutoInstrumentationUtil($tracer->loggerFactory()); + } + + /** @inheritDoc */ + public function name(): string + { + return InstrumentationNames::WORDPRESS; + } + + /** @inheritDoc */ + public function keywords(): array + { + return []; + } + + public function register(RegistrationContextInterface $ctx): void + { + } + + /** @inheritDoc */ + public function requiresUserlandCodeInstrumentation(): bool + { + return false; + } + + private function switchToFailedMode(): void + { + if ($this->isInFailedMode) { + return; + } + + ($loggerProxy = $this->logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->includeStackTrace()->log('Switching to FAILED mode'); + $this->isInFailedMode = true; + } + + private function setReadyToWrapFilterCallbacks(): void + { + $this->isReadyToWrapFilterCallbacks = true; + } + + /** + * @param string $filePath + * @param LoggerFactory $loggerFactory + * @param ?string $groupKind + * @param ?string $groupName + * + * @return void + * + * @param-out string $groupKind + * @param-out ?string $groupName + */ + public static function findAddonInfoFromFilePath(string $filePath, LoggerFactory $loggerFactory, /* out */ ?string &$groupKind, /* out */ ?string &$groupName): void + { + $logger = null; + $loggerProxyTrace = null; + if ($loggerFactory->isEnabledForLevel(Level::TRACE)) { + $logger = $loggerFactory->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__)->addContext('filePath', $filePath); + $loggerProxyTrace = $logger->ifTraceLevelEnabledNoLine(__FUNCTION__); + } + + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Entered'); + + $groupKind = self::CALLBACK_GROUP_KIND_CORE; + $groupName = null; + + $currentGroupKind = null; + /** @var ?int $posAfterAddonsSubDir */ + $posAfterAddonsSubDir = null; + foreach (self::CALLBACK_SUBDIR_SUBPATH_TO_GROUP_KIND as $subDirSubPath => $currentGroupKind) { + $pluginsSubDirPos = strpos($filePath, $subDirSubPath); + if ($pluginsSubDirPos !== false) { + $posAfterAddonsSubDir = $pluginsSubDirPos + strlen($subDirSubPath); + break; + } + } + if ($posAfterAddonsSubDir === null) { + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Not found any of the known sub-paths in the given path'); + return; + } + $logger && $logger->addContext('posAfterAddonsSubDir', $posAfterAddonsSubDir); + + $dirSeparatorAfterPluginPos = strpos($filePath, DIRECTORY_SEPARATOR, $posAfterAddonsSubDir); + if ($dirSeparatorAfterPluginPos !== false && $dirSeparatorAfterPluginPos > $posAfterAddonsSubDir) { + $groupKind = $currentGroupKind; + $groupName = substr($filePath, $posAfterAddonsSubDir, $dirSeparatorAfterPluginPos - $posAfterAddonsSubDir); + return; + } + + $fileExtAfterPluginPos = strpos($filePath, '.php', $posAfterAddonsSubDir); + if ($fileExtAfterPluginPos !== false && $fileExtAfterPluginPos > $posAfterAddonsSubDir) { + $groupKind = $currentGroupKind; + $groupName = substr($filePath, $posAfterAddonsSubDir, $fileExtAfterPluginPos - $posAfterAddonsSubDir); + return; + } + + $loggerProxyTrace && $loggerProxyTrace->log(__LINE__, 'Found one of the known sub-paths but the suffix is not as expected'); + } + + /** + * @param Closure|string $callback + * @param Logger $logger + * + * @return ?string + * + * @throws ReflectionException + */ + private static function getCallbackSourceFilePathImplForFunc($callback, Logger $logger): ?string + { + $reflectFunc = new ReflectionFunction($callback); + if (($srcFilePath = $reflectFunc->getFileName()) === false) { + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Failed to get file name from ReflectionFunction of captured callback'); + return null; + } + return $srcFilePath; + } + + /** + * @param object|string $classInstanceOrName + * @param Logger $logger + * + * @return ?string + * + * @throws ReflectionException + */ + private static function getCallbackSourceFilePathImplForClass($classInstanceOrName, Logger $logger): ?string + { + /** @var object|class-string $classInstanceOrName */ + $reflectClass = new ReflectionClass($classInstanceOrName); + if (($srcFilePath = $reflectClass->getFileName()) === false) { + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log('Failed to get file name from ReflectionClass of captured callback', ['classInstanceOrName' => $classInstanceOrName]); + return null; + } + return $srcFilePath; + } + + /** + * @param mixed $callback + * @param Logger $logger + * + * @return ?string + * + * @throws ReflectionException + */ + private static function getCallbackSourceFilePathImpl($callback, Logger $logger): ?string + { + // If callback is a Closure or string but not 'Class::method' + if ($callback instanceof Closure) { + return self::getCallbackSourceFilePathImplForFunc($callback, $logger); + } + + // If callback is a string but not 'Class::method' + if (is_string($callback)) { + if (($afterClassNamePos = strpos($callback, '::')) === false) { + return self::getCallbackSourceFilePathImplForFunc($callback, $logger); + } + $className = substr($callback, /* offset */ 0, /* length */ $afterClassNamePos); + return self::getCallbackSourceFilePathImplForClass($className, $logger); + } + + if (!is_array($callback)) { + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('callback of unexpected type'); + return null; + } + + if (ArrayUtil::isEmpty($callback)) { + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('callback is an empty array'); + return null; + } + + $firstElement = $callback[0]; + + if (is_string($firstElement) || is_object($firstElement)) { + return self::getCallbackSourceFilePathImplForClass($firstElement, $logger); + } + + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log('callback is an array but its first element is of unexpected type', ['firstElement type' => DbgUtil::getType($firstElement), 'firstElement' => $firstElement]); + return null; + } + + /** + * @param mixed $callback + * @param LoggerFactory $loggerFactory + * + * @return ?string + */ + public static function getCallbackSourceFilePath($callback, LoggerFactory $loggerFactory): ?string + { + $logger = $loggerFactory->loggerForClass(LogCategory::AUTO_INSTRUMENTATION, __NAMESPACE__, __CLASS__, __FILE__) + ->addAllContext(['callback type' => DbgUtil::getType($callback), 'callback' => $callback]); + + try { + return self::getCallbackSourceFilePathImpl($callback, $logger); + } catch (ReflectionException $e) { + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->logThrowable($e, 'Failed to reflect captured callback'); + return null; + } + } + + /** + * @param mixed $callback + * @param-out string $groupKind + * @param-out ?string $groupName + */ + private function findCallbackInfo($callback, /* out */ ?string &$groupKind, /* out */ ?string &$groupName): void + { + if (($srcFilePath = self::getCallbackSourceFilePath($callback, $this->loggerFactory)) === null) { + $groupKind = self::CALLBACK_GROUP_KIND_CORE; + $groupName = null; + return; + } + + self::findAddonInfoFromFilePath($srcFilePath, $this->loggerFactory, /* out */ $groupKind, /* out */ $groupName); + } + + public function directCall(string $method): void + { + if ($this->isInFailedMode) { + return; + } + + $logger = $this->logger->inherit()->addAllContext(['method' => $method]); + + switch ($method) { + case self::DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS: + $this->setReadyToWrapFilterCallbacks(); + return; + + default: + ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Unexpected method'); + $this->switchToFailedMode(); + } + } + + /** + * @param ?string $instrumentedClassFullName + * @param string $instrumentedFunction + * @param mixed[] $capturedArgs + * + * @return null|callable(?Throwable $thrown, mixed $returnValue): void + */ + public function preHook(?string $instrumentedClassFullName, string $instrumentedFunction, array $capturedArgs): ?callable + { + if ($this->isInFailedMode) { + return null /* <- null means there is no post-hook */; + } + + $logger = $this->logger->inherit()->addAllContext( + ['instrumentedClassFullName' => $instrumentedClassFullName, 'instrumentedFunction' => $instrumentedFunction, 'capturedArgs' => $capturedArgs] + ); + + // We should cover all the function instrumented in src/ext/WordPress_instrumentation.c + + if ($instrumentedClassFullName !== null) { + if ($instrumentedClassFullName !== 'WP_Hook') { + ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Unexpected instrumentedClassFullName'); + $this->switchToFailedMode(); + return null /* <- null means there is no post-hook */; + } + if ($instrumentedFunction !== 'add_filter') { + ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Unexpected instrumentedFunction'); + $this->switchToFailedMode(); + return null /* <- null means there is no post-hook */; + } + + $this->preHookAddFilter($capturedArgs); + return null /* <- null means there is no post-hook */; + } + + switch ($instrumentedFunction) { + case '_wp_filter_build_unique_id': + $this->preHookWpFilterBuildUniqueId($capturedArgs); + return null /* <- null means there is no post-hook */; + case 'get_template': + /** + * @param ?Throwable $thrown + * @param mixed $returnValue + */ + return function (?Throwable $thrown, $returnValue): void { + $this->postHookGetTemplate($thrown, $returnValue); + }; + default: + ($loggerProxy = $logger->ifErrorLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Unexpected instrumentedFunction'); + $this->switchToFailedMode(); + } + return null /* <- null means there is no post-hook */; + } + + /** + * @param mixed[] $capturedArgs + * + * @return void + */ + private function preHookAddFilter(array $capturedArgs): void + { + if (!$this->isReadyToWrapFilterCallbacks) { + static $isFirstTime = true; + if ($isFirstTime) { + ($loggerProxy = $this->logger->ifWarningLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log('First attempt to wrap callback but it is not ready yet'); + $isFirstTime = false; + } + return; + } + + if (!$this->preHookAddFilterImpl($capturedArgs)) { + $this->switchToFailedMode(); + } + } + + /** + * @param mixed[] $capturedArgs + * + * @return void + */ + private function preHookWpFilterBuildUniqueId(array $capturedArgs): void + { + if (!$this->preHookWpFilterBuildUniqueIdImpl($capturedArgs)) { + $this->switchToFailedMode(); + } + } + + /** + * @param mixed[] $capturedArgs + * + * @return bool + */ + private function preHookAddFilterImpl(array $capturedArgs): bool + { + if (!$this->verifyHookNameCallbackArgs($capturedArgs)) { + return false; + } + /** @var string $hookName */ + $hookName = $capturedArgs[0]; + $callback =& $capturedArgs[1]; + + if ($callback instanceof WordPressFilterCallbackWrapper) { + return true; + } + + $originalCallback = $callback; + $this->findCallbackInfo($originalCallback, /* out */ $callbackGroupKind, /* out */ $callbackGroupName); + $wrapper = new WordPressFilterCallbackWrapper($hookName, $originalCallback, $callbackGroupKind, $callbackGroupName); + $callback = $wrapper; + + ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log('Callback has been wrapped', ['original callback' => $originalCallback, 'wrapper' => $wrapper]); + return true; + } + + /** + * @param mixed[] $capturedArgs + * + * @return bool + */ + private function preHookWpFilterBuildUniqueIdImpl(array $capturedArgs): bool + { + if (!$this->verifyHookNameCallbackArgs($capturedArgs)) { + return false; + } + $callback =& $capturedArgs[1]; + + if (!($callback instanceof WordPressFilterCallbackWrapper)) { + return true; + } + + $wrapper = $callback; + $originalCallback = $wrapper->getWrappedCallback(); + $callback = $originalCallback; + + ($loggerProxy = $this->logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log('Callback has been unwrapped', ['original callback' => $originalCallback, 'wrapper' => $wrapper]); + return true; + } + + /** + * @param mixed[] $capturedArgs + * + * @return bool + */ + private function verifyHookNameCallbackArgs(array $capturedArgs): bool + { + // + // We should get (see src/ext/WordPress_instrumentation.c): + // [0] $hook_name parameter by value + // [1] $callback parameter by reference + // + // function add_filter($hook_name, $callback, $priority = 10, $accepted_args = 1) + // function _wp_filter_build_unique_id($hook_name, $callback, $priority) + + return $this->util->verifyExactArgsCount(2, $capturedArgs) + && $this->util->verifyIsString($capturedArgs[0], 'hook_name') + && $this->util->verifyIsCallable($capturedArgs[1], /* shouldCheckSyntaxOnly */ true, '$callback'); + } + + /** + * @param ?Throwable $thrown + * @param mixed $returnValue + */ + private function postHookGetTemplate(?Throwable $thrown, $returnValue): void + { + $logger = $this->logger->inherit()->addAllContext(['thrown' => $thrown, 'returnValue type' => DbgUtil::getType($returnValue), 'returnValue' => $returnValue]); + + if ($thrown !== null) { + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Instrumented function has thrown so there is no return value'); + return; + } + + if ($returnValue === null) { + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Return value is null'); + return; + } + + if (!is_string($returnValue)) { + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Return value is not a string'); + return; + } + + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log('Recording WordPress theme as a label on transaction', ['theme' => $returnValue, 'label key' => self::LABEL_KEY_FOR_WORDPRESS_THEME]); + ElasticApm::getCurrentTransaction()->context()->setLabel(self::LABEL_KEY_FOR_WORDPRESS_THEME, $returnValue); + } +} diff --git a/src/ElasticApm/Impl/AutoInstrument/WordPressFilterCallbackWrapper.php b/src/ElasticApm/Impl/AutoInstrument/WordPressFilterCallbackWrapper.php new file mode 100644 index 000000000..56a9a32a6 --- /dev/null +++ b/src/ElasticApm/Impl/AutoInstrument/WordPressFilterCallbackWrapper.php @@ -0,0 +1,94 @@ +hookName = $hookName; + $this->callback = $callback; + $this->callbackGroupKind = $callbackGroupKind; + $this->callbackGroupName = $callbackGroupName; + } + + /** + * @return mixed + */ + public function getWrappedCallback() + { + return $this->callback; + } + + /** + * @return mixed + */ + public function __invoke() + { + $args = func_get_args(); + return AutoInstrumentationUtil::captureCurrentSpan( + $this->hookName . ' - ' . ($this->callbackGroupName ?? WordPressAutoInstrumentation::SPAN_NAME_PART_FOR_CORE) /* <- name */, + $this->callbackGroupKind /* <- type */, + /** + * @return mixed + */ + function () use ($args) { + return call_user_func_array($this->callback, $args); // @phpstan-ignore-line - $this->callback should have type callable + }, + $this->callbackGroupName /* <- subtype */, + $this->hookName /* <- action */ + ); + } +} diff --git a/src/ElasticApm/Impl/Config/AllOptionsMetadata.php b/src/ElasticApm/Impl/Config/AllOptionsMetadata.php index 518ff2262..610656bdf 100644 --- a/src/ElasticApm/Impl/Config/AllOptionsMetadata.php +++ b/src/ElasticApm/Impl/Config/AllOptionsMetadata.php @@ -77,6 +77,12 @@ public static function get(): array /** @var array> $value */ $value = [ OptionNames::API_KEY => new NullableStringOptionMetadata(), + OptionNames::AST_PROCESS_ENABLED => new BoolOptionMetadata(/* defaultValue: */ true), + OptionNames::AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE + => new BoolOptionMetadata(/* defaultValue: */ true), + OptionNames::AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX + => new NullableStringOptionMetadata(), + OptionNames::AST_PROCESS_DEBUG_DUMP_OUT_DIR => new NullableStringOptionMetadata(), OptionNames::ASYNC_BACKEND_COMM => new BoolOptionMetadata(/* default */ true), OptionNames::BREAKDOWN_METRICS => new BoolOptionMetadata(/* default */ true), OptionNames::CAPTURE_ERRORS => new BoolOptionMetadata(/* default */ true), @@ -102,6 +108,7 @@ public static function get(): array OptionNames::SPAN_COMPRESSION_ENABLED => new BoolOptionMetadata(/* default */ true), OptionNames::SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION => self::buildDurationMetadataInMilliseconds(/* default */ 50), OptionNames::SPAN_COMPRESSION_SAME_KIND_MAX_DURATION => self::buildDurationMetadataInMilliseconds(/* default */ 0), + OptionNames::STACK_TRACE_LIMIT => new IntOptionMetadata(/* min */ -1, /* max */ null, /* default */ OptionDefaultValues::STACK_TRACE_LIMIT), OptionNames::TRANSACTION_IGNORE_URLS => new NullableWildcardListOptionMetadata(), OptionNames::TRANSACTION_MAX_SPANS => self::buildPositiveOrZeroIntMetadata(OptionDefaultValues::TRANSACTION_MAX_SPANS), OptionNames::TRANSACTION_SAMPLE_RATE => new FloatOptionMetadata(/* min */ 0.0, /* max */ 1.0, /* default */ 1.0), diff --git a/src/ElasticApm/Impl/Config/OptionDefaultValues.php b/src/ElasticApm/Impl/Config/OptionDefaultValues.php index 9be02393c..b24ba9436 100644 --- a/src/ElasticApm/Impl/Config/OptionDefaultValues.php +++ b/src/ElasticApm/Impl/Config/OptionDefaultValues.php @@ -34,5 +34,6 @@ final class OptionDefaultValues { use StaticClassTrait; + public const STACK_TRACE_LIMIT = 50; public const TRANSACTION_MAX_SPANS = 500; } diff --git a/src/ElasticApm/Impl/Config/OptionNames.php b/src/ElasticApm/Impl/Config/OptionNames.php index 8e674ac2a..1e8751928 100644 --- a/src/ElasticApm/Impl/Config/OptionNames.php +++ b/src/ElasticApm/Impl/Config/OptionNames.php @@ -35,6 +35,10 @@ final class OptionNames use StaticClassTrait; public const API_KEY = 'api_key'; + public const AST_PROCESS_ENABLED = 'ast_process_enabled'; + public const AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE = 'ast_process_debug_dump_converted_back_to_source'; + public const AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX = 'ast_process_debug_dump_for_path_prefix'; + public const AST_PROCESS_DEBUG_DUMP_OUT_DIR = 'ast_process_debug_dump_out_dir'; public const ASYNC_BACKEND_COMM = 'async_backend_comm'; public const BREAKDOWN_METRICS = 'breakdown_metrics'; public const CAPTURE_ERRORS = 'capture_errors'; @@ -62,6 +66,7 @@ final class OptionNames public const SPAN_COMPRESSION_ENABLED = 'span_compression_enabled'; public const SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION = 'span_compression_exact_match_max_duration'; public const SPAN_COMPRESSION_SAME_KIND_MAX_DURATION = 'span_compression_same_kind_max_duration'; + public const STACK_TRACE_LIMIT = 'stack_trace_limit'; public const TRANSACTION_IGNORE_URLS = 'transaction_ignore_urls'; public const TRANSACTION_MAX_SPANS = 'transaction_max_spans'; public const TRANSACTION_SAMPLE_RATE = 'transaction_sample_rate'; diff --git a/src/ElasticApm/Impl/Config/Snapshot.php b/src/ElasticApm/Impl/Config/Snapshot.php index a0afc2ad8..c24596418 100644 --- a/src/ElasticApm/Impl/Config/Snapshot.php +++ b/src/ElasticApm/Impl/Config/Snapshot.php @@ -98,6 +98,18 @@ final class Snapshot implements LoggableInterface /** @var string */ private $apiKey; + /** @var bool */ + private $astProcessEnabled; + + /** @var bool */ + private $astProcessDebugDumpConvertedBackToSource; + + /** @var string */ + private $astProcessDebugDumpForPathPrefix; + + /** @var string */ + private $astProcessDebugDumpOutDir; + /** @var bool */ private $asyncBackendComm; @@ -176,6 +188,9 @@ final class Snapshot implements LoggableInterface /** @var float */ private $spanCompressionSameKindMaxDuration; + /** @var int */ + private $stackTraceLimit; + /** @var ?WildcardListMatcher */ private $transactionIgnoreUrls; @@ -246,6 +261,11 @@ public function parsedValueFor(string $optName) return $this->optNameToParsedValue[$optName]; } + public function astProcessEnabled(): bool + { + return $this->astProcessEnabled; + } + public function breakdownMetrics(): bool { return $this->breakdownMetrics; @@ -351,6 +371,11 @@ public function spanCompressionSameKindMaxDuration(): float return $this->spanCompressionSameKindMaxDuration; } + public function stackTraceLimit(): int + { + return $this->stackTraceLimit; + } + public function transactionIgnoreUrls(): ?WildcardListMatcher { return $this->transactionIgnoreUrls; diff --git a/src/ElasticApm/Impl/Span.php b/src/ElasticApm/Impl/Span.php index b9e57d714..8abde9a8d 100644 --- a/src/ElasticApm/Impl/Span.php +++ b/src/ElasticApm/Impl/Span.php @@ -479,16 +479,21 @@ public function endSpanEx(int $numberOfStackFramesToSkip, ?float $duration = nul return; } - // This method is part of public API so it should be kept in the stack trace - // if $numberOfStackFramesToSkip is 0 - $this->stackTrace = StackTraceUtil::captureCurrent( - $numberOfStackFramesToSkip, - true /* <- hideElasticApmImpl */ - ); - $this->onAboutToEnd->callCallbacks($this); if ($this->shouldBeSentToApmServer()) { + /** + * stack_trace_limit + * 0 - stack trace collection should be disabled + * any positive integer value - the value is the maximum number of frames to collect + * -1 - all frames should be collected + */ + $stackTraceLimit = $this->containingTransaction->getStackTraceLimitConfig(); + if ($stackTraceLimit !== 0) { + // This method is part of public API so it should be kept in the stack trace + // if $numberOfStackFramesToSkip is 0 + $this->stackTrace = StackTraceUtil::captureCurrent($numberOfStackFramesToSkip, /* hideElasticApmImpl */ true); + } $this->prepareForSerialization(); $this->parentExecutionSegment->onChildSpanEnded($this); } diff --git a/src/ElasticApm/Impl/Transaction.php b/src/ElasticApm/Impl/Transaction.php index 3742803d8..34c7c3ce0 100644 --- a/src/ElasticApm/Impl/Transaction.php +++ b/src/ElasticApm/Impl/Transaction.php @@ -95,6 +95,9 @@ final class Transaction extends ExecutionSegment implements TransactionInterface /** @var ?bool */ private $cachedIsSpanCompressionEnabled = null; + /** @var ?int */ + private $cachedStackTraceLimitConfig = null; + public function __construct(TransactionBuilder $builder) { $this->tracer = $builder->tracer; @@ -567,6 +570,28 @@ public function isSpanCompressionEnabled(): bool return $this->cachedIsSpanCompressionEnabled; } + public function getStackTraceLimitConfig(): int + { + if ($this->cachedStackTraceLimitConfig === null) { + $this->cachedStackTraceLimitConfig = $this->getConfig()->stackTraceLimit(); + + /** + * stack_trace_limit + * 0 - stack trace collection should be disabled + * any positive integer value - the value is the maximum number of frames to collect + * -1 - all frames should be collected + */ + $msgPrefix = $this->cachedStackTraceLimitConfig === 0 + ? 'Span stack trace collection is DISABLED' + : ($this->cachedStackTraceLimitConfig < 0 + ? 'Span stack trace collection will include all the frames' + : 'Span stack trace collection will include up to ' . $this->cachedStackTraceLimitConfig . ' frames'); + ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log($msgPrefix . ' (set by configuration option `' . OptionNames::STACK_TRACE_LIMIT . '\')'); + } + return $this->cachedStackTraceLimitConfig; + } + private function prepareForSerialization(): void { SerializationUtil::prepareForSerialization(/* ref */ $this->context); diff --git a/src/ElasticApm/Impl/Util/ArrayUtil.php b/src/ElasticApm/Impl/Util/ArrayUtil.php index 73707a095..391e0d2ce 100644 --- a/src/ElasticApm/Impl/Util/ArrayUtil.php +++ b/src/ElasticApm/Impl/Util/ArrayUtil.php @@ -224,16 +224,4 @@ public static function removeKeyIfExists(string $key, array &$array): void unset($array[$key]); } } - - /** - * @template TKey of string|int - * @template TValue - * - * @param array $from - * @param array $to - */ - public static function append(array $from, /* in,out */ array &$to): void - { - $to = array_merge($to, $from); - } } diff --git a/src/ext/AST_debug.c b/src/ext/AST_debug.c new file mode 100644 index 000000000..5fb72e979 --- /dev/null +++ b/src/ext/AST_debug.c @@ -0,0 +1,841 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "AST_debug.h" +#include "ConfigSnapshot.h" +#include "log.h" +#include +#include +#include +#include +#include +#include "util.h" +#include "util_for_PHP.h" +#include "elastic_apm_alloc.h" +#include "AST_util.h" + +#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_AUTO_INSTRUMENT + +static bool g_astProcessDebugDumpIsEnabled = false; +static bool g_astProcessDebugDumpConvertedBackToSource = false; +static StringBuffer g_astProcessDebugDumpForPathPrefix; +static StringBuffer g_astProcessDebugDumpOutDir; + +String zendAstMagicConstAttrToString( zend_ast_attr attr ) +{ + switch ( attr ) + { + case T_DIR: return "__DIR__"; + case T_FILE: return "__FILE__"; + case T_LINE: return "__LINE__"; + case T_NS_C: return "__NAMESPACE__"; + case T_CLASS_C: return "__CLASS__"; + case T_TRAIT_C: return "__TRAIT__"; + case T_METHOD_C: return "__METHOD__"; + case T_FUNC_C: return "__FUNCTION__"; + default: return NULL; + } +} + +String streamZendAstMagicConstAttr( zend_ast_attr attr, TextOutputStream* txtOutStream ) +{ + String asString = zendAstMagicConstAttrToString( attr ); + return asString == NULL ? streamPrintf( txtOutStream, "UNKNOWN (as int: %d)", (int)attr ) : asString; +} + +String zendAstKindToString( zend_ast_kind kind ) +{ +# define ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( enumMember ) \ + case enumMember: \ + return (#enumMember) \ + /**/ + + // Up to date with PHP v8.2.3 + switch ( kind ) + { + /** + * zend_ast_kind enum values as of PHP 7.2 + */ + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_AND ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARG_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARRAY ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARRAY_ELEM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_OP ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_REF ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_BINARY_OP ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_BREAK ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CALL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CAST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CATCH ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CATCH_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST_DECL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLONE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLOSURE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLOSURE_USES ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_COALESCE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONDITIONAL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_DECL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_ELEM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONTINUE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DECLARE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DIM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DO_WHILE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ECHO ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EMPTY ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ENCAPS_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EXIT ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EXPR_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FOR ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FOREACH ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FUNC_DECL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GLOBAL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GOTO ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GREATER ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GREATER_EQUAL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GROUP_USE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_HALT_COMPILER ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_IF ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_IF_ELEM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_INCLUDE_OR_EVAL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_INSTANCEOF ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ISSET ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_LABEL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MAGIC_CONST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD_CALL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD_REFERENCE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAME_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAMESPACE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NEW ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_OR ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARAM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARAM_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_POST_DEC ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_POST_INC ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRE_DEC ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRE_INC ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRINT ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_DECL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_ELEM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_REF ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_RETURN ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SHELL_EXEC ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SILENCE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC_CALL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC_PROP ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STMT_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH_CASE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_THROW ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_ADAPTATIONS ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_ALIAS ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_PRECEDENCE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRY ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_MINUS ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_OP ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_PLUS ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNPACK ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNSET ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE_ELEM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE_TRAIT ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_VAR ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_WHILE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_YIELD ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_YIELD_FROM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ZNODE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ZVAL ); + + /** + * values added in PHP 7.3 + */ + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */ + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONSTANT ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONSTANT_CLASS ); + #endif + + /** + * values added in PHP 7.4 + */ + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 4, 0 ) /* if PHP version from 7.4.0 */ + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARROW_FUNC ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_COALESCE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_NAME ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_GROUP ); + #endif + + /** + * values added in PHP 8.0 + */ + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) /* if PHP version from 8.0.0 */ + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ATTRIBUTE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ATTRIBUTE_GROUP ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ATTRIBUTE_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST_GROUP ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MATCH ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MATCH_ARM ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MATCH_ARM_LIST ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAMED_ARG ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NULLSAFE_METHOD_CALL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NULLSAFE_PROP ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE_UNION ); + #endif + + /** + * values added in PHP 8.1 + */ + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 1, 0 ) /* if PHP version from 8.1.0 */ + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CALLABLE_CONVERT ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_ENUM_INIT ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ENUM_CASE ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE_INTERSECTION ); + #endif + + default: + return NULL; + } +# undef ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE +} + +String streamZendAstKind( zend_ast_kind kind, TextOutputStream* txtOutStream ) +{ + String asString = zendAstKindToString( kind ); + return asString == NULL ? streamPrintf( txtOutStream, "UNKNOWN (as int: %d)", (int)kind ) : asString; +} + +typedef void (* DebugDumpAstPrintLine )( void* ctx, String text, UInt nestingDepth ); + +struct DebugDumpAstPrinter +{ + DebugDumpAstPrintLine printLine; + void* ctx; +}; +typedef struct DebugDumpAstPrinter DebugDumpAstPrinter; + +void debugDumpAst( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth ); + +void debugDumpAstPrintLineFormattedText( DebugDumpAstPrinter* printer, String text, UInt nestingDepth ) +{ + printer->printLine( printer->ctx, text, nestingDepth ); +} + +UInt getAstLineNumber( zend_ast* ast ) +{ + return (UInt) zend_ast_get_lineno( ast ); +} + +void debugDumpAstPrintLineForNull( DebugDumpAstPrinter* printer, UInt nestingDepth ) +{ + debugDumpAstPrintLineFormattedText( printer, "NULL", nestingDepth ); +} + +void debugDumpAstPrintLineTemplate( DebugDumpAstPrinter* printer, zend_ast_kind kind, UInt lineNumber, String attrAsString, UInt childCount, String additionalInfo, UInt nestingDepth ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + String text = ( additionalInfo == NULL ) + ? streamPrintf( &txtOutStream, "%s (line: %u, attr: %s, childCount: %u)" + , streamZendAstKind( kind, &txtOutStream ), lineNumber, attrAsString, childCount ) + : streamPrintf( &txtOutStream, "%s (line: %u, attr: %s, childCount: %u, %s)" + , streamZendAstKind( kind, &txtOutStream ), lineNumber, attrAsString, childCount, additionalInfo ); + + debugDumpAstPrintLineFormattedText( printer, text, nestingDepth ); +} + +static inline +String streamAstAttribute( zend_ast_attr attr, TextOutputStream* txtOutStream ) +{ + return streamPrintf( txtOutStream, "%u", attr ); +} + +void debugDumpAstPrintLineDefault( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + String attrAsString = streamAstAttribute( ast->attr, &txtOutStream ); + + debugDumpAstPrintLineTemplate( printer, ast->kind, getAstLineNumber( ast ), attrAsString, getAstChildren( ast ).count, /* additionalInfo */ NULL, nestingDepth ); +} + +size_t calcNumberOfNonWhiteChars( StringView strVw ) +{ + size_t result = 0; + ELASTIC_APM_FOR_EACH_INDEX( i, strVw.length ) + { + if ( ! isWhiteSpace( strVw.begin[ i ] ) ) + { + ++result; + } + } + return result; +} + +void debugDumpAstPrintLineForDecl( DebugDumpAstPrinter* printer, zend_ast_decl* astDecl, UInt nestingDepth ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + String attrAsString = streamAstAttribute( astDecl->attr, &txtOutStream ); + size_t docCommentNumberOfNonWhiteChars = calcNumberOfNonWhiteChars( nullableZStringToStringView( astDecl->doc_comment ) ); + String additionalInfo = streamPrintf( + &txtOutStream + , "name: %s, end line: %u, flags: %u, doc_comment: %s" + , nullableZStringToString( astDecl->name ), (UInt)( astDecl->start_lineno ), (UInt)( astDecl->flags ) + , astDecl->doc_comment == NULL ? "NULL" : streamPrintf( &txtOutStream, "[number of non-white chars: %u]", (unsigned)docCommentNumberOfNonWhiteChars ) + ); + + debugDumpAstPrintLineTemplate( printer, astDecl->kind, astDecl->start_lineno, attrAsString, getAstChildren( (zend_ast*)astDecl ).count, additionalInfo, nestingDepth ); +} + +void debugDumpAstPrintLineForMagicConst( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + String attrAsString = streamZendAstMagicConstAttr( ast->attr, &txtOutStream ); + + debugDumpAstPrintLineTemplate( printer, ast->kind, getAstLineNumber( ast ), attrAsString, getAstChildren( ast ).count, /* additionalInfo */ NULL, nestingDepth ); +} + +String debugDumpAstZvalStreamVal( zend_ast* ast, TextOutputStream* txtOutStream ) +{ + zval* zVal = zend_ast_get_zval( ast ); + if ( zVal == NULL ) + { + return "ast->val is NULL"; + } + + int zValType = (int)Z_TYPE_P( zVal ); + switch ( zValType ) + { + case IS_STRING: + { + StringView strVw = zStringToStringView( Z_STR_P( zVal ) ); + return streamPrintf( txtOutStream, "type: string, value: %.*s", (int)(strVw.length), strVw.begin ); + } + + case IS_LONG: + return streamPrintf( txtOutStream, "type: long, value: %"PRId64, (Int64)(Z_LVAL_P( zVal )) ); + + case IS_DOUBLE: + return streamPrintf( txtOutStream, "type: double, value: %f", (double)(Z_DVAL_P( zVal )) ); + + case IS_NULL: + return streamPrintf( txtOutStream, "type: null" ); + + case IS_FALSE: + return streamPrintf( txtOutStream, "type: false" ); + case IS_TRUE: + return streamPrintf( txtOutStream, "type: true " ); + + default: + return streamPrintf( txtOutStream, "type: %s (type ID as int: %d)", zend_get_type_by_const( zValType ), (int)zValType ); + } +} + +void debugDumpAstPrintLineForZVal( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + String attrAsString = streamAstAttribute( ast->attr, &txtOutStream ); + String additionalInfo = debugDumpAstZvalStreamVal( ast, &txtOutStream ); + + debugDumpAstPrintLineTemplate( printer, ast->kind, getAstLineNumber( ast ), attrAsString, getAstChildren( ast ).count, additionalInfo, nestingDepth ); +} + +void debugDumpAstPrintLineForBinaryOp( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + String attrAsString = streamPrintf( &txtOutStream, "opcode: %s (ID as int: %d)", zend_get_opcode_name( (zend_uchar)(ast->attr) ), (int)(ast->attr) ); + + debugDumpAstPrintLineTemplate( printer, ast->kind, getAstLineNumber( ast ), attrAsString, getAstChildren( ast ).count, /* additionalInfo */ NULL, nestingDepth ); +} + +void debugDumpAstPrintLineDispatch( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth ) +{ + if ( isAstDecl( ast->kind ) ) + { + debugDumpAstPrintLineForDecl( printer, (zend_ast_decl*)ast, nestingDepth ); + return; + } + + switch ( ast->kind ) + { + case ZEND_AST_BINARY_OP: + debugDumpAstPrintLineForBinaryOp( printer, ast, nestingDepth ); + return; + + case ZEND_AST_MAGIC_CONST: + debugDumpAstPrintLineForMagicConst( printer, ast, nestingDepth ); + return; + + case ZEND_AST_ZVAL: + debugDumpAstPrintLineForZVal( printer, ast, nestingDepth ); + return; + default: + debugDumpAstPrintLineDefault( printer, ast, nestingDepth ); + return; + } +} + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "misc-no-recursion" +void debugDumpAst( DebugDumpAstPrinter* printer, zend_ast* ast, UInt nestingDepth ) +{ + if ( ast == NULL ) + { + debugDumpAstPrintLineForNull( printer, nestingDepth ); + return; + } + + debugDumpAstPrintLineDispatch( printer, ast, nestingDepth ); + + ZendAstPtrArrayView children = getAstChildren( ast ); + ELASTIC_APM_FOR_EACH_INDEX( i, children.count ) + { + debugDumpAst( printer, children.values[ i ], nestingDepth + 1 ); + } +} +#pragma clang diagnostic pop + +struct DebugDumpAstPrintToLogCtx +{ + LogLevel logLevel; +}; +typedef struct DebugDumpAstPrintToLogCtx DebugDumpAstPrintToLogCtx; + +void debugDumpAstPrintLineToLog( void* ctx, String text, UInt nestingDepth ) +{ + const DebugDumpAstPrintToLogCtx* const localCtx = (const DebugDumpAstPrintToLogCtx*)ctx; + + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + ELASTIC_APM_LOG_WITH_LEVEL( localCtx->logLevel, "%s%s", streamIndent( nestingDepth, &txtOutStream ), text ); +} + +void debugDumpAstTreeToLog( zend_ast* ast, LogLevel logLevel ) +{ + if ( maxEnabledLogLevel() < logLevel ) + { + return; + } + + DebugDumpAstPrintToLogCtx ctx = (DebugDumpAstPrintToLogCtx){ .logLevel = logLevel }; + DebugDumpAstPrinter printer = (DebugDumpAstPrinter){ .printLine = &debugDumpAstPrintLineToLog, .ctx = &ctx }; + debugDumpAst( &printer, ast, /* nestingDepth */ 0 ); +} + + +struct DebugDumpAstPrintToTextOutputStreamCtx +{ + TextOutputStream* txtOutStream; + String result; +}; +typedef struct DebugDumpAstPrintToTextOutputStreamCtx DebugDumpAstPrintToTextOutputStreamCtx; + +void debugDumpAstPrintLineToTextOutputStream( void* ctx, String text, UInt nestingDepth ) +{ + ELASTIC_APM_UNUSED( nestingDepth ); + + DebugDumpAstPrintToTextOutputStreamCtx* const localCtx = (DebugDumpAstPrintToTextOutputStreamCtx*)ctx; + + localCtx->result = streamPrintf( localCtx->txtOutStream, "%s", text ); +} + +String streamZendAstNode( zend_ast* ast, TextOutputStream* txtOutStream ) +{ + DebugDumpAstPrintToTextOutputStreamCtx ctx = (DebugDumpAstPrintToTextOutputStreamCtx){ .txtOutStream = txtOutStream, .result = NULL }; + DebugDumpAstPrinter printer = (DebugDumpAstPrinter){ .printLine = &debugDumpAstPrintLineToTextOutputStream, .ctx = &ctx }; + debugDumpAstPrintLineDispatch( &printer, ast, /* nestingDepth */ 0 ); + return ctx.result; +} + +struct DebugDumpAstPrintToFileCtx +{ + FILE* outFile; +}; +typedef struct DebugDumpAstPrintToFileCtx DebugDumpAstPrintToFileCtx; + +void debugDumpAstPrintLineToFile( void* ctx, String text, UInt nestingDepth ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + const DebugDumpAstPrintToFileCtx* const localCtx = (const DebugDumpAstPrintToFileCtx*)ctx; + + fputs( streamIndent( nestingDepth, &txtOutStream ), localCtx->outFile ); + fputs( text, localCtx->outFile ); + fputs( "\n", localCtx->outFile ); +} + +bool isFileSystemPathPrefix( StringView path, StringView pathPrefix ) +{ + return isStringViewPrefix( + path, + pathPrefix, + /* shouldIgnoreCase */ +# ifdef PHP_WIN32 + true +# else // #ifdef PHP_WIN32 + false +# endif // #ifdef PHP_WIN32 + ); +} + +ResultCode ensureDirectoryExists( String dirFullPath ) +{ +# ifdef PHP_WIN32 + + return resultFailure; + +# else // #ifdef PHP_WIN32 + + int mkdirRetVal = mkdir( dirFullPath, /* mode */ S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH ); + if ( mkdirRetVal != 0 ) + { + int errnoValue = errno; + if (errnoValue == EEXIST) + { + return resultSuccess; + } + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + ELASTIC_APM_LOG_ERROR( "mkdir failed; dirFullPath: `%s', mkdirRetVal: %d, errno: %d (%s)", dirFullPath, mkdirRetVal, errnoValue, streamErrNo( errnoValue, &txtOutStream ) ); + return resultFailure; + } + return resultSuccess; + +# endif // #ifdef PHP_WIN32 +} + +ResultCode ensureDirectoriesExist( StringView fullPath ) +{ + ELASTIC_APM_ASSERT( ! isEmptyStringView( fullPath ), "fullPath should not be empty" ); + + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "fullPath: %s", fullPath.begin ); + + ResultCode resultCode; + StringBuffer dirFullPath = ELASTIC_APM_EMPTY_STRING_BUFFER; + size_t dirFullPathLen = 0; + + ELASTIC_APM_MALLOC_STRING_BUFFER_IF_FAILED_GOTO( /* maxLength */ fullPath.length, /* out */ dirFullPath ); + dirFullPath.begin[ 0 ] = '\0'; + ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( /* suffixToAppend */ fullPath, dirFullPath, /* in,out */ &dirFullPathLen ) ); + ELASTIC_APM_ASSERT_EQ_UINT64( dirFullPathLen, fullPath.length ); + + char directorySeparator = +# ifdef PHP_WIN32 + '\\'; +# else // #ifdef PHP_WIN32 + '/'; +# endif // #ifdef PHP_WIN32 + + for ( MutableString begin = &( dirFullPath.begin[ 0 ] ), current = begin + 1, end = begin + fullPath.length ; current != end ; ) + { + char* pDirSep = strchr( current, directorySeparator ); + if ( pDirSep == NULL ) + { + break; + } + + char savedDirSep = *pDirSep; + *pDirSep = '\0'; + ELASTIC_APM_CALL_IF_FAILED_GOTO( ensureDirectoryExists( dirFullPath.begin ) ); + *pDirSep = savedDirSep; + current = pDirSep + 1; + } + + resultCode = resultSuccess; + finally: + + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ dirFullPath ); + + ELASTIC_APM_UNUSED( resultCode ); + + ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "fullPath: %s", fullPath.begin ); + return resultCode; + + failure: + goto finally; +} + +ResultCode buildFileFullPath( StringViewArrayView pathParts, /* out */ StringBuffer* pResult ) +{ + ResultCode resultCode; + StringBuffer result = ELASTIC_APM_EMPTY_STRING_BUFFER; + size_t length = 0; + size_t contentLength = 0; + + ELASTIC_APM_FOR_EACH_INDEX( i, pathParts.count ) + { + length += pathParts.values[ i ].length; + } + + ELASTIC_APM_MALLOC_STRING_BUFFER_IF_FAILED_GOTO( /* maxLength */ length, /* out */ result ); + result.begin[ 0 ] = '\0'; + + ELASTIC_APM_FOR_EACH_INDEX( i, pathParts.count ) + { + ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( /* suffixToAppend */ pathParts.values[ i ], result, /* in,out */ &contentLength ) ); + } + ELASTIC_APM_ASSERT_EQ_UINT64( contentLength, length ); + + *pResult = result; + result = ELASTIC_APM_EMPTY_STRING_BUFFER; + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ result ); + goto finally; +} + +void debugDumpAstSubTreeConvertedBackToSource( StringView compiledFileFullPath, StringView compiledFileRelativePath, zend_ast* ast, StringView isBeforeProcessSuffix, TextOutputStream* txtOutStream ) +{ + ResultCode resultCode; + FILE* convertedBackToSourceFile = NULL; + int errnoValue = 0; + StringBuffer convertedBackToSourceFileFullPath = ELASTIC_APM_EMPTY_STRING_BUFFER; + zend_string* convertedBackToSourceText = NULL; + + StringView convertedBackToSourceFileExtensionSuffix = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".php" ); + StringView convertedBackToSourceFileFullPathParts[] = { + stringBufferToView( g_astProcessDebugDumpOutDir ), + compiledFileRelativePath, + isBeforeProcessSuffix, + ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".converted_back_to_source" ), + convertedBackToSourceFileExtensionSuffix + }; + ELASTIC_APM_CALL_IF_FAILED_GOTO( buildFileFullPath( ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( StringViewArrayView, convertedBackToSourceFileFullPathParts ), /* out */ &convertedBackToSourceFileFullPath ) ); + + errnoValue = openFile( convertedBackToSourceFileFullPath.begin, "w", /* out */ &convertedBackToSourceFile ); + if ( errnoValue != 0 ) + { + ELASTIC_APM_LOG_ERROR( "Failed to open file; convertedBackToSourceFileFullPath: %s; errno: %d (%s)", convertedBackToSourceFileFullPath.begin, errnoValue, streamErrNo( errnoValue, txtOutStream ) ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + + ELASTIC_APM_LOG_INFO( "Printing AST converted back to source of %s to %s ...", compiledFileFullPath.begin, convertedBackToSourceFileFullPath.begin ); + convertedBackToSourceText = zend_ast_export( /* prefix */ "", ast, /* suffix */ "" ); + String textAsCString = nullableZStringToString( convertedBackToSourceText ); + if ( textAsCString == NULL ) + { + ELASTIC_APM_LOG_INFO( "Nothing to print for AST of %s converted back to source to %s", compiledFileFullPath.begin, convertedBackToSourceFileFullPath.begin ); + } + else + { + fputs( textAsCString, convertedBackToSourceFile ); + ELASTIC_APM_LOG_INFO( "Printed AST converted back to source of %s to %s. Contents:\n%s" + , compiledFileFullPath.begin, convertedBackToSourceFileFullPath.begin, nullableZStringToString( convertedBackToSourceText ) ); + } + + resultCode = resultSuccess; + finally: + if ( convertedBackToSourceFile != NULL ) + { + fclose( convertedBackToSourceFile ); + convertedBackToSourceFile = NULL; + } + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ convertedBackToSourceFileFullPath ); + if ( convertedBackToSourceText != NULL ) + { + zend_string_release( convertedBackToSourceText ); + convertedBackToSourceText = NULL; + } + + ELASTIC_APM_UNUSED( resultCode ); + return; + + failure: + goto finally; +} + +void debugDumpAstSubTreeToFile( StringView compiledFileFullPath, zend_ast* ast, bool isBeforeProcess ) +{ + ResultCode resultCode; + StringBuffer debugDumpFileFullPath = ELASTIC_APM_EMPTY_STRING_BUFFER; + FILE* debugDumpFile = NULL; + int errnoValue = 0; + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin ); + + StringView pathPrefix = stringBufferToView( g_astProcessDebugDumpForPathPrefix ); + if ( ! isFileSystemPathPrefix( compiledFileFullPath, pathPrefix ) ) + { + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "Skipping this file because it does not have required prefix: %s", pathPrefix.begin ); + ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); + } + + StringView compiledFileRelativePath = subStringView( compiledFileFullPath, pathPrefix.length ); + StringView isBeforeProcessSuffix = isBeforeProcess ? ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".before_AST_process" ) : ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".after_AST_process" ); + StringView debugDumpFileExtensionSuffix = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ".txt" ); + StringView debugDumpFileFullPathParts[] = { + stringBufferToView( g_astProcessDebugDumpOutDir ), + compiledFileRelativePath, + isBeforeProcessSuffix, + debugDumpFileExtensionSuffix + }; + ELASTIC_APM_CALL_IF_FAILED_GOTO( buildFileFullPath( ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( StringViewArrayView, debugDumpFileFullPathParts ), /* out */ &debugDumpFileFullPath ) ); + + ELASTIC_APM_CALL_IF_FAILED_GOTO( ensureDirectoriesExist( stringBufferToView( debugDumpFileFullPath ) ) ); + + errnoValue = openFile( debugDumpFileFullPath.begin, "w", /* out */ &debugDumpFile ); + if ( errnoValue != 0 ) + { + ELASTIC_APM_LOG_ERROR( "Failed to open file; debugDumpFileFullPath: %s; errno: %d (%s)", debugDumpFileFullPath.begin, errnoValue, streamErrNo( errnoValue, &txtOutStream ) ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + + ELASTIC_APM_LOG_INFO( "Printing AST debug dump of %s to %s ...", compiledFileFullPath.begin, debugDumpFileFullPath.begin ); + DebugDumpAstPrintToFileCtx ctx = (DebugDumpAstPrintToFileCtx){ .outFile = debugDumpFile }; + DebugDumpAstPrinter printer = (DebugDumpAstPrinter){ .printLine = &debugDumpAstPrintLineToFile, .ctx = &ctx }; + debugDumpAst( &printer, ast, /* nestingDepth */ 0 ); + ELASTIC_APM_LOG_INFO( "Printed AST debug dump of %s to %s", compiledFileFullPath.begin, debugDumpFileFullPath.begin ); + + if ( g_astProcessDebugDumpConvertedBackToSource ) + { + debugDumpAstSubTreeConvertedBackToSource( compiledFileFullPath, compiledFileRelativePath, ast, isBeforeProcessSuffix, &txtOutStream ); + } + + resultCode = resultSuccess; + finally: + ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG(); + if ( debugDumpFile != NULL ) + { + fclose( debugDumpFile ); + debugDumpFile = NULL; + } + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ debugDumpFileFullPath ); + + ELASTIC_APM_UNUSED( resultCode ); + return; + + failure: + goto finally; +} + +void debugDumpAstTree( StringView compiledFileFullPath, zend_ast* ast, bool isBeforeProcess ) +{ + LogLevel logLevel = g_astProcessDebugDumpIsEnabled ? logLevel_debug : logLevel_trace; + ELASTIC_APM_LOG_FUNCTION_ENTRY_MSG_WITH_LEVEL( logLevel, "compiledFileFullPath: %s, isBeforeProcess: %s, g_astProcessDebugDumpIsEnabled: %s" + , compiledFileFullPath.begin, boolToString( isBeforeProcess ), boolToString( g_astProcessDebugDumpIsEnabled ) ); + + debugDumpAstTreeToLog( ast, g_astProcessDebugDumpIsEnabled ? logLevel_debug : logLevel_trace ); + + if ( g_astProcessDebugDumpIsEnabled ) + { + debugDumpAstSubTreeToFile( compiledFileFullPath, ast, isBeforeProcess ); + } + + ELASTIC_APM_LOG_FUNCTION_EXIT_MSG_WITH_LEVEL( logLevel, "compiledFileFullPath: %s, isBeforeProcess: %s, g_astProcessDebugDumpIsEnabled: %s" + , compiledFileFullPath.begin, boolToString( isBeforeProcess ), boolToString( g_astProcessDebugDumpIsEnabled ) ); +} + +StringView directorySeparatorAsStringView() +{ + return ELASTIC_APM_STRING_LITERAL_TO_VIEW( +# ifdef PHP_WIN32 + "\\" +# else // #ifdef PHP_WIN32 + "/" +# endif // #ifdef PHP_WIN32 + ); +} + +ResultCode ensureTrailingDirectorySeparator( StringView inPath, /* out */ StringBuffer* result ) +{ + ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "inPath: %s", inPath.begin ); + + ResultCode resultCode; + StringBuffer outPathBuf = ELASTIC_APM_EMPTY_STRING_BUFFER; + size_t outPathLen = 0; + size_t outPathMaxLen = inPath.length + 1; + StringView directorySeparator = directorySeparatorAsStringView(); + + ELASTIC_APM_MALLOC_STRING_BUFFER_IF_FAILED_GOTO( /* maxLength */ outPathMaxLen, /* out */ outPathBuf ); + outPathBuf.begin[ 0 ] = '\0'; + + ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( /* suffixToAppend */ inPath, outPathBuf, /* in,out */ &outPathLen ) ); + if ( ! isStringViewSuffix( inPath, directorySeparator ) ) + { + ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( /* suffixToAppend */ directorySeparator, outPathBuf, /* in,out */ &outPathLen ) ); + } + + *result = outPathBuf; + outPathBuf = ELASTIC_APM_EMPTY_STRING_BUFFER; + resultCode = resultSuccess; + finally: + ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG( "inPath: %s", inPath.begin ); + return resultCode; + + failure: + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ outPathBuf ); + goto finally; + +#undef ELASTIC_APM_DIRECTORY_SEPARATOR +} + +void astProcessDebugDumpOnRequestInit( const ConfigSnapshot* config ) +{ + ResultCode resultCode; + + if ( config->astProcessDebugDumpOutDir == NULL ) + { + ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); + } + + StringView pathPrefix = config->astProcessDebugDumpForPathPrefix == NULL ? ELASTIC_APM_EMPTY_STRING_VIEW : stringToView( config->astProcessDebugDumpForPathPrefix ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( ensureTrailingDirectorySeparator( pathPrefix, /* out */ &g_astProcessDebugDumpForPathPrefix ) ); + + ELASTIC_APM_CALL_IF_FAILED_GOTO( ensureTrailingDirectorySeparator( stringToView( config->astProcessDebugDumpOutDir ), /* out */ &g_astProcessDebugDumpOutDir ) ); + + g_astProcessDebugDumpConvertedBackToSource = config->astProcessDebugDumpConvertedBackToSource; + g_astProcessDebugDumpIsEnabled = true; + resultCode = resultSuccess; + finally: + ELASTIC_APM_UNUSED( resultCode ); + return; + + failure: + goto finally; +} + +void astProcessDebugDumpOnRequestShutdown() +{ + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ g_astProcessDebugDumpOutDir ); + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ g_astProcessDebugDumpForPathPrefix ); + g_astProcessDebugDumpConvertedBackToSource = false; + g_astProcessDebugDumpIsEnabled = false; +} diff --git a/src/ext/AST_debug.h b/src/ext/AST_debug.h new file mode 100644 index 000000000..dd0bbf52a --- /dev/null +++ b/src/ext/AST_debug.h @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +#include +#include "TextOutputStream_forward_decl.h" +#include "ConfigSnapshot_forward_decl.h" +#include "LogLevel.h" +#include "basic_types.h" +#include "StringView.h" + +void astProcessDebugDumpOnRequestInit( const ConfigSnapshot* config ); +void astProcessDebugDumpOnRequestShutdown(); + +String streamZendAstKind( zend_ast_kind kind, TextOutputStream* txtOutStream ); +String streamZendAstNode( zend_ast* ast, TextOutputStream* txtOutStream ); + +void debugDumpAstTreeToLog( zend_ast* ast, LogLevel logLevel ); +void debugDumpAstTree( StringView compiledFileFullPath, zend_ast* ast, bool isBeforeProcess ); diff --git a/src/ext/AST_instrumentation.c b/src/ext/AST_instrumentation.c index 210b497fa..6cfbdcc9c 100644 --- a/src/ext/AST_instrumentation.c +++ b/src/ext/AST_instrumentation.c @@ -18,459 +18,1626 @@ */ #include "AST_instrumentation.h" +#include "ConfigSnapshot.h" +#include "ConfigManager.h" #include "log.h" +#include "AST_debug.h" +#include #include -#include -#include -#include +#include +#include +#include +#include "WordPress_instrumentation.h" #include "util.h" +#include "util_for_PHP.h" +#include "AST_util.h" +#include "elastic_apm_alloc.h" #define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_AUTO_INSTRUMENT -zend_ast_process_t original_zend_ast_process; +static bool g_isOriginalZendAstProcessSet = false; +static zend_ast_process_t g_originalZendAstProcess = NULL; -#define ZEND_AST_ALLOC( size ) zend_arena_alloc(&CG(ast_arena), size); +static bool g_isLoadingAgentPhpCode = false; -String zendAstKindToString( zend_ast_kind kind ) +void elasticApmBeforeLoadingAgentPhpCode() { -# define ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( enumMember ) \ - case enumMember: \ - return #enumMember; \ - /**/ + g_isLoadingAgentPhpCode = true; +} + +void elasticApmAfterLoadingAgentPhpCode() +{ + g_isLoadingAgentPhpCode = false; +} + +bool getStringFromAstZVal( zend_ast* astZval, /* out */ StringView* pResult ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); - switch ( kind ) + if ( astZval == NULL ) { - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ZVAL ) - #ifdef ZEND_AST_CONSTANT - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONSTANT ) - #endif - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ZNODE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FUNC_DECL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLOSURE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS ) - #ifdef ZEND_AST_ARROW_FUNC - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARROW_FUNC ) - #endif - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARG_LIST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARRAY ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ENCAPS_LIST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EXPR_LIST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STMT_LIST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_IF ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH_LIST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CATCH_LIST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARAM_LIST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLOSURE_USES ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_DECL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_DECL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST_DECL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAME_LIST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_ADAPTATIONS ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MAGIC_CONST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE ) - #ifdef ZEND_AST_CONSTANT_CLASS - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONSTANT_CLASS ) - #endif - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_VAR ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNPACK ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_PLUS ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_MINUS ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CAST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EMPTY ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ISSET ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SILENCE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SHELL_EXEC ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLONE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_EXIT ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRINT ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_INCLUDE_OR_EVAL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNARY_OP ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRE_INC ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PRE_DEC ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_POST_INC ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_POST_DEC ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_YIELD_FROM ) - #ifdef ZEND_AST_CLASS_NAME - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_NAME ) - #endif - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GLOBAL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_UNSET ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_RETURN ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_LABEL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_REF ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_HALT_COMPILER ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ECHO ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_THROW ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GOTO ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_BREAK ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONTINUE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DIM ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC_PROP ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CALL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CLASS_CONST ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_REF ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_OP ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_BINARY_OP ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GREATER ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GREATER_EQUAL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_AND ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_OR ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ARRAY_ELEM ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NEW ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_INSTANCEOF ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_YIELD ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_COALESCE ) - #ifdef ZEND_AST_ASSIGN_COALESCE - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_ASSIGN_COALESCE ) - #endif - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_WHILE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DO_WHILE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_IF_ELEM ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_SWITCH_CASE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_DECLARE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE_TRAIT ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_PRECEDENCE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD_REFERENCE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_NAMESPACE ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_USE_ELEM ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRAIT_ALIAS ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_GROUP_USE ) - #ifdef ZEND_AST_PROP_GROUP - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_GROUP ) - #endif - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_METHOD_CALL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_STATIC_CALL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONDITIONAL ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TRY ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CATCH ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARAM ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROP_ELEM ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_CONST_ELEM ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FOR ) - ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_FOREACH ) + ELASTIC_APM_LOG_TRACE( "Returning false - astZval == NULL" ); + return false; + } - default: - return "UNKNOWN"; + if ( astZval->kind != ZEND_AST_ZVAL ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - astZval->kind: %s", streamZendAstKind( astZval->kind, &txtOutStream ) ); + return false; + } + + zval* zVal = zend_ast_get_zval( astZval ); + if ( zVal == NULL ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - zVal == NULL" ); + return false; + } + int zValType = (int)Z_TYPE_P( zVal ); + if ( zValType != IS_STRING ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - zValType: %s (%d)", zend_get_type_by_const( zValType ), (int)zValType ); + return false; + } + + zend_string* zString = Z_STR_P( zVal ); + *pResult = zStringToStringView( zString ); + ELASTIC_APM_LOG_TRACE( "Returning true - with result string [length: %"PRIu64"]: %.*s", (UInt64)(pResult->length), (int)(pResult->length), pResult->begin ); + return true; +} + +bool getAstDeclName( zend_ast_decl* astDecl, /* out */ StringView* name ) +{ + if ( astDecl->name == NULL ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - astAsDecl->name == NULL" ); + return false; + } + + *name = zStringToStringView( astDecl->name ); + ELASTIC_APM_LOG_TRACE( "Returning true - name [length: %"PRIu64"]: %.*s", (UInt64)(name->length), (int)(name->length), name->begin ); + return true; +} + +bool getAstFunctionParameters( zend_ast_decl* astDecl, /* out */ ZendAstPtrArrayView* paramsAsts ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + ELASTIC_APM_ASSERT( astDecl->kind == ZEND_AST_FUNC_DECL || astDecl->kind == ZEND_AST_METHOD, "astDecl->kind: %s", streamZendAstKind( astDecl->kind, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); + ELASTIC_APM_ASSERT_VALID_PTR( paramsAsts ); + + // function list of parameters is always child[ 0 ] - see zend_compile_func_decl + zend_ast* astFuncParams = astDecl->child[ 0 ]; + if ( ! ( ( astFuncParams->kind == ZEND_AST_PARAM_LIST ) && zend_ast_is_list( astFuncParams ) ) ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - zend_ast_is_list( astFuncParams ): %s, astFuncParams->kind: %s" + , boolToString( zend_ast_is_list( astFuncParams ) ), streamZendAstKind( astFuncParams->kind, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); + return false; + } + zend_ast_list* astFuncParamsAsList = zend_ast_get_list( astFuncParams ); + + paramsAsts->count = astFuncParamsAsList->children; + paramsAsts->values = &( astFuncParamsAsList->child[ 0 ] ); + + return true; +} + +bool getAstFunctionParameterName( zend_ast_decl* astDecl, unsigned int parameterIndex, /* out */ StringView* name ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + ZendAstPtrArrayView paramsAsts; + + if ( ! getAstFunctionParameters( astDecl, /* out */ ¶msAsts ) ) + { + return false; + } + + if ( parameterIndex >= paramsAsts.count ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - paramsAsts.count: %u, parameterIndex: %u", (unsigned)paramsAsts.count, parameterIndex ); + return false; + } + + zend_ast* param = paramsAsts.values[ parameterIndex ]; + if ( param == NULL ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - param == NULL" ); + return false; + } + + if ( param->kind != ZEND_AST_PARAM ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - param->kind: %s", streamZendAstKind( param->kind, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); + return false; + } + + if ( zend_ast_get_num_children( param ) == 0 ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - zend_ast_get_num_children( param ): %d", (int)zend_ast_get_num_children( param ) ); + return false; } -# undef ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE + + // parameter name is in child[ 1 ] + return getStringFromAstZVal( param->child[ 1 ], /* out */ name ); } -static inline -size_t calcAstListAllocSize( uint32_t children ) +zend_string* createZStringForAst( StringView inStr ) { - return sizeof( zend_ast_list ) - sizeof( zend_ast* ) + sizeof( zend_ast* ) * children; + return zend_string_init( inStr.begin, inStr.length, /* persistent: */ false ); } -static -zend_ast* createZvalAst( zval* zv, uint32_t attr, uint32_t lineno ) +bool isZendAstListKind( zend_ast_kind kind ) { - #if PHP_VERSION_ID >= 70300 /* if PHP version is 7.3.0 and later */ - zend_ast* ast = zend_ast_create_zval_with_lineno( zv, lineno ); - ast->attr = attr; + return ((kind >> ZEND_AST_IS_LIST_SHIFT) & 1) != 0; +} + +/** + * zend_ast_create and zend_ast_create_ex allowed up to 4 child* parameters for version before PHP v8 + * and the limit was increased to 5 in PHP v8 + * + * @see ZEND_AST_SPEC_CALL_EX + */ +static size_t g_maxCreateAstChildCount = + #if PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) + 4 #else - zend_ast* ast = zend_ast_create_zval_with_lineno( zv, attr, lineno ); + 5 #endif +; + +zend_ast* createAstEx( zend_ast_kind kind, zend_ast_attr attr, ZendAstPtrArrayView children ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + ELASTIC_APM_ASSERT_LE_UINT64( children.count, g_maxCreateAstChildCount ); + ELASTIC_APM_ASSERT( ! isZendAstListKind( kind ), "kind: %s", streamZendAstKind( kind, &txtOutStream ) ); - return ast; + switch( children.count ) + { + case 0: + return zend_ast_create_ex( kind, attr ); + case 1: + return zend_ast_create_ex( kind, attr, children.values[ 0 ] ); + case 2: + return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ] ); + case 3: + return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ], children.values[ 2 ] ); + case 4: + return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ], children.values[ 2 ], children.values[ 3 ] ); + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) + case 5: + return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ], children.values[ 2 ], children.values[ 3 ], children.values[ 4 ] ); + #endif + } } -static -zend_ast* createStringAst( char* str, size_t len, uint32_t attr, uint32_t lineno ) +ResultCode createAstExCheckChildrenCount( zend_ast_kind kind, zend_ast_attr attr, ZendAstPtrArrayView children, /* out */ zend_ast** pResult ) { - zval zv; - zend_ast* ast; - ZVAL_NEW_STR( &zv, zend_string_init( str, len, 0 ) ); - ast = createZvalAst( &zv, attr, /* lineno */ 0 ); - return ast; + ResultCode resultCode; + + if ( children.count > g_maxCreateAstChildCount ) + { + ELASTIC_APM_LOG_ERROR( "Number of children is larger than max; children.count: %u, g_maxCreateAstChildCount: %u", (unsigned)children.count, (unsigned)g_maxCreateAstChildCount ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE_EX( resultFailure ); + } + + *pResult = createAstEx( kind, attr, children ); + + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; } -static -zend_ast* createCatchTypeAst( uint32_t lineno ) +zend_ast* createAstWithAttribute( zend_ast_kind kind, zend_ast_attr attr ) { - zend_ast * name = createStringAst( "Throwable", sizeof( "Throwable" ) - 1, ZEND_NAME_FQ, lineno ); - return zend_ast_create_list( 1, ZEND_AST_NAME_LIST, name ); + return createAstEx( kind, attr, ELASTIC_APM_MAKE_EMPTY_ARRAY_VIEW( ZendAstPtrArrayView ) ); } -static -zend_ast* createCatchAst( uint32_t lineno ) +zend_ast* createAstWithAttributeAndOneChild( zend_ast_kind kind, zend_ast_attr attr, zend_ast* child ) { - zend_ast * exVarNameAst = createStringAst( "ex", sizeof( "ex" ) - 1, /* attr: */ 0, lineno ); - zend_ast * catchTypeAst = createCatchTypeAst( lineno ); + return createAstEx( kind, attr, ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, /* count */ 1, &child ) ); +} - zend_ast * instrumentationPostHookCallAst = - zend_ast_create( ZEND_AST_CALL - , createStringAst( "instrumentationPostHookException", sizeof( "instrumentationPostHookException" ) - 1, ZEND_NAME_FQ, lineno ) - , zend_ast_create_list( 1 - , ZEND_AST_ARG_LIST - , zend_ast_create( ZEND_AST_VAR - , createStringAst( "ex", sizeof( "ex" ) - 1, /* attr: */ 0, lineno ) ) - ) ); +zend_ast* createAstWithAttributeAndTwoChildren( zend_ast_kind kind, zend_ast_attr attr, zend_ast* child0, zend_ast* child1 ) +{ + zend_ast* children[] = { child0, child1 }; + return createAstEx( kind, attr, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ZendAstPtrArrayView, children ) ); +} - zend_ast * throwAst = zend_ast_create( ZEND_AST_THROW, instrumentationPostHookCallAst ); - throwAst->lineno = lineno; - zend_ast * throwStmtListAst = zend_ast_create_list( 1, ZEND_AST_STMT_LIST, throwAst ); - throwStmtListAst->lineno = lineno; - zend_ast * catchAst = zend_ast_create_list( 1 - , ZEND_AST_CATCH_LIST - , zend_ast_create( ZEND_AST_CATCH - , catchTypeAst - , exVarNameAst - , throwStmtListAst ) ); - catchAst->lineno = lineno; - return catchAst; +zend_ast* createAstWithAttributeAndThreeChildren( zend_ast_kind kind, zend_ast_attr attr, zend_ast* child0, zend_ast* child1, zend_ast* child2 ) +{ + zend_ast* children[] = { child0, child1, child2 }; + return createAstEx( kind, attr, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ZendAstPtrArrayView, children ) ); } -struct TransformContext +zend_ast* createAstWithOneChild( zend_ast_kind kind, zend_ast* child ) { - bool isInsideFunction; - bool isFunctionRetByRef; -}; -typedef struct TransformContext TransformContext; + return createAstWithAttributeAndOneChild( kind, /* attr */ 0, child ); +} -TransformContext g_transformContext = { .isInsideFunction = false, .isFunctionRetByRef = false }; +zend_ast* createAstWithTwoChildren( zend_ast_kind kind, zend_ast* child0, zend_ast* child1 ) +{ + return createAstWithAttributeAndTwoChildren( kind, /* attr */ 0, child0, child1 ); +} -TransformContext makeTransformContext( TransformContext* base, bool isInsideFunction, bool isFunctionRetByRef ) +zend_ast* createAstWithThreeChildren( zend_ast_kind kind, zend_ast* child0, zend_ast* child1, zend_ast* child2 ) { - TransformContext transformCtx = *base; - transformCtx.isInsideFunction = isInsideFunction; - transformCtx.isFunctionRetByRef = isFunctionRetByRef; - return transformCtx; + return createAstWithAttributeAndThreeChildren( kind, /* attr */ 0, child0, child1, child2 ); } -static -zend_ast* transformAst( zend_ast* ast, int nestingDepth ); +zend_ast* createAstMagicConst( zend_ast_attr attr, uint32_t lineNumber ) +{ + zend_ast* result = createAstWithAttribute( ZEND_AST_MAGIC_CONST, attr ); + result->lineno = lineNumber; + return result; +} -static -zend_ast* transformFunctionAst( zend_ast* originalAst, int nestingDepth ) +zend_ast* createAstMagicConst__FUNCTION__( uint32_t lineNumber ) +{ + return createAstMagicConst( T_FUNC_C, lineNumber ); +} + +zend_ast* createAstMagicConst__CLASS__( uint32_t lineNumber ) +{ + return createAstMagicConst( T_CLASS_C, lineNumber ); +} + +zend_ast* createAstZValWithAttribute( zval* zv, zend_ast_attr attr, uint32_t lineNumber ) +{ + zend_ast* result = zend_ast_create_zval_with_lineno( + zv, + #if PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version before 7.3.0 */ + attr, + #endif + lineNumber + ); + + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */ + result->attr = attr; + #endif + + return result; +} + +zend_ast* createAstZValStringWithAttribute( StringView inStr, zend_ast_attr attr, uint32_t lineNumber ) +{ + zend_string* asZString = createZStringForAst( inStr ); + zval stringAsZVal; + ZVAL_NEW_STR( &stringAsZVal, asZString ); + return createAstZValWithAttribute( &stringAsZVal, attr, lineNumber ); +} + +zend_ast* createAstZValString( StringView inStr, uint32_t lineNumber ) +{ + return createAstZValStringWithAttribute( inStr, /* attr */ 0, lineNumber ); +} + +zend_ast* createAstVar( StringView name, uint32_t lineNumber ) +{ + // ZEND_AST_VAR (256) (line: 121) + // ZEND_AST_ZVAL (64) (line: 483) [type: string, value: hook_name] + + return createAstWithOneChild( /* kind */ ZEND_AST_VAR, createAstZValString( name, lineNumber ) ); +} + +zend_ast* createAstConst( StringView name, zend_ast_attr nameAstAttr, uint32_t lineNumber ) +{ + // ZEND_AST_CONST (line: 20, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 20, attr: 1) [type: string, value: null] + + zend_ast* nameAst = createAstZValString( name, lineNumber ); + nameAst->attr = nameAstAttr; + return createAstWithOneChild( /* kind */ ZEND_AST_CONST, /* child0 */ nameAst ); +} + +zend_ast* createAstConstNull( uint32_t lineNumber ) +{ + return createAstConst( ELASTIC_APM_STRING_LITERAL_TO_VIEW( "null" ), ZEND_NAME_NOT_FQ, lineNumber ); +} + +zend_ast* createAstGlobalConst( StringView name, uint32_t lineNumber ) +{ + return createAstConst( name, /* nameAstAttr */ 0, lineNumber ); +} + +/** + * @see zend_ast_create_list_* in zend_ast.h + */ +static size_t g_elasticApmCreateAstListExChildrenCount = 2; + +zend_ast* createAstListEx( zend_ast_kind kind, zend_ast_attr attr, ZendAstPtrArrayView children ) { char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( originalAst->kind ) ); - - zend_ast * transformedAst; - zend_ast_decl* funcDeclAst = (zend_ast_decl*) originalAst; - - TransformContext savedTransformCtx = makeTransformContext( - &g_transformContext - , /* isInsideFunction: */ true - , /* isFunctionRetByRef */ funcDeclAst->flags & ZEND_ACC_RETURN_REFERENCE ); - - if ( ! isStringViewPrefixIgnoringCase( stringToView( ZSTR_VAL( funcDeclAst->name ) ) - , ELASTIC_APM_STRING_LITERAL_TO_VIEW( "functionToInstrument" ) ) ) - { - transformedAst = originalAst; - goto finally; - } - - zend_ast_list* funcStmtListAst = zend_ast_get_list( funcDeclAst->child[ 2 ] ); - - // guess at feasible line numbers - uint32_t funcStmtBeginLineNumber = funcStmtListAst->lineno; - uint32_t funcStmtEndLineNumber = funcStmtListAst->child[ funcStmtListAst->children - 1 ]->lineno; - - zend_ast * callInstrumentationPreHookAst = - zend_ast_create( - ZEND_AST_CALL - , createStringAst( "instrumentationPreHook", sizeof( "instrumentationPreHook" ) - 1, ZEND_NAME_FQ, originalAst->lineno ) - , zend_ast_create_list( 1 - , ZEND_AST_ARG_LIST - , zend_ast_create( ZEND_AST_CALL - , createStringAst( "func_get_args", sizeof( "func_get_args" ) - 1, ZEND_NAME_FQ, originalAst->lineno ) - , zend_ast_create_list( 0, ZEND_AST_ARG_LIST ) ) - ) ); - - zend_ast * callInstrumentationPostHookAst = - zend_ast_create( - ZEND_AST_CALL - , createStringAst( "instrumentationPostHookRetVoid", sizeof( "instrumentationPostHookRetVoid" ) - 1, ZEND_NAME_FQ, originalAst->lineno ) - , zend_ast_create_list( 0, ZEND_AST_ARG_LIST ) ); - - zend_ast * catchAst = createCatchAst( funcStmtEndLineNumber ); - zend_ast * finallyAst = NULL; - - zend_ast * tryCatchAst = zend_ast_create( ZEND_AST_TRY, transformAst( funcDeclAst->child[ 2 ], nestingDepth + 1 ), catchAst, finallyAst ); - tryCatchAst->lineno = funcStmtBeginLineNumber; - zend_ast_list* newFuncBodyAst = ZEND_AST_ALLOC( calcAstListAllocSize( 3 ) ); - newFuncBodyAst->kind = ZEND_AST_STMT_LIST; - newFuncBodyAst->lineno = funcStmtBeginLineNumber; - newFuncBodyAst->children = 3; - newFuncBodyAst->child[ 0 ] = callInstrumentationPreHookAst; - newFuncBodyAst->child[ 1 ] = tryCatchAst; - newFuncBodyAst->child[ 2 ] = callInstrumentationPostHookAst; - funcDeclAst->child[ 2 ] = (zend_ast*) newFuncBodyAst; - transformedAst = originalAst; - finally: - g_transformContext = savedTransformCtx; - textOutputStreamRewind( &txtOutStream ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( transformedAst->kind ) ); - return transformedAst; + ELASTIC_APM_ASSERT_LE_UINT64( children.count, g_elasticApmCreateAstListExChildrenCount ); + ELASTIC_APM_ASSERT( isZendAstListKind( kind ), "kind: %s", streamZendAstKind( kind, &txtOutStream ) ); + + zend_ast* result = NULL; + + switch( children.count ) + { + case 0: + result = zend_ast_create_list( children.count, kind ); + break; + case 1: + result = zend_ast_create_list( children.count, kind, children.values[ 0 ] ); + break; + case 2: + result = zend_ast_create_list( children.count, kind, children.values[ 0 ], children.values[ 1 ] ); + break; + } + + zend_ast_list* resultAsList = (zend_ast_list*)result; + resultAsList->attr = attr; + return result; +} + +zend_ast* createAstListWithAttribute( zend_ast_kind kind, zend_ast_attr attr, uint32_t lineNumber ) +{ + zend_ast* result = createAstListEx( kind, attr, ELASTIC_APM_MAKE_EMPTY_ARRAY_VIEW( ZendAstPtrArrayView ) ); + ((zend_ast_list*)result)->lineno = lineNumber; + return result; +} + +zend_ast* createAstList( zend_ast_kind kind, uint32_t lineNumber ) +{ + return createAstListWithAttribute( kind, /* attr */ 0, lineNumber ); } -static -zend_ast* transformReturnAst( zend_ast* originalAst, int nestingDepth ) +void addChildToAstList( zend_ast* child, /* in,out */ zend_ast** pInSrcListOutNewList ) { + ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( pInSrcListOutNewList ); + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( originalAst->kind ) ); + ELASTIC_APM_ASSERT( zend_ast_is_list( *pInSrcListOutNewList ), "kind: %s", streamZendAstKind( (*pInSrcListOutNewList)->kind, &txtOutStream ) ); + + zend_ast* newList = zend_ast_list_add( /* in */ *pInSrcListOutNewList, child ); + *pInSrcListOutNewList = newList; +} + +zend_ast* createAstListWithOneChild( zend_ast_kind kind, zend_ast* child ) +{ + return createAstListEx( kind, /* attr */ 0, ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, /* count */ 1, &child ) ); +} + +zend_ast* createAstListWithTwoChildren( zend_ast_kind kind, zend_ast* child0, zend_ast* child1 ) +{ + zend_ast* children[] = { child0, child1 }; + return createAstListEx( kind, /* attr */ 0, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ZendAstPtrArrayView, children ) ); +} + +zend_ast* createAstListWithThreeChildren( zend_ast_kind kind, zend_ast* child0, zend_ast* child1, zend_ast* child2 ) +{ + zend_ast* result = createAstListWithTwoChildren( kind, child0, child1 ); + addChildToAstList( child2, /* in,out */ &result ); + return result; +} + +ResultCode createCapturedArgsAstArray( zend_ast_decl* astDecl, ArgCaptureSpecArrayView argCaptureSpecs, uint32_t lineNumber, /* out */ zend_ast** pResult ) +{ + // AST for PHP code: + // + // (..., [$hook_name, &$callback]) + // ^^^^^^^^^^^^^^^^^^^^^^^^ - captured args AST array + // + // ZEND_AST_ARRAY (129) (line: 121, attr: 3) <- [$hook_name, /* ref */ &$callback] and 3 == ZEND_ARRAY_SYNTAX_SHORT + // ZEND_AST_ARRAY_ELEM (526) (line: 121, attr: 0) + // ZEND_AST_VAR (256) (line: 121, attr: 0) + // ZEND_AST_ZVAL (64) (line: 121, attr: 0) [type: string, value: hook_name] + // NULL + // ZEND_AST_ARRAY_ELEM (526) (line: 121, attr: 1) <- attr == 1 because callback variable is taken by reference + // ZEND_AST_VAR (256) (line: 121, attr: 0) + // ZEND_AST_ZVAL (64) (line: 121, attr: 0) [type: string, value: callback] + // NULL + + ELASTIC_APM_ASSERT_VALID_OUT_PTR_TO_PTR( pResult ); - zend_ast * transformedAst; + ResultCode resultCode; + zend_ast* result = createAstListWithAttribute( ZEND_AST_ARRAY, /* attr */ ZEND_ARRAY_SYNTAX_SHORT, lineNumber ); - // if there isn't an active function then don't wrap it - // e.g. return at file scope - if ( ! g_transformContext.isInsideFunction ) + ELASTIC_APM_FOR_EACH_INDEX( i, argCaptureSpecs.count ) { - transformedAst = originalAst; - goto finally; + StringView parameterName; + ArgCaptureSpec argCaptureSpec = argCaptureSpecs.values[ i ]; + if ( argCaptureSpec == dontCaptureArg ) + { + continue; + } + ELASTIC_APM_ASSERT( argCaptureSpec == captureArgByRef || argCaptureSpec == captureArgByValue, "argCaptureSpec: %d, i: %d", argCaptureSpec, (int)i ); + // ZEND_AST_ARRAY_ELEM attribute should be 1 when passed by reference and 0 when passed by value + zend_ast_attr arrayElemAttr = argCaptureSpec == captureArgByRef ? 1 : 0; + if ( ! getAstFunctionParameterName( astDecl, /* parameterIndex */ i, /* out */ &( parameterName ) ) ) + { + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + zend_ast* varAst = createAstVar( parameterName, lineNumber ); + // Array element value is the first child (i.e., index 0) and array element key is the second child (i.e., index 1) + zend_ast* arrayElement = createAstWithAttributeAndTwoChildren( ZEND_AST_ARRAY_ELEM, arrayElemAttr, /* array element value */ varAst, /* array element key */ NULL ); + addChildToAstList( arrayElement, /* in,out */ &result ); } - zend_ast * returnExprAst = originalAst->child[ 0 ]; - // If it's an empty return; - if ( returnExprAst == NULL ) + *pResult = result; + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; +} + +zend_ast* createAstStandaloneFunctionCall( StringView funcName, bool isFullyQualified, zend_ast* astArgList ) +{ + // AST for PHP code: + // + // \elastic_apm_ast_instrumentation_pre_hook(__CLASS__, __FUNCTION__, [$hook_name, &$callback]) + // + // ZEND_AST_CALL (line: 15, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 15, attr: 0) [type: string, value: elastic_apm_ast_instrumentation_pre_hook] + // ZEND_AST_ARG_LIST (line: 15, attr: 0, childCount: 3) + + uint32_t lineNumber = zend_ast_get_lineno( astArgList ); + zend_ast_attr zValAttr = isFullyQualified ? ZEND_NAME_FQ : ZEND_NAME_NOT_FQ; + return createAstWithTwoChildren( ZEND_AST_CALL, createAstZValStringWithAttribute( funcName, zValAttr, lineNumber ), astArgList ); +} + +zend_ast* createAstStandaloneFqFunctionCall( StringView funcName, zend_ast* astArgList ) +{ + return createAstStandaloneFunctionCall( funcName, /* isFullyQualified */ true, astArgList ); +} + +zend_ast* createAstStandaloneNotFqFunctionCall( StringView funcName, zend_ast* astArgList ) +{ + return createAstStandaloneFunctionCall( funcName, /* isFullyQualified */ false, astArgList ); +} + +ResultCode createPreHookAstArgListByCaptureSpec( zend_ast_decl* astDecl, ArgCaptureSpecArrayView argCaptureSpecs, /* out */ zend_ast** pResult ) +{ + // AST for PHP code: + // + // \elastic_apm_ast_instrumentation_pre_hook(, __FUNCTION__, [$hook_name, &$callback]) + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // is __CLASS__ for methods and null for standalone functions + // + // ZEND_AST_ARG_LIST (line: 15, attr: 0, childCount: 3) + // ZEND_AST_MAGIC_CONST (line: 15, attr: __CLASS__) + // ZEND_AST_MAGIC_CONST (line: 15, attr: __FUNCTION__) + // ZEND_AST_ARRAY (line: 15, attr: 3, childCount: 2) + + ELASTIC_APM_ASSERT_VALID_OUT_PTR_TO_PTR( pResult ); + + ResultCode resultCode; + uint32_t lineNumber = astDecl->start_lineno; + zend_ast* capturedArgsAstArray = NULL; + + ELASTIC_APM_CALL_IF_FAILED_GOTO( createCapturedArgsAstArray( astDecl, argCaptureSpecs, lineNumber, /* out */ &capturedArgsAstArray ) ); + + *pResult = createAstListWithThreeChildren( + ZEND_AST_ARG_LIST + , astDecl->kind == ZEND_AST_METHOD ? createAstMagicConst__CLASS__( lineNumber ) : createAstConstNull( lineNumber ) + , createAstMagicConst__FUNCTION__( lineNumber ) + , capturedArgsAstArray + ); + resultCode = resultSuccess; + finally: + ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG(); + return resultCode; + + failure: + goto finally; +} + +static StringView g_elastic_apm_ast_instrumentation_pre_hook_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "elastic_apm_ast_instrumentation_pre_hook" ); + +/** + * function body is always child[ 2 ] + * + * @see zend_compile_func_decl + */ +static const size_t g_funcDeclBodyChildIndex = 2; + +ResultCode insertAstForFunctionPreHook( zend_ast_decl* funcAstDecl, ArgCaptureSpecArrayView argCaptureSpecs ) +{ + // Before: + // + // function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { + // //////////////////////////// + // // original function body // + // //////////////////////////// + // } + // + // ZEND_AST_FUNC_DECL (name: add_filter, line: 7, flags: 0, attr: 0, childCount: 4) + // ZEND_AST_PARAM_LIST (line: 7, attr: 0, childCount: 4) + // NULL + // ZEND_AST_STMT_LIST (line: 7, attr: 0, childCount: 4) <- original function body + // NULL + // + // After: + // + // function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { /* fold-into-one-line-begin */ + // \elastic_apm_ast_instrumentation_pre_hook( /* pre-hook args */ ); + // { /* fold-into-one-line-end */ + // //////////////////////////// + // // original function body // + // //////////////////////////// + // } } + // + // ZEND_AST_FUNC_DECL (name: add_filter, line: 24, flags: 0, attr: 0, childCount: 4) + // ZEND_AST_PARAM_LIST (line: 24, attr: 0, childCount: 4) + // NULL + // ZEND_AST_STMT_LIST (line: 24, attr: 0, childCount: 2) <- new function body + // ZEND_AST_CALL (line: 24, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 24, attr: 0) [type: string, value: elastic_apm_ast_instrumentation_pre_hook] + // ZEND_AST_ARG_LIST (line: 24, attr: 0, childCount: 3) <- pre-hook args + // ZEND_AST_STMT_LIST (line: 24, attr: 0, childCount: 4) <- original function body + // NULL + + ELASTIC_APM_ASSERT_VALID_PTR( funcAstDecl ); + + ResultCode resultCode; + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + String dbgCompiledFileName = stringIfNotNullElse( nullableZStringToStringView( CG(compiled_filename) ).begin, "" ); + + ELASTIC_APM_ASSERT( funcAstDecl->kind == ZEND_AST_FUNC_DECL || funcAstDecl->kind == ZEND_AST_METHOD, "funcAstDecl->kind: %s", streamZendAstKind( funcAstDecl->kind, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); + + StringView dbgFuncName; + if ( ! getAstDeclName( funcAstDecl, /* out */ &dbgFuncName ) ) { - zend_ast * callInstrumentationPostHookAst = zend_ast_create( - ZEND_AST_CALL - , createStringAst( "instrumentationPostHookRetVoid", sizeof( "instrumentationPostHookRetVoid" ) - 1, ZEND_NAME_FQ, originalAst->lineno ) - , zend_ast_create_list( 0, ZEND_AST_ARG_LIST ) ); - transformedAst = zend_ast_create_list( 2, ZEND_AST_STMT_LIST, callInstrumentationPostHookAst, originalAst ); - goto finally; + ELASTIC_APM_LOG_ERROR( "Failed to get function name - returning failure" ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "dbgFuncName: %s, compiled_filename: %s", dbgFuncName.begin, dbgCompiledFileName ); + debugDumpAstTreeToLog( (zend_ast*) funcAstDecl, logLevel_debug ); - // Either: return by reference or not - char* name; - size_t len; - if ( g_transformContext.isFunctionRetByRef ) + zend_ast* originalFuncBodyAst = funcAstDecl->child[ g_funcDeclBodyChildIndex ]; + if ( originalFuncBodyAst == NULL ) { - name = "instrumentationPostHookRetByRef"; - len = sizeof( "instrumentationPostHookRetByRef" ) - 1; + ELASTIC_APM_LOG_TRACE( "originalFuncBodyAst == NULL" ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } - else + if ( originalFuncBodyAst->kind != ZEND_AST_STMT_LIST ) { - name = "instrumentationPostHookRetNotByRef"; - len = sizeof( "instrumentationPostHookRetNotByRef" ) - 1; + ELASTIC_APM_LOG_TRACE( "Expected originalFuncBodyAst->kind to be ZEND_AST_STMT_LIST but it is %s", streamZendAstKind( originalFuncBodyAst->kind, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } - zend_ast * callInstrumentationPostHookAst = zend_ast_create( - ZEND_AST_CALL - , createStringAst( name, len, ZEND_NAME_FQ, originalAst->lineno ) - , zend_ast_create_list( 1, ZEND_AST_ARG_LIST, returnExprAst ) ); - originalAst->child[ 0 ] = callInstrumentationPostHookAst; - transformedAst = originalAst; + zend_ast* preHookCallAstArgList = NULL; + ELASTIC_APM_CALL_IF_FAILED_GOTO( createPreHookAstArgListByCaptureSpec( funcAstDecl, argCaptureSpecs, /* out */ &preHookCallAstArgList ) ); + + funcAstDecl->child[ g_funcDeclBodyChildIndex ] = createAstListWithTwoChildren( + ZEND_AST_STMT_LIST + , createAstStandaloneFqFunctionCall( g_elastic_apm_ast_instrumentation_pre_hook_funcName, preHookCallAstArgList ) + , originalFuncBodyAst + ); + + resultCode = resultSuccess; finally: - textOutputStreamRewind( &txtOutStream ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( transformedAst->kind ) ); - return transformedAst; + ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG(); + debugDumpAstTreeToLog( (zend_ast*) funcAstDecl, logLevel_debug ); + return resultCode; + + failure: + goto finally; +} + +zend_ast* createDirectCallAstArgList( uint32_t lineNumber, StringView constNameForMethodName ) +{ + // PHP code: + // + // \elastic_apm_ast_instrumentation_direct_call(\ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // + // AST: + // + // ZEND_AST_ARG_LIST (line: 63, attr: 0, childCount: 1) + // ZEND_AST_CONST (line: 63, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 63, attr: 0) [type: string, value: ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS] + + return createAstListWithOneChild( ZEND_AST_ARG_LIST, createAstGlobalConst( constNameForMethodName, lineNumber ) ); } -static -zend_ast* transformChildrenAst( zend_ast* ast, int nestingDepth ) +static StringView g_elastic_apm_ast_instrumentation_direct_call_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "elastic_apm_ast_instrumentation_direct_call" ); + +ResultCode appendDirectCallToInstrumentation( zend_ast_decl** pAstChildSlot, StringView constNameForMethodName ) { + // Before: + // + // function _wp_filter_build_unique_id( $hook_name, $callback, $priority ) { + // // ... + // } + // + // ZEND_AST_FUNC_DECL (name: _wp_filter_build_unique_id, line: 44, flags: 0, attr: 0, childCount: 4) <- original function declaration + // + // After: + // + // { function _wp_filter_build_unique_id($hook_name, $callback, $priority ) { markerForElasticApmTestsFoldAstIntoOneLineBegin(); + // // ... + // } } /* fold-into-one-line-begin */ + // \elastic_apm_ast_instrumentation_direct_call(/* direct call args */); + // /* fold-into-one-line-end */ } + // + // ZEND_AST_STMT_LIST (line: 44, attr: 0, childCount: 2) + // ZEND_AST_FUNC_DECL (name: _wp_filter_build_unique_id, line: 44, flags: 0, attr: 0, childCount: 4) <- original function declaration + // ZEND_AST_CALL (line: 63, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 63, attr: 0) [type: string, value: elastic_apm_ast_instrumentation_direct_call] + + ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( pAstChildSlot ); + + ResultCode resultCode; char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( ast->kind ) ); + String dbgCompiledFileName = stringIfNotNullElse( nullableZStringToStringView( CG(compiled_filename) ).begin, "" ); + zend_ast_decl* appendToAstDecl = *pAstChildSlot; + uint32_t lineNumber = appendToAstDecl->end_lineno; + + ELASTIC_APM_ASSERT( appendToAstDecl->kind == ZEND_AST_FUNC_DECL, "appendToAst->kind: %s", streamZendAstKind( appendToAstDecl->kind, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); + + StringView dbgFuncName; + if ( ! getAstDeclName( appendToAstDecl, /* out */ &dbgFuncName ) ) + { + ELASTIC_APM_LOG_ERROR( "Failed to get function name - returning failure" ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "dbgFuncName: %s, compiled_filename: %s", dbgFuncName.begin, dbgCompiledFileName ); + debugDumpAstTreeToLog( (zend_ast*) ( *pAstChildSlot ), logLevel_debug ); + + zend_ast* appendedCallAstArgList = createDirectCallAstArgList( lineNumber, constNameForMethodName ); + zend_ast* appendedCallAst = createAstStandaloneFqFunctionCall( g_elastic_apm_ast_instrumentation_direct_call_funcName, appendedCallAstArgList ); + + *((zend_ast**)pAstChildSlot) = createAstListWithTwoChildren( ZEND_AST_STMT_LIST, (zend_ast*) appendToAstDecl, appendedCallAst ); + + resultCode = resultSuccess; + finally: + ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG( "dbgFuncName: %s, compiled_filename: %s", dbgFuncName.begin, dbgCompiledFileName ); + debugDumpAstTreeToLog( (zend_ast*) ( *pAstChildSlot ), logLevel_debug ); + return resultCode; + + failure: + goto finally; +} + +static StringView g_wrappedFunctionNewNameSuffix = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "ElasticApmWrapped" ); + +ResultCode createWrappedFunctionNewName( StringView originalName, /* out */ StringBuffer* pResult ) +{ + ResultCode resultCode; + StringBuffer result = ELASTIC_APM_EMPTY_STRING_BUFFER; + size_t newNameLength = originalName.length + g_wrappedFunctionNewNameSuffix.length; + size_t contentLength = 0; + + ELASTIC_APM_MALLOC_STRING_BUFFER_IF_FAILED_GOTO( /* maxLength */ newNameLength, /* out */ result ); + result.begin[ 0 ] = '\0'; + + ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( originalName, result, /* in,out */ &contentLength ) ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( appendToStringBuffer( g_wrappedFunctionNewNameSuffix, result, /* in,out */ &contentLength ) ); + ELASTIC_APM_ASSERT_EQ_UINT64( contentLength, newNameLength ); + + *pResult = result; + result = ELASTIC_APM_EMPTY_STRING_BUFFER; + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ result ); + goto finally; +} + +zend_string* cloneZStringForAst( zend_string* src ) +{ + if ( src == NULL ) + { + return NULL; + } + + return createZStringForAst( zStringToStringView( src ) ); +} + +zend_ast* cloneAstZVal( zend_ast* ast, uint32_t lineNumber ) +{ + zval clonedZVal; + ZVAL_COPY( /* out */ &clonedZVal, zend_ast_get_zval( ast ) ); + return createAstZValWithAttribute( &clonedZVal, ast->attr, lineNumber ); +} - zend_ast * transformedAst = ast; +// ZEND_AST_CONSTANT was added in PHP 7.3 +#if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */ +zend_ast* cloneAstConstant( zend_ast* ast, uint32_t lineNumber ) +{ + zend_ast* result = zend_ast_create_constant( zend_ast_get_constant_name( ast ), ast->attr ); + zval* pResultZVal = zend_ast_get_zval( result ); + Z_LINENO_P( pResultZVal ) = lineNumber; + return result; +} +#endif - uint32_t childrenCount = zend_ast_get_num_children( ast ); - ELASTIC_APM_FOR_EACH_INDEX( i, childrenCount ) +ResultCode cloneAstTree( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult ); + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "misc-no-recursion" +ResultCode cloneAstDecl( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult ) +{ + ResultCode resultCode; + ZendAstPtrArrayView children = getAstChildren( ast ); + zend_ast_decl* astDecl = (zend_ast_decl*)ast; + zend_ast** clonedChildren = NULL; + + if ( children.count != elasticApmZendAstDeclChildrenCount ) { - if ( ast->child[ i ] != NULL ) + ELASTIC_APM_LOG_ERROR( "Number of children is not as expected; children.count: %u, elasticApmZendAstDeclChildrenCount: %u" + , (unsigned)children.count, (unsigned)elasticApmZendAstDeclChildrenCount ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE_EX( resultFailure ); + } + + clonedChildren = emalloc( sizeof( zend_ast* ) * children.count ); + ELASTIC_APM_FOR_EACH_INDEX( i, children.count ) + { + clonedChildren[ i ] = NULL; + ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstTree( children.values[ i ], lineNumber, /* out */ &( clonedChildren[ i ] ) ) ); + } + + *pResult = zend_ast_create_decl( + astDecl->kind + , astDecl->flags + , lineNumber /* <- start_lineno */ + , cloneZStringForAst( astDecl->doc_comment ) + , cloneZStringForAst( astDecl->name ) + , clonedChildren[ 0 ] + , clonedChildren[ 1 ] + , clonedChildren[ 2 ] + , clonedChildren[ 3 ] + /** + * number of child* parameters accepted by zend_ast_create_decl + * 4 before PHP v8.0.0 + * 5 from PHP v8.0.0 + */ + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) + , clonedChildren[ 4 ] + #endif + ); + + resultCode = resultSuccess; + finally: + if ( clonedChildren != NULL ) + { + efree( clonedChildren ); + clonedChildren = NULL; + } + return resultCode; + + failure: + goto finally; +} + +ResultCode cloneAstList( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult ) +{ + ResultCode resultCode; + zend_ast_list* astList = zend_ast_get_list( ast ); + zend_ast* result = createAstListWithAttribute( astList->kind, astList->attr, lineNumber ); + ELASTIC_APM_FOR_EACH_INDEX( i, astList->children ) + { + zend_ast* clonedChildAst = NULL; + ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstTree( astList->child[ i ], lineNumber, /* out */ &clonedChildAst ) ); + addChildToAstList( clonedChildAst, /* in,out */ &result ); + } + + *pResult = result; + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; +} + +ResultCode cloneFallbackAst( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult ) +{ + ResultCode resultCode; + ZendAstPtrArrayView children = getAstChildren( ast ); + zend_ast** clonedChildren = NULL; + + if ( children.count != 0 ) + { + clonedChildren = emalloc( sizeof( zend_ast* ) * children.count ); + } + ELASTIC_APM_FOR_EACH_INDEX( i, children.count ) + { + clonedChildren[ i ] = NULL; + ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstTree( children.values[ i ], lineNumber, /* out */ &( clonedChildren[ i ] ) ) ); + } + + ELASTIC_APM_CALL_IF_FAILED_GOTO( createAstExCheckChildrenCount( ast->kind, ast->attr, ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, children.count, clonedChildren ), /* out */ pResult ) ); + (*pResult)->lineno = lineNumber; + + resultCode = resultSuccess; + finally: + if ( clonedChildren != NULL ) + { + efree( clonedChildren ); + clonedChildren = NULL; + } + return resultCode; + + failure: + goto finally; +} + +ResultCode cloneAstTree( zend_ast* ast, uint32_t lineNumber, /* out */ zend_ast** pResult ) +{ + /** + * @see zend_ast_copy + */ + + ResultCode resultCode; + + if ( ast == NULL ) + { + *pResult = NULL; + ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); + } + + if ( ast->kind == ZEND_AST_ZVAL ) + { + *pResult = cloneAstZVal( ast, lineNumber ); + ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); + } + + // ZEND_AST_CONSTANT was added in PHP 7.3 + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */ + if ( ast->kind == ZEND_AST_CONSTANT ) + { + *pResult = cloneAstConstant( ast, lineNumber ); + ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); + } + #endif + + if ( isAstDecl( ast->kind ) ) + { + ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstDecl( ast, lineNumber, /* out */ pResult ) ); + ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); + } + + if ( zend_ast_is_list( ast ) ) + { + ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneAstList( ast, lineNumber, /* out */ pResult ) ); + ELASTIC_APM_SET_RESULT_CODE_TO_SUCCESS_AND_GOTO_FINALLY(); + } + + ELASTIC_APM_CALL_IF_FAILED_GOTO( cloneFallbackAst( ast, lineNumber, /* out */ pResult ) ); + + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; +} +#pragma clang diagnostic pop + +zend_ast* createAstAssign( StringView varName, zend_ast* rhsAst ) +{ + // PHP code: + // + // $args = func_get_args(); + // $postHook = \elastic_apm_ast_instrumentation_pre_hook(/* instrumentedClassFullName */ null, __FUNCTION__, $args); + // + // AST: + // + // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: args) + // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2) <- rhsAst + + return createAstWithTwoChildren( ZEND_AST_ASSIGN, createAstVar( varName, zend_ast_get_lineno( rhsAst ) ), rhsAst ); +} + +static StringView g_argsVarName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "args" ); +static StringView g_postHookVarName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "postHook" ); + +zend_ast* createPreHookAstArgList( bool isMethod, uint32_t lineNumber ) +{ + // PHP code: + // + // \elastic_apm_ast_instrumentation_pre_hook(, __FUNCTION__, $args); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // is __CLASS__ for methods and null for standalone functions + // + // AST: + // + // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 3) + // ZEND_AST_CONST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: null) + // ZEND_AST_MAGIC_CONST (line: 48, attr: __FUNCTION__, childCount: 0) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: args) + + return createAstListWithThreeChildren( + ZEND_AST_ARG_LIST + , isMethod ? createAstMagicConst__CLASS__( lineNumber ) : createAstConstNull( lineNumber ) + , createAstMagicConst__FUNCTION__( lineNumber ) + , createAstVar( g_argsVarName, lineNumber ) + ); +} + +void createWrapperFunctionBodyPrologAst( /* in,out */ zend_ast** appendToAstStmtList ) +{ + // PHP code: + // + // $args = func_get_args(); + // $postHook = \elastic_apm_ast_instrumentation_pre_hook(/* instrumentedClassFullName */ null, __FUNCTION__, $args); + // + // AST: + // + // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: args) + // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: func_get_args) + // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 0) + // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: postHook) + // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: elastic_apm_ast_instrumentation_pre_hook) + // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 3) + + ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( appendToAstStmtList ); + + uint32_t lineNumber = zend_ast_get_lineno( *appendToAstStmtList ); + zend_ast* func_get_args_astCall = createAstStandaloneFqFunctionCall( ELASTIC_APM_STRING_LITERAL_TO_VIEW( "func_get_args" ), createAstList( ZEND_AST_ARG_LIST, lineNumber ) ); + addChildToAstList( createAstAssign( g_argsVarName, func_get_args_astCall ), /* in,out */ appendToAstStmtList ); + zend_ast* preHookAstCall = createAstStandaloneFqFunctionCall( g_elastic_apm_ast_instrumentation_pre_hook_funcName, createPreHookAstArgList( /* isMethod */ false, lineNumber ) ); + addChildToAstList( createAstAssign( g_postHookVarName, preHookAstCall ), /* in,out */ appendToAstStmtList ); +} + +zend_ast* createCallPostHookIfNotNullAst( zend_ast* thrownAst, zend_ast* retValAst ) +{ + // PHP code: + // + // if ($postHook !== null) $postHook(/* thrown */ null, $retVal); + // + // or + // + // if ($postHook !== null) $postHook($thrown, /* retVal */ null); + // + // AST: + // + // ZEND_AST_IF (line: 48, attr: 0, childCount: 1) + // ZEND_AST_IF_ELEM (line: 48, attr: 0, childCount: 2) + // ZEND_AST_BINARY_OP (line: 48, attr: 17, childCount: 2) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: postHook) + // ZEND_AST_CONST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: null) + // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: postHook) + // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 2) + // ZEND_AST_CONST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: null) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: retVal) + // + // or + // + // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 2) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: thrown) + // ZEND_AST_CONST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: null) + + ELASTIC_APM_ASSERT_VALID_PTR( thrownAst ); + ELASTIC_APM_ASSERT_VALID_PTR( retValAst ); + + uint32_t lineNumber = zend_ast_get_lineno( thrownAst ); + + return createAstListWithOneChild( + ZEND_AST_IF + , createAstWithTwoChildren( + ZEND_AST_IF_ELEM + , zend_ast_create_binary_op( + ZEND_IS_NOT_IDENTICAL + , createAstVar( g_postHookVarName, lineNumber ) + , createAstConstNull( lineNumber ) + ) + , createAstWithTwoChildren( + ZEND_AST_CALL + , createAstVar( g_postHookVarName, lineNumber ) + , createAstListWithTwoChildren( ZEND_AST_ARG_LIST, thrownAst, retValAst ) + ) + ) + ); +} + +zend_ast* createWrappedFunctionCallAstArgList( uint32_t lineNumber ) +{ + // PHP code: + // + // get_templateElasticApmWrapped(...$args); + // ^^^^^^^^ + // + // AST: + // + // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_UNPACK (line: 48, attr: 0, childCount: 1) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: args) + + return createAstListWithOneChild( + ZEND_AST_ARG_LIST + , createAstWithOneChild( + ZEND_AST_UNPACK + , createAstVar( g_argsVarName, lineNumber ) + ) + ); +} + +static StringView g_retValVarName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "retVal" ); + +zend_ast* createWrapperFunctionBodyTryBlockAst( StringView wrappedFunctionNewName, uint32_t lineNumber ) +{ + // PHP code: + // + // $retVal = get_templateElasticApmWrapped(...$args); + // if ($postHook !== null) $postHook(/* thrown */ null, $retVal); + // return $retVal; + // + // AST: + // + // ZEND_AST_STMT_LIST (line: 48, attr: 0, childCount: 3) + // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: retVal) + // ZEND_AST_CALL (line: 48, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 48, attr: 1, childCount: 0, type: string, value: get_templateElasticApmWrapped) + // ZEND_AST_ARG_LIST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_IF (line: 48, attr: 0, childCount: 1) <- if ($postHook !== null) $postHook(/* thrown */ null, $retVal); + // ZEND_AST_RETURN (line: 48, attr: 0, childCount: 1) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: retVal) + +// return createAstList( ZEND_AST_STMT_LIST, lineNumber ); + + return createAstListWithThreeChildren( + ZEND_AST_STMT_LIST + , createAstAssign( g_retValVarName, createAstStandaloneNotFqFunctionCall( wrappedFunctionNewName, createWrappedFunctionCallAstArgList( lineNumber ) ) ) + , createCallPostHookIfNotNullAst( /* thrownAst */ createAstConstNull( lineNumber ), createAstVar( g_retValVarName, lineNumber ) ) + , createAstWithOneChild( ZEND_AST_RETURN, createAstVar( g_retValVarName, lineNumber ) ) + ); +} + +static StringView g_thrownVarName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "thrown" ); + +zend_ast* createWrapperFunctionBodyCatchPartAst( uint32_t lineNumber ) +{ + // PHP code: + // + // } catch (\Throwable $thrown) { + // if ($postHook !== null) $postHook($thrown, /* retVal */ null); + // throw $thrown; + // } + // + // AST: + // + // ZEND_AST_CATCH_LIST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_CATCH (line: 48, attr: 0, childCount: 3) + // ZEND_AST_NAME_LIST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: Throwable) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: thrown) + // ZEND_AST_STMT_LIST (line: 48, attr: 0, childCount: 2) + // ZEND_AST_IF (line: 48, attr: 0, childCount: 1) <- if ($postHook !== null) $postHook($thrown, /* retVal */ null); + // ZEND_AST_THROW (line: 48, attr: 0, childCount: 1) + // ZEND_AST_VAR (line: 48, attr: 0, childCount: 1) + // ZEND_AST_ZVAL (line: 48, attr: 0, childCount: 0, type: string, value: thrown) + + /** + * @see zend_compile_try + */ + + return createAstListWithOneChild( + ZEND_AST_CATCH_LIST + , createAstWithThreeChildren( + ZEND_AST_CATCH + /* ZEND_AST_CATCH child[ 0 ] - class name(s) */ + , createAstListWithOneChild( ZEND_AST_NAME_LIST, createAstZValString( ELASTIC_APM_STRING_LITERAL_TO_VIEW( "Throwable" ), lineNumber ) ) + /* ZEND_AST_CATCH child[ 1 ] - var name */ + , createAstZValString( g_thrownVarName, lineNumber ) + /* ZEND_AST_CATCH child[ 2 ] - block */ + , createAstListWithTwoChildren( + ZEND_AST_STMT_LIST + , createCallPostHookIfNotNullAst( createAstVar( g_thrownVarName, lineNumber ), /* retValAst */ createAstConstNull( lineNumber ) ) + , createAstWithOneChild( ZEND_AST_THROW, createAstVar( g_thrownVarName, lineNumber ) ) + ) + ) + ); +} + +void createWrapperFunctionBodyTryCatchAst( StringView wrappedFunctionNewName, /* in,out */ zend_ast** appendToAstStmtList ) +{ + // PHP code: + // + // try { + // // ... + // } catch ( ... ) { + // // ... + // } + // + // AST: + // + // ZEND_AST_TRY (line: 48, attr: 0, childCount: 3) + // ZEND_AST_STMT_LIST (line: 48, attr: 0, childCount: 3) <- try block + // ZEND_AST_CATCH_LIST (line: 48, attr: 0, childCount: 1) + // ZEND_AST_CATCH (line: 48, attr: 0, childCount: 3) <- catch block + // NULL <- no finally block + + ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( appendToAstStmtList ); + + uint32_t lineNumber = zend_ast_get_lineno( *appendToAstStmtList ); + + /** + * @see zend_compile_try + */ + zend_ast* astTryCatch = createAstWithThreeChildren( + ZEND_AST_TRY + , createWrapperFunctionBodyTryBlockAst( wrappedFunctionNewName, lineNumber ) + , createWrapperFunctionBodyCatchPartAst( lineNumber ) + , NULL /* <- no finally block */ + ); + + addChildToAstList( astTryCatch, /* in,out */ appendToAstStmtList ); +} + +zend_ast* createWrapperFunctionBodyAst( StringView wrappedFunctionNewName, uint32_t lineNumber ) +{ + // PHP code: + // + // // prolog + // try { + // // ... + // } catch ( ... ) { + // // ... + // } + // + // AST: + // + // ZEND_AST_STMT_LIST (line: 48, attr: 0, childCount: 3) + // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2) <- // part of prolog + // ZEND_AST_ASSIGN (line: 48, attr: 0, childCount: 2) <- // part of prolog + // ZEND_AST_TRY (line: 48, attr: 0, childCount: 3) + + zend_ast* funcBodyAstStmtList = createAstList( ZEND_AST_STMT_LIST, lineNumber ); + + createWrapperFunctionBodyPrologAst( /* in,out */ &funcBodyAstStmtList ); + createWrapperFunctionBodyTryCatchAst( wrappedFunctionNewName, /* in,out */ &funcBodyAstStmtList ); + + return funcBodyAstStmtList; +} + +ResultCode createWrapperFunctionAst( zend_ast_decl* originalFuncAstDecl, StringView wrappedFunctionNewName, /* out */ zend_ast_decl** pResult ) +{ + ELASTIC_APM_ASSERT_VALID_PTR( originalFuncAstDecl ); + ELASTIC_APM_ASSERT_VALID_OUT_PTR_TO_PTR( pResult ); + + ResultCode resultCode; + zend_ast* originalFuncBodyAst = NULL; + zend_ast_decl* clonedFuncDecl = NULL; + uint32_t lineNumber = originalFuncAstDecl->end_lineno; + + originalFuncBodyAst = originalFuncAstDecl->child[ g_funcDeclBodyChildIndex ]; + // Temporarily set the body to NULL because we don't want to clone it + // We restore it back after clone call + originalFuncAstDecl->child[ g_funcDeclBodyChildIndex ] = NULL; + resultCode = cloneAstTree( (zend_ast*)originalFuncAstDecl, lineNumber, /* out */ (zend_ast**)&clonedFuncDecl ); + originalFuncAstDecl->child[ g_funcDeclBodyChildIndex ] = originalFuncBodyAst; + if ( resultCode != resultSuccess ) + { + goto failure; + } + clonedFuncDecl->child[ g_funcDeclBodyChildIndex ] = createWrapperFunctionBodyAst( wrappedFunctionNewName, lineNumber ); + + *pResult = clonedFuncDecl; + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; +} + +uint32_t findAstDeclStartLineNumber( zend_ast_decl* astDecl ) +{ + ZendAstPtrArrayView children = getAstDeclChildren( astDecl ); + uint32_t result = astDecl->start_lineno; + ELASTIC_APM_FOR_EACH_INDEX( i, children.count ) + { + zend_ast* child = children.values[ i ]; + if ( ( child != NULL ) && ( zend_ast_get_lineno( child ) < result ) ) { - ast->child[ i ] = transformAst( ast->child[ i ], nestingDepth + 1 ); + result = zend_ast_get_lineno( child ); } } + return result; +} + +ResultCode wrapStandaloneFunctionAstWithPrePostHooks( /* in,out */ zend_ast_decl** pAstChildSlot ) +{ + // Before: + // + // function get_template() { + // // ... + // } + // + // ZEND_AST_FUNC_DECL (name: get_template, line: 16, flags: 0, attr: 0, childCount: 4) <- original function under original name + // + // After: + // + // { function get_templateElasticApmWrapped() { + // // ... + // } // fold-AST-into-one-line-begin + // + // function get_template() + // { + // // wrapper function body + // } + // } // fold-AST-into-one-line-end + // + // ZEND_AST_STMT_LIST (line: 16, attr: 0, childCount: 2) + // ZEND_AST_FUNC_DECL (name: get_templateElasticApmWrapped, line: 16, flags: 0, attr: 0, childCount: 4) <- original function under wrapped name + // ZEND_AST_FUNC_DECL (name: get_template, line: 25, flags: 0, attr: 0, childCount: 4) <- wrapper function under original name + + ELASTIC_APM_ASSERT_VALID_IN_PTR_TO_PTR( pAstChildSlot ); + + ResultCode resultCode; + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + String dbgCompiledFileName = stringIfNotNullElse( nullableZStringToStringView( CG(compiled_filename) ).begin, "" ); + zend_ast_decl* originalFuncAstDecl = *pAstChildSlot; // it's not created by this function, so we should NOT clean it on failure + StringBuffer wrappedFunctionNewName = ELASTIC_APM_EMPTY_STRING_BUFFER; + zend_ast_decl* wrapperFuncAst = NULL; + + ELASTIC_APM_ASSERT( originalFuncAstDecl->kind == ZEND_AST_FUNC_DECL, "originalFuncAstDecl->kind: %s", streamZendAstKind( originalFuncAstDecl->kind, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); + + StringView originalFuncName; + if ( ! getAstDeclName( originalFuncAstDecl, /* out */ &originalFuncName ) ) + { + ELASTIC_APM_LOG_ERROR( "Failed to get function name - returning failure" ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "originalFuncName: %s, compiled_filename: %s", originalFuncName.begin, dbgCompiledFileName ); + debugDumpAstTreeToLog( (zend_ast*) ( *pAstChildSlot ), logLevel_debug ); + + ELASTIC_APM_CALL_IF_FAILED_GOTO( createWrappedFunctionNewName( originalFuncName, /* out */ &wrappedFunctionNewName ) ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( createWrapperFunctionAst( originalFuncAstDecl, stringBufferToView( wrappedFunctionNewName ), /* out */ &wrapperFuncAst ) ); + zend_ast* newCombinedAst = createAstListWithTwoChildren( ZEND_AST_STMT_LIST, (zend_ast*)originalFuncAstDecl, (zend_ast*)wrapperFuncAst ); + newCombinedAst->lineno = findAstDeclStartLineNumber( originalFuncAstDecl ); + originalFuncAstDecl->name = createZStringForAst( stringBufferToView( wrappedFunctionNewName ) ); + *((zend_ast**)pAstChildSlot) = newCombinedAst; + resultCode = resultSuccess; + finally: + ELASTIC_APM_FREE_STRING_BUFFER_AND_SET_TO_NULL( /* in,out */ wrappedFunctionNewName ); + ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT_MSG( "originalFuncName: %s, compiled_filename: %s", originalFuncName.begin, dbgCompiledFileName ); + debugDumpAstTreeToLog( (zend_ast*) ( *pAstChildSlot ), logLevel_debug ); + return resultCode; + + failure: + goto finally; +} + +bool getAstName( zend_ast* ast, /* out */ StringView* name ) +{ + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + switch ( ast->kind ) + { + case ZEND_AST_CLASS: + case ZEND_AST_FUNC_DECL: + case ZEND_AST_METHOD: + return getAstDeclName( (zend_ast_decl*)ast, /* out */ name ); + + default: + ELASTIC_APM_ASSERT( false, "Unexpected ast->kind: %s", streamZendAstKind( ast->kind, &txtOutStream ) ); + return false; + } +} + +bool parseAstNamespace( zend_ast* astNamespace, /* out */ StringView* pName, /* out */ zend_ast** pEnclosedScope ) +{ + // PHP code: + // + // namespace // global + // { + // } + // + // AST: + // + // ZEND_AST_NAMESPACE (line: 4, attr: 0, childCount: 2) + // NULL + // ZEND_AST_STMT_LIST (line: 4, attr: 0, childCount: 5) + + // PHP code: + // + // namespace MyNamespace; + // + // AST: + // + // ZEND_AST_NAMESPACE (line: 3, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 3, attr: 0) [type: string, value: MyNamespace] + // NULL + // + // PHP code: + // + // namespace MyNamespace + // { + // } + // + // AST: + // + // ZEND_AST_NAMESPACE (line: 3, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 3, attr: 0) [type: string, value: MyNamespace] + // ZEND_AST_STMT_LIST (line: 4, attr: 0, childCount: 5) + + + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + zend_ast* nameAstZval = NULL; + zend_ast* enclosedScopeAst = NULL; + StringView name; + + ELASTIC_APM_ASSERT( astNamespace->kind == ZEND_AST_NAMESPACE, "ast->kind: %s", streamZendAstKind( astNamespace->kind, &txtOutStream ) ); textOutputStreamRewind( &txtOutStream ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( transformedAst->kind ) ); - return transformedAst; + ELASTIC_APM_ASSERT_VALID_PTR( pName ); + ELASTIC_APM_ASSERT_VALID_OUT_PTR_TO_PTR( pEnclosedScope ); + + uint32_t childrenCountAstNamespace = zend_ast_get_num_children( astNamespace ); + if ( childrenCountAstNamespace < 2 ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - childrenCountAstNamespace: %u", (unsigned)childrenCountAstNamespace ); + return false; + } + + nameAstZval = astNamespace->child[ 0 ]; + if ( nameAstZval == NULL ) + { + name = ELASTIC_APM_EMPTY_STRING_VIEW; + } + else if ( ! getStringFromAstZVal( nameAstZval, /* out */ &name ) ) + { + return false; + } + + enclosedScopeAst = astNamespace->child[ 1 ]; + if ( ( enclosedScopeAst != NULL ) && ( enclosedScopeAst->kind != ZEND_AST_STMT_LIST ) ) + { + ELASTIC_APM_LOG_TRACE( "Returning false - enclosedScopeAst->kind: %s", streamZendAstKind( enclosedScopeAst->kind, &txtOutStream ) ); + return false; + } + + *pName = name; + *pEnclosedScope = enclosedScopeAst; + ELASTIC_APM_LOG_TRACE( "Returning true - name [length: %"PRIu64"]: %.*s", (UInt64)(pName->length), (int)(pName->length), pName->begin ); + return true; } -static -zend_ast* transformAst( zend_ast* ast, int nestingDepth ) +typedef bool (* CheckFindAstReqs)( zend_ast* ast, void* ctx ); + +bool checkFunctionReqs( zend_ast* ast, void* ctx ) { char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( ast->kind ) ); + ELASTIC_APM_ASSERT( ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_METHOD, "ast->kind: %s", streamZendAstKind( ast->kind, &txtOutStream ) ); + textOutputStreamRewind( &txtOutStream ); - zend_ast * transformedAst; + size_t minParamsCount = *(size_t*)ctx; - if ( zend_ast_is_list( ast ) ) + ZendAstPtrArrayView paramsAsts; + if ( ! getAstFunctionParameters( (zend_ast_decl*)ast, /* out */ ¶msAsts ) ) + { + return false; + } + + return paramsAsts.count >= minParamsCount; +} + +bool findAstOfKindCheckNode( zend_ast* ast, zend_ast_kind kindToFind, StringView name, CheckFindAstReqs checkFindAstReqs, void* checkFindAstReqsCtx ) +{ + if ( ast->kind != kindToFind ) { - zend_ast_list* list = zend_ast_get_list( ast ); - uint32_t i; - for ( i = 0 ; i < list->children ; i ++ ) + return false; + } + + if ( name.begin != NULL ) + { + StringView astName; + if ( ! ( getAstName( ast, /* out */ &astName ) && areStringViewsEqual( astName, name ) ) ) { - if ( list->child[ i ] ) - { - list->child[ i ] = transformAst( list->child[ i ], nestingDepth + 1 ); - } + return false; } - transformedAst = ast; - goto finally; } - switch ( ast->kind ) + return ( checkFindAstReqs == NULL ) || checkFindAstReqs( ast, checkFindAstReqsCtx ); +} + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "misc-no-recursion" +zend_ast** findChildSlotAstByKind( zend_ast* ast, zend_ast_kind kindToFind, StringView namespace, StringView name, CheckFindAstReqs checkFindAstReqs, void* checkFindAstReqsCtx ) +{ + if ( ! zend_ast_is_list( ast ) ) { - case ZEND_AST_ZVAL: - #ifdef ZEND_AST_CONSTANT - case ZEND_AST_CONSTANT: - #endif - transformedAst = ast; - goto finally; + return NULL; + } - case ZEND_AST_FUNC_DECL: - case ZEND_AST_METHOD: - transformedAst = transformFunctionAst( ast, nestingDepth + 1 ); - goto finally; + zend_ast_list* astAsList = zend_ast_get_list( ast ); + ELASTIC_APM_FOR_EACH_INDEX( i, astAsList->children ) + { + zend_ast* child = astAsList->child[ i ]; + if ( zend_ast_is_list( child ) ) + { + zend_ast** foundAst = findChildSlotAstByKind( child, kindToFind, namespace, name, checkFindAstReqs, checkFindAstReqsCtx ); + if ( foundAst != NULL ) + { + return foundAst; + } + continue; + } - case ZEND_AST_RETURN: - transformedAst = transformReturnAst( ast, nestingDepth + 1 ); - goto finally; + if ( child->kind == ZEND_AST_NAMESPACE ) + { + StringView foundNamespaceName; + zend_ast* namespaceEnclosedScope = NULL; + if ( ! parseAstNamespace( child, /* out */ &foundNamespaceName, /* out */ &namespaceEnclosedScope ) ) + { + continue; + } - default: - transformedAst = transformChildrenAst( ast, nestingDepth + 1 ); - goto finally; + if ( ! areStringViewsEqual( foundNamespaceName, namespace ) ) + { + continue; + } + + if ( namespaceEnclosedScope != NULL ) + { + zend_ast** foundAst = findChildSlotAstByKind( namespaceEnclosedScope, kindToFind, namespace, name, checkFindAstReqs, checkFindAstReqsCtx ); + if ( foundAst != NULL ) + { + return foundAst; + } + continue; + } + } + + if ( findAstOfKindCheckNode( astAsList->child[ i ], kindToFind, name, checkFindAstReqs, checkFindAstReqsCtx ) ) + { + // It's not &child on purpose since child is a local variable + return &( astAsList->child[ i ] ); + } } - finally: + return NULL; +} +#pragma clang diagnostic pop + +zend_ast_decl** findChildSlotForStandaloneFunctionAst( zend_ast* rootAst, StringView namespace, StringView funcName, size_t minParamsCount ) +{ + return (zend_ast_decl**) findChildSlotAstByKind( rootAst, ZEND_AST_FUNC_DECL, namespace, funcName, checkFunctionReqs, &minParamsCount ); +} + +zend_ast_decl* findClassAst( zend_ast* rootAst, StringView namespace, StringView className ) +{ + zend_ast** result = findChildSlotAstByKind( rootAst, ZEND_AST_CLASS, namespace, className, /* checkFuncDeclReqs */ NULL, /* checkFindAstReqsCtx */ NULL ); + return (zend_ast_decl*)(*result); +} + +zend_ast_decl** findChildSlotForMethodAst( zend_ast_decl* astClass, StringView methodName, size_t minParamsCount ) +{ + // ZEND_AST_CLASS (name: WP_Hook, line: 6, flags: 32, attr: 0, childCount: 3) + // NULL + // ZEND_AST_NAME_LIST (line: 6, attr: 0, childCount: 2) + // ZEND_AST_ZVAL (line: 6, attr: 1) [type: string, value: Iterator] + // ZEND_AST_ZVAL (line: 6, attr: 1) [type: string, value: ArrayAccess] + // ZEND_AST_STMT_LIST (line: 6, attr: 0, childCount: 1) + // ZEND_AST_METHOD (name: add_filter, line: 7, flags: 1, attr: 0, childCount: 4) + + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + ELASTIC_APM_ASSERT( astClass->kind == ZEND_AST_CLASS, "ast->kind: %s", streamZendAstKind( astClass->kind, &txtOutStream ) ); textOutputStreamRewind( &txtOutStream ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "%s: kind: %s", streamIndent( nestingDepth, &txtOutStream ), zendAstKindToString( transformedAst->kind ) ); - return transformedAst; + + zend_ast_decl* astAsDecl = (zend_ast_decl*)astClass; + + // class body is always child[ 2 ] - see zend_compile_class_decl + return (zend_ast_decl**) findChildSlotAstByKind( astAsDecl->child[ 2 ], ZEND_AST_METHOD, /* namespace */ ELASTIC_APM_EMPTY_STRING_VIEW, methodName, checkFunctionReqs, &minParamsCount ); } -static -void elasticApmProcessAstRoot( zend_ast* ast ) +void elasticApmTransformAstImpl( zend_ast* ast ) { - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "ast->kind: %s", zendAstKindToString( ast->kind ) ); + StringView compiledFileFullPath = nullableZStringToStringView( CG( compiled_filename) ); + if ( compiledFileFullPath.begin == NULL ) + { + return; + } + + size_t fileIndex; + if ( ! wordPressInstrumentationShouldTransformAstInFile( compiledFileFullPath, /* out */ &fileIndex ) ) + { + return; + } + + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin ); + debugDumpAstTree( compiledFileFullPath, ast, /* isBeforeProcess */ true ); - zend_ast * transformedAst = transformAst( ast, 0 ); - if ( original_zend_ast_process != NULL ) original_zend_ast_process( transformedAst ); + wordPressInstrumentationTransformAst( fileIndex, compiledFileFullPath, ast ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT(); + ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin ); + debugDumpAstTree( compiledFileFullPath, ast, /* isBeforeProcess */ false ); } -void astInstrumentationInit() +void elasticApmTransformAst( zend_ast* ast ) { - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); + ELASTIC_APM_ASSERT( g_isOriginalZendAstProcessSet, "g_originalZendAstProcess: %p", g_originalZendAstProcess ); - original_zend_ast_process = zend_ast_process; - zend_ast_process = elasticApmProcessAstRoot; + if ( ( ! g_isLoadingAgentPhpCode ) && ast != NULL ) + { + elasticApmTransformAstImpl( ast ); + } - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT(); + if ( g_originalZendAstProcess != NULL ) + { + g_originalZendAstProcess( ast ); + } } -void astInstrumentationShutdown() +void astInstrumentationOnModuleInit( const ConfigSnapshot* config ) { - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); + if ( config->astProcessEnabled ) + { + g_originalZendAstProcess = zend_ast_process; + g_isOriginalZendAstProcessSet = true; + zend_ast_process = elasticApmTransformAst; + ELASTIC_APM_LOG_DEBUG( "Changed zend_ast_process: from %p to elasticApmTransformAst (%p)", g_originalZendAstProcess, elasticApmTransformAst ); + } else { + ELASTIC_APM_LOG_DEBUG( "AST processing will be DISABLED because configuration option %s (astProcessEnabled) is set to false", ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_ENABLED ); + } +} - zend_ast_process = original_zend_ast_process; +void astInstrumentationOnModuleShutdown() +{ + if ( g_isOriginalZendAstProcessSet ) + { + zend_ast_process_t zendAstProcessBeforeRestore = zend_ast_process; + zend_ast_process = g_originalZendAstProcess; + g_originalZendAstProcess = NULL; + g_isOriginalZendAstProcessSet = false; + ELASTIC_APM_LOG_DEBUG( "Restored zend_ast_process: from %p (%s elasticApmTransformAst: %p) -> %p" + , zendAstProcessBeforeRestore, zendAstProcessBeforeRestore == elasticApmTransformAst ? "==" : "!=", elasticApmTransformAst, g_originalZendAstProcess ); + } +} - ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT(); +void astInstrumentationOnRequestInit( const ConfigSnapshot* config ) +{ + astProcessDebugDumpOnRequestInit( config ); + wordPressInstrumentationOnRequestInit(); +} + +void astInstrumentationOnRequestShutdown() +{ + wordPressInstrumentationOnRequestShutdown(); + astProcessDebugDumpOnRequestShutdown(); } diff --git a/src/ext/AST_instrumentation.h b/src/ext/AST_instrumentation.h index c024e4295..822d7445a 100644 --- a/src/ext/AST_instrumentation.h +++ b/src/ext/AST_instrumentation.h @@ -19,6 +19,34 @@ #pragma once -void astInstrumentationInit(); +#include +#include "ConfigSnapshot_forward_decl.h" +#include "StringView.h" +#include "TextOutputStream.h" +#include "ResultCode.h" +#include "ArrayView.h" -void astInstrumentationShutdown(); +enum ArgCaptureSpec +{ + captureArgByValue, + captureArgByRef, + dontCaptureArg +}; +typedef enum ArgCaptureSpec ArgCaptureSpec; +ELASTIC_APM_DECLARE_ARRAY_VIEW( ArgCaptureSpec, ArgCaptureSpecArrayView ); + +void astInstrumentationOnModuleInit( const ConfigSnapshot* config ); +void astInstrumentationOnModuleShutdown(); + +void astInstrumentationOnRequestInit( const ConfigSnapshot* config ); +void astInstrumentationOnRequestShutdown(); + +zend_ast_decl** findChildSlotForStandaloneFunctionAst( zend_ast* rootAst, StringView namespace, StringView funcName, size_t minParamsCount ); +zend_ast_decl* findClassAst( zend_ast* rootAst, StringView namespace, StringView className ); +zend_ast_decl** findChildSlotForMethodAst( zend_ast_decl* astClass, StringView methodName, size_t minParamsCount ); + +ResultCode insertAstForFunctionPreHook( zend_ast_decl* funcAstDecl, ArgCaptureSpecArrayView argCaptureSpecs ); +ResultCode appendDirectCallToInstrumentation( zend_ast_decl** pAstChildSlot, StringView constNameForMethodName ); +ResultCode wrapStandaloneFunctionAstWithPrePostHooks( zend_ast_decl** pAstChildSlot ); + +String streamZendAstKind( zend_ast_kind kind, TextOutputStream* txtOutStream ); diff --git a/src/ext/AST_util.h b/src/ext/AST_util.h new file mode 100644 index 000000000..62d72d54d --- /dev/null +++ b/src/ext/AST_util.h @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +#include +#include +#include "ArrayView.h" +#include "basic_macros.h" + +typedef zend_ast* ZendAstPtr; +ELASTIC_APM_DECLARE_ARRAY_VIEW( ZendAstPtr, ZendAstPtrArrayView ); + +static inline +bool isAstDecl( zend_ast_kind kind ) +{ + switch( kind ) + { + case ZEND_AST_FUNC_DECL: + case ZEND_AST_CLOSURE: + case ZEND_AST_METHOD: + case ZEND_AST_CLASS: + // ZEND_AST_ARROW_FUNC was added in PHP 7.4 + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 4, 0 ) + case ZEND_AST_ARROW_FUNC: + #endif + return true; + + default: + return false; + } +} + +/** + * zend_ast_decl.child is array of size + * 4 before PHP v8.0.0 + * 5 from PHP v8.0.0 + * + * @see zend_ast_decl and zend_ast_create_decl + */ +enum +{ + elasticApmZendAstDeclChildrenCount = + #if PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) /* if PHP version before 8.0.0 */ + 4 + #else + 5 + #endif +}; + +static inline +ZendAstPtrArrayView getAstDeclChildren( zend_ast_decl* astDecl ) +{ + ELASTIC_APM_STATIC_ASSERT( ELASTIC_APM_STATIC_ARRAY_SIZE( astDecl->child ) == elasticApmZendAstDeclChildrenCount ); + return ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, elasticApmZendAstDeclChildrenCount, &( astDecl->child[ 0 ] ) ); +} + +static inline +ZendAstPtrArrayView getAstChildren( zend_ast* ast ) +{ + /** @see enum zend_ast_copy */ + + if ( zend_ast_is_list( ast ) ) + { + zend_ast_list* astList = zend_ast_get_list( ast ); + return ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, (size_t)( astList->children ), &( astList->child[ 0 ] ) ); + } + + if ( isAstDecl( ast->kind ) ) + { + return getAstDeclChildren( (zend_ast_decl*)ast ); + } + + switch ( ast->kind ) + { + /** + * special nodes + * + * @see enum _zend_ast_kind + */ + case ZEND_AST_ZNODE: + case ZEND_AST_ZVAL: + // ZEND_AST_CONSTANT was added in PHP 7.3 + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 7, 3, 0 ) /* if PHP version from 7.3.0 */ + case ZEND_AST_CONSTANT: + #endif + return ELASTIC_APM_MAKE_EMPTY_ARRAY_VIEW( ZendAstPtrArrayView ); + + default: + return ELASTIC_APM_MAKE_ARRAY_VIEW( ZendAstPtrArrayView, (size_t)zend_ast_get_num_children( ast ), &( ast->child[ 0 ] ) ); + } +} diff --git a/src/ext/ConfigManager.c b/src/ext/ConfigManager.c index 7fce5c9d2..ff8a148e0 100644 --- a/src/ext/ConfigManager.c +++ b/src/ext/ConfigManager.c @@ -764,6 +764,10 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, apiKey ) # if ( ELASTIC_APM_ASSERT_ENABLED_01 != 0 ) ELASTIC_APM_DEFINE_ENUM_FIELD_ACCESS_FUNCS( AssertLevel, assertLevel ) # endif +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, astProcessEnabled ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, astProcessDebugDumpConvertedBackToSource ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, astProcessDebugDumpForPathPrefix ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, astProcessDebugDumpOutDir ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( optionalBoolValue, asyncBackendComm ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, bootstrapPhpPartFile ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, breakdownMetrics ) @@ -802,6 +806,7 @@ ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, serviceVersion ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( boolValue, spanCompressionEnabled ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, spanCompressionExactMatchMaxDuration ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, spanCompressionSameKindMaxDuration ) +ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, stackTraceLimit ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, transactionIgnoreUrls ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, transactionMaxSpans ) ELASTIC_APM_DEFINE_FIELD_ACCESS_FUNCS( stringValue, transactionSampleRate ) @@ -921,6 +926,30 @@ static void initOptionsMetadata( OptionMetadata* optsMeta ) /* isUniquePrefixEnough: */ true ); #endif + ELASTIC_APM_INIT_METADATA( + buildBoolOptionMetadata, + astProcessEnabled, + ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_ENABLED, + /* defaultValue: */ true ); + + ELASTIC_APM_INIT_METADATA( + buildBoolOptionMetadata, + astProcessDebugDumpConvertedBackToSource, + ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE, + /* defaultValue: */ true ); + + ELASTIC_APM_INIT_METADATA( + buildStringOptionMetadata, + astProcessDebugDumpForPathPrefix, + ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX, + /* defaultValue: */ NULL ); + + ELASTIC_APM_INIT_METADATA( + buildStringOptionMetadata, + astProcessDebugDumpOutDir, + ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_OUT_DIR, + /* defaultValue: */ NULL ); + ELASTIC_APM_INIT_METADATA( buildOptionalBoolOptionMetadata, asyncBackendComm, @@ -1110,6 +1139,12 @@ static void initOptionsMetadata( OptionMetadata* optsMeta ) ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION, /* defaultValue: */ NULL ); + ELASTIC_APM_INIT_METADATA( + buildStringOptionMetadata, + stackTraceLimit, + ELASTIC_APM_CFG_OPT_NAME_STACK_TRACE_LIMIT, + /* defaultValue: */ NULL ); + ELASTIC_APM_INIT_METADATA( buildStringOptionMetadata, transactionIgnoreUrls, diff --git a/src/ext/ConfigManager.h b/src/ext/ConfigManager.h index 686067057..d1cf321ea 100644 --- a/src/ext/ConfigManager.h +++ b/src/ext/ConfigManager.h @@ -66,6 +66,10 @@ enum OptionId #if ( ELASTIC_APM_ASSERT_ENABLED_01 != 0 ) optionId_assertLevel, #endif + optionId_astProcessEnabled, + optionId_astProcessDebugDumpConvertedBackToSource, + optionId_astProcessDebugDumpForPathPrefix, + optionId_astProcessDebugDumpOutDir, optionId_asyncBackendComm, optionId_bootstrapPhpPartFile, optionId_breakdownMetrics, @@ -104,6 +108,7 @@ enum OptionId optionId_spanCompressionEnabled, optionId_spanCompressionExactMatchMaxDuration, optionId_spanCompressionSameKindMaxDuration, + optionId_stackTraceLimit, optionId_transactionIgnoreUrls, optionId_transactionMaxSpans, optionId_transactionSampleRate, @@ -218,6 +223,20 @@ const ConfigSnapshot* getGlobalCurrentConfigSnapshot(); #define ELASTIC_APM_CFG_OPT_NAME_ASSERT_LEVEL "assert_level" # endif +/** + * Internal configuration option (not included in public documentation) + */ +#define ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_ENABLED "ast_process_enabled" + +/** + * Internal configuration options (not included in public documentation) + * In addition to supportability this option is used by component tests as well. + * @see tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php + */ +#define ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE "ast_process_debug_dump_converted_back_to_source" +#define ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX "ast_process_debug_dump_for_path_prefix" +#define ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_OUT_DIR "ast_process_debug_dump_out_dir" + /** * Internal configuration option (not included in public documentation) */ @@ -296,6 +315,7 @@ const ConfigSnapshot* getGlobalCurrentConfigSnapshot(); #define ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_ENABLED "span_compression_enabled" #define ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION "span_compression_exact_match_max_duration" #define ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION "span_compression_same_kind_max_duration" +#define ELASTIC_APM_CFG_OPT_NAME_STACK_TRACE_LIMIT "stack_trace_limit" #define ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_IGNORE_URLS "transaction_ignore_urls" #define ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_MAX_SPANS "transaction_max_spans" #define ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_SAMPLE_RATE "transaction_sample_rate" diff --git a/src/ext/ConfigSnapshot.h b/src/ext/ConfigSnapshot.h index 1628d3462..0cf9740b5 100644 --- a/src/ext/ConfigSnapshot.h +++ b/src/ext/ConfigSnapshot.h @@ -79,6 +79,7 @@ struct ConfigSnapshot bool spanCompressionEnabled; String spanCompressionExactMatchMaxDuration; String spanCompressionSameKindMaxDuration; + String stackTraceLimit; String transactionIgnoreUrls; String transactionMaxSpans; String transactionSampleRate; diff --git a/src/ext/WordPress_instrumentation.c b/src/ext/WordPress_instrumentation.c new file mode 100644 index 000000000..7f64baacf --- /dev/null +++ b/src/ext/WordPress_instrumentation.c @@ -0,0 +1,251 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "WordPress_instrumentation.h" +#include "log.h" +#include "AST_instrumentation.h" +#include "util.h" +#include "TextOutputStream.h" + +#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_AUTO_INSTRUMENT + +enum WordPressInstrumentationFileToTransformAstIndex +{ + wordPress_instrumentation_file_to_transform_AST_plugin_php, + wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php, + wordPress_instrumentation_file_to_transform_AST_theme_php, + + number_of_WordPress_instrumentation_files_to_transform_AST +}; + +#define ELASTIC_APM_WP_INCLUDES_PREFIX "wp-includes/" + +static StringView g_filesToTransformAstPathSuffix[ number_of_WordPress_instrumentation_files_to_transform_AST ] = +{ + [ wordPress_instrumentation_file_to_transform_AST_plugin_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "plugin.php" ), + [ wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "class-wp-hook.php" ), + [ wordPress_instrumentation_file_to_transform_AST_theme_php ] = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WP_INCLUDES_PREFIX "theme.php" ) +}; + +#undef ELASTIC_APM_WP_INCLUDES_PREFIX + +struct WordPressInstrumentationRequestScopedState +{ + bool isInFailedMode; + bool seenFile[ number_of_WordPress_instrumentation_files_to_transform_AST ]; +}; +typedef struct WordPressInstrumentationRequestScopedState WordPressInstrumentationRequestScopedState; + +static WordPressInstrumentationRequestScopedState g_wordPressInstrumentationRequestScopedState; + +void wordPressInstrumentationSwitchToFailedMode( String dbgCalledFromFunc ) +{ + if ( g_wordPressInstrumentationRequestScopedState.isInFailedMode ) + { + return; + } + + ELASTIC_APM_LOG_ERROR( "Switched to failed mode; dbgCalledFromFunc: %s", dbgCalledFromFunc ); + + g_wordPressInstrumentationRequestScopedState.isInFailedMode = true; +} + +void wordPressInstrumentationOnRequestInit() +{ + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); + + g_wordPressInstrumentationRequestScopedState.isInFailedMode = false; + + ELASTIC_APM_FOR_EACH_INDEX( i, number_of_WordPress_instrumentation_files_to_transform_AST ) + { + g_wordPressInstrumentationRequestScopedState.seenFile[ i ] = false; + } + + ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT(); +} + +void wordPressInstrumentationOnRequestShutdown() +{ + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); + ELASTIC_APM_LOG_DEBUG_FUNCTION_EXIT(); +} + +static +ResultCode insertPreHookForFunctionWithHookNameCallbackParams( zend_ast_decl* astDecl ) +{ + // standalone function: + // function _wp_filter_build_unique_id( $hook_name, $callback, $priority ) + // class WP_Hook method + // public function add_filter( $hook_name, $callback, $priority, $accepted_args ) + ArgCaptureSpec argCaptureSpecArr[] = { /* capture $hook_name by value */ captureArgByValue, /* capture $callback by reference */ captureArgByRef }; + return insertAstForFunctionPreHook( astDecl, ELASTIC_APM_MAKE_ARRAY_VIEW_FROM_STATIC( ArgCaptureSpecArrayView, argCaptureSpecArr ) ); +} + +static StringView g_globalNamespace = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "" ); + +static StringView g_wp_filter_build_unique_id_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "_wp_filter_build_unique_id" ); + +ResultCode wordPressInstrumentationTransformFile_plugin_php( zend_ast* ast ) +{ + ResultCode resultCode; + zend_ast_decl** p_wp_filter_build_unique_id_astFuncDecl = NULL; + + // function _wp_filter_build_unique_id( $hook_name, $callback, $priority ) + p_wp_filter_build_unique_id_astFuncDecl = findChildSlotForStandaloneFunctionAst( ast, g_globalNamespace, g_wp_filter_build_unique_id_funcName, /* minParamsCount */ 3 ); + + if ( p_wp_filter_build_unique_id_astFuncDecl == NULL ) + { + ELASTIC_APM_LOG_ERROR( "Function %s are not found", g_wp_filter_build_unique_id_funcName.begin ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + + ELASTIC_APM_CALL_IF_FAILED_GOTO( insertPreHookForFunctionWithHookNameCallbackParams( *p_wp_filter_build_unique_id_astFuncDecl ) ); + // It's important to record if we instrumented _wp_filter_build_unique_id successfully. + // _wp_filter_build_unique_id instrumentation alone cannot make application work incorrectly + // because it checks if $callback is an instance our WordPressFilterCallbackWrapper class before unwrapping it. + // On the other hand add_filter instrumentation alone CAN make application work incorrectly + // if _wp_filter_build_unique_id was not instrumented as well. + // So we record if we instrumented _wp_filter_build_unique_id successfully + // and PHP part wraps callbacks only if it sees the record that _wp_filter_build_unique_id was instrumented successfully. + StringView setReadyToWrapFilterCallbacksConstName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS_CONST_NAME ); + ELASTIC_APM_CALL_IF_FAILED_GOTO( appendDirectCallToInstrumentation( p_wp_filter_build_unique_id_astFuncDecl, setReadyToWrapFilterCallbacksConstName ) ); + + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; +} + +static StringView g_WP_Hook_className = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "WP_Hook" ); +static StringView g_add_filter_methodName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "add_filter" ); + +ResultCode wordPressInstrumentationTransformFile_class_wp_hook_php( zend_ast* ast ) +{ + ResultCode resultCode; + zend_ast_decl* WP_Hook_astClassDecl = NULL; + zend_ast_decl** p_add_filter_astMethod = NULL; + + WP_Hook_astClassDecl = findClassAst( ast, g_globalNamespace, g_WP_Hook_className ); + if ( WP_Hook_astClassDecl == NULL ) + { + ELASTIC_APM_LOG_TRACE( "Class %s not found", g_WP_Hook_className.begin ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + + // public function add_filter( $hook_name, $callback, $priority, $accepted_args ) + p_add_filter_astMethod = findChildSlotForMethodAst( WP_Hook_astClassDecl, g_add_filter_methodName, /* minParamsCount */ 4 ); + if ( p_add_filter_astMethod == NULL ) + { + ELASTIC_APM_LOG_ERROR( "Method %s (in class %s) not found", g_add_filter_methodName.begin, g_WP_Hook_className.begin ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + + ELASTIC_APM_CALL_IF_FAILED_GOTO( insertPreHookForFunctionWithHookNameCallbackParams( *p_add_filter_astMethod ) ); + + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; +} + +static StringView g_get_template_funcName = ELASTIC_APM_STRING_LITERAL_TO_VIEW( "get_template" ); + +ResultCode wordPressInstrumentationTransformFile_theme_php( zend_ast* ast ) +{ + ResultCode resultCode; + zend_ast_decl** p_get_template_astFuncDecl = NULL; + + // function get_template() + p_get_template_astFuncDecl = findChildSlotForStandaloneFunctionAst( ast, g_globalNamespace, g_get_template_funcName, /* minParamsCount */ 0 ); + + if ( p_get_template_astFuncDecl == NULL ) + { + ELASTIC_APM_LOG_ERROR( "Function %s was not found", g_get_template_funcName.begin ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + + ELASTIC_APM_CALL_IF_FAILED_GOTO( wrapStandaloneFunctionAstWithPrePostHooks( p_get_template_astFuncDecl ) ); + + resultCode = resultSuccess; + finally: + return resultCode; + + failure: + goto finally; +} + +bool wordPressInstrumentationShouldTransformAstInFile( StringView compiledFileFullPath, size_t* pFileIndex ) +{ + if ( g_wordPressInstrumentationRequestScopedState.isInFailedMode ) + { + return false; + } + + ELASTIC_APM_FOR_EACH_INDEX( i, number_of_WordPress_instrumentation_files_to_transform_AST ) + { + if ( g_wordPressInstrumentationRequestScopedState.seenFile[ i ] ) + { + continue; + } + + if ( isStringViewSuffix( compiledFileFullPath, g_filesToTransformAstPathSuffix[ i ] ) ) + { + *pFileIndex = i; + return true; + } + } + + return false; +} + +typedef ResultCode (* WordPressTransformAstForFileFunc )( zend_ast* ast ); + +static WordPressTransformAstForFileFunc g_transformAstForFileFuncs[ number_of_WordPress_instrumentation_files_to_transform_AST ] = +{ + [ wordPress_instrumentation_file_to_transform_AST_plugin_php ] = wordPressInstrumentationTransformFile_plugin_php, + [ wordPress_instrumentation_file_to_transform_AST_class_wp_hook_php ] = wordPressInstrumentationTransformFile_class_wp_hook_php, + [ wordPress_instrumentation_file_to_transform_AST_theme_php ] = wordPressInstrumentationTransformFile_theme_php +}; + +void wordPressInstrumentationTransformAst( size_t fileIndex, StringView compiledFileFullPath, zend_ast* ast ) +{ + ELASTIC_APM_ASSERT_LT_UINT64( fileIndex, number_of_WordPress_instrumentation_files_to_transform_AST ); + ELASTIC_APM_ASSERT( ! g_wordPressInstrumentationRequestScopedState.seenFile[ fileIndex ], "fileIndex: %u, file: %s", (UInt)fileIndex, g_filesToTransformAstPathSuffix[ fileIndex ].begin ); + g_wordPressInstrumentationRequestScopedState.seenFile[ fileIndex ] = true; + + ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "compiledFileFullPath: %s", compiledFileFullPath.begin ); + + ResultCode resultCode; + + ELASTIC_APM_CALL_IF_FAILED_GOTO( g_transformAstForFileFuncs[ fileIndex ]( ast ) ); + + resultCode = resultSuccess; + finally: + ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG(); + ELASTIC_APM_UNUSED( resultCode ); + return; + + failure: + wordPressInstrumentationSwitchToFailedMode( __FUNCTION__ ); + goto finally; +} diff --git a/src/ext/WordPress_instrumentation.h b/src/ext/WordPress_instrumentation.h new file mode 100644 index 000000000..8656ade0b --- /dev/null +++ b/src/ext/WordPress_instrumentation.h @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +#include +#include "StringView.h" + +void wordPressInstrumentationOnRequestInit(); +void wordPressInstrumentationOnRequestShutdown(); + +bool wordPressInstrumentationShouldTransformAstInFile( StringView compiledFileFullPath, /* out */ size_t* pFileIndex ); +void wordPressInstrumentationTransformAst( size_t fileIndex, StringView compiledFileFullPath, zend_ast* ast ); + diff --git a/src/ext/config.m4 b/src/ext/config.m4 index 0eecd664f..ec76c6b7a 100644 --- a/src/ext/config.m4 +++ b/src/ext/config.m4 @@ -79,6 +79,7 @@ if test "$PHP_ELASTIC_APM" != "no"; then AC_DEFINE(HAVE_ELASTIC_APM, 1, [ Have elastic_apm support ]) ELASTIC_APM_PHP_EXT_SOURCES="\ + AST_debug.c \ AST_instrumentation.c \ backend_comm.c \ ConfigManager.c \ @@ -101,6 +102,7 @@ if test "$PHP_ELASTIC_APM" != "no"; then tracer_PHP_part.c \ util.c \ util_for_PHP.c \ + WordPress_instrumentation.c \ " PHP_NEW_EXTENSION(elastic_apm, $ELASTIC_APM_PHP_EXT_SOURCES, $ext_shared) diff --git a/src/ext/config.w32 b/src/ext/config.w32 index 5321f310c..dc72f4f11 100644 --- a/src/ext/config.w32 +++ b/src/ext/config.w32 @@ -13,6 +13,7 @@ if (PHP_ELASTIC_APM != 'no') { && CHECK_LIB("normaliz.lib", "elastic_apm", PHP_ELASTIC_APM) ) { ELASTIC_APM_PHP_EXT_SOURCES = + "AST_debug.c" + " " + "AST_instrumentation.c" + " " + "backend_comm.c" + " " + "ConfigManager.c" + " " + @@ -34,6 +35,7 @@ if (PHP_ELASTIC_APM != 'no') { "tracer_PHP_part.c" + " " + "util.c" + " " + "util_for_PHP.c" + " " + + "WordPress_instrumentation.c" + " " + ""; EXTENSION('elastic_apm', ELASTIC_APM_PHP_EXT_SOURCES, null, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1'); diff --git a/src/ext/constants.h b/src/ext/constants.h index 524fb56c8..48067fd69 100644 --- a/src/ext/constants.h +++ b/src/ext/constants.h @@ -53,3 +53,6 @@ ELASTIC_APM_STATIC_ASSERT( errorIdSizeInBytes <= idMaxSizeInBytes ); #define ELASTIC_APM_NUMBER_OF_MICROSECONDS_IN_MILLISECOND (1000) // 10^3 #define ELASTIC_APM_NUMBER_OF_NANOSECONDS_IN_SECOND (1000000000L) // 10^9 #define ELASTIC_APM_NUMBER_OF_NANOSECONDS_IN_MILLISECOND (1000000L) // 10^6 + +#define ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS "setReadyToWrapFilterCallbacks" +#define ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS_CONST_NAME "ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS" diff --git a/src/ext/elastic_apm.c b/src/ext/elastic_apm.c index cfdd4dbc4..e42ea2238 100644 --- a/src/ext/elastic_apm.c +++ b/src/ext/elastic_apm.c @@ -25,12 +25,14 @@ #include #include #include +#include "constants.h" #include "lifecycle.h" #include "supportability_zend.h" #include "elastic_apm_API.h" #include "ConfigManager.h" #include "elastic_apm_assert.h" #include "elastic_apm_alloc.h" +#include "tracer_PHP_part.h" #define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_EXT_INFRA @@ -139,6 +141,10 @@ PHP_INI_BEGIN() #if ( ELASTIC_APM_ASSERT_ENABLED_01 != 0 ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_ASSERT_LEVEL ) #endif + ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_ENABLED ) + ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE ) + ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX ) + ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_AST_PROCESS_DEBUG_DUMP_OUT_DIR ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_ASYNC_BACKEND_COMM ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_BOOTSTRAP_PHP_PART_FILE ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_BREAKDOWN_METRICS ) @@ -177,6 +183,7 @@ PHP_INI_BEGIN() ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_ENABLED ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION ) + ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_STACK_TRACE_LIMIT ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_IGNORE_URLS ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_MAX_SPANS ) ELASTIC_APM_INI_ENTRY( ELASTIC_APM_CFG_OPT_NAME_TRANSACTION_SAMPLE_RATE ) @@ -282,6 +289,10 @@ PHP_MINIT_FUNCTION(elastic_apm) REGISTER_LONG_CONSTANT( "ELASTIC_APM_MEMORY_TRACKING_LEVEL_EACH_ALLOCATION_WITH_STACK_TRACE", memoryTrackingLevel_eachAllocationWithStackTrace, CONST_CS|CONST_PERSISTENT ); REGISTER_LONG_CONSTANT( "ELASTIC_APM_MEMORY_TRACKING_LEVEL_ALL", memoryTrackingLevel_all, CONST_CS|CONST_PERSISTENT ); + REGISTER_STRING_CONSTANT( ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS_CONST_NAME + , ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS + , CONST_CS | CONST_PERSISTENT ); + elasticApmModuleInit( type, module_number ); // We ignore errors because we want the monitored application to continue working @@ -615,6 +626,46 @@ PHP_FUNCTION( elastic_apm_get_last_php_error ) } /* }}} */ +ZEND_BEGIN_ARG_INFO_EX( elastic_apm_before_loading_agent_php_code_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 ) +ZEND_END_ARG_INFO() +/* {{{ elastic_apm_before_loading_agent_php_code(): void + */ +PHP_FUNCTION( elastic_apm_before_loading_agent_php_code ) +{ + elasticApmBeforeLoadingAgentPhpCode(); +} +/* }}} */ + +ZEND_BEGIN_ARG_INFO_EX( elastic_apm_after_loading_agent_php_code_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 ) +ZEND_END_ARG_INFO() +/* {{{ elastic_apm_after_loading_agent_php_code(): void + */ +PHP_FUNCTION( elastic_apm_after_loading_agent_php_code ) +{ + elasticApmAfterLoadingAgentPhpCode(); +} +/* }}} */ + +ZEND_BEGIN_ARG_INFO_EX( elastic_apm_ast_instrumentation_pre_hook_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 ) +ZEND_END_ARG_INFO() +/* {{{ elastic_apm_after_loading_agent_php_code(): void + */ +PHP_FUNCTION( elastic_apm_ast_instrumentation_pre_hook ) +{ + tracerPhpPartAstInstrumentationCallPreHook( execute_data, return_value ); +} +/* }}} */ + +ZEND_BEGIN_ARG_INFO_EX( elastic_apm_ast_instrumentation_direct_call_arginfo, /* _unused */ 0, /* return_reference: */ 0, /* required_num_args: */ 0 ) +ZEND_END_ARG_INFO() +/* {{{ elastic_apm_after_loading_agent_php_code(): void + */ +PHP_FUNCTION( elastic_apm_ast_instrumentation_direct_call ) +{ + tracerPhpPartAstInstrumentationDirectCall( execute_data ); +} +/* }}} */ + /* {{{ arginfo */ ZEND_BEGIN_ARG_INFO(elastic_apm_no_paramters_arginfo, 0) @@ -634,6 +685,10 @@ static const zend_function_entry elastic_apm_functions[] = PHP_FE( elastic_apm_log, elastic_apm_log_arginfo ) PHP_FE( elastic_apm_get_last_thrown, elastic_apm_get_last_thrown_arginfo ) PHP_FE( elastic_apm_get_last_php_error, elastic_apm_get_last_php_error_arginfo ) + PHP_FE( elastic_apm_before_loading_agent_php_code, elastic_apm_before_loading_agent_php_code_arginfo ) + PHP_FE( elastic_apm_after_loading_agent_php_code, elastic_apm_after_loading_agent_php_code_arginfo ) + PHP_FE( elastic_apm_ast_instrumentation_pre_hook, elastic_apm_ast_instrumentation_pre_hook_arginfo ) + PHP_FE( elastic_apm_ast_instrumentation_direct_call, elastic_apm_ast_instrumentation_direct_call_arginfo ) PHP_FE_END }; /* }}} */ diff --git a/src/ext/elastic_apm_API.h b/src/ext/elastic_apm_API.h index 221a9d4c2..b8c6b9241 100644 --- a/src/ext/elastic_apm_API.h +++ b/src/ext/elastic_apm_API.h @@ -42,3 +42,6 @@ void resetCallInterceptionOnRequestShutdown(); ResultCode elasticApmSendToServer( StringView userAgentHttpHeader, StringView serializedEvents ); ResultCode replaceSleepWithResumingAfterSignalImpl(); + +void elasticApmBeforeLoadingAgentPhpCode(); +void elasticApmAfterLoadingAgentPhpCode(); diff --git a/src/ext/lifecycle.c b/src/ext/lifecycle.c index 10869e8d8..6b3b49477 100644 --- a/src/ext/lifecycle.c +++ b/src/ext/lifecycle.c @@ -41,6 +41,7 @@ #include "elastic_apm_API.h" #include "tracer_PHP_part.h" #include "backend_comm.h" +#include "AST_instrumentation.h" #define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_LIFECYCLE @@ -166,6 +167,8 @@ void elasticApmModuleInit( int moduleType, int moduleNumber ) } tracer->curlInited = true; + astInstrumentationOnModuleInit( config ); + resultCode = resultSuccess; finally: @@ -195,6 +198,8 @@ void elasticApmModuleShutdown( int moduleType, int moduleNumber ) goto finally; } + astInstrumentationOnModuleShutdown(); + backgroundBackendCommOnModuleShutdown( config ); if ( tracer->curlInited ) @@ -595,9 +600,6 @@ void elasticApmRequestInit() ELASTIC_APM_CALL_IF_FAILED_GOTO( replaceSleepWithResumingAfterSignalImpl() ); } - ELASTIC_APM_CALL_IF_FAILED_GOTO( bootstrapTracerPhpPart( config, &requestInitStartTime ) ); - -// readSystemMetrics( &tracer->startSystemMetricsReading ); if ( config->captureErrors ) { @@ -620,6 +622,13 @@ void elasticApmRequestInit() ELASTIC_APM_LOG_DEBUG( "capture_errors (captureErrors) configuration option is set to false which means errors will NOT be captured" ); } + if ( config->astProcessEnabled ) + { + astInstrumentationOnRequestInit( config ); + } + + ELASTIC_APM_CALL_IF_FAILED_GOTO( tracerPhpPartOnRequestInit( config, &requestInitStartTime ) ); + resultCode = resultSuccess; finally: @@ -685,7 +694,13 @@ void elasticApmRequestShutdown() resultCode = resultSuccess; goto finally; } + + tracerPhpPartOnRequestShutdown(); + if ( config->astProcessEnabled ) + { + astInstrumentationOnRequestShutdown(); + } if ( isOriginalZendThrowExceptionHookSet ) { @@ -709,11 +724,6 @@ void elasticApmRequestShutdown() isOriginalZendErrorCallbackSet = false; } - // We should shutdown PHP part first because sendMetrics() uses metadata sent by PHP part on shutdown - shutdownTracerPhpPart( config ); - - // sendMetrics( tracer, config ); - resetCallInterceptionOnRequestShutdown(); resetLastPhpErrorData(); diff --git a/src/ext/tracer_PHP_part.c b/src/ext/tracer_PHP_part.c index db133ea26..64466472f 100644 --- a/src/ext/tracer_PHP_part.c +++ b/src/ext/tracer_PHP_part.c @@ -22,6 +22,7 @@ #include "Tracer.h" #include "util_for_PHP.h" #include "basic_macros.h" +#include "elastic_apm_API.h" #include "ConfigSnapshot.h" #define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_C_TO_PHP @@ -32,31 +33,94 @@ #define ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_PRE_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "internalFuncCallPreHook" #define ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_POST_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "internalFuncCallPostHook" #define ELASTIC_APM_PHP_PART_EMPTY_METHOD_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "emptyMethod" +#define ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_PRE_HOOK_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "astInstrumentationPreHook" +#define ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_DIRECT_CALL_FUNC ELASTIC_APM_PHP_PART_FUNC_PREFIX "astInstrumentationDirectCall" -ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint* requestInitStartTime ) +enum TracerPhpPartState { - char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; - TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); - ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "config->bootstrapPhpPartFile: %s" - , streamUserString( config->bootstrapPhpPartFile, &txtOutStream ) ); + tracerPhpPartState_before_bootstrap, + tracerPhpPartState_after_bootstrap, + tracerPhpPartState_after_shutdown, + tracerPhpPartState_failed, + + numberOfTracerPhpPartState +}; +typedef enum TracerPhpPartState TracerPhpPartState; + +StringView tracerPhpPartStateNames[ numberOfTracerPhpPartState ] = +{ + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_before_bootstrap ), + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_after_bootstrap ), + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_after_shutdown ), + ELASTIC_APM_ENUM_NAMES_ARRAY_PAIR( tracerPhpPartState_failed ), +}; + +#define ELASTIC_APM_UNKNOWN_TRACER_PHP_PART_STATE_AS_STRING "" + +static inline +bool isValidTracerPhpPartState( TracerPhpPartState value ) +{ + return ( tracerPhpPartState_before_bootstrap <= value ) && ( value < numberOfTracerPhpPartState ); +} + +static inline +String tracerPhpPartStateToString( TracerPhpPartState value ) +{ + if ( isValidTracerPhpPartState( value ) ) + { + return tracerPhpPartStateNames[ value ].begin; + } + return ELASTIC_APM_UNKNOWN_TRACER_PHP_PART_STATE_AS_STRING; +} + +static TracerPhpPartState g_tracerPhpPartState = numberOfTracerPhpPartState; + +void switchTracerPhpPartStateToFailed( String reason, String dbgCalledFromFunc ) +{ + if ( g_tracerPhpPartState == tracerPhpPartState_failed ) + { + return; + } + + ELASTIC_APM_LOG_ERROR( "Switching tracer PHP part state to failed; reason: %s, current state: %s, called from %s" + , reason, tracerPhpPartStateToString( g_tracerPhpPartState ), dbgCalledFromFunc ); + g_tracerPhpPartState = tracerPhpPartState_failed; +} + +ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint* requestInitStartTime ) +{ ResultCode resultCode; + bool shouldRevertLoadingAgentPhpCode = false; bool bootstrapTracerPhpPartRetVal; zval maxEnabledLevel; ZVAL_UNDEF( &maxEnabledLevel ); zval requestInitStartTimeZval; ZVAL_UNDEF( &requestInitStartTimeZval ); + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY_MSG( "config->bootstrapPhpPartFile: %s, g_tracerPhpPartState: %s" + , streamUserString( config->bootstrapPhpPartFile, &txtOutStream ), tracerPhpPartStateToString( g_tracerPhpPartState ) ); + textOutputStreamRewind( &txtOutStream ); + + if ( g_tracerPhpPartState != tracerPhpPartState_before_bootstrap ) + { + switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + if ( config->bootstrapPhpPartFile == NULL ) { - // For now, we don't consider `bootstrap_php_part_file' option not being set as a failure GetConfigManagerOptionMetadataResult getMetaRes; getConfigManagerOptionMetadata( getGlobalTracer()->configManager, optionId_bootstrapPhpPartFile, &getMetaRes ); - ELASTIC_APM_LOG_ERROR( "Configuration option `%s' is not set", getMetaRes.optName ); - resultCode = resultSuccess; - goto finally; + switchTracerPhpPartStateToFailed( /* reason */ streamPrintf( &txtOutStream, "Configuration option `%s' is not set", getMetaRes.optName ), __FUNCTION__ ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } + elasticApmBeforeLoadingAgentPhpCode(); + shouldRevertLoadingAgentPhpCode = true; + ELASTIC_APM_CALL_IF_FAILED_GOTO( loadPhpFile( config->bootstrapPhpPartFile ) ); ZVAL_LONG( &maxEnabledLevel, getGlobalTracer()->logger.maxEnabledLevel ); @@ -73,32 +137,34 @@ ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } + g_tracerPhpPartState = tracerPhpPartState_after_bootstrap; resultCode = resultSuccess; finally: zval_dtor( &requestInitStartTimeZval ); zval_dtor( &maxEnabledLevel ); + if ( shouldRevertLoadingAgentPhpCode ) + { + elasticApmAfterLoadingAgentPhpCode(); + } ELASTIC_APM_LOG_DEBUG_RESULT_CODE_FUNCTION_EXIT(); return resultCode; failure: + switchTracerPhpPartStateToFailed( /* reason */ "Failed to bootstrap tracer PHP part", __FUNCTION__ ); goto finally; } -void shutdownTracerPhpPart( const ConfigSnapshot* config ) +void shutdownTracerPhpPart() { ELASTIC_APM_LOG_DEBUG_FUNCTION_ENTRY(); ResultCode resultCode; - if ( config->bootstrapPhpPartFile == NULL ) + if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap ) { - // For now, we don't consider `bootstrap_php_part_file' option not being set as a failure - GetConfigManagerOptionMetadataResult getMetaRes; - getConfigManagerOptionMetadata( getGlobalTracer()->configManager, optionId_bootstrapPhpPartFile, &getMetaRes ); - ELASTIC_APM_LOG_ERROR( "Configuration option `%s' is not set", getMetaRes.optName ); - resultCode = resultSuccess; - goto finally; + switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); } ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetVoid( @@ -106,6 +172,7 @@ void shutdownTracerPhpPart( const ConfigSnapshot* config ) , /* argsCount */ 0 , /* args */ NULL ) ); + g_tracerPhpPartState = tracerPhpPartState_after_shutdown; resultCode = resultSuccess; finally: @@ -116,25 +183,29 @@ void shutdownTracerPhpPart( const ConfigSnapshot* config ) return; failure: + switchTracerPhpPartStateToFailed( /* reason */ "Failed to shut down tracer PHP part", __FUNCTION__ ); goto finally; } +static uint32_t g_maxInterceptedCallArgsCount = 100; + bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zend_execute_data* execute_data ) { ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "interceptRegistrationId: %u", interceptRegistrationId ); ResultCode resultCode; zval preHookRetVal; - bool shouldCallPostHook; - + ZVAL_UNDEF( &preHookRetVal ); + bool shouldCallPostHook = false; zval interceptRegistrationIdAsZval; ZVAL_UNDEF( &interceptRegistrationIdAsZval ); + zval phpPartArgs[ g_maxInterceptedCallArgsCount + 2 ]; - enum + if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap ) { - maxInterceptedCallArgsCount = 100 - }; - zval phpPartArgs[maxInterceptedCallArgsCount + 2]; + switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } // The first argument to PHP part's interceptedCallPreHook() is $interceptRegistrationId ZVAL_LONG( &interceptRegistrationIdAsZval, interceptRegistrationId ); @@ -151,7 +222,7 @@ bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zen } uint32_t interceptedCallArgsCount; - getArgsFromZendExecuteData( execute_data, maxInterceptedCallArgsCount, &( phpPartArgs[ 2 ] ), &interceptedCallArgsCount ); + getArgsFromZendExecuteData( execute_data, g_maxInterceptedCallArgsCount, &( phpPartArgs[ 2 ] ), &interceptedCallArgsCount ); ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetZval( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_INTERNAL_FUNC_CALL_PRE_HOOK_FUNC ) @@ -177,6 +248,7 @@ bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zen return shouldCallPostHook; failure: + switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ ); goto finally; } @@ -188,6 +260,12 @@ void tracerPhpPartInternalFuncCallPostHook( uint32_t dbgInterceptRegistrationId, ResultCode resultCode; zval phpPartArgs[ 2 ]; + if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap ) + { + switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + // The first argument to PHP part's interceptedCallPostHook() is $hasExitedByException (bool) ZVAL_FALSE( &( phpPartArgs[ 0 ] ) ); @@ -211,6 +289,7 @@ void tracerPhpPartInternalFuncCallPostHook( uint32_t dbgInterceptRegistrationId, return; failure: + switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ ); goto finally; } @@ -222,6 +301,12 @@ void tracerPhpPartInterceptedCallEmptyMethod() zval phpPartDummyArgs[ 1 ]; ZVAL_UNDEF( &( phpPartDummyArgs[ 0 ] ) ); + if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap ) + { + switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetVoid( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_EMPTY_METHOD_FUNC ) @@ -230,7 +315,6 @@ void tracerPhpPartInterceptedCallEmptyMethod() ELASTIC_APM_LOG_TRACE( "Successfully finished call to PHP part" ); resultCode = resultSuccess; - finally: ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT(); @@ -238,5 +322,76 @@ void tracerPhpPartInterceptedCallEmptyMethod() return; failure: + switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ ); + goto finally; +} + +void tracerPhpPartLogArguments( LogLevel logLevel, uint32_t argsCount, zval args[] ) +{ + if ( maxEnabledLogLevel() < logLevel ) + { + return; + } + + char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; + TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); + + ELASTIC_APM_FOR_EACH_INDEX( i, argsCount ) + { + ELASTIC_APM_LOG_WITH_LEVEL( logLevel, "Argument #%u: %s", (unsigned)i, streamZVal( &( args[ i ] ), &txtOutStream ) ); + } +} + +void tracerPhpPartForwardCall( StringView phpFuncName, zend_execute_data* execute_data, /* out */ zval* retVal, String dbgCalledFrom ) +{ + ResultCode resultCode; + uint32_t callArgsCount; + zval callArgs[ g_maxInterceptedCallArgsCount ]; + + ELASTIC_APM_LOG_TRACE_FUNCTION_ENTRY_MSG( "phpFuncName: %s, dbgCalledFrom: %s", phpFuncName.begin, dbgCalledFrom ); + + if ( g_tracerPhpPartState != tracerPhpPartState_after_bootstrap ) + { + switchTracerPhpPartStateToFailed( /* reason */ "Unexpected current tracer PHP part state", __FUNCTION__ ); + ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE(); + } + + getArgsFromZendExecuteData( execute_data, g_maxInterceptedCallArgsCount, &( callArgs[ 0 ] ), &callArgsCount ); + tracerPhpPartLogArguments( logLevel_trace, callArgsCount, callArgs ); + + ELASTIC_APM_CALL_IF_FAILED_GOTO( callPhpFunctionRetZval( phpFuncName, callArgsCount, callArgs, /* out */ retVal ) ); + + resultCode = resultSuccess; + finally: + + ELASTIC_APM_LOG_TRACE_RESULT_CODE_FUNCTION_EXIT_MSG( "retVal type: %s (type ID as int: %d)", zend_get_type_by_const( (int)Z_TYPE_P( retVal ) ), (int)Z_TYPE_P( retVal ) ); + ELASTIC_APM_UNUSED( resultCode ); + return; + + failure: + switchTracerPhpPartStateToFailed( /* reason */ "Failed to call tracer PHP part", __FUNCTION__ ); + ZVAL_NULL( retVal ); goto finally; } + +void tracerPhpPartAstInstrumentationCallPreHook( zend_execute_data* execute_data, zval* return_value ) +{ + tracerPhpPartForwardCall( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_PRE_HOOK_FUNC ), execute_data, /* out */ return_value, __FUNCTION__ ); +} + +void tracerPhpPartAstInstrumentationDirectCall( zend_execute_data* execute_data ) +{ + zval unusedRetVal; + tracerPhpPartForwardCall( ELASTIC_APM_STRING_LITERAL_TO_VIEW( ELASTIC_APM_PHP_PART_AST_INSTRUMENTATION_DIRECT_CALL_FUNC ), execute_data, /* out */ &unusedRetVal, __FUNCTION__ ); +} + +ResultCode tracerPhpPartOnRequestInit( const ConfigSnapshot* config, const TimePoint* requestInitStartTime ) +{ + g_tracerPhpPartState = tracerPhpPartState_before_bootstrap; + return bootstrapTracerPhpPart( config, requestInitStartTime ); +} + +void tracerPhpPartOnRequestShutdown() +{ + shutdownTracerPhpPart(); +} diff --git a/src/ext/tracer_PHP_part.h b/src/ext/tracer_PHP_part.h index 0569d3471..9380c85eb 100644 --- a/src/ext/tracer_PHP_part.h +++ b/src/ext/tracer_PHP_part.h @@ -24,12 +24,13 @@ #include "ConfigSnapshot_forward_decl.h" #include "time_util.h" -ResultCode bootstrapTracerPhpPart( const ConfigSnapshot* config, const TimePoint* requestInitStartTime ); - -void shutdownTracerPhpPart( const ConfigSnapshot* config ); +ResultCode tracerPhpPartOnRequestInit( const ConfigSnapshot* config, const TimePoint* requestInitStartTime ); +void tracerPhpPartOnRequestShutdown(); bool tracerPhpPartInternalFuncCallPreHook( uint32_t interceptRegistrationId, zend_execute_data* execute_data ); - void tracerPhpPartInternalFuncCallPostHook( uint32_t dbgInterceptRegistrationId, zval* interceptedCallRetValOrThrown ); void tracerPhpPartInterceptedCallEmptyMethod(); + +void tracerPhpPartAstInstrumentationCallPreHook( zend_execute_data* execute_data, zval* return_value ); +void tracerPhpPartAstInstrumentationDirectCall( zend_execute_data* execute_data ); diff --git a/src/ext/unit_tests/util_tests.c b/src/ext/unit_tests/util_tests.c index bf7f09c75..fb54c8c9a 100644 --- a/src/ext/unit_tests/util_tests.c +++ b/src/ext/unit_tests/util_tests.c @@ -226,12 +226,41 @@ void test_sizeToBytes( void** testFixtureState ) } } +static +bool isStringViewSuffixWrapperForTest( String str, String suffix ) +{ + return isStringViewSuffix( stringToView(str), stringToView(suffix) ); +} + +static +void isStringViewSuffix_test( void** testFixtureState ) +{ + ELASTIC_APM_UNUSED( testFixtureState ); + + ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "", "" ) ); + ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "a", "a" ) ); + ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "ab", "b" ) ); + ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "ab", "ab" ) ); + ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "abc", "bc" ) ); + ELASTIC_APM_CMOCKA_ASSERT( isStringViewSuffixWrapperForTest( "abc", "abc" ) ); + + ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "", "a" ) ); + ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "a", "A" ) ); + ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "A", "a" ) ); + ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "a", "ab" ) ); + ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "ab", "aB" ) ); + ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "ab", "abc" ) ); + ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "abc", "abC" ) ); + ELASTIC_APM_CMOCKA_ASSERT( ! isStringViewSuffixWrapperForTest( "abc", "abcd" ) ); +} + int run_util_tests() { const struct CMUnitTest tests [] = { ELASTIC_APM_CMOCKA_UNIT_TEST( areStringViewsEqual_test ), ELASTIC_APM_CMOCKA_UNIT_TEST( areStringsEqualIgnoringCase_test ), + ELASTIC_APM_CMOCKA_UNIT_TEST( isStringViewSuffix_test ), ELASTIC_APM_CMOCKA_UNIT_TEST( calcAlignedSize_test ), ELASTIC_APM_CMOCKA_UNIT_TEST( trim_StringView_test ), ELASTIC_APM_CMOCKA_UNIT_TEST( test_parseDecimalInteger ), diff --git a/src/ext/util_for_PHP.c b/src/ext/util_for_PHP.c index c7368f4fd..8b1388f1f 100644 --- a/src/ext/util_for_PHP.c +++ b/src/ext/util_for_PHP.c @@ -309,4 +309,39 @@ bool detectOpcachePreload() { void enableAccessToServerGlobal() { zend_is_auto_global_str(ZEND_STRL("_SERVER")); -} \ No newline at end of file +} + +String streamZVal( const zval* zVal, TextOutputStream* txtOutStream ) +{ + if ( zVal == NULL ) + { + return "NULL"; + } + + int zValType = (int)Z_TYPE_P( zVal ); + switch ( zValType ) + { + case IS_STRING: + { + StringView strVw = zStringToStringView( Z_STR_P( zVal ) ); + return streamPrintf( txtOutStream, "type: string, value [length: %"PRIu64"]: %.*s", (UInt64)(strVw.length), (int)(strVw.length), strVw.begin ); + } + + case IS_LONG: + return streamPrintf( txtOutStream, "type: long, value: %"PRId64, (Int64)(Z_LVAL_P( zVal )) ); + + case IS_DOUBLE: + return streamPrintf( txtOutStream, "type: double, value: %f", (double)(Z_DVAL_P( zVal )) ); + + case IS_NULL: + return streamPrintf( txtOutStream, "type: null" ); + + case IS_FALSE: + return streamPrintf( txtOutStream, "type: false" ); + case IS_TRUE: + return streamPrintf( txtOutStream, "type: true " ); + + default: + return streamPrintf( txtOutStream, "type: %s (type ID as int: %d)", zend_get_type_by_const( zValType ), (int)zValType ); + } +} diff --git a/src/ext/util_for_PHP.h b/src/ext/util_for_PHP.h index 8a6f69e7e..ad0f852bc 100644 --- a/src/ext/util_for_PHP.h +++ b/src/ext/util_for_PHP.h @@ -105,3 +105,6 @@ void enableAccessToServerGlobal(); } \ } while( 0 ) \ /**/ + + +String streamZVal( const zval* zVal, TextOutputStream* txtOutStream ); diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php index c6b059e61..a0213b54f 100644 --- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php +++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php @@ -122,6 +122,12 @@ private static function buildOptionNameToRawToValue(): array return [ OptionNames::API_KEY => $stringRawToParsedValues(['1my_api_key3', "my api \t key"]), + OptionNames::AST_PROCESS_ENABLED => $boolRawToParsedValues(), + OptionNames::AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE + => $boolRawToParsedValues(), + OptionNames::AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX + => $stringRawToParsedValues(['/', '/myDir']), + OptionNames::AST_PROCESS_DEBUG_DUMP_OUT_DIR => $stringRawToParsedValues(['/', '/myDir']), OptionNames::ASYNC_BACKEND_COMM => $asyncBackendCommValues, OptionNames::BREAKDOWN_METRICS => $boolRawToParsedValues(), OptionNames::CAPTURE_ERRORS => $boolRawToParsedValues(), @@ -154,6 +160,7 @@ private static function buildOptionNameToRawToValue(): array => $durationRawToParsedValues, OptionNames::SPAN_COMPRESSION_SAME_KIND_MAX_DURATION => $durationRawToParsedValues, + OptionNames::STACK_TRACE_LIMIT => $intRawToParsedValues, OptionNames::TRANSACTION_IGNORE_URLS => $wildcardListRawToParsedValues, OptionNames::TRANSACTION_MAX_SPANS => $intRawToParsedValues, OptionNames::TRANSACTION_SAMPLE_RATE => $doubleRawToParsedValues, diff --git a/tests/ElasticApmTests/ComponentTests/Util/AllComponentTestsOptionsMetadata.php b/tests/ElasticApmTests/ComponentTests/Util/AllComponentTestsOptionsMetadata.php index d40a1bfd8..ce2ddd47f 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/AllComponentTestsOptionsMetadata.php +++ b/tests/ElasticApmTests/ComponentTests/Util/AllComponentTestsOptionsMetadata.php @@ -66,6 +66,7 @@ public static function get(): array self::APP_CODE_HOST_KIND_OPTION_NAME => new NullableAppCodeHostKindOptionMetadata(), 'app_code_php_exe' => new NullableStringOptionMetadata(), self::APP_CODE_PHP_INI_OPTION_NAME => new NullableStringOptionMetadata(), + 'compare_ast_converted_back_to_source' => new BoolOptionMetadata(true), self::DATA_PER_PROCESS_OPTION_NAME => new NullableCustomOptionMetadata( function (string $rawValue): TestInfraDataPerProcess { $deserializedObj = new TestInfraDataPerProcess(); diff --git a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php index 5787e865c..6b8bcd0ed 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php +++ b/tests/ElasticApmTests/ComponentTests/Util/AppCodeHostParams.php @@ -100,6 +100,18 @@ public function setAgentOption(string $optName, $optVal, ?AgentConfigSourceKind $this->agentOptions[$sourceKindAsString][$optName] = $optVal; } + /** + * @param string $optName + * @param string|int|float|bool $optVal + * @param ?AgentConfigSourceKind $sourceKind + */ + public function setAgentOptionIfNotDefaultValue(string $optName, $optVal, ?AgentConfigSourceKind $sourceKind = null): void + { + if (!ConfigUtilForTests::isAgentOptionDefaultValue($optName, $optVal)) { + $this->setAgentOption($optName, $optVal, $sourceKind); + } + } + /** * @param array $optsMap * @param ?AgentConfigSourceKind $sourceKind diff --git a/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php b/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php index 12c1cf771..94bc89068 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php +++ b/tests/ElasticApmTests/ComponentTests/Util/ComponentTestCaseBase.php @@ -29,8 +29,10 @@ use Elastic\Apm\Impl\Log\Level as LogLevel; use Elastic\Apm\Impl\Log\LoggableToString; use Elastic\Apm\Impl\Tracer; +use Elastic\Apm\Impl\Util\BoolUtil; use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\RangeUtil; +use ElasticApmTests\Util\AssertMessageStack; use ElasticApmTests\Util\DataFromAgent; use ElasticApmTests\Util\IterableUtilForTests; use ElasticApmTests\Util\MixedMap; @@ -185,6 +187,9 @@ protected function verifyOneTransactionNoSpans(DataFromAgent $dataFromAgent): Tr */ protected static function implTestIsAutoInstrumentationEnabled(string $instrClassName, array $expectedNames): void { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['instrClassName' => $instrClassName, 'expectedNames' => $expectedNames]); + /** @var AutoInstrumentationBase $instr */ $instr = new $instrClassName(self::buildTracerForTests()->build()); $actualNames = $instr->keywords(); @@ -208,13 +213,14 @@ protected static function implTestIsAutoInstrumentationEnabled(string $instrClas }; foreach ($expectedNames as $name) { + $dbgCtx->pushSubScope(); foreach ($genDisabledVariants($name) as $disableInstrumentationsOptVal) { - $tracer = self::buildTracerForTests() - ->withConfig(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal) - ->build(); + $dbgCtx->clearCurrentSubScope(['name' => $name, 'disableInstrumentationsOptVal' => $disableInstrumentationsOptVal]); + $tracer = self::buildTracerForTests()->withConfig(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal)->build(); $instr = new $instrClassName($tracer); - self::assertFalse($instr->isEnabled(), $disableInstrumentationsOptVal); + self::assertFalse($instr->isEnabled()); } + $dbgCtx->popSubScope(); } /** @@ -225,13 +231,24 @@ protected static function implTestIsAutoInstrumentationEnabled(string $instrClas yield '*someOtherDummyInstrumentationA*, *someOtherDummyInstrumentationB*'; }; + $dbgCtx->pushSubScope(); foreach ($genEnabledVariants() as $disableInstrumentationsOptVal) { - $tracer = self::buildTracerForTests() - ->withConfig(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal) - ->build(); + $dbgCtx->clearCurrentSubScope(['disableInstrumentationsOptVal' => $disableInstrumentationsOptVal]); + $tracer = self::buildTracerForTests()->withConfig(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal)->build(); $instr = new $instrClassName($tracer); - self::assertTrue($instr->isEnabled(), $disableInstrumentationsOptVal); + self::assertTrue($instr->isEnabled()); } + $dbgCtx->popSubScope(); + + $dbgCtx->pushSubScope(); + foreach ([true, false] as $astProcessEnabled) { + $dbgCtx->clearCurrentSubScope(['astProcessEnabled' => $astProcessEnabled]); + $expectedIsEnabled = $astProcessEnabled || (!$instr->requiresUserlandCodeInstrumentation()); + $tracer = self::buildTracerForTests()->withConfig(OptionNames::AST_PROCESS_ENABLED, BoolUtil::toString($astProcessEnabled))->build(); + $instr = new $instrClassName($tracer); + self::assertSame($expectedIsEnabled, $instr->isEnabled()); + } + $dbgCtx->popSubScope(); } /** @@ -252,6 +269,25 @@ public function adaptToSmoke(iterable $variants): iterable return []; } + /** + * @template TKey of array-key + * @template TValue + * + * @param iterable $variants + * + * @return iterable + */ + public function adaptKeyValueToSmoke(iterable $variants): iterable + { + if (!self::isSmoke()) { + return $variants; + } + foreach ($variants as $key => $value) { + return [$key => $value]; + } + return []; + } + /** * @return iterable */ diff --git a/tests/ElasticApmTests/ComponentTests/Util/ConfigSnapshotForTests.php b/tests/ElasticApmTests/ComponentTests/Util/ConfigSnapshotForTests.php index faa9f74de..49ae08e05 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/ConfigSnapshotForTests.php +++ b/tests/ElasticApmTests/ComponentTests/Util/ConfigSnapshotForTests.php @@ -48,6 +48,9 @@ final class ConfigSnapshotForTests implements LoggableInterface /** @var ?string */ public $appCodePhpIni; + /** @var bool */ + public $compareAstConvertedBackToSource; + /** @var TestInfraDataPerProcess */ public $dataPerProcess; diff --git a/tests/ElasticApmTests/ComponentTests/Util/ConfigUtilForTests.php b/tests/ElasticApmTests/ComponentTests/Util/ConfigUtilForTests.php index 25e129dc3..8bbd004f1 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/ConfigUtilForTests.php +++ b/tests/ElasticApmTests/ComponentTests/Util/ConfigUtilForTests.php @@ -110,4 +110,16 @@ public static function allAgentLogLevelRelatedOptionNames(): iterable } } } + + /** + * @param string $optName + * @param string|int|float|bool $optVal + * + * @return bool + */ + public static function isAgentOptionDefaultValue(string $optName, $optVal): bool + { + $optMeta = AllOptionsMetadata::get()[$optName]; + return $optVal === $optMeta->defaultValue(); + } } diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockBridge.php b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockBridge.php new file mode 100644 index 000000000..664074bc9 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockBridge.php @@ -0,0 +1,323 @@ + */ + public static $expectedCallbackArgs = null; + + /** @var mixed */ + public static $expectedCallbackReturnValue = null; + + /** @var int */ + public static $mockMuPluginCallbackCallsCount = 0; + + /** @var int */ + public static $mockPluginCallbackCallsCount = 0; + + /** @var int */ + public static $mockThemeCallbackCallsCount = 0; + + /** @var int */ + public static $mockPartOfCoreCallbackCallsCount = 0; + + public const MOCK_MU_PLUGIN_HOOK_NAME = 'save_post'; + public const MOCK_PLUGIN_HOOK_NAME = 'media_upload_newtab'; + public const MOCK_THEME_HOOK_NAME = 'set_header_font'; + public const MOCK_PART_OF_CORE_HOOK_NAME = 'update_footer'; + + public const MOCK_MU_PLUGIN_NAME = 'my_mock_mu_plugin'; + public const MOCK_PLUGIN_NAME = 'my_mock_plugin'; + public const MOCK_THEME_NAME = 'my_mock_theme'; + + public static function loadMockSource(string $srcVariantBaseDir, bool $isExpectedVariant): void + { + $wpIncludesDir = $srcVariantBaseDir . DIRECTORY_SEPARATOR . WordPressAutoInstrumentationTest::WP_INCLUDES_DIR_NAME; + require $wpIncludesDir . DIRECTORY_SEPARATOR . 'plugin.php'; + require $wpIncludesDir . DIRECTORY_SEPARATOR . 'theme.php'; + + if ($isExpectedVariant) { + return; + } + + $wpContentDir = $srcVariantBaseDir . DIRECTORY_SEPARATOR . 'wp-content'; + require FileUtilForTests::listToPath([$wpContentDir, 'mu-plugins', self::MOCK_MU_PLUGIN_NAME . '.php']); + require FileUtilForTests::listToPath([$wpContentDir, 'plugins', self::MOCK_PLUGIN_NAME, 'main.php']); + require FileUtilForTests::listToPath([$wpContentDir, 'themes', self::MOCK_THEME_NAME, 'index.php']); + require FileUtilForTests::listToPath([$srcVariantBaseDir, 'wp-admin', 'part_of_core.php']); + } + + /** + * @return mixed + * + * @noinspection PhpReturnDocTypeMismatchInspection + */ + private static function callWpGetTemplate() + { + /** + * @noinspection PhpFullyQualifiedNameUsageInspection + * @phpstan-ignore-next-line + */ + return \get_template(); + } + + /** + * @param string $hookName + * @param mixed $firstArg + * @param mixed ...$args + * + * @return mixed + */ + private static function callWpApplyFilters(string $hookName, $firstArg, ...$args) + { + /** + * @noinspection PhpFullyQualifiedNameUsageInspection + * @phpstan-ignore-next-line + */ + return \apply_filters($hookName, $firstArg, ...$args); + } + + /** + * @param string $hookName + * @param mixed ...$args + */ + private static function callWpDoAction(string $hookName, ...$args): void + { + /** + * @noinspection PhpFullyQualifiedNameUsageInspection + * @phpstan-ignore-next-line + */ + \do_action($hookName, ...$args); + } + + public static function runMockSource(MixedMap $appCodeArgs): void + { + $activeTheme = $appCodeArgs->getNullableString(WordPressAutoInstrumentationTest::EXPECTED_THEME_KEY); + $muPluginCallsCount = $appCodeArgs->getInt(WordPressAutoInstrumentationTest::MU_PLUGIN_CALLS_COUNT_KEY); + $pluginCallsCount = $appCodeArgs->getInt(WordPressAutoInstrumentationTest::PLUGIN_CALLS_COUNT_KEY); + $themeCallsCount = $appCodeArgs->getInt(WordPressAutoInstrumentationTest::THEME_CALLS_COUNT_KEY); + $partOfCoreCallsCount = $appCodeArgs->getInt(WordPressAutoInstrumentationTest::PART_OF_CORE_CALLS_COUNT_KEY); + + // Have instrumented get_template() return non-string value first to test that instrumentation handles it correctly + WordPressMockBridge::$activeTheme = false; + TestCaseBase::assertSame(self::$activeTheme, self::callWpGetTemplate()); + WordPressMockBridge::$activeTheme = $activeTheme; + TestCaseBase::assertSame(self::$activeTheme, self::callWpGetTemplate()); + + $applyFiltersCallsCount = 0; + $doActionCallsCount = 0; + $callIndex = 0; + + $invokeCallbacks = function (string $hookName) use (&$callIndex, &$applyFiltersCallsCount, &$doActionCallsCount): void { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['callIndex' => $callIndex, 'applyFiltersCallsCount' => $applyFiltersCallsCount, 'doActionCallsCount' => $doActionCallsCount]); + $shouldUseApplyFilters = ($callIndex % 2) === 0; + $dbgCtx->add(['shouldUseApplyFilters' => $shouldUseApplyFilters]); + // do_action allows no arguments but apply_filters requires at least one argument + $argsCount = ($callIndex % 3) + ($shouldUseApplyFilters ? 1 : 0); + $dbgCtx->add(['argsCount' => $argsCount]); + $args = []; + foreach (RangeUtil::generateUpTo($argsCount) as $i) { + switch ($i % 4) { + case 0: + $args[] = 'dummy string arg'; + break; + case 1: + $args[] = 98761234; + break; + case 2: + $args[] = 3.1416; + break; + case 3: + $args[] = new stdClass(); + break; + } + } + $dbgCtx->add(['args' => $args]); + TestCaseBase::assertCount($argsCount, $args); + + self::$expectedCallbackArgs = $args; + self::$expectedCallbackReturnValue = ($callIndex % 5) === 0 ? new stdClass() : 'dummy string return value'; + if ($shouldUseApplyFilters) { + $actualRetVal = self::callWpApplyFilters($hookName, ...$args); + ++$applyFiltersCallsCount; + TestCaseBase::assertSame(self::$expectedCallbackReturnValue, $actualRetVal); + } else { + self::callWpDoAction($hookName, ...$args); + ++$doActionCallsCount; + } + self::$expectedCallbackArgs = null; + self::$expectedCallbackReturnValue = null; + ++$callIndex; + }; + + foreach (RangeUtil::generateUpTo($muPluginCallsCount) as $ignored) { + $invokeCallbacks(self::MOCK_MU_PLUGIN_HOOK_NAME); + } + + foreach (RangeUtil::generateUpTo($pluginCallsCount) as $ignored) { + $invokeCallbacks(self::MOCK_PLUGIN_HOOK_NAME); + } + + foreach (RangeUtil::generateUpTo($themeCallsCount) as $ignored) { + $invokeCallbacks(self::MOCK_THEME_HOOK_NAME); + } + + foreach (RangeUtil::generateUpTo($partOfCoreCallsCount) as $ignored) { + $invokeCallbacks(self::MOCK_PART_OF_CORE_HOOK_NAME); + } + + $txCtx = ElasticApm::getCurrentTransaction()->context(); + $txCtx->setLabel(WordPressAutoInstrumentationTest::APPLY_FILTERS_CALLS_COUNT_KEY, $applyFiltersCallsCount); + $txCtx->setLabel(WordPressAutoInstrumentationTest::DO_ACTION_CALLS_COUNT_KEY, $doActionCallsCount); + $txCtx->setLabel(WordPressAutoInstrumentationTest::MU_PLUGIN_CALLS_COUNT_KEY, self::$mockMuPluginCallbackCallsCount); + $txCtx->setLabel(WordPressAutoInstrumentationTest::PLUGIN_CALLS_COUNT_KEY, self::$mockPluginCallbackCallsCount); + $txCtx->setLabel(WordPressAutoInstrumentationTest::THEME_CALLS_COUNT_KEY, self::$mockThemeCallbackCallsCount); + $txCtx->setLabel(WordPressAutoInstrumentationTest::PART_OF_CORE_CALLS_COUNT_KEY, self::$mockPartOfCoreCallbackCallsCount); + } + + /** + * @param array $actualArgs + */ + public static function assertCallbackArgsAsExpected(array $actualArgs): void + { + TestCaseBase::assertNotNull(self::$expectedCallbackArgs); + TestCaseBase::assertEqualLists(self::$expectedCallbackArgs, $actualArgs); + } + + public static function callWpFilterBuildUniqueId(string $hookName, callable $callback, int $priority): ?string + { + /** + * @noinspection PhpFullyQualifiedNameUsageInspection + * @phpstan-ignore-next-line + */ + return \_wp_filter_build_unique_id($hookName, $callback, $priority); + } + + private static function newWpHook(): WordPressMockWpHook + { + /** + * @noinspection PhpFullyQualifiedNameUsageInspection + * @phpstan-ignore-next-line + */ + return new \WP_Hook(); + } + + /** + * @param array &$wpFilterGlobal + * @param string $hookName + * @param callable $callback + * @param int $priority + * @param int $acceptedArgsCount + */ + public static function mockImplAddFiler(array &$wpFilterGlobal, string $hookName, callable $callback, int $priority, int $acceptedArgsCount): void + { + if (!array_key_exists($hookName, $wpFilterGlobal)) { + $wpFilterGlobal[$hookName] = self::newWpHook(); + } + + /** + * @phpstan-ignore-next-line + */ + $wpFilterGlobal[$hookName]->add_filter($hookName, $callback, $priority, $acceptedArgsCount); + } + + /** + * @param array &$wpFilterGlobal + * @param string $hookName + * @param callable $callback + * @param int $priority + * + * @return bool + */ + public static function mockImplRemoveFilter(array &$wpFilterGlobal, string $hookName, callable $callback, int $priority): bool + { + if (!array_key_exists($hookName, $wpFilterGlobal)) { + return false; + } + + $wpHook = $wpFilterGlobal[$hookName]; + $wpHook->mockImplRemoveFilter($hookName, $callback, $priority); + if ($wpHook->mockImplIsEmpty()) { + unset($wpFilterGlobal[$hookName]); + } + + return true; + } + + /** + * @param array $wpFilterGlobal + * @param string $hookName + * @param mixed $firstArg + * @param mixed ...$restOfArgs + * + * @return mixed + */ + public static function mockImplApplyFilters(array $wpFilterGlobal, string $hookName, $firstArg, ...$restOfArgs) + { + if (!array_key_exists($hookName, $wpFilterGlobal)) { + return $firstArg; + } + + $allArgs = $restOfArgs; + array_unshift(/* ref */ $allArgs, $firstArg); + + return $wpFilterGlobal[$hookName]->mockApplyFilters($allArgs); + } + + /** + * @param array $wpFilterGlobal + * @param string $hookName + * @param mixed ...$args + */ + public static function mockImplDoAction(array $wpFilterGlobal, string $hookName, ...$args): void + { + if (!array_key_exists($hookName, $wpFilterGlobal)) { + return; + } + + $wpFilterGlobal[$hookName]->mockApplyFilters($args); + } +} diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockWpHook.php b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockWpHook.php new file mode 100644 index 000000000..19a126cb0 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressMockWpHook.php @@ -0,0 +1,75 @@ + */ + private $idToCallback = []; + + public function __construct() + { + } + + public function mockImplAddFilter(string $hookName, callable $callback, int $priority): void + { + $id = WordPressMockBridge::callWpFilterBuildUniqueId($hookName, $callback, $priority); + Assert::assertIsString($id); + $this->idToCallback[$id] = $callback; + } + + public function mockImplRemoveFilter(string $hookName, callable $callback, int $priority): void + { + $id = WordPressMockBridge::callWpFilterBuildUniqueId($hookName, $callback, $priority); + Assert::assertIsString($id); + unset($this->idToCallback[$id]); + } + + public function mockImplIsEmpty(): bool + { + return ArrayUtil::isEmpty($this->idToCallback); + } + + /** + * @param array $args + * + * @return mixed + */ + public function mockApplyFilters(array $args) + { + $retVal = null; + foreach ($this->idToCallback as $callback) { + $retVal = call_user_func_array($callback, $args); + } + return $retVal; + } +} diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/WordPressSpanExpectationsBuilder.php b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressSpanExpectationsBuilder.php new file mode 100644 index 000000000..fae111745 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/WordPressSpanExpectationsBuilder.php @@ -0,0 +1,75 @@ +startNew(); + $result->name->setValue($hookName . ' - ' . $addonName); + $result->type->setValue($addonGroup); + $result->subtype->setValue($addonName); + $result->action->setValue($hookName); + return $result; + } + + public function forPluginFilterCallback(string $hookName, string $pluginName): SpanExpectations + { + return $this->forAddonFilterCallback($hookName, /* addonGroup */ self::EXPECTED_SPAN_TYPE_FOR_PLUGIN, $pluginName); + } + + public function forThemeFilterCallback(string $hookName, string $themeName): SpanExpectations + { + return $this->forAddonFilterCallback($hookName, /* addonGroup */ self::EXPECTED_SPAN_TYPE_FOR_THEME, $themeName); + } + + public function forCoreFilterCallback(string $hookName): SpanExpectations + { + /** + * @see WordPressFilterCallbackWrapper::__invoke + */ + $result = $this->startNew(); + $result->name->setValue($hookName . ' - WordPress core'); + $result->type->setValue(self::EXPECTED_SPAN_TYPE_FOR_CORE); + $result->subtype->setValue(null); + $result->action->setValue($hookName); + return $result; + } +} diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/README.md b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/README.md new file mode 100644 index 000000000..d8e1ba168 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/README.md @@ -0,0 +1 @@ +# Source code equivalent to WordPress mock source code after AST auto instrumentation diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/class-wp-hook.php b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/class-wp-hook.php new file mode 100644 index 000000000..8c9439d66 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/class-wp-hook.php @@ -0,0 +1,28 @@ +>> END Elasitc APM tests marker to fold into one line */ + $this->mockImplAddFilter($hook_name, $callback, $priority); + } } + + private function dummy_last_method(int $x): int + { + return $x * 2; + } +} diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/plugin.php b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/plugin.php new file mode 100644 index 000000000..a0c1742e5 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/plugin.php @@ -0,0 +1,122 @@ +>> END Elasitc APM tests marker to fold into one line */ + if ( is_string( $callback ) ) { + return $callback; + } + + if ( is_object( $callback ) ) { + // Closures are currently implemented as objects. + $callback = array( $callback, '' ); + } else { + $callback = (array) $callback; + } + + if ( is_object( $callback[0] ) ) { + // Object class calling. + return spl_object_hash( $callback[0] ) . $callback[1]; + } elseif ( is_string( $callback[0] ) ) { + // Static calling. + return $callback[0] . '::' . $callback[1]; + } +} }/* <<< BEGIN Elasitc APM tests marker to fold into one line */ + + \elastic_apm_ast_instrumentation_direct_call(\ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS); +} +/* >>> END Elasitc APM tests marker to fold into one line */ + +function dummy_last_function_in_plugin_php(int $x): int +{ + return $x * 2; +} diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/theme.php b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/theme.php new file mode 100644 index 000000000..ed57f913c --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/expected_process_AST_output/wp-includes/theme.php @@ -0,0 +1,79 @@ +value = $value; + } +} + +define('MY_DUMMY_GLOBAL_FLOAT_CONST', 3.1416); + +/** + * Retrieves name of the active theme. + * + * @since 1.5.0 + * + * @return string Template name. + */ +{ #[MyDummyAttribute('get_template attribute arg')] +function get_templateElasticApmWrapped_suffixToBeRemovedByElasticApmTests( + /** doc comment for param1 */ + #[MyDummyAttribute('param1 attribute arg')] + int $param1 = __LINE__, + ?string $param2 = MyDummyAttribute::DUMMY_CONST, + /** doc comment for param3 */ + #[MyDummyAttribute()] + float &$param3 = MY_DUMMY_GLOBAL_FLOAT_CONST +) { + /** + * get_template() body + */ + return WordPressMockBridge::$activeTheme; +}/* <<< BEGIN Elasitc APM tests marker to fold into one line */ + +/** + * Retrieves name of the active theme. + * + * @since 1.5.0 + * + * @return string Template name. + */ +#[MyDummyAttribute('get_template attribute arg')] +function get_template( + /** doc comment for param1 */ + #[MyDummyAttribute('param1 attribute arg')] + int $param1 = __LINE__, + ?string $param2 = MyDummyAttribute::DUMMY_CONST, + /** doc comment for param3 */ + #[MyDummyAttribute()] + float &$param3 = MY_DUMMY_GLOBAL_FLOAT_CONST +) { + $args = \func_get_args(); + $postHook = \elastic_apm_ast_instrumentation_pre_hook(/* instrumentedClassFullName */ null, __FUNCTION__, $args); + try { + $retVal = get_templateElasticApmWrapped_suffixToBeRemovedByElasticApmTests(...$args); + if ($postHook !== null) $postHook(/* thrown */ null, $retVal); + return $retVal; + } catch (\Throwable $thrown) { + if ($postHook !== null) $postHook($thrown, /* retVal */ null); + throw $thrown; + } +} +} +/* >>> END Elasitc APM tests marker to fold into one line */ + +function &dummy_last_function_in_theme_php(int &$x): int { + return $x; +} diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/README.md b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/README.md new file mode 100644 index 000000000..86513e060 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/README.md @@ -0,0 +1 @@ +# WordPress mock source code diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-admin/part_of_core.php b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-admin/part_of_core.php new file mode 100644 index 000000000..8c4fe7913 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-admin/part_of_core.php @@ -0,0 +1,30 @@ +mockImplAddFilter($hook_name, $callback, $priority); + } + + private function dummy_last_method(int $x): int + { + return $x * 2; + } +} diff --git a/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-includes/plugin.php b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-includes/plugin.php new file mode 100644 index 000000000..e007ed02d --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPress/mock_src/wp-includes/plugin.php @@ -0,0 +1,113 @@ +value = $value; + } +} + +define('MY_DUMMY_GLOBAL_FLOAT_CONST', 3.1416); + +/** + * Retrieves name of the active theme. + * + * @since 1.5.0 + * + * @return string Template name. + */ +#[MyDummyAttribute('get_template attribute arg')] +function get_template( + /** doc comment for param1 */ + #[MyDummyAttribute('param1 attribute arg')] + int $param1 = __LINE__, + ?string $param2 = MyDummyAttribute::DUMMY_CONST, + /** doc comment for param3 */ + #[MyDummyAttribute()] + float &$param3 = MY_DUMMY_GLOBAL_FLOAT_CONST +) { + /** + * get_template() body + */ + return WordPressMockBridge::$activeTheme; +} + +function &dummy_last_function_in_theme_php(int &$x): int { + return $x; +} diff --git a/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php b/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php new file mode 100644 index 000000000..2f26b5071 --- /dev/null +++ b/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php @@ -0,0 +1,603 @@ +>> END Elasitc APM tests marker to fold into one line */'; + + private const DEBUG_DUMP_DIR_NAME = 'debug_dump'; + + private const DEBUG_DUMP_FILE_EXTENSION = 'txt'; + private const BEFORE_AST_PROCESS_FILE_NAME_SUFFIX = 'before_AST_process'; + private const AFTER_AST_PROCESS_FILE_NAME_SUFFIX = 'after_AST_process'; + + private const CONVERTED_BACK_TO_SOURCE_FILE_EXTENSION = 'php'; + private const AST_CONVERTED_BACK_TO_SOURCE_FILE_NAME_SUFFIX = 'converted_back_to_source'; + + private const ADAPTED_FILE_NAME_SUFFIX = 'adapted'; + private const SUFFIX_TO_BE_REMOVED_BY_ELASTIC_APM_TESTS = '_suffixToBeRemovedByElasticApmTests'; + + /** + * @return string[] + */ + private static function expectedFilesToBeAstTransformed(): array + { + /** + * @see src/ext/WordPress_instrumentation.c + */ + + $result = []; + $result[] = self::WP_INCLUDES_DIR_NAME . DIRECTORY_SEPARATOR . 'plugin.php'; + $result[] = self::WP_INCLUDES_DIR_NAME . DIRECTORY_SEPARATOR . 'class-wp-hook.php'; + $result[] = self::WP_INCLUDES_DIR_NAME . DIRECTORY_SEPARATOR . 'theme.php'; + return $result; + } + + private static function getLoggerForThisClass(): Logger + { + static $logger = null; + if ($logger === null) { + $logger = AmbientContextForTests::loggerFactory()->loggerForClass(LogCategoryForTests::TEST, __NAMESPACE__, __CLASS__, __FILE__); + } + return $logger; + } + + public function testIsAutoInstrumentationEnabled(): void + { + // In production code ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS is defined by the native part of the agent + // but if we don't load elastic_apm extension in the component tests so we need to define a dummy + $constantName = 'ELASTIC_APM_WORDPRESS_DIRECT_CALL_METHOD_SET_READY_TO_WRAP_FILTER_CALLBACKS'; + if (!defined($constantName)) { + define($constantName, 'dummy unused value'); + } + + self::implTestIsAutoInstrumentationEnabled(WordPressAutoInstrumentation::class, /* expectedNames */ ['wordpress']); + } + + private static function buildInputOrExpectedOutputVariantSubDir(string $baseDir, bool $isExpectedVariant): string + { + return $baseDir . DIRECTORY_SEPARATOR . ($isExpectedVariant ? 'expected_process_AST_output' : 'mock_src'); + } + + public static function removeAttributes(string $fileContents): string + { + $adaptedLines = []; + foreach (TextUtilForTests::iterateLinesEx($fileContents) as [$line, $endOfLine]) { + $line = trim($line); + + if (TextUtil::isSuffixOf(']', $line)) { + $attrStartPos = strpos($line, '#['); + if ($attrStartPos !== false) { + $line = substr($line, /* offset */ 0, $attrStartPos); + $endOfLine = ''; + } + } + + $adaptedLines[] = $line . $endOfLine; + } + + return implode(/* separator */ '', $adaptedLines); + } + + public static function foldTextWithMarkersIntoOneLine(string $fileContents): string + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['fileContents' => $fileContents]); + + $adaptedLines = []; + $isBetweenMarkers = false; + foreach (TextUtilForTests::iterateLinesEx($fileContents) as [$line, $endOfLine]) { + if ($isBetweenMarkers) { + if (($endMarkerStartPos = strpos($line, WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_END_MARKER)) !== false) { + // Verify that line with end marker does not have any other non-whitespace text + self::assertTrue(TextUtil::isEmptyString(trim(substr($line, /* offset */ 0, $endMarkerStartPos)))); + self::assertTrue(TextUtil::isEmptyString(trim(substr($line, /* offset */ $endMarkerStartPos + strlen(WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_END_MARKER))))); + $adaptedLines[] = $endOfLine; + $isBetweenMarkers = false; + continue; + } + + $line = trim($line); + + // Attributes were introduced in PHP 8 and earlier PHP versions interpret the rest of the line after # as a comment + if (PHP_MAJOR_VERSION < 8 && TextUtil::isPrefixOf('#', $line)) { + continue; + } + + if (!TextUtil::isEmptyString($line)) { + $adaptedLines[] = ' ' . $line; + } + continue; + } + + if (($beginMarkerStartPos = strpos($line, WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_BEGIN_MARKER)) !== false) { + $isBetweenMarkers = true; + // Add part before begin marker start position + $adaptedLines[] = substr($line, /* offset */ 0, $beginMarkerStartPos); + // Verify that there is no non-whitespace text after marker end + $partAfterMarkerEnd = substr($line, /* offset */ $beginMarkerStartPos + strlen(WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_BEGIN_MARKER)); + $dbgCtx->add(['partAfterMarkerEnd' => $partAfterMarkerEnd]); + self::assertTrue(TextUtil::isEmptyString(trim($partAfterMarkerEnd))); + continue; + } + + $adaptedLines[] = $line . $endOfLine; + } + + return implode(/* separator */ '', $adaptedLines); + } + + private static function adaptSourceFileContent(bool $isExpectedVariant, string $fileContents): string + { + $adaptedFileContents = $fileContents; + + // Attributes were introduced in PHP 8 and earlier PHP versions interpret the rest of the line after # as a comment + if (PHP_MAJOR_VERSION < 8) { + $adaptedFileContents = self::removeAttributes($adaptedFileContents); + } + + if ($isExpectedVariant) { + $adaptedFileContents = self::foldTextWithMarkersIntoOneLine($adaptedFileContents); + } + + return $adaptedFileContents; + } + + private static function adaptSourceTree(bool $isExpectedVariant, string $fromDir, string $toDir): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['isExpectedVariant' => $isExpectedVariant, 'fromDir' => $fromDir, 'toDir' => $toDir]); + + $logger = self::getLoggerForThisClass(); + $loggerProxyDebug = $logger->ifDebugLevelEnabledNoLine(__FUNCTION__); + + self::assertNotFalse($fromDirEntries = scandir($fromDir)); + $dbgCtx->pushSubScope(); + foreach ($fromDirEntries as $entryName) { + if ($entryName == '.' || $entryName == '..') { + continue; + } + $dbgCtx->clearCurrentSubScope(['entryName' => $entryName]); + $fromDirEntryFullPath = $fromDir . DIRECTORY_SEPARATOR . $entryName; + if (is_dir($fromDirEntryFullPath)) { + $toSubDirFullPath = $toDir . DIRECTORY_SEPARATOR . $entryName; + $loggerProxyDebug && $loggerProxyDebug->log(__LINE__, 'Creating directory...', ['toSubDirFullPath' => $toSubDirFullPath]); + self::assertTrue(mkdir($toSubDirFullPath)); + self::adaptSourceTree($isExpectedVariant, $fromDirEntryFullPath, $toSubDirFullPath); + continue; + } + + $srcFileInfo = new SplFileInfo($fromDirEntryFullPath); + if (!($srcFileInfo->isFile() && ($srcFileInfo->getExtension() === 'php'))) { + continue; + } + $srcFileRelPath = FileUtilForTests::convertPathRelativeTo($fromDirEntryFullPath, $fromDir); + $adaptedSrcFileFullPath = FileUtilForTests::listToPath([$toDir, $srcFileRelPath]); + $loggerProxyDebug && $loggerProxyDebug->log(__LINE__, 'Creating file...', ['adaptedSrcFileFullPath' => $adaptedSrcFileFullPath]); + $dbgCtx->add(['fromDirEntryFullPath' => $fromDirEntryFullPath, 'adaptedSrcFileFullPath' => $adaptedSrcFileFullPath]); + self::assertFileExists($fromDirEntryFullPath); + self::assertNotFalse($srcFileContents = file_get_contents($fromDirEntryFullPath)); + $adaptedSrcFileContents = self::adaptSourceFileContent($isExpectedVariant, $srcFileContents); + if ($adaptedSrcFileContents !== $srcFileContents) { + ($loggerProxy = $logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log('Contents of ' . $adaptedSrcFileFullPath . ':' . "\n" . $adaptedSrcFileContents); + } + self::assertNotFalse(file_put_contents($adaptedSrcFileFullPath, $adaptedSrcFileContents)); + self::assertFileExists($adaptedSrcFileFullPath); + $loggerProxyDebug && $loggerProxyDebug->log(__LINE__, 'Created file', ['adaptedSrcFileFullPath' => $adaptedSrcFileFullPath]); + } + $dbgCtx->popSubScope(); + } + + /** + * @return iterable + */ + public function dataProviderForTestAstProcessOnMockSource(): iterable + { + $result = (new DataProviderForTestBuilder()) + ->addBoolKeyedDimensionAllValuesCombinable(self::IS_AST_PROCESS_ENABLED_KEY) + ->addBoolKeyedDimensionAllValuesCombinable(self::IS_AST_PROCESS_DEBUG_DUMP_ENABLED_KEY) + ->build(); + + return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result)); + } + + /** + * @dataProvider dataProviderForTestAstProcessOnMockSource + */ + public function testAstProcessOnMockSource(MixedMap $testArgs): void + { + $subDirName = FileUtilForTests::buildTempSubDirName(__CLASS__, __FUNCTION__); + self::runAndEscalateLogLevelOnFailure( + self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs), + function () use ($subDirName, $testArgs): void { + $tempOutDir = FileUtilForTests::createTempSubDir($subDirName); + try { + $this->implTestAstProcessOnMockSource($tempOutDir, $testArgs); + } finally { + FileUtilForTests::deleteTempSubDir($subDirName); + } + } + ); + } + + private static function removeSuffixToBeRemovedByElasticApmTests(string $fileContents): string + { + $adaptedLines = []; + foreach (TextUtilForTests::iterateLines($fileContents, /* keepEndOfLine */ true) as $line) { + $adaptedLines[] = str_replace(self::SUFFIX_TO_BE_REMOVED_BY_ELASTIC_APM_TESTS, '', $line); + } + + return implode(/* separator */ '', $adaptedLines); + } + + private static function adaptManuallyInstrumentedGeneratedFile(/* in,out */ string &$filePath, /* in,out */ string &$fileContents): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['filePath' => $filePath, 'fileContents' => $fileContents]); + + $fileInfo = new SplFileInfo($filePath); + $adaptedFileName = $fileInfo->getBasename($fileInfo->getExtension()) . self::ADAPTED_FILE_NAME_SUFFIX . '.' . $fileInfo->getExtension(); + $adaptedFilePath = $fileInfo->getPath() . DIRECTORY_SEPARATOR . $adaptedFileName; + $adaptedFileContents = $fileContents; + $adaptedFileContents = self::removeSuffixToBeRemovedByElasticApmTests($adaptedFileContents); + $dbgCtx->add(['adaptedFilePath' => $adaptedFilePath, 'adaptedFileContents' => $adaptedFileContents]); + self::assertNotFalse(file_put_contents($adaptedFilePath, $adaptedFileContents)); + $filePath = $adaptedFilePath; + $fileContents = $adaptedFileContents; + } + + private static function logFileContentOnMismatch(string $filePath, string $fileContents): void + { + $logger = self::getLoggerForThisClass(); + $loggerProxy = $logger->ifCriticalLevelEnabledNoLine(__FUNCTION__); + if ($loggerProxy === null) { + return; + } + + $loggerProxy->log(__LINE__, 'Content of ' . $filePath . ' begin [length: ' . strlen($fileContents) . ']:' . PHP_EOL . $fileContents); + $loggerProxy->log(__LINE__, 'Content of ' . $filePath . ' end'); + } + + private static function verifyAstProcessGeneratedFiles(string $astProcessDebugDumpOutDir, string $phpFileRelativePath): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['astProcessDebugDumpOutDir' => $astProcessDebugDumpOutDir, 'phpFileRelativePath' => $phpFileRelativePath]); + + $logger = self::getLoggerForThisClass()->addAllContext(['astProcessDebugDumpOutDir' => $astProcessDebugDumpOutDir, 'phpFileRelativePath' => $phpFileRelativePath]); + + /** + * @param-out string $fileFullPath + * @param-out string $fileContents + */ + $getGeneratedFileContents = function ( + bool $isExpectedVariant, + bool $isAstDebugDump, + ?string &$fileFullPath, + ?string &$fileContents + ) use ( + $astProcessDebugDumpOutDir, + $phpFileRelativePath + ): void { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['isExpectedVariant' => $isExpectedVariant, 'isAstDebugDump' => $isAstDebugDump, 'fileFullPath' => $fileFullPath]); + + $outSubDir = self::buildInputOrExpectedOutputVariantSubDir($astProcessDebugDumpOutDir, $isExpectedVariant); + $fileName = $phpFileRelativePath . '.' . ($isExpectedVariant ? self::BEFORE_AST_PROCESS_FILE_NAME_SUFFIX : self::AFTER_AST_PROCESS_FILE_NAME_SUFFIX); + $fileName .= '.' . ($isAstDebugDump ? self::DEBUG_DUMP_FILE_EXTENSION : (self::AST_CONVERTED_BACK_TO_SOURCE_FILE_NAME_SUFFIX . '.' . self::CONVERTED_BACK_TO_SOURCE_FILE_EXTENSION)); + + $fileFullPath = FileUtilForTests::listToPath([$outSubDir, $fileName]); + self::assertFileExists($fileFullPath); + $fileContents = file_get_contents($fileFullPath); + self::assertNotFalse($fileContents); + }; + + ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Starting...'); + + $getGeneratedFileContents(/* isExpectedVariant */ true, /* isAstDebugDump */ true, /* out */ $expectedAstFilePath, /* out */ $expectedAstFileContents); + self::adaptManuallyInstrumentedGeneratedFile(/* in,out */ $expectedAstFilePath, /* out */ $expectedAstFileContents); + $getGeneratedFileContents(/* isExpectedVariant */ false, /* isAstDebugDump */ true, /* out */ $actualAstFilePath, /* out */ $actualAstFileContents); + $astMatches = ($actualAstFileContents === $expectedAstFileContents); + + if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) { + $getGeneratedFileContents(/* isExpectedVariant */ true, /* isAstDebugDump */ false, /* out */ $expectedPhpFilePath, /* out */ $expectedPhpFileContents); + self::adaptManuallyInstrumentedGeneratedFile(/* in,out */ $expectedPhpFilePath, /* out */ $expectedPhpFileContents); + $getGeneratedFileContents(/* isExpectedVariant */ false, /* isAstDebugDump */ false, /* out */ $actualPhpFilePath, /* out */ $actualPhpFileContents); + $phpMatches = ($actualPhpFileContents === $expectedPhpFileContents); + } else { + $phpMatches = true; + $expectedPhpFilePath = ''; + $expectedPhpFileContents = ''; + $actualPhpFilePath = ''; + $actualPhpFileContents = ''; + } + + if ($astMatches && $phpMatches) { + return; + } + + $logCtx = ['astMatches' => $astMatches]; + if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) { + $logCtx['phpMatches'] = $phpMatches; + } + + ($loggerProxy = $logger->ifCriticalLevelEnabled(__LINE__, __FUNCTION__)) + && $loggerProxy->log('Actual generated files do not match the expected', $logCtx); + + self::logFileContentOnMismatch($expectedAstFilePath, $expectedAstFileContents); + if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) { + self::logFileContentOnMismatch($expectedPhpFilePath, $expectedPhpFileContents); + } + self::logFileContentOnMismatch($actualAstFilePath, $actualAstFileContents); + if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) { + self::logFileContentOnMismatch($actualPhpFilePath, $actualPhpFileContents); + } + + if (!$astMatches) { + self::assertSame($expectedAstFilePath, $actualAstFilePath); + } elseif (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) { + self::assertSame($expectedPhpFilePath, $actualPhpFileContents); + } + } + + public static function appCodeForTestAstProcessOnMockSource(MixedMap $appCodeArgs): void + { + $srcVariantBaseDir = $appCodeArgs->getString(self::SRC_VARIANT_DIR_KEY); + $isExpectedVariant = $appCodeArgs->getBool(self::IS_EXPECTED_VARIANT_KEY); + WordPressMockBridge::loadMockSource($srcVariantBaseDir, $isExpectedVariant); + } + + private function implTestAstProcessOnMockSource(string $tempOutDir, MixedMap $testArgs): void + { + $adaptedSrcBaseDir = FileUtilForTests::listToPath([$tempOutDir, self::ADAPTED_SOURCE_DIR_NAME]); + $astProcessDebugDumpOutDir = FileUtilForTests::listToPath([$tempOutDir, self::DEBUG_DUMP_DIR_NAME]); + self::assertTrue(mkdir($adaptedSrcBaseDir)); + + $isAstProcessEnabled = $testArgs->getBool(self::IS_AST_PROCESS_ENABLED_KEY); + $isAstProcessDebugDumpEnabled = $testArgs->getBool(self::IS_AST_PROCESS_DEBUG_DUMP_ENABLED_KEY); + + $testCaseHandle = $this->getTestCaseHandle(); + $appCodeHost = $testCaseHandle->ensureMainAppCodeHost( + function (AppCodeHostParams $appCodeParams) use ($adaptedSrcBaseDir, $astProcessDebugDumpOutDir, $isAstProcessEnabled, $isAstProcessDebugDumpEnabled): void { + $appCodeParams->setAgentOptionIfNotDefaultValue(OptionNames::AST_PROCESS_ENABLED, $isAstProcessEnabled); + $appCodeParams->setAgentOption(OptionNames::AST_PROCESS_DEBUG_DUMP_FOR_PATH_PREFIX, $adaptedSrcBaseDir); + if ($isAstProcessDebugDumpEnabled) { + $appCodeParams->setAgentOption(OptionNames::AST_PROCESS_DEBUG_DUMP_OUT_DIR, $astProcessDebugDumpOutDir); + } + $appCodeParams->setAgentOptionIfNotDefaultValue(OptionNames::AST_PROCESS_DEBUG_DUMP_CONVERTED_BACK_TO_SOURCE, AmbientContextForTests::testConfig()->compareAstConvertedBackToSource); + } + ); + + foreach ([true, false] as $isExpectedVariant) { + $srcVariantBaseDir = self::buildInputOrExpectedOutputVariantSubDir(self::SRC_VARIANTS_DIR, $isExpectedVariant); + $adaptedSrcVariantBaseDir = self::buildInputOrExpectedOutputVariantSubDir($adaptedSrcBaseDir, $isExpectedVariant); + self::assertTrue(mkdir($adaptedSrcVariantBaseDir)); + self::adaptSourceTree($isExpectedVariant, $srcVariantBaseDir, $adaptedSrcVariantBaseDir); + $appCodeHost->sendRequest( + AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestAstProcessOnMockSource']), + function (AppCodeRequestParams $appCodeRequestParams) use ($adaptedSrcVariantBaseDir, $isExpectedVariant): void { + $appCodeRequestParams->setAppCodeArgs([self::SRC_VARIANT_DIR_KEY => $adaptedSrcVariantBaseDir, self::IS_EXPECTED_VARIANT_KEY => $isExpectedVariant]); + } + ); + } + + if ($isAstProcessEnabled && $isAstProcessDebugDumpEnabled) { + foreach (self::expectedFilesToBeAstTransformed() as $phpFileRelativePath) { + self::verifyAstProcessGeneratedFiles($astProcessDebugDumpOutDir, $phpFileRelativePath); + } + } else { + self::assertDirectoryDoesNotExist($astProcessDebugDumpOutDir); + } + } + + /** + * @return iterable + */ + public function dataProviderForTestOnMockSource(): iterable + { + $disableInstrumentationsVariants = [ + '' => true, + 'WordPress' => false, + ]; + + $result = (new DataProviderForTestBuilder()) + ->addBoolKeyedDimensionAllValuesCombinable(self::IS_AST_PROCESS_ENABLED_KEY) + ->addGeneratorOnlyFirstValueCombinable( + AutoInstrumentationUtilForTests::disableInstrumentationsDataProviderGenerator( + $disableInstrumentationsVariants + ) + ) + ->addKeyedDimensionOnlyFirstValueCombinable(self::EXPECTED_THEME_KEY, ['my_favorite_theme', null, 'some_other_theme']) + ->addKeyedDimensionOnlyFirstValueCombinable(self::PLUGIN_CALLS_COUNT_KEY, [10, 1, 2, 0]) + ->addKeyedDimensionOnlyFirstValueCombinable(self::MU_PLUGIN_CALLS_COUNT_KEY, [7, 1, 2, 0]) + ->addKeyedDimensionOnlyFirstValueCombinable(self::THEME_CALLS_COUNT_KEY, [13, 1, 2, 0]) + ->addKeyedDimensionOnlyFirstValueCombinable(self::PART_OF_CORE_CALLS_COUNT_KEY, [11, 1, 2, 0]) + ->build(); + + return DataProviderForTestBuilder::convertEachDataSetToMixedMap(self::adaptKeyValueToSmoke($result)); + } + + /** + * @dataProvider dataProviderForTestOnMockSource + */ + public function testOnMockSource(MixedMap $testArgs): void + { + self::runAndEscalateLogLevelOnFailure( + self::buildDbgDescForTestWithArtgs(__CLASS__, __FUNCTION__, $testArgs), + function () use ($testArgs): void { + $this->implTestOnMockSource($testArgs); + } + ); + } + + public static function appCodeForTestOnMockSource(MixedMap $appCodeArgs): void + { + $srcVariantBaseDir = self::buildInputOrExpectedOutputVariantSubDir(self::SRC_VARIANTS_DIR, /* isExpectedVariant */ false); + WordPressMockBridge::loadMockSource($srcVariantBaseDir, /* isExpectedVariant */ false); + + WordPressMockBridge::runMockSource($appCodeArgs); + } + + private function implTestOnMockSource(MixedMap $testArgs): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['testArgs' => $testArgs]); + + $isAstProcessEnabled = $testArgs->getBool(self::IS_AST_PROCESS_ENABLED_KEY); + $disableInstrumentationsOptVal = $testArgs->getString(AutoInstrumentationUtilForTests::DISABLE_INSTRUMENTATIONS_KEY); + $isInstrumentationEnabled = $testArgs->getBool(AutoInstrumentationUtilForTests::IS_INSTRUMENTATION_ENABLED_KEY); + $isWordPressDataToBeExpected = $isAstProcessEnabled && $isInstrumentationEnabled; + $expectedTheme = $testArgs->getNullableString(self::EXPECTED_THEME_KEY); + $expectedMuPluginCallsCount = $testArgs->getInt(self::MU_PLUGIN_CALLS_COUNT_KEY); + $expectedPluginCallsCount = $testArgs->getInt(self::PLUGIN_CALLS_COUNT_KEY); + $expectedThemeCallsCount = $testArgs->getInt(self::THEME_CALLS_COUNT_KEY); + $expectedPartOfCoreCallsCount = $testArgs->getInt(self::PART_OF_CORE_CALLS_COUNT_KEY); + + $testCaseHandle = $this->getTestCaseHandle(); + $appCodeHost = $testCaseHandle->ensureMainAppCodeHost( + function (AppCodeHostParams $appCodeParams) use ($isAstProcessEnabled, $disableInstrumentationsOptVal): void { + $appCodeParams->setAgentOptionIfNotDefaultValue(OptionNames::AST_PROCESS_ENABLED, $isAstProcessEnabled); + $appCodeParams->setAgentOption(OptionNames::DISABLE_INSTRUMENTATIONS, $disableInstrumentationsOptVal); + // Disable span compression to have all the expected spans individually + $appCodeParams->setAgentOption(OptionNames::SPAN_COMPRESSION_ENABLED, false); + } + ); + + $expectationsBuilder = new WordPressSpanExpectationsBuilder(); + /** @var SpanExpectations[] $expectedSpans */ + $expectedSpans = []; + if ($isWordPressDataToBeExpected) { + /** + * @see WordPressMockBridge::runMockSource + */ + foreach (RangeUtil::generateUpTo($expectedMuPluginCallsCount) as $ignored) { + $expectedSpans[] = $expectationsBuilder->forPluginFilterCallback(WordPressMockBridge::MOCK_MU_PLUGIN_HOOK_NAME, WordPressMockBridge::MOCK_MU_PLUGIN_NAME); + } + foreach (RangeUtil::generateUpTo($expectedPluginCallsCount) as $ignored) { + $expectedSpans[] = $expectationsBuilder->forPluginFilterCallback(WordPressMockBridge::MOCK_PLUGIN_HOOK_NAME, WordPressMockBridge::MOCK_PLUGIN_NAME); + } + foreach (RangeUtil::generateUpTo($expectedThemeCallsCount) as $ignored) { + $expectedSpans[] = $expectationsBuilder->forThemeFilterCallback(WordPressMockBridge::MOCK_THEME_HOOK_NAME, WordPressMockBridge::MOCK_THEME_NAME); + } + foreach (RangeUtil::generateUpTo($expectedPartOfCoreCallsCount) as $ignored) { + $expectedSpans[] = $expectationsBuilder->forCoreFilterCallback(WordPressMockBridge::MOCK_PART_OF_CORE_HOOK_NAME); + } + } + + $appCodeHost->sendRequest( + AppCodeTarget::asRouted([__CLASS__, 'appCodeForTestOnMockSource']), + function (AppCodeRequestParams $appCodeRequestParams) use ($testArgs): void { + $appCodeRequestParams->setAppCodeArgs($testArgs); + } + ); + + $dataFromAgent = $testCaseHandle->waitForDataFromAgent( + (new ExpectedEventCounts())->transactions(1)->spans(count($expectedSpans)) + ); + + $tx = $dataFromAgent->singleTransaction(); + $dbgCtx->add(['tx' => $tx]); + + if ((!$isWordPressDataToBeExpected) || ($expectedTheme === null)) { + self::assertTrue($tx->context === null || $tx->context->labels === null || !array_key_exists(self::EXPECTED_LABEL_KEY_FOR_WORDPRESS_THEME, $tx->context->labels)); + } else { + self::assertSame($expectedTheme, self::getLabel($tx, self::EXPECTED_LABEL_KEY_FOR_WORDPRESS_THEME)); + } + + $actualMuPluginCallsCount = self::getLabel($tx, self::MU_PLUGIN_CALLS_COUNT_KEY); + self::assertSame($expectedMuPluginCallsCount, $actualMuPluginCallsCount); + $actualPluginCallsCount = self::getLabel($tx, self::PLUGIN_CALLS_COUNT_KEY); + self::assertSame($expectedPluginCallsCount, $actualPluginCallsCount); + $actualThemeCallsCount = self::getLabel($tx, self::THEME_CALLS_COUNT_KEY); + self::assertSame($expectedThemeCallsCount, $actualThemeCallsCount); + $actualPartOfCoreCallsCount = self::getLabel($tx, self::PART_OF_CORE_CALLS_COUNT_KEY); + self::assertSame($expectedPartOfCoreCallsCount, $actualPartOfCoreCallsCount); + + $applyFiltersCallsCount = self::getLabel($tx, self::APPLY_FILTERS_CALLS_COUNT_KEY); + self::assertIsInt($applyFiltersCallsCount); + $doActionCallsCount = self::getLabel($tx, self::DO_ACTION_CALLS_COUNT_KEY); + self::assertIsInt($doActionCallsCount); + self::assertSame($applyFiltersCallsCount + $doActionCallsCount, $expectedMuPluginCallsCount + $expectedPluginCallsCount + $actualThemeCallsCount + $expectedPartOfCoreCallsCount); + + if (!$isWordPressDataToBeExpected) { + return; + } + + SpanSequenceValidator::updateExpectationsEndTime($expectedSpans); + SpanSequenceValidator::assertSequenceAsExpected($expectedSpans, array_values($dataFromAgent->idToSpan)); + } +} diff --git a/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php b/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php new file mode 100644 index 000000000..72e8d466a --- /dev/null +++ b/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php @@ -0,0 +1,258 @@ +add( + [ + 'filePath' => $filePath, + 'adaptedFilePath' => $adaptedFilePath, + 'expectedGroupKind' => $expectedGroupKind, + 'actualGroupKind' => $actualGroupKind, + 'expectedGroupName' => $expectedGroupName, + 'actualGroupName' => $actualGroupName, + ] + ); + self::assertSame($expectedGroupKind, $actualGroupKind); + self::assertSame($expectedGroupName, $actualGroupName); + }; + + $pluginFilePathToExpectedName = [ + '/var/www/html/wp-content/plugins/hello-dolly/hello.php' => 'hello-dolly', + '/var/www/html/wp-content/plugins/hello-dolly/' => 'hello-dolly', + '/wp-content/plugins/hello-dolly/hello.php' => 'hello-dolly', + '/wp-content/plugins/hello.php' => 'hello', + + '/var/www/html/wp-content/mu-plugins/hello-dolly/hello.php' => 'hello-dolly', + '/var/www/html/wp-content/mu-plugins/hello-dolly/' => 'hello-dolly', + '/wp-content/mu-plugins/hello-dolly/hello.php' => 'hello-dolly', + '/wp-content/mu-plugins/hello.php' => 'hello', + 'mock_src/wp-content/mu-plugins/my_mock_mu_plugin.php' => 'my_mock_mu_plugin', + ]; + foreach ($pluginFilePathToExpectedName as $filePath => $expectedGroupName) { + $testImpl($filePath, /* $expectedGroupKind */ WordPressAutoInstrumentation::CALLBACK_GROUP_KIND_PLUGIN, $expectedGroupName); + } + + $themeFilePathToExpectedName = [ + '/var/www/html/wp-content/themes/hello-dolly/hello.php' => 'hello-dolly', + '/var/www/html/wp-content/themes/hello-dolly/' => 'hello-dolly', + '/wp-content/themes/hello-dolly/hello.php' => 'hello-dolly', + '/wp-content/themes/hello.php' => 'hello', + ]; + foreach ($themeFilePathToExpectedName as $filePath => $expectedGroupName) { + $testImpl($filePath, /* $expectedGroupKind */ WordPressAutoInstrumentation::CALLBACK_GROUP_KIND_THEME, $expectedGroupName); + } + + $coreFilePaths = [ + 'wp-content/plugins/hello-dolly/hello.php', + '/var/www/html/wp-content/plugins/hello-dolly', + '/var/www/html/wp-content/mu-plugins/hello-dolly', + 'wp-content/mu-plugins/hello-dolly/hello.php', + '/var/www/html/wp-content/themes/hello-dolly', + 'wp-content/themes/hello-dolly/hello.php', + '', + '/', + '//', + '/abc', + ]; + foreach ($coreFilePaths as $filePath) { + $testImpl($filePath, /* $expectedGroupKind */ WordPressAutoInstrumentation::CALLBACK_GROUP_KIND_CORE, /* expectedGroupName */ null); + } + } + + /** + * @return iterable + */ + public static function dataProviderForTestFoldTextWithMarkersIntoOneLine(): iterable + { + yield ['', '']; + yield ['some text', 'some text']; + + $indent = "\t \t"; + $whiteSpace = "\t\t \t\t"; + $whiteSpaceBeforeBeginMarker = "\t\t\t \t\t\t \t\t\t"; + $input + = $indent . 'Original line 1' . PHP_EOL . + $indent . 'Original line 2' . $whiteSpaceBeforeBeginMarker . WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_BEGIN_MARKER . $whiteSpace . PHP_EOL . + $indent . 'Injected into line 2 - part A' . $whiteSpace . PHP_EOL . + $indent . 'Injected into line 2 - part B' . $whiteSpace . PHP_EOL . + $indent . WordPressAutoInstrumentationTest::FOLD_INTO_ONE_LINE_END_MARKER . $whiteSpace . PHP_EOL . + $indent . 'Original line 3' . PHP_EOL; + $expectedOutput + = $indent . 'Original line 1' . PHP_EOL . + $indent . 'Original line 2' . $whiteSpaceBeforeBeginMarker . ' ' . 'Injected into line 2 - part A' . ' ' . 'Injected into line 2 - part B' . PHP_EOL . + $indent . 'Original line 3' . PHP_EOL; + yield [$input, $expectedOutput]; + yield [$expectedOutput, $expectedOutput]; + } + + /** + * @dataProvider dataProviderForTestFoldTextWithMarkersIntoOneLine + * + * @param string $input + * @param string $expectedOutput + */ + public static function testFoldTextWithMarkersIntoOneLine(string $input, string $expectedOutput): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['input' => $input, 'expectedOutput' => $expectedOutput]); + + $actualOutput = WordPressAutoInstrumentationTest::foldTextWithMarkersIntoOneLine($input); + self::assertSame($expectedOutput, $actualOutput); + } + + public static function dummyStaticMethodForTestGetCallbackSourceFilePath(): string + { + return __FUNCTION__; + } + + public function dummyMethodForTestGetCallbackSourceFilePath(): string + { + return __FUNCTION__; + } + + /** + * @param string $methodName + * @param mixed[] $args + */ + public function __call(string $methodName, array $args): void + { + } + + /** + * @param string $methodName + * @param mixed[] $args + */ + public static function __callStatic(string $methodName, array $args): void + { + } + + public function testGetCallbackSourceFilePath(): void + { + $testImpl = + /** + * @param mixed $callback + * @param ?string $expectedResult + */ + function ($callback, ?string $expectedResult): void { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['callback' => $callback, 'expectedResult' => $expectedResult]); + $actualResult = WordPressAutoInstrumentation::getCallbackSourceFilePath($callback, AmbientContextForTests::loggerFactory()); + $dbgCtx->add(['actualResult' => $actualResult]); + self::assertSame($expectedResult, $actualResult); + }; + + $testImpl('\dummyFuncForTestsWithoutNamespace', DUMMY_FUNC_FOR_TESTS_WITHOUT_NAMESPACE_CALLABLE_FILE_NAME); + $testImpl('dummyFuncForTestsWithoutNamespace', DUMMY_FUNC_FOR_TESTS_WITHOUT_NAMESPACE_CALLABLE_FILE_NAME); + $testImpl('\ElasticApmTests\dummyFuncForTestsWithNamespace', DUMMY_FUNC_FOR_TESTS_WITH_NAMESPACE_CALLABLE_FILE_NAME); + $testImpl('ElasticApmTests\dummyFuncForTestsWithNamespace', DUMMY_FUNC_FOR_TESTS_WITH_NAMESPACE_CALLABLE_FILE_NAME); + + $testImpl(__CLASS__ . '::' . self::dummyStaticMethodForTestGetCallbackSourceFilePath(), __FILE__); + $testImpl([__CLASS__, self::dummyStaticMethodForTestGetCallbackSourceFilePath()], __FILE__); + + $testImpl(__CLASS__ . '::' . self::dummyMethodForTestGetCallbackSourceFilePath(), __FILE__); + $testImpl([__CLASS__, self::dummyMethodForTestGetCallbackSourceFilePath()], __FILE__); + $testImpl([$this, self::dummyMethodForTestGetCallbackSourceFilePath()], __FILE__); + + $testImpl(__CLASS__ . '::' . 'implictNonExistentMethod', __FILE__); + $testImpl([__CLASS__, 'implictNonExistentMethod'], __FILE__); + $testImpl([$this, 'implictNonExistentMethod'], __FILE__); + + $dummyClosure = function () { + }; + $testImpl($dummyClosure, __FILE__); + + $testImpl('', null); + $testImpl(null, null); + $testImpl(new stdClass(), null); + $testImpl(1, null); + $testImpl([stdClass::class, 'implictNonExistentMethod'], null); + $testImpl('invalid name', null); + $testImpl([], null); + $testImpl([1], null); + $testImpl([1, 2], null); + $testImpl(['a', 'b'], null); + $testImpl(['a'], null); + $testImpl([stdClass::class], null); + } + + /** + * @return iterable + */ + public static function dataProviderForTestRemoveAttributes(): iterable + { + yield ['', '']; + yield ['some text', 'some text']; + + $input + = '#[MyDummyAttribute]' . PHP_EOL . + 'function get_template()' . PHP_EOL; + $expectedOutput + = 'function get_template()' . PHP_EOL; + yield [$input, $expectedOutput]; + yield [$expectedOutput, $expectedOutput]; + + $input + = '{ #[MyDummyAttribute]' . PHP_EOL . + 'function get_template()' . PHP_EOL; + $expectedOutput + = '{ function get_template()' . PHP_EOL; + yield [$input, $expectedOutput]; + yield [$expectedOutput, $expectedOutput]; + } + + /** + * @dataProvider dataProviderForTestRemoveAttributes + * + * @param string $input + * @param string $expectedOutput + */ + public static function testRemoveAttributes(string $input, string $expectedOutput): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['input' => $input, 'expectedOutput' => $expectedOutput]); + + $actualOutput = WordPressAutoInstrumentationTest::removeAttributes($input); + self::assertSame($expectedOutput, $actualOutput); + } +} diff --git a/tests/ElasticApmTests/Util/ArrayUtilTest.php b/tests/ElasticApmTests/Util/ArrayUtilTest.php index 1d8f92026..3b0a7a98d 100644 --- a/tests/ElasticApmTests/Util/ArrayUtilTest.php +++ b/tests/ElasticApmTests/Util/ArrayUtilTest.php @@ -74,4 +74,60 @@ public static function testIterateListInReverse(array $inputArray, array $expect self::assertSame($expectedVal, $actualVal); } } + + /** + * @param mixed[] $args + * + * @return void + */ + private static function verifyArgs(array $args): void + { + self::assertCount(1, $args); + $arg0 = $args[0]; + self::assertIsString($arg0); + } + + /** + * @param mixed[] $args + * + * @return void + */ + private static function instrumentationFunc(array $args): void + { + self::assertCount(1, $args); + self::verifyArgs($args); + $someParam =& $args[0]; + self::assertSame('value set by instrumentedFunc caller', $someParam); + self::assertIsString($someParam); + $someParam = 'value set by instrumentationFunc'; + } + + /** @noinspection PhpSameParameterValueInspection */ + private static function instrumentedFunc(string $someParam): string + { + self::instrumentationFunc([&$someParam]); + return $someParam; + } + + public static function testReferencesInArray(): void + { + $instrumentedFuncRetVal = self::instrumentedFunc('value set by instrumentedFunc caller'); + self::assertSame('value set by instrumentationFunc', $instrumentedFuncRetVal); + } + + public static function testRemoveElementFromTwoLevelArrayViaReferenceToFirstLevel(): void + { + $myArr = [ + 'level 1 - a' => [ + 'level 2 - a' => 'value for level 2 - a', + 'level 2 - b' => 'value for level 2 - b', + ] + ]; + $level1ValRef =& $myArr['level 1 - a']; + self::assertArrayHasKey('level 2 - a', $level1ValRef); + self::assertSame('value for level 2 - a', $level1ValRef['level 2 - a']); + unset($level1ValRef['level 2 - a']); + self::assertArrayNotHasKey('level 2 - a', $myArr['level 1 - a']); + self::assertArrayHasKey('level 2 - b', $myArr['level 1 - a']); + } } diff --git a/tests/ElasticApmTests/Util/FileUtilForTests.php b/tests/ElasticApmTests/Util/FileUtilForTests.php index 34be22ced..1a5eb6ac3 100644 --- a/tests/ElasticApmTests/Util/FileUtilForTests.php +++ b/tests/ElasticApmTests/Util/FileUtilForTests.php @@ -25,11 +25,14 @@ use Closure; use Elastic\Apm\Impl\Log\LoggableToString; +use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\ExceptionUtil; use Elastic\Apm\Impl\Util\StaticClassTrait; use Elastic\Apm\Impl\Util\TextUtil; use ElasticApmTests\ComponentTests\Util\AmbientContextForTests; -use PHPUnit\Framework\Assert; +use ElasticApmTests\ComponentTests\Util\EnvVarUtilForTests; +use ElasticApmTests\ComponentTests\Util\OsUtilForTests; +use ElasticApmTests\ComponentTests\Util\ProcessUtilForTests; use RuntimeException; /** @@ -101,8 +104,7 @@ public static function createTempFile(?string $dbgTempFilePurpose = null): strin { $tempFileFullPath = tempnam(sys_get_temp_dir(), /* prefix */ 'ElasticApmTests_'); $logCategory = LogCategoryForTests::TEST; - $logger - = AmbientContextForTests::loggerFactory()->loggerForClass($logCategory, __NAMESPACE__, __CLASS__, __FILE__); + $logger = AmbientContextForTests::loggerFactory()->loggerForClass($logCategory, __NAMESPACE__, __CLASS__, __FILE__); if ($tempFileFullPath === false) { ($loggerProxy = $logger->ifCriticalLevelEnabled(__LINE__, __FUNCTION__)) @@ -110,7 +112,7 @@ public static function createTempFile(?string $dbgTempFilePurpose = null): strin 'Failed to create a temporary file', ['$dbgTempFilePurpose' => $dbgTempFilePurpose] ); - Assert::fail(LoggableToString::convert(['$dbgTempFilePurpose' => $dbgTempFilePurpose])); + TestCaseBase::fail(LoggableToString::convert(['$dbgTempFilePurpose' => $dbgTempFilePurpose])); } ($loggerProxy = $logger->ifTraceLevelEnabled(__LINE__, __FUNCTION__)) @@ -121,4 +123,63 @@ public static function createTempFile(?string $dbgTempFilePurpose = null): strin return $tempFileFullPath; } + + private static function buildTempSubDirFullPath(string $subDirName): string + { + return sys_get_temp_dir() . DIRECTORY_SEPARATOR . $subDirName; + } + + public static function deleteTempSubDir(string $subDirName): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['subDirName' => $subDirName]); + + $tempSubDirFullPath = self::buildTempSubDirFullPath($subDirName); + $dbgCtx->add(['tempSubDirFullPath' => $tempSubDirFullPath]); + if (!file_exists($tempSubDirFullPath)) { + return; + } + TestCaseBase::assertTrue(is_dir($tempSubDirFullPath)); + + $deleteDirShellCmd = OsUtilForTests::isWindows() + ? sprintf('rd /s /q "%s"', $tempSubDirFullPath) + : sprintf('rm -rf "%s"', $tempSubDirFullPath); + + ProcessUtilForTests::startProcessAndWaitUntilExit($deleteDirShellCmd, EnvVarUtilForTests::getAll(), /* shouldCaptureStdOutErr */ true, /* $expectedExitCode */ 0); + } + + /** + * @param class-string $testClassName + * @param string $testMethodName + * + * @return string + */ + public static function buildTempSubDirName(string $testClassName, string $testMethodName): string + { + return 'ElasticApmTests_' . ClassNameUtil::fqToShort($testClassName) . '_' . $testMethodName . '_PID=' . getmypid(); + } + + public static function createTempSubDir(string $subDirName): string + { + AssertMessageStack::newScope(/* out */ $dbgCtx); + $dbgCtx->add(['subDirName' => $subDirName]); + + $tempSubDirFullPath = self::buildTempSubDirFullPath($subDirName); + $dbgCtx->add(['tempSubDirFullPath' => $tempSubDirFullPath]); + self::deleteTempSubDir($tempSubDirFullPath); + TestCaseBase::assertTrue(mkdir($tempSubDirFullPath)); + return $tempSubDirFullPath; + } + + public static function convertPathRelativeTo(string $absPath, string $relativeToAbsPath): string + { + TestCaseBase::assertTrue(TextUtil::isPrefixOf($relativeToAbsPath, $absPath)); + $relPath = substr($absPath, /* offset */ strlen($relativeToAbsPath)); + foreach (['/', DIRECTORY_SEPARATOR] as $dirSeparator) { + while (TextUtil::isPrefixOf($dirSeparator, $relPath)) { + $relPath = substr($relPath, /* offset */ strlen($dirSeparator)); + } + } + return $relPath; + } } diff --git a/tests/ElasticApmTests/Util/TestCaseBase.php b/tests/ElasticApmTests/Util/TestCaseBase.php index 274fbfc0c..26a8f15ed 100644 --- a/tests/ElasticApmTests/Util/TestCaseBase.php +++ b/tests/ElasticApmTests/Util/TestCaseBase.php @@ -1050,4 +1050,22 @@ public static function assertContainsEx($needle, iterable $haystack, string $mes throw $ex; } } + + public static function assertDirectoryDoesNotExist(string $directory, string $message = ''): void + { + /** + * Method assertDirectoryDoesNotExist was added in PHPUnit 9 as an alias for already existing assertDirectoryNotExists + * and assertDirectoryNotExists was deprecated. + * We still use PHPUnit 8.5 when testing under older PHP versions so we need a facade to work on both + * - PHPUnit 8.5, where assertDirectoryDoesNotExist does not exist + * and + * - PHPUnit 9, where assertDirectoryNotExists is deprecated + */ + if (method_exists(Assert::class, __FUNCTION__)) { + Assert::assertDirectoryDoesNotExist($directory, $message); + } else { + /** @noinspection PhpDeprecationInspection, PhpUnitDeprecatedCallsIn10VersionInspection */ + Assert::assertDirectoryNotExists($directory, $message); + } + } }