Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f57f4077fd | ||
|
|
006d1fa1df | ||
|
|
b1e834a8df | ||
|
|
28e4b1bd73 | ||
|
|
35a5d6a66c | ||
|
|
01b19247f1 | ||
|
|
15ea024d48 | ||
|
|
ce5a2614fa | ||
|
|
beda0966db | ||
|
|
1fd5a96561 | ||
|
|
be8c5d01c6 | ||
|
|
97c5c11c25 | ||
|
|
7d660acc05 | ||
|
|
ab71df3ce1 | ||
|
|
37adc03c11 | ||
|
|
3db2ece145 | ||
|
|
1bbb03302a |
@@ -2,6 +2,29 @@
|
||||
Changelog for package rclcpp
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
19.2.0 (2023-02-24)
|
||||
-------------------
|
||||
* to create a sublogger while getting child of Logger (`#1717 <https://github.com/ros2/rclcpp/issues/1717>`_)
|
||||
* Fix documentation of Context class (`#2107 <https://github.com/ros2/rclcpp/issues/2107>`_)
|
||||
* fixes for rmw callbacks in qos_event class (`#2102 <https://github.com/ros2/rclcpp/issues/2102>`_)
|
||||
* Contributors: Alberto Soragna, Chen Lihui, Silvio Traversaro
|
||||
|
||||
19.1.0 (2023-02-14)
|
||||
-------------------
|
||||
* Add support for timers on reset callback (`#1979 <https://github.com/ros2/rclcpp/issues/1979>`_)
|
||||
* Topic node guard condition in executor (`#2074 <https://github.com/ros2/rclcpp/issues/2074>`_)
|
||||
* Fix bug on the disorder of calling shutdown callback (`#2097 <https://github.com/ros2/rclcpp/issues/2097>`_)
|
||||
* Contributors: Barry Xu, Chen Lihui, mauropasse
|
||||
|
||||
19.0.0 (2023-01-30)
|
||||
-------------------
|
||||
* Add default constructor to NodeInterfaces (`#2094 <https://github.com/ros2/rclcpp/issues/2094>`_)
|
||||
* Fix clock state cached time to be a copy, not a reference. (`#2092 <https://github.com/ros2/rclcpp/issues/2092>`_)
|
||||
* Fix -Wmaybe-uninitialized warning (`#2081 <https://github.com/ros2/rclcpp/issues/2081>`_)
|
||||
* Fix the keep_last warning when using system defaults. (`#2082 <https://github.com/ros2/rclcpp/issues/2082>`_)
|
||||
* Add in a fix for older compilers. (`#2075 <https://github.com/ros2/rclcpp/issues/2075>`_)
|
||||
* Contributors: Alexander Hans, Chris Lalancette, Shane Loretz
|
||||
|
||||
18.0.0 (2022-12-29)
|
||||
-------------------
|
||||
* Implement Unified Node Interface (NodeInterfaces class) (`#2041 <https://github.com/ros2/rclcpp/issues/2041>`_)
|
||||
|
||||
@@ -65,8 +65,11 @@ using PreShutdownCallbackHandle = ShutdownCallbackHandle;
|
||||
/// Context which encapsulates shared state between nodes and other similar entities.
|
||||
/**
|
||||
* A context also represents the lifecycle between init and shutdown of rclcpp.
|
||||
* It is often used in conjunction with rclcpp::init, or rclcpp::init_local,
|
||||
* and rclcpp::shutdown.
|
||||
* Nodes may be attached to a particular context by passing to the rclcpp::Node
|
||||
* constructor a rclcpp::NodeOptions instance in which the Context is set via
|
||||
* rclcpp::NodeOptions::context.
|
||||
* Nodes will be automatically removed from the context when destructed.
|
||||
* Contexts may be shutdown by calling rclcpp::shutdown.
|
||||
*/
|
||||
class Context : public std::enable_shared_from_this<Context>
|
||||
{
|
||||
@@ -376,10 +379,10 @@ private:
|
||||
// attempt to acquire another sub context.
|
||||
std::recursive_mutex sub_contexts_mutex_;
|
||||
|
||||
std::unordered_set<std::shared_ptr<OnShutdownCallback>> on_shutdown_callbacks_;
|
||||
std::vector<std::shared_ptr<OnShutdownCallback>> on_shutdown_callbacks_;
|
||||
mutable std::mutex on_shutdown_callbacks_mutex_;
|
||||
|
||||
std::unordered_set<std::shared_ptr<PreShutdownCallback>> pre_shutdown_callbacks_;
|
||||
std::vector<std::shared_ptr<PreShutdownCallback>> pre_shutdown_callbacks_;
|
||||
mutable std::mutex pre_shutdown_callbacks_mutex_;
|
||||
|
||||
/// Condition variable for timed sleep (see sleep_for).
|
||||
@@ -398,20 +401,22 @@ private:
|
||||
|
||||
using ShutdownCallback = ShutdownCallbackHandle::ShutdownCallbackType;
|
||||
|
||||
template<ShutdownType shutdown_type>
|
||||
RCLCPP_LOCAL
|
||||
ShutdownCallbackHandle
|
||||
add_shutdown_callback(
|
||||
ShutdownType shutdown_type,
|
||||
ShutdownCallback callback);
|
||||
|
||||
template<ShutdownType shutdown_type>
|
||||
RCLCPP_LOCAL
|
||||
bool
|
||||
remove_shutdown_callback(
|
||||
ShutdownType shutdown_type,
|
||||
const ShutdownCallbackHandle & callback_handle);
|
||||
|
||||
template<ShutdownType shutdown_type>
|
||||
RCLCPP_LOCAL
|
||||
std::vector<rclcpp::Context::ShutdownCallback>
|
||||
get_shutdown_callback(ShutdownType shutdown_type) const;
|
||||
get_shutdown_callback() const;
|
||||
};
|
||||
|
||||
/// Return a copy of the list of context shared pointers.
|
||||
|
||||
@@ -560,11 +560,20 @@ protected:
|
||||
virtual void
|
||||
spin_once_impl(std::chrono::nanoseconds timeout);
|
||||
|
||||
typedef std::map<rclcpp::node_interfaces::NodeBaseInterface::WeakPtr,
|
||||
const rclcpp::GuardCondition *,
|
||||
std::owner_less<rclcpp::node_interfaces::NodeBaseInterface::WeakPtr>>
|
||||
WeakNodesToGuardConditionsMap;
|
||||
|
||||
typedef std::map<rclcpp::CallbackGroup::WeakPtr,
|
||||
const rclcpp::GuardCondition *,
|
||||
std::owner_less<rclcpp::CallbackGroup::WeakPtr>>
|
||||
WeakCallbackGroupsToGuardConditionsMap;
|
||||
|
||||
/// maps nodes to guard conditions
|
||||
WeakNodesToGuardConditionsMap
|
||||
weak_nodes_to_guard_conditions_ RCPPUTILS_TSA_GUARDED_BY(mutex_);
|
||||
|
||||
/// maps callback groups to guard conditions
|
||||
WeakCallbackGroupsToGuardConditionsMap
|
||||
weak_groups_to_guard_conditions_ RCPPUTILS_TSA_GUARDED_BY(mutex_);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "rclcpp/visibility_control.hpp"
|
||||
|
||||
@@ -122,6 +123,7 @@ private:
|
||||
: name_(new std::string(name)) {}
|
||||
|
||||
std::shared_ptr<const std::string> name_;
|
||||
std::shared_ptr<std::pair<std::string, std::string>> logger_sublogger_pairname_ = nullptr;
|
||||
|
||||
public:
|
||||
RCLCPP_PUBLIC
|
||||
@@ -157,13 +159,7 @@ public:
|
||||
*/
|
||||
RCLCPP_PUBLIC
|
||||
Logger
|
||||
get_child(const std::string & suffix)
|
||||
{
|
||||
if (!name_) {
|
||||
return Logger();
|
||||
}
|
||||
return Logger(*name_ + "." + suffix);
|
||||
}
|
||||
get_child(const std::string & suffix);
|
||||
|
||||
/// Set level for current logger.
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,10 @@ struct NodeInterfacesStorage
|
||||
: interfaces_(init_tuple<decltype(node), InterfaceTs ...>(node))
|
||||
{}
|
||||
|
||||
NodeInterfacesStorage()
|
||||
: interfaces_()
|
||||
{}
|
||||
|
||||
explicit NodeInterfacesStorage(std::shared_ptr<InterfaceTs>... args)
|
||||
: interfaces_(args ...)
|
||||
{}
|
||||
|
||||
@@ -147,10 +147,9 @@ public:
|
||||
: NodeInterfacesSupportsT(node)
|
||||
{}
|
||||
|
||||
/// NodeT::SharedPtr Constructor
|
||||
template<typename NodeT>
|
||||
NodeInterfaces(std::shared_ptr<NodeT> node) // NOLINT(runtime/explicit)
|
||||
: NodeInterfaces(node ? *node : throw std::invalid_argument("given node pointer is nullptr"))
|
||||
// Create a NodeInterfaces object with no bound interfaces
|
||||
NodeInterfaces()
|
||||
: NodeInterfacesSupportsT()
|
||||
{}
|
||||
|
||||
explicit NodeInterfaces(std::shared_ptr<InterfaceTs>... args)
|
||||
|
||||
@@ -80,7 +80,9 @@ struct RCLCPP_PUBLIC QoSInitialization
|
||||
size_t depth;
|
||||
|
||||
/// Constructor which takes both a history policy and a depth (even if it would be unused).
|
||||
QoSInitialization(rmw_qos_history_policy_t history_policy_arg, size_t depth_arg);
|
||||
QoSInitialization(
|
||||
rmw_qos_history_policy_t history_policy_arg, size_t depth_arg,
|
||||
bool print_depth_warning = true);
|
||||
|
||||
/// Create a QoSInitialization from an existing rmw_qos_profile_t, using its history and depth.
|
||||
static
|
||||
@@ -97,7 +99,7 @@ struct RCLCPP_PUBLIC KeepAll : public rclcpp::QoSInitialization
|
||||
/// Use to initialize the QoS with the keep_last history setting and the given depth.
|
||||
struct RCLCPP_PUBLIC KeepLast : public rclcpp::QoSInitialization
|
||||
{
|
||||
explicit KeepLast(size_t depth);
|
||||
explicit KeepLast(size_t depth, bool print_depth_warning = true);
|
||||
};
|
||||
|
||||
/// Encapsulation of Quality of Service settings.
|
||||
|
||||
@@ -214,10 +214,11 @@ protected:
|
||||
void
|
||||
set_on_new_event_callback(rcl_event_callback_t callback, const void * user_data);
|
||||
|
||||
rcl_event_t event_handle_;
|
||||
size_t wait_set_event_index_;
|
||||
std::recursive_mutex callback_mutex_;
|
||||
std::function<void(size_t)> on_new_event_callback_{nullptr};
|
||||
|
||||
rcl_event_t event_handle_;
|
||||
size_t wait_set_event_index_;
|
||||
};
|
||||
|
||||
template<typename EventCallbackT, typename ParentHandleT>
|
||||
|
||||
@@ -149,11 +149,48 @@ public:
|
||||
bool
|
||||
exchange_in_use_by_wait_set_state(bool in_use_state);
|
||||
|
||||
/// Set a callback to be called when the timer is reset
|
||||
/**
|
||||
* You should aim to make this callback fast and not blocking.
|
||||
* If you need to do a lot of work or wait for some other event, you should
|
||||
* spin it off to another thread.
|
||||
*
|
||||
* Calling it again will override any previously set callback.
|
||||
* An exception will be thrown if the callback is not callable.
|
||||
*
|
||||
* This function is thread-safe.
|
||||
*
|
||||
* If you want more information available in the callback,
|
||||
* you may use a lambda with captures or std::bind.
|
||||
*
|
||||
* \param[in] callback functor to be called whenever timer is reset
|
||||
*/
|
||||
RCLCPP_PUBLIC
|
||||
void
|
||||
set_on_reset_callback(std::function<void(size_t)> callback);
|
||||
|
||||
/// Unset the callback registered for reset timer
|
||||
RCLCPP_PUBLIC
|
||||
void
|
||||
clear_on_reset_callback();
|
||||
|
||||
protected:
|
||||
std::recursive_mutex callback_mutex_;
|
||||
// Declare callback before timer_handle_, so on destruction
|
||||
// the callback is destroyed last. Otherwise, the rcl timer
|
||||
// callback would point briefly to a destroyed function.
|
||||
// Clearing the callback on timer destructor also makes sure
|
||||
// the rcl callback is cleared before on_reset_callback_.
|
||||
std::function<void(size_t)> on_reset_callback_{nullptr};
|
||||
|
||||
Clock::SharedPtr clock_;
|
||||
std::shared_ptr<rcl_timer_t> timer_handle_;
|
||||
|
||||
std::atomic<bool> in_use_by_wait_set_{false};
|
||||
|
||||
RCLCPP_PUBLIC
|
||||
void
|
||||
set_on_reset_callback(rcl_event_callback_t callback, const void * user_data);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="2">
|
||||
<name>rclcpp</name>
|
||||
<version>18.0.0</version>
|
||||
<version>19.2.0</version>
|
||||
<description>The ROS client library in C++.</description>
|
||||
|
||||
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>
|
||||
|
||||
@@ -365,49 +365,45 @@ Context::on_shutdown(OnShutdownCallback callback)
|
||||
rclcpp::OnShutdownCallbackHandle
|
||||
Context::add_on_shutdown_callback(OnShutdownCallback callback)
|
||||
{
|
||||
return add_shutdown_callback(ShutdownType::on_shutdown, callback);
|
||||
return add_shutdown_callback<ShutdownType::on_shutdown>(callback);
|
||||
}
|
||||
|
||||
bool
|
||||
Context::remove_on_shutdown_callback(const OnShutdownCallbackHandle & callback_handle)
|
||||
{
|
||||
return remove_shutdown_callback(ShutdownType::on_shutdown, callback_handle);
|
||||
return remove_shutdown_callback<ShutdownType::on_shutdown>(callback_handle);
|
||||
}
|
||||
|
||||
rclcpp::PreShutdownCallbackHandle
|
||||
Context::add_pre_shutdown_callback(PreShutdownCallback callback)
|
||||
{
|
||||
return add_shutdown_callback(ShutdownType::pre_shutdown, callback);
|
||||
return add_shutdown_callback<ShutdownType::pre_shutdown>(callback);
|
||||
}
|
||||
|
||||
bool
|
||||
Context::remove_pre_shutdown_callback(
|
||||
const PreShutdownCallbackHandle & callback_handle)
|
||||
{
|
||||
return remove_shutdown_callback(ShutdownType::pre_shutdown, callback_handle);
|
||||
return remove_shutdown_callback<ShutdownType::pre_shutdown>(callback_handle);
|
||||
}
|
||||
|
||||
template<Context::ShutdownType shutdown_type>
|
||||
rclcpp::ShutdownCallbackHandle
|
||||
Context::add_shutdown_callback(
|
||||
ShutdownType shutdown_type,
|
||||
ShutdownCallback callback)
|
||||
{
|
||||
auto callback_shared_ptr =
|
||||
std::make_shared<ShutdownCallbackHandle::ShutdownCallbackType>(callback);
|
||||
|
||||
switch (shutdown_type) {
|
||||
case ShutdownType::pre_shutdown:
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pre_shutdown_callbacks_mutex_);
|
||||
pre_shutdown_callbacks_.emplace(callback_shared_ptr);
|
||||
}
|
||||
break;
|
||||
case ShutdownType::on_shutdown:
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(on_shutdown_callbacks_mutex_);
|
||||
on_shutdown_callbacks_.emplace(callback_shared_ptr);
|
||||
}
|
||||
break;
|
||||
static_assert(
|
||||
shutdown_type == ShutdownType::pre_shutdown || shutdown_type == ShutdownType::on_shutdown);
|
||||
|
||||
if constexpr (shutdown_type == ShutdownType::pre_shutdown) {
|
||||
std::lock_guard<std::mutex> lock(pre_shutdown_callbacks_mutex_);
|
||||
pre_shutdown_callbacks_.emplace_back(callback_shared_ptr);
|
||||
} else {
|
||||
std::lock_guard<std::mutex> lock(on_shutdown_callbacks_mutex_);
|
||||
on_shutdown_callbacks_.emplace_back(callback_shared_ptr);
|
||||
}
|
||||
|
||||
ShutdownCallbackHandle callback_handle;
|
||||
@@ -415,73 +411,74 @@ Context::add_shutdown_callback(
|
||||
return callback_handle;
|
||||
}
|
||||
|
||||
template<Context::ShutdownType shutdown_type>
|
||||
bool
|
||||
Context::remove_shutdown_callback(
|
||||
ShutdownType shutdown_type,
|
||||
const ShutdownCallbackHandle & callback_handle)
|
||||
{
|
||||
std::mutex * mutex_ptr = nullptr;
|
||||
std::unordered_set<
|
||||
std::shared_ptr<ShutdownCallbackHandle::ShutdownCallbackType>> * callback_list_ptr;
|
||||
|
||||
switch (shutdown_type) {
|
||||
case ShutdownType::pre_shutdown:
|
||||
mutex_ptr = &pre_shutdown_callbacks_mutex_;
|
||||
callback_list_ptr = &pre_shutdown_callbacks_;
|
||||
break;
|
||||
case ShutdownType::on_shutdown:
|
||||
mutex_ptr = &on_shutdown_callbacks_mutex_;
|
||||
callback_list_ptr = &on_shutdown_callbacks_;
|
||||
break;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(*mutex_ptr);
|
||||
auto callback_shared_ptr = callback_handle.callback.lock();
|
||||
const auto callback_shared_ptr = callback_handle.callback.lock();
|
||||
if (callback_shared_ptr == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return callback_list_ptr->erase(callback_shared_ptr) == 1;
|
||||
|
||||
const auto remove_callback = [&callback_shared_ptr](auto & mutex, auto & callback_vector) {
|
||||
const std::lock_guard<std::mutex> lock(mutex);
|
||||
auto iter = callback_vector.begin();
|
||||
for (; iter != callback_vector.end(); iter++) {
|
||||
if ((*iter).get() == callback_shared_ptr.get()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (iter == callback_vector.end()) {
|
||||
return false;
|
||||
}
|
||||
callback_vector.erase(iter);
|
||||
return true;
|
||||
};
|
||||
|
||||
static_assert(
|
||||
shutdown_type == ShutdownType::pre_shutdown || shutdown_type == ShutdownType::on_shutdown);
|
||||
|
||||
if constexpr (shutdown_type == ShutdownType::pre_shutdown) {
|
||||
return remove_callback(pre_shutdown_callbacks_mutex_, pre_shutdown_callbacks_);
|
||||
} else {
|
||||
return remove_callback(on_shutdown_callbacks_mutex_, on_shutdown_callbacks_);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<rclcpp::Context::OnShutdownCallback>
|
||||
Context::get_on_shutdown_callbacks() const
|
||||
{
|
||||
return get_shutdown_callback(ShutdownType::on_shutdown);
|
||||
return get_shutdown_callback<ShutdownType::on_shutdown>();
|
||||
}
|
||||
|
||||
std::vector<rclcpp::Context::PreShutdownCallback>
|
||||
Context::get_pre_shutdown_callbacks() const
|
||||
{
|
||||
return get_shutdown_callback(ShutdownType::pre_shutdown);
|
||||
return get_shutdown_callback<ShutdownType::pre_shutdown>();
|
||||
}
|
||||
|
||||
template<Context::ShutdownType shutdown_type>
|
||||
std::vector<rclcpp::Context::ShutdownCallback>
|
||||
Context::get_shutdown_callback(ShutdownType shutdown_type) const
|
||||
Context::get_shutdown_callback() const
|
||||
{
|
||||
std::mutex * mutex_ptr = nullptr;
|
||||
const std::unordered_set<
|
||||
std::shared_ptr<ShutdownCallbackHandle::ShutdownCallbackType>> * callback_list_ptr;
|
||||
const auto get_callback_vector = [this](auto & mutex, auto & callback_set) {
|
||||
const std::lock_guard<std::mutex> lock(mutex);
|
||||
std::vector<rclcpp::Context::ShutdownCallback> callbacks;
|
||||
for (auto & callback : callback_set) {
|
||||
callbacks.push_back(*callback);
|
||||
}
|
||||
return callbacks;
|
||||
};
|
||||
|
||||
switch (shutdown_type) {
|
||||
case ShutdownType::pre_shutdown:
|
||||
mutex_ptr = &pre_shutdown_callbacks_mutex_;
|
||||
callback_list_ptr = &pre_shutdown_callbacks_;
|
||||
break;
|
||||
case ShutdownType::on_shutdown:
|
||||
mutex_ptr = &on_shutdown_callbacks_mutex_;
|
||||
callback_list_ptr = &on_shutdown_callbacks_;
|
||||
break;
|
||||
static_assert(
|
||||
shutdown_type == ShutdownType::pre_shutdown || shutdown_type == ShutdownType::on_shutdown);
|
||||
|
||||
if constexpr (shutdown_type == ShutdownType::pre_shutdown) {
|
||||
return get_callback_vector(pre_shutdown_callbacks_mutex_, pre_shutdown_callbacks_);
|
||||
} else {
|
||||
return get_callback_vector(on_shutdown_callbacks_mutex_, on_shutdown_callbacks_);
|
||||
}
|
||||
|
||||
std::vector<rclcpp::Context::ShutdownCallback> callbacks;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*mutex_ptr);
|
||||
for (auto & iter : *callback_list_ptr) {
|
||||
callbacks.emplace_back(*iter);
|
||||
}
|
||||
}
|
||||
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
std::shared_ptr<rcl_context_t>
|
||||
|
||||
@@ -112,6 +112,12 @@ Executor::~Executor()
|
||||
}
|
||||
weak_groups_to_guard_conditions_.clear();
|
||||
|
||||
for (const auto & pair : weak_nodes_to_guard_conditions_) {
|
||||
auto guard_condition = pair.second;
|
||||
memory_strategy_->remove_guard_condition(guard_condition);
|
||||
}
|
||||
weak_nodes_to_guard_conditions_.clear();
|
||||
|
||||
// Finalize the wait set.
|
||||
if (rcl_wait_set_fini(&wait_set_) != RCL_RET_OK) {
|
||||
RCUTILS_LOG_ERROR_NAMED(
|
||||
@@ -274,6 +280,10 @@ Executor::add_node(rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_pt
|
||||
}
|
||||
});
|
||||
|
||||
const auto & gc = node_ptr->get_notify_guard_condition();
|
||||
weak_nodes_to_guard_conditions_[node_ptr] = &gc;
|
||||
// Add the node's notify condition to the guard condition handles
|
||||
memory_strategy_->add_guard_condition(gc);
|
||||
weak_nodes_.push_back(node_ptr);
|
||||
}
|
||||
|
||||
@@ -378,6 +388,9 @@ Executor::remove_node(rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node
|
||||
}
|
||||
}
|
||||
|
||||
memory_strategy_->remove_guard_condition(&node_ptr->get_notify_guard_condition());
|
||||
weak_nodes_to_guard_conditions_.erase(node_ptr);
|
||||
|
||||
std::atomic_bool & has_executor = node_ptr->get_associated_with_executor_atomic();
|
||||
has_executor.store(false);
|
||||
}
|
||||
@@ -706,6 +719,12 @@ Executor::wait_for_work(std::chrono::nanoseconds timeout)
|
||||
auto weak_node_ptr = pair.second;
|
||||
if (weak_group_ptr.expired() || weak_node_ptr.expired()) {
|
||||
invalid_group_ptrs.push_back(weak_group_ptr);
|
||||
auto node_guard_pair = weak_nodes_to_guard_conditions_.find(weak_node_ptr);
|
||||
if (node_guard_pair != weak_nodes_to_guard_conditions_.end()) {
|
||||
auto guard_condition = node_guard_pair->second;
|
||||
weak_nodes_to_guard_conditions_.erase(weak_node_ptr);
|
||||
memory_strategy_->remove_guard_condition(guard_condition);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::for_each(
|
||||
|
||||
@@ -12,14 +12,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "rcl_logging_interface/rcl_logging_interface.h"
|
||||
#include "rcl/logging_rosout.h"
|
||||
|
||||
#include "rclcpp/exceptions.hpp"
|
||||
#include "rclcpp/logger.hpp"
|
||||
#include "rclcpp/logging.hpp"
|
||||
|
||||
#include "./logging_mutex.hpp"
|
||||
|
||||
namespace rclcpp
|
||||
{
|
||||
|
||||
@@ -62,6 +67,46 @@ get_logging_directory()
|
||||
return path;
|
||||
}
|
||||
|
||||
Logger
|
||||
Logger::get_child(const std::string & suffix)
|
||||
{
|
||||
if (!name_) {
|
||||
return Logger();
|
||||
}
|
||||
|
||||
rcl_ret_t rcl_ret = RCL_RET_OK;
|
||||
std::shared_ptr<std::recursive_mutex> logging_mutex;
|
||||
logging_mutex = get_global_logging_mutex();
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> guard(*logging_mutex);
|
||||
rcl_ret = rcl_logging_rosout_add_sublogger((*name_).c_str(), suffix.c_str());
|
||||
if (RCL_RET_OK != rcl_ret) {
|
||||
exceptions::throw_from_rcl_error(
|
||||
rcl_ret, "failed to call rcl_logging_rosout_add_sublogger",
|
||||
rcutils_get_error_state(), rcutils_reset_error);
|
||||
}
|
||||
}
|
||||
|
||||
Logger logger(*name_ + RCUTILS_LOGGING_SEPARATOR_STRING + suffix);
|
||||
if (RCL_RET_OK == rcl_ret) {
|
||||
logger.logger_sublogger_pairname_.reset(
|
||||
new std::pair<std::string, std::string>({*name_, suffix}),
|
||||
[logging_mutex](std::pair<std::string, std::string> * logger_sublogger_pairname_ptr) {
|
||||
std::lock_guard<std::recursive_mutex> guard(*logging_mutex);
|
||||
rcl_ret_t rcl_ret = rcl_logging_rosout_remove_sublogger(
|
||||
logger_sublogger_pairname_ptr->first.c_str(),
|
||||
logger_sublogger_pairname_ptr->second.c_str());
|
||||
delete logger_sublogger_pairname_ptr;
|
||||
if (RCL_RET_OK != rcl_ret) {
|
||||
exceptions::throw_from_rcl_error(
|
||||
rcl_ret, "failed to call rcl_logging_rosout_remove_sublogger",
|
||||
rcutils_get_error_state(), rcutils_reset_error);
|
||||
}
|
||||
});
|
||||
}
|
||||
return logger;
|
||||
}
|
||||
|
||||
void
|
||||
Logger::set_level(Level level)
|
||||
{
|
||||
|
||||
@@ -45,9 +45,19 @@ std::string qos_policy_name_from_kind(rmw_qos_policy_kind_t policy_kind)
|
||||
}
|
||||
}
|
||||
|
||||
QoSInitialization::QoSInitialization(rmw_qos_history_policy_t history_policy_arg, size_t depth_arg)
|
||||
QoSInitialization::QoSInitialization(
|
||||
rmw_qos_history_policy_t history_policy_arg, size_t depth_arg,
|
||||
bool print_depth_warning)
|
||||
: history_policy(history_policy_arg), depth(depth_arg)
|
||||
{}
|
||||
{
|
||||
if (history_policy == RMW_QOS_POLICY_HISTORY_KEEP_LAST && depth == 0 && print_depth_warning) {
|
||||
RCLCPP_WARN_ONCE(
|
||||
rclcpp::get_logger(
|
||||
"rclcpp"),
|
||||
"A zero depth with KEEP_LAST doesn't make sense; no data could be stored. "
|
||||
"This will be interpreted as SYSTEM_DEFAULT");
|
||||
}
|
||||
}
|
||||
|
||||
QoSInitialization
|
||||
QoSInitialization::from_rmw(const rmw_qos_profile_t & rmw_qos)
|
||||
@@ -55,8 +65,9 @@ QoSInitialization::from_rmw(const rmw_qos_profile_t & rmw_qos)
|
||||
switch (rmw_qos.history) {
|
||||
case RMW_QOS_POLICY_HISTORY_KEEP_ALL:
|
||||
return KeepAll();
|
||||
case RMW_QOS_POLICY_HISTORY_KEEP_LAST:
|
||||
case RMW_QOS_POLICY_HISTORY_SYSTEM_DEFAULT:
|
||||
return KeepLast(rmw_qos.depth, false);
|
||||
case RMW_QOS_POLICY_HISTORY_KEEP_LAST:
|
||||
case RMW_QOS_POLICY_HISTORY_UNKNOWN:
|
||||
default:
|
||||
return KeepLast(rmw_qos.depth);
|
||||
@@ -67,16 +78,9 @@ KeepAll::KeepAll()
|
||||
: QoSInitialization(RMW_QOS_POLICY_HISTORY_KEEP_ALL, 0)
|
||||
{}
|
||||
|
||||
KeepLast::KeepLast(size_t depth)
|
||||
: QoSInitialization(RMW_QOS_POLICY_HISTORY_KEEP_LAST, depth)
|
||||
KeepLast::KeepLast(size_t depth, bool print_depth_warning)
|
||||
: QoSInitialization(RMW_QOS_POLICY_HISTORY_KEEP_LAST, depth, print_depth_warning)
|
||||
{
|
||||
if (depth == 0) {
|
||||
RCLCPP_WARN_ONCE(
|
||||
rclcpp::get_logger(
|
||||
"rclcpp"),
|
||||
"A zero depth with KEEP_LAST doesn't make sense; no data could be stored."
|
||||
"This will be interpreted as SYSTEM_DEFAULT");
|
||||
}
|
||||
}
|
||||
|
||||
QoS::QoS(
|
||||
@@ -84,15 +88,6 @@ QoS::QoS(
|
||||
const rmw_qos_profile_t & initial_profile)
|
||||
: rmw_qos_profile_(initial_profile)
|
||||
{
|
||||
if (qos_initialization.history_policy == RMW_QOS_POLICY_HISTORY_KEEP_LAST &&
|
||||
qos_initialization.depth == 0)
|
||||
{
|
||||
RCLCPP_WARN_ONCE(
|
||||
rclcpp::get_logger(
|
||||
"rclcpp"),
|
||||
"A zero depth with KEEP_LAST doesn't make sense; no data could be stored."
|
||||
"This will be interpreted as SYSTEM_DEFAULT");
|
||||
}
|
||||
rmw_qos_profile_.history = qos_initialization.history_policy;
|
||||
rmw_qos_profile_.depth = qos_initialization.depth;
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ QOSEventHandlerBase::~QOSEventHandlerBase()
|
||||
// This clearing is not needed for other rclcpp entities like pub/subs, since
|
||||
// they do own the underlying rmw entities, which are destroyed
|
||||
// on their rclcpp destructors, thus no risk of dangling pointers.
|
||||
if (on_new_event_callback_) {
|
||||
clear_on_ready_callback();
|
||||
}
|
||||
clear_on_ready_callback();
|
||||
|
||||
if (rcl_event_fini(&event_handle_) != RCL_RET_OK) {
|
||||
RCUTILS_LOG_ERROR_NAMED(
|
||||
|
||||
@@ -37,7 +37,8 @@ class ClocksState final
|
||||
{
|
||||
public:
|
||||
ClocksState()
|
||||
: logger_(rclcpp::get_logger("rclcpp"))
|
||||
: logger_(rclcpp::get_logger("rclcpp")),
|
||||
last_time_msg_(std::make_shared<builtin_interfaces::msg::Time>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -53,13 +54,8 @@ public:
|
||||
ros_time_active_ = true;
|
||||
|
||||
// Update all attached clocks to zero or last recorded time
|
||||
std::lock_guard<std::mutex> guard(clock_list_lock_);
|
||||
auto time_msg = std::make_shared<builtin_interfaces::msg::Time>();
|
||||
if (last_msg_set_) {
|
||||
time_msg = std::make_shared<builtin_interfaces::msg::Time>(last_msg_set_->clock);
|
||||
}
|
||||
for (auto it = associated_clocks_.begin(); it != associated_clocks_.end(); ++it) {
|
||||
set_clock(time_msg, true, *it);
|
||||
set_clock(last_time_msg_, true, *it);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,11 +97,7 @@ public:
|
||||
std::lock_guard<std::mutex> guard(clock_list_lock_);
|
||||
associated_clocks_.push_back(clock);
|
||||
// Set the clock to zero unless there's a recently received message
|
||||
auto time_msg = std::make_shared<builtin_interfaces::msg::Time>();
|
||||
if (last_msg_set_) {
|
||||
time_msg = std::make_shared<builtin_interfaces::msg::Time>(last_msg_set_->clock);
|
||||
}
|
||||
set_clock(time_msg, ros_time_active_, clock);
|
||||
set_clock(last_time_msg_, ros_time_active_, clock);
|
||||
}
|
||||
|
||||
// Detach a clock
|
||||
@@ -171,7 +163,7 @@ public:
|
||||
// Cache the last clock message received
|
||||
void cache_last_msg(std::shared_ptr<const rosgraph_msgs::msg::Clock> msg)
|
||||
{
|
||||
last_msg_set_ = msg;
|
||||
last_time_msg_ = std::make_shared<builtin_interfaces::msg::Time>(msg->clock);
|
||||
}
|
||||
|
||||
bool are_all_clocks_rcl_ros_time()
|
||||
@@ -199,7 +191,7 @@ private:
|
||||
// This is needed when new clocks are added.
|
||||
bool ros_time_active_{false};
|
||||
// Last set message to be passed to newly registered clocks
|
||||
std::shared_ptr<const rosgraph_msgs::msg::Clock> last_msg_set_;
|
||||
std::shared_ptr<builtin_interfaces::msg::Time> last_time_msg_{nullptr};
|
||||
};
|
||||
|
||||
class TimeSource::NodeState final
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
#include "rclcpp/contexts/default_context.hpp"
|
||||
#include "rclcpp/exceptions.hpp"
|
||||
#include "rmw/impl/cpp/demangle.hpp"
|
||||
|
||||
#include "rclcpp/contexts/default_context.hpp"
|
||||
#include "rclcpp/detail/cpp_callback_trampoline.hpp"
|
||||
#include "rclcpp/exceptions.hpp"
|
||||
#include "rclcpp/logging.hpp"
|
||||
#include "rcutils/logging_macros.h"
|
||||
|
||||
using rclcpp::TimerBase;
|
||||
@@ -71,7 +74,9 @@ TimerBase::TimerBase(
|
||||
}
|
||||
|
||||
TimerBase::~TimerBase()
|
||||
{}
|
||||
{
|
||||
clear_on_reset_callback();
|
||||
}
|
||||
|
||||
void
|
||||
TimerBase::cancel()
|
||||
@@ -96,7 +101,11 @@ TimerBase::is_canceled()
|
||||
void
|
||||
TimerBase::reset()
|
||||
{
|
||||
rcl_ret_t ret = rcl_timer_reset(timer_handle_.get());
|
||||
rcl_ret_t ret = RCL_RET_OK;
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(callback_mutex_);
|
||||
ret = rcl_timer_reset(timer_handle_.get());
|
||||
}
|
||||
if (ret != RCL_RET_OK) {
|
||||
rclcpp::exceptions::throw_from_rcl_error(ret, "Couldn't reset timer");
|
||||
}
|
||||
@@ -138,3 +147,73 @@ TimerBase::exchange_in_use_by_wait_set_state(bool in_use_state)
|
||||
{
|
||||
return in_use_by_wait_set_.exchange(in_use_state);
|
||||
}
|
||||
|
||||
void
|
||||
TimerBase::set_on_reset_callback(std::function<void(size_t)> callback)
|
||||
{
|
||||
if (!callback) {
|
||||
throw std::invalid_argument(
|
||||
"The callback passed to set_on_reset_callback "
|
||||
"is not callable.");
|
||||
}
|
||||
|
||||
auto new_callback =
|
||||
[callback, this](size_t reset_calls) {
|
||||
try {
|
||||
callback(reset_calls);
|
||||
} catch (const std::exception & exception) {
|
||||
RCLCPP_ERROR_STREAM(
|
||||
rclcpp::get_logger("rclcpp"),
|
||||
"rclcpp::TimerBase@" << this <<
|
||||
" caught " << rmw::impl::cpp::demangle(exception) <<
|
||||
" exception in user-provided callback for the 'on reset' callback: " <<
|
||||
exception.what());
|
||||
} catch (...) {
|
||||
RCLCPP_ERROR_STREAM(
|
||||
rclcpp::get_logger("rclcpp"),
|
||||
"rclcpp::TimerBase@" << this <<
|
||||
" caught unhandled exception in user-provided callback " <<
|
||||
"for the 'on reset' callback");
|
||||
}
|
||||
};
|
||||
|
||||
std::lock_guard<std::recursive_mutex> lock(callback_mutex_);
|
||||
|
||||
// Set it temporarily to the new callback, while we replace the old one.
|
||||
// This two-step setting, prevents a gap where the old std::function has
|
||||
// been replaced but rcl hasn't been told about the new one yet.
|
||||
set_on_reset_callback(
|
||||
rclcpp::detail::cpp_callback_trampoline<
|
||||
decltype(new_callback), const void *, size_t>,
|
||||
static_cast<const void *>(&new_callback));
|
||||
|
||||
// Store the std::function to keep it in scope, also overwrites the existing one.
|
||||
on_reset_callback_ = new_callback;
|
||||
|
||||
// Set it again, now using the permanent storage.
|
||||
set_on_reset_callback(
|
||||
rclcpp::detail::cpp_callback_trampoline<
|
||||
decltype(on_reset_callback_), const void *, size_t>,
|
||||
static_cast<const void *>(&on_reset_callback_));
|
||||
}
|
||||
|
||||
void
|
||||
TimerBase::clear_on_reset_callback()
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(callback_mutex_);
|
||||
|
||||
if (on_reset_callback_) {
|
||||
set_on_reset_callback(nullptr, nullptr);
|
||||
on_reset_callback_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
TimerBase::set_on_reset_callback(rcl_event_callback_t callback, const void * user_data)
|
||||
{
|
||||
rcl_ret_t ret = rcl_timer_set_on_reset_callback(timer_handle_.get(), callback, user_data);
|
||||
|
||||
if (ret != RCL_RET_OK) {
|
||||
rclcpp::exceptions::throw_from_rcl_error(ret, "Failed to set timer on reset callback");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,6 +582,9 @@ target_link_libraries(test_logger ${PROJECT_NAME})
|
||||
ament_add_gmock(test_logging test_logging.cpp)
|
||||
target_link_libraries(test_logging ${PROJECT_NAME})
|
||||
|
||||
ament_add_gmock(test_context test_context.cpp)
|
||||
target_link_libraries(test_context ${PROJECT_NAME})
|
||||
|
||||
ament_add_gtest(test_time test_time.cpp
|
||||
APPEND_LIBRARY_DIRS "${append_library_dirs}")
|
||||
if(TARGET test_time)
|
||||
@@ -736,6 +739,12 @@ if(TARGET test_rosout_qos)
|
||||
target_link_libraries(test_rosout_qos ${PROJECT_NAME})
|
||||
endif()
|
||||
|
||||
ament_add_gtest(test_rosout_subscription test_rosout_subscription.cpp)
|
||||
if(TARGET test_rosout_subscription)
|
||||
ament_target_dependencies(test_rosout_subscription "rcl")
|
||||
target_link_libraries(test_rosout_subscription ${PROJECT_NAME})
|
||||
endif()
|
||||
|
||||
ament_add_gtest(test_executor test_executor.cpp
|
||||
APPEND_LIBRARY_DIRS "${append_library_dirs}"
|
||||
TIMEOUT 120)
|
||||
|
||||
@@ -33,6 +33,15 @@ protected:
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(TestNodeInterfaces, default_constructor) {
|
||||
auto node = std::make_shared<rclcpp::Node>("my_node");
|
||||
using rclcpp::node_interfaces::NodeInterfaces;
|
||||
using rclcpp::node_interfaces::NodeBaseInterface;
|
||||
using rclcpp::node_interfaces::NodeGraphInterface;
|
||||
NodeInterfaces<NodeBaseInterface, NodeGraphInterface> interfaces;
|
||||
interfaces = NodeInterfaces<NodeBaseInterface, NodeGraphInterface>(*node);
|
||||
}
|
||||
|
||||
/*
|
||||
Testing NodeInterfaces construction from nodes.
|
||||
*/
|
||||
@@ -44,7 +53,7 @@ TEST_F(TestNodeInterfaces, node_interfaces_nominal) {
|
||||
using rclcpp::node_interfaces::NodeInterfaces;
|
||||
using rclcpp::node_interfaces::NodeBaseInterface;
|
||||
using rclcpp::node_interfaces::NodeGraphInterface;
|
||||
auto node_interfaces = NodeInterfaces<NodeBaseInterface, NodeGraphInterface>(node);
|
||||
auto node_interfaces = NodeInterfaces<NodeBaseInterface, NodeGraphInterface>(*node);
|
||||
}
|
||||
|
||||
// Implicit conversion of rclcpp::Node into function that uses NodeInterfaces of base.
|
||||
@@ -55,7 +64,7 @@ TEST_F(TestNodeInterfaces, node_interfaces_nominal) {
|
||||
auto base_interface = ni.get<NodeBaseInterface>();
|
||||
};
|
||||
|
||||
some_func(node);
|
||||
some_func(*node);
|
||||
}
|
||||
|
||||
// Implicit narrowing of NodeInterfaces into a new interface NodeInterfaces with fewer interfaces.
|
||||
@@ -67,7 +76,7 @@ TEST_F(TestNodeInterfaces, node_interfaces_nominal) {
|
||||
auto base_interface = ni_with_one.get<NodeBaseInterface>();
|
||||
};
|
||||
|
||||
NodeInterfaces<NodeBaseInterface, NodeGraphInterface> ni_with_two(node);
|
||||
NodeInterfaces<NodeBaseInterface, NodeGraphInterface> ni_with_two(*node);
|
||||
|
||||
some_func(ni_with_two);
|
||||
}
|
||||
@@ -102,7 +111,7 @@ TEST_F(TestNodeInterfaces, node_interfaces_standard_interfaces) {
|
||||
rclcpp::node_interfaces::NodeWaitablesInterface,
|
||||
rclcpp::node_interfaces::NodeParametersInterface,
|
||||
rclcpp::node_interfaces::NodeTimeSourceInterface
|
||||
>(node);
|
||||
>(*node);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -134,7 +143,7 @@ TEST_F(TestNodeInterfaces, ni_init) {
|
||||
NodeWaitablesInterface,
|
||||
NodeParametersInterface,
|
||||
NodeTimeSourceInterface
|
||||
>(node);
|
||||
>(*node);
|
||||
|
||||
{
|
||||
auto base = ni.get<NodeBaseInterface>();
|
||||
@@ -198,7 +207,7 @@ TEST_F(TestNodeInterfaces, ni_all_init) {
|
||||
using rclcpp::node_interfaces::NodeParametersInterface;
|
||||
using rclcpp::node_interfaces::NodeTimeSourceInterface;
|
||||
|
||||
auto ni = rclcpp::node_interfaces::NodeInterfaces<ALL_RCLCPP_NODE_INTERFACES>(node);
|
||||
auto ni = rclcpp::node_interfaces::NodeInterfaces<ALL_RCLCPP_NODE_INTERFACES>(*node);
|
||||
|
||||
{
|
||||
auto base = ni.get<NodeBaseInterface>();
|
||||
|
||||
@@ -340,6 +340,53 @@ TYPED_TEST(TestAddCallbackGroupsToExecutor, subscriber_triggered_to_receive_mess
|
||||
EXPECT_TRUE(received_message_future.get());
|
||||
}
|
||||
|
||||
/*
|
||||
* Test callback group created after spin.
|
||||
* A subscriber with a new callback group that created after executor spin not received a message
|
||||
* because the executor can't be triggered while a subscriber created, see
|
||||
* https://github.com/ros2/rclcpp/issues/2067
|
||||
*/
|
||||
TYPED_TEST(TestAddCallbackGroupsToExecutor, callback_group_create_after_spin)
|
||||
{
|
||||
auto node = std::make_shared<rclcpp::Node>("my_node", "/ns");
|
||||
|
||||
// create a publisher to send data
|
||||
rclcpp::QoS qos = rclcpp::QoS(1).reliable().transient_local();
|
||||
rclcpp::Publisher<test_msgs::msg::Empty>::SharedPtr publisher =
|
||||
node->create_publisher<test_msgs::msg::Empty>("topic_name", qos);
|
||||
publisher->publish(test_msgs::msg::Empty());
|
||||
|
||||
// create a thread running an executor
|
||||
rclcpp::executors::SingleThreadedExecutor executor;
|
||||
executor.add_node(node);
|
||||
std::promise<bool> received_message_promise;
|
||||
auto received_message_future = received_message_promise.get_future();
|
||||
rclcpp::FutureReturnCode return_code = rclcpp::FutureReturnCode::TIMEOUT;
|
||||
std::thread executor_thread = std::thread(
|
||||
[&executor, &received_message_future, &return_code]() {
|
||||
return_code = executor.spin_until_future_complete(received_message_future, 5s);
|
||||
});
|
||||
|
||||
// to create a callback group after spin
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
rclcpp::CallbackGroup::SharedPtr cb_grp = node->create_callback_group(
|
||||
rclcpp::CallbackGroupType::MutuallyExclusive);
|
||||
|
||||
// expect the subscriber to receive a message
|
||||
auto sub_callback = [&received_message_promise](test_msgs::msg::Empty::ConstSharedPtr) {
|
||||
received_message_promise.set_value(true);
|
||||
};
|
||||
// create a subscription using the `cb_grp` callback group
|
||||
auto options = rclcpp::SubscriptionOptions();
|
||||
options.callback_group = cb_grp;
|
||||
rclcpp::Subscription<test_msgs::msg::Empty>::SharedPtr subscription =
|
||||
node->create_subscription<test_msgs::msg::Empty>("topic_name", qos, sub_callback, options);
|
||||
|
||||
executor_thread.join();
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::SUCCESS, return_code);
|
||||
EXPECT_TRUE(received_message_future.get());
|
||||
}
|
||||
|
||||
/*
|
||||
* Test removing callback group from executor that its not associated with.
|
||||
*/
|
||||
|
||||
216
rclcpp/test/rclcpp/test_context.cpp
Normal file
216
rclcpp/test/rclcpp/test_context.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
// Copyright 2023 Sony Group 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 <gtest/gtest.h>
|
||||
|
||||
#include "rclcpp/context.hpp"
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
TEST(TestContext, check_pre_shutdown_callback_order) {
|
||||
auto context = std::make_shared<rclcpp::Context>();
|
||||
context->init(0, nullptr);
|
||||
|
||||
int result[4] = {0, 0, 0, 0};
|
||||
int index = 0;
|
||||
|
||||
auto callback1 = [&result, &index]() {
|
||||
result[index] = 1;
|
||||
index++;
|
||||
};
|
||||
auto callback2 = [&result, &index]() {
|
||||
result[index] = 2;
|
||||
index++;
|
||||
};
|
||||
auto callback3 = [&result, &index]() {
|
||||
result[index] = 3;
|
||||
index++;
|
||||
};
|
||||
auto callback4 = [&result, &index]() {
|
||||
result[index] = 4;
|
||||
index++;
|
||||
};
|
||||
|
||||
context->add_pre_shutdown_callback(callback1);
|
||||
context->add_pre_shutdown_callback(callback2);
|
||||
context->add_pre_shutdown_callback(callback3);
|
||||
context->add_pre_shutdown_callback(callback4);
|
||||
|
||||
context->shutdown("for test");
|
||||
|
||||
EXPECT_TRUE(result[0] == 1 && result[1] == 2 && result[2] == 3 && result[3] == 4);
|
||||
}
|
||||
|
||||
TEST(TestContext, check_on_shutdown_callback_order) {
|
||||
auto context = std::make_shared<rclcpp::Context>();
|
||||
context->init(0, nullptr);
|
||||
|
||||
int result[4] = {0, 0, 0, 0};
|
||||
int index = 0;
|
||||
|
||||
auto callback1 = [&result, &index]() {
|
||||
result[index] = 1;
|
||||
index++;
|
||||
};
|
||||
auto callback2 = [&result, &index]() {
|
||||
result[index] = 2;
|
||||
index++;
|
||||
};
|
||||
auto callback3 = [&result, &index]() {
|
||||
result[index] = 3;
|
||||
index++;
|
||||
};
|
||||
auto callback4 = [&result, &index]() {
|
||||
result[index] = 4;
|
||||
index++;
|
||||
};
|
||||
|
||||
context->add_on_shutdown_callback(callback1);
|
||||
context->add_on_shutdown_callback(callback2);
|
||||
context->add_on_shutdown_callback(callback3);
|
||||
context->add_on_shutdown_callback(callback4);
|
||||
|
||||
context->shutdown("for test");
|
||||
|
||||
EXPECT_TRUE(result[0] == 1 && result[1] == 2 && result[2] == 3 && result[3] == 4);
|
||||
}
|
||||
|
||||
TEST(TestContext, check_mixed_register_shutdown_callback_order) {
|
||||
auto context = std::make_shared<rclcpp::Context>();
|
||||
context->init(0, nullptr);
|
||||
|
||||
int result[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||
int index = 0;
|
||||
|
||||
auto callback1 = [&result, &index]() {
|
||||
result[index] = 1;
|
||||
index++;
|
||||
};
|
||||
auto callback2 = [&result, &index]() {
|
||||
result[index] = 2;
|
||||
index++;
|
||||
};
|
||||
auto callback3 = [&result, &index]() {
|
||||
result[index] = 3;
|
||||
index++;
|
||||
};
|
||||
auto callback4 = [&result, &index]() {
|
||||
result[index] = 4;
|
||||
index++;
|
||||
};
|
||||
auto callback5 = [&result, &index]() {
|
||||
result[index] = 5;
|
||||
index++;
|
||||
};
|
||||
auto callback6 = [&result, &index]() {
|
||||
result[index] = 6;
|
||||
index++;
|
||||
};
|
||||
auto callback7 = [&result, &index]() {
|
||||
result[index] = 7;
|
||||
index++;
|
||||
};
|
||||
auto callback8 = [&result, &index]() {
|
||||
result[index] = 8;
|
||||
index++;
|
||||
};
|
||||
|
||||
// Mixed register
|
||||
context->add_pre_shutdown_callback(callback1);
|
||||
context->add_on_shutdown_callback(callback5);
|
||||
context->add_pre_shutdown_callback(callback2);
|
||||
context->add_on_shutdown_callback(callback6);
|
||||
context->add_pre_shutdown_callback(callback3);
|
||||
context->add_on_shutdown_callback(callback7);
|
||||
context->add_pre_shutdown_callback(callback4);
|
||||
context->add_on_shutdown_callback(callback8);
|
||||
|
||||
context->shutdown("for test");
|
||||
|
||||
EXPECT_TRUE(
|
||||
result[0] == 1 && result[1] == 2 && result[2] == 3 && result[3] == 4 &&
|
||||
result[4] == 5 && result[5] == 6 && result[6] == 7 && result[7] == 8);
|
||||
}
|
||||
|
||||
TEST(TestContext, check_pre_shutdown_callback_order_after_del) {
|
||||
auto context = std::make_shared<rclcpp::Context>();
|
||||
context->init(0, nullptr);
|
||||
|
||||
int result[4] = {0, 0, 0, 0};
|
||||
int index = 0;
|
||||
|
||||
auto callback1 = [&result, &index]() {
|
||||
result[index] = 1;
|
||||
index++;
|
||||
};
|
||||
auto callback2 = [&result, &index]() {
|
||||
result[index] = 2;
|
||||
index++;
|
||||
};
|
||||
auto callback3 = [&result, &index]() {
|
||||
result[index] = 3;
|
||||
index++;
|
||||
};
|
||||
auto callback4 = [&result, &index]() {
|
||||
result[index] = 4;
|
||||
index++;
|
||||
};
|
||||
|
||||
context->add_pre_shutdown_callback(callback1);
|
||||
auto callback_handle = context->add_pre_shutdown_callback(callback2);
|
||||
context->add_pre_shutdown_callback(callback3);
|
||||
context->add_pre_shutdown_callback(callback4);
|
||||
|
||||
EXPECT_TRUE(context->remove_pre_shutdown_callback(callback_handle));
|
||||
EXPECT_FALSE(context->remove_pre_shutdown_callback(callback_handle));
|
||||
|
||||
context->shutdown("for test");
|
||||
|
||||
EXPECT_TRUE(result[0] == 1 && result[1] == 3 && result[2] == 4 && result[3] == 0);
|
||||
}
|
||||
|
||||
TEST(TestContext, check_on_shutdown_callback_order_after_del) {
|
||||
auto context = std::make_shared<rclcpp::Context>();
|
||||
context->init(0, nullptr);
|
||||
|
||||
int result[4] = {0, 0, 0, 0};
|
||||
int index = 0;
|
||||
|
||||
auto callback1 = [&result, &index]() {
|
||||
result[index] = 1;
|
||||
index++;
|
||||
};
|
||||
auto callback2 = [&result, &index]() {
|
||||
result[index] = 2;
|
||||
index++;
|
||||
};
|
||||
auto callback3 = [&result, &index]() {
|
||||
result[index] = 3;
|
||||
index++;
|
||||
};
|
||||
auto callback4 = [&result, &index]() {
|
||||
result[index] = 4;
|
||||
index++;
|
||||
};
|
||||
|
||||
context->add_on_shutdown_callback(callback1);
|
||||
auto callback_handle = context->add_on_shutdown_callback(callback2);
|
||||
context->add_on_shutdown_callback(callback3);
|
||||
context->add_on_shutdown_callback(callback4);
|
||||
|
||||
EXPECT_TRUE(context->remove_on_shutdown_callback(callback_handle));
|
||||
EXPECT_FALSE(context->remove_on_shutdown_callback(callback_handle));
|
||||
|
||||
context->shutdown("for test");
|
||||
|
||||
EXPECT_TRUE(result[0] == 1 && result[1] == 3 && result[2] == 4 && result[3] == 0);
|
||||
}
|
||||
180
rclcpp/test/rclcpp/test_rosout_subscription.cpp
Normal file
180
rclcpp/test/rclcpp/test_rosout_subscription.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2021 Open Source Robotics Foundation, Inc.
|
||||
//
|
||||
// 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 <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include "rcl_interfaces/msg/log.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
class TestRosoutSubscription : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
static void SetUpTestCase()
|
||||
{
|
||||
rclcpp::init(0, nullptr);
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
rclcpp::shutdown();
|
||||
}
|
||||
|
||||
void SetUp()
|
||||
{
|
||||
node = std::make_shared<rclcpp::Node>("test_rosout_subscription", "/ns");
|
||||
sub = node->create_subscription<rcl_interfaces::msg::Log>(
|
||||
"/rosout", 10, [this](rcl_interfaces::msg::Log::ConstSharedPtr msg) {
|
||||
if (msg->msg == this->rosout_msg_data &&
|
||||
msg->name == this->rosout_msg_name)
|
||||
{
|
||||
received_msg_promise.set_value(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void TearDown()
|
||||
{
|
||||
node.reset();
|
||||
}
|
||||
|
||||
rclcpp::Node::SharedPtr node;
|
||||
rclcpp::Subscription<rcl_interfaces::msg::Log>::SharedPtr sub;
|
||||
std::promise<bool> received_msg_promise;
|
||||
std::string rosout_msg_data;
|
||||
std::string rosout_msg_name;
|
||||
};
|
||||
|
||||
TEST_F(TestRosoutSubscription, test_rosoutsubscription_getchild) {
|
||||
std::string logger_name = "ns.test_rosout_subscription.child";
|
||||
this->rosout_msg_data = "SOMETHING";
|
||||
this->rosout_msg_name = logger_name;
|
||||
{
|
||||
// before calling get_child of Logger
|
||||
{
|
||||
RCLCPP_INFO(
|
||||
rclcpp::get_logger(logger_name), this->rosout_msg_data.c_str());
|
||||
auto future = received_msg_promise.get_future();
|
||||
auto return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::TIMEOUT, return_code);
|
||||
received_msg_promise = {};
|
||||
}
|
||||
|
||||
rclcpp::Logger child_logger = this->node->get_logger().get_child("child");
|
||||
ASSERT_EQ(child_logger.get_name(), logger_name);
|
||||
|
||||
// after calling get_child of Logger
|
||||
// 1. use child_logger directly
|
||||
{
|
||||
RCLCPP_INFO(child_logger, this->rosout_msg_data.c_str());
|
||||
auto future = received_msg_promise.get_future();
|
||||
auto return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::SUCCESS, return_code);
|
||||
EXPECT_TRUE(future.get());
|
||||
received_msg_promise = {};
|
||||
}
|
||||
|
||||
// 2. use rclcpp::get_logger
|
||||
{
|
||||
RCLCPP_INFO(rclcpp::get_logger(logger_name), this->rosout_msg_data.c_str());
|
||||
auto future = received_msg_promise.get_future();
|
||||
auto return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::SUCCESS, return_code);
|
||||
EXPECT_TRUE(future.get());
|
||||
received_msg_promise = {};
|
||||
}
|
||||
}
|
||||
|
||||
// `child_logger` is end of life, there is no sublogger
|
||||
{
|
||||
RCLCPP_INFO(rclcpp::get_logger(logger_name), this->rosout_msg_data.c_str());
|
||||
auto future = received_msg_promise.get_future();
|
||||
auto return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::TIMEOUT, return_code);
|
||||
received_msg_promise = {};
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TestRosoutSubscription, test_rosoutsubscription_parent_log) {
|
||||
std::string logger_name = "ns.test_rosout_subscription";
|
||||
this->rosout_msg_data = "SOMETHING";
|
||||
this->rosout_msg_name = logger_name;
|
||||
|
||||
rclcpp::Logger logger = this->node->get_logger();
|
||||
ASSERT_EQ(logger.get_name(), logger_name);
|
||||
RCLCPP_INFO(logger, this->rosout_msg_data.c_str());
|
||||
auto future = received_msg_promise.get_future();
|
||||
auto return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::SUCCESS, return_code);
|
||||
EXPECT_TRUE(future.get());
|
||||
received_msg_promise = {};
|
||||
}
|
||||
|
||||
TEST_F(TestRosoutSubscription, test_rosoutsubscription_child_log) {
|
||||
std::string logger_name = "ns.test_rosout_subscription.child1";
|
||||
this->rosout_msg_data = "SOMETHING";
|
||||
this->rosout_msg_name = logger_name;
|
||||
|
||||
rclcpp::Logger logger = this->node->get_logger();
|
||||
RCLCPP_INFO(logger, this->rosout_msg_data.c_str());
|
||||
auto future = received_msg_promise.get_future();
|
||||
auto return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::TIMEOUT, return_code);
|
||||
received_msg_promise = {};
|
||||
|
||||
logger = this->node->get_logger().get_child("child1");
|
||||
RCLCPP_INFO(logger, this->rosout_msg_data.c_str());
|
||||
future = received_msg_promise.get_future();
|
||||
return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::SUCCESS, return_code);
|
||||
EXPECT_TRUE(future.get());
|
||||
received_msg_promise = {};
|
||||
|
||||
logger = this->node->get_logger().get_child("child2");
|
||||
RCLCPP_INFO(logger, this->rosout_msg_data.c_str());
|
||||
future = received_msg_promise.get_future();
|
||||
return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::TIMEOUT, return_code);
|
||||
received_msg_promise = {};
|
||||
|
||||
this->rosout_msg_name = "ns.test_rosout_subscription.child2";
|
||||
RCLCPP_INFO(logger, this->rosout_msg_data.c_str());
|
||||
future = received_msg_promise.get_future();
|
||||
return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::SUCCESS, return_code);
|
||||
EXPECT_TRUE(future.get());
|
||||
received_msg_promise = {};
|
||||
}
|
||||
|
||||
TEST_F(TestRosoutSubscription, test_rosoutsubscription_getchild_hierarchy) {
|
||||
std::string logger_name = "ns.test_rosout_subscription.child.grandchild";
|
||||
this->rosout_msg_data = "SOMETHING";
|
||||
this->rosout_msg_name = logger_name;
|
||||
|
||||
rclcpp::Logger grandchild_logger =
|
||||
this->node->get_logger().get_child("child").get_child("grandchild");
|
||||
ASSERT_EQ(grandchild_logger.get_name(), logger_name);
|
||||
RCLCPP_INFO(grandchild_logger, this->rosout_msg_data.c_str());
|
||||
auto future = received_msg_promise.get_future();
|
||||
auto return_code = rclcpp::spin_until_future_complete(this->node, future, 3s);
|
||||
ASSERT_EQ(rclcpp::FutureReturnCode::SUCCESS, return_code);
|
||||
EXPECT_TRUE(future.get());
|
||||
received_msg_promise = {};
|
||||
}
|
||||
@@ -3,6 +3,15 @@ Changelog for package rclcpp_action
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
19.2.0 (2023-02-24)
|
||||
-------------------
|
||||
|
||||
19.1.0 (2023-02-14)
|
||||
-------------------
|
||||
|
||||
19.0.0 (2023-01-30)
|
||||
-------------------
|
||||
|
||||
18.0.0 (2022-12-29)
|
||||
-------------------
|
||||
* Explicitly set callback type (`#2059 <https://github.com/ros2/rclcpp/issues/2059>`_)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="2">
|
||||
<name>rclcpp_action</name>
|
||||
<version>18.0.0</version>
|
||||
<version>19.2.0</version>
|
||||
<description>Adds action APIs for C++.</description>
|
||||
|
||||
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
Changelog for package rclcpp_components
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
19.2.0 (2023-02-24)
|
||||
-------------------
|
||||
|
||||
19.1.0 (2023-02-14)
|
||||
-------------------
|
||||
|
||||
19.0.0 (2023-01-30)
|
||||
-------------------
|
||||
* Improve component_manager_isolated shutdown (`#2085 <https://github.com/ros2/rclcpp/issues/2085>`_)
|
||||
* Contributors: Michael Carroll
|
||||
|
||||
18.0.0 (2022-12-29)
|
||||
-------------------
|
||||
* Update maintainers (`#2043 <https://github.com/ros2/rclcpp/issues/2043>`_)
|
||||
|
||||
@@ -38,6 +38,16 @@ class ComponentManagerIsolated : public rclcpp_components::ComponentManager
|
||||
{
|
||||
std::shared_ptr<rclcpp::Executor> executor;
|
||||
std::thread thread;
|
||||
std::atomic_bool thread_initialized;
|
||||
|
||||
/// Constructor for the wrapper.
|
||||
/// This is necessary as atomic variables don't have copy/move operators
|
||||
/// implemented so this structure is not copyable/movable by default
|
||||
explicit DedicatedExecutorWrapper(std::shared_ptr<rclcpp::Executor> exec)
|
||||
: executor(exec),
|
||||
thread_initialized(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -59,15 +69,19 @@ protected:
|
||||
void
|
||||
add_node_to_executor(uint64_t node_id) override
|
||||
{
|
||||
DedicatedExecutorWrapper executor_wrapper;
|
||||
auto exec = std::make_shared<ExecutorT>();
|
||||
exec->add_node(node_wrappers_[node_id].get_node_base_interface());
|
||||
executor_wrapper.executor = exec;
|
||||
executor_wrapper.thread = std::thread(
|
||||
[exec]() {
|
||||
|
||||
// Emplace rather than std::move since move operations are deleted for atomics
|
||||
auto result = dedicated_executor_wrappers_.emplace(std::make_pair(node_id, exec));
|
||||
DedicatedExecutorWrapper & wrapper = result.first->second;
|
||||
wrapper.executor = exec;
|
||||
auto & thread_initialized = wrapper.thread_initialized;
|
||||
wrapper.thread = std::thread(
|
||||
[exec, &thread_initialized]() {
|
||||
thread_initialized = true;
|
||||
exec->spin();
|
||||
});
|
||||
dedicated_executor_wrappers_[node_id] = std::move(executor_wrapper);
|
||||
}
|
||||
/// Remove component node from executor model, it's invoked in on_unload_node()
|
||||
/**
|
||||
@@ -90,15 +104,21 @@ private:
|
||||
*/
|
||||
void cancel_executor(DedicatedExecutorWrapper & executor_wrapper)
|
||||
{
|
||||
// We can't immediately call the cancel() API on an executor because if it is not
|
||||
// already spinning, this operation will have no effect.
|
||||
// We rely on the assumption that this class creates executors and then immediately makes them
|
||||
// spin in a separate thread, i.e. the time gap between when the executor is created and when
|
||||
// it starts to spin is small (although it's not negligible).
|
||||
// Verify that the executor thread has begun spinning.
|
||||
// If it has not, then wait until the thread starts to ensure
|
||||
// that cancel() will fully stop the execution
|
||||
//
|
||||
// This prevents a previous race condition that occurs between the
|
||||
// creation of the executor spin thread and cancelling an executor
|
||||
|
||||
while (!executor_wrapper.executor->is_spinning()) {
|
||||
// This is an arbitrarily small delay to avoid busy looping
|
||||
rclcpp::sleep_for(std::chrono::milliseconds(1));
|
||||
if (!executor_wrapper.thread_initialized) {
|
||||
auto context = this->get_node_base_interface()->get_context();
|
||||
|
||||
// Guarantee that either the executor is spinning or we are shutting down.
|
||||
while (!executor_wrapper.executor->is_spinning() && rclcpp::ok(context)) {
|
||||
// This is an arbitrarily small delay to avoid busy looping
|
||||
rclcpp::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
// After the while loop we are sure that the executor is now spinning, so
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="2">
|
||||
<name>rclcpp_components</name>
|
||||
<version>18.0.0</version>
|
||||
<version>19.2.0</version>
|
||||
<description>Package containing tools for dynamically loadable components</description>
|
||||
|
||||
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>
|
||||
|
||||
@@ -3,6 +3,15 @@ Changelog for package rclcpp_lifecycle
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
19.2.0 (2023-02-24)
|
||||
-------------------
|
||||
|
||||
19.1.0 (2023-02-14)
|
||||
-------------------
|
||||
|
||||
19.0.0 (2023-01-30)
|
||||
-------------------
|
||||
|
||||
18.0.0 (2022-12-29)
|
||||
-------------------
|
||||
* Implement Unified Node Interface (NodeInterfaces class) (`#2041 <https://github.com/ros2/rclcpp/issues/2041>`_)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="2">
|
||||
<name>rclcpp_lifecycle</name>
|
||||
<version>18.0.0</version>
|
||||
<version>19.2.0</version>
|
||||
<description>Package containing a prototype for lifecycle implementation</description>
|
||||
|
||||
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>
|
||||
|
||||
Reference in New Issue
Block a user