log_database_proxy_model.cpp
Go to the documentation of this file.
1 // *****************************************************************************
2 //
3 // Copyright (c) 2015, Southwest Research Institute® (SwRI®)
4 // All rights reserved.
5 //
6 // Redistribution and use in source and binary forms, with or without
7 // modification, are permitted provided that the following conditions are met:
8 // * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 // * Neither the name of Southwest Research Institute® (SwRI®) nor the
14 // names of its contributors may be used to endorse or promote products
15 // derived from this software without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 // ARE DISCLAIMED. IN NO EVENT SHALL Southwest Research Institute® BE LIABLE
21 // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
27 // DAMAGE.
28 //
29 // *****************************************************************************
30 
31 #include <stdio.h>
32 #include <algorithm>
33 #include <iterator>
34 
35 #include <ros/time.h>
36 #include <rosbag/bag.h>
37 
41 
42 #include <QColor>
43 #include <QFile>
44 #include <QTextStream>
45 #include <QTimer>
46 #include <QSettings>
47 #include <QtGlobal>
48 
49 namespace swri_console
50 {
52  :
53  colorize_logs_(true),
54  display_time_(true),
55  display_absolute_time_(false),
56  use_regular_expressions_(false),
57  debug_color_(Qt::gray),
58  info_color_(Qt::black),
59  warn_color_(QColor(255,127,0)),
60  error_color_(Qt::red),
61  fatal_color_(Qt::magenta),
62  db_(db),
63  failedSearchText_(""),
64  failedSearchIndex_(0)
65 {
66  QObject::connect(db_, SIGNAL(databaseCleared()),
67  this, SLOT(handleDatabaseCleared()));
68  QObject::connect(db_, SIGNAL(messagesAdded()),
69  this, SLOT(processNewMessages()));
70 
71  QObject::connect(db_, SIGNAL(minTimeUpdated()),
72  this, SLOT(minTimeUpdated()));
73 }
74 
76 {
77 }
78 
79 void LogDatabaseProxyModel::setNodeFilter(const std::set<std::string> &names)
80 {
81  names_ = names;
82  reset();
83 }
84 
85 void LogDatabaseProxyModel::setSeverityFilter(uint8_t severity_mask)
86 {
87  severity_mask_ = severity_mask;
88  reset();
89 }
90 
92 {
93  if (absolute == display_absolute_time_) {
94  return;
95  }
96 
97  display_absolute_time_ = absolute;
98 
99  QSettings settings;
101 
102  if (display_time_ && msg_mapping_.size()) {
103  Q_EMIT dataChanged(index(0), index(msg_mapping_.size()));
104  }
105 }
106 
107 
109 {
110  if (colorize_logs == colorize_logs_) {
111  return;
112  }
113 
114  colorize_logs_ = colorize_logs;
115  QSettings settings;
116  settings.setValue(SettingsKeys::COLORIZE_LOGS, colorize_logs_);
117 
118  if (msg_mapping_.size()) {
119  Q_EMIT dataChanged(index(0), index(msg_mapping_.size()));
120  }
121 }
122 
124 {
125  if (display == display_time_) {
126  return;
127  }
128 
129  display_time_ = display;
130 
131  QSettings settings;
133 
134  if (msg_mapping_.size()) {
135  Q_EMIT dataChanged(index(0), index(msg_mapping_.size()));
136  }
137 }
138 
140 {
141  if (useRegexps == use_regular_expressions_) {
142  return;
143  }
144 
145  use_regular_expressions_ = useRegexps;
146  QSettings settings;
147  settings.setValue(SettingsKeys::USE_REGEXPS, useRegexps);
148  reset();
149 }
150 
152  const QStringList &list)
153 {
154  include_strings_ = list;
155  // The text and regexp filters are always updated at the same time, so this
156  // value will be saved by setIncludeRegexpPattern.
157  reset();
158 }
159 
161  const QStringList &list)
162 {
163  exclude_strings_ = list;
164  // The text and regexp filters are always updated at the same time, so this
165  // value will be saved by setExcludeRegexpPattern.
166  reset();
167 }
168 
169 
171 {
172  include_regexp_.setPattern(pattern);
173  QSettings settings;
174  settings.setValue(SettingsKeys::INCLUDE_FILTER, pattern);
175  reset();
176 }
177 
179 {
180  exclude_regexp_.setPattern(pattern);
181  QSettings settings;
182  settings.setValue(SettingsKeys::EXCLUDE_FILTER, pattern);
183  reset();
184 }
185 
186 void LogDatabaseProxyModel::setDebugColor(const QColor& debug_color)
187 {
188  debug_color_ = debug_color;
189  QSettings settings;
190  settings.setValue(SettingsKeys::DEBUG_COLOR, debug_color);
191  reset();
192 }
193 
194 void LogDatabaseProxyModel::setInfoColor(const QColor& info_color)
195 {
196  info_color_ = info_color;
197  QSettings settings;
198  settings.setValue(SettingsKeys::INFO_COLOR, info_color);
199  reset();
200 }
201 
202 void LogDatabaseProxyModel::setWarnColor(const QColor& warn_color)
203 {
204  warn_color_ = warn_color;
205  QSettings settings;
206  settings.setValue(SettingsKeys::WARN_COLOR, warn_color);
207  reset();
208 }
209 
210 void LogDatabaseProxyModel::setErrorColor(const QColor& error_color)
211 {
212  error_color_ = error_color;
213  QSettings settings;
214  settings.setValue(SettingsKeys::ERROR_COLOR, error_color);
215  reset();
216 }
217 
218 void LogDatabaseProxyModel::setFatalColor(const QColor& fatal_color)
219 {
220  fatal_color_ = fatal_color;
221  QSettings settings;
222  settings.setValue(SettingsKeys::FATAL_COLOR, fatal_color);
223  reset();
224 }
225 
226 int LogDatabaseProxyModel::rowCount(const QModelIndex &parent) const
227 {
228  if (parent.isValid()) {
229  return 0;
230  }
231 
232  return msg_mapping_.size();
233 }
234 
235 
237 {
238  if (use_regular_expressions_ && !include_regexp_.isValid()) {
239  return false;
240  }
241  return true;
242 }
243 
245 {
246  if (use_regular_expressions_ && !exclude_regexp_.isValid()) {
247  return false;
248  }
249  return true;
250 }
251 
252 // Locates the next index based on search criteria, VCM 25 April 2017
253 // searchText_ - string from searchText, all upper case and trimmed spaces
254 // index - currently selected item in messageList
255 // increment - +1 = next||search(i.e. down), -1 = prev (i.e. up)
256 int LogDatabaseProxyModel::getItemIndex(const QString searchText, int index, int increment)
257 {
258  int searchNotFound = -1; // indicates search not found
259  int counter=0; // used to stop loop once full list has been searched
260  bool partialSearch = false; // tells main loop to run a partial search, triggered by prior failed search
261  if(searchText==""||msg_mapping_.size()==0) // skip search for 1)empty string 2)empty set
262  {
263  clearSearchFailure(); // reset failed search variables
264  return searchNotFound;
265  }
266 
267  // round corners for searches
268  if(index<0) // if index < 0, set to size()-1;
269  {
270  index = msg_mapping_.size()-1;
271  }
272  else if(index>=msg_mapping_.size()) // if index >size(), set to 0;
273  {
274  index = 0;
275  }
276 
277  // trigger partial search if:
278  // searchText_ conaints prior failed text
279  // prior failed text is not empty
280  // failed index is not 0
281  // failed search index isn't greater than current index, this could happen through user
282  // interface message selection. Software should clear the variables when UI is adjusted.
283  if(searchText.contains(failedSearchText_) && failedSearchText_ != "" && failedSearchIndex_ !=0 && failedSearchIndex_ <= msg_mapping_.size() )
284  {
285  partialSearch = true;
286  index = failedSearchIndex_-1;
287  counter = failedSearchIndex_;
288  }
289  int i;
290  for(i=0; i<msg_mapping_.size();i++) // loop through all messages until end or match is found
291  {
292  const LineMap line_idx = msg_mapping_[index];
293  const LogEntry &item = db_->log()[line_idx.log_index];
294  QString tempString = item.text.join("|"); // concatenate strings
295  if(tempString.toUpper().contains(searchText)) // search match found
296  {
297  clearSearchFailure(); // reset failed search variables
298  return index; // match found, return location and exit loop
299  }
300  counter++; // used to track total search length
301  if(counter>=msg_mapping_.size()) // exit if all messages have been scanned
302  {
303  if((!partialSearch)||(failedSearchText_ == "")) // store failed text if one isn't already stored
304  {
305  failedSearchText_ = searchText;
306  }
308  return searchNotFound; // match not found, return -1 and exit loop
309  }
310  // increment (next/search) or decrement (prev) index then address corner rounding
311  index = index + increment;
312  if(index<0) // less than 0 set to max
313  {
314  index = msg_mapping_.size()-1;
315  }
316  else if(index>=msg_mapping_.size()) // greater than max, set to 0
317  {
318  index = 0;
319  }
320  }
321 
322  return -1;
323 }
324 
326 {
327  // reset failed search variables, VCM 27 April 2017
328  failedSearchIndex_ = 0;
329  failedSearchText_ = "";
330 }
331 
333  const QModelIndex &index, int role) const
334 {
335  switch (role)
336  {
337  // Currently we're only returning data for these roles, so return immediately
338  // if we're being queried for anything else.
339  case Qt::DisplayRole:
340  case Qt::ToolTipRole:
341  case ExtendedLogRole:
342  break;
343  case Qt::ForegroundRole:
344  if (colorize_logs_) {
345  break;
346  }
347  default:
348  return QVariant();
349  }
350 
351  if (index.parent().isValid() &&
352  static_cast<size_t>(index.row()) >= msg_mapping_.size()) {
353  return QVariant();
354  }
355 
356  const LineMap line_idx = msg_mapping_[index.row()];
357  const LogEntry &item = db_->log()[line_idx.log_index];
358 
359  if (role == Qt::DisplayRole) {
360  char level = '?';
361  if (item.level == rosgraph_msgs::Log::DEBUG) {
362  level = 'D';
363  } else if (item.level == rosgraph_msgs::Log::INFO) {
364  level = 'I';
365  } else if (item.level == rosgraph_msgs::Log::WARN) {
366  level = 'W';
367  } else if (item.level == rosgraph_msgs::Log::ERROR) {
368  level = 'E';
369  } else if (item.level == rosgraph_msgs::Log::FATAL) {
370  level = 'F';
371  }
372 
373  char stamp[128];
375  snprintf(stamp, sizeof(stamp),
376  "%u.%09u",
377  item.stamp.sec,
378  item.stamp.nsec);
379  } else {
380  ros::Duration t = item.stamp - db_->minTime();
381 
382  int32_t secs = t.sec;
383  int hours = secs / 60 / 60;
384  int minutes = (secs / 60) % 60;
385  int seconds = (secs % 60);
386  int milliseconds = t.nsec / 1000000;
387 
388  snprintf(stamp, sizeof(stamp),
389  "%d:%02d:%02d:%03d",
390  hours, minutes, seconds, milliseconds);
391  }
392 
393  char header[1024];
394  if (display_time_) {
395  snprintf(header, sizeof(header),
396  "[%c %s] ", level, stamp);
397  } else {
398  snprintf(header, sizeof(header),
399  "[%c] ", level);
400  }
401 
402  // For multiline messages, we only want to display the header for
403  // the first line. For the subsequent lines, we generate a header
404  // and then fill it with blank lines so that the messages are
405  // aligned properly (assuming monospaced font).
406  if (line_idx.line_index != 0) {
407  size_t len = strnlen(header, sizeof(header));
408  for (size_t i = 0; i < len; i++) {
409  header[i] = ' ';
410  }
411  }
412 
413  return QVariant(QString(header) + item.text[line_idx.line_index]);
414  }
415  else if (role == Qt::ForegroundRole && colorize_logs_) {
416  switch (item.level) {
417  case rosgraph_msgs::Log::DEBUG:
418  return QVariant(debug_color_);
419  case rosgraph_msgs::Log::INFO:
420  return QVariant(info_color_);
421  case rosgraph_msgs::Log::WARN:
422  return QVariant(warn_color_);
423  case rosgraph_msgs::Log::ERROR:
424  return QVariant(error_color_);
425  case rosgraph_msgs::Log::FATAL:
426  return QVariant(fatal_color_);
427  default:
428  return QVariant(info_color_);
429  }
430  }
431  else if (role == Qt::ToolTipRole) {
432  char buffer[4096];
433  snprintf(buffer, sizeof(buffer),
434  "<p style='white-space:pre'>"
435  "Timestamp: %d.%09d\n"
436  "Seq: %d\n"
437  "Node: %s\n"
438  "Function: %s\n"
439  "File: %s\n"
440  "Line: %d\n"
441  "\n",
442  item.stamp.sec,
443  item.stamp.nsec,
444  item.seq,
445  item.node.c_str(),
446  item.function.c_str(),
447  item.file.c_str(),
448  item.line);
449 
450  QString text = (QString(buffer) +
451  item.text.join("\n") +
452  QString("</p>"));
453 
454  return QVariant(text);
455  } else if (role == LogDatabaseProxyModel::ExtendedLogRole) {
456  char buffer[4096];
457  snprintf(buffer, sizeof(buffer),
458  "Timestamp: %d.%09d\n"
459  "Node: %s\n"
460  "Function: %s\n"
461  "File: %s\n"
462  "Line: %d\n"
463  "Message: ",
464  item.stamp.sec,
465  item.stamp.nsec,
466  item.node.c_str(),
467  item.function.c_str(),
468  item.file.c_str(),
469  item.line);
470 
471  QString text = (QString(buffer) +
472  item.text.join("\n"));
473 
474  return QVariant(text);
475  }
476 
477  return QVariant();
478 }
479 
481 {
482  beginResetModel();
483  msg_mapping_.clear();
484  early_mapping_.clear();
485  earliest_log_index_ = db_->log().size();
487  endResetModel();
489 }
490 
491 
492 void LogDatabaseProxyModel::saveToFile(const QString& filename) const
493 {
494  if (filename.endsWith(".bag", Qt::CaseInsensitive)) {
495  saveBagFile(filename);
496  }
497  else {
498  saveTextFile(filename);
499  }
500 }
501 
502 void LogDatabaseProxyModel::saveBagFile(const QString& filename) const
503 {
504  rosbag::Bag bag(filename.toStdString().c_str(), rosbag::bagmode::Write);
505 
506  size_t idx = 0;
507  while (idx < msg_mapping_.size()) {
508  const LineMap line_map = msg_mapping_[idx];
509  const LogEntry &item = db_->log()[line_map.log_index];
510 
511  rosgraph_msgs::Log log;
512  log.file = item.file;
513  log.function = item.function;
514  log.header.seq = item.seq;
515  if (item.stamp < ros::TIME_MIN) {
516  // Note: I think TIME_MIN is the minimum representation of
517  // ros::Time, so this branch should be impossible. Nonetheless,
518  // it doesn't hurt.
519  log.header.stamp = ros::Time::now();
520  qWarning("Msg with seq %d had time (%d); it's less than ros::TIME_MIN, which is invalid. "
521  "Writing 'now' instead.",
522  log.header.seq, item.stamp.sec);
523  } else {
524  log.header.stamp = item.stamp;
525  }
526  log.level = item.level;
527  log.line = item.line;
528  log.msg = item.text.join("\n").toStdString();
529  log.name = item.node;
530  bag.write("/rosout", log.header.stamp, log);
531 
532  // Advance to the next line with a different log index.
533  idx++;
534  while (idx < msg_mapping_.size() && msg_mapping_[idx].log_index == line_map.log_index) {
535  idx++;
536  }
537  }
538  bag.close();
539 }
540 
541 void LogDatabaseProxyModel::saveTextFile(const QString& filename) const
542 {
543  QFile outFile(filename);
544  outFile.open(QFile::WriteOnly);
545  QTextStream outstream(&outFile);
546  for(size_t i = 0; i < msg_mapping_.size(); i++)
547  {
548  QString line = data(index(i), Qt::DisplayRole).toString();
549  outstream << line << '\n';
550  }
551  outstream.flush();
552  outFile.close();
553 }
554 
556 {
557  reset();
558  clearSearchFailure(); // reset failed search variables, VCM 26 April 2017
559 }
560 
562 {
563  std::deque<LineMap> new_items;
564 
565  // Process all messages from latest_log_index_ to the end of the
566  // log.
567  for (;
568  latest_log_index_ < db_->log().size();
570  {
571  const LogEntry &item = db_->log()[latest_log_index_];
572  if (!acceptLogEntry(item)) {
573  continue;
574  }
575 
576  for (int i = 0; i < item.text.size(); i++) {
577  new_items.push_back(LineMap(latest_log_index_, i));
578  }
579  }
580 
581  if (!new_items.empty()) {
582  beginInsertRows(QModelIndex(),
583  msg_mapping_.size(),
584  msg_mapping_.size() + new_items.size() - 1);
585  msg_mapping_.insert(msg_mapping_.end(),
586  new_items.begin(),
587  new_items.end());
588  endInsertRows();
589 
590  Q_EMIT messagesAdded();
591  }
592 }
593 
595 {
596  // We process old messages in two steps. First, we process the
597  // remaining messages in chunks and store them in the early_mapping_
598  // buffer if they pass all the filters. When the early mapping
599  // buffer is large enough (or we have processed everything), then we
600  // merge the early_mapping buffer in the main buffer. This approach
601  // allows us to process very large logs without causing major lag
602  // for the user.
603 
604  for (size_t i = 0;
605  earliest_log_index_ != 0 && i < 100;
606  earliest_log_index_--, i++)
607  {
608  const LogEntry &item = db_->log()[earliest_log_index_-1];
609  if (!acceptLogEntry(item)) {
610  continue;
611  }
612 
613  for (int i = 0; i < item.text.size(); i++) {
614  // Note that we have to add the lines backwards to maintain the proper order.
615  early_mapping_.push_front(
616  LineMap(earliest_log_index_-1, item.text.size()-1-i));
617  }
618  }
619 
620  if ((earliest_log_index_ == 0 && early_mapping_.size()) ||
621  (early_mapping_.size() > 200)) {
622  beginInsertRows(QModelIndex(),
623  0,
624  early_mapping_.size() - 1);
625  msg_mapping_.insert(msg_mapping_.begin(),
626  early_mapping_.begin(),
627  early_mapping_.end());
628  early_mapping_.clear();
629  endInsertRows();
630 
631  Q_EMIT messagesAdded();
632  }
633 
635 }
636 
638 {
639  // If we have older logs that still need to be processed, schedule a
640  // callback at the next idle time.
641  if (earliest_log_index_ > 0) {
642  QTimer::singleShot(0, this, SLOT(processOldMessages()));
643  }
644 }
645 
647 {
648  if (!(item.level & severity_mask_)) {
649  return false;
650  }
651 
652  if (names_.count(item.node) == 0) {
653  return false;
654  }
655 
656  if (!testIncludeFilter(item)) {
657  return false;
658  }
659 
661  // For multi-line messages, we join the lines together with a
662  // space to make it easy for users to use filters that spread
663  // across the new lines.
664 
665  // Don't let an empty regexp filter out everything
666  return exclude_regexp_.isEmpty() || exclude_regexp_.indexIn(item.text.join(" ")) < 0;
667  } else {
668  for (int i = 0; i < exclude_strings_.size(); i++) {
669  if (item.text.join(" ").contains(exclude_strings_[i], Qt::CaseInsensitive)) {
670  return false;
671  }
672  }
673  }
674 
675  return true;
676 }
677 
678 // Return true if the item message contains at least one of the
679 // strings in include_filter_. Always returns true if there are no
680 // include strings.
682 {
684  return include_regexp_.indexIn(item.text.join(" ")) >= 0;
685  } else {
686  if (include_strings_.empty()) {
687  return true;
688  }
689 
690  for (int i = 0; i < include_strings_.size(); i++) {
691  if (item.text.join(" ").contains(include_strings_[i], Qt::CaseInsensitive)) {
692  return true;
693  }
694  }
695  }
696 
697  return false;
698 }
699 
701 {
702  if (display_time_ &&
704  && msg_mapping_.size()) {
705  Q_EMIT dataChanged(index(0), index(msg_mapping_.size()));
706  }
707 }
708 } // namespace swri_console
void saveBagFile(const QString &filename) const
static const QString INFO_COLOR
Definition: settings_keys.h:60
static const QString DISPLAY_TIMESTAMPS
Definition: settings_keys.h:47
void setFatalColor(const QColor &fatal_color)
static const QString FATAL_COLOR
Definition: settings_keys.h:63
void setIncludeFilters(const QStringList &list)
static const QString USE_REGEXPS
Definition: settings_keys.h:49
virtual QVariant data(const QModelIndex &index, int role) const
const Time TIME_MIN(0, 1)
static const QString WARN_COLOR
Definition: settings_keys.h:61
void setInfoColor(const QColor &info_color)
const ros::Time & minTime() const
Definition: log_database.h:65
void setSeverityFilter(uint8_t severity_mask)
void setWarnColor(const QColor &warn_color)
void setErrorColor(const QColor &error_color)
void setExcludeFilters(const QStringList &list)
virtual int rowCount(const QModelIndex &parent) const
static const QString EXCLUDE_FILTER
Definition: settings_keys.h:51
static const QString DEBUG_COLOR
Definition: settings_keys.h:59
void saveTextFile(const QString &filename) const
void saveToFile(const QString &filename) const
const std::deque< LogEntry > & log()
Definition: log_database.h:64
static const QString COLORIZE_LOGS
Definition: settings_keys.h:64
void setNodeFilter(const std::set< std::string > &names)
int getItemIndex(const QString searchText, int index, int increment)
static const QString ABSOLUTE_TIMESTAMPS
Definition: settings_keys.h:48
void setDebugColor(const QColor &debug_color)
static const QString ERROR_COLOR
Definition: settings_keys.h:62
static const QString INCLUDE_FILTER
Definition: settings_keys.h:50
static Time now()
void setExcludeRegexpPattern(const QString &pattern)
void setIncludeRegexpPattern(const QString &pattern)


swri_console
Author(s):
autogenerated on Fri Apr 3 2020 03:20:03