diff --git a/data/debug-interface/script.js b/data/debug-interface/script.js index 50f401f3..b0868549 100644 --- a/data/debug-interface/script.js +++ b/data/debug-interface/script.js @@ -25,7 +25,7 @@ function selectSection(event, section) { - console.log("Selected tab " + section) + console.log("Selected tab " + section); var i, tabcontent, tablinks; tabcontent = document.getElementsByClassName("tabcontent"); @@ -42,6 +42,7 @@ function selectSection(event, section) { event.currentTarget.className += " active"; } + /* ========================================================================*/ /* Websocket connection /* ========================================================================*/ @@ -51,14 +52,15 @@ var webSocketConnected = false; function toggleWebsocketConnection() { if (webSocketConnected) { - disconnectWebsocket() + disconnectWebsocket(); } else { - connectWebsocket() + connectWebsocket(); } } + function connectWebsocket() { - var urlString = "ws://" + window.location.hostname + ":2626" + var urlString = "ws://" + window.location.hostname + ":2626"; console.log("Connecting to: " + urlString); try { @@ -84,7 +86,7 @@ function connectWebsocket() { var message = messageEvent.data; console.log("WebSocket data received: " + message); document.getElementById("logsTextArea").value += message; - document.getElementById("logsTextArea").scrollTop = document.getElementById("logsTextArea").scrollHeight + document.getElementById("logsTextArea").scrollTop = document.getElementById("logsTextArea").scrollHeight; }; } catch (exception) { @@ -93,9 +95,10 @@ function connectWebsocket() { } + function disconnectWebsocket() { console.log("Disconnecting from: " + webSocket.url); - webSocket.close() + webSocket.close(); webSocketConnected = false; document.getElementById("toggleLogsButton").innerHTML = "Start logs"; } @@ -110,6 +113,7 @@ function showFile(path) { window.open(path, '_blank'); } + function downloadFile(filePath, fileName) { console.log("Download file requested " + filePath + " --> " + fileName); var element = document.createElement('a'); @@ -121,6 +125,63 @@ function downloadFile(filePath, fileName) { document.body.removeChild(element); } + +function generateReport() { + console.log("Requesting to generate report file " + "/debug/report"); + + var button = document.getElementById("generateReportButton"); + var textArea = document.getElementById("generateReportTextArea"); + + // Request report file generation + var reportGenerateRequest = new XMLHttpRequest(); + reportGenerateRequest.open("GET", "/debug/report", true); + reportGenerateRequest.send(null); + + button.disabled = true; + textArea.value = ""; + + reportGenerateRequest.onreadystatechange = function() { + if (reportGenerateRequest.readyState == 4) { + console.log("Report generation finished with " + reportGenerateRequest.status); + + if (reportGenerateRequest.status != 200) { + console.log("Report generation finished with error."); + textArea.value = "Something went wrong :("; + button.disabled = false; + return; + } + + console.log(reportGenerateRequest.responseText); + var responseMap = JSON.parse(reportGenerateRequest.responseText); + var fileName = responseMap['fileName']; + var fileSize = responseMap['fileSize']; + var md5Sum = responseMap['md5sum']; + + console.log("Report generation finished. " + fileName + " " + fileSize + "B | " + md5Sum) + + textArea.value = "Report generated successfully: " + fileName + "\n"; + textArea.value += "\n"; + textArea.value += "Size: " + fileSize + " Bytes" + "\n"; + textArea.value += "MD5 checksum: " + md5Sum + "\n"; + + // Now download the generated report + var fileRequestUrl = "/debug/report?filename=" + fileName; + console.log("Download report file " + fileRequestUrl); + var element = document.createElement('a'); + element.setAttribute('href', fileRequestUrl); + element.setAttribute('download', fileName); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + + // Enable button again + button.disabled = false; + } + }; +} + + /* ========================================================================*/ /* Network test functions /* ========================================================================*/ @@ -136,16 +197,17 @@ function startPingTest() { var request = new XMLHttpRequest(); request.open("GET", "/debug/ping", true); request.send(null); - button.disabled = true + button.disabled = true; request.onreadystatechange = function() { if (request.readyState == 4) { console.log(request.responseText); - textArea.value = request.responseText - button.disabled = false + textArea.value = request.responseText; + button.disabled = false; } }; } + function startDigTest() { console.log("Start dig test"); var textArea = document.getElementById("digTextArea"); @@ -158,16 +220,17 @@ function startDigTest() { var request = new XMLHttpRequest(); request.open("GET", "/debug/dig", true); request.send(null); - button.disabled = true + button.disabled = true; request.onreadystatechange = function() { if (request.readyState == 4) { console.log(request.responseText); - textArea.value = request.responseText - button.disabled = false + textArea.value = request.responseText; + button.disabled = false; } }; } + function startTracePathTest() { console.log("Start trace path test"); var textArea = document.getElementById("tracePathTextArea"); @@ -180,12 +243,12 @@ function startTracePathTest() { var request = new XMLHttpRequest(); request.open("GET", "/debug/tracepath", true); request.send(null); - button.disabled = true + button.disabled = true; request.onreadystatechange = function() { if (request.readyState == 4) { console.log(request.responseText); - textArea.value = request.responseText - button.disabled = false + textArea.value = request.responseText; + button.disabled = false; } }; } diff --git a/data/debug-interface/styles.css b/data/debug-interface/styles.css index 2720258e..86b041c8 100644 --- a/data/debug-interface/styles.css +++ b/data/debug-interface/styles.css @@ -87,7 +87,7 @@ textarea { cursor: pointer; padding: 14px 16px; opacity: 0.8; - transition: 0.3s; + transition: 0.5s; font-size: 18px; font-family: "Ubuntu", Helvetica, "Helvetica Neue", Arial; } @@ -102,7 +102,7 @@ textarea { .tabcontent { display: none; - animation: fadeEffect 0.5s; + animation: fadeEffect 0.8s; } @keyframes fadeEffect { @@ -113,6 +113,7 @@ textarea { .console-textarea { color: white; margin-top: 20px; + margin-bottom: 20px; padding: 15px; font-family: "Ubuntu Mono", "monospace"; font-size: 100%; @@ -128,6 +129,8 @@ textarea { .warning { background-color: #ed3146; + margin-top: 20px; + margin-bottom: 20px; border-radius: 10px; opacity: 0.8; width: 80%; @@ -175,7 +178,7 @@ textarea { .download-path-column { float: left; - width: 40%; + width: 30%; padding: 10px; } diff --git a/debian/control b/debian/control index b6cf8bea..593c38bb 100644 --- a/debian/control +++ b/debian/control @@ -59,12 +59,15 @@ Depends: libqt5network5, logrotate, avahi-daemon, bluez, + tar, + iputils-tracepath, + iputils-ping, + dnsutils, nymea-translations, libnymea1 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends} -Recommends: nymea-webinterface, - nymea-cli, +Recommends: nymea-cli, network-manager Replaces: guhd Description: An open source IoT server - daemon diff --git a/libnymea-core/debugreportgenerator.cpp b/libnymea-core/debugreportgenerator.cpp new file mode 100644 index 00000000..ea23f301 --- /dev/null +++ b/libnymea-core/debugreportgenerator.cpp @@ -0,0 +1,266 @@ +#include "debugreportgenerator.h" +#include "loggingcategories.h" +#include "nymeasettings.h" +#include "nymeacore.h" + +#include +#include +#include +#include +#include +#include + +namespace nymeaserver { + +DebugReportGenerator::DebugReportGenerator(QObject *parent) : QObject(parent) +{ + +} + +DebugReportGenerator::~DebugReportGenerator() +{ + cleanupReport(); +} + +QByteArray DebugReportGenerator::reportFileData() const +{ + return m_reportFileData; +} + +QString DebugReportGenerator::reportFileName() +{ + return m_reportFileName; +} + +QString DebugReportGenerator::md5Sum() const +{ + return m_md5Sum; +} + +void DebugReportGenerator::generateReport() +{ + qCDebug(dcDebugServer()) << "Start generating debug report"; + m_reportFileName = QDateTime::currentDateTime().toString("yyyyMMddhhmm") + "-nymea-debug-report"; + + 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 += ".tag.gz"; + + saveConfigs(); + saveLogFiles(); + saveEnv(); + + 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("/tmp"); + connect(m_compressProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onCompressProcessFinished(int, QProcess::ExitStatus))); + m_compressProcess->start("tar", { "-zcf", m_reportFileName, "-C", "/tmp/", m_reportDirectory.dirName() } ); + qCDebug(dcDebugServer()) << "Execut command" << m_compressProcess->program() << m_compressProcess->arguments(); + } +} + +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::SettingsRoleDevices).fileName(), "config"); + copyFileToReportDirectory(NymeaSettings(NymeaSettings::SettingsRoleDeviceStates).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("/tmp/" + 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("/tmp/" + m_reportFileName); + if (!reportFile.open(QIODevice::ReadOnly)) { + qCWarning(dcDebugServer()) << "Could not open report file name for reading" << reportFile.fileName(); + 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; + emit finished(true); + } + + reportFile.close(); + + // Todo: start expire timer + QTimer::singleShot(30000, this, &DebugReportGenerator::timeout); + + process->deleteLater(); + process = nullptr; +} + +} diff --git a/libnymea-core/debugreportgenerator.h b/libnymea-core/debugreportgenerator.h new file mode 100644 index 00000000..cae20ea7 --- /dev/null +++ b/libnymea-core/debugreportgenerator.h @@ -0,0 +1,56 @@ +#ifndef DEBUGREPORTGENERATOR_H +#define DEBUGREPORTGENERATOR_H + +#include +#include +#include + +namespace nymeaserver { + +class DebugReportGenerator : public QObject +{ + Q_OBJECT +public: + explicit DebugReportGenerator(QObject *parent = nullptr); + ~DebugReportGenerator(); + + QByteArray reportFileData() const; + QString reportFileName(); + QString md5Sum() const; + + void generateReport(); + +private: + QDir m_reportDirectory; + QString m_reportFileName; + + QProcess *m_compressProcess = nullptr; + QList m_runningProcesses; + + QByteArray m_reportFileData; + QString m_md5Sum; + + void copyFileToReportDirectory(const QString &fileName, const QString &subDirectory = QString()); + void verifyRunningProcessesFinished(); + + void saveLogFiles(); + void saveConfigs(); + void saveEnv(); + + void cleanupReport(); + +signals: + void finished(bool success); + void timeout(); + +private slots: + void onPingProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + void onDigProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + void onTracePathProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + void onCompressProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + +}; + +} + +#endif // DEBUGREPORTGENERATOR_H diff --git a/libnymea-core/debugserverhandler.cpp b/libnymea-core/debugserverhandler.cpp index fb99366f..cf9fa7b4 100644 --- a/libnymea-core/debugserverhandler.cpp +++ b/libnymea-core/debugserverhandler.cpp @@ -29,7 +29,7 @@ #include #include #include - +#include QtMessageHandler DebugServerHandler::s_oldLogMessageHandler = nullptr; QList DebugServerHandler::s_websocketClients; @@ -39,25 +39,17 @@ namespace nymeaserver { DebugServerHandler::DebugServerHandler(QObject *parent) : QObject(parent) { - m_websocketServer = new QWebSocketServer("Debug server", QWebSocketServer::NonSecureMode, this); - connect(m_websocketServer, &QWebSocketServer::newConnection, this, &DebugServerHandler::onWebsocketClientConnected); - - // FIXME: enable disable server with debug server - if (!m_websocketServer->listen(QHostAddress::Any, 2626)) { - qCWarning(dcWebServer()) << "DebugServer: The debug server websocket interface could not listen on" << m_websocketServer->serverUrl().toString(); - } - qCDebug(dcWebServer()) << "DebugServer: Started debug server websocket interface on" << m_websocketServer->serverUrl().toString(); - - s_oldLogMessageHandler = qInstallMessageHandler(&logMessageHandler); + connect(NymeaCore::instance()->configuration(), &NymeaConfiguration::debugServerEnabledChanged, this, &DebugServerHandler::onDebugServerEnabledChanged); + onDebugServerEnabledChanged(NymeaCore::instance()->configuration()->debugServerEnabled()); } -HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) +HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath, const QUrlQuery &requestQuery) { - qCDebug(dcWebServer()) << "DebugServer: Debug request for" << requestPath; + qCDebug(dcDebugServer()) << "Debug request for" << requestPath; // Check if debug page request if (requestPath == "/debug" || requestPath == "/debug/") { - qCDebug(dcWebServer()) << "DebugServer: Create debug interface page"; + qCDebug(dcDebugServer()) << "Create debug interface page"; // Fallback default debug page HttpReply *reply = RestResource::createSuccessReply(); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); @@ -67,10 +59,10 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) // Check if this is a logdb requested if (requestPath.startsWith("/debug/logdb.sql")) { - qCDebug(dcWebServer()) << "DebugServer: Loading" << NymeaCore::instance()->configuration()->logDBName(); + qCDebug(dcDebugServer()) << "Loading" << NymeaCore::instance()->configuration()->logDBName(); QFile logDatabaseFile(NymeaCore::instance()->configuration()->logDBName()); if (!logDatabaseFile.exists()) { - qCWarning(dcWebServer()) << "DebugServer: Could not read log database file for debug download" << NymeaCore::instance()->configuration()->logDBName() << "file does not exist."; + qCWarning(dcDebugServer()) << "Could not read log database file for debug download" << NymeaCore::instance()->configuration()->logDBName() << "file does not exist."; HttpReply *reply = RestResource::createErrorReply(HttpReply::NotFound); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); //: The HTTP error message of the debug interface. The %1 represents the file name. @@ -79,7 +71,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) } if (!logDatabaseFile.open(QFile::ReadOnly)) { - qCWarning(dcWebServer()) << "DebugServer: Could not read log database file for debug download" << NymeaCore::instance()->configuration()->logDBName(); + qCWarning(dcDebugServer()) << "Could not read log database file for debug download" << NymeaCore::instance()->configuration()->logDBName(); HttpReply *reply = RestResource::createErrorReply(HttpReply::Forbidden); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); //: The HTTP error message of the debug interface. The %1 represents the file name. @@ -100,10 +92,10 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) // Check if this is a syslog requested if (requestPath.startsWith("/debug/syslog")) { QString syslogFileName = "/var/log/syslog"; - qCDebug(dcWebServer()) << "DebugServer: Loading" << syslogFileName; + qCDebug(dcDebugServer()) << "Loading" << syslogFileName; QFile syslogFile(syslogFileName); if (!syslogFile.exists()) { - qCWarning(dcWebServer()) << "DebugServer: Could not read log database file for debug download" << syslogFileName << "file does not exist."; + qCWarning(dcDebugServer()) << "Could not read log database file for debug download" << syslogFileName << "file does not exist."; HttpReply *reply = RestResource::createErrorReply(HttpReply::NotFound); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(syslogFileName))); @@ -111,7 +103,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) } if (!syslogFile.open(QFile::ReadOnly)) { - qCWarning(dcWebServer()) << "DebugServer: Could not read syslog file for debug download" << syslogFileName; + qCWarning(dcDebugServer()) << "Could not read syslog file for debug download" << syslogFileName; HttpReply *reply = RestResource::createErrorReply(HttpReply::Forbidden); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(syslogFileName))); @@ -131,10 +123,10 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) if (requestPath.startsWith("/debug/settings")) { if (requestPath.startsWith("/debug/settings/devices")) { QString settingsFileName = NymeaSettings(NymeaSettings::SettingsRoleDevices).fileName(); - qCDebug(dcWebServer()) << "DebugServer: Loading" << settingsFileName; + qCDebug(dcDebugServer()) << "Loading" << settingsFileName; QFile settingsFile(settingsFileName); if (!settingsFile.exists()) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName << "file does not exist."; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName << "file does not exist."; HttpReply *reply = RestResource::createErrorReply(HttpReply::NotFound); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(settingsFileName))); @@ -142,7 +134,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) } if (!settingsFile.open(QFile::ReadOnly)) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName; HttpReply *reply = RestResource::createErrorReply(HttpReply::Forbidden); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(settingsFileName))); @@ -160,10 +152,10 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) if (requestPath.startsWith("/debug/settings/rules")) { QString settingsFileName = NymeaSettings(NymeaSettings::SettingsRoleRules).fileName(); - qCDebug(dcWebServer()) << "DebugServer: Loading" << settingsFileName; + qCDebug(dcDebugServer()) << "Loading" << settingsFileName; QFile settingsFile(settingsFileName); if (!settingsFile.exists()) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName << "file does not exist."; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName << "file does not exist."; HttpReply *reply = RestResource::createErrorReply(HttpReply::NotFound); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(settingsFileName))); @@ -171,7 +163,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) } if (!settingsFile.open(QFile::ReadOnly)) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName; HttpReply *reply = RestResource::createErrorReply(HttpReply::Forbidden); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(settingsFileName))); @@ -189,10 +181,10 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) if (requestPath.startsWith("/debug/settings/nymead")) { QString settingsFileName = NymeaSettings(NymeaSettings::SettingsRoleGlobal).fileName(); - qCDebug(dcWebServer()) << "DebugServer: Loading" << settingsFileName; + qCDebug(dcDebugServer()) << "Loading" << settingsFileName; QFile settingsFile(settingsFileName); if (!settingsFile.exists()) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName << "file does not exist."; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName << "file does not exist."; HttpReply *reply = RestResource::createErrorReply(HttpReply::NotFound); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(settingsFileName))); @@ -200,7 +192,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) } if (!settingsFile.open(QFile::ReadOnly)) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName; HttpReply *reply = RestResource::createErrorReply(HttpReply::Forbidden); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(settingsFileName))); @@ -218,10 +210,10 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) if (requestPath.startsWith("/debug/settings/devicestates")) { QString settingsFileName = NymeaSettings(NymeaSettings::SettingsRoleDeviceStates).fileName(); - qCDebug(dcWebServer()) << "DebugServer: Loading" << settingsFileName; + qCDebug(dcDebugServer()) << "Loading" << settingsFileName; QFile settingsFile(settingsFileName); if (!settingsFile.exists()) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName << "file does not exist."; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName << "file does not exist."; HttpReply *reply = RestResource::createErrorReply(HttpReply::NotFound); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(settingsFileName))); @@ -229,7 +221,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) } if (!settingsFile.open(QFile::ReadOnly)) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName; HttpReply *reply = RestResource::createErrorReply(HttpReply::Forbidden); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(settingsFileName))); @@ -247,10 +239,10 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) if (requestPath.startsWith("/debug/settings/plugins")) { QString settingsFileName = NymeaSettings(NymeaSettings::SettingsRolePlugins).fileName(); - qCDebug(dcWebServer()) << "DebugServer: Loading" << settingsFileName; + qCDebug(dcDebugServer()) << "Loading" << settingsFileName; QFile settingsFile(settingsFileName); if (!settingsFile.exists()) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName << "file does not exist."; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName << "file does not exist."; HttpReply *reply = RestResource::createErrorReply(HttpReply::NotFound); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(settingsFileName))); @@ -258,7 +250,36 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) } if (!settingsFile.open(QFile::ReadOnly)) { - qCWarning(dcWebServer()) << "DebugServer: Could not read file for debug download" << settingsFileName; + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName; + HttpReply *reply = RestResource::createErrorReply(HttpReply::Forbidden); + reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); + reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(settingsFileName))); + return reply; + } + + QByteArray settingsFileData = settingsFile.readAll(); + settingsFile.close(); + + HttpReply *reply = RestResource::createSuccessReply(); + reply->setHeader(HttpReply::ContentTypeHeader, "text/plain"); + reply->setPayload(settingsFileData); + return reply; + } + + if (requestPath.startsWith("/debug/settings/tags")) { + QString settingsFileName = NymeaSettings(NymeaSettings::SettingsRoleTags).fileName(); + qCDebug(dcDebugServer()) << "Loading" << settingsFileName; + QFile settingsFile(settingsFileName); + if (!settingsFile.exists()) { + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName << "file does not exist."; + HttpReply *reply = RestResource::createErrorReply(HttpReply::NotFound); + reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); + reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(settingsFileName))); + return reply; + } + + if (!settingsFile.open(QFile::ReadOnly)) { + qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName; HttpReply *reply = RestResource::createErrorReply(HttpReply::Forbidden); reply->setHeader(HttpReply::ContentTypeHeader, "text/html"); reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(settingsFileName))); @@ -280,7 +301,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) if (m_pingProcess || m_pingReply) return RestResource::createErrorReply(HttpReply::InternalServerError); - qCDebug(dcWebServer()) << "DebugServer: Start ping nymea.io process"; + qCDebug(dcDebugServer()) << "Start ping nymea.io process"; m_pingProcess = new QProcess(this); m_pingProcess->setProcessChannelMode(QProcess::MergedChannels); connect(m_pingProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onPingProcessFinished(int,QProcess::ExitStatus))); @@ -295,7 +316,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) if (m_digProcess || m_digReply) return RestResource::createErrorReply(HttpReply::InternalServerError); - qCDebug(dcWebServer()) << "DebugServer: Start dig nymea.io process"; + qCDebug(dcDebugServer()) << "Start dig nymea.io process"; m_digProcess = new QProcess(this); m_digProcess->setProcessChannelMode(QProcess::MergedChannels); connect(m_digProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onDigProcessFinished(int,QProcess::ExitStatus))); @@ -310,7 +331,7 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) if (m_tracePathProcess || m_tracePathReply) return RestResource::createErrorReply(HttpReply::InternalServerError); - qCDebug(dcWebServer()) << "DebugServer: Start tracepath nymea.io process"; + qCDebug(dcDebugServer()) << "Start tracepath nymea.io process"; m_tracePathProcess = new QProcess(this); m_tracePathProcess->setProcessChannelMode(QProcess::MergedChannels); connect(m_tracePathProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onTracePathProcessFinished(int,QProcess::ExitStatus))); @@ -320,6 +341,38 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) return m_tracePathReply; } + if (requestPath.startsWith("/debug/report")) { + // 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; + } + + } else { + DebugReportGenerator *debugReportGenerator = new DebugReportGenerator(this); + connect(debugReportGenerator, &DebugReportGenerator::finished, this, &DebugServerHandler::onDebugReportGeneratorFinished); + connect(debugReportGenerator, &DebugReportGenerator::timeout, this, &DebugServerHandler::onDebugReportGeneratorTimeout); + debugReportGenerator->generateReport(); + + HttpReply *debugReportReply = RestResource::createAsyncReply(); + m_runningReportGenerators.insert(debugReportGenerator, debugReportReply); + + return debugReportReply; + } + } // Check if this is a resource file request if (resourceFileExits(requestPath)) { @@ -327,25 +380,45 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath) } // If nothing matches, redirect to /debug page - qCWarning(dcWebServer()) << "DebugServer: Resource for debug interface not found. Redirecting to /debug"; + qCWarning(dcDebugServer()) << "Resource for debug interface not found. Redirecting to /debug"; HttpReply *reply = RestResource::createErrorReply(HttpReply::PermanentRedirect); reply->setHeader(HttpReply::LocationHeader, "/debug"); return reply; } -void DebugServerHandler::logMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message) { - s_oldLogMessageHandler(type, context, message); +void DebugServerHandler::logMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message) +{ + s_oldLogMessageHandler(type, context, message); - foreach (QWebSocket *client, s_websocketClients) { - client->sendTextMessage(message + "\n"); - } + QString finalMessage; + switch (type) { + case QtDebugMsg: + finalMessage = QString(" I | %1: %2\n").arg(context.category).arg(message); + break; + case QtInfoMsg: + finalMessage = QString(" I | %1: %2\n").arg(context.category).arg(message); + break; + case QtWarningMsg: + finalMessage = QString(" W | %1: %2\n").arg(context.category).arg(message); + break; + case QtCriticalMsg: + finalMessage = QString(" C | %1: %2\n").arg(context.category).arg(message); + break; + case QtFatalMsg: + finalMessage = QString(" F | %1: %2\n").arg(context.category).arg(message); + break; + } + + foreach (QWebSocket *client, s_websocketClients) { + client->sendTextMessage(finalMessage); + } } QByteArray DebugServerHandler::loadResourceData(const QString &resourceFileName) { QFile resourceFile(QString(":%1").arg(resourceFileName)); if (!resourceFile.open(QFile::ReadOnly)) { - qCWarning(dcWebServer()) << "DebugServer: Could not open resource file" << resourceFile.fileName(); + qCWarning(dcDebugServer()) << "Could not open resource file" << resourceFile.fileName(); return QByteArray(); } @@ -387,6 +460,144 @@ HttpReply *DebugServerHandler::processDebugFileRequest(const QString &requestPat return reply; } + +void DebugServerHandler::onDebugServerEnabledChanged(bool enabled) +{ + if (enabled) { + m_websocketServer = new QWebSocketServer("Debug server", QWebSocketServer::NonSecureMode, this); + connect(m_websocketServer, &QWebSocketServer::newConnection, this, &DebugServerHandler::onWebsocketClientConnected); + if (!m_websocketServer->listen(QHostAddress::Any, 2626)) { + qCWarning(dcDebugServer()) << "The debug server websocket interface could not listen on" << m_websocketServer->serverUrl().toString(); + m_websocketServer->deleteLater(); + m_websocketServer = nullptr; + return; + } + + qCDebug(dcDebugServer()) << "The debug server websocket interface has been started on" << m_websocketServer->serverUrl().toString(); + } else { + if (m_websocketServer) { + m_websocketServer->close(); + qCDebug(dcDebugServer()) << "The debug server websocket interface has been closed" << m_websocketServer->serverUrl().toString(); + m_websocketServer->deleteLater(); + m_websocketServer = nullptr; + } + } +} + +void DebugServerHandler::onWebsocketClientConnected() +{ + QWebSocket *client = m_websocketServer->nextPendingConnection(); + + if (s_websocketClients.isEmpty()) { + qCDebug(dcDebugServer()) << "Install debug message handler for live logs."; + s_oldLogMessageHandler = qInstallMessageHandler(&logMessageHandler); + } + + s_websocketClients.append(client); + qCDebug(dcDebugServer()) << "New websocket client connected:" << client->peerAddress().toString(); + + connect(client, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onWebsocketClientError(QAbstractSocket::SocketError))); + connect(client, &QWebSocket::disconnected, this, &DebugServerHandler::onWebsocketClientDisconnected); +} + +void DebugServerHandler::onWebsocketClientDisconnected() +{ + QWebSocket *client = static_cast(sender()); + qCDebug(dcDebugServer()) << "Websocket client disconnected" << client->peerAddress().toString(); + s_websocketClients.removeAll(client); + client->deleteLater(); + + if (s_websocketClients.isEmpty()) { + qCDebug(dcDebugServer()) << "Uninstall debug message handler for live logs and restore default message handler"; + qInstallMessageHandler(s_oldLogMessageHandler); + s_oldLogMessageHandler = nullptr; + } +} + +void DebugServerHandler::onWebsocketClientError(QAbstractSocket::SocketError error) +{ + QWebSocket *client = static_cast(sender()); + qCWarning(dcDebugServer()) << "Websocket client error" << client->peerAddress().toString() << error << client->errorString(); +} + +void DebugServerHandler::onPingProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qCDebug(dcDebugServer()) << "Ping process finished" << exitCode << exitStatus; + QByteArray processOutput = m_pingProcess->readAll(); + qCDebug(dcDebugServer()) << "Ping output:" << endl << qUtf8Printable(processOutput); + + m_pingReply->setPayload(processOutput); + m_pingReply->setHttpStatusCode(HttpReply::Ok); + m_pingReply->finished(); + m_pingReply = nullptr; + + m_pingProcess->deleteLater(); + m_pingProcess = nullptr; +} + +void DebugServerHandler::onDigProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qCDebug(dcDebugServer()) << "Dig process finished" << exitCode << exitStatus; + QByteArray processOutput = m_digProcess->readAll(); + qCDebug(dcDebugServer()) << "Dig output:" << endl << qUtf8Printable(processOutput); + + m_digReply->setPayload(processOutput); + m_digReply->setHttpStatusCode(HttpReply::Ok); + m_digReply->finished(); + m_digReply = nullptr; + + m_digProcess->deleteLater(); + m_digProcess = nullptr; +} + +void DebugServerHandler::onTracePathProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + qCDebug(dcDebugServer()) << "Tracepath process finished" << exitCode << exitStatus; + QByteArray processOutput = m_tracePathProcess->readAll(); + qCDebug(dcDebugServer()) << "Tracepath output:" << endl << qUtf8Printable(processOutput); + + m_tracePathReply->setPayload(processOutput); + m_tracePathReply->setHttpStatusCode(HttpReply::Ok); + m_tracePathReply->finished(); + m_tracePathReply = nullptr; + + m_tracePathProcess->deleteLater(); + m_tracePathProcess = nullptr; +} + +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(); + } +} + QByteArray DebugServerHandler::createDebugXmlDocument() { QByteArray data; @@ -432,8 +643,8 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeEmptyElement("link"); writer.writeAttribute("rel", "icon"); writer.writeAttribute("type", "image/png"); - writer.writeAttribute("sizes", "64x64"); - writer.writeAttribute("href", "/debug/favicons/favicon-64x64.png"); + writer.writeAttribute("sizes", "96x96"); + writer.writeAttribute("href", "/debug/favicons/favicon-96x96.png"); writer.writeEmptyElement("link"); writer.writeAttribute("rel", "icon"); @@ -447,6 +658,45 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeAttribute("sizes", "196x196"); writer.writeAttribute("href", "/debug/favicons/favicon-196x196.png"); + writer.writeEmptyElement("link"); + writer.writeAttribute("rel", "apple-touch-icon-precomposed"); + writer.writeAttribute("sizes", "57x57"); + writer.writeAttribute("href", "/debug/favicons/apple-touch-icon-57x57.png"); + + writer.writeEmptyElement("link"); + writer.writeAttribute("rel", "apple-touch-icon-precomposed"); + writer.writeAttribute("sizes", "60x60"); + writer.writeAttribute("href", "/debug/favicons/apple-touch-icon-60x60.png"); + + writer.writeEmptyElement("link"); + writer.writeAttribute("rel", "apple-touch-icon-precomposed"); + writer.writeAttribute("sizes", "72x72"); + writer.writeAttribute("href", "/debug/favicons/apple-touch-icon-72x72.png"); + + writer.writeEmptyElement("link"); + writer.writeAttribute("rel", "apple-touch-icon-precomposed"); + writer.writeAttribute("sizes", "76x76"); + writer.writeAttribute("href", "/debug/favicons/apple-touch-icon-76x76.png"); + + writer.writeEmptyElement("link"); + writer.writeAttribute("rel", "apple-touch-icon-precomposed"); + writer.writeAttribute("sizes", "114x114"); + writer.writeAttribute("href", "/debug/favicons/apple-touch-icon-114x114.png"); + + writer.writeEmptyElement("link"); + writer.writeAttribute("rel", "apple-touch-icon-precomposed"); + writer.writeAttribute("sizes", "120x120"); + writer.writeAttribute("href", "/debug/favicons/apple-touch-icon-144x144.png"); + + writer.writeEmptyElement("link"); + writer.writeAttribute("rel", "apple-touch-icon-precomposed"); + writer.writeAttribute("sizes", "144x144"); + writer.writeAttribute("href", "/debug/favicons/apple-touch-icon-144x144.png"); + + writer.writeEmptyElement("link"); + writer.writeAttribute("rel", "apple-touch-icon-precomposed"); + writer.writeAttribute("sizes", "152x152"); + writer.writeAttribute("href", "/debug/favicons/apple-touch-icon-152x152.png"); //: The header title of the debug server interface writer.writeTextElement("title", tr("Debug nymea")); @@ -541,9 +791,9 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeEndElement(); // div warning - writer.writeEmptyElement("hr"); // System information section + writer.writeEmptyElement("hr"); //: The server information section of the debug interface writer.writeTextElement("h2", tr("Server information")); writer.writeEmptyElement("hr"); @@ -677,6 +927,54 @@ QByteArray DebugServerHandler::createDebugXmlDocument() } writer.writeEndElement(); // table + + // Generate report + writer.writeEmptyElement("hr"); + //: In the server information section of the debug interface + writer.writeTextElement("h2", tr("Generate report")); + writer.writeEmptyElement("hr"); + + writer.writeTextElement("p", tr("If you want to provide all the debug information to a developer, you can generate a report file, " + "which contains all information needed for reproducing a system and get information about possible problems.")); + + // Warning + writer.writeStartElement("div"); + writer.writeAttribute("class", "warning"); + // Warning image + writer.writeStartElement("div"); + writer.writeAttribute("class", "warning-image-area"); + writer.writeEmptyElement("img"); + writer.writeAttribute("class", "warning-image"); + writer.writeAttribute("src", "/debug/warning.svg"); + writer.writeEndElement(); // div warning image + // Warning message + writer.writeStartElement("div"); + writer.writeAttribute("class", "warning-message"); + //: The warning message of the debug interface + writer.writeCharacters(tr("Do not share these generated information public, since they can contain sensible data and should be shared very carefully and only with people you trust!")); + writer.writeEndElement(); // div warning message + writer.writeEndElement(); // div warning + + // Generate report button + writer.writeStartElement("button"); + writer.writeAttribute("class", "button"); + writer.writeAttribute("type", "button"); + writer.writeAttribute("id", "generateReportButton"); + writer.writeAttribute("onClick", "generateReport()"); + //: The generate debug report button text of the debug interface + writer.writeCharacters(tr("Generate report file")); + writer.writeEndElement(); // button + + // Logs output + writer.writeStartElement("textarea"); + writer.writeAttribute("class", "console-textarea"); + writer.writeAttribute("id", "generateReportTextArea"); + writer.writeAttribute("readonly", "readonly"); + writer.writeAttribute("rows", "5"); + writer.writeCharacters(""); + writer.writeEndElement(); // textarea + + writer.writeEndElement(); // information-section // --------------------------------------------------------------------------- @@ -1027,8 +1325,58 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeEndElement(); // button writer.writeEndElement(); // form writer.writeEndElement(); // div show-button-column - writer.writeEndElement(); // div download-row + + + + // Download row + writer.writeStartElement("div"); + writer.writeAttribute("class", "download-row"); + + writer.writeStartElement("div"); + writer.writeAttribute("class", "download-name-column"); + //: The tag settings download description of the debug interface + writer.writeTextElement("p", tr("Tag settings")); + writer.writeEndElement(); // div download-name-column + + writer.writeStartElement("div"); + writer.writeAttribute("class", "download-path-column"); + writer.writeTextElement("p", NymeaSettings(NymeaSettings::SettingsRoleTags).fileName()); + writer.writeEndElement(); // div download-path-column + + writer.writeStartElement("div"); + writer.writeAttribute("class", "download-button-column"); + writer.writeStartElement("form"); + writer.writeAttribute("class", "download-button"); + writer.writeStartElement("button"); + writer.writeAttribute("class", "button"); + writer.writeAttribute("type", "button"); + if (!QFile::exists(NymeaSettings(NymeaSettings::SettingsRoleTags).fileName())) { + writer.writeAttribute("disabled", "true"); + } + writer.writeAttribute("onClick", "downloadFile('/debug/settings/tags', 'tags.conf')"); + writer.writeCharacters(tr("Download")); + writer.writeEndElement(); // button + writer.writeEndElement(); // form + writer.writeEndElement(); // div download-button-column + + writer.writeStartElement("div"); + writer.writeAttribute("class", "show-button-column"); + writer.writeStartElement("form"); + writer.writeAttribute("class", "show-button"); + writer.writeStartElement("button"); + writer.writeAttribute("class", "button"); + writer.writeAttribute("type", "button"); + if (!QFile::exists(NymeaSettings(NymeaSettings::SettingsRoleDeviceStates).fileName())) { + writer.writeAttribute("disabled", "true"); + } + writer.writeAttribute("onClick", "showFile('/debug/settings/tags')"); + writer.writeCharacters(tr("Show")); + writer.writeEndElement(); // button + writer.writeEndElement(); // form + writer.writeEndElement(); // div show-button-column + writer.writeEndElement(); // div download-row + writer.writeEndElement(); // downloads-section @@ -1048,11 +1396,10 @@ QByteArray DebugServerHandler::createDebugXmlDocument() writer.writeTextElement("p", tr("This section allows you to perform different network connectivity tests in order " "to find out if the device where nymea is running has full network connectivity.")); - // Ping section writer.writeEmptyElement("hr"); //: The ping section of the debug interface - writer.writeTextElement("h3", tr("Ping nymea.io")); + writer.writeTextElement("h3", tr("Ping")); writer.writeEmptyElement("hr"); writer.writeTextElement("p", tr("This test makes four ping attempts to the nymea.io server.")); @@ -1080,7 +1427,7 @@ QByteArray DebugServerHandler::createDebugXmlDocument() // Dig section writer.writeEmptyElement("hr"); //: The DNS lookup section of the debug interface - writer.writeTextElement("h3", tr("DNS lookup for nymea.io")); + writer.writeTextElement("h3", tr("DNS lookup")); writer.writeEmptyElement("hr"); writer.writeTextElement("p", tr("This test makes a dynamic name server lookup for nymea.io.")); @@ -1108,7 +1455,7 @@ QByteArray DebugServerHandler::createDebugXmlDocument() // Trace section writer.writeEmptyElement("hr"); //: The trace section of the debug interface - writer.writeTextElement("h3", tr("Trace path to nymea.io")); + writer.writeTextElement("h3", tr("Trace path")); writer.writeEmptyElement("hr"); writer.writeTextElement("p", tr("This test showes the trace path from the nymea device to the nymea.io server.")); @@ -1263,74 +1610,4 @@ QByteArray DebugServerHandler::createErrorXmlDocument(HttpReply::HttpStatusCode return data; } -void DebugServerHandler::onWebsocketClientConnected() -{ - QWebSocket *client = m_websocketServer->nextPendingConnection(); - s_websocketClients.append(client); - qCDebug(dcWebServer()) << "DebugServer: New websocket client connected:" << client->peerAddress().toString(); - - connect(client, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onWebsocketClientError(QAbstractSocket::SocketError))); - connect(client, &QWebSocket::disconnected, this, &DebugServerHandler::onWebsocketClientDisconnected); -} - -void DebugServerHandler::onWebsocketClientDisconnected() -{ - QWebSocket *client = static_cast(sender()); - qCDebug(dcWebServer()) << "DebugServer: Websocket client disconnected" << client->peerAddress().toString(); - s_websocketClients.removeAll(client); - client->deleteLater(); -} - -void DebugServerHandler::onWebsocketClientError(QAbstractSocket::SocketError error) -{ - QWebSocket *client = static_cast(sender()); - qCWarning(dcWebServer()) << "DebugServer: Websocket client error" << client->peerAddress().toString() << error << client->errorString(); -} - -void DebugServerHandler::onPingProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - qCDebug(dcWebServer()) << "DebugServer: Ping process finished" << exitCode << exitStatus; - QByteArray processOutput = m_pingProcess->readAll(); - qCDebug(dcWebServer()) << "DebugServer: Ping output:" << endl << qUtf8Printable(processOutput); - - m_pingReply->setPayload(processOutput); - m_pingReply->setHttpStatusCode(HttpReply::Ok); - m_pingReply->finished(); - m_pingReply = nullptr; - - m_pingProcess->deleteLater(); - m_pingProcess = nullptr; -} - -void DebugServerHandler::onDigProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - qCDebug(dcWebServer()) << "DebugServer: Dig process finished" << exitCode << exitStatus; - QByteArray processOutput = m_digProcess->readAll(); - qCDebug(dcWebServer()) << "DebugServer: Dig output:" << endl << qUtf8Printable(processOutput); - - m_digReply->setPayload(processOutput); - m_digReply->setHttpStatusCode(HttpReply::Ok); - m_digReply->finished(); - m_digReply = nullptr; - - m_digProcess->deleteLater(); - m_digProcess = nullptr; -} - -void DebugServerHandler::onTracePathProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) -{ - qCDebug(dcWebServer()) << "DebugServer: Tracepath process finished" << exitCode << exitStatus; - QByteArray processOutput = m_tracePathProcess->readAll(); - qCDebug(dcWebServer()) << "DebugServer: Tracepath output:" << endl << qUtf8Printable(processOutput); - - m_tracePathReply->setPayload(processOutput); - m_tracePathReply->setHttpStatusCode(HttpReply::Ok); - m_tracePathReply->finished(); - m_tracePathReply = nullptr; - - m_tracePathProcess->deleteLater(); - m_tracePathProcess = nullptr; -} - - } diff --git a/libnymea-core/debugserverhandler.h b/libnymea-core/debugserverhandler.h index 1f3418f2..92ed51a9 100644 --- a/libnymea-core/debugserverhandler.h +++ b/libnymea-core/debugserverhandler.h @@ -24,9 +24,11 @@ #include #include #include +#include #include #include "httpreply.h" +#include "debugreportgenerator.h" namespace nymeaserver { @@ -36,7 +38,7 @@ class DebugServerHandler : public QObject public: explicit DebugServerHandler(QObject *parent = nullptr); - HttpReply *processDebugRequest(const QString &requestPath); + HttpReply *processDebugRequest(const QString &requestPath, const QUrlQuery &requestQuery); private: static QtMessageHandler s_oldLogMessageHandler; @@ -54,6 +56,9 @@ private: QProcess *m_tracePathProcess = nullptr; HttpReply *m_tracePathReply = nullptr; + QHash m_runningReportGenerators; + QHash m_finishedReportGenerators; + QByteArray loadResourceData(const QString &resourceFileName); QString getResourceFileName(const QString &requestPath); bool resourceFileExits(const QString &requestPath); @@ -64,6 +69,8 @@ private: QByteArray createErrorXmlDocument(HttpReply::HttpStatusCode statusCode, const QString &errorMessage); private slots: + void onDebugServerEnabledChanged(bool enabled); + void onWebsocketClientConnected(); void onWebsocketClientDisconnected(); void onWebsocketClientError(QAbstractSocket::SocketError error); @@ -71,7 +78,8 @@ private slots: void onPingProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void onDigProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void onTracePathProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); - + void onDebugReportGeneratorFinished(bool success); + void onDebugReportGeneratorTimeout(); }; } diff --git a/libnymea-core/libnymea-core.pro b/libnymea-core/libnymea-core.pro index b7f00db6..a5d1b287 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -97,6 +97,7 @@ HEADERS += nymeacore.h \ tagging/tag.h \ jsonrpc/tagshandler.h \ cloud/cloudtransport.h \ + debugreportgenerator.h SOURCES += nymeacore.cpp \ tcpserver.cpp \ @@ -178,3 +179,4 @@ SOURCES += nymeacore.cpp \ tagging/tag.cpp \ jsonrpc/tagshandler.cpp \ cloud/cloudtransport.cpp \ + debugreportgenerator.cpp diff --git a/libnymea-core/webserver.cpp b/libnymea-core/webserver.cpp index 3867a6ad..76f82c34 100644 --- a/libnymea-core/webserver.cpp +++ b/libnymea-core/webserver.cpp @@ -373,7 +373,6 @@ void WebServer::readClient() // Check if this is a debug call if (request.url().path().startsWith("/debug")) { - // Check if debug server is enabled if (NymeaCore::instance()->configuration()->debugServerEnabled()) { // Verify methods @@ -386,7 +385,8 @@ void WebServer::readClient() return; } - HttpReply *reply = NymeaCore::instance()->debugServerHandler()->processDebugRequest(request.url().path()); + qCDebug(dcDebugServer()) << "Request:" << request.url().toString(); + HttpReply *reply = NymeaCore::instance()->debugServerHandler()->processDebugRequest(request.url().path(), request.urlQuery()); reply->setClientId(clientId); // Handle async replies diff --git a/libnymea/loggingcategories.cpp b/libnymea/loggingcategories.cpp index 108aa880..2afac8ef 100644 --- a/libnymea/loggingcategories.cpp +++ b/libnymea/loggingcategories.cpp @@ -33,6 +33,7 @@ Q_LOGGING_CATEGORY(dcLogEngine, "LogEngine") Q_LOGGING_CATEGORY(dcTcpServer, "TcpServer") Q_LOGGING_CATEGORY(dcTcpServerTraffic, "TcpServerTraffic") Q_LOGGING_CATEGORY(dcWebServer, "WebServer") +Q_LOGGING_CATEGORY(dcDebugServer, "DebugServer") Q_LOGGING_CATEGORY(dcWebSocketServer, "WebSocketServer") Q_LOGGING_CATEGORY(dcWebSocketServerTraffic, "WebSocketServerTraffic") Q_LOGGING_CATEGORY(dcJsonRpc, "JsonRpc") diff --git a/libnymea/loggingcategories.h b/libnymea/loggingcategories.h index 01731caf..c39ead63 100644 --- a/libnymea/loggingcategories.h +++ b/libnymea/loggingcategories.h @@ -41,6 +41,7 @@ Q_DECLARE_LOGGING_CATEGORY(dcLogEngine) Q_DECLARE_LOGGING_CATEGORY(dcTcpServer) Q_DECLARE_LOGGING_CATEGORY(dcTcpServerTraffic) Q_DECLARE_LOGGING_CATEGORY(dcWebServer) +Q_DECLARE_LOGGING_CATEGORY(dcDebugServer) Q_DECLARE_LOGGING_CATEGORY(dcWebSocketServer) Q_DECLARE_LOGGING_CATEGORY(dcWebSocketServerTraffic) Q_DECLARE_LOGGING_CATEGORY(dcJsonRpc) diff --git a/server/main.cpp b/server/main.cpp index 757d278f..d6599c34 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -110,6 +110,7 @@ int main(int argc, char *argv[]) "TcpServer", "TcpServerTraffic", "WebServer", + "DebugServer", "WebSocketServer", "WebSocketServerTraffic", "JsonRpc", @@ -238,7 +239,7 @@ int main(int argc, char *argv[]) bool startForeground = parser.isSet(foregroundOption); if (startForeground) { // inform about userid - int userId = getuid(); + uint userId = getuid(); if (userId != 0) { // check if config directory for logfile exists if (!QDir().mkpath(NymeaSettings::settingsPath())) {