diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a6332d..6b9dc0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,6 @@ jobs: -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} -DBUILD_TESTING=OFF - -DNETFT_BUILD_CLI=OFF -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/build/cmake-316-install - name: Build and install core run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index ad7bf07..462d2f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ All notable changes to this project are documented in this file. ### Fixed -- Update the pinned native core to netft-cpp 0.3.1 so fail-stop clients do not receive - stalled or backward FT-sequence samples before the corresponding fault. +- Update the pinned native core to netft-cpp 0.3.3, including lifecycle-safe + client destruction from a sample callback. - Exclude the upstream legacy CLI sources from the Python package's private core snapshot. ## 2.1.0 - 2026-07-29 diff --git a/CMakeLists.txt b/CMakeLists.txt index 379d2de..90a9232 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,6 @@ option(PYNETFT_BUILD_TESTING "Build pyNetFT native tests" OFF) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(BUILD_TESTING OFF CACHE BOOL "" FORCE) -set(NETFT_BUILD_CLI OFF CACHE BOOL "" FORCE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) diff --git a/README.md b/README.md index b813413..fcea154 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ pyNetFT is a synchronous, typed Python client for ATI Industrial Automation Net F/T Ethernet force/torque sensors. It discovers the sensor calibration, streams RDT measurements through a native C++ core, and exposes raw counts, physical measurements, health, and recovery information without requiring NumPy. -- **Tested native core:** the protocol and recovery implementation is pinned to [netft-cpp 0.3.1](https://github.com/netft/netft-cpp/releases/tag/v0.3.1) and is exercised with offline fake sensors and native sanitizers. +- **Tested native core:** the protocol and recovery implementation is pinned to [netft-cpp 0.3.3](https://github.com/netft/netft-cpp/releases/tag/v0.3.3) and is exercised with offline fake sensors and native sanitizers. - **Sensor-aware data:** calibration, force and torque units, configuration revisions, sequence progress, and faults remain visible to the application. - **Self-contained typed wheels:** Linux, macOS, and Windows wheels include the native core and a minimal static HTTP-only curl build, plus inline type information for Python 3.10–3.14. diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 7b816fd..b24722c 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,13 +1,12 @@ cmake_minimum_required(VERSION 3.16) -project(netft VERSION 0.3.1 LANGUAGES CXX) +project(netft VERSION 0.3.3 LANGUAGES CXX) include(CTest) include(GNUInstallDirs) include(CMakePackageConfigHelpers) option(NETFT_SANITIZERS "Enable address and undefined behavior sanitizers" OFF) -option(NETFT_BUILD_CLI "Build the netft command-line tool" ON) add_library(netft_sanitizers INTERFACE) if(NETFT_SANITIZERS) @@ -68,17 +67,6 @@ target_link_libraries(netft $ ) -if(NETFT_BUILD_CLI) - add_library(netft_cli_lib STATIC app/cli.cpp) - target_compile_features(netft_cli_lib PUBLIC cxx_std_17) - target_include_directories(netft_cli_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/app) - target_link_libraries(netft_cli_lib PUBLIC netft::netft) - - add_executable(netft_cli app/main.cpp) - set_target_properties(netft_cli PROPERTIES OUTPUT_NAME netft) - target_link_libraries(netft_cli PRIVATE netft_cli_lib) -endif() - install( DIRECTORY include/netft DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} @@ -90,13 +78,6 @@ install( LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) -if(NETFT_BUILD_CLI) - install( - TARGETS netft_cli - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - ) -endif() - set(netft_cmake_install_dir "${CMAKE_INSTALL_LIBDIR}/cmake/netft") configure_package_config_file( cmake/netftConfig.cmake.in diff --git a/core/SNAPSHOT.sha256 b/core/SNAPSHOT.sha256 index 3922b0d..6006ee5 100644 --- a/core/SNAPSHOT.sha256 +++ b/core/SNAPSHOT.sha256 @@ -1,15 +1,15 @@ -c493d94026a5eb53f0589341d33915b8180c34bd712f2f9f2191e41ca48dd8f5 CMakeLists.txt +3db6fdfbb62d6073fa3f77784235cfe2eff06a1f102fb1d0f3a3f6da02dafa01 CMakeLists.txt c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 LICENSE -e78f98877ffe87b93dcec3d30d9c59337d3fa0e2e473ecc7134994a0c3bb7092 UPSTREAM +926aedecaf33e0d9a178360ea455ee9cbdd54537f89d75835409fdb28fdcd908 UPSTREAM 40701f04e67bb18052b6c8f1db60cf5d41baab09e5396f5a88c1eb14ad1a7139 cmake/netftConfig.cmake.in -ebb4c53b7e7e65880580a7649e1c1a142e3a9db1fdec7d108d2d7dde0ad0d36d include/netft/client.hpp +e4a52fe7c7e13de95a29bdbd688d295a4a8ddc1805276274b0c14d4092f7253d include/netft/client.hpp 4f7fb383da5456fd8477e1981f4ff3eba9b82ec3a98f3c395e7d482faf75c112 include/netft/discovery.hpp 1beed1a22ad16aa801e0cceb0950548a32d263e01f260a86e72ec58d8f8d1a74 include/netft/export.hpp 912892bcb8bb5f95392c9c74983d91aab103b7adc5735505b0ac97e1012893b1 include/netft/status.hpp a7c899858aa7a0a28780504f14c1ecdaebb375607119a03500903f3782f45a68 include/netft/types.hpp -e8a68945aaebfd19a1cb1f5cf71f4666d997fa351a41e9adcd137b50a7eb3ae7 src/client.cpp +7199f02fa0fd88059323f507edbf073b2a4827c692d1fd824893cbad8ea9215b src/client.cpp a7e0bcba7a8cd5ccb1ab04658ea735f2b6a3f2bac81c3bce95578e3c14349650 src/detail/client_impl.cpp -5d194b6b6cb8b04e742ee3e1508ac7b8e7972b5bbc7a02c94003d1f402dfccf1 src/detail/client_impl.hpp +b202cc6e250866e681d16b480911f77d865d9187300111f8482bc4a8b0424b34 src/detail/client_impl.hpp ad8744385642693bb4666c428a5dbd0d6eff350b0079a6c9ff720512e89b6a53 src/detail/fault_latch.cpp be488c557e17044948cd62e6de07bf54862d389a584b40b78336479cecdaf23c src/detail/fault_latch.hpp f5953a7fc4fd5eee56f7aa3568fe7417010e3e767cfbb0ee5922ee5f8aa2a9aa src/detail/protocol.cpp diff --git a/core/UPSTREAM b/core/UPSTREAM index 45b22aa..c7bc616 100644 --- a/core/UPSTREAM +++ b/core/UPSTREAM @@ -1,4 +1,4 @@ repository=https://github.com/netft/netft-cpp -tag=v0.3.1 -commit=859eeda8b077093f9bc49d9c1e5506c334647e7b +tag=v0.3.3 +commit=3259b8576b16fb150b51e9ef9090366c5e6efcba paths=CMakeLists.txt,LICENSE,cmake,include,src diff --git a/core/include/netft/client.hpp b/core/include/netft/client.hpp index 4a24373..772a79f 100644 --- a/core/include/netft/client.hpp +++ b/core/include/netft/client.hpp @@ -21,6 +21,10 @@ class NETFT_API Client { using SampleCallback = std::function; explicit Client(Config config); + // Destruction is synchronous except when initiated by the sample callback. + // In that case worker shutdown and storage reclamation are deferred because + // a callback cannot join its own thread. The callback must not access the + // Client after initiating destruction. ~Client(); Client(const Client &) = delete; Client &operator=(const Client &) = delete; diff --git a/core/src/client.cpp b/core/src/client.cpp index e253b28..4f56e79 100644 --- a/core/src/client.cpp +++ b/core/src/client.cpp @@ -1,14 +1,67 @@ #include "netft/client.hpp" +#include +#include +#include +#include #include #include "detail/client_impl.hpp" namespace netft { +namespace { + +template class DeferredDestroyer { +public: + DeferredDestroyer() : worker_(&DeferredDestroyer::run, this) {} + + void enqueue(std::unique_ptr value) { + { + std::scoped_lock lock(mutex_); + pending_.push_back(std::move(value)); + } + condition_.notify_one(); + } + +private: + void run() noexcept { + for (;;) { + std::unique_ptr value; + { + std::unique_lock lock(mutex_); + condition_.wait(lock, [&] { return !pending_.empty(); }); + value = std::move(pending_.front()); + pending_.pop_front(); + } + value->stop(); + } + } + + std::mutex mutex_; + std::condition_variable condition_; + std::deque> pending_; + std::thread worker_; +}; + +template DeferredDestroyer &deferred_destroyer() { + // Callbacks can outlive static teardown. Keeping this worker alive for the + // process lifetime avoids both destruction-order races and lost enqueues. + static auto *instance = new DeferredDestroyer; + return *instance; +} + +} // namespace Client::Client(Config config) : impl_(std::make_unique(std::move(config))) {} -Client::~Client() { stop(); } +Client::~Client() { + if (impl_ && impl_->called_from_worker_thread()) { + impl_->stop(); + deferred_destroyer().enqueue(std::move(impl_)); + return; + } + stop(); +} void Client::start(SampleCallback callback) { impl_->start(std::move(callback)); } diff --git a/core/src/detail/client_impl.hpp b/core/src/detail/client_impl.hpp index 512ea21..77d9ad8 100644 --- a/core/src/detail/client_impl.hpp +++ b/core/src/detail/client_impl.hpp @@ -24,6 +24,11 @@ class NETFT_LOCAL Client::Impl { void start(SampleCallback callback); void stop() noexcept; + [[nodiscard]] bool called_from_worker_thread() const noexcept { + std::scoped_lock lifecycle_lock(lifecycle_mutex_); + return !worker_exited_.load(std::memory_order_acquire) && + active_worker_id_ == std::this_thread::get_id(); + } void bias(); bool wait_for_first_sample(std::chrono::duration timeout); bool faulted() const noexcept;