Merge branch 'master' into updater

This commit is contained in:
Guy Scharf 2020-07-21 13:51:43 -07:00
commit b0d4fcd628
13 changed files with 297 additions and 52 deletions

View File

@ -92,7 +92,7 @@ void DeviceConnectionManager::replay(QFile* file)
// Return singleton instance of DeviceConnectionManager, creating it if necessary. // Return singleton instance of DeviceConnectionManager, creating it if necessary.
inline DeviceConnectionManager & DeviceConnectionManager::getInstance() DeviceConnectionManager & DeviceConnectionManager::getInstance()
{ {
static DeviceConnectionManager instance; static DeviceConnectionManager instance;
return instance; return instance;

View File

@ -13,7 +13,6 @@
// connections to devices. For now it just supports serial ports. // connections to devices. For now it just supports serial ports.
#include <QtSerialPort/QSerialPort> #include <QtSerialPort/QSerialPort>
#include <QXmlStreamWriter>
#include <QHash> #include <QHash>
#include <QVariant> #include <QVariant>
@ -263,6 +262,9 @@ public:
* supports Bluetooth and BLE as well as serial. Such a class might then be * supports Bluetooth and BLE as well as serial. Such a class might then be
* used instead of port "name" between DeviceConnectionManager and clients. * used instead of port "name" between DeviceConnectionManager and clients.
*/ */
class QXmlStreamWriter;
class QXmlStreamReader;
class SerialPortInfo class SerialPortInfo
{ {
public: public:

View File

@ -116,6 +116,14 @@ void XmlRecorder::epilogue()
m_xml->writeEndElement(); // close enclosing tag 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) XmlReplay::XmlReplay(QFile* file, const QString & tag)
: m_tag(tag), m_file(file), m_pendingSignal(nullptr), m_parent(nullptr) : 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) { if (writer != nullptr) {
writer->lock(); writer->lock();
writer->xml() << *this; writer->xml() << *this;
writer->flush();
writer->unlock(); writer->unlock();
} }
} }

View File

@ -54,6 +54,7 @@ public:
inline QXmlStreamWriter & xml() { return *m_xml; } inline QXmlStreamWriter & xml() { return *m_xml; }
inline void lock() { m_mutex.lock(); } inline void lock() { m_mutex.lock(); }
inline void unlock() { m_mutex.unlock(); } inline void unlock() { m_mutex.unlock(); }
void flush();
protected: protected:
XmlRecorder(XmlRecorder* parent, const QString & id, const QString & tag); // constructor used by substreams XmlRecorder(XmlRecorder* parent, const QString & id, const QString & tag); // constructor used by substreams

View File

@ -8,8 +8,9 @@
* for more details. */ * for more details. */
#include "logger.h" #include "logger.h"
#include "SleepLib/preferences.h"
#define ASSERTS_SUCK #include "version.h"
#include <QDir>
QThreadPool * otherThreadPool = NULL; QThreadPool * otherThreadPool = NULL;
@ -47,12 +48,9 @@ void MyOutputHandler(QtMsgType type, const QMessageLogContext &context, const QS
if (logger && logger->isRunning()) { if (logger && logger->isRunning()) {
logger->append(msg); 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) { if (type == QtFatalMsg) {
abort(); abort();
@ -84,15 +82,65 @@ void LogThread::connectionReady()
{ {
strlock.lock(); strlock.lock();
connected = true; connected = true;
logTrigger.wakeAll();
strlock.unlock(); strlock.unlock();
qDebug() << "Logging UI initialized"; 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() void shutdownLogger()
{ {
if (logger) { if (logger) {
logger->quit(); logger->quit();
otherThreadPool->waitForDone(-1); // The thread is automatically destroyed when its run() method exits.
otherThreadPool->waitForDone(-1); // wait until that happens
logger = NULL; logger = NULL;
} }
delete otherThreadPool; delete otherThreadPool;
@ -103,47 +151,132 @@ LogThread * logger = NULL;
void LogThread::append(QString msg) void LogThread::append(QString msg)
{ {
QString tmp = QString("%1: %2").arg(logtime.elapsed(), 5, 10, QChar('0')).arg(msg); QString tmp = QString("%1: %2").arg(logtime.elapsed(), 5, 10, QChar('0')).arg(msg);
//QStringList appears not to be threadsafe appendClean(tmp);
strlock.lock();
buffer.append(tmp);
strlock.unlock();
} }
void LogThread::appendClean(QString msg) void LogThread::appendClean(QString msg)
{ {
fprintf(stderr, "%s\n", msg.toLocal8Bit().constData());
strlock.lock(); strlock.lock();
buffer.append(msg); buffer.append(msg);
logTrigger.wakeAll();
strlock.unlock(); strlock.unlock();
} }
void LogThread::quit() { void LogThread::quit() {
qDebug() << "Shutting down logging thread"; qDebug() << "Shutting down logging thread";
running = false; qInstallMessageHandler(0); // Remove our logger.
strlock.lock(); strlock.lock();
while (!buffer.isEmpty()) { running = false; // Force the thread to exit after its next iteration.
QString msg = buffer.takeFirst(); logTrigger.wakeAll(); // Trigger the final flush.
fprintf(stderr, "%s\n", msg.toLocal8Bit().constData()); strlock.unlock(); // Release the lock so that the thread can complete.
}
strlock.unlock();
} }
void LogThread::run() void LogThread::run()
{ {
QMutexLocker lock(&strlock);
running = true; running = true;
s_LoggerRunning.unlock(); // unlock as soon as the thread begins to run s_LoggerRunning.unlock(); // unlock as soon as the thread begins to run
do { do {
strlock.lock(); logTrigger.wait(&strlock); // releases strlock while it waits
//int r = receivers(SIGNAL(outputLog(QString()))); while (connected && m_logFile && !buffer.isEmpty()) {
while (connected && !buffer.isEmpty()) {
QString msg = buffer.takeFirst(); QString msg = buffer.takeFirst();
fprintf(stderr, "%s\n", msg.toLocal8Bit().data()); if (m_logStream) {
emit outputLog(msg); *m_logStream << msg << endl;
}
emit outputLog(msg);
} }
strlock.unlock();
QThread::msleep(1000);
} while (running); } 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();
}
}
}
}

View File

@ -5,25 +5,32 @@
#include <QRunnable> #include <QRunnable>
#include <QThreadPool> #include <QThreadPool>
#include <QMutex> #include <QMutex>
#include <QWaitCondition>
#include <QTime> #include <QTime>
void initializeLogger(); void initializeLogger();
void shutdownLogger(); void shutdownLogger();
QString GetLogDir();
void rotateLogs(const QString & filePath, int maxPrevious=-1);
void MyOutputHandler(QtMsgType type, const QMessageLogContext &context, const QString &msgtxt); void MyOutputHandler(QtMsgType type, const QMessageLogContext &context, const QString &msgtxt);
class LogThread:public QObject, public QRunnable class LogThread:public QObject, public QRunnable
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit LogThread() : QRunnable() { running = false; logtime.start(); connected = false; } explicit LogThread() : QRunnable() { running = false; logtime.start(); connected = false; m_logFile = nullptr; m_logStream = nullptr; }
virtual ~LogThread() {} virtual ~LogThread();
void run(); void run();
void append(QString msg); void append(QString msg);
void appendClean(QString msg); void appendClean(QString msg);
bool isRunning() { return running; } bool isRunning() { return running; }
void connectionReady(); void connectionReady();
void logToFile();
QString logFileName();
void quit(); void quit();
@ -36,6 +43,9 @@ protected:
volatile bool running; volatile bool running;
QTime logtime; QTime logtime;
bool connected; bool connected;
class QFile* m_logFile;
class QTextStream* m_logStream;
QWaitCondition logTrigger;
}; };
extern LogThread * logger; extern LogThread * logger;

View File

@ -28,6 +28,7 @@
#include "SleepLib/profiles.h" #include "SleepLib/profiles.h"
#include "translation.h" #include "translation.h"
#include "SleepLib/common.h" #include "SleepLib/common.h"
#include "SleepLib/deviceconnection.h"
#include <ctime> #include <ctime>
#include <chrono> #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!) // Initialize preferences system (Don't use p_pref before this point!)
@ -636,6 +647,21 @@ int main(int argc, char *argv[]) {
MD300W1Loader::Register(); MD300W1Loader::Register();
ViatomLoader::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... schema::setOrders(); // could be called in init...
// Scan for user profiles // Scan for user profiles
@ -653,7 +679,11 @@ int main(int argc, char *argv[]) {
mainwin->SetupGUI(); mainwin->SetupGUI();
mainwin->show(); mainwin->show();
return a.exec(); int result = a.exec();
DeviceConnectionManager::getInstance().record(nullptr);
return result;
} }
#endif // !UNITTEST_MODE #endif // !UNITTEST_MODE

View File

@ -2682,7 +2682,7 @@ void MainWindow::on_actionCreate_Card_zip_triggered()
infostr = datacard.loader->loaderName(); infostr = datacard.loader->loaderName();
} }
prefix = infostr; 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)")); 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() void MainWindow::on_actionCreate_OSCAR_Data_zip_triggered()
{ {
QString folder; 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. // Note: macOS ignores this and points to OSCAR's most recently used directory for saving.
folder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); 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)")); 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; qDebug() << "Create zip of OSCAR data folder:" << filename;
QDir oscarData(GetAppData()); QDir oscarData(GetAppData());
QFile debugLog(oscarData.canonicalPath() + QDir::separator() + "debuglog.txt");
ZipFile z; ZipFile z;
bool ok = z.Open(filename); bool ok = z.Open(filename);
@ -2759,29 +2808,22 @@ void MainWindow::on_actionCreate_OSCAR_Data_zip_triggered()
prog->setWindowModality(Qt::ApplicationModal); prog->setWindowModality(Qt::ApplicationModal);
prog->open(); prog->open();
// Build the list of files and exclude any existing debug log. // Build the list of files.
FileQueue files; FileQueue files;
files.AddDirectory(oscarData.canonicalPath(), oscarData.dirName()); 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...")); prog->setMessage(tr("Creating zip..."));
// Create the zip. // Create the zip.
ok = z.AddFiles(files, prog); 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(); z.Close();
} else { } else {
qWarning() << "Unable to open" << filename; qWarning() << "Unable to open" << filename;

View File

@ -344,6 +344,8 @@ class MainWindow : public QMainWindow
void on_actionCreate_Card_zip_triggered(); void on_actionCreate_Card_zip_triggered();
void on_actionCreate_Log_zip_triggered();
void on_actionCreate_OSCAR_Data_zip_triggered(); void on_actionCreate_OSCAR_Data_zip_triggered();
void on_actionReport_a_Bug_triggered(); void on_actionReport_a_Bug_triggered();

View File

@ -2866,6 +2866,7 @@ p, li { white-space: pre-wrap; }
<addaction name="actionShow_Performance_Counters"/> <addaction name="actionShow_Performance_Counters"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionCreate_Card_zip"/> <addaction name="actionCreate_Card_zip"/>
<addaction name="actionCreate_Log_zip"/>
<addaction name="actionCreate_OSCAR_Data_zip"/> <addaction name="actionCreate_OSCAR_Data_zip"/>
<addaction name="actionReport_a_Bug"/> <addaction name="actionReport_a_Bug"/>
<addaction name="separator"/> <addaction name="separator"/>
@ -3232,6 +3233,11 @@ p, li { white-space: pre-wrap; }
<string>Create zip of CPAP data card</string> <string>Create zip of CPAP data card</string>
</property> </property>
</action> </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"> <action name="actionCreate_OSCAR_Data_zip">
<property name="text"> <property name="text">
<string>Create zip of all OSCAR data</string> <string>Create zip of all OSCAR data</string>

View File

@ -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 # Create a debug GUI build by adding "CONFIG+=memdebug" to your qmake command
memdebug { memdebug {
CONFIG += debug
!win32 { # add memory checking on Linux and macOS debug builds !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 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) { lessThan(QT_MAJOR_VERSION,5)|lessThan(QT_MINOR_VERSION,9) {

View File

@ -225,7 +225,7 @@ bool FileQueue::AddFile(const QString & path, const QString & prefix)
return true; return true;
} }
int FileQueue::Remove(const QString & path) int FileQueue::Remove(const QString & path, QString* outName)
{ {
QFileInfo fi(path); QFileInfo fi(path);
QString canonicalPath = fi.canonicalFilePath(); QString canonicalPath = fi.canonicalFilePath();
@ -235,6 +235,13 @@ int FileQueue::Remove(const QString & path)
while (i.hasNext()) { while (i.hasNext()) {
Entry & entry = i.next(); Entry & entry = i.next();
if (entry.path == canonicalPath) { 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()) { if (fi.isDir()) {
m_dir_count--; m_dir_count--;
} else { } else {
@ -243,10 +250,12 @@ int FileQueue::Remove(const QString & path)
} }
i.remove(); i.remove();
removed++; removed++;
qDebug().noquote() << "skipping file:" << path;
} }
} }
if (removed > 1) {
qWarning().noquote() << removed << "copies found in zip queue:" << path;
}
return removed; return removed;
} }

View File

@ -63,7 +63,7 @@ public:
~FileQueue() = default; ~FileQueue() = default;
//!brief Remove a file from the queue, return the number of instances removed. //!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. //!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=""); bool AddDirectory(const QString & path, const QString & prefix="");