diff --git a/Dockerfile.redhat b/Dockerfile.redhat index 3a59009f21..aabc451d32 100644 --- a/Dockerfile.redhat +++ b/Dockerfile.redhat @@ -318,7 +318,7 @@ RUN rm -f /usr/lib64/cmake/OpenSSL/OpenSSLConfig.cmake # Builds unit tests together with ovms server in one step # It speeds up CI when tests are executed outside of the image building # hadolint ignore=SC2046 -RUN bazel build --jobs=$JOBS ${debug_bazel_flags} ${minitrace_flags} //src:ovms $(if [ "$OPTIMIZE_BUILDING_TESTS" == "1" ] ; then echo -n //src:ovms_test; fi) +RUN bazel build --jobs=$JOBS ${debug_bazel_flags} ${minitrace_flags} //src:ovms $(if [ "$OPTIMIZE_BUILDING_TESTS" == "1" ] ; then echo -n "//src:ovms_test //src:python_runtime_library_test"; fi) # Tests execution COPY ci/check_coverage.bat /ovms/ diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index f7e57e380c..cc317cd7b5 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -326,7 +326,7 @@ ARG OPTIMIZE_BUILDING_TESTS=0 # Builds unit tests together with ovms server in one step # It speeds up CI when tests are executed outside of the image building # hadolint ignore=SC2046 -RUN if [ "$FUZZER_BUILD" == "0" ]; then bazel build --jobs=$JOBS ${debug_bazel_flags} ${minitrace_flags} //src:ovms $(if [ "${OPTIMIZE_BUILDING_TESTS}" == "1" ] ; then echo -n //src:ovms_test; fi); fi; +RUN if [ "$FUZZER_BUILD" == "0" ]; then bazel build --jobs=$JOBS ${debug_bazel_flags} ${minitrace_flags} //src:ovms $(if [ "${OPTIMIZE_BUILDING_TESTS}" == "1" ] ; then echo -n "//src:ovms_test //src:python_runtime_library_test"; fi); fi; ARG RUN_TESTS=0 RUN if [ "$RUN_TESTS" == "1" ] ; then mkdir -p demos/common/export_models/ && mv export_model.py demos/common/export_models/ && ./prepare_llm_models.sh /ovms/src/test/llm_testing docker && ./run_unit_tests.sh ; fi diff --git a/create_package.sh b/create_package.sh index 78546e32d4..e216f7aba9 100755 --- a/create_package.sh +++ b/create_package.sh @@ -65,6 +65,13 @@ if [ -f /ovms_release/lib/libsrc_Slibovms_Ushared.so ] ; then \ fi # Add Python bindings for pyovms, openvino, openvino_tokenizers and openvino_genai, so they are all available for OVMS Python servables +if ! [[ $debug_bazel_flags == *"_py_off"* ]]; then + if [ ! -f /ovms_release/lib/libovmspython.so ]; then + echo "Missing libovmspython.so in package staging. Ensure //src/python:libovmspython is built." + exit 1 + fi +fi + if ! [[ $debug_bazel_flags == *"_py_off"* ]]; then cp -r /opt/intel/openvino/python /ovms_release/lib/python ; fi if ! [[ $debug_bazel_flags == *"_py_off"* ]] && [ "$FUZZER_BUILD" == "0" ]; then mv /ovms_release/lib/pyovms.so /ovms_release/lib/python ; fi if ! [[ $debug_bazel_flags == *"_py_off"* ]]; then mv /ovms_release/lib/python/bin/convert_tokenizer /ovms_release/bin/convert_tokenizer ; \ @@ -106,7 +113,7 @@ ls -lahR /ovms_release/ # removing 29MB of cpython packages for unsupported python versions rls_python=cpython-"$(python3 --version 2>&1 | awk '{gsub(/\./, "", $2); print $2}' | cut -c1-3)" -find /ovms_release/ovms/lib/python/openvino -name *cpython* | grep -vZ $rls_python | xargs rm -rf -- +find /ovms_release/lib/python/openvino -name *cpython* | grep -vZ $rls_python | xargs rm -rf -- mkdir -p /ovms_pkg/${BASE_OS} cd /ovms_pkg/${BASE_OS} diff --git a/run_unit_tests.sh b/run_unit_tests.sh index 9c054291c8..07bf369ffc 100755 --- a/run_unit_tests.sh +++ b/run_unit_tests.sh @@ -25,6 +25,7 @@ FAIL_LOG=${FAIL_LOG:-"fail.log"} if [ -f /etc/redhat-release ] ; then dist="--//:distro=redhat" ; fi debug_bazel_flags=${debug_bazel_flags:-"--config=mp_on_py_on $dist"} TEST_FILTER="--test_filter=*" +UNIT_TEST_TARGETS="//src:ovms_test //src:python_runtime_library_test" SHARED_OPTIONS=" \ --jobs=$JOBS \ ${debug_bazel_flags} \ @@ -93,7 +94,7 @@ if [ "$RUN_TESTS" == "1" ] ; then if [ "$CHECK_COVERAGE" == "1" ] ; then if bazel coverage --instrumentation_filter="-src/test" --combined_report=lcov \ ${SHARED_OPTIONS} ${TEST_FILTER} \ - //src:ovms_test ${SHARED_OPTIONS} > ${TEST_LOG} 2>&1 ; then + ${UNIT_TEST_TARGETS} ${SHARED_OPTIONS} > ${TEST_LOG} 2>&1 ; then if ! generate_coverage_report ; then compress_logs exit 1 @@ -104,12 +105,12 @@ if [ "$RUN_TESTS" == "1" ] ; then fi fi bazel test ${SHARED_OPTIONS} "${TEST_FILTER}" //src/python/binding:test_python_binding || exit 1 - bazel build ${SHARED_OPTIONS} //src:ovms_test || exit 1 + bazel build ${SHARED_OPTIONS} ${UNIT_TEST_TARGETS} || exit 1 echo "Executing unit tests" failed=0 # For RH UBI and Ubuntu - if ! bazel test --jobs=$JOBS ${debug_bazel_flags} ${SHARED_OPTIONS} --test_summary=detailed --test_output=streamed --test_filter="*" //src:ovms_test > ${TEST_LOG} 2>&1 ; then + if ! bazel test --jobs=$JOBS ${debug_bazel_flags} ${SHARED_OPTIONS} --test_summary=detailed --test_output=streamed --test_filter="*" ${UNIT_TEST_TARGETS} > ${TEST_LOG} 2>&1 ; then failed=1 fi cat ${TEST_LOG} | tail -500 diff --git a/src/BUILD b/src/BUILD index b9810a0e07..b2daa8805f 100644 --- a/src/BUILD +++ b/src/BUILD @@ -776,7 +776,9 @@ ovms_cc_library( ], deps = select({ "//:not_disable_python": [ - "//src/python:libovmspythonmodule", + "//src/python:pythonnoderesources", + "//src/python:pythonexecutorcalculator", + "//src/python:pytensorovtensorconvertercalculator", ], "//:disable_python": [] }) + select({ @@ -950,10 +952,13 @@ ovms_cc_library( "//conditions:default": ["-lOpenCL"], # TODO make as direct dependency "//src:windows" : ["/DEFAULTLIB:Rpcrt4.lib"],}), data = select({ - "//:not_disable_python": [ + "//:is_windows_and_python_is_enabled": [ + "//src/python/binding:pyovms.pyd", + ], + "//:disable_python": [], + "//conditions:default": [ "//src/python/binding:pyovms.so", ], - "//:disable_python": [] }) + select({ "//:is_windows_and_python_is_enabled": [ "//src/python/binding:copy_pyovms", @@ -2256,10 +2261,13 @@ cc_binary( "//:disable_mediapipe" : [], }), data = select({ - "//:not_disable_python": [ + "//:is_windows_and_python_is_enabled": [ + "//src/python/binding:pyovms.pyd", + ], + "//:disable_python": [], + "//conditions:default": [ "//src/python/binding:pyovms.so", ], - "//:disable_python": [] }), # linkstatic = False, # Use for dynamic linking when necessary ) @@ -2513,7 +2521,15 @@ cc_test( "//src:libcustom_node_image_transformation.so", "//src:libcustom_node_add_one.so", "//src:libcustom_node_horizontal_ocr.so", - ], + ] + select({ + "//:is_windows_and_python_is_enabled": [ + "//src/python:libovmspython.dll", + ], + "//:disable_python": [], + "//conditions:default": [ + "//src/python:libovmspython.so", + ], + }), deps = [ "optimum-cli", "//src:ovms_lib", @@ -2567,6 +2583,11 @@ cc_test( [ "serialization_common", ], + }) + select({ + "//:not_disable_python": [ + "//src/python:libovmspythonmodule", + ], + "//:disable_python": [], }), copts = COPTS_TESTS, local_defines = COMMON_LOCAL_DEFINES, @@ -2587,6 +2608,34 @@ cc_library( copts = COPTS_TESTS, linkopts = COMMON_STATIC_LIBS_LINKOPTS, ) + +cc_test( + name = "python_runtime_library_test", + srcs = [ + "test/python_runtime_library_test.cpp", + ], + copts = ["-Wno-format-security"], + data = select({ + "//src:windows": [ + "//src/python:libovmspython.dll", + "//src/python/binding:pyovms.pyd", + ], + "//:disable_python": [], + "//conditions:default": [ + "//src/python:libovmspython.so", + "//src/python/binding:pyovms.so", + ], + }), + linkopts = select({ + "//src:windows": [], + "//conditions:default": ["-ldl"], + }), + visibility = ["//visibility:public"], + deps = [ + "@com_google_googletest//:gtest_main", + ], +) + cc_library( name = "test_test_models", hdrs = ["test/test_models.hpp",], @@ -2746,6 +2795,9 @@ cc_library( srcs = ["test/python_environment.cpp",], linkopts = [], deps = PYBIND_DEPS + [ + "//src/python:libovmspythonmodule", + "//src:cpp_headers", + "libovmsstatus", "@com_google_googletest//:gtest", ], local_defines = COMMON_LOCAL_DEFINES, diff --git a/src/kfs_frontend/kfs_graph_executor_impl.cpp b/src/kfs_frontend/kfs_graph_executor_impl.cpp index 034f6f0907..3952e4bc56 100644 --- a/src/kfs_frontend/kfs_graph_executor_impl.cpp +++ b/src/kfs_frontend/kfs_graph_executor_impl.cpp @@ -748,6 +748,11 @@ static Status deserializeTensor(const std::string& requestedName, const KFSReque #if (PYTHON_DISABLE == 0) static Status deserializeTensor(const std::string& requestedName, const KFSRequest& request, std::unique_ptr>& outTensor, PythonBackend* pythonBackend) { + if (pythonBackend == nullptr) { + const std::string details = "Python backend is not available. Ensure libovmspython runtime library is accessible when using Python tensor inputs."; + SPDLOG_DEBUG("[servable name: {} version: {}] {}", request.model_name(), request.model_version(), details); + return Status(StatusCode::MEDIAPIPE_EXECUTION_ERROR, details); + } auto requestInputItr = request.inputs().begin(); auto status = getRequestInput(requestInputItr, requestedName, request); if (!status.ok()) { diff --git a/src/module.hpp b/src/module.hpp index c9abcadd67..1816e7803d 100644 --- a/src/module.hpp +++ b/src/module.hpp @@ -18,6 +18,7 @@ namespace ovms { class Config; class Status; +class PythonBackend; enum class ModuleState { NOT_INITIALIZED, STARTED_INITIALIZE, @@ -34,6 +35,13 @@ class Module { public: virtual Status start(const ovms::Config& config) = 0; virtual void shutdown() = 0; + virtual PythonBackend* getPythonBackend() const { + return nullptr; + } + virtual bool ownsPythonInterpreter() const { + return false; + } + virtual void releaseGILFromThisThread() const {} virtual ~Module() = default; ModuleState getState() const; }; diff --git a/src/python/BUILD b/src/python/BUILD index 8b5cb2e70d..c5d289d725 100644 --- a/src/python/BUILD +++ b/src/python/BUILD @@ -16,7 +16,97 @@ load("@pybind11_bazel//:build_defs.bzl", "pybind_extension") load("@mediapipe//mediapipe/framework/port:build_config.bzl", "mediapipe_cc_proto_library", "mediapipe_proto_library") -load("//:common_settings.bzl", "PYBIND_DEPS", "ovms_cc_library") +load("@aspect_bazel_lib//:e2e/copy_action/copy.bzl", "simple_copy_file") +load("//:common_settings.bzl", + "COMMON_STATIC_LIBS_LINKOPTS", + "COMMON_FUZZER_COPTS", "COMMON_FUZZER_LINKOPTS", + "COMMON_LOCAL_DEFINES", "PYBIND_DEPS", "ovms_cc_library") + +# Copts for the python shared library. Same hardening flags as +# LINUX_COMMON_STATIC_LIBS_COPTS but WITHOUT -fvisibility=hidden. +# Omitting -fvisibility=hidden lets entry-point symbols +# (PythonInterpreterModule, etc.) remain visible in the ELF dynamic +# symbol table, and avoids a -Werror=attributes conflict that arises +# when OvmsPyTensor (default visibility) has a pybind11::object field +# (hidden visibility). +_SHARED_LIB_COPTS_LINUX = [ + "-Wall", + "-Wno-unknown-pragmas", + "-Wno-sign-compare", + # -fvisibility=hidden intentionally omitted + "-Werror", + "-Wno-deprecated-declarations", + "-Wimplicit-fallthrough", + "-fcf-protection=full", + "-Wformat", + "-Wformat-security", + "-Werror=format-security", + "-Wl,-z,noexecstack", + "-fPIC", + "-Wl,-z,relro", + "-Wl,-z,relro,-z,now", + "-Wl,-z,nodlopen", + "-fstack-protector-strong", + # pybind11 declares its entire namespace with + # __attribute__((visibility("hidden"))), so any struct that contains a + # pybind11 type (e.g. py::object) will trigger -Wattributes when the + # containing struct has default visibility. This is expected and safe + # for code compiled into a single DSO — suppress the diagnostic here. + "-Wno-attributes", +] + +_SHARED_LIB_COPTS_WINDOWS = [ + "/guard:cf", + "/W4", + "/WX", + "/external:anglebrackets", + "/external:W0", + "/sdl", + "/analyze", + "/Gy", + "/GS", + "/DYNAMICBASE", + "/Qspectre", + "/wd4305", + "/wd4324", + "/wd4068", + "/wd4458", + "/wd4100", + "/wd4389", + "/wd4127", + "/wd4673", + "/wd4670", + "/wd4244", + "/wd4297", + "/wd4702", + "/wd4267", + "/wd4996", + "/wd6240", + "/wd6326", + "/wd6385", + "/wd6294", + "/guard:cf", + "/utf-8", +] + +COPTS_SO = select({ + "//conditions:default": _SHARED_LIB_COPTS_LINUX, + "//src:windows": _SHARED_LIB_COPTS_WINDOWS, +}) + select({ + "//conditions:default": ["-DPYTHON_DISABLE=1"], + "//:not_disable_python": ["-DPYTHON_DISABLE=0"], +}) + select({ + "//conditions:default": ["-DMEDIAPIPE_DISABLE=1"], + "//:not_disable_mediapipe": ["-DMEDIAPIPE_DISABLE=0"], +}) + select({ + "//conditions:default": [], + "//:fuzzer_build": COMMON_FUZZER_COPTS, +}) + +LINKOPTS_SO = COMMON_STATIC_LIBS_LINKOPTS + select({ + "//conditions:default": [], + "//:fuzzer_build": COMMON_FUZZER_LINKOPTS, +}) mediapipe_proto_library( name = "pythonexecutorcalculator_proto", # pythonexecutorcalculator_cc_proto - just mediapipe stuff with mediapipe_proto_library adding nonvisible target @@ -75,7 +165,7 @@ ovms_cc_library( "pythonexecutorcalculator_cc_proto", "utils", ], - visibility = ["//visibility:private"], + visibility = ["//src:__pkg__"], alwayslink = 1, data = ["//src/python/binding:pyovms.so"], ) @@ -91,7 +181,7 @@ ovms_cc_library( "pytensorovtensorconvertercalculator_cc_proto", "pythonbackend", ], - visibility = ["//visibility:private"], + visibility = ["//src:__pkg__"], alwayslink = 1, data = ["//src/python/binding:pyovms.so"], ) @@ -110,7 +200,7 @@ ovms_cc_library( "//src/mediapipe_internal:node_initializer", "//src:libovmslogging", ], - visibility = ["//visibility:private"], + visibility = ["//src:__pkg__"], alwayslink = 1, data = ["//src/python/binding:pyovms.so"], ) @@ -145,3 +235,93 @@ ovms_cc_library( alwayslink = 1, data = ["//src/python/binding:pyovms.so"], ) + +# Lightweight target for tests that need direct PythonInterpreterModule usage +# without pulling MediaPipe calculator registration code. +ovms_cc_library( + name = "pythoninterpretermodule_runtime", + hdrs = ["pythoninterpretermodule.hpp",], + srcs = ["pythoninterpretermodule.cpp",], + deps = PYBIND_DEPS + [ + "//src:cpp_headers", + "//src:libovmslogging", + "//src:libovms_module", + "pythonbackend", + ], + visibility = ["//src:__pkg__"], + alwayslink = 1, + data = ["//src/python/binding:pyovms.so"], +) + +# Shared library built from all Python binding sources. +# Deps whose symbols are compiled with -fvisibility=hidden (the project default) +# are linked into the .so with hidden visibility; only the python module +# interface symbols compiled via srcs/COPTS_SO are exported. +cc_binary( + name = "libovmspython.so", + linkshared = True, + srcs = [ + "ovms_py_tensor.cpp", + "ovms_py_tensor.hpp", + "python_backend.cpp", + "python_backend.hpp", + "pythoninterpretermodule.cpp", + "pythoninterpretermodule.hpp", + "python_runtime_entry.cpp", + "utils.hpp", + ], + deps = PYBIND_DEPS + [ + "//src:libovmslogging", + "//src:libovmsstatus", + "//src:libovms_module", + "//src:cpp_headers", + ], + copts = COPTS_SO, + linkopts = LINKOPTS_SO, + local_defines = COMMON_LOCAL_DEFINES, + visibility = ["//visibility:public"], +) + +simple_copy_file( + name = "copy_libovmspython", + src = "libovmspython.so", + out = "libovmspython.dll", + visibility = ["//visibility:public"], +) + +# cc_import wrapper so that other targets can depend on the shared library +# the same way they previously depended on :libovmspythonmodule. +cc_import( + name = "libovmspython_unix", + shared_library = ":libovmspython.so", + hdrs = [ + "ovms_py_tensor.hpp", + "python_backend.hpp", + "pythoninterpretermodule.hpp", + "utils.hpp", + ], + visibility = ["//visibility:private"], +) + +cc_import( + name = "libovmspython_windows", + interface_library = ":libovmspython.so.if.lib", + shared_library = ":copy_libovmspython", + hdrs = [ + "ovms_py_tensor.hpp", + "python_backend.hpp", + "pythoninterpretermodule.hpp", + "utils.hpp", + ], + visibility = ["//visibility:private"], +) + +alias( + name = "libovmspython", + actual = select({ + "//src:windows": ":libovmspython_windows", + "//conditions:default": ":libovmspython_unix", + }), + visibility = ["//visibility:public"], +) + diff --git a/src/python/python_backend.hpp b/src/python/python_backend.hpp index 058fd815ab..9389b0231f 100644 --- a/src/python/python_backend.hpp +++ b/src/python/python_backend.hpp @@ -32,24 +32,32 @@ using namespace py::literals; namespace ovms { +#if defined(_WIN32) +#define PYTHON_BACKEND_EXPORT __declspec(dllexport) +#else +#define PYTHON_BACKEND_EXPORT __attribute__((visibility("default"))) +#endif + class PythonBackend { std::unique_ptr pyovmsModule; std::unique_ptr tensorClass; public: - PythonBackend(); - ~PythonBackend(); - static bool createPythonBackend(std::unique_ptr& pythonBackend); + PYTHON_BACKEND_EXPORT PythonBackend(); + PYTHON_BACKEND_EXPORT ~PythonBackend(); + PYTHON_BACKEND_EXPORT static bool createPythonBackend(std::unique_ptr& pythonBackend); - bool createOvmsPyTensor(const std::string& name, void* ptr, const std::vector& shape, const std::string& datatype, + PYTHON_BACKEND_EXPORT bool createOvmsPyTensor(const std::string& name, void* ptr, const std::vector& shape, const std::string& datatype, py::ssize_t size, std::unique_ptr>& outTensor, bool copy = false); - bool createEmptyOvmsPyTensor(const std::string& name, const std::vector& shape, const std::string& datatype, + PYTHON_BACKEND_EXPORT bool createEmptyOvmsPyTensor(const std::string& name, const std::vector& shape, const std::string& datatype, py::ssize_t size, std::unique_ptr>& outTensor); // Checks if object is tensorClass instance. Throws UnexpectedPythonObjectError if it's not. - void validateOvmsPyTensor(const py::object& object) const; + PYTHON_BACKEND_EXPORT void validateOvmsPyTensor(const py::object& object) const; - bool getOvmsPyTensorData(std::unique_ptr>& outTensor, void** data); + PYTHON_BACKEND_EXPORT bool getOvmsPyTensorData(std::unique_ptr>& outTensor, void** data); }; + +#undef PYTHON_BACKEND_EXPORT } // namespace ovms diff --git a/src/python/python_executor_calculator.cc b/src/python/python_executor_calculator.cc index 4e36cda652..3e713ab0ab 100644 --- a/src/python/python_executor_calculator.cc +++ b/src/python/python_executor_calculator.cc @@ -200,6 +200,9 @@ class PythonExecutorCalculator : public CalculatorBase { } nodeResources = it->second; + if (nodeResources == nullptr || nodeResources->pythonBackend == nullptr) { + return absl::Status(absl::StatusCode::kFailedPrecondition, "Python backend is not available for PythonExecutorCalculator"); + } outputTimestamp = mediapipe::Timestamp(mediapipe::Timestamp::Unset()); LOG(INFO) << "PythonExecutorCalculator [Node: " << cc->NodeName() << "] Open end"; return absl::OkStatus(); diff --git a/src/python/python_runtime_entry.cpp b/src/python/python_runtime_entry.cpp new file mode 100644 index 0000000000..ee6a749f23 --- /dev/null +++ b/src/python/python_runtime_entry.cpp @@ -0,0 +1,77 @@ +//***************************************************************************** +// Copyright 2026 Intel Corporation +// +// Licensed 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 "../module.hpp" +#include "pythoninterpretermodule.hpp" + +#include + +#pragma warning(push) +#pragma warning(disable : 6326 28182 6011 28020) +#include +#pragma warning(pop) + +namespace py = pybind11; + +#if defined(_WIN32) +#define PYTHON_RUNTIME_EXPORT __declspec(dllexport) +#else +#define PYTHON_RUNTIME_EXPORT __attribute__((visibility("default"))) +#endif + +extern "C" PYTHON_RUNTIME_EXPORT ovms::Module* OVMS_createPythonInterpreterModule() { + return new ovms::PythonInterpreterModule(); +} + +extern "C" PYTHON_RUNTIME_EXPORT bool OVMS_validatePythonEnvironment(const char** errorMessage) { + static thread_local std::string lastError; + if (errorMessage != nullptr) { + *errorMessage = nullptr; + } + + bool ownsInterpreter = false; + try { + if (!Py_IsInitialized()) { + py::initialize_interpreter(); + ownsInterpreter = true; + } + { + py::gil_scoped_acquire acquire; + // Validate that OVMS Python bindings are importable and executable. + py::module_::import("pyovms"); + } + if (ownsInterpreter) { + py::finalize_interpreter(); + } + return true; + } catch (const py::error_already_set& e) { + lastError = e.what(); + } catch (const std::exception& e) { + lastError = e.what(); + } catch (...) { + lastError = "Unknown python runtime validation error"; + } + + if (ownsInterpreter && Py_IsInitialized()) { + py::finalize_interpreter(); + } + if (errorMessage != nullptr) { + *errorMessage = lastError.c_str(); + } + return false; +} + +#undef PYTHON_RUNTIME_EXPORT diff --git a/src/python/pythoninterpretermodule.hpp b/src/python/pythoninterpretermodule.hpp index e87a3cc28f..1a9e5751ed 100644 --- a/src/python/pythoninterpretermodule.hpp +++ b/src/python/pythoninterpretermodule.hpp @@ -39,9 +39,9 @@ class PythonInterpreterModule : public Module { ~PythonInterpreterModule(); Status start(const ovms::Config& config) override; void shutdown() override; - PythonBackend* getPythonBackend() const; - void releaseGILFromThisThread() const; + PythonBackend* getPythonBackend() const override; + void releaseGILFromThisThread() const override; void reacquireGILForThisThread() const; - bool ownsPythonInterpreter() const; + bool ownsPythonInterpreter() const override; }; } // namespace ovms diff --git a/src/servablemanagermodule.cpp b/src/servablemanagermodule.cpp index 3c9e8ad291..a1104e7b12 100644 --- a/src/servablemanagermodule.cpp +++ b/src/servablemanagermodule.cpp @@ -23,9 +23,6 @@ #include "metrics/metric_module.hpp" #include "modelmanager.hpp" #include "server.hpp" -#if (PYTHON_DISABLE == 0) -#include "python/pythoninterpretermodule.hpp" -#endif namespace ovms { class PythonBackend; @@ -33,7 +30,7 @@ class PythonBackend; ServableManagerModule::ServableManagerModule(ovms::Server& ovmsServer) { PythonBackend* pythonBackend = nullptr; #if (PYTHON_DISABLE == 0) - auto pythonModule = dynamic_cast(ovmsServer.getModule(PYTHON_INTERPRETER_MODULE_NAME)); + auto pythonModule = ovmsServer.getModule(PYTHON_INTERPRETER_MODULE_NAME); if (pythonModule != nullptr) pythonBackend = pythonModule->getPythonBackend(); #endif diff --git a/src/server.cpp b/src/server.cpp index bdf572f76c..3f1ca69f3d 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -32,11 +32,13 @@ #include #ifdef __linux__ +#include #include #include #include #elif _WIN32 #include +#include #include #include @@ -71,13 +73,165 @@ #include "stringutils.hpp" #include "version.hpp" +using grpc::ServerBuilder; + +namespace ovms { + #if (PYTHON_DISABLE == 0) -#include "python/pythoninterpretermodule.hpp" +namespace { +#ifdef __linux__ +using PythonLibraryHandle = void*; +#elif _WIN32 +using PythonLibraryHandle = HMODULE; #endif +using CreatePythonInterpreterModuleFn = Module* (*)(); +using ValidatePythonEnvironmentFn = bool (*)(const char** errorMessage); -using grpc::ServerBuilder; +PythonLibraryHandle pythonRuntimeHandle = nullptr; +CreatePythonInterpreterModuleFn createPythonInterpreterModuleFn = nullptr; +ValidatePythonEnvironmentFn validatePythonEnvironmentFn = nullptr; -namespace ovms { +bool ensurePythonRuntimeLoaded() { + if (createPythonInterpreterModuleFn != nullptr && validatePythonEnvironmentFn != nullptr) { + return true; + } + +#ifdef __linux__ + std::vector candidates{ + "libovmspython.so", + "./libovmspython.so", + "src/python/libovmspython.so", + "./src/python/libovmspython.so", + "bazel-bin/src/python/libovmspython.so", + "./bazel-bin/src/python/libovmspython.so"}; + + for (const auto& candidate : candidates) { + pythonRuntimeHandle = dlopen(candidate.c_str(), RTLD_NOW | RTLD_LOCAL); + if (pythonRuntimeHandle != nullptr) { + break; + } + } + + if (pythonRuntimeHandle == nullptr) { + SPDLOG_WARN("Python runtime library libovmspython.so failed to load: {}", dlerror()); + return false; + } + createPythonInterpreterModuleFn = reinterpret_cast(dlsym(pythonRuntimeHandle, "OVMS_createPythonInterpreterModule")); + if (createPythonInterpreterModuleFn == nullptr) { + SPDLOG_WARN("Python runtime library libovmspython.so missing symbol OVMS_createPythonInterpreterModule: {}", dlerror()); + dlclose(pythonRuntimeHandle); + pythonRuntimeHandle = nullptr; + return false; + } + validatePythonEnvironmentFn = reinterpret_cast(dlsym(pythonRuntimeHandle, "OVMS_validatePythonEnvironment")); + if (validatePythonEnvironmentFn == nullptr) { + SPDLOG_WARN("Python runtime library libovmspython.so missing symbol OVMS_validatePythonEnvironment: {}", dlerror()); + createPythonInterpreterModuleFn = nullptr; + dlclose(pythonRuntimeHandle); + pythonRuntimeHandle = nullptr; + return false; + } +#elif _WIN32 + std::vector candidates{ + "libovmspython.dll", + ".\\libovmspython.dll", + "src\\python\\libovmspython.dll", + ".\\src\\python\\libovmspython.dll", + "bazel-bin\\src\\python\\libovmspython.dll", + ".\\bazel-bin\\src\\python\\libovmspython.dll"}; + + char executablePath[MAX_PATH] = {0}; + DWORD executablePathLength = GetModuleFileNameA(nullptr, executablePath, MAX_PATH); + if (executablePathLength > 0 && executablePathLength < MAX_PATH) { + std::string exePath(executablePath, executablePathLength); + std::string exeDir = "."; + size_t separatorPos = exePath.find_last_of("\\/"); + if (separatorPos != std::string::npos) { + exeDir = exePath.substr(0, separatorPos); + } + + std::vector executableRelativeCandidates{ + exeDir + "\\libovmspython.dll", + exeDir + "\\src\\python\\libovmspython.dll", + exeDir + "\\..\\src\\python\\libovmspython.dll", + }; + + std::string runfilesRoot = exePath + ".runfiles"; + std::vector runfilesCandidates{ + runfilesRoot + "\\src\\python\\libovmspython.dll", + runfilesRoot + "\\_main\\src\\python\\libovmspython.dll", + runfilesRoot + "\\model_server\\src\\python\\libovmspython.dll", + }; + + candidates.insert(candidates.end(), executableRelativeCandidates.begin(), executableRelativeCandidates.end()); + candidates.insert(candidates.end(), runfilesCandidates.begin(), runfilesCandidates.end()); + } + + for (const auto& candidate : candidates) { + pythonRuntimeHandle = LoadLibraryA(candidate.c_str()); + if (pythonRuntimeHandle != nullptr) { + break; + } + } + + if (pythonRuntimeHandle == nullptr) { + DWORD error = GetLastError(); + SPDLOG_WARN("Python runtime library libovmspython.dll failed to load: {} ({})", error, std::system_category().message(error)); + return false; + } + createPythonInterpreterModuleFn = reinterpret_cast(GetProcAddress(pythonRuntimeHandle, "OVMS_createPythonInterpreterModule")); + if (createPythonInterpreterModuleFn == nullptr) { + DWORD error = GetLastError(); + SPDLOG_WARN("Python runtime library libovmspython.dll missing symbol OVMS_createPythonInterpreterModule: {} ({})", error, std::system_category().message(error)); + FreeLibrary(pythonRuntimeHandle); + pythonRuntimeHandle = nullptr; + return false; + } + validatePythonEnvironmentFn = reinterpret_cast(GetProcAddress(pythonRuntimeHandle, "OVMS_validatePythonEnvironment")); + if (validatePythonEnvironmentFn == nullptr) { + DWORD error = GetLastError(); + SPDLOG_WARN("Python runtime library libovmspython.dll missing symbol OVMS_validatePythonEnvironment: {} ({})", error, std::system_category().message(error)); + createPythonInterpreterModuleFn = nullptr; + FreeLibrary(pythonRuntimeHandle); + pythonRuntimeHandle = nullptr; + return false; + } +#endif + + const char* pythonRuntimeValidationError = nullptr; + if (!validatePythonEnvironmentFn(&pythonRuntimeValidationError)) { + SPDLOG_WARN("Python runtime environment validation failed. Ensure Python dependencies and PYTHONPATH are configured. Details: {}", + pythonRuntimeValidationError != nullptr ? pythonRuntimeValidationError : "Unknown error"); + createPythonInterpreterModuleFn = nullptr; + validatePythonEnvironmentFn = nullptr; +#ifdef __linux__ + dlclose(pythonRuntimeHandle); +#elif _WIN32 + FreeLibrary(pythonRuntimeHandle); +#endif + pythonRuntimeHandle = nullptr; + return false; + } + + SPDLOG_INFO("Python runtime library loaded successfully"); + return true; +} + +void unloadPythonRuntime() { + createPythonInterpreterModuleFn = nullptr; + validatePythonEnvironmentFn = nullptr; + if (pythonRuntimeHandle == nullptr) { + return; + } +#ifdef __linux__ + dlclose(pythonRuntimeHandle); +#elif _WIN32 + FreeLibrary(pythonRuntimeHandle); +#endif + pythonRuntimeHandle = nullptr; +} +} // namespace +#endif Server& Server::instance() { static Server global; @@ -315,8 +469,12 @@ std::unique_ptr Server::createModule(const std::string& name) { if (name == SERVABLE_MANAGER_MODULE_NAME) return std::make_unique(*this); #if (PYTHON_DISABLE == 0) - if (name == PYTHON_INTERPRETER_MODULE_NAME) - return std::make_unique(); + if (name == PYTHON_INTERPRETER_MODULE_NAME) { + if (!ensurePythonRuntimeLoaded()) { + return nullptr; + } + return std::unique_ptr(createPythonInterpreterModuleFn()); + } #endif if (name == METRICS_MODULE_NAME) return std::make_unique(); @@ -387,8 +545,16 @@ Status Server::startModules(ovms::Config& config) { #if (PYTHON_DISABLE == 0) if (config.getServerSettings().withPython) { - INSERT_MODULE(PYTHON_INTERPRETER_MODULE_NAME, it); - START_MODULE(it); + auto pythonModule = this->createModule(PYTHON_INTERPRETER_MODULE_NAME); + if (pythonModule == nullptr) { + SPDLOG_WARN("Python requested in configuration, but runtime library could not be loaded. Continuing with Python features disabled."); + } else { + std::unique_lock lock(modulesMtx); + std::tie(it, inserted) = this->modules.emplace(PYTHON_INTERPRETER_MODULE_NAME, std::move(pythonModule)); + if (!inserted) + return Status(StatusCode::MODULE_ALREADY_INSERTED, PYTHON_INTERPRETER_MODULE_NAME); + START_MODULE(it); + } } #endif #if MTR_ENABLED @@ -427,12 +593,12 @@ Status Server::startModules(ovms::Config& config) { START_MODULE(it); #if (PYTHON_DISABLE == 0) if (config.getServerSettings().withPython) { - GET_MODULE(PYTHON_INTERPRETER_MODULE_NAME, it); - auto pythonModule = dynamic_cast(it->second.get()); - if (pythonModule->ownsPythonInterpreter()) { + std::shared_lock lock(modulesMtx); + auto pythonModuleIt = modules.find(PYTHON_INTERPRETER_MODULE_NAME); + if (pythonModuleIt != modules.end() && pythonModuleIt->second != nullptr && pythonModuleIt->second->ownsPythonInterpreter()) { // Natively GIL is held by the thread that initialized interpreter, so we only need to release it, if we own the interpreter. // If it was initialized externally, then the external thread shall release the GIL before launching that module. - pythonModule->releaseGILFromThisThread(); + pythonModuleIt->second->releaseGILFromThisThread(); } } #endif @@ -492,6 +658,9 @@ void Server::shutdownModules() { // this is because the OS can have a delay between freeing up port before it can be requested and used again std::shared_lock lock(modulesMtx); modules.clear(); +#if (PYTHON_DISABLE == 0) + unloadPythonRuntime(); +#endif } static int statusToExitCode(const Status& status) { diff --git a/src/test/python_environment.cpp b/src/test/python_environment.cpp index 72f6425d4c..f3d8ea5ea1 100644 --- a/src/test/python_environment.cpp +++ b/src/test/python_environment.cpp @@ -16,29 +16,76 @@ #include "python_environment.hpp" #include +#include + +#include "../config.hpp" +#include "../status.hpp" + +namespace { +PythonEnvironment* g_pythonEnvironment = nullptr; +} void PythonEnvironment::SetUp() { #if (PYTHON_DISABLE == 0) - py::initialize_interpreter(); - releaseGILFromThisThread(); + pythonModule = std::make_unique(); + auto status = pythonModule->start(ovms::Config::instance()); + if (!status.ok()) { + throw std::runtime_error("Global python interpreter module failed to start"); + } + if (pythonModule->ownsPythonInterpreter()) { + pythonModule->releaseGILFromThisThread(); + } + g_pythonEnvironment = this; #endif } void PythonEnvironment::TearDown() { #if (PYTHON_DISABLE == 0) - reacquireGILForThisThread(); - py::finalize_interpreter(); + g_pythonEnvironment = nullptr; + if (pythonModule != nullptr) { + if (pythonModule->ownsPythonInterpreter()) { + pythonModule->reacquireGILForThisThread(); + } + pythonModule->shutdown(); + pythonModule.reset(); + } #endif } -void PythonEnvironment::releaseGILFromThisThread() const { +ovms::PythonBackend* PythonEnvironment::getPythonBackend() const { #if (PYTHON_DISABLE == 0) - this->GILScopedRelease = std::make_unique(); + if (pythonModule == nullptr) { + return nullptr; + } + return pythonModule->getPythonBackend(); +#else + return nullptr; #endif } -void PythonEnvironment::reacquireGILForThisThread() const { +ovms::PythonInterpreterModule* PythonEnvironment::getPythonInterpreterModule() const { +#if (PYTHON_DISABLE == 0) + return pythonModule.get(); +#else + return nullptr; +#endif +} + +ovms::PythonBackend* getGlobalPythonBackend() { + auto* pythonInterpreterModule = getGlobalPythonInterpreterModule(); + if (pythonInterpreterModule == nullptr) { + return nullptr; + } + return pythonInterpreterModule->getPythonBackend(); +} + +ovms::PythonInterpreterModule* getGlobalPythonInterpreterModule() { #if (PYTHON_DISABLE == 0) - this->GILScopedRelease.reset(); + if (g_pythonEnvironment == nullptr) { + return nullptr; + } + return g_pythonEnvironment->getPythonInterpreterModule(); +#else + return nullptr; #endif } diff --git a/src/test/python_environment.hpp b/src/test/python_environment.hpp index 26fe5f38b0..51963ca7ea 100644 --- a/src/test/python_environment.hpp +++ b/src/test/python_environment.hpp @@ -19,19 +19,21 @@ #include #include -#pragma warning(push) -#pragma warning(disable : 6326 28182 6011 28020) -#include // everything needed for embedding -#pragma warning(pop) +#include "../python/pythoninterpretermodule.hpp" -namespace py = pybind11; +namespace ovms { +class PythonBackend; +} class PythonEnvironment : public testing::Environment { - mutable std::unique_ptr GILScopedRelease; + std::unique_ptr pythonModule; public: void SetUp() override; void TearDown() override; - void releaseGILFromThisThread() const; - void reacquireGILForThisThread() const; + ovms::PythonInterpreterModule* getPythonInterpreterModule() const; + ovms::PythonBackend* getPythonBackend() const; }; + +ovms::PythonBackend* getGlobalPythonBackend(); +ovms::PythonInterpreterModule* getGlobalPythonInterpreterModule(); diff --git a/src/test/python_runtime_library_test.cpp b/src/test/python_runtime_library_test.cpp new file mode 100644 index 0000000000..38f07bb5be --- /dev/null +++ b/src/test/python_runtime_library_test.cpp @@ -0,0 +1,285 @@ +//***************************************************************************** +// Copyright 2026 Intel Corporation +// +// Licensed 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 +#include +#include +#include + +#include +#include + +#ifdef __linux__ +#include +#endif + +#ifdef _WIN32 +#include +#endif + +using testing::HasSubstr; + +namespace { + +using ValidatePythonEnvironmentFn = bool (*)(const char** errorMessage); + +class ScopedSharedLibrary { +public: +#ifdef _WIN32 + using HandleType = HMODULE; +#else + using HandleType = void*; +#endif + +private: + HandleType handle; + +public: + explicit ScopedSharedLibrary(const std::filesystem::path& path) : + handle( +#ifdef _WIN32 + LoadLibraryA(path.string().c_str()) +#else + dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL) +#endif + ) { + } + + ~ScopedSharedLibrary() { + if (handle != nullptr) { +#ifdef _WIN32 + FreeLibrary(handle); +#else + dlclose(handle); +#endif + } + } + + HandleType get() const { + return handle; + } +}; + +const char* getLibraryLoadError() { +#ifdef _WIN32 + return "LoadLibrary failed"; +#else + const char* error = dlerror(); + return error == nullptr ? "dlopen failed" : error; +#endif +} + +void* findSymbol(ScopedSharedLibrary::HandleType handle, const char* symbolName) { +#ifdef _WIN32 + return reinterpret_cast(GetProcAddress(handle, symbolName)); +#else + return dlsym(handle, symbolName); +#endif +} + +class ScopedEnvironmentVariable { + std::string name; + bool hadValue; + std::string previousValue; + +public: + ScopedEnvironmentVariable(const std::string& name, const std::string& value) : + name(name), + hadValue(false) { + if (const char* currentValue = std::getenv(name.c_str()); currentValue != nullptr) { + hadValue = true; + previousValue = currentValue; + } +#ifdef _WIN32 + _putenv_s(name.c_str(), value.c_str()); +#else + setenv(name.c_str(), value.c_str(), 1); +#endif + } + + ~ScopedEnvironmentVariable() { + if (hadValue) { +#ifdef _WIN32 + _putenv_s(name.c_str(), previousValue.c_str()); +#else + setenv(name.c_str(), previousValue.c_str(), 1); +#endif + } else { +#ifdef _WIN32 + _putenv_s(name.c_str(), ""); +#else + unsetenv(name.c_str()); +#endif + } + } +}; + +std::string getRuntimeLibraryFilename() { +#ifdef _WIN32 + return "libovmspython.dll"; +#else + return "libovmspython.so"; +#endif +} + +std::string getBindingFilename() { +#ifdef _WIN32 + return "pyovms.pyd"; +#else + return "pyovms.so"; +#endif +} + +std::filesystem::path findLibrary(const std::string& libName) { + std::vector searchPaths; + + if (const char* testSrcDir = std::getenv("TEST_SRCDIR"); testSrcDir != nullptr && testSrcDir[0] != '\0') { + std::filesystem::path srcDir(testSrcDir); + if (const char* workspace = std::getenv("TEST_WORKSPACE"); workspace != nullptr && workspace[0] != '\0') { + searchPaths.emplace_back(srcDir / workspace / "src/python" / libName); + searchPaths.emplace_back(srcDir / workspace / "bazel-bin" / "src/python" / libName); + } + searchPaths.emplace_back(srcDir / "_main" / "src/python" / libName); + searchPaths.emplace_back(srcDir / "_main" / "bazel-bin" / "src/python" / libName); + searchPaths.emplace_back(srcDir / "model_server" / "src/python" / libName); + searchPaths.emplace_back(srcDir / "model_server" / "bazel-bin" / "src/python" / libName); + } + + try { + const auto testBinaryPath = std::filesystem::canonical("/proc/self/exe"); + const auto runfilesDir = testBinaryPath.string() + ".runfiles"; + searchPaths.emplace_back(std::filesystem::path(runfilesDir) / "src/python" / libName); + searchPaths.emplace_back(std::filesystem::path(runfilesDir) / "_main" / "src/python" / libName); + searchPaths.emplace_back(std::filesystem::path(runfilesDir) / "model_server" / "src/python" / libName); + } catch (...) { + } + + searchPaths.emplace_back(std::filesystem::path("bazel-bin/src/python") / libName); + searchPaths.emplace_back(std::filesystem::path("src/python") / libName); + searchPaths.emplace_back(libName); + + for (const auto& path : searchPaths) { + if (std::filesystem::exists(path)) { + return path; + } + } + + return {}; +} + +std::filesystem::path findPyovmsBinding() { + std::vector searchPaths; + + if (const char* testSrcDir = std::getenv("TEST_SRCDIR"); testSrcDir != nullptr && testSrcDir[0] != '\0') { + std::filesystem::path srcDir(testSrcDir); + const std::string bindingFilename = getBindingFilename(); + if (const char* workspace = std::getenv("TEST_WORKSPACE"); workspace != nullptr && workspace[0] != '\0') { + searchPaths.emplace_back(srcDir / workspace / "src/python/binding" / bindingFilename); + searchPaths.emplace_back(srcDir / workspace / "bazel-bin" / "src/python/binding" / bindingFilename); + } + searchPaths.emplace_back(srcDir / "_main" / "src/python/binding" / bindingFilename); + searchPaths.emplace_back(srcDir / "_main" / "bazel-bin" / "src/python/binding" / bindingFilename); + searchPaths.emplace_back(srcDir / "model_server" / "src/python/binding" / bindingFilename); + searchPaths.emplace_back(srcDir / "model_server" / "bazel-bin" / "src/python/binding" / bindingFilename); + } + + try { + const std::string bindingFilename = getBindingFilename(); + const auto testBinaryPath = std::filesystem::canonical("/proc/self/exe"); + const auto runfilesDir = testBinaryPath.string() + ".runfiles"; + searchPaths.emplace_back(std::filesystem::path(runfilesDir) / "src/python/binding" / bindingFilename); + searchPaths.emplace_back(std::filesystem::path(runfilesDir) / "_main" / "src/python/binding" / bindingFilename); + searchPaths.emplace_back(std::filesystem::path(runfilesDir) / "model_server" / "src/python/binding" / bindingFilename); + } catch (...) { + } + + const std::string bindingFilename = getBindingFilename(); + searchPaths.emplace_back(std::filesystem::path("bazel-bin/src/python/binding") / bindingFilename); + searchPaths.emplace_back(std::filesystem::path("src/python/binding") / bindingFilename); + searchPaths.emplace_back(bindingFilename); + + for (const auto& path : searchPaths) { + if (std::filesystem::exists(path)) { + return path; + } + } + + return {}; +} + +} // namespace + +TEST(PythonRuntimeLibrary, MissingLibraryPathFailsToLoad) { + const auto missingLibraryPath = std::filesystem::temp_directory_path() / ("missing_" + getRuntimeLibraryFilename()); + ASSERT_FALSE(std::filesystem::exists(missingLibraryPath)); + + ScopedSharedLibrary library(missingLibraryPath); + + EXPECT_EQ(library.get(), nullptr); +} + +TEST(PythonRuntimeLibrary, ExistingLibraryExportsRequiredSymbols) { + const auto runtimeLibraryFilename = getRuntimeLibraryFilename(); + const auto libraryPath = findLibrary(runtimeLibraryFilename); + ASSERT_FALSE(libraryPath.empty()) << "Could not find " << runtimeLibraryFilename; + + ScopedSharedLibrary library(libraryPath); + ASSERT_NE(library.get(), nullptr) << getLibraryLoadError(); + + EXPECT_NE(findSymbol(library.get(), "OVMS_createPythonInterpreterModule"), nullptr); + EXPECT_NE(findSymbol(library.get(), "OVMS_validatePythonEnvironment"), nullptr); +} + +TEST(PythonRuntimeLibrary, ValidationFailsWithoutBindingOnPythonPath) { + const auto runtimeLibraryFilename = getRuntimeLibraryFilename(); + const auto libraryPath = findLibrary(runtimeLibraryFilename); + ASSERT_FALSE(libraryPath.empty()) << "Could not find " << runtimeLibraryFilename; + + ScopedSharedLibrary library(libraryPath); + ASSERT_NE(library.get(), nullptr) << getLibraryLoadError(); + + const auto emptyPythonPath = std::filesystem::temp_directory_path() / "ovms_empty_pythonpath"; + std::filesystem::create_directories(emptyPythonPath); + ScopedEnvironmentVariable pythonPathEnv("PYTHONPATH", emptyPythonPath.string()); + + auto validate = reinterpret_cast(findSymbol(library.get(), "OVMS_validatePythonEnvironment")); + ASSERT_NE(validate, nullptr); + + const char* errorMessage = nullptr; + EXPECT_FALSE(validate(&errorMessage)); + ASSERT_NE(errorMessage, nullptr); + EXPECT_THAT(std::string(errorMessage), HasSubstr("pyovms")); +} + +TEST(PythonRuntimeLibrary, ValidationSucceedsWithBindingOnPythonPath) { + const auto runtimeLibraryFilename = getRuntimeLibraryFilename(); + const auto libraryPath = findLibrary(runtimeLibraryFilename); + ASSERT_FALSE(libraryPath.empty()) << "Could not find " << runtimeLibraryFilename; + const auto bindingPath = findPyovmsBinding(); + ASSERT_FALSE(bindingPath.empty()) << "Could not find " << getBindingFilename(); + + ScopedSharedLibrary library(libraryPath); + ASSERT_NE(library.get(), nullptr) << getLibraryLoadError(); + + ScopedEnvironmentVariable pythonPathEnv("PYTHONPATH", bindingPath.parent_path().string()); + + auto validate = reinterpret_cast(findSymbol(library.get(), "OVMS_validatePythonEnvironment")); + ASSERT_NE(validate, nullptr); + + const char* errorMessage = nullptr; + EXPECT_TRUE(validate(&errorMessage)); + EXPECT_EQ(errorMessage, nullptr); +} diff --git a/src/test/pythonnode_test.cpp b/src/test/pythonnode_test.cpp index 5b31146d19..b87336b409 100644 --- a/src/test/pythonnode_test.cpp +++ b/src/test/pythonnode_test.cpp @@ -39,8 +39,8 @@ #include "src/metrics/metric_config.hpp" #include "src/metrics/metric_module.hpp" #include "../model_service.hpp" +#include "../module.hpp" #include "../precision.hpp" -#include "../python/pythoninterpretermodule.hpp" #include "../python/pythonnoderesources.hpp" #include "../servablemanagermodule.hpp" #include "../server.hpp" @@ -58,6 +58,7 @@ #include "c_api_test_utils.hpp" #include "constructor_enabled_model_manager.hpp" #include "platform_utils.hpp" +#include "python_environment.hpp" #include "test_utils.hpp" namespace py = pybind11; @@ -112,7 +113,19 @@ class PythonFlowTest : public ::testing::Test { }; static PythonBackend* getPythonBackend() { - return dynamic_cast(ovms::Server::instance().getModule(PYTHON_INTERPRETER_MODULE_NAME))->getPythonBackend(); + auto* pythonModule = ovms::Server::instance().getModule(PYTHON_INTERPRETER_MODULE_NAME); + if (pythonModule != nullptr) { + auto* pythonBackend = pythonModule->getPythonBackend(); + if (pythonBackend != nullptr) { + return pythonBackend; + } + } + + auto* pythonBackend = getGlobalPythonBackend(); + if (pythonBackend == nullptr) { + throw std::runtime_error("Python backend is not available"); + } + return pythonBackend; } // --------------------------------------- OVMS initializing Python nodes tests diff --git a/src/test/unit_tests.cpp b/src/test/unit_tests.cpp index 4a841c6171..0b26f90f5b 100644 --- a/src/test/unit_tests.cpp +++ b/src/test/unit_tests.cpp @@ -14,6 +14,8 @@ // limitations under the License. //***************************************************************************** +#include + #include "environment.hpp" #include "gpuenvironment.hpp" #include "gguf_environment.hpp" diff --git a/windows_create_package.bat b/windows_create_package.bat index 13402df4ce..959fedef5c 100644 --- a/windows_create_package.bat +++ b/windows_create_package.bat @@ -53,11 +53,20 @@ if !errorlevel! neq 0 exit /b !errorlevel! set "dest_dir=C:\opt" if /i "%with_python%"=="true" ( + if not exist %cd%\bazel-out\x64_windows-opt\bin\src\python\libovmspython.dll ( + echo Missing libovmspython.dll in bazel output. Ensure //src/python:libovmspython is built. + exit /b 1 + ) + :: Copy pyovms module md dist\windows\ovms\python copy %cd%\bazel-out\x64_windows-opt\bin\src\python\binding\pyovms.pyd dist\windows\ovms\python if !errorlevel! neq 0 exit /b !errorlevel! + :: Copy shared OVMS python runtime library required by ovms.exe when Python is enabled. + copy %cd%\bazel-out\x64_windows-opt\bin\src\python\libovmspython.dll dist\windows\ovms + if !errorlevel! neq 0 exit /b !errorlevel! + :: Prepare self-contained python set "python_version=3.12.10" diff --git a/windows_test.bat b/windows_test.bat index ebd8a8147e..a1ad1ffae2 100644 --- a/windows_test.bat +++ b/windows_test.bat @@ -31,8 +31,12 @@ set "OVMS_MEDIA_URL_ALLOW_REDIRECTS=1" IF "%~2"=="--with_python" ( set "bazelBuildArgs=--config=win_mp_on_py_on --action_env OpenVINO_DIR=%openvino_dir%" + set "testTargets=//src:ovms_test //src:python_runtime_library_test" + set "runPythonRuntimeTest=%cd%\bazel-bin\src\python_runtime_library_test.exe --gtest_filter=!gtestFilter! >> win_full_test.log 2>&1" ) ELSE ( set "bazelBuildArgs=--config=win_mp_on_py_off --action_env OpenVINO_DIR=%openvino_dir%" + set "testTargets=//src:ovms_test" + set "runPythonRuntimeTest=" ) IF "%~3"=="" ( @@ -41,7 +45,7 @@ IF "%~3"=="" ( set "gtestFilter=%3" ) -set "buildTestCommand=bazel %bazelStartupCmd% build %bazelBuildArgs% --jobs=%NUMBER_OF_PROCESSORS% --verbose_failures //src:ovms_test" +set "buildTestCommand=bazel %bazelStartupCmd% build %bazelBuildArgs% --jobs=%NUMBER_OF_PROCESSORS% --verbose_failures %testTargets%" set "changeConfigsCmd=python windows_change_test_configs.py" set "runTest=%cd%\bazel-bin\src\ovms_test.exe --gtest_filter=!gtestFilter! > win_full_test.log 2>&1" @@ -99,6 +103,13 @@ if !errorlevel! neq 0 exit /b !errorlevel! :: Start unit test echo Running: %runTest% %runTest% +if !errorlevel! neq 0 goto :exit_build_error + +IF "%~2"=="--with_python" ( + echo Running: %runPythonRuntimeTest% + %runPythonRuntimeTest% + if !errorlevel! neq 0 goto :exit_build_error +) :: Cut tests log to results set regex="\[ .* ms" @@ -111,6 +122,6 @@ if !errorlevel! equ 0 goto :exit_build_error echo [INFO] Tests finished with no failures. Check the summary in win_test_summary.log. exit /b 0 :exit_build_error -echo [ERROR] Check tests summary in 'win_test_summary.log' and tests logs in 'win_full_test.log'. Rerun failed test with: windows_setupvars.bat and %cd%\bazel-bin\src\ovms_test.exe --gtest_filter='*.*' +echo [ERROR] Check tests summary in 'win_test_summary.log' and tests logs in 'win_full_test.log'. Rerun failed tests with: windows_setupvars.bat and %cd%\bazel-bin\src\ovms_test.exe --gtest_filter='*.*' and %cd%\bazel-bin\src\python_runtime_library_test.exe --gtest_filter='*.*' exit /b 1 endlocal