• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

BehaviorTree / BehaviorTree.CPP / 21685310357

04 Feb 2026 07:27PM UTC coverage: 78.018% (-1.8%) from 79.835%
21685310357

Pull #1109

github

web-flow
Merge 1450df827 into facf5a0f4
Pull Request #1109: Fix clang-tidy warnings across tests, examples, and samples

0 of 2 new or added lines in 1 file covered. (0.0%)

107 existing lines in 2 files now uncovered.

4614 of 5914 relevant lines covered (78.02%)

22473.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/loggers/groot2_publisher.cpp
1
#include "behaviortree_cpp/loggers/groot2_publisher.h"
2

3
#include "zmq_addon.hpp"
4

5
#include "behaviortree_cpp/loggers/groot2_protocol.h"
6
#include "behaviortree_cpp/xml_parsing.h"
7

8
#include <tuple>
9

10
namespace BT
11
{
12
//------------------------------------------------------
13

14
enum
15
{
16
  IDLE_FROM_SUCCESS = 10 + static_cast<int>(NodeStatus::SUCCESS),
17
  IDLE_FROM_FAILURE = 10 + static_cast<int>(NodeStatus::FAILURE),
18
  IDLE_FROM_RUNNING = 10 + static_cast<int>(NodeStatus::RUNNING)
19
};
20

21
struct Transition
22
{
23
  // when serializing, we will remove the initial time and serialize only
24
  // 6 bytes, instead of 8
25
  uint64_t timestamp_usec;
26
  // if you have more than 64.000 nodes, you are doing something wrong :)
27
  uint16_t node_uid;
28
  // enough bits to contain NodeStatus
29
  uint8_t status;
30

31
  uint8_t padding[5];
32
};
33

34
namespace
35
{
UNCOV
36
std::array<char, 16> CreateRandomUUID()
×
37
{
UNCOV
38
  std::random_device rd;
×
UNCOV
39
  std::mt19937 gen(rd());
×
UNCOV
40
  std::uniform_int_distribution<uint32_t> dist;
×
UNCOV
41
  std::array<char, 16> out{};
×
UNCOV
42
  char* bytes = out.data();
×
UNCOV
43
  for(int i = 0; i < 16; i += 4)
×
44
  {
45
    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
UNCOV
46
    *reinterpret_cast<uint32_t*>(bytes + i) = dist(gen);
×
47
  }
48
  // variant must be 10xxxxxx
UNCOV
49
  bytes[8] &= static_cast<char>(0xBF);
×
UNCOV
50
  bytes[8] |= static_cast<char>(0x80);
×
51

52
  // version must be 0100xxxx
UNCOV
53
  bytes[6] &= 0x4F;
×
UNCOV
54
  bytes[6] |= 0x40;
×
55

UNCOV
56
  return out;
×
UNCOV
57
}
×
58
}  // namespace
59

60
struct Groot2Publisher::PImpl
61
{
UNCOV
62
  PImpl() : context(), server(context, ZMQ_REP), publisher(context, ZMQ_PUB)
×
63
  {
UNCOV
64
    server.set(zmq::sockopt::linger, 0);
×
UNCOV
65
    publisher.set(zmq::sockopt::linger, 0);
×
66

UNCOV
67
    const int timeout_rcv = 100;
×
UNCOV
68
    server.set(zmq::sockopt::rcvtimeo, timeout_rcv);
×
UNCOV
69
    publisher.set(zmq::sockopt::rcvtimeo, timeout_rcv);
×
70

UNCOV
71
    const int timeout_ms = 1000;
×
UNCOV
72
    server.set(zmq::sockopt::sndtimeo, timeout_ms);
×
UNCOV
73
    publisher.set(zmq::sockopt::sndtimeo, timeout_ms);
×
UNCOV
74
  }
×
75

76
  unsigned server_port = 0;
77
  std::string server_address;
78
  std::string publisher_address;
79

80
  std::string tree_xml;
81

82
  std::atomic_bool active_server = true;
83
  std::thread server_thread;
84

85
  std::mutex status_mutex;
86

87
  std::string status_buffer;
88
  // each element of this map points to a character in _p->status_buffer
89
  std::unordered_map<uint16_t, char*> status_buffermap;
90

91
  // weak reference to the tree.
92
  std::unordered_map<std::string, std::weak_ptr<BT::Tree::Subtree>> subtrees;
93
  std::unordered_map<uint16_t, std::weak_ptr<BT::TreeNode>> nodes_by_uid;
94

95
  std::mutex hooks_map_mutex;
96
  std::unordered_map<uint16_t, Monitor::Hook::Ptr> pre_hooks;
97
  std::unordered_map<uint16_t, Monitor::Hook::Ptr> post_hooks;
98

99
  std::mutex last_heartbeat_mutex;
100
  std::chrono::steady_clock::time_point last_heartbeat;
UNCOV
101
  std::chrono::milliseconds max_heartbeat_delay = std::chrono::milliseconds(5000);
×
102

103
  std::atomic_bool recording = false;
104
  std::deque<Transition> transitions_buffer;
105
  std::chrono::microseconds recording_fist_time{};
106

107
  std::thread heartbeat_thread;
108

109
  zmq::context_t context;
110
  zmq::socket_t server;
111
  zmq::socket_t publisher;
112
};
113

UNCOV
114
Groot2Publisher::Groot2Publisher(const BT::Tree& tree, unsigned server_port)
×
UNCOV
115
  : StatusChangeLogger(tree.rootNode()), _p(new PImpl())
×
116
{
UNCOV
117
  _p->server_port = server_port;
×
UNCOV
118
  _p->tree_xml = WriteTreeToXML(tree, true, true);
×
119

120
  //-------------------------------
121
  // Prepare the status buffer
UNCOV
122
  size_t node_count = 0;
×
UNCOV
123
  for(const auto& subtree : tree.subtrees)
×
124
  {
UNCOV
125
    node_count += subtree->nodes.size();
×
126
  }
UNCOV
127
  _p->status_buffer.resize(3 * node_count);
×
128

UNCOV
129
  unsigned ptr_offset = 0;
×
UNCOV
130
  char* buffer_ptr = _p->status_buffer.data();
×
131

UNCOV
132
  for(const auto& subtree : tree.subtrees)
×
133
  {
134
    auto name =
UNCOV
135
        subtree->instance_name.empty() ? subtree->tree_ID : subtree->instance_name;
×
UNCOV
136
    _p->subtrees.insert({ name, subtree });
×
137

UNCOV
138
    for(const auto& node : subtree->nodes)
×
139
    {
UNCOV
140
      _p->nodes_by_uid.insert({ node->UID(), node });
×
141

UNCOV
142
      ptr_offset += Monitor::Serialize(buffer_ptr, ptr_offset, node->UID());
×
UNCOV
143
      _p->status_buffermap.insert({ node->UID(), buffer_ptr + ptr_offset });
×
UNCOV
144
      ptr_offset += Monitor::Serialize(buffer_ptr, ptr_offset, uint8_t(NodeStatus::IDLE));
×
145
    }
UNCOV
146
  }
×
147
  //-------------------------------
UNCOV
148
  _p->server_address = StrCat("tcp://*:", std::to_string(server_port));
×
UNCOV
149
  _p->publisher_address = StrCat("tcp://*:", std::to_string(server_port + 1));
×
150

UNCOV
151
  _p->server.bind(_p->server_address.c_str());
×
UNCOV
152
  _p->publisher.bind(_p->publisher_address.c_str());
×
153

UNCOV
154
  _p->server_thread = std::thread(&Groot2Publisher::serverLoop, this);
×
UNCOV
155
  _p->heartbeat_thread = std::thread(&Groot2Publisher::heartbeatLoop, this);
×
UNCOV
156
}
×
157

158
void Groot2Publisher::setMaxHeartbeatDelay(std::chrono::milliseconds delay)
×
159
{
160
  _p->max_heartbeat_delay = delay;
×
161
}
×
162

163
std::chrono::milliseconds Groot2Publisher::maxHeartbeatDelay() const
×
164
{
165
  return _p->max_heartbeat_delay;
×
166
}
167

UNCOV
168
Groot2Publisher::~Groot2Publisher()
×
169
{
170
  // First, signal threads to stop
UNCOV
171
  _p->active_server = false;
×
172

173
  // Shutdown the ZMQ context to unblock any recv() calls immediately.
174
  // This prevents waiting for the recv timeout (100ms) before threads can exit.
175
  // Context shutdown will cause all blocking operations to return with ETERM error.
UNCOV
176
  _p->context.shutdown();
×
177

178
  // Now join the threads - they should exit quickly
UNCOV
179
  if(_p->server_thread.joinable())
×
180
  {
UNCOV
181
    _p->server_thread.join();
×
182
  }
183

UNCOV
184
  if(_p->heartbeat_thread.joinable())
×
185
  {
UNCOV
186
    _p->heartbeat_thread.join();
×
187
  }
188

189
  // Remove hooks after threads are stopped to avoid race conditions
UNCOV
190
  removeAllHooks();
×
191

UNCOV
192
  flush();
×
193

194
  // Explicitly close sockets before context is destroyed.
195
  // This ensures proper cleanup on all platforms, especially Windows.
NEW
196
  _p->server.close();
×
NEW
197
  _p->publisher.close();
×
UNCOV
198
}
×
199

UNCOV
200
void Groot2Publisher::callback(Duration ts, const TreeNode& node, NodeStatus prev_status,
×
201
                               NodeStatus new_status)
202
{
UNCOV
203
  const std::unique_lock<std::mutex> lk(_p->status_mutex);
×
UNCOV
204
  auto status = static_cast<char>(new_status);
×
205

UNCOV
206
  if(new_status == NodeStatus::IDLE)
×
207
  {
208
    status = static_cast<char>(10 + static_cast<int>(prev_status));
×
209
  }
UNCOV
210
  *(_p->status_buffermap.at(node.UID())) = status;
×
211

UNCOV
212
  if(_p->recording)
×
213
  {
214
    Transition trans{};
×
215
    trans.node_uid = node.UID();
×
216
    trans.status = static_cast<uint8_t>(new_status);
×
217
    auto timestamp = ts - _p->recording_fist_time;
×
218
    trans.timestamp_usec =
×
219
        std::chrono::duration_cast<std::chrono::microseconds>(timestamp).count();
×
220
    _p->transitions_buffer.push_back(trans);
×
221
    while(_p->transitions_buffer.size() > 1000)
×
222
    {
223
      _p->transitions_buffer.pop_front();
×
224
    }
225
  }
UNCOV
226
}
×
227

UNCOV
228
void Groot2Publisher::flush()
×
229
{
230
  // nothing to do here...
UNCOV
231
}
×
232

UNCOV
233
void Groot2Publisher::serverLoop()
×
234
{
UNCOV
235
  auto const serialized_uuid = CreateRandomUUID();
×
236

UNCOV
237
  auto& socket = _p->server;
×
238

239
  auto sendErrorReply = [&socket](const std::string& msg) {
×
240
    zmq::multipart_t error_msg;
×
241
    error_msg.addstr("error");
×
242
    error_msg.addstr(msg);
×
243
    error_msg.send(socket);
×
244
  };
×
245

246
  // initialize _p->last_heartbeat
247
  {
UNCOV
248
    const std::unique_lock lk(_p->last_heartbeat_mutex);
×
UNCOV
249
    _p->last_heartbeat = std::chrono::steady_clock::now();
×
UNCOV
250
  }
×
251

UNCOV
252
  while(_p->active_server)
×
253
  {
UNCOV
254
    zmq::multipart_t requestMsg;
×
255
    try
256
    {
UNCOV
257
      if(!requestMsg.recv(socket) || requestMsg.size() == 0)
×
258
      {
259
        continue;
×
260
      }
261
    }
UNCOV
262
    catch(const zmq::error_t&)
×
263
    {
264
      // Context was terminated or socket error - exit the loop
UNCOV
265
      break;
×
UNCOV
266
    }
×
267

268
    // this heartbeat will help establishing if Groot is connected or not
269
    {
270
      const std::unique_lock lk(_p->last_heartbeat_mutex);
×
271
      _p->last_heartbeat = std::chrono::steady_clock::now();
×
272
    }
×
273

274
    std::string const request_str = requestMsg[0].to_string();
×
275
    if(request_str.size() != Monitor::RequestHeader::size())
×
276
    {
277
      sendErrorReply("wrong request header");
×
278
      continue;
×
279
    }
280

281
    auto request_header = Monitor::DeserializeRequestHeader(request_str);
×
282

283
    Monitor::ReplyHeader reply_header;
×
284
    reply_header.request = request_header;
×
285
    reply_header.request.protocol = Monitor::kProtocolID;
×
286
    reply_header.tree_id = serialized_uuid;
×
287

288
    zmq::multipart_t reply_msg;
×
289
    reply_msg.addstr(Monitor::SerializeHeader(reply_header));
×
290

291
    switch(request_header.type)
×
292
    {
293
      case Monitor::RequestType::FULLTREE: {
×
294
        reply_msg.addstr(_p->tree_xml);
×
295
      }
296
      break;
×
297

298
      case Monitor::RequestType::STATUS: {
×
299
        const std::unique_lock<std::mutex> lk(_p->status_mutex);
×
300
        reply_msg.addstr(_p->status_buffer);
×
301
      }
×
302
      break;
×
303

304
      case Monitor::RequestType::BLACKBOARD: {
×
305
        if(requestMsg.size() != 2)
×
306
        {
307
          sendErrorReply("must be 2 parts message");
×
308
          continue;
×
309
        }
310
        std::string const bb_names_str = requestMsg[1].to_string();
×
311
        auto msg = generateBlackboardsDump(bb_names_str);
×
312
        reply_msg.addmem(msg.data(), msg.size());
×
313
      }
×
314
      break;
×
315

316
      case Monitor::RequestType::HOOK_INSERT: {
×
317
        if(requestMsg.size() != 2)
×
318
        {
319
          sendErrorReply("must be 2 parts message");
×
320
          continue;
×
321
        }
322

323
        auto InsertHook = [this](nlohmann::json const& json) {
×
324
          uint16_t const node_uid = json["uid"].get<uint16_t>();
×
325
          Position const pos = static_cast<Position>(json["position"].get<int>());
×
326

327
          if(auto hook = getHook(pos, node_uid))
×
328
          {
329
            std::unique_lock<std::mutex> lk(hook->mutex);
×
330
            const bool was_interactive = (hook->mode == Monitor::Hook::Mode::BREAKPOINT);
×
331
            BT::Monitor::from_json(json, *hook);
×
332

333
            // if it WAS interactive and it is not anymore, unlock it
334
            if(was_interactive && (hook->mode == Monitor::Hook::Mode::REPLACE))
×
335
            {
336
              hook->ready = true;
×
337
              lk.unlock();
×
338
              hook->wakeup.notify_all();
×
339
            }
340
          }
×
341
          else  // if not found, create a new one
342
          {
343
            auto new_hook = std::make_shared<Monitor::Hook>();
×
344
            BT::Monitor::from_json(json, *new_hook);
×
345
            insertHook(new_hook);
×
346
          }
×
347
        };
×
348

349
        auto const received_json = nlohmann::json::parse(requestMsg[1].to_string());
×
350

351
        // the json may contain a Hook or an array of Hooks
352
        if(received_json.is_array())
×
353
        {
354
          for(auto const& json : received_json)
×
355
          {
356
            InsertHook(json);
×
357
          }
358
        }
359
        else
360
        {
361
          InsertHook(received_json);
×
362
        }
363
      }
×
364
      break;
×
365

366
      case Monitor::RequestType::BREAKPOINT_UNLOCK: {
×
367
        if(requestMsg.size() != 2)
×
368
        {
369
          sendErrorReply("must be 2 parts message");
×
370
          continue;
×
371
        }
372

373
        auto json = nlohmann::json::parse(requestMsg[1].to_string());
×
374
        const uint16_t node_uid = json.at("uid").get<uint16_t>();
×
375
        const std::string status_str = json.at("desired_status").get<std::string>();
×
376
        auto position = static_cast<Position>(json.at("position").get<int>());
×
377
        const bool remove = json.at("remove_when_done").get<bool>();
×
378

379
        NodeStatus desired_status = NodeStatus::SKIPPED;
×
380
        if(status_str == "SUCCESS")
×
381
        {
382
          desired_status = NodeStatus::SUCCESS;
×
383
        }
384
        else if(status_str == "FAILURE")
×
385
        {
386
          desired_status = NodeStatus::FAILURE;
×
387
        }
388

389
        if(!unlockBreakpoint(position, node_uid, desired_status, remove))
×
390
        {
391
          sendErrorReply("Node ID not found");
×
392
          continue;
×
393
        }
394
      }
×
395
      break;
×
396

397
      case Monitor::RequestType::REMOVE_ALL_HOOKS: {
×
398
        removeAllHooks();
×
399
      }
400
      break;
×
401

402
      case Monitor::RequestType::DISABLE_ALL_HOOKS: {
×
403
        enableAllHooks(false);
×
404
      }
405
      break;
×
406

407
      case Monitor::RequestType::HOOK_REMOVE: {
×
408
        if(requestMsg.size() != 2)
×
409
        {
410
          sendErrorReply("must be 2 parts message");
×
411
          continue;
×
412
        }
413

414
        auto json = nlohmann::json::parse(requestMsg[1].to_string());
×
415
        const uint16_t node_uid = json.at("uid").get<uint16_t>();
×
416
        auto position = static_cast<Position>(json.at("position").get<int>());
×
417

418
        if(!removeHook(position, node_uid))
×
419
        {
420
          sendErrorReply("Node ID not found");
×
421
          continue;
×
422
        }
423
      }
×
424
      break;
×
425

426
      case Monitor::RequestType::HOOKS_DUMP: {
×
427
        const std::unique_lock<std::mutex> lk(_p->hooks_map_mutex);
×
428
        auto json_out = nlohmann::json::array();
×
429
        for(const auto& [node_uid, breakpoint] : _p->pre_hooks)
×
430
        {
431
          std::ignore = node_uid;  // unused in this loop
×
432
          json_out.push_back(*breakpoint);
×
433
        }
434
        reply_msg.addstr(json_out.dump());
×
435
      }
×
436
      break;
×
437

438
      case Monitor::RequestType::TOGGLE_RECORDING: {
×
439
        if(requestMsg.size() != 2)
×
440
        {
441
          sendErrorReply("must be 2 parts message");
×
442
          continue;
×
443
        }
444

445
        auto const cmd = (requestMsg[1].to_string());
×
446
        if(cmd == "start")
×
447
        {
448
          _p->recording = true;
×
449
          // to keep the first time for callback
450
          _p->recording_fist_time = std::chrono::duration_cast<std::chrono::microseconds>(
×
451
              std::chrono::high_resolution_clock::now().time_since_epoch());
×
452
          // to send consistent time for client
453
          auto now = std::chrono::duration_cast<std::chrono::microseconds>(
×
454
              std::chrono::system_clock::now().time_since_epoch());
×
455
          reply_msg.addstr(std::to_string(now.count()));
×
456
          const std::unique_lock<std::mutex> lk(_p->status_mutex);
×
457
          _p->transitions_buffer.clear();
×
458
        }
×
459
        else if(cmd == "stop")
×
460
        {
461
          _p->recording = false;
×
462
        }
463
      }
×
464
      break;
×
465

466
      case Monitor::RequestType::GET_TRANSITIONS: {
×
467
        thread_local std::string trans_buffer;
×
468
        trans_buffer.resize(9 * _p->transitions_buffer.size());
×
469

470
        const std::unique_lock<std::mutex> lk(_p->status_mutex);
×
471
        size_t offset = 0;
×
472
        for(const auto& trans : _p->transitions_buffer)
×
473
        {
474
          std::memcpy(&trans_buffer[offset], &trans.timestamp_usec, 6);
×
475
          offset += 6;
×
476
          std::memcpy(&trans_buffer[offset], &trans.node_uid, 2);
×
477
          offset += 2;
×
478
          std::memcpy(&trans_buffer[offset], &trans.status, 1);
×
479
          offset += 1;
×
480
        }
481
        _p->transitions_buffer.clear();
×
482
        trans_buffer.resize(offset);
×
483
        reply_msg.addstr(trans_buffer);
×
484
      }
×
485
      break;
×
486

487
      default: {
×
488
        sendErrorReply("Request not recognized");
×
489
        continue;
×
490
      }
491
    }
×
492
    // send the reply
493
    try
494
    {
495
      reply_msg.send(socket);
×
496
    }
497
    catch(const zmq::error_t&)
×
498
    {
499
      // Context was terminated or socket error - exit the loop
500
      break;
×
501
    }
×
UNCOV
502
  }
×
UNCOV
503
}
×
504

505
void BT::Groot2Publisher::enableAllHooks(bool enable)
×
506
{
507
  const std::unique_lock<std::mutex> lk(_p->hooks_map_mutex);
×
508
  for(const auto& [node_uid, hook] : _p->pre_hooks)
×
509
  {
510
    std::ignore = node_uid;  // unused in this loop
×
511
    std::unique_lock<std::mutex> lk(hook->mutex);
×
512
    hook->enabled = enable;
×
513
    // when disabling, remember to wake up blocked ones
514
    if(!hook->enabled && hook->mode == Monitor::Hook::Mode::BREAKPOINT)
×
515
    {
516
      lk.unlock();
×
517
      hook->wakeup.notify_all();
×
518
    }
519
  }
×
520
}
×
521

UNCOV
522
void Groot2Publisher::heartbeatLoop()
×
523
{
UNCOV
524
  bool has_heartbeat = true;
×
525

UNCOV
526
  while(_p->active_server)
×
527
  {
UNCOV
528
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
×
529

UNCOV
530
    auto now = std::chrono::steady_clock::now();
×
UNCOV
531
    const bool prev_heartbeat = has_heartbeat;
×
532

533
    {
UNCOV
534
      const std::unique_lock lk(_p->last_heartbeat_mutex);
×
UNCOV
535
      has_heartbeat = (now - _p->last_heartbeat < _p->max_heartbeat_delay);
×
UNCOV
536
    }
×
537

538
    // if we loose or gain heartbeat, disable/enable all breakpoints
UNCOV
539
    if(has_heartbeat != prev_heartbeat)
×
540
    {
541
      enableAllHooks(has_heartbeat);
×
542
    }
543
  }
UNCOV
544
}
×
545

546
std::vector<uint8_t> Groot2Publisher::generateBlackboardsDump(const std::string& bb_list)
×
547
{
548
  auto json = nlohmann::json();
×
549
  auto const bb_names = BT::splitString(bb_list, ';');
×
550
  for(auto name : bb_names)
×
551
  {
552
    std::string const bb_name(name);
×
553
    auto it = _p->subtrees.find(bb_name);
×
554

555
    if(it != _p->subtrees.end())
×
556
    {
557
      // lock the weak pointer
558
      if(auto subtree = it->second.lock())
×
559
      {
560
        json[bb_name] = ExportBlackboardToJSON(*subtree->blackboard);
×
561
      }
×
562
    }
563
  }
×
564
  return nlohmann::json::to_msgpack(json);
×
565
}
×
566

567
bool Groot2Publisher::insertHook(std::shared_ptr<Monitor::Hook> hook)
×
568
{
569
  auto const node_uid = hook->node_uid;
×
570
  auto it = _p->nodes_by_uid.find(node_uid);
×
571
  if(it == _p->nodes_by_uid.end())
×
572
  {
573
    return false;
×
574
  }
575
  const TreeNode::Ptr node = it->second.lock();
×
576
  if(!node)
×
577
  {
578
    return false;
×
579
  }
580

581
  auto injectedCallback = [hook, this](TreeNode& node) -> NodeStatus {
×
582
    std::unique_lock<std::mutex> lk(hook->mutex);
×
583
    if(!hook->enabled)
×
584
    {
585
      return NodeStatus::SKIPPED;
×
586
    }
587

588
    // Notify that a breakpoint was reached, using the _p->publisher
589
    const Monitor::RequestHeader breakpoint_request(Monitor::BREAKPOINT_REACHED);
×
590
    zmq::multipart_t request_msg;
×
591
    request_msg.addstr(Monitor::SerializeHeader(breakpoint_request));
×
592
    request_msg.addstr(std::to_string(hook->node_uid));
×
593
    request_msg.send(_p->publisher);
×
594

595
    // wait until someone wake us up
596
    if(hook->mode == Monitor::Hook::Mode::BREAKPOINT)
×
597
    {
598
      hook->wakeup.wait(lk, [hook]() { return hook->ready || !hook->enabled; });
×
599

600
      hook->ready = false;
×
601
      // wait was unblocked but it could be the breakpoint becoming disabled.
602
      // in this case, just skip
603
      if(!hook->enabled)
×
604
      {
605
        return NodeStatus::SKIPPED;
×
606
      }
607
    }
608

609
    if(hook->remove_when_done)
×
610
    {
611
      // self-destruction at the end of this lambda function
612
      const std::unique_lock<std::mutex> lk(_p->hooks_map_mutex);
×
613
      _p->pre_hooks.erase(hook->node_uid);
×
614
      node.setPreTickFunction({});
×
615
    }
×
616
    return hook->desired_status;
×
617
  };
×
618

619
  const std::unique_lock<std::mutex> lk(_p->hooks_map_mutex);
×
620
  _p->pre_hooks[node_uid] = hook;
×
621
  node->setPreTickFunction(injectedCallback);
×
622

623
  return true;
×
624
}
×
625

626
bool Groot2Publisher::unlockBreakpoint(Position pos, uint16_t node_uid, NodeStatus result,
×
627
                                       bool remove)
628
{
629
  auto it = _p->nodes_by_uid.find(node_uid);
×
630
  if(it == _p->nodes_by_uid.end())
×
631
  {
632
    return false;
×
633
  }
634
  const TreeNode::Ptr node = it->second.lock();
×
635
  if(!node)
×
636
  {
637
    return false;
×
638
  }
639

640
  auto hook = getHook(pos, node_uid);
×
641
  if(!hook)
×
642
  {
643
    return false;
×
644
  }
645

646
  {
647
    std::unique_lock<std::mutex> lk(hook->mutex);
×
648
    hook->desired_status = result;
×
649
    hook->remove_when_done |= remove;
×
650
    if(hook->mode == Monitor::Hook::Mode::BREAKPOINT)
×
651
    {
652
      hook->ready = true;
×
653
      lk.unlock();
×
654
      hook->wakeup.notify_all();
×
655
    }
656
  }
×
657
  return true;
×
658
}
×
659

660
bool Groot2Publisher::removeHook(Position pos, uint16_t node_uid)
×
661
{
662
  auto it = _p->nodes_by_uid.find(node_uid);
×
663
  if(it == _p->nodes_by_uid.end())
×
664
  {
665
    return false;
×
666
  }
667
  const TreeNode::Ptr node = it->second.lock();
×
668
  if(!node)
×
669
  {
670
    return false;
×
671
  }
672

673
  auto hook = getHook(pos, node_uid);
×
674
  if(!hook)
×
675
  {
676
    return false;
×
677
  }
678

679
  {
680
    const std::unique_lock<std::mutex> lk(_p->hooks_map_mutex);
×
681
    _p->pre_hooks.erase(node_uid);
×
682
  }
×
683
  node->setPreTickFunction({});
×
684

685
  // Disable breakpoint, if it was interactive and blocked
686
  {
687
    std::unique_lock<std::mutex> lk(hook->mutex);
×
688
    if(hook->mode == Monitor::Hook::Mode::BREAKPOINT)
×
689
    {
690
      hook->enabled = false;
×
691
      lk.unlock();
×
692
      hook->wakeup.notify_all();
×
693
    }
694
  }
×
695
  return true;
×
696
}
×
697

UNCOV
698
void Groot2Publisher::removeAllHooks()
×
699
{
UNCOV
700
  std::vector<uint16_t> uids;
×
701

UNCOV
702
  for(auto pos : { Position::PRE, Position::POST })
×
703
  {
UNCOV
704
    uids.clear();
×
UNCOV
705
    auto* hooks = pos == Position::PRE ? &_p->pre_hooks : &_p->post_hooks;
×
UNCOV
706
    std::unique_lock<std::mutex> lk(_p->hooks_map_mutex);
×
UNCOV
707
    if(!hooks->empty())
×
708
    {
709
      uids.reserve(hooks->size());
×
710
      for(const auto& [node_uid, hook_ptr] : *hooks)
×
711
      {
712
        std::ignore = hook_ptr;  // unused in this loop
×
713
        uids.push_back(node_uid);
×
714
      }
715

716
      lk.unlock();
×
717
      for(auto node_uid : uids)
×
718
      {
719
        removeHook(pos, node_uid);
×
720
      }
721
    }
UNCOV
722
  }
×
UNCOV
723
}
×
724

725
Monitor::Hook::Ptr Groot2Publisher::getHook(Position pos, uint16_t node_uid)
×
726
{
727
  auto* hooks = pos == Position::PRE ? &_p->pre_hooks : &_p->post_hooks;
×
728
  const std::unique_lock<std::mutex> lk(_p->hooks_map_mutex);
×
729
  auto bk_it = hooks->find(node_uid);
×
730
  if(bk_it == hooks->end())
×
731
  {
732
    return {};
×
733
  }
734
  return bk_it->second;
×
735
}
×
736

737
}  // namespace BT
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc