diff --git a/data/debug-interface/script.js b/data/debug-interface/script.js index aafd9528..65dc28a8 100644 --- a/data/debug-interface/script.js +++ b/data/debug-interface/script.js @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Copyright (C) 2018 Simon Stürz * + * Copyright (C) 2018-2019 Simon Stürz * * * * This file is part of nymea. * * * @@ -44,7 +44,7 @@ function selectSection(event, section) { /* ========================================================================*/ -/* Websocket connection +/* Websocket connection for log live view /* ========================================================================*/ var webSocket = null; @@ -126,39 +126,45 @@ function downloadFile(filePath, fileName) { } +/* ========================================================================*/ +/* Generate report +/* ========================================================================*/ + var generateReportTimer = null; -function generateReport() { - console.log("Requesting to generate report file " + "/debug/report"); - - var textArea = document.getElementById("generateReportTextArea"); - var button = document.getElementById("generateReportButton"); - +function pollReportResult() { // Request report file generation var reportGenerateRequest = new XMLHttpRequest(); reportGenerateRequest.open("GET", "/debug/report", true); reportGenerateRequest.send(null); - button.disabled = true; - textArea.value = "."; - - // Start the timer - generateReportTimer = setTimeout(generateReportTimerTimeout, 1000); - reportGenerateRequest.onreadystatechange = function() { if (reportGenerateRequest.readyState == 4) { console.log("Report generation finished with " + reportGenerateRequest.status); + /* 204: the report is not ready yet. */ + if (reportGenerateRequest.status == 204) { + // Restart poll timer + generateReportTimer = setTimeout(generateReportTimerTimeout, 1000); + return; + } + + /* Check if the generation went fine */ if (reportGenerateRequest.status != 200) { console.log("Report generation finished with error."); clearTimeout(generateReportTimer); - textArea.value = "Something went wrong :("; + textArea.value = "Something went wrong :(" + reportGenerateRequest.status; button.disabled = false; return; } - // Stop the timer + /* The report is finished! Show information and start downloading it. */ + + /* Stop the timer */ clearTimeout(generateReportTimer); + var textArea = document.getElementById("generateReportTextArea"); + var button = document.getElementById("generateReportButton"); + console.log(reportGenerateRequest.responseText); var responseMap = JSON.parse(reportGenerateRequest.responseText); @@ -190,10 +196,24 @@ function generateReport() { }; } -function generateReportTimerTimeout() { +function generateReport() { + console.log("Requesting to generate report file " + "/debug/report"); + + var textArea = document.getElementById("generateReportTextArea"); + var button = document.getElementById("generateReportButton"); + + button.disabled = true; + textArea.value = "."; + + pollReportResult(); + +} + +function generateReportTimerTimeout() { var textArea = document.getElementById("generateReportTextArea"); textArea.value += "."; - generateReportTimer = setTimeout(generateReportTimerTimeout, 1000); + + pollReportResult(); } /* ========================================================================*/ diff --git a/libnymea-core/debugreportgenerator.cpp b/libnymea-core/debugreportgenerator.cpp index b0a112eb..b8318103 100644 --- a/libnymea-core/debugreportgenerator.cpp +++ b/libnymea-core/debugreportgenerator.cpp @@ -28,7 +28,9 @@ #include #include #include +#include #include +#include #include #include @@ -41,6 +43,7 @@ DebugReportGenerator::DebugReportGenerator(QObject *parent) : QObject(parent) DebugReportGenerator::~DebugReportGenerator() { + // Clean up any leftover files cleanupReport(); } @@ -59,6 +62,16 @@ 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"; @@ -83,6 +96,7 @@ void DebugReportGenerator::generateReport() saveConfigs(); saveLogFiles(); saveEnv(); + saveSystemInformation(); QProcess *pingProcess = new QProcess(this); pingProcess->setProcessChannelMode(QProcess::MergedChannels); @@ -131,6 +145,37 @@ void DebugReportGenerator::verifyRunningProcessesFinished() } } +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; + stream << "Hostname: " << QSysInfo::machineHostName() << endl; + 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/"); @@ -269,18 +314,22 @@ void DebugReportGenerator::onCompressProcessFinished(int exitCode, QProcess::Exi 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(); - // Todo: start expire timer - QTimer::singleShot(30000, this, &DebugReportGenerator::timeout); + // 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; diff --git a/libnymea-core/debugreportgenerator.h b/libnymea-core/debugreportgenerator.h index 14b7f29d..a34e6fd6 100644 --- a/libnymea-core/debugreportgenerator.h +++ b/libnymea-core/debugreportgenerator.h @@ -34,15 +34,21 @@ public: explicit DebugReportGenerator(QObject *parent = nullptr); ~DebugReportGenerator(); + QByteArray reportFileData() const; QString reportFileName(); QString md5Sum() const; + bool isReady() const; + bool isValid() const; + void generateReport(); private: QDir m_reportDirectory; QString m_reportFileName; + bool m_isReady = false; + bool m_isValid = false; QProcess *m_compressProcess = nullptr; QList m_runningProcesses; @@ -53,6 +59,7 @@ private: void copyFileToReportDirectory(const QString &fileName, const QString &subDirectory = QString()); void verifyRunningProcessesFinished(); + void saveSystemInformation(); void saveLogFiles(); void saveConfigs(); void saveEnv(); diff --git a/libnymea-core/debugserverhandler.cpp b/libnymea-core/debugserverhandler.cpp index a0402f8b..e0afb0ee 100644 --- a/libnymea-core/debugserverhandler.cpp +++ b/libnymea-core/debugserverhandler.cpp @@ -378,35 +378,70 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath, c } if (requestPath.startsWith("/debug/report")) { + + // The client can poll this url in order to get information about the current report generating process. + // If there is currently no report generated, start generating it and inform client that there is a report on the way (204) + // If there is already a report generation in progress, inform the client that it's not ready yet (204) + // If the report is ready, return information of the file and the client will start downloading it (202 and file informations). + // Check if download request or generate request if (requestQuery.hasQueryItem("filename")) { QString fileName = requestQuery.queryItemValue("filename"); qCDebug(dcDebugServer()) << "Report download request for" << fileName; - if (m_finishedReportGenerators.contains(fileName)) { - HttpReply *downloadReportReply = RestResource::createSuccessReply(); - DebugReportGenerator *generator = m_finishedReportGenerators.take(fileName); - downloadReportReply->setPayload(generator->reportFileData()); - downloadReportReply->setHeader(HttpReply::ContentTypeHeader, "application/tar+gzip;"); - generator->deleteLater(); - - return downloadReportReply; - } else { - qCWarning(dcDebugServer()) << "The requested file does not exist any more" << fileName; - HttpReply *downloadReportReply = RestResource::createErrorReply(HttpReply::NotFound); - return downloadReportReply; + if (!m_debugReportGenerator) { + qCWarning(dcDebugServer()) << "There is currently no debug report generator. The requested file does not exist."; + return RestResource::createErrorReply(HttpReply::NotFound); } + if (m_debugReportGenerator->reportFileName() != fileName) { + qCWarning(dcDebugServer()) << "The requested file is not the file from the current debug report generator" << m_debugReportGenerator->reportFileName() << "!=" << fileName; + return RestResource::createErrorReply(HttpReply::NotFound); + + } + + // Everything looks good, send the requested debug report + HttpReply *downloadReportReply = RestResource::createSuccessReply(); + downloadReportReply->setPayload(m_debugReportGenerator->reportFileData()); + downloadReportReply->setHeader(HttpReply::ContentTypeHeader, "application/tar+gzip;"); + return downloadReportReply; } else { - DebugReportGenerator *debugReportGenerator = new DebugReportGenerator(this); - connect(debugReportGenerator, &DebugReportGenerator::finished, this, &DebugServerHandler::onDebugReportGeneratorFinished); - connect(debugReportGenerator, &DebugReportGenerator::timeout, this, &DebugServerHandler::onDebugReportGeneratorTimeout); - debugReportGenerator->generateReport(); + // Generate or poll request + if (!m_debugReportGenerator) { + qCDebug(dcDebugServer()) << "Create new debug report generator and start generating report..."; + m_debugReportGenerator = new DebugReportGenerator(this); + connect(m_debugReportGenerator, &DebugReportGenerator::finished, this, &DebugServerHandler::onDebugReportGeneratorFinished); + connect(m_debugReportGenerator, &DebugReportGenerator::timeout, this, &DebugServerHandler::onDebugReportGeneratorTimeout); + m_debugReportGenerator->generateReport(); + // Note: no content will bring the client to poll this report + return RestResource::createErrorReply(HttpReply::NoContent); + } else { + // There is a running generator, check if the report is ready + if (!m_debugReportGenerator->isReady()) { + qCDebug(dcDebugServer()) << "Report is not ready yet"; + // Note: no content tells the client the report is not ready yet + return RestResource::createErrorReply(HttpReply::NoContent); + } else { + if (m_debugReportGenerator->isValid()) { + // Success, the debug report is ready and valid + QVariantMap reportInformation; + reportInformation.insert("fileName", m_debugReportGenerator->reportFileName()); + reportInformation.insert("fileSize", m_debugReportGenerator->reportFileData().size()); + reportInformation.insert("md5sum", m_debugReportGenerator->md5Sum()); - HttpReply *debugReportReply = RestResource::createAsyncReply(); - m_runningReportGenerators.insert(debugReportGenerator, debugReportReply); - - return debugReportReply; + HttpReply * httpReply = RestResource::createSuccessReply(); + httpReply->setHttpStatusCode(HttpReply::Ok); + httpReply->setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); + httpReply->setPayload(QJsonDocument::fromVariant(reportInformation).toJson(QJsonDocument::Indented)); + return httpReply; + } else { + qCWarning(dcDebugServer()) << "The debug report generator finished with error."; + m_debugReportGenerator->deleteLater(); + m_debugReportGenerator = nullptr; + return RestResource::createErrorReply(HttpReply::InternalServerError); + } + } + } } } @@ -496,7 +531,6 @@ HttpReply *DebugServerHandler::processDebugFileRequest(const QString &requestPat return reply; } - void DebugServerHandler::onDebugServerEnabledChanged(bool enabled) { if (enabled) { @@ -605,32 +639,14 @@ void DebugServerHandler::onDebugReportGeneratorFinished(bool success) { DebugReportGenerator *debugReportGenerator = static_cast(sender()); qCDebug(dcDebugServer()) << "Report generation finished" << (success ? "successfully" : "with error") << debugReportGenerator->reportFileName(); - HttpReply *httpReply = m_runningReportGenerators.take(debugReportGenerator); - - if (success) { - QVariantMap reportInformation; - reportInformation.insert("fileName", debugReportGenerator->reportFileName()); - reportInformation.insert("fileSize", debugReportGenerator->reportFileData().size()); - reportInformation.insert("md5sum", debugReportGenerator->md5Sum()); - httpReply->setHttpStatusCode(HttpReply::Ok); - httpReply->setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";"); - httpReply->setPayload(QJsonDocument::fromVariant(reportInformation).toJson(QJsonDocument::Indented)); - - m_finishedReportGenerators.insert(debugReportGenerator->reportFileName(), debugReportGenerator); - } else { - httpReply->setHttpStatusCode(HttpReply::InternalServerError); - } - - httpReply->finished(); } void DebugServerHandler::onDebugReportGeneratorTimeout() { - DebugReportGenerator *debugReportGenerator = static_cast(sender()); - qCWarning(dcDebugServer()) << "Report generation timeouted. Cleaning up" << debugReportGenerator->reportFileName(); - if (m_finishedReportGenerators.values().contains(debugReportGenerator)) { - m_finishedReportGenerators.remove(debugReportGenerator->reportFileName()); - debugReportGenerator->deleteLater(); + qCWarning(dcDebugServer()) << "Debug report expired."; + if (m_debugReportGenerator) { + m_debugReportGenerator->deleteLater(); + m_debugReportGenerator = nullptr; } } @@ -857,7 +873,7 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeEndElement(); // div warning - // System information section + // Server information section writer.writeEmptyElement("hr"); //: The server information section of the debug interface writer.writeTextElement("h2", tr("Server information")); @@ -865,71 +881,6 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeStartElement("table"); - writer.writeStartElement("tr"); - //: The user name in the server infromation section of the debug interface - writer.writeTextElement("th", tr("User")); - writer.writeTextElement("td", qgetenv("USER")); - writer.writeEndElement(); // tr - - writer.writeStartElement("tr"); - //: The Qt build version description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Compiled with Qt version")); - writer.writeTextElement("td", QT_VERSION_STR); - writer.writeEndElement(); // tr - - writer.writeStartElement("tr"); - //: The Qt runtime version description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Qt runtime version")); - writer.writeTextElement("td", qVersion()); - writer.writeEndElement(); // tr - - writer.writeStartElement("tr"); - //: The command description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Command")); - writer.writeTextElement("td", QCoreApplication::arguments().join(' ')); - writer.writeEndElement(); // tr - - if (!qgetenv("SNAP").isEmpty()) { - // Note: http://snapcraft.io/docs/reference/env - - writer.writeStartElement("tr"); - //: The snap name description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Snap name")); - writer.writeTextElement("td", qgetenv("SNAP_NAME")); - writer.writeEndElement(); // tr - - writer.writeStartElement("tr"); - //: The snap version description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Snap version")); - writer.writeTextElement("td", qgetenv("SNAP_VERSION")); - writer.writeEndElement(); // tr - - writer.writeStartElement("tr"); - //: The snap directory description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Snap directory")); - writer.writeTextElement("td", qgetenv("SNAP")); - writer.writeEndElement(); // tr - - writer.writeStartElement("tr"); - //: The snap application data description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Snap application data")); - writer.writeTextElement("td", qgetenv("SNAP_DATA")); - writer.writeEndElement(); // tr - - writer.writeStartElement("tr"); - //: The snap user data description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Snap user data")); - writer.writeTextElement("td", qgetenv("SNAP_USER_DATA")); - writer.writeEndElement(); // tr - - writer.writeStartElement("tr"); - //: The snap common data description in the server infromation section of the debug interface - writer.writeTextElement("th", tr("Snap common data")); - writer.writeTextElement("td", qgetenv("SNAP_COMMON")); - writer.writeEndElement(); // tr - } - - writer.writeStartElement("tr"); //: The server name description in the server infromation section of the debug interface writer.writeTextElement("th", tr("Server name")); @@ -978,6 +929,117 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeTextElement("td", NymeaSettings(NymeaSettings::SettingsRoleGlobal).translationsPath()); writer.writeEndElement(); // tr + writer.writeStartElement("tr"); + //: The user name in the server infromation section of the debug interface + writer.writeTextElement("th", tr("User")); + writer.writeTextElement("td", qgetenv("USER")); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The command description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Command")); + writer.writeTextElement("td", QCoreApplication::arguments().join(' ')); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The Qt build version description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Compiled with Qt version")); + writer.writeTextElement("td", QT_VERSION_STR); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The Qt runtime version description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Qt runtime version")); + writer.writeTextElement("td", qVersion()); + writer.writeEndElement(); // tr + + if (!qgetenv("SNAP").isEmpty()) { + // Note: http://snapcraft.io/docs/reference/env + + writer.writeStartElement("tr"); + //: The snap name description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Snap name")); + writer.writeTextElement("td", qgetenv("SNAP_NAME")); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The snap version description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Snap version")); + writer.writeTextElement("td", qgetenv("SNAP_VERSION")); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The snap directory description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Snap directory")); + writer.writeTextElement("td", qgetenv("SNAP")); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The snap application data description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Snap application data")); + writer.writeTextElement("td", qgetenv("SNAP_DATA")); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The snap user data description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Snap user data")); + writer.writeTextElement("td", qgetenv("SNAP_USER_DATA")); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The snap common data description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Snap common data")); + writer.writeTextElement("td", qgetenv("SNAP_COMMON")); + writer.writeEndElement(); // tr + } + + writer.writeEndElement(); // table + + + // System information section + writer.writeEmptyElement("hr"); + //: The system information section of the debug interface + writer.writeTextElement("h2", tr("System information")); + writer.writeEmptyElement("hr"); + + writer.writeStartElement("table"); + + writer.writeStartElement("tr"); + //: The command description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Hostname")); + writer.writeTextElement("td", QSysInfo::machineHostName()); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The command description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Architecture")); + writer.writeTextElement("td", QSysInfo::currentCpuArchitecture()); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The command description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Kernel type")); + writer.writeTextElement("td", QSysInfo::kernelType()); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The command description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Kernel version")); + writer.writeTextElement("td", QSysInfo::kernelVersion()); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The command description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Product type")); + writer.writeTextElement("td", QSysInfo::productType()); + writer.writeEndElement(); // tr + + writer.writeStartElement("tr"); + //: The command description in the server infromation section of the debug interface + writer.writeTextElement("th", tr("Product version")); + writer.writeTextElement("td", QSysInfo::productVersion()); + writer.writeEndElement(); // tr + writer.writeEndElement(); // table // Generate report @@ -1627,7 +1689,7 @@ QByteArray DebugServerHandler::createDebugXmlDocument() // Footer writer.writeStartElement("div"); writer.writeAttribute("class", "footer"); - writer.writeTextElement("p", QString("Copyright %1 2018 guh GmbH.").arg(QChar(0xA9))); + writer.writeTextElement("p", QString("Copyright %1 %2 guh GmbH.").arg(QChar(0xA9)).arg(COPYRIGHT_YEAR_STRING)); //: The footer license note of the debug interface writer.writeTextElement("p", tr("Released under the GNU GENERAL PUBLIC LICENSE Version 2.")); writer.writeEndElement(); // div footer diff --git a/libnymea-core/debugserverhandler.h b/libnymea-core/debugserverhandler.h index a42b1146..9a701d4f 100644 --- a/libnymea-core/debugserverhandler.h +++ b/libnymea-core/debugserverhandler.h @@ -56,8 +56,7 @@ private: QProcess *m_tracePathProcess = nullptr; HttpReply *m_tracePathReply = nullptr; - QHash m_runningReportGenerators; - QHash m_finishedReportGenerators; + DebugReportGenerator *m_debugReportGenerator = nullptr; QByteArray loadResourceData(const QString &resourceFileName); QString getResourceFileName(const QString &requestPath);