From b43f534be53df8f061a1272b5f425c95df358887 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Mon, 14 Sep 2026 23:48:04 +0000 Subject: [PATCH] telemetry: clean up Python registry objects with a module capsule - Fixes #325 --- .../robotpy-telemetry/telemetry/src/main.cpp | 10 ++++ .../robotpy-telemetry/tests/test_shutdown.py | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 subprojects/robotpy-telemetry/tests/test_shutdown.py diff --git a/subprojects/robotpy-telemetry/telemetry/src/main.cpp b/subprojects/robotpy-telemetry/telemetry/src/main.cpp index 339450a46..5b74bce84 100644 --- a/subprojects/robotpy-telemetry/telemetry/src/main.cpp +++ b/subprojects/robotpy-telemetry/telemetry/src/main.cpp @@ -1,7 +1,17 @@ #include "rpy/MockTelemetryBackendFunctions.h" #include "semiwrap_init.telemetry._telemetry.hpp" +#include "wpi/telemetry/TelemetryRegistry.hpp" SEMIWRAP_PYBIND11_MODULE(m) { initWrapper(m); wpi::telemetry::python::InitializeMockBackendValueTypes(m); + + static int unused; // the capsule needs something to reference + py::capsule cleanup(&unused, [](void*) { + // Release Python callbacks, backends, and cached entries during module + // teardown, not from C++ static destructors after Python has finalized. + wpi::telemetry::TelemetryRegistry::SetReportWarning(nullptr); + wpi::telemetry::TelemetryRegistry::Reset(); + }); + m.add_object("_cleanup", cleanup); } diff --git a/subprojects/robotpy-telemetry/tests/test_shutdown.py b/subprojects/robotpy-telemetry/tests/test_shutdown.py new file mode 100644 index 000000000..419164328 --- /dev/null +++ b/subprojects/robotpy-telemetry/tests/test_shutdown.py @@ -0,0 +1,46 @@ +import subprocess +import sys +import textwrap + +import pytest + + +@pytest.mark.parametrize( + "setup", + [ + 'telemetry.TelemetryRegistry.register_backend("/test", Backend())', + 'telemetry.TelemetryRegistry.register_backend("/test", Backend()); ' + 'telemetry.log("/test/value", 1)', + 'telemetry.TelemetryRegistry.register_backend("/test", ' + "telemetry.MultiTelemetryBackend([Backend()])); " + 'telemetry.log("/test/value", 1)', + "telemetry.TelemetryRegistry.set_report_warning(lambda path, msg: None)", + ], + ids=["backend", "cached-entry", "multi-backend", "warning-callback"], +) +def test_python_registry_objects_are_safe_at_shutdown(setup): + # A subprocess is necessary: normal test cleanup masks registry objects + # being destroyed by C++ static destructors after Python has finalized. + code = textwrap.dedent(""" + import telemetry + + class Entry(telemetry.TelemetryEntry): + def log_int64(self, value, timestamp): + pass + + class Backend(telemetry.TelemetryBackend): + def get_entry(self, path): + return Entry() + """) + code += "\n" + setup + + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + timeout=30, + ) + + assert result.returncode == 0, result.stdout + result.stderr + # Cleanup must not emit ignored exceptions during interpreter shutdown. + assert result.stderr == ""