ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
ecvConsole.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "ecvConsole.h"
9 
10 #include "MainWindow.h"
11 #include "ecvHead.h"
12 #include "ecvPersistentSettings.h"
13 #include "ecvSettingManager.h"
14 
15 // CV_DB_LIB
16 #include <ecvSingleton.h>
17 
18 // CV_APP_COMMON
19 #include <CommonSettings.h>
20 #include <ecvOptions.h>
21 
22 // Qt
23 #include <QtCompat.h>
24 
25 #include <QApplication>
26 #include <QClipboard>
27 #include <QColor>
28 #include <QDateTime>
29 #include <QDir>
30 #include <QFileInfo>
31 #include <QKeyEvent>
32 #include <QMessageBox>
33 #include <QStandardPaths>
34 #include <QThread>
35 #include <QTime>
36 
37 // system
38 #include <cassert>
39 #ifdef QT_DEBUG
40 #include <iostream>
41 #endif
42 
43 #ifdef _WIN32
44 #include <process.h>
45 #define getpid _getpid
46 #else
47 #include <unistd.h>
48 #endif
49 
50 /***************
51  *** Globals ***
52  ***************/
53 
54 // unique console instance
56 
59 
60 // ecvCustomQListWidget
62  : QListWidget(parent) {}
63 
65  if (event->matches(QKeySequence::Copy)) {
66  int itemsCount = count();
67  QStringList strings;
68  for (int i = 0; i < itemsCount; ++i) {
69  if (item(i)->isSelected()) {
70  strings << item(i)->text();
71  }
72  }
73 
74  QApplication::clipboard()->setText(strings.join("\n"));
75  } else {
76  QListWidget::keyPressEvent(event);
77  }
78 }
79 
80 // ecvConsole
81 ecvConsole* ecvConsole::TheInstance(bool autoInit /*=true*/) {
82  if (!s_console.instance && autoInit) {
83  s_console.instance = new ecvConsole;
85  }
86 
87  return s_console.instance;
88 }
89 
90 void ecvConsole::ReleaseInstance(bool flush /*=true*/) {
91  if (flush && s_console.instance) {
92  // DGM: just in case some messages are still in the queue
93  s_console.instance->refresh();
94  }
95  CVLog::RegisterInstance(nullptr);
96  s_console.release();
97 }
98 
100  : m_textDisplay(nullptr),
101  m_parentWidget(nullptr),
102  m_parentWindow(nullptr),
103  m_logStream(nullptr) {}
104 
106  setLogFile(QString()); // to close/delete any active stream
107 }
108 
109 void myMessageOutput(QtMsgType type,
110  const QMessageLogContext& context,
111  const QString& msg) {
112 #ifndef QT_DEBUG
114  return;
115  }
116 
117  if (type == QtDebugMsg) {
118  return;
119  }
120 #endif
121 
122  QString message =
123  QString("[%1] ").arg(context.function) +
124  msg; // QString("%1 (%1:%1,
125  // %1)").arg(msg).arg(context.file).arg(context.line).arg(context.function);
126 
127  // in this function, you can write the message to any stream!
128  switch (type) {
129  case QtDebugMsg:
130  CVLog::PrintDebug(msg);
131  break;
132  case QtWarningMsg:
133  message.prepend("[Qt WARNING] ");
134  CVLog::Warning(message);
135  break;
136  case QtCriticalMsg:
137  message.prepend("[Qt CRITICAL] ");
138  CVLog::Warning(message);
139  break;
140  case QtFatalMsg:
141  message.prepend("[Qt FATAL] ");
142  CVLog::Warning(message);
143  break;
144  case QtInfoMsg:
145  message.prepend("[Qt INFO] ");
146  CVLog::Warning(message);
147  break;
148  }
149 
150 #ifdef QT_DEBUG
151  // Also send the message to the console so we can look at the output when CC
152  // has quit
153  // (in Qt Creator's Application Output for example)
154  switch (type) {
155  case QtDebugMsg:
156  case QtWarningMsg:
157  case QtInfoMsg:
158  std::cout << message.toStdString() << std::endl;
159  break;
160 
161  case QtCriticalMsg:
162  case QtFatalMsg:
163  std::cerr << message.toStdString() << std::endl;
164  break;
165  }
166 
167 #endif
168 }
169 
172 
173  // persistent settings
174  ecvSettingManager::setValue(ecvPS::Console(), "QtMessagesEnabled",
176 }
177 
178 void ecvConsole::Init(QListWidget* textDisplay /*=0*/,
179  QWidget* parentWidget /*=0*/,
180  MainWindow* parentWindow /*=0*/,
181  bool redirectToStdOut /*=false*/) {
182  // should be called only once!
183  if (s_console.instance) {
184  assert(false);
185  return;
186  }
187 
188  s_console.instance = new ecvConsole;
189  s_console.instance->m_textDisplay = textDisplay;
190  s_console.instance->m_parentWidget = parentWidget;
191  s_console.instance->m_parentWindow = parentWindow;
192  s_redirectToStdOut = redirectToStdOut;
193 
194  // auto-start
195  if (textDisplay) {
196  // load from persistent settings
199  "QtMessagesEnabled", false)
200  .toBool();
201 
202  // set log file with prefix
203  s_console.instance->setLogFile(Settings::LOGFILE_PREFIX);
204 
205  // install : set the callback for Qt messages
206  qInstallMessageHandler(myMessageOutput);
207 
208  s_console.instance->setAutoRefresh(true);
209  }
210 
212 }
213 
214 void ecvConsole::setAutoRefresh(bool state) {
215  if (state) {
216  connect(&m_timer, &QTimer::timeout, this, &ecvConsole::refresh);
217  m_timer.start(1000);
218  } else {
219  m_timer.stop();
220  disconnect(&m_timer, &QTimer::timeout, this, &ecvConsole::refresh);
221  }
222 }
223 
225  m_mutex.lock();
226 
227  if (m_textDisplay && !m_queue.isEmpty()) {
228  // Note: Message filtering based on verbosity level is now done in
229  // CVLog::LogMessage(), so we just display all messages in the queue
230  for (QVector<ConsoleItemType>::const_iterator it = m_queue.constBegin();
231  it != m_queue.constEnd(); ++it) {
232  // it->second = message severity
233  int level = it->second;
234 
235  // destination: console widget (log file is already written in
236  // logMessage()) it->first = message text
237  QListWidgetItem* item = new QListWidgetItem(it->first);
238 
239  // set color based on the message severity
240  // Error
241  if ((it->second & LOG_ERROR) == LOG_ERROR) {
242  item->setForeground(Qt::red);
243  }
244  // Warning
245  else if ((it->second & LOG_WARNING) == LOG_WARNING) {
246  item->setForeground(Qt::darkRed);
247  // we also force the console visibility if a warning message
248  // arrives!
249  if (m_parentWindow) {
251  }
252  }
253 #ifdef QT_DEBUG
254  else if (it->second & DEBUG_FLAG) {
255  item->setForeground(Qt::blue);
256  }
257 #endif
258 
259  m_textDisplay->addItem(item);
260  }
261 
262  m_textDisplay->scrollToBottom();
263  }
264 
265  m_queue.clear();
266 
267  // Flush log file periodically (non-critical messages may not have been
268  // flushed yet)
269  if (m_logStream) {
270  m_logFile.flush();
271  }
272 
273  m_mutex.unlock();
274 }
275 
276 void ecvConsole::logMessage(const QString& message, int level) {
277  // Note: Message filtering based on verbosity level is now done in
278  // CVLog::LogMessage(), so we just process all messages that reach here
279 
280  // QString line = __LINE__;
281  // QString filename = __FILE__;
282  // QString functionname = __FUNCTION__;
283 
284  QString formatedMessage =
285  QStringLiteral("[") + DATETIME + QStringLiteral("] ") + message;
286 
287  if (s_redirectToStdOut) {
288  printf("%s\n", qPrintable(message));
289  }
290  if (m_textDisplay || m_logStream) {
291  m_mutex.lock();
292 
293  // Write to log file immediately for crash safety (all messages)
294  // UI update will still be handled by the timer for performance
295  if (m_logStream) {
296  *m_logStream << formatedMessage << QtCompat::endl;
297  // Flush immediately for ERROR/WARNING, or every few messages for
298  // others
299  if ((level & LOG_ERROR) || (level & LOG_WARNING)) {
300  m_logFile.flush();
301  }
302  }
303 
304  // Queue for UI update
305  if (m_textDisplay) {
306  m_queue.push_back(ConsoleItemType(formatedMessage, level));
307  }
308 
309  m_mutex.unlock();
310  }
311 #ifdef QT_DEBUG
312  else {
313  // Error
314  if ((level & LOG_ERROR) == LOG_ERROR) {
315  if (level & LOG_VERBOSE)
316  printf("ERR-DBG: ");
317  else
318  printf("ERR: ");
319  }
320  // Warning
321  else if ((level & LOG_WARNING) == LOG_WARNING) {
322  if (level & LOG_VERBOSE)
323  printf("WARN-DBG: ");
324  else
325  printf("WARN: ");
326  }
327  // Standard
328  else {
329  if (level & LOG_VERBOSE)
330  printf("MSG-DBG: ");
331  else
332  printf("MSG: ");
333  }
334  printf(" %s\n", qPrintable(formatedMessage));
335  }
336 #endif
337 
338  // we display the error messages in a popup dialog
339  if ((level & LOG_ERROR) == LOG_ERROR && qApp && m_parentWidget &&
340  QThread::currentThread() == qApp->thread()) {
341  QMessageBox::warning(m_parentWidget, "Error", message);
342  }
343 }
344 
345 QString ecvConsole::generateLogFileName(const QString& prefix) {
346  // Generate timestamp in glog format: YYYYMMDD-HHMMSS
347  QString timestamp =
348  QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss");
349 
350  // Get process ID
351  qint64 pid = getpid();
352 
353  // Generate log filename: <prefix>.<timestamp>.<pid>.log
354  return QString("%1.%2.%3.log").arg(prefix).arg(timestamp).arg(pid);
355 }
356 
358  QStringList candidatePaths;
359 
360 #ifdef _WIN32
361  // Windows: Use application directory first, then temp directory
362  candidatePaths << QCoreApplication::applicationDirPath() + "/logs";
363  candidatePaths << QStandardPaths::writableLocation(
364  QStandardPaths::TempLocation) +
365  "/ACloudViewerCache/logs";
366 
367 #elif defined(__APPLE__)
368  // macOS: App bundle is read-only, use standard locations
369  // 1. User's log directory (~/Library/Logs/ACloudViewerCache)
370  QString appLogPath =
371  QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
372  if (!appLogPath.isEmpty()) {
373  // On macOS, AppDataLocation typically points to ~/Library/Application
374  // Support/ACloudViewer We want to use ~/Library/Logs/ACloudViewerCache
375  // instead
376  QDir appDir(appLogPath);
377  appDir.cdUp(); // Go to ~/Library/Application Support
378  appDir.cdUp(); // Go to ~/Library
379  if (appDir.cd("Logs")) {
380  candidatePaths << appDir.absolutePath() + "/ACloudViewerCache";
381  }
382  }
383 
384  // 2. Standard AppDataLocation as fallback
385  if (!appLogPath.isEmpty()) {
386  candidatePaths << appLogPath + "/logs";
387  }
388 
389  // 3. User's home directory
390  QString homePath =
391  QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
392  if (!homePath.isEmpty()) {
393  candidatePaths << homePath + "/.ACloudViewerCache/logs";
394  }
395 
396  // 4. Try application directory (might work if not in app bundle)
397  candidatePaths << QCoreApplication::applicationDirPath() + "/logs";
398 
399  // 5. Fallback to temp directory
400  candidatePaths << QStandardPaths::writableLocation(
401  QStandardPaths::TempLocation) +
402  "/ACloudViewerCache/logs";
403 
404 #else
405  // Linux/Unix: Try multiple locations with fallback for permission issues
406  // 1. Try application directory first (for portable installations)
407  candidatePaths << QCoreApplication::applicationDirPath() + "/logs";
408 
409  // 2. Try user's local data directory (usually
410  // ~/.local/share/ACloudViewerCache/logs)
411  QString dataPath =
412  QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
413  if (!dataPath.isEmpty()) {
414  candidatePaths << dataPath + "/logs";
415  }
416 
417  // 3. Try user's home directory (hidden directory)
418  QString homePath =
419  QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
420  if (!homePath.isEmpty()) {
421  candidatePaths << homePath + "/.ACloudViewerCache/logs";
422  }
423 
424  // 4. Fallback to temp directory
425  candidatePaths << QStandardPaths::writableLocation(
426  QStandardPaths::TempLocation) +
427  "/ACloudViewerCache/logs";
428 #endif
429 
430  // Try each candidate path
431  for (const QString& path : candidatePaths) {
432  QDir dir(path);
433 
434  // Try to create the directory if it doesn't exist
435  if (!dir.exists()) {
436  if (dir.mkpath(".")) {
437  // Successfully created directory
438  return path;
439  }
440  } else {
441  // Directory exists, check if writable
442  QFileInfo dirInfo(path);
443  if (dirInfo.isWritable()) {
444  return path;
445  }
446  }
447  }
448 
449  // If all else fails, return temp directory path (Qt should always have
450  // access)
451  return QStandardPaths::writableLocation(QStandardPaths::TempLocation);
452 }
453 
454 bool ecvConsole::setLogFile(const QString& logPrefix) {
455  // close previous stream (if any)
456  if (m_logStream) {
457  m_mutex.lock();
458  delete m_logStream;
459  m_logStream = nullptr;
460  m_mutex.unlock();
461 
462  if (m_logFile.isOpen()) {
463  m_logFile.close();
464  }
465  }
466 
467  if (!logPrefix.isEmpty()) {
468  // Get appropriate log directory
469  QString logDir = getLogDirectory();
470 
471  // Generate log file name with timestamp and PID
472  QString logFileName = generateLogFileName(logPrefix);
473 
474  // Construct full log path
475  QString logPath = logDir + "/" + logFileName;
476 
477  m_logFile.setFileName(logPath);
478  if (!m_logFile.open(QFile::Text | QFile::WriteOnly | QFile::Append)) {
479  return Error(
480  QString("[Console] Failed to open/create log file '%1'")
481  .arg(logPath));
482  }
483 
484  // Log the actual log file path for user reference
485  QString infoMsg =
486  QString("[Console] Log file created: %1").arg(logPath);
487 
488  m_mutex.lock();
489  m_logStream = new QTextStream(&m_logFile);
490  // Write header to log file
491  *m_logStream << "========================================"
492  << QtCompat::endl;
493  *m_logStream << "ACloudViewer Log File" << QtCompat::endl;
494  *m_logStream << "Started at: "
495  << QDateTime::currentDateTime().toString(
496  "yyyy-MM-dd HH:mm:ss")
497  << QtCompat::endl;
498  *m_logStream << "Log file: " << logPath << QtCompat::endl;
499  *m_logStream << "========================================"
500  << QtCompat::endl;
501  m_mutex.unlock();
502 
503  setAutoRefresh(true);
504 
505  // Print info message to console
506  Print(infoMsg);
507  }
508 
509  return true;
510 }
MouseEvent event
int count
char type
static bool PrintDebug(const char *format,...)
Same as Print, but works only in Debug mode.
Definition: CVLog.cpp:153
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
static bool Print(const char *format,...)
Prints out a formatted message in console.
Definition: CVLog.cpp:113
@ DEBUG_FLAG
Definition: CVLog.h:49
@ LOG_VERBOSE
Definition: CVLog.h:43
@ LOG_WARNING
Definition: CVLog.h:46
@ LOG_ERROR
Definition: CVLog.h:47
static CVLog * TheInstance()
Returns the static and unique instance.
Definition: CVLog.cpp:53
static void RegisterInstance(CVLog *logInstance)
Registers a unique instance.
Definition: CVLog.cpp:81
static bool Error(const char *format,...)
Display an error dialog with formatted message.
Definition: CVLog.cpp:143
void forceConsoleDisplay() override
Forces display of console widget.
Console.
Definition: ecvConsole.h:35
ecvConsole()
Default constructor.
Definition: ecvConsole.cpp:99
static QString getLogDirectory()
Get appropriate log directory path (handles permissions on Ubuntu)
Definition: ecvConsole.cpp:357
QWidget * m_parentWidget
Parent widget.
Definition: ecvConsole.h:109
static void ReleaseInstance(bool flush=true)
Releases unique instance.
Definition: ecvConsole.cpp:90
QMutex m_mutex
Mutex for concurrent thread access to console.
Definition: ecvConsole.h:115
static void Init(QListWidget *textDisplay=nullptr, QWidget *parentWidget=nullptr, MainWindow *parentWindow=nullptr, bool redirectToStdOut=false)
Inits console (and optionaly associates it with a text output widget)
Definition: ecvConsole.cpp:178
MainWindow * m_parentWindow
Parent window (if any)
Definition: ecvConsole.h:112
static void EnableQtMessages(bool state)
Whether to show Qt messages (qDebug / qWarning / etc.) in Console.
Definition: ecvConsole.cpp:170
QPair< QString, int > ConsoleItemType
Queue element type (message + color)
Definition: ecvConsole.h:118
QTextStream * m_logStream
Log file stream.
Definition: ecvConsole.h:129
bool setLogFile(const QString &logPrefix)
Sets log file with prefix (generates timestamped log file like glog)
Definition: ecvConsole.cpp:454
QTimer m_timer
Timer for auto-refresh.
Definition: ecvConsole.h:124
void refresh()
Refreshes console (display all messages still in queue)
Definition: ecvConsole.cpp:224
static bool s_showQtMessagesInConsole
Whether to show Qt messages (qDebug / qWarning / etc.) in Console.
Definition: ecvConsole.h:132
static bool QtMessagesEnabled()
Definition: ecvConsole.h:80
~ecvConsole() override
Destructor.
Definition: ecvConsole.cpp:105
static QString generateLogFileName(const QString &prefix)
Generate log file name with timestamp and pid.
Definition: ecvConsole.cpp:345
QWidget * parentWidget()
Returns the parent widget (if any)
Definition: ecvConsole.h:83
QFile m_logFile
Log file.
Definition: ecvConsole.h:127
static bool s_redirectToStdOut
Definition: ecvConsole.h:133
QListWidget * m_textDisplay
Associated text display widget.
Definition: ecvConsole.h:106
void logMessage(const QString &message, int level) override
Generic message logging method.
Definition: ecvConsole.cpp:276
void setAutoRefresh(bool state)
Sets auto-refresh state.
Definition: ecvConsole.cpp:214
QVector< ConsoleItemType > m_queue
Queue for incoming messages.
Definition: ecvConsole.h:121
void keyPressEvent(QKeyEvent *event) override
Definition: ecvConsole.cpp:64
ecvCustomQListWidget(QWidget *parent=nullptr)
Definition: ecvConsole.cpp:61
static const QString Console()
static QVariant getValue(const QString &section, const QString &key, const QVariant &defaultValue=QVariant())
static void setValue(const QString &section, const QString &key, const QVariant &value)
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
Definition: ecvConsole.cpp:109
static ecvSingleton< ecvConsole > s_console
Definition: ecvConsole.cpp:55
#define DATETIME
Definition: ecvHead.h:24
ImGuiContext * context
Definition: Window.cpp:76
QTextStream & endl(QTextStream &stream)
Definition: QtCompat.h:718
static QString LOGFILE_PREFIX
Tensor Append(const Tensor &self, const Tensor &other, const utility::optional< int64_t > &axis)
Appends the two tensors, along the given axis into a new tensor. Both the tensors must have same data...
static const std::string path
Definition: PointCloud.cpp:59
bool Copy(const std::string &from, const std::string &to, bool include_parent_dir=false, const std::string &extname="")
Copy a file or directory.
Definition: FileSystem.cpp:249
constexpr Rgb red(MAX, 0, 0)
constexpr Rgb blue(0, 0, MAX)
Generic singleton encapsulation structure.
Definition: ecvSingleton.h:12