diff --git a/oscar/SleepLib/deviceconnection.cpp b/oscar/SleepLib/deviceconnection.cpp index 06da68bc..ab5cd39c 100644 --- a/oscar/SleepLib/deviceconnection.cpp +++ b/oscar/SleepLib/deviceconnection.cpp @@ -92,7 +92,7 @@ void DeviceConnectionManager::replay(QFile* file) // Return singleton instance of DeviceConnectionManager, creating it if necessary. -inline DeviceConnectionManager & DeviceConnectionManager::getInstance() +DeviceConnectionManager & DeviceConnectionManager::getInstance() { static DeviceConnectionManager instance; return instance; diff --git a/oscar/SleepLib/deviceconnection.h b/oscar/SleepLib/deviceconnection.h index f56bc031..352aab29 100644 --- a/oscar/SleepLib/deviceconnection.h +++ b/oscar/SleepLib/deviceconnection.h @@ -13,7 +13,6 @@ // connections to devices. For now it just supports serial ports. #include <QtSerialPort/QSerialPort> -#include <QXmlStreamWriter> #include <QHash> #include <QVariant> @@ -263,6 +262,9 @@ public: * supports Bluetooth and BLE as well as serial. Such a class might then be * used instead of port "name" between DeviceConnectionManager and clients. */ +class QXmlStreamWriter; +class QXmlStreamReader; + class SerialPortInfo { public: diff --git a/oscar/SleepLib/xmlreplay.cpp b/oscar/SleepLib/xmlreplay.cpp index 6782d018..cd121dcd 100644 --- a/oscar/SleepLib/xmlreplay.cpp +++ b/oscar/SleepLib/xmlreplay.cpp @@ -116,6 +116,14 @@ void XmlRecorder::epilogue() m_xml->writeEndElement(); // close enclosing tag } +void XmlRecorder::flush() +{ + if (m_file) { + if (!m_file->flush()) { + qWarning().noquote() << "Unable to flush XML to" << m_file->fileName(); + } + } +} XmlReplay::XmlReplay(QFile* file, const QString & tag) : m_tag(tag), m_file(file), m_pendingSignal(nullptr), m_parent(nullptr) @@ -416,6 +424,7 @@ void XmlReplayEvent::record(XmlRecorder* writer) const if (writer != nullptr) { writer->lock(); writer->xml() << *this; + writer->flush(); writer->unlock(); } } diff --git a/oscar/SleepLib/xmlreplay.h b/oscar/SleepLib/xmlreplay.h index 81fd12a9..5c784cd9 100644 --- a/oscar/SleepLib/xmlreplay.h +++ b/oscar/SleepLib/xmlreplay.h @@ -54,6 +54,7 @@ public: inline QXmlStreamWriter & xml() { return *m_xml; } inline void lock() { m_mutex.lock(); } inline void unlock() { m_mutex.unlock(); } + void flush(); protected: XmlRecorder(XmlRecorder* parent, const QString & id, const QString & tag); // constructor used by substreams diff --git a/oscar/logger.cpp b/oscar/logger.cpp index 986b5c40..8c1ade84 100644 --- a/oscar/logger.cpp +++ b/oscar/logger.cpp @@ -8,8 +8,9 @@ * for more details. */ #include "logger.h" - -#define ASSERTS_SUCK +#include "SleepLib/preferences.h" +#include "version.h" +#include <QDir> QThreadPool * otherThreadPool = NULL; @@ -47,12 +48,9 @@ void MyOutputHandler(QtMsgType type, const QMessageLogContext &context, const QS if (logger && logger->isRunning()) { logger->append(msg); + } else { + fprintf(stderr, "%s\n", msg.toLocal8Bit().constData()); } -#ifdef ASSERTS_SUCK -// else { -// fprintf(stderr, "%s\n", msg.toLocal8Bit().data()); -// } -#endif if (type == QtFatalMsg) { abort(); @@ -84,15 +82,65 @@ void LogThread::connectionReady() { strlock.lock(); connected = true; + logTrigger.wakeAll(); strlock.unlock(); qDebug() << "Logging UI initialized"; } +void LogThread::logToFile() +{ + if (m_logStream) { + qWarning().noquote() << "Already logging to" << m_logFile->fileName(); + return; + } + + QString debugLog = GetLogDir() + "/debug.txt"; + rotateLogs(debugLog); // keep a limited set of previous logs + + strlock.lock(); + m_logFile = new QFile(debugLog); + Q_ASSERT(m_logFile); + if (m_logFile->open(QFile::ReadWrite | QFile::Text)) { + m_logStream = new QTextStream(m_logFile); + } + logTrigger.wakeAll(); + strlock.unlock(); + + if (m_logStream) { + qDebug().noquote() << "Logging to" << debugLog; + } else { + qWarning().noquote() << "Unable to open" << debugLog; + } +} + +LogThread::~LogThread() +{ + QMutexLocker lock(&strlock); + + Q_ASSERT(running == false); + if (m_logStream) { + delete m_logStream; + m_logStream = nullptr; + Q_ASSERT(m_logFile); + delete m_logFile; + m_logFile = nullptr; + } +} + +QString LogThread::logFileName() +{ + if (!m_logFile) { + return ""; + } + return m_logFile->fileName(); +} + void shutdownLogger() { if (logger) { logger->quit(); - otherThreadPool->waitForDone(-1); + // The thread is automatically destroyed when its run() method exits. + otherThreadPool->waitForDone(-1); // wait until that happens logger = NULL; } delete otherThreadPool; @@ -103,47 +151,132 @@ LogThread * logger = NULL; void LogThread::append(QString msg) { QString tmp = QString("%1: %2").arg(logtime.elapsed(), 5, 10, QChar('0')).arg(msg); - //QStringList appears not to be threadsafe - strlock.lock(); - buffer.append(tmp); - strlock.unlock(); + appendClean(tmp); } void LogThread::appendClean(QString msg) { + fprintf(stderr, "%s\n", msg.toLocal8Bit().constData()); strlock.lock(); buffer.append(msg); + logTrigger.wakeAll(); strlock.unlock(); } void LogThread::quit() { qDebug() << "Shutting down logging thread"; - running = false; + qInstallMessageHandler(0); // Remove our logger. + strlock.lock(); - while (!buffer.isEmpty()) { - QString msg = buffer.takeFirst(); - fprintf(stderr, "%s\n", msg.toLocal8Bit().constData()); - } - strlock.unlock(); + running = false; // Force the thread to exit after its next iteration. + logTrigger.wakeAll(); // Trigger the final flush. + strlock.unlock(); // Release the lock so that the thread can complete. } void LogThread::run() { + QMutexLocker lock(&strlock); + running = true; s_LoggerRunning.unlock(); // unlock as soon as the thread begins to run do { - strlock.lock(); - //int r = receivers(SIGNAL(outputLog(QString()))); - while (connected && !buffer.isEmpty()) { + logTrigger.wait(&strlock); // releases strlock while it waits + while (connected && m_logFile && !buffer.isEmpty()) { QString msg = buffer.takeFirst(); - fprintf(stderr, "%s\n", msg.toLocal8Bit().data()); - emit outputLog(msg); + if (m_logStream) { + *m_logStream << msg << endl; + } + emit outputLog(msg); } - strlock.unlock(); - QThread::msleep(1000); } while (running); + + // strlock will be released when lock goes out of scope } +QString GetLogDir() +{ + static const QString LOG_DIR_NAME = "logs"; + + Q_ASSERT(!GetAppData().isEmpty()); // If GetLogDir gets called before GetAppData() is valid, this would point at root. + QDir oscarData(GetAppData()); + Q_ASSERT(oscarData.exists()); + if (!oscarData.exists(LOG_DIR_NAME)) { + oscarData.mkdir(LOG_DIR_NAME); + } + QDir logDir(oscarData.canonicalPath() + "/" + LOG_DIR_NAME); + if (!logDir.exists()) { + qWarning() << "Unable to create" << logDir.absolutePath() << "reverting to" << oscarData.canonicalPath(); + logDir = oscarData; + } + Q_ASSERT(logDir.exists()); + + return logDir.canonicalPath(); +} + +void rotateLogs(const QString & filePath, int maxPrevious) +{ + if (maxPrevious < 0) { + if (getVersion().IsReleaseVersion()) { + maxPrevious = 1; + } else { + // keep more in testing builds + maxPrevious = 4; + } + } + + // Build the list of rotated logs for this filePath. + QFileInfo info(filePath); + QString path = QDir(info.absolutePath()).canonicalPath(); + QString base = info.baseName(); + QString ext = info.completeSuffix(); + if (!ext.isEmpty()) { + ext = "." + ext; + } + if (path.isEmpty()) { + qWarning() << "Skipping log rotation, directory does not exist:" << info.absoluteFilePath(); + return; + } + + QStringList logs; + logs.append(filePath); + for (int i = 0; i < maxPrevious; i++) { + logs.append(QString("%1/%2.%3%4").arg(path).arg(base).arg(i).arg(ext)); + } + + // Remove the expired log. + QFileInfo expired(logs[maxPrevious]); + if (expired.exists()) { + if (expired.isDir()) { + QDir dir(expired.canonicalFilePath()); + //qDebug() << "Removing expired log directory" << dir.canonicalPath(); + if (!dir.removeRecursively()) { + qWarning() << "Unable to delete expired log directory" << dir.canonicalPath(); + } + } else { + QFile file(expired.canonicalFilePath()); + //qDebug() << "Removing expired log file" << file.fileName(); + if (!file.remove()) { + qWarning() << "Unable to delete expired log file" << file.fileName(); + } + } + } + + // Rotate the remaining logs. + for (int i = maxPrevious; i > 0; i--) { + QFileInfo from(logs[i-1]); + QFileInfo to(logs[i]); + if (from.exists()) { + if (to.exists()) { + qWarning() << "Unable to rotate log:" << to.absoluteFilePath() << "exists"; + continue; + } + //qDebug() << "Renaming" << from.absoluteFilePath() << "to" << to.absoluteFilePath(); + if (!QFile::rename(from.absoluteFilePath(), to.absoluteFilePath())) { + qWarning() << "Unable to rename" << from.absoluteFilePath() << "to" << to.absoluteFilePath(); + } + } + } +} diff --git a/oscar/logger.h b/oscar/logger.h index 6868c25c..2f4a1803 100644 --- a/oscar/logger.h +++ b/oscar/logger.h @@ -5,25 +5,32 @@ #include <QRunnable> #include <QThreadPool> #include <QMutex> +#include <QWaitCondition> #include <QTime> void initializeLogger(); void shutdownLogger(); +QString GetLogDir(); +void rotateLogs(const QString & filePath, int maxPrevious=-1); + + void MyOutputHandler(QtMsgType type, const QMessageLogContext &context, const QString &msgtxt); class LogThread:public QObject, public QRunnable { Q_OBJECT public: - explicit LogThread() : QRunnable() { running = false; logtime.start(); connected = false; } - virtual ~LogThread() {} + explicit LogThread() : QRunnable() { running = false; logtime.start(); connected = false; m_logFile = nullptr; m_logStream = nullptr; } + virtual ~LogThread(); void run(); void append(QString msg); void appendClean(QString msg); bool isRunning() { return running; } void connectionReady(); + void logToFile(); + QString logFileName(); void quit(); @@ -36,6 +43,9 @@ protected: volatile bool running; QTime logtime; bool connected; + class QFile* m_logFile; + class QTextStream* m_logStream; + QWaitCondition logTrigger; }; extern LogThread * logger; diff --git a/oscar/main.cpp b/oscar/main.cpp index 23b96666..cc19464d 100644 --- a/oscar/main.cpp +++ b/oscar/main.cpp @@ -28,6 +28,7 @@ #include "SleepLib/profiles.h" #include "translation.h" #include "SleepLib/common.h" +#include "SleepLib/deviceconnection.h" #include <ctime> #include <chrono> @@ -544,6 +545,16 @@ int main(int argc, char *argv[]) { } } + // Make sure the data directory exists. + if (!newDir.mkpath(".")) { + QMessageBox::warning(nullptr, QObject::tr("Exiting"), + QObject::tr("Unable to create the OSCAR data folder at")+"\n"+ + GetAppData()); + return 0; + } + + // Begin logging to file now that there's a data folder. + logger->logToFile(); /////////////////////////////////////////////////////////////////////////////////////////// // Initialize preferences system (Don't use p_pref before this point!) @@ -636,6 +647,21 @@ int main(int argc, char *argv[]) { MD300W1Loader::Register(); ViatomLoader::Register(); + // Begin logging device connection activity. + QString connectionsLogDir = GetLogDir() + "/connections"; + rotateLogs(connectionsLogDir); // keep a limited set of previous logs + if (!QDir(connectionsLogDir).mkpath(".")) { + qWarning().noquote() << "Unable to create directory" << connectionsLogDir; + } + + QFile deviceLog(connectionsLogDir + "/devices.xml"); + if (deviceLog.open(QFile::ReadWrite)) { + qDebug().noquote() << "Logging device connections to" << deviceLog.fileName(); + DeviceConnectionManager::getInstance().record(&deviceLog); + } else { + qWarning().noquote() << "Unable to start device connection logging to" << deviceLog.fileName(); + } + schema::setOrders(); // could be called in init... // Scan for user profiles @@ -653,7 +679,11 @@ int main(int argc, char *argv[]) { mainwin->SetupGUI(); mainwin->show(); - return a.exec(); + int result = a.exec(); + + DeviceConnectionManager::getInstance().record(nullptr); + + return result; } #endif // !UNITTEST_MODE diff --git a/oscar/mainwindow.cpp b/oscar/mainwindow.cpp index e7890dfe..c330304d 100644 --- a/oscar/mainwindow.cpp +++ b/oscar/mainwindow.cpp @@ -2682,7 +2682,7 @@ void MainWindow::on_actionCreate_Card_zip_triggered() infostr = datacard.loader->loaderName(); } prefix = infostr; - folder += QDir::separator() + infostr + ".zip"; + folder += "/" + infostr + ".zip"; filename = QFileDialog::getSaveFileName(this, tr("Choose where to save zip"), folder, tr("ZIP files (*.zip)")); @@ -2727,6 +2727,56 @@ void MainWindow::on_actionCreate_Card_zip_triggered() } } + +void MainWindow::on_actionCreate_Log_zip_triggered() +{ + QString folder; + + // Note: macOS ignores this and points to OSCAR's most recently used directory for saving. + folder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + folder += "/OSCAR-logs.zip"; + QString filename = QFileDialog::getSaveFileName(this, tr("Choose where to save zip"), folder, tr("ZIP files (*.zip)")); + if (filename.isEmpty()) { + return; // aborted + } + if (!filename.toLower().endsWith(".zip")) { + filename += ".zip"; + } + + qDebug() << "Create zip of OSCAR diagnostic logs:" << filename; + + ZipFile z; + bool ok = z.Open(filename); + if (ok) { + ProgressDialog * prog = new ProgressDialog(this); + prog->setMessage(tr("Creating zip...")); + + // Build the list of files. + FileQueue files; + files.AddDirectory(GetLogDir(), "logs"); + + // Defer the current debug log to the end. + QString debugLog = logger->logFileName(); + QString debugLogZipName; + int exists = files.Remove(debugLog, &debugLogZipName); + if (exists) { + files.AddFile(debugLog, debugLogZipName); + } + + // Create the zip. + ok = z.AddFiles(files, prog); + z.Close(); + } else { + qWarning() << "Unable to open" << filename; + } + if (!ok) { + QMessageBox::warning(nullptr, STR_MessageBox_Error, + QObject::tr("Unable to create zip!"), + QMessageBox::Ok); + } +} + + void MainWindow::on_actionCreate_OSCAR_Data_zip_triggered() { QString folder; @@ -2734,7 +2784,7 @@ void MainWindow::on_actionCreate_OSCAR_Data_zip_triggered() // Note: macOS ignores this and points to OSCAR's most recently used directory for saving. folder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); - folder += QDir::separator() + STR_AppData + ".zip"; + folder += "/" + STR_AppData + ".zip"; QString filename = QFileDialog::getSaveFileName(this, tr("Choose where to save zip"), folder, tr("ZIP files (*.zip)")); @@ -2749,7 +2799,6 @@ void MainWindow::on_actionCreate_OSCAR_Data_zip_triggered() qDebug() << "Create zip of OSCAR data folder:" << filename; QDir oscarData(GetAppData()); - QFile debugLog(oscarData.canonicalPath() + QDir::separator() + "debuglog.txt"); ZipFile z; bool ok = z.Open(filename); @@ -2759,29 +2808,22 @@ void MainWindow::on_actionCreate_OSCAR_Data_zip_triggered() prog->setWindowModality(Qt::ApplicationModal); prog->open(); - // Build the list of files and exclude any existing debug log. + // Build the list of files. FileQueue files; files.AddDirectory(oscarData.canonicalPath(), oscarData.dirName()); - files.Remove(debugLog.fileName()); + + // Defer the current debug log to the end. + QString debugLog = logger->logFileName(); + QString debugLogZipName; + int exists = files.Remove(debugLog, &debugLogZipName); + if (exists) { + files.AddFile(debugLog, debugLogZipName); + } prog->setMessage(tr("Creating zip...")); // Create the zip. ok = z.AddFiles(files, prog); - if (ok && z.aborted() == false) { - // Update the debug log and add it last. - ok = debugLog.open(QIODevice::WriteOnly); - if (ok) { - debugLog.write(ui->logText->toPlainText().toLocal8Bit().data()); - debugLog.close(); - QString debugLogName = oscarData.dirName() + "/" + QFileInfo(debugLog).fileName(); - ok = z.AddFile(debugLog.fileName(), debugLogName); - if (!ok) { - qWarning() << "Unable to add debug log to zip!"; - } - } - } - z.Close(); } else { qWarning() << "Unable to open" << filename; diff --git a/oscar/mainwindow.h b/oscar/mainwindow.h index 0f389833..93834d6b 100644 --- a/oscar/mainwindow.h +++ b/oscar/mainwindow.h @@ -344,6 +344,8 @@ class MainWindow : public QMainWindow void on_actionCreate_Card_zip_triggered(); + void on_actionCreate_Log_zip_triggered(); + void on_actionCreate_OSCAR_Data_zip_triggered(); void on_actionReport_a_Bug_triggered(); diff --git a/oscar/mainwindow.ui b/oscar/mainwindow.ui index df56f053..9b797bae 100644 --- a/oscar/mainwindow.ui +++ b/oscar/mainwindow.ui @@ -2866,6 +2866,7 @@ p, li { white-space: pre-wrap; } <addaction name="actionShow_Performance_Counters"/> <addaction name="separator"/> <addaction name="actionCreate_Card_zip"/> + <addaction name="actionCreate_Log_zip"/> <addaction name="actionCreate_OSCAR_Data_zip"/> <addaction name="actionReport_a_Bug"/> <addaction name="separator"/> @@ -3232,6 +3233,11 @@ p, li { white-space: pre-wrap; } <string>Create zip of CPAP data card</string> </property> </action> + <action name="actionCreate_Log_zip"> + <property name="text"> + <string>Create zip of OSCAR diagnostic logs</string> + </property> + </action> <action name="actionCreate_OSCAR_Data_zip"> <property name="text"> <string>Create zip of all OSCAR data</string> diff --git a/oscar/oscar.pro b/oscar/oscar.pro index 37aa096f..5becfa8d 100644 --- a/oscar/oscar.pro +++ b/oscar/oscar.pro @@ -500,6 +500,7 @@ lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,9) { # Create a debug GUI build by adding "CONFIG+=memdebug" to your qmake command memdebug { + CONFIG += debug !win32 { # add memory checking on Linux and macOS debug builds QMAKE_CFLAGS += -g -Werror -fsanitize=address -fno-omit-frame-pointer -fno-common -fsanitize-address-use-after-scope lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,9) { diff --git a/oscar/zip.cpp b/oscar/zip.cpp index 60a439c8..48c51fa8 100644 --- a/oscar/zip.cpp +++ b/oscar/zip.cpp @@ -225,7 +225,7 @@ bool FileQueue::AddFile(const QString & path, const QString & prefix) return true; } -int FileQueue::Remove(const QString & path) +int FileQueue::Remove(const QString & path, QString* outName) { QFileInfo fi(path); QString canonicalPath = fi.canonicalFilePath(); @@ -235,6 +235,13 @@ int FileQueue::Remove(const QString & path) while (i.hasNext()) { Entry & entry = i.next(); if (entry.path == canonicalPath) { + if (outName) { + // If the caller cares about the name, it will most likely be re-added later rather than skipped. + *outName = entry.name; + } else { + qDebug().noquote() << "skipping file:" << path; + } + if (fi.isDir()) { m_dir_count--; } else { @@ -243,10 +250,12 @@ int FileQueue::Remove(const QString & path) } i.remove(); removed++; - qDebug().noquote() << "skipping file:" << path; } } + if (removed > 1) { + qWarning().noquote() << removed << "copies found in zip queue:" << path; + } return removed; } diff --git a/oscar/zip.h b/oscar/zip.h index d5d2fd8e..c7488f53 100644 --- a/oscar/zip.h +++ b/oscar/zip.h @@ -63,7 +63,7 @@ public: ~FileQueue() = default; //!brief Remove a file from the queue, return the number of instances removed. - int Remove(const QString & path); + int Remove(const QString & path, QString* outName=nullptr); //!brief Recursively add a directory and its contents to the queue along with the prefix to be used in an archive. bool AddDirectory(const QString & path, const QString & prefix="");