Add status report generator and finish debug interface improvements

pull/135/head
Simon Stürz 2018-11-15 11:33:36 +01:00 committed by Michael Zanetti
parent b2cb22df66
commit a0c2143c4b
12 changed files with 829 additions and 148 deletions

View File

@ -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;
}
};
}

View File

@ -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;
}

7
debian/control vendored
View File

@ -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

View File

@ -0,0 +1,266 @@
#include "debugreportgenerator.h"
#include "loggingcategories.h"
#include "nymeasettings.h"
#include "nymeacore.h"
#include <QDir>
#include <QFile>
#include <QTimer>
#include <QDateTime>
#include <QCryptographicHash>
#include <QProcessEnvironment>
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<QProcess *>(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<QProcess *>(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<QProcess *>(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<QProcess *>(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;
}
}

View File

@ -0,0 +1,56 @@
#ifndef DEBUGREPORTGENERATOR_H
#define DEBUGREPORTGENERATOR_H
#include <QDir>
#include <QObject>
#include <QProcess>
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<QProcess *> 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

View File

@ -29,7 +29,7 @@
#include <QXmlStreamWriter>
#include <QCoreApplication>
#include <QMessageLogger>
#include <QJsonDocument>
QtMessageHandler DebugServerHandler::s_oldLogMessageHandler = nullptr;
QList<QWebSocket*> 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<QWebSocket *>(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<QWebSocket *>(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<DebugReportGenerator *>(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<DebugReportGenerator *>(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<QWebSocket *>(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<QWebSocket *>(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;
}
}

View File

@ -24,9 +24,11 @@
#include <QTimer>
#include <QObject>
#include <QProcess>
#include <QUrlQuery>
#include <QWebSocketServer>
#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<DebugReportGenerator *, HttpReply *> m_runningReportGenerators;
QHash<QString, DebugReportGenerator *> 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();
};
}

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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())) {