Improve debug report and report download mechanism

This commit is contained in:
Simon Stürz 2019-10-02 13:12:50 +02:00
parent 6aa624c261
commit 79cdf8316e
5 changed files with 269 additions and 132 deletions

View File

@ -1,6 +1,6 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Simon Stürz <simon.stuerz@guh.io> *
* Copyright (C) 2018-2019 Simon Stürz <simon.stuerz@nymea.io> *
* *
* This file is part of nymea. *
* *
@ -44,7 +44,7 @@ function selectSection(event, section) {
/* ========================================================================*/
/* Websocket connection
/* Websocket connection for log live view
/* ========================================================================*/
var webSocket = null;
@ -126,39 +126,45 @@ function downloadFile(filePath, fileName) {
}
/* ========================================================================*/
/* Generate report
/* ========================================================================*/
var generateReportTimer = null;
function generateReport() {
console.log("Requesting to generate report file " + "/debug/report");
var textArea = document.getElementById("generateReportTextArea");
var button = document.getElementById("generateReportButton");
function pollReportResult() {
// Request report file generation
var reportGenerateRequest = new XMLHttpRequest();
reportGenerateRequest.open("GET", "/debug/report", true);
reportGenerateRequest.send(null);
button.disabled = true;
textArea.value = ".";
// Start the timer
generateReportTimer = setTimeout(generateReportTimerTimeout, 1000);
reportGenerateRequest.onreadystatechange = function() {
if (reportGenerateRequest.readyState == 4) {
console.log("Report generation finished with " + reportGenerateRequest.status);
/* 204: the report is not ready yet. */
if (reportGenerateRequest.status == 204) {
// Restart poll timer
generateReportTimer = setTimeout(generateReportTimerTimeout, 1000);
return;
}
/* Check if the generation went fine */
if (reportGenerateRequest.status != 200) {
console.log("Report generation finished with error.");
clearTimeout(generateReportTimer);
textArea.value = "Something went wrong :(";
textArea.value = "Something went wrong :(" + reportGenerateRequest.status;
button.disabled = false;
return;
}
// Stop the timer
/* The report is finished! Show information and start downloading it. */
/* Stop the timer */
clearTimeout(generateReportTimer);
var textArea = document.getElementById("generateReportTextArea");
var button = document.getElementById("generateReportButton");
console.log(reportGenerateRequest.responseText);
var responseMap = JSON.parse(reportGenerateRequest.responseText);
@ -190,10 +196,24 @@ function generateReport() {
};
}
function generateReportTimerTimeout() {
function generateReport() {
console.log("Requesting to generate report file " + "/debug/report");
var textArea = document.getElementById("generateReportTextArea");
var button = document.getElementById("generateReportButton");
button.disabled = true;
textArea.value = ".";
pollReportResult();
}
function generateReportTimerTimeout() {
var textArea = document.getElementById("generateReportTextArea");
textArea.value += ".";
generateReportTimer = setTimeout(generateReportTimerTimeout, 1000);
pollReportResult();
}
/* ========================================================================*/

View File

@ -28,7 +28,9 @@
#include <QFile>
#include <QTimer>
#include <QDateTime>
#include <QSysInfo>
#include <QStandardPaths>
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QProcessEnvironment>
@ -41,6 +43,7 @@ DebugReportGenerator::DebugReportGenerator(QObject *parent) : QObject(parent)
DebugReportGenerator::~DebugReportGenerator()
{
// Clean up any leftover files
cleanupReport();
}
@ -59,6 +62,16 @@ QString DebugReportGenerator::md5Sum() const
return m_md5Sum;
}
bool DebugReportGenerator::isReady() const
{
return m_isReady;
}
bool DebugReportGenerator::isValid() const
{
return m_isValid;
}
void DebugReportGenerator::generateReport()
{
qCDebug(dcDebugServer()) << "Start generating debug report";
@ -83,6 +96,7 @@ void DebugReportGenerator::generateReport()
saveConfigs();
saveLogFiles();
saveEnv();
saveSystemInformation();
QProcess *pingProcess = new QProcess(this);
pingProcess->setProcessChannelMode(QProcess::MergedChannels);
@ -131,6 +145,37 @@ void DebugReportGenerator::verifyRunningProcessesFinished()
}
}
void DebugReportGenerator::saveSystemInformation()
{
QFile outputFile(m_reportDirectory.path() + "/sysinfo.txt");
if (!outputFile.open(QIODevice::ReadWrite)) {
qCWarning(dcDebugServer()) << "Could not open sysinfo file" << outputFile.fileName();
return;
}
qCDebug(dcDebugServer()) << "Write system information file" << outputFile.fileName();
QTextStream stream(&outputFile);
stream << "Server name: " << NymeaCore::instance()->configuration()->serverName() << endl;
stream << "Server version: " << NYMEA_VERSION_STRING << endl;
stream << "JSON-RPC version: " << JSON_PROTOCOL_VERSION << endl;
stream << "Language: " << NymeaCore::instance()->configuration()->locale().name() << " (" << NymeaCore::instance()->configuration()->locale().nativeCountryName() << " - " << NymeaCore::instance()->configuration()->locale().nativeLanguageName() << ")" << endl;
stream << "Timezone: " << QString::fromUtf8(NymeaCore::instance()->configuration()->timeZone()) << endl;
stream << "Server UUID: " << NymeaCore::instance()->configuration()->serverUuid().toString() << endl;
stream << "Settings path: " << NymeaSettings::settingsPath() << endl;
stream << "Translations path: " << NymeaSettings(NymeaSettings::SettingsRoleGlobal).translationsPath() << endl;
stream << "User: " << qgetenv("USER") << endl;
stream << "Command: " << QCoreApplication::arguments().join(' ') << endl;
stream << "Qt runtime version: " << qVersion() << endl;
stream << "" << endl;
stream << "Hostname: " << QSysInfo::machineHostName() << endl;
stream << "Architecture: " << QSysInfo::currentCpuArchitecture() << endl;
stream << "Kernel type: " << QSysInfo::kernelType() << endl;
stream << "Kernel version: " << QSysInfo::kernelVersion() << endl;
stream << "Product type: " << QSysInfo::productType() << endl;
stream << "Product version: " << QSysInfo::productVersion() << endl;
outputFile.close();
}
void DebugReportGenerator::saveLogFiles()
{
QDir logDir("/var/log/");
@ -269,18 +314,22 @@ void DebugReportGenerator::onCompressProcessFinished(int exitCode, QProcess::Exi
QFile reportFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + m_reportFileName);
if (!reportFile.open(QIODevice::ReadOnly)) {
qCWarning(dcDebugServer()) << "Could not open report file name for reading" << reportFile.fileName();
m_isReady = true;
m_isValid = false;
emit finished(false);
} else {
m_reportFileData = reportFile.readAll();
m_md5Sum = QString::fromUtf8(QCryptographicHash::hash(m_reportFileData, QCryptographicHash::Md5).toHex());
qCDebug(dcDebugServer()) << "File generated successfully" << reportFile.fileName() << m_reportFileData.size() << "B" << m_md5Sum;
m_isReady = true;
m_isValid = true;
emit finished(true);
}
reportFile.close();
// Todo: start expire timer
QTimer::singleShot(30000, this, &DebugReportGenerator::timeout);
// When this timer expires, the debug report is not valid any more and will be deleted
QTimer::singleShot(120000, this, &DebugReportGenerator::timeout);
process->deleteLater();
process = nullptr;

View File

@ -34,15 +34,21 @@ public:
explicit DebugReportGenerator(QObject *parent = nullptr);
~DebugReportGenerator();
QByteArray reportFileData() const;
QString reportFileName();
QString md5Sum() const;
bool isReady() const;
bool isValid() const;
void generateReport();
private:
QDir m_reportDirectory;
QString m_reportFileName;
bool m_isReady = false;
bool m_isValid = false;
QProcess *m_compressProcess = nullptr;
QList<QProcess *> m_runningProcesses;
@ -53,6 +59,7 @@ private:
void copyFileToReportDirectory(const QString &fileName, const QString &subDirectory = QString());
void verifyRunningProcessesFinished();
void saveSystemInformation();
void saveLogFiles();
void saveConfigs();
void saveEnv();

View File

@ -378,35 +378,70 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath, c
}
if (requestPath.startsWith("/debug/report")) {
// The client can poll this url in order to get information about the current report generating process.
// If there is currently no report generated, start generating it and inform client that there is a report on the way (204)
// If there is already a report generation in progress, inform the client that it's not ready yet (204)
// If the report is ready, return information of the file and the client will start downloading it (202 and file informations).
// Check if download request or generate request
if (requestQuery.hasQueryItem("filename")) {
QString fileName = requestQuery.queryItemValue("filename");
qCDebug(dcDebugServer()) << "Report download request for" << fileName;
if (m_finishedReportGenerators.contains(fileName)) {
HttpReply *downloadReportReply = RestResource::createSuccessReply();
DebugReportGenerator *generator = m_finishedReportGenerators.take(fileName);
downloadReportReply->setPayload(generator->reportFileData());
downloadReportReply->setHeader(HttpReply::ContentTypeHeader, "application/tar+gzip;");
generator->deleteLater();
return downloadReportReply;
} else {
qCWarning(dcDebugServer()) << "The requested file does not exist any more" << fileName;
HttpReply *downloadReportReply = RestResource::createErrorReply(HttpReply::NotFound);
return downloadReportReply;
if (!m_debugReportGenerator) {
qCWarning(dcDebugServer()) << "There is currently no debug report generator. The requested file does not exist.";
return RestResource::createErrorReply(HttpReply::NotFound);
}
if (m_debugReportGenerator->reportFileName() != fileName) {
qCWarning(dcDebugServer()) << "The requested file is not the file from the current debug report generator" << m_debugReportGenerator->reportFileName() << "!=" << fileName;
return RestResource::createErrorReply(HttpReply::NotFound);
}
// Everything looks good, send the requested debug report
HttpReply *downloadReportReply = RestResource::createSuccessReply();
downloadReportReply->setPayload(m_debugReportGenerator->reportFileData());
downloadReportReply->setHeader(HttpReply::ContentTypeHeader, "application/tar+gzip;");
return downloadReportReply;
} else {
DebugReportGenerator *debugReportGenerator = new DebugReportGenerator(this);
connect(debugReportGenerator, &DebugReportGenerator::finished, this, &DebugServerHandler::onDebugReportGeneratorFinished);
connect(debugReportGenerator, &DebugReportGenerator::timeout, this, &DebugServerHandler::onDebugReportGeneratorTimeout);
debugReportGenerator->generateReport();
// Generate or poll request
if (!m_debugReportGenerator) {
qCDebug(dcDebugServer()) << "Create new debug report generator and start generating report...";
m_debugReportGenerator = new DebugReportGenerator(this);
connect(m_debugReportGenerator, &DebugReportGenerator::finished, this, &DebugServerHandler::onDebugReportGeneratorFinished);
connect(m_debugReportGenerator, &DebugReportGenerator::timeout, this, &DebugServerHandler::onDebugReportGeneratorTimeout);
m_debugReportGenerator->generateReport();
// Note: no content will bring the client to poll this report
return RestResource::createErrorReply(HttpReply::NoContent);
} else {
// There is a running generator, check if the report is ready
if (!m_debugReportGenerator->isReady()) {
qCDebug(dcDebugServer()) << "Report is not ready yet";
// Note: no content tells the client the report is not ready yet
return RestResource::createErrorReply(HttpReply::NoContent);
} else {
if (m_debugReportGenerator->isValid()) {
// Success, the debug report is ready and valid
QVariantMap reportInformation;
reportInformation.insert("fileName", m_debugReportGenerator->reportFileName());
reportInformation.insert("fileSize", m_debugReportGenerator->reportFileData().size());
reportInformation.insert("md5sum", m_debugReportGenerator->md5Sum());
HttpReply *debugReportReply = RestResource::createAsyncReply();
m_runningReportGenerators.insert(debugReportGenerator, debugReportReply);
return debugReportReply;
HttpReply * httpReply = RestResource::createSuccessReply();
httpReply->setHttpStatusCode(HttpReply::Ok);
httpReply->setHeader(HttpReply::ContentTypeHeader, "application/json; charset=\"utf-8\";");
httpReply->setPayload(QJsonDocument::fromVariant(reportInformation).toJson(QJsonDocument::Indented));
return httpReply;
} else {
qCWarning(dcDebugServer()) << "The debug report generator finished with error.";
m_debugReportGenerator->deleteLater();
m_debugReportGenerator = nullptr;
return RestResource::createErrorReply(HttpReply::InternalServerError);
}
}
}
}
}
@ -496,7 +531,6 @@ HttpReply *DebugServerHandler::processDebugFileRequest(const QString &requestPat
return reply;
}
void DebugServerHandler::onDebugServerEnabledChanged(bool enabled)
{
if (enabled) {
@ -605,32 +639,14 @@ void DebugServerHandler::onDebugReportGeneratorFinished(bool success)
{
DebugReportGenerator *debugReportGenerator = static_cast<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();
qCWarning(dcDebugServer()) << "Debug report expired.";
if (m_debugReportGenerator) {
m_debugReportGenerator->deleteLater();
m_debugReportGenerator = nullptr;
}
}
@ -857,7 +873,7 @@ QByteArray DebugServerHandler::createDebugXmlDocument()
writer.writeEndElement(); // div warning
// System information section
// Server information section
writer.writeEmptyElement("hr");
//: The server information section of the debug interface
writer.writeTextElement("h2", tr("Server information"));
@ -865,71 +881,6 @@ QByteArray DebugServerHandler::createDebugXmlDocument()
writer.writeStartElement("table");
writer.writeStartElement("tr");
//: The user name in the server infromation section of the debug interface
writer.writeTextElement("th", tr("User"));
writer.writeTextElement("td", qgetenv("USER"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The Qt build version description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Compiled with Qt version"));
writer.writeTextElement("td", QT_VERSION_STR);
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The Qt runtime version description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Qt runtime version"));
writer.writeTextElement("td", qVersion());
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The command description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Command"));
writer.writeTextElement("td", QCoreApplication::arguments().join(' '));
writer.writeEndElement(); // tr
if (!qgetenv("SNAP").isEmpty()) {
// Note: http://snapcraft.io/docs/reference/env
writer.writeStartElement("tr");
//: The snap name description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap name"));
writer.writeTextElement("td", qgetenv("SNAP_NAME"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap version description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap version"));
writer.writeTextElement("td", qgetenv("SNAP_VERSION"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap directory description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap directory"));
writer.writeTextElement("td", qgetenv("SNAP"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap application data description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap application data"));
writer.writeTextElement("td", qgetenv("SNAP_DATA"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap user data description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap user data"));
writer.writeTextElement("td", qgetenv("SNAP_USER_DATA"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap common data description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap common data"));
writer.writeTextElement("td", qgetenv("SNAP_COMMON"));
writer.writeEndElement(); // tr
}
writer.writeStartElement("tr");
//: The server name description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Server name"));
@ -978,6 +929,117 @@ QByteArray DebugServerHandler::createDebugXmlDocument()
writer.writeTextElement("td", NymeaSettings(NymeaSettings::SettingsRoleGlobal).translationsPath());
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The user name in the server infromation section of the debug interface
writer.writeTextElement("th", tr("User"));
writer.writeTextElement("td", qgetenv("USER"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The command description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Command"));
writer.writeTextElement("td", QCoreApplication::arguments().join(' '));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The Qt build version description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Compiled with Qt version"));
writer.writeTextElement("td", QT_VERSION_STR);
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The Qt runtime version description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Qt runtime version"));
writer.writeTextElement("td", qVersion());
writer.writeEndElement(); // tr
if (!qgetenv("SNAP").isEmpty()) {
// Note: http://snapcraft.io/docs/reference/env
writer.writeStartElement("tr");
//: The snap name description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap name"));
writer.writeTextElement("td", qgetenv("SNAP_NAME"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap version description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap version"));
writer.writeTextElement("td", qgetenv("SNAP_VERSION"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap directory description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap directory"));
writer.writeTextElement("td", qgetenv("SNAP"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap application data description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap application data"));
writer.writeTextElement("td", qgetenv("SNAP_DATA"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap user data description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap user data"));
writer.writeTextElement("td", qgetenv("SNAP_USER_DATA"));
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The snap common data description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Snap common data"));
writer.writeTextElement("td", qgetenv("SNAP_COMMON"));
writer.writeEndElement(); // tr
}
writer.writeEndElement(); // table
// System information section
writer.writeEmptyElement("hr");
//: The system information section of the debug interface
writer.writeTextElement("h2", tr("System information"));
writer.writeEmptyElement("hr");
writer.writeStartElement("table");
writer.writeStartElement("tr");
//: The command description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Hostname"));
writer.writeTextElement("td", QSysInfo::machineHostName());
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The command description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Architecture"));
writer.writeTextElement("td", QSysInfo::currentCpuArchitecture());
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The command description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Kernel type"));
writer.writeTextElement("td", QSysInfo::kernelType());
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The command description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Kernel version"));
writer.writeTextElement("td", QSysInfo::kernelVersion());
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The command description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Product type"));
writer.writeTextElement("td", QSysInfo::productType());
writer.writeEndElement(); // tr
writer.writeStartElement("tr");
//: The command description in the server infromation section of the debug interface
writer.writeTextElement("th", tr("Product version"));
writer.writeTextElement("td", QSysInfo::productVersion());
writer.writeEndElement(); // tr
writer.writeEndElement(); // table
// Generate report
@ -1627,7 +1689,7 @@ QByteArray DebugServerHandler::createDebugXmlDocument()
// Footer
writer.writeStartElement("div");
writer.writeAttribute("class", "footer");
writer.writeTextElement("p", QString("Copyright %1 2018 guh GmbH.").arg(QChar(0xA9)));
writer.writeTextElement("p", QString("Copyright %1 %2 guh GmbH.").arg(QChar(0xA9)).arg(COPYRIGHT_YEAR_STRING));
//: The footer license note of the debug interface
writer.writeTextElement("p", tr("Released under the GNU GENERAL PUBLIC LICENSE Version 2."));
writer.writeEndElement(); // div footer

View File

@ -56,8 +56,7 @@ private:
QProcess *m_tracePathProcess = nullptr;
HttpReply *m_tracePathReply = nullptr;
QHash<DebugReportGenerator *, HttpReply *> m_runningReportGenerators;
QHash<QString, DebugReportGenerator *> m_finishedReportGenerators;
DebugReportGenerator *m_debugReportGenerator = nullptr;
QByteArray loadResourceData(const QString &resourceFileName);
QString getResourceFileName(const QString &requestPath);