diff --git a/elastic-otel-php.properties b/elastic-otel-php.properties index ef1ad5b..65d92dc 100644 --- a/elastic-otel-php.properties +++ b/elastic-otel-php.properties @@ -1,4 +1,4 @@ version=0.3.0 supported_php_versions=(81 82 83 84) php_headers_version=2.0 -logger_features_enum_values=ALL=0,MODULE=1,REQUEST=2,TRANSPORT=3,BOOTSTRAP=4,HOOKS=5,INSTRUMENTATION=6,OTEL=7 +logger_features_enum_values=ALL=0,MODULE=1,REQUEST=2,TRANSPORT=3,BOOTSTRAP=4,HOOKS=5,INSTRUMENTATION=6,OTEL=7,DEPGUARD=8 diff --git a/prod/native/extension/code/Hooking.cpp b/prod/native/extension/code/Hooking.cpp index da450ba..c18771f 100644 --- a/prod/native/extension/code/Hooking.cpp +++ b/prod/native/extension/code/Hooking.cpp @@ -111,7 +111,26 @@ static void elastic_execute_internal(INTERNAL_FUNCTION_PARAMETERS) { ELASTICAPM_G(globals)->inferredSpans_->attachBacktraceIfInterrupted(); } -void Hooking::replaceHooks(bool enableInferredSpansHooks) { +static zend_op_array *elastic_compile_file(zend_file_handle *file_handle, int type) { + std::string_view file = file_handle->opened_path ? std::string_view(ZSTR_VAL(file_handle->opened_path), ZSTR_LEN(file_handle->opened_path)) : std::string_view(ZSTR_VAL(file_handle->filename), ZSTR_LEN(file_handle->filename)); + + if (ELASTICAPM_G(globals)->dependencyAutoLoaderGuard_->shouldDiscardFileCompilation(file)) { + return nullptr; + } + + zend_try { + if (Hooking::getInstance().getOriginalZendCompileFile()) { + return Hooking::getInstance().getOriginalZendCompileFile()(file_handle, type); + } + } + zend_catch { + ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "%s: original call error", __FUNCTION__); + } + zend_end_try(); + return nullptr; +} + +void Hooking::replaceHooks(bool enableInferredSpansHooks, bool enableDepenecyAutoloaderGuard) { zend_observer_error_register(elastic_observer_error_cb); if (enableInferredSpansHooks) { @@ -119,6 +138,11 @@ void Hooking::replaceHooks(bool enableInferredSpansHooks) { zend_execute_internal = elastic_execute_internal; zend_interrupt_function = elastic_interrupt_function; } + + if (enableDepenecyAutoloaderGuard) { + ELOGF_DEBUG(ELASTICAPM_G(globals)->logger_, HOOKS, "Hooked into zend_compile_file, original ptr: %p, new ptr: %p", zend_compile_file, elastic_compile_file); + zend_compile_file = elastic_compile_file; + } } } // namespace elasticapm::php diff --git a/prod/native/extension/code/Hooking.h b/prod/native/extension/code/Hooking.h index 62b17df..6030fcd 100644 --- a/prod/native/extension/code/Hooking.h +++ b/prod/native/extension/code/Hooking.h @@ -28,6 +28,7 @@ class Hooking { public: using zend_execute_internal_t = void (*)(zend_execute_data *execute_data, zval *return_value); using zend_interrupt_function_t = void (*)(zend_execute_data *execute_data); + using zend_compile_file_t = zend_op_array *(*)(zend_file_handle *file_handle, int type); static Hooking &getInstance() { static Hooking instance; @@ -37,11 +38,13 @@ class Hooking { void fetchOriginalHooks() { original_execute_internal_ = zend_execute_internal; original_zend_interrupt_function_ = zend_interrupt_function; + original_zend_compile_file_ = zend_compile_file; } void restoreOriginalHooks() { zend_execute_internal = original_execute_internal_; zend_interrupt_function = original_zend_interrupt_function_; + zend_compile_file = original_zend_compile_file_; } zend_execute_internal_t getOriginalExecuteInternal() { @@ -52,7 +55,11 @@ class Hooking { return original_zend_interrupt_function_; } - void replaceHooks(bool enableInferredSpansHooks); + zend_compile_file_t getOriginalZendCompileFile() { + return original_zend_compile_file_; + } + + void replaceHooks(bool enableInferredSpansHooks, bool enableDepenecyAutoloaderGuard); private: Hooking(Hooking const &) = delete; @@ -61,6 +68,7 @@ class Hooking { zend_execute_internal_t original_execute_internal_ = nullptr; zend_interrupt_function_t original_zend_interrupt_function_ = nullptr; + zend_compile_file_t original_zend_compile_file_ = nullptr; }; } // namespace elasticapm::php \ No newline at end of file diff --git a/prod/native/extension/code/ModuleInit.cpp b/prod/native/extension/code/ModuleInit.cpp index 15778ad..75da437 100644 --- a/prod/native/extension/code/ModuleInit.cpp +++ b/prod/native/extension/code/ModuleInit.cpp @@ -85,7 +85,7 @@ void elasticApmModuleInit(int moduleType, int moduleNumber) { ELOGF_DEBUG(globals->logger_, MODULE, "MINIT Replacing hooks"); elasticapm::php::Hooking::getInstance().fetchOriginalHooks(); - elasticapm::php::Hooking::getInstance().replaceHooks(globals->config_->get().inferred_spans_enabled); + elasticapm::php::Hooking::getInstance().replaceHooks(globals->config_->get().inferred_spans_enabled, globals->config_->get().dependency_autoloader_guard_enabled); zend_observer_activate(); zend_observer_fcall_register(elasticapm::php::elasticRegisterObserver); diff --git a/prod/native/extension/phpt/tests/includes/bootstrap_mock.inc b/prod/native/extension/phpt/tests/includes/bootstrap_mock.inc index 1445baf..34ed017 100644 --- a/prod/native/extension/phpt/tests/includes/bootstrap_mock.inc +++ b/prod/native/extension/phpt/tests/includes/bootstrap_mock.inc @@ -15,4 +15,14 @@ final class PhpPartFacade public static function handleError(): void { } + + public static function inferredSpans(int $durationMs, bool $internalFunction): bool { + return true; + } + + public static function debugPreHook(mixed $object, array $params, ?string $class, string $function, ?string $filename, ?int $lineno): void { + } + + public static function debugPostHook(mixed $object, array $params, mixed $retval, ?Throwable $exception): void { + } } diff --git a/prod/native/libcommon/code/AgentGlobals.cpp b/prod/native/libcommon/code/AgentGlobals.cpp index db32361..3dba0ee 100644 --- a/prod/native/libcommon/code/AgentGlobals.cpp +++ b/prod/native/libcommon/code/AgentGlobals.cpp @@ -31,6 +31,7 @@ #include "InstrumentedFunctionHooksStorage.h" #include "CommonUtils.h" #include "transport/HttpTransportAsync.h" +#include "DependencyAutoLoaderGuard.h" #include "LogFeature.h" #include @@ -49,13 +50,14 @@ AgentGlobals::AgentGlobals(std::shared_ptr logger, config_(std::make_shared(std::move(updateConfigurationSnapshot))), logger_(std::move(logger)), bridge_(std::move(bridge)), + dependencyAutoLoaderGuard_(std::make_shared(bridge_, logger_)), hooksStorage_(std::move(hooksStorage)), sapi_(std::make_shared(bridge_->getPhpSapiName())), inferredSpans_(std::move(inferredSpans)), periodicTaskExecutor_(), httpTransportAsync_(std::make_unique>(logger_, config_)), sharedMemory_(std::make_shared()), - requestScope_(std::make_shared(logger_, bridge_, sapi_, sharedMemory_, inferredSpans_, config_, [hs = hooksStorage_]() { hs->clear(); }, [this]() { return getPeriodicTaskExecutor();})), + requestScope_(std::make_shared(logger_, bridge_, sapi_, sharedMemory_, dependencyAutoLoaderGuard_, inferredSpans_, config_, [hs = hooksStorage_]() { hs->clear(); }, [this]() { return getPeriodicTaskExecutor();})), logSinkStdErr_(std::move(logSinkStdErr)), logSinkSysLog_(std::move(logSinkSysLog)), logSinkFile_(std::move(logSinkFile)) diff --git a/prod/native/libcommon/code/AgentGlobals.h b/prod/native/libcommon/code/AgentGlobals.h index 4eca478..b626c8f 100644 --- a/prod/native/libcommon/code/AgentGlobals.h +++ b/prod/native/libcommon/code/AgentGlobals.h @@ -37,12 +37,13 @@ class ConfigurationSnapshot; class LoggerSinkInterface; class LogSinkFile; class InstrumentedFunctionHooksStorageInterface; +class DependencyAutoLoaderGuard; namespace transport { class CurlSender; class HttpEndpoints; template class HttpTransportAsync; -} +} // namespace transport // clang-format off @@ -64,6 +65,7 @@ class AgentGlobals { std::shared_ptr config_; std::shared_ptr logger_; std::shared_ptr bridge_; + std::shared_ptr dependencyAutoLoaderGuard_; std::shared_ptr hooksStorage_; std::shared_ptr sapi_; std::shared_ptr inferredSpans_; diff --git a/prod/native/libcommon/code/ConfigurationManager.h b/prod/native/libcommon/code/ConfigurationManager.h index 0580826..328e7ad 100644 --- a/prod/native/libcommon/code/ConfigurationManager.h +++ b/prod/native/libcommon/code/ConfigurationManager.h @@ -118,7 +118,9 @@ class ConfigurationManager { BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_REDUCTION_ENABLED, OptionMetadata::type::boolean, false), BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_STACKTRACE_ENABLED, OptionMetadata::type::boolean, false), BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_SAMPLING_INTERVAL, OptionMetadata::type::duration, false), - BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_MIN_DURATION, OptionMetadata::type::duration, false)}; + BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_MIN_DURATION, OptionMetadata::type::duration, false), + BUILD_METADATA(ELASTIC_OTEL_CFG_OPT_NAME_DEPENDENCY_AUTOLOADER_GUARD_ENABLED, OptionMetadata::type::boolean, false) + }; // clang-format on }; diff --git a/prod/native/libcommon/code/ConfigurationSnapshot.h b/prod/native/libcommon/code/ConfigurationSnapshot.h index e4e203a..e4913f3 100644 --- a/prod/native/libcommon/code/ConfigurationSnapshot.h +++ b/prod/native/libcommon/code/ConfigurationSnapshot.h @@ -47,6 +47,8 @@ #define ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_SAMPLING_INTERVAL inferred_spans_sampling_interval #define ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_MIN_DURATION inferred_spans_min_duration +#define ELASTIC_OTEL_CFG_OPT_NAME_DEPENDENCY_AUTOLOADER_GUARD_ENABLED dependency_autoloader_guard_enabled + namespace elasticapm::php { using namespace std::string_literals; @@ -74,6 +76,8 @@ struct ConfigurationSnapshot { std::chrono::milliseconds ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_SAMPLING_INTERVAL = std::chrono::milliseconds(50); std::chrono::milliseconds ELASTIC_OTEL_CFG_OPT_NAME_INFERRED_SPANS_MIN_DURATION = std::chrono::milliseconds(0); + bool ELASTIC_OTEL_CFG_OPT_NAME_DEPENDENCY_AUTOLOADER_GUARD_ENABLED = true; + uint64_t revision = 0; }; diff --git a/prod/native/libcommon/code/DependencyAutoLoaderGuard.cpp b/prod/native/libcommon/code/DependencyAutoLoaderGuard.cpp new file mode 100644 index 0000000..fc3740b --- /dev/null +++ b/prod/native/libcommon/code/DependencyAutoLoaderGuard.cpp @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or 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 "DependencyAutoLoaderGuard.h" +#include "LoggerInterface.h" +#include "PhpBridgeInterface.h" + +#include +#include +#include +#include + +namespace elasticapm::php { +using namespace std::string_view_literals; + +void DependencyAutoLoaderGuard::setBootstrapPath(std::string_view bootstrapFilePath) { + auto [major, minor] = bridge_->getPhpVersionMajorMinor(); + auto path = std::filesystem::path(bootstrapFilePath).parent_path(); + path /= std::format("vendor_{}{}", major, minor); + vendorPath_ = path.c_str(); + ELOGF_DEBUG(logger_, DEPGUARD, "vendor path set to: " PRsv, PRsvArg(vendorPath_)); +} + +bool DependencyAutoLoaderGuard::shouldDiscardFileCompilation(std::string_view fileName) { + try { + std::string compiledFilePath = std::filesystem::exists(fileName) ? std::filesystem::canonical(fileName) : fileName; + + if (compiledFilePath.starts_with(vendorPath_)) { + return false; + } + + auto [lastClass, lastFunction] = bridge_->getNewlyCompiledFiles( + [this](std::string_view name) { + // storing only dependencies delivered by EDOT + if (name.starts_with(vendorPath_)) { + if (name.substr(vendorPath_.length()).starts_with("/composer/")) { // skip compsoer files - they must be compiled + ELOGF_TRACE(logger_, DEPGUARD, "Skipping storage of composer files: " PRsv, PRsvArg(name)); + return; + } + compiledFiles_.insert(name); + ELOGF_TRACE(logger_, DEPGUARD, "Storing file: " PRsv, PRsvArg(name)); + } + }, + lastClass_, lastFunction_); + + lastClass_ = lastClass; + lastFunction_ = lastFunction; + + if (wasDeliveredByEDOT(compiledFilePath)) { + ELOGF_DEBUG(logger_, DEPGUARD, "Compilation of file '" PRsv "' will be discarded", PRsvArg(compiledFilePath)); + return true; + } + + } catch (std::exception const &e) { + ELOGF_WARNING(logger_, DEPGUARD, "shouldDiscardFileCompilation of file '" PRsv "' throwed: %s", PRsvArg(fileName), e.what()); + return false; + } + + return false; +} + +bool DependencyAutoLoaderGuard::wasDeliveredByEDOT(std::string_view fileName) const { + constexpr std::string_view vendor = "/vendor/"sv; + + auto vendorPos = fileName.find(vendor); + if (vendorPos == std::string_view::npos) { + return false; + } + + auto afterVendor = fileName.substr(vendorPos + vendor.size()); + + auto found = std::find_if(std::begin(compiledFiles_), std::end(compiledFiles_), [afterVendor, bootstrapLen = vendorPath_.length()](std::string_view storedFile) -> bool { + std::string_view fileView = storedFile.substr(bootstrapLen + 1); // add 1 for slash + if (fileView == afterVendor) { + return true; + } + return false; + }); + + if (found != std::end(compiledFiles_)) { + return true; + } + return false; +} + +} // namespace elasticapm::php \ No newline at end of file diff --git a/prod/native/libcommon/code/DependencyAutoLoaderGuard.h b/prod/native/libcommon/code/DependencyAutoLoaderGuard.h new file mode 100644 index 0000000..4da8a7f --- /dev/null +++ b/prod/native/libcommon/code/DependencyAutoLoaderGuard.h @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or 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 + +namespace elasticapm::php { + +class LoggerInterface; +class PhpBridgeInterface; + +class DependencyAutoLoaderGuard { +public: + DependencyAutoLoaderGuard(std::shared_ptr bridge, std::shared_ptr logger) : bridge_(std::move(bridge)), logger_(std::move(logger)) { + } + + void setBootstrapPath(std::string_view bootstrapFilePath); + + void onRequestInit() { + clear(); + } + + void onRequestShutdown() { + clear(); + } + + bool shouldDiscardFileCompilation(std::string_view fileName); + +private: + bool wasDeliveredByEDOT(std::string_view fileName) const; + + void clear() { + lastClass_ = 0; + lastFunction_ = 0; + compiledFiles_.clear(); + } + +private: + std::shared_ptr bridge_; + std::shared_ptr logger_; + std::set compiledFiles_; // string_view is safe because we're removing data on request end, they're request scope safe + + std::size_t lastClass_ = 0; + std::size_t lastFunction_ = 0; + + std::string vendorPath_; +}; +} // namespace elasticapm::php \ No newline at end of file diff --git a/prod/native/libcommon/code/PhpBridgeInterface.h b/prod/native/libcommon/code/PhpBridgeInterface.h index 89f626c..757a810 100644 --- a/prod/native/libcommon/code/PhpBridgeInterface.h +++ b/prod/native/libcommon/code/PhpBridgeInterface.h @@ -21,6 +21,7 @@ #include "LogLevel.h" #include +#include #include #include #include @@ -59,6 +60,11 @@ class PhpBridgeInterface { virtual bool isScriptRestricedByOpcacheAPI() const = 0; virtual bool detectOpcacheRestartPending() const = 0; virtual bool isOpcacheEnabled() const = 0; + + virtual void getCompiledFiles(std::function recordFile) const = 0; + virtual std::pair getNewlyCompiledFiles(std::function recordFile, std::size_t lastClassIndex, std::size_t lastFunctionIndex) const = 0; + + virtual std::pair getPhpVersionMajorMinor() const = 0; }; } diff --git a/prod/native/libcommon/code/RequestScope.h b/prod/native/libcommon/code/RequestScope.h index 2bd7d1f..31c36ab 100644 --- a/prod/native/libcommon/code/RequestScope.h +++ b/prod/native/libcommon/code/RequestScope.h @@ -21,6 +21,7 @@ #include "ConfigurationStorage.h" #include "CommonUtils.h" +#include "DependencyAutoLoaderGuard.h" #include "Diagnostics.h" #include "InferredSpans.h" #include "LoggerInterface.h" @@ -39,7 +40,7 @@ class RequestScope { using clearHooks_t = std::function; using getPeriodicTaskExecutor_t = std::function()>; - RequestScope(std::shared_ptr log, std::shared_ptr bridge, std::shared_ptr sapi, std::shared_ptr sharedMemory, std::shared_ptr inferredSpans, std::shared_ptr config, clearHooks_t clearHooks, getPeriodicTaskExecutor_t getPeriodicTaskExecutor) : log_(log), bridge_(std::move(bridge)), sapi_(std::move(sapi)), sharedMemory_(sharedMemory), inferredSpans_(std::move(inferredSpans)), config_(config), clearHooks_(std::move(clearHooks)), getPeriodicTaskExecutor_(std::move(getPeriodicTaskExecutor)) { + RequestScope(std::shared_ptr log, std::shared_ptr bridge, std::shared_ptr sapi, std::shared_ptr sharedMemory, std::shared_ptr dependencyGuard, std::shared_ptr inferredSpans, std::shared_ptr config, clearHooks_t clearHooks, getPeriodicTaskExecutor_t getPeriodicTaskExecutor) : log_(log), bridge_(std::move(bridge)), sapi_(std::move(sapi)), sharedMemory_(sharedMemory), dependencyGuard_(dependencyGuard), inferredSpans_(std::move(inferredSpans)), config_(config), clearHooks_(std::move(clearHooks)), getPeriodicTaskExecutor_(std::move(getPeriodicTaskExecutor)) { } void onRequestInit() { @@ -61,12 +62,18 @@ class RequestScope { requestCounter_++; + if (requestCounter_ == 1) { + dependencyGuard_->setBootstrapPath((*config_)->bootstrap_php_part_file); + } + auto requestStartTime = std::chrono::system_clock::now(); bridge_->enableAccessToServerGlobal(); preloadDetected_ = requestCounter_ == 1 ? bridge_->detectOpcachePreload() : false; + dependencyGuard_->onRequestInit(); + if (requestCounter_ == 1 && preloadDetected_) { ELOGF_DEBUG(log_, REQUEST, "opcache.preload request detected on init"); return; @@ -134,6 +141,7 @@ class RequestScope { void onRequestPostDeactivate() { ELOGF_DEBUG(log_, REQUEST, "%s", __FUNCTION__); + dependencyGuard_->onRequestShutdown(); resetRequest(); if (!bootstrapSuccessfull_) { @@ -177,6 +185,7 @@ class RequestScope { std::shared_ptr bridge_; std::shared_ptr sapi_; std::shared_ptr sharedMemory_; + std::shared_ptr dependencyGuard_; std::shared_ptr inferredSpans_; std::shared_ptr config_; clearHooks_t clearHooks_; diff --git a/prod/native/libcommon/test/DependencyAutoLoaderGuardTest.cpp b/prod/native/libcommon/test/DependencyAutoLoaderGuardTest.cpp new file mode 100644 index 0000000..f487672 --- /dev/null +++ b/prod/native/libcommon/test/DependencyAutoLoaderGuardTest.cpp @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or 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 "DependencyAutoLoaderGuard.h" +#include "PhpBridgeMock.h" +#include "Logger.h" + +#include +#include +#include + +using namespace std::literals; + +namespace elasticapm::php::test { + +using namespace std::string_view_literals; + +class DependencyAutoLoaderGuardTest : public ::testing::Test { +public: + DependencyAutoLoaderGuardTest() { + if (std::getenv("ELASTIC_OTEL_DEBUG_LOG_TESTS")) { + auto serr = std::make_shared(); + serr->setLevel(logLevel_trace); + reinterpret_cast(log_.get())->attachSink(serr); + } + } + std::shared_ptr log_ = std::make_shared(std::vector>()); + std::shared_ptr bridge_{std::make_shared<::testing::StrictMock>()}; + DependencyAutoLoaderGuard guard_{bridge_, log_}; +}; + +TEST_F(DependencyAutoLoaderGuardTest, discardAppFileBecauseItWasDeliveredByEDOT) { + EXPECT_CALL(*bridge_, getPhpVersionMajorMinor()).Times(::testing::Exactly(1)).WillOnce(::testing::Return(std::pair(8, 4))); + + guard_.setBootstrapPath("/elatic/prod/php/bootstrap.php"); + + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/elatic/prod/php/vendor_84/first-package/test.php")); // file from elastic scope - no action + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/elatic/prod/php/vendor_84/second-package/test.php")); // file from elastic scope - no action + + // clang-format off + EXPECT_CALL(*bridge_, getNewlyCompiledFiles(::testing::_, 0, 0)).Times(::testing::Exactly(1)).WillOnce( + ::testing::DoAll( + ::testing::InvokeArgument<0>("/elatic/prod/php/vendor_84/first-package/test.php"sv), // we have that file in cache + ::testing::InvokeArgument<0>("/elatic/prod/php/vendor_84/second-package/test.php"sv), // we have that file in cache + ::testing::Return(std::pair(2, 1)))); // returns index in class/file hashmaps + // clang-format on + + ASSERT_TRUE(guard_.shouldDiscardFileCompilation("/app/vendor/first-package/test.php")); // file from app scope - test it - should discard - file is EDOT delivered +} + +TEST_F(DependencyAutoLoaderGuardTest, discardSecondAppFileBecauseItWasDeliveredByEDOT) { + EXPECT_CALL(*bridge_, getPhpVersionMajorMinor()).Times(::testing::Exactly(1)).WillOnce(::testing::Return(std::pair(8, 4))); + + guard_.setBootstrapPath("/elatic/prod/php/bootstrap.php"); + + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/elatic/prod/php/vendor_84/first-package/test.php")); // file from elastic scope - no action + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/elatic/prod/php/vendor_84/second-package/test.php")); // file from elastic scope - no action + + // clang-format off + EXPECT_CALL(*bridge_, getNewlyCompiledFiles(::testing::_, 0, 0)).Times(::testing::Exactly(1)).WillOnce( + ::testing::DoAll( + ::testing::InvokeArgument<0>("/elatic/prod/php/vendor_84/first-package/test.php"sv), // we have that file in cache + ::testing::InvokeArgument<0>("/elatic/prod/php/vendor_84/second-package/test.php"sv), // we have that file in cache + ::testing::Return(std::pair(2, 1)))); // returns index in class/file hashmaps + // clang-format on + + ASSERT_TRUE(guard_.shouldDiscardFileCompilation("/app/vendor/second-package/test.php")); // file from app scope - test it - should discard - file is EDOT delivered +} + +TEST_F(DependencyAutoLoaderGuardTest, getCompiledFilesListProgressively) { + EXPECT_CALL(*bridge_, getPhpVersionMajorMinor()).Times(::testing::Exactly(1)).WillOnce(::testing::Return(std::pair(8, 4))); + + guard_.setBootstrapPath("/elatic/prod/php/bootstrap.php"); + + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/elatic/prod/php/vendor_84/first-package/test.php")); // file from elastic scope - no action + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/elatic/prod/php/vendor_84/second-package/test.php")); // file from elastic scope - no action + + ::testing::InSequence s; + + // clang-format off + EXPECT_CALL(*bridge_, getNewlyCompiledFiles(::testing::_, 0, 0)).Times(::testing::Exactly(1)).WillOnce( + ::testing::DoAll( + ::testing::InvokeArgument<0>("/elatic/prod/php/vendor_84/first-package/test.php"sv), // we have that file in cache + ::testing::Return(std::pair(10, 20)))); // returns index in class/file hashmaps + // clang-format on + + ASSERT_TRUE(guard_.shouldDiscardFileCompilation("/app/vendor/first-package/test.php")); // file from app scope - test it - should discard - file is EDOT delivered + + // clang-format off + EXPECT_CALL(*bridge_, getNewlyCompiledFiles(::testing::_, 10, 20)).Times(::testing::Exactly(1)).WillOnce( + ::testing::DoAll( + ::testing::InvokeArgument<0>("/elatic/prod/php/vendor_84/second-package/test.php"sv), // we have that file in cache + ::testing::Return(std::pair(11, 21)))); // returns index in class/file hashmaps + // clang-format on + + ASSERT_TRUE(guard_.shouldDiscardFileCompilation("/app/vendor/second-package/test.php")); // file from app scope - test it - should discard - file is EDOT delivered + + // clang-format off + EXPECT_CALL(*bridge_, getNewlyCompiledFiles(::testing::_, 11, 21)).Times(::testing::Exactly(1)).WillOnce( + ::testing::DoAll( + ::testing::Return(std::pair(11, 21)))); // returns index in class/file hashmaps + // clang-format on + + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/app/vendor/third-package/test.php")); // file from app scope - test it - should NOT discard - file is NOT EDOT delivered +} + +TEST_F(DependencyAutoLoaderGuardTest, fileNotInVendorFolder) { + EXPECT_CALL(*bridge_, getPhpVersionMajorMinor()).Times(::testing::Exactly(1)).WillOnce(::testing::Return(std::pair(8, 4))); + guard_.setBootstrapPath("/elatic/prod/php/bootstrap.php"); + + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/elatic/prod/php/vendor_84/first-package/test.php")); // file from elastic scope - no action + + ::testing::InSequence s; + + // clang-format off + EXPECT_CALL(*bridge_, getNewlyCompiledFiles(::testing::_, 0, 0)).Times(::testing::Exactly(1)).WillOnce( + ::testing::DoAll( + ::testing::InvokeArgument<0>("/elatic/prod/php/vendor_84/first-package/test.php"sv), // we have that file in cache + ::testing::Return(std::pair(10, 20)))); // returns index in class/file hashmaps + // clang-format on + + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/app/first-package/test.php")); // file from app scope - test it - should discard - file is EDOT delivered +} + +TEST_F(DependencyAutoLoaderGuardTest, wrongVendorFolder_shouldntHappen) { + EXPECT_CALL(*bridge_, getPhpVersionMajorMinor()).Times(::testing::Exactly(1)).WillOnce(::testing::Return(std::pair(8, 4))); + + guard_.setBootstrapPath("/elatic/prod/php/bootstrap.php"); + + ::testing::InSequence s; + + // clang-format off + EXPECT_CALL(*bridge_, getNewlyCompiledFiles(::testing::_, 0, 0)).Times(::testing::Exactly(1)).WillOnce( + ::testing::DoAll( + ::testing::InvokeArgument<0>("/elatic/prod/php/vendor_80/first-package/test.php"sv), // we have that file in cache + ::testing::Return(std::pair(2, 1)))); // returns index in class/file hashmaps + // clang-format on + + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/elatic/prod/php/vendor_80/first-package/test.php")); // file NOT from elastic scope - wrong vendor folder + + // clang-format off + EXPECT_CALL(*bridge_, getNewlyCompiledFiles(::testing::_, 2, 1)).Times(::testing::Exactly(1)).WillOnce( + ::testing::DoAll( + ::testing::Return(std::pair(2, 1)))); // returns index in class/file hashmaps + // clang-format on + + ASSERT_FALSE(guard_.shouldDiscardFileCompilation("/app/vendor/first-package/test.php")); +} + +} // namespace elasticapm::php::test diff --git a/prod/native/libcommon/test/PhpBridgeMock.h b/prod/native/libcommon/test/PhpBridgeMock.h new file mode 100644 index 0000000..7892c57 --- /dev/null +++ b/prod/native/libcommon/test/PhpBridgeMock.h @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or 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 "PhpBridgeInterface.h" +#include + +namespace elasticapm::php::test { + +class PhpBridgeMock : public PhpBridgeInterface { +public: + MOCK_METHOD(bool, callInferredSpans, (std::chrono::milliseconds duration), (const, override)); + MOCK_METHOD(bool, callPHPSideEntryPoint, (LogLevel logLevel, std::chrono::time_point requestInitStart), (const, override)); + MOCK_METHOD(bool, callPHPSideExitPoint, (), (const, override)); + MOCK_METHOD(bool, callPHPSideErrorHandler, (int type, std::string_view errorFilename, uint32_t errorLineno, std::string_view message), (const, override)); + + MOCK_METHOD(std::vector, getExtensionList, (), (const, override)); + MOCK_METHOD(std::string, getPhpInfo, (), (const, override)); + + MOCK_METHOD(std::string_view, getPhpSapiName, (), (const, override)); + + MOCK_METHOD(std::optional, getCurrentExceptionMessage, (), (const, override)); + + MOCK_METHOD(void, compileAndExecuteFile, (std::string_view fileName), (const, override)); + + MOCK_METHOD(void, enableAccessToServerGlobal, (), (const, override)); + + MOCK_METHOD(bool, detectOpcachePreload, (), (const, override)); + MOCK_METHOD(bool, isScriptRestricedByOpcacheAPI, (), (const, override)); + MOCK_METHOD(bool, detectOpcacheRestartPending, (), (const, override)); + MOCK_METHOD(bool, isOpcacheEnabled, (), (const, override)); + + MOCK_METHOD(void, getCompiledFiles, (std::function recordFile), (const, override)); + MOCK_METHOD((std::pair), getNewlyCompiledFiles, (std::function recordFile, std::size_t lastClassIndex, std::size_t lastFunctionIndex), (const, override)); + + MOCK_METHOD((std::pair), getPhpVersionMajorMinor, (), (const, override)); +}; + +} // namespace elasticapm::php::test diff --git a/prod/native/libphpbridge/code/PhpBridge.cpp b/prod/native/libphpbridge/code/PhpBridge.cpp index 566c09d..3ef55f2 100644 --- a/prod/native/libphpbridge/code/PhpBridge.cpp +++ b/prod/native/libphpbridge/code/PhpBridge.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -403,4 +404,59 @@ void getCurrentException(zval *zv, zend_object *exception) { } } +void PhpBridge::getCompiledFiles(std::function recordFile) const { + + void *ptr = nullptr; + ZEND_HASH_FOREACH_PTR(EG(class_table), ptr) { + zend_class_entry *ce = static_cast(ptr); + if (ce && ce->type == ZEND_USER_CLASS) { + auto filename = ce->info.user.filename; + if (filename) { + recordFile(std::string_view(ZSTR_VAL(filename), ZSTR_LEN(filename))); + } + } + } + ZEND_HASH_FOREACH_END(); + + ZEND_HASH_FOREACH_PTR(EG(function_table), ptr) { + zend_function *func = static_cast(ptr); + if (func->type == ZEND_USER_FUNCTION && func->op_array.filename) { + recordFile(std::string_view(ZSTR_VAL(func->op_array.filename), ZSTR_LEN(func->op_array.filename))); + } + } + ZEND_HASH_FOREACH_END(); +} + +std::pair PhpBridge::getNewlyCompiledFiles(std::function recordFile, std::size_t lastClassIndex, std::size_t lastFunctionIndex) const { + + void *ptr = nullptr; + + ZEND_HASH_FOREACH_PTR_FROM(EG(class_table), ptr, lastClassIndex) { + lastClassIndex++; + zend_class_entry *ce = static_cast(ptr); + if (ce && ce->type == ZEND_USER_CLASS) { + auto filename = ce->info.user.filename; + if (filename) { + recordFile(std::string_view(ZSTR_VAL(filename), ZSTR_LEN(filename))); + } + } + } + ZEND_HASH_FOREACH_END(); + + ZEND_HASH_FOREACH_PTR_FROM(EG(function_table), ptr, lastFunctionIndex) { + lastFunctionIndex++; + zend_function *func = static_cast(ptr); + if (func->type == ZEND_USER_FUNCTION && func->op_array.filename) { + recordFile(std::string_view(ZSTR_VAL(func->op_array.filename), ZSTR_LEN(func->op_array.filename))); + } + } + ZEND_HASH_FOREACH_END(); + + return {lastClassIndex, lastFunctionIndex}; +} + +std::pair PhpBridge::getPhpVersionMajorMinor() const { + return {PHP_MAJOR_VERSION, PHP_MINOR_VERSION}; +} + } // namespace elasticapm::php diff --git a/prod/native/libphpbridge/code/PhpBridge.h b/prod/native/libphpbridge/code/PhpBridge.h index f1a091a..f92ea75 100644 --- a/prod/native/libphpbridge/code/PhpBridge.h +++ b/prod/native/libphpbridge/code/PhpBridge.h @@ -58,6 +58,10 @@ class PhpBridge : public PhpBridgeInterface { bool detectOpcacheRestartPending() const final; bool isOpcacheEnabled() const final; + void getCompiledFiles(std::function recordFile) const final; + std::pair getNewlyCompiledFiles(std::function recordFile, std::size_t lastClassIndex, std::size_t lastFunctionIndex) const final; + + std::pair getPhpVersionMajorMinor() const final; private: std::shared_ptr log_; diff --git a/prod/native/phpbridge_extension/code/BridgeModuleFunctions.cpp b/prod/native/phpbridge_extension/code/BridgeModuleFunctions.cpp index 1a84f42..a96f882 100644 --- a/prod/native/phpbridge_extension/code/BridgeModuleFunctions.cpp +++ b/prod/native/phpbridge_extension/code/BridgeModuleFunctions.cpp @@ -350,6 +350,39 @@ PHP_FUNCTION(getCurrentException) { RETURN_COPY(zv.get()); } +PHP_FUNCTION(getCompiledFiles) { + BRIDGE_G(globals)->bridge.getCompiledFiles([](std::string_view file) { BRIDGE_G(globals)->logger->printf(LogLevel::logLevel_info, PRsv, PRsvArg(file)); }); + RETURN_NULL(); +} + +PHP_FUNCTION(getNewlyCompiledFiles) { + long lastClass = 0; + long lastFunction = 0; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_LONG(lastClass) + Z_PARAM_LONG(lastFunction) + ZEND_PARSE_PARAMETERS_END(); + + auto [retLastClass, retLastFunc] = BRIDGE_G(globals)->bridge.getNewlyCompiledFiles([](std::string_view file) { BRIDGE_G(globals)->logger->printf(LogLevel::logLevel_info, PRsv, PRsvArg(file)); }, lastClass, lastFunction); + + zval zlastClass, zlastFunc; + ZVAL_LONG(&zlastClass, retLastClass); + ZVAL_LONG(&zlastFunc, retLastFunc); + + RETURN_ARR(zend_new_pair(&zlastClass, &zlastFunc)); +} + +PHP_FUNCTION(getPhpVersionMajorMinor) { + auto [major, minor] = BRIDGE_G(globals)->bridge.getPhpVersionMajorMinor(); + + zval zMajor, zMinor; + ZVAL_LONG(&zMajor, major); + ZVAL_LONG(&zMinor, minor); + + RETURN_ARR(zend_new_pair(&zMajor, &zMinor)); +} + ZEND_BEGIN_ARG_INFO(no_paramters_arginfo, 0) ZEND_END_ARG_INFO() @@ -377,6 +410,10 @@ const zend_function_entry phpbridge_functions[] = { PHP_FE( getExceptionName, no_paramters_arginfo ) PHP_FE( getCurrentException, no_paramters_arginfo ) + PHP_FE( getCompiledFiles, no_paramters_arginfo ) + PHP_FE( getNewlyCompiledFiles, no_paramters_arginfo ) + PHP_FE( getPhpVersionMajorMinor, no_paramters_arginfo ) + PHP_FE_END }; // clang-format on \ No newline at end of file diff --git a/prod/native/phpbridge_extension/phpt/tests/getCompiledFiles.phpt b/prod/native/phpbridge_extension/phpt/tests/getCompiledFiles.phpt new file mode 100644 index 0000000..0ae967a --- /dev/null +++ b/prod/native/phpbridge_extension/phpt/tests/getCompiledFiles.phpt @@ -0,0 +1,28 @@ +--TEST-- +getCompiledFiles +--INI-- +extension=/elastic/phpbridge.so +--FILE-- + +--EXPECTF-- +[%a] %a/tests/getCompiledFiles.php +require +[%a] %a/tests/getCompiledFiles.php +[%a] %a/tests/includes/someClass.inc +Test completed diff --git a/prod/native/phpbridge_extension/phpt/tests/getNewlyCompiledFiles.phpt b/prod/native/phpbridge_extension/phpt/tests/getNewlyCompiledFiles.phpt new file mode 100644 index 0000000..c2dad89 --- /dev/null +++ b/prod/native/phpbridge_extension/phpt/tests/getNewlyCompiledFiles.phpt @@ -0,0 +1,31 @@ +--TEST-- +getNewlyCompiledFiles +--INI-- +extension=/elastic/phpbridge.so +--FILE-- + +--EXPECTF-- +[%a] %a/tests/getNewlyCompiledFiles.php +require +[%a] %a/tests/includes/someClass.inc +Test completed diff --git a/prod/native/phpbridge_extension/phpt/tests/getPhpVersionMajorMinor.phpt b/prod/native/phpbridge_extension/phpt/tests/getPhpVersionMajorMinor.phpt new file mode 100644 index 0000000..b5f8f97 --- /dev/null +++ b/prod/native/phpbridge_extension/phpt/tests/getPhpVersionMajorMinor.phpt @@ -0,0 +1,23 @@ +--TEST-- +getPhpVersionMajorMinor +--INI-- +extension=/elastic/phpbridge.so +--FILE-- + +--EXPECTF-- +ALL OK +Test completed diff --git a/prod/native/phpbridge_extension/phpt/tests/includes/someClass.inc b/prod/native/phpbridge_extension/phpt/tests/includes/someClass.inc new file mode 100644 index 0000000..323ba47 --- /dev/null +++ b/prod/native/phpbridge_extension/phpt/tests/includes/someClass.inc @@ -0,0 +1,11 @@ +