/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2020, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU General Public License as published by the Free Software * Foundation, GNU version 3. This project is distributed in the hope that it * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with * this project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "debugreportgenerator.h" #include "loggingcategories.h" #include "nymeasettings.h" #include "nymeacore.h" #include "nymeaconfiguration.h" #include "version.h" #include #include #include #include #include #include #include #include #include #include namespace nymeaserver { DebugReportGenerator::DebugReportGenerator(QObject *parent) : QObject(parent) { } DebugReportGenerator::~DebugReportGenerator() { // Clean up any leftover files cleanupReport(); } QByteArray DebugReportGenerator::reportFileData() const { return m_reportFileData; } QString DebugReportGenerator::reportFileName() { return m_reportFileName; } QString DebugReportGenerator::md5Sum() const { return m_md5Sum; } bool DebugReportGenerator::isReady() const { return m_isReady; } bool DebugReportGenerator::isValid() const { return m_isValid; } void DebugReportGenerator::generateReport() { qCDebug(dcDebugServer()) << "Start generating debug report"; m_reportFileName = QDateTime::currentDateTime().toString("yyyyMMddhhmm") + "-nymea-debug-report"; m_reportDirectory.setPath(QString("%1/%2").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).arg(m_reportFileName)); //m_reportDirectory.setPath(QString("/tmp/%1").arg(m_reportFileName)); if (!m_reportDirectory.exists()) { qCDebug(dcDebugServer()) << "Create temporary folder to collect the data" << m_reportDirectory.path(); if (!m_reportDirectory.mkpath(m_reportDirectory.path())) { qCWarning(dcDebugServer()) << "Could not create output directory for debug report"; emit finished(false); return; } m_reportDirectory.mkpath(m_reportDirectory.path() + "/config"); m_reportDirectory.mkpath(m_reportDirectory.path() + "/network"); m_reportDirectory.mkpath(m_reportDirectory.path() + "/logs"); } m_reportFileName += ".tar.gz"; saveConfigs(); saveLogFiles(); saveEnv(); saveSystemInformation(); QProcess *pingProcess = new QProcess(this); pingProcess->setProcessChannelMode(QProcess::MergedChannels); connect(pingProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onPingProcessFinished(int,QProcess::ExitStatus))); QProcess *digProcess = new QProcess(this); digProcess->setProcessChannelMode(QProcess::MergedChannels); connect(digProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onDigProcessFinished(int,QProcess::ExitStatus))); QProcess *tracePathProcess = new QProcess(this); tracePathProcess->setProcessChannelMode(QProcess::MergedChannels); connect(tracePathProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onTracePathProcessFinished(int,QProcess::ExitStatus))); m_runningProcesses.append(pingProcess); m_runningProcesses.append(digProcess); m_runningProcesses.append(tracePathProcess); pingProcess->start("ping", { "-c", "4", "nymea.io" } ); digProcess->start("dig", { "nymea.io" } ); tracePathProcess->start("tracepath", { "nymea.io" } ); } void DebugReportGenerator::copyFileToReportDirectory(const QString &fileName, const QString &subDirectory) { QFileInfo fileInfo(fileName); if (fileInfo.exists()) { QString destination = m_reportDirectory.path() + "/" + subDirectory; if (!QFile::copy(fileName, destination + "/" + fileInfo.fileName())) { qCWarning(dcDebugServer()) << "Could not copy file" << fileName << "to" << destination; } else { qCDebug(dcDebugServer()) << "Copy file" << fileName << "-->" << destination; } } } void DebugReportGenerator::verifyRunningProcessesFinished() { if (m_runningProcesses.isEmpty()) { qCDebug(dcDebugServer()) << "All async processes are finished. Start compressing the file."; m_compressProcess = new QProcess(this); m_compressProcess->setProcessChannelMode(QProcess::MergedChannels); m_compressProcess->setWorkingDirectory(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); connect(m_compressProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onCompressProcessFinished(int, QProcess::ExitStatus))); m_compressProcess->start("tar", { "-zcf", m_reportFileName, "-C", QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/", m_reportDirectory.dirName() } ); qCDebug(dcDebugServer()) << "Execut command" << m_compressProcess->program() << m_compressProcess->arguments(); } } void DebugReportGenerator::saveSystemInformation() { QFile outputFile(m_reportDirectory.path() + "/sysinfo.txt"); if (!outputFile.open(QIODevice::ReadWrite)) { qCWarning(dcDebugServer()) << "Could not open sysinfo file" << outputFile.fileName(); return; } qCDebug(dcDebugServer()) << "Write system information file" << outputFile.fileName(); QTextStream stream(&outputFile); stream << "Server name: " << NymeaCore::instance()->configuration()->serverName() << endl; stream << "Server version: " << NYMEA_VERSION_STRING << endl; stream << "JSON-RPC version: " << JSON_PROTOCOL_VERSION << endl; stream << "Language: " << NymeaCore::instance()->configuration()->locale().name() << " (" << NymeaCore::instance()->configuration()->locale().nativeCountryName() << " - " << NymeaCore::instance()->configuration()->locale().nativeLanguageName() << ")" << endl; stream << "Timezone: " << QString::fromUtf8(NymeaCore::instance()->configuration()->timeZone()) << endl; stream << "Server UUID: " << NymeaCore::instance()->configuration()->serverUuid().toString() << endl; stream << "Settings path: " << NymeaSettings::settingsPath() << endl; stream << "Translations path: " << NymeaSettings(NymeaSettings::SettingsRoleGlobal).translationsPath() << endl; stream << "User: " << qgetenv("USER") << endl; stream << "Command: " << QCoreApplication::arguments().join(' ') << endl; stream << "Qt runtime version: " << qVersion() << endl; stream << "" << endl; #if (QT_VERSION < QT_VERSION_CHECK(5, 6, 0)) stream << "Hostname: " << QHostInfo::localHostName() << endl; #else stream << "Hostname: " << QSysInfo::machineHostName() << endl; #endif stream << "Architecture: " << QSysInfo::currentCpuArchitecture() << endl; stream << "Kernel type: " << QSysInfo::kernelType() << endl; stream << "Kernel version: " << QSysInfo::kernelVersion() << endl; stream << "Product type: " << QSysInfo::productType() << endl; stream << "Product version: " << QSysInfo::productVersion() << endl; outputFile.close(); } void DebugReportGenerator::saveLogFiles() { QDir logDir("/var/log/"); QStringList syslogFiles = logDir.entryList(QStringList() << "syslog*" << "nymea.*", QDir::Files); foreach (const QString &logFile, syslogFiles) { copyFileToReportDirectory(logDir.path() + "/" + logFile, "logs"); } } void DebugReportGenerator::saveConfigs() { // Start copy files setting files copyFileToReportDirectory(NymeaSettings(NymeaSettings::SettingsRoleGlobal).fileName(), "config"); copyFileToReportDirectory(NymeaSettings(NymeaSettings::SettingsRoleThings).fileName(), "config"); copyFileToReportDirectory(NymeaSettings(NymeaSettings::SettingsRoleThingStates).fileName(), "config"); copyFileToReportDirectory(NymeaSettings(NymeaSettings::SettingsRoleRules).fileName(), "config"); copyFileToReportDirectory(NymeaSettings(NymeaSettings::SettingsRolePlugins).fileName(), "config"); copyFileToReportDirectory(NymeaSettings(NymeaSettings::SettingsRoleTags).fileName(), "config"); copyFileToReportDirectory(NymeaCore::instance()->configuration()->logDBName(), "config"); } void DebugReportGenerator::saveEnv() { QFile outputFile(m_reportDirectory.path() + "/env.txt"); if (!outputFile.open(QIODevice::ReadWrite)) { qCWarning(dcDebugServer()) << "Could not open env file" << outputFile.fileName(); return; } QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QTextStream stream(&outputFile); foreach(const QString &key, env.keys()) { qCDebug(dcDebugServer()) << "Process environment:" << key << "-->" << env.value(key); stream << key << "=" << env.value(key) << "\n"; } outputFile.close(); } void DebugReportGenerator::cleanupReport() { QFile reportFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + m_reportFileName); if (reportFile.exists()) { qCDebug(dcDebugServer()) << "Delete report file" << reportFile.fileName(); if (!reportFile.remove()) { qCWarning(dcDebugServer()) << "Could not delete report file" << reportFile.fileName(); } } if (m_reportDirectory.exists()) { qCDebug(dcDebugServer()) << "Clean up report directory" << m_reportDirectory.path(); if (!m_reportDirectory.removeRecursively()) { qCWarning(dcDebugServer()) << "Could not delete report directory" << m_reportDirectory.path(); } } } void DebugReportGenerator::onPingProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { QProcess *process = static_cast(sender()); qCDebug(dcDebugServer()) << "Ping process finished" << exitCode << exitStatus; QByteArray processOutput = process->readAll(); QFile outputFile(m_reportDirectory.path() + "/network/ping.txt"); if (!outputFile.open(QIODevice::ReadWrite)) { qCWarning(dcDebugServer()) << "Could not open ping file" << outputFile.fileName(); } else { qCDebug(dcDebugServer()) << "Write result into file" << outputFile.fileName(); outputFile.write(processOutput); outputFile.close(); } m_runningProcesses.removeAll(process); process->deleteLater(); process = nullptr; verifyRunningProcessesFinished(); } void DebugReportGenerator::onDigProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { QProcess *process = static_cast(sender()); qCDebug(dcDebugServer()) << "Dig process finished" << exitCode << exitStatus; QByteArray processOutput = process->readAll(); QFile outputFile(m_reportDirectory.path() + "/network/dns-lookup.txt"); if (!outputFile.open(QIODevice::ReadWrite)) { qCWarning(dcDebugServer()) << "Could not open dig file" << outputFile.fileName(); } else { qCDebug(dcDebugServer()) << "Write result into file" << outputFile.fileName(); outputFile.write(processOutput); outputFile.close(); } m_runningProcesses.removeAll(process); process->deleteLater(); process = nullptr; verifyRunningProcessesFinished(); } void DebugReportGenerator::onTracePathProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { QProcess *process = static_cast(sender()); qCDebug(dcDebugServer()) << "Tracepath process finished" << exitCode << exitStatus; QByteArray processOutput = process->readAll(); QFile outputFile(m_reportDirectory.path() + "/network/tracepath.txt"); if (!outputFile.open(QIODevice::ReadWrite)) { qCWarning(dcDebugServer()) << "Could not open dig file" << outputFile.fileName(); } else { qCDebug(dcDebugServer()) << "Write result into file" << outputFile.fileName(); outputFile.write(processOutput); outputFile.close(); } m_runningProcesses.removeAll(process); process->deleteLater(); process = nullptr; verifyRunningProcessesFinished(); } void DebugReportGenerator::onCompressProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { QProcess *process = static_cast(sender()); qCDebug(dcDebugServer()) << "Compress process finished" << exitCode << exitStatus; qCDebug(dcDebugServer()) << "Clean up report directory" << m_reportDirectory.path(); if (!m_reportDirectory.removeRecursively()) { qCWarning(dcDebugServer()) << "Could not delete report directory" << m_reportDirectory.path(); } // Read the file QFile reportFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + m_reportFileName); if (!reportFile.open(QIODevice::ReadOnly)) { qCWarning(dcDebugServer()) << "Could not open report file name for reading" << reportFile.fileName(); m_isReady = true; m_isValid = false; emit finished(false); } else { m_reportFileData = reportFile.readAll(); m_md5Sum = QString::fromUtf8(QCryptographicHash::hash(m_reportFileData, QCryptographicHash::Md5).toHex()); qCDebug(dcDebugServer()) << "File generated successfully" << reportFile.fileName() << m_reportFileData.size() << "B" << m_md5Sum; m_isReady = true; m_isValid = true; emit finished(true); } reportFile.close(); // When this timer expires, the debug report is not valid any more and will be deleted QTimer::singleShot(120000, this, &DebugReportGenerator::timeout); process->deleteLater(); process = nullptr; } }