diff --git a/CMakeLists.txt b/CMakeLists.txt index 765c792..9f64be8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,6 @@ project(pstack LANGUAGES C CXX VERSION "${PSTACK_VERSION}" ) include (GNUInstallDirs) enable_testing() -add_subdirectory(tests) option(PYTHON3 "Compile with python 3 support" OFF) option(PYTHON2 "Compile with python 2 support" ON) @@ -181,9 +180,14 @@ add_test(NAME procself COMMAND tests/procself) if (PTRACE_TESTS) add_test(NAME suspend COMMAND tests/suspend) endif() +if (Python3_Development_FOUND OR Python2_Development_FOUND) + add_test(NAME python_path_test COMMAND tests/python_path_test) +endif() if (PYTHON3 AND Python3_Development_FOUND) add_test(NAME pydict COMMAND env PSTACK_BIN=${PSTACK_BIN}${CMAKE_CURRENT_SOURCE_DIR}/tests/pydict_test.py) endif() +add_subdirectory(tests) + # for automake and rpmbuild add_custom_target(check COMMAND make test) diff --git a/libpstack/python.h b/libpstack/python.h index 8fbf96c..0638436 100644 --- a/libpstack/python.h +++ b/libpstack/python.h @@ -85,6 +85,7 @@ struct PythonPrinter { }; bool pthreadTidOffset(Procman::Process &proc, size_t *offsetp); PyInterpInfo getPyInterpInfo(Procman::Process &proc); +std::pair getPythonVersionFromFilename(const std::string &filename); template ssize_t pyRefcnt(const T *t); template diff --git a/python.cc b/python.cc index b623fed..22b36b7 100644 --- a/python.cc +++ b/python.cc @@ -19,29 +19,41 @@ getPyInterpInfo(Procman::Process &proc) { if (libpython == nullptr) return PyInterpInfo {nullptr, 0, 0, "", 0}; - std::string filename = libpython->io->filename(); + auto [major, minor] = getPythonVersionFromFilename(libpython->io->filename()); - auto index = filename.find("python"); + if (proc.context.verbose > 0) + *proc.context.debug << "python version is: " << major << "." << minor << std::endl; + + return PyInterpInfo { + libpython, libpythonAddr, interpreterHead, + "v" + std::to_string(major) + "." + std::to_string(minor), + V2HEX(major, minor)}; +} + +std::pair +getPythonVersionFromFilename(const std::string &filename) { + auto lastSlash = filename.rfind('/'); + auto index = filename.find("python", lastSlash == std::string::npos ? 0 : lastSlash); + + if (index == std::string::npos) + throw Exception() << "Could not find 'python' in filename: " << filename; if (filename.length() < index + 9) //index + len("pythonX.Y") - throw Exception() << "Can't parse python version from lib/exec name: " << filename; + throw Exception() << "Can't parse python version from lib/exec name: " + << filename << " (too short)"; char majorChar = filename[index + 6]; char minorChar = filename[index + 8]; if (!isdigit(majorChar) || !isdigit(minorChar)) - throw Exception() << "lib/exec name doesn't match \"*pythonX.Y.*\" format"; + throw Exception() + << "lib/exec name " << filename + << " doesn't match \"*pythonX.Y.*\" format. Found '" + << majorChar << "' and '" << minorChar << "' where digits were expected."; int major = majorChar - '0'; int minor = minorChar - '0'; - - if (proc.context.verbose > 0) - *proc.context.debug << "python version is: " << major << "." << minor << std::endl; - - return PyInterpInfo { - libpython, libpythonAddr, interpreterHead, - "v" + std::to_string(major) + "." + std::to_string(minor), - V2HEX(major, minor)}; + return {major, minor}; } std::tuple diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d2218f9..10e2cf4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,6 +37,10 @@ target_link_libraries(procself dwelf procman) target_link_libraries(suspend dwelf procman) SET_TARGET_PROPERTIES(noreturn PROPERTIES COMPILE_FLAGS "-O2 -g") +if (Python3_Development_FOUND OR Python2_Development_FOUND) + add_executable(python_path_test python_path_test.cc) + target_link_libraries(python_path_test procman dwelf) +endif() add_custom_command( OUTPUT basic-no-unwind-gen diff --git a/tests/python_path_test.cc b/tests/python_path_test.cc new file mode 100644 index 0000000..16f7615 --- /dev/null +++ b/tests/python_path_test.cc @@ -0,0 +1,63 @@ +#include +#include +#include +#include +#include +#include "libpstack/python.h" +#include "libpstack/exception.h" + +// Mock Exception for standalone testing if needed, but we are linking against libpstack/procman +// which contains the Exception class. + +void test_py_version_from_filename(const std::string& path, int expected_major, int expected_minor, bool should_throw) { + try { + auto [major, minor] = pstack::getPythonVersionFromFilename(path); + if (should_throw) { + errx(1, "Expected exception for path: %s, but got %d.%d", path.c_str(), major, minor); + } + if (major != expected_major || minor != expected_minor) { + errx(1, "Failed for path: %s. Expected %d.%d, got %d.%d", path.c_str(), expected_major, expected_minor, major, minor); + } + std::cout << "PASS: " << path << " -> " << major << "." << minor << std::endl; + } catch (const std::exception& e) { + if (!should_throw) { + errx(1, "Unexpected exception for path: %s: %s", path.c_str(), e.what()); + } + std::cout << "PASS: " << path << " -> threw exception as expected (" << e.what() << ")" << std::endl; + } +} + +int main() { + // Standard cases + test_py_version_from_filename("/usr/lib/libpython3.9.so", 3, 9, false); + test_py_version_from_filename("/usr/lib/python3.9/config-3.9-x86_64-linux-gnu/libpython3.9.so", 3, 9, false); + test_py_version_from_filename("libpython2.7.so", 2, 7, false); + // Complex paths (Bazel-like) + test_py_version_from_filename("/execroot/_main/bazel-out/k8-opt/bin/Foo/Foo.runfiles/+_repo_rules+RPM/python39/_python3.9_dc/libpython3.9.so.1.0", 3, 9, false); + + // Filename only cases + test_py_version_from_filename("python3.9", 3, 9, false); + test_py_version_from_filename("python3.9.so", 3, 9, false); + + // Additional separators (should be handled by standard path logic, but good to check) + test_py_version_from_filename("/usr//lib//python3.8//libpython3.8.so", 3, 8, false); + + // Edge cases for length and format + // Too short + test_py_version_from_filename("/usr/bin/python", 0, 0, true); + // Too short for X.Y logic + test_py_version_from_filename("/usr/bin/python3", 0, 0, true); + // Missing dot/digit at expected pos + test_py_version_from_filename("libpython39.so", 0, 0, true); + + // Directory contains "python", filename does not + test_py_version_from_filename("/opt/python/bin/my_app", 0, 0, true); + + // other invalid names + test_py_version_from_filename("/usr/lib/libpython.so", 0, 0, true); + test_py_version_from_filename("not_a_python_lib.so", 0, 0, true); + // Ends in slash, no filename python + test_py_version_from_filename("/usr/lib/python3.9/", 0, 0, true); + + return 0; +}