From 838446e262ac628c748b2e2b2f2ec0cfaff01508 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 16 Nov 2019 19:41:17 -0500 Subject: split loot stuff to loot.h/cpp, no changes in functionality --- src/loot.cpp | 244 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/loot.cpp (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp new file mode 100644 index 00000000..b19c59ed --- /dev/null +++ b/src/loot.cpp @@ -0,0 +1,244 @@ +#include "spawn.h" +#include "organizercore.h" +#include +#include + +using namespace MOBase; + +void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) +{ + SECURITY_ATTRIBUTES secAttributes; + secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); + secAttributes.bInheritHandle = TRUE; + secAttributes.lpSecurityDescriptor = nullptr; + + if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) { + log::error("failed to create stdout reroute"); + } + + if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) { + log::error("failed to correctly set up the stdout reroute"); + *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE; + } +} + +std::string readFromPipe(HANDLE stdOutRead) +{ + static const int chunkSize = 128; + std::string result; + + char buffer[chunkSize + 1]; + buffer[chunkSize] = '\0'; + + DWORD read = 1; + while (read > 0) { + if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) { + break; + } + if (read > 0) { + result.append(buffer, read); + if (read < chunkSize) { + break; + } + } + } + return result; +} + +void processLOOTOut( + OrganizerCore& core, + const std::string &lootOut, std::string &errorMessages, + QProgressDialog &dialog) +{ + std::vector lines; + boost::split(lines, lootOut, boost::is_any_of("\r\n")); + + std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); + std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); + + for (const std::string &line : lines) { + if (line.length() > 0) { + size_t progidx = line.find("[progress]"); + size_t erroridx = line.find("[error]"); + if (progidx != std::string::npos) { + dialog.setLabelText(line.substr(progidx + 11).c_str()); + } else if (erroridx != std::string::npos) { + log::warn("{}", line); + errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); + } else { + std::smatch match; + if (std::regex_match(line, match, exRequires)) { + std::string modName(match[1].first, match[1].second); + std::string dependency(match[2].first, match[2].second); + core.pluginList()->addInformation(modName.c_str(), QObject::tr("depends on missing \"%1\"").arg(dependency.c_str())); + } else if (std::regex_match(line, match, exIncompatible)) { + std::string modName(match[1].first, match[1].second); + std::string dependency(match[2].first, match[2].second); + core.pluginList()->addInformation(modName.c_str(), QObject::tr("incompatible with \"%1\"").arg(dependency.c_str())); + } else { + log::debug("[loot] {}", line); + } + } + } + } +} + +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) +{ + std::string errorMessages; + + //m_OrganizerCore.currentProfile()->writeModlistNow(); + core.savePluginList(); + + //Create a backup of the load orders w/ LOOT in name + //to make sure that any sorting is easily undo-able. + //Need to figure out how I want to do that. + + bool success = false; + + try { + QProgressDialog dialog(parent); + + dialog.setLabelText(QObject::tr("Please wait while LOOT is running")); + dialog.setMaximum(0); + dialog.show(); + + QString outPath = QDir::temp().absoluteFilePath("lootreport.json"); + + QStringList parameters; + parameters + << "--game" << core.managedGame()->gameShortName() + << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) + << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) + << "--out" << QString("\"%1\"").arg(outPath); + + if (didUpdateMasterList) { + parameters << "--skipUpdateMasterlist"; + } + + HANDLE stdOutWrite = INVALID_HANDLE_VALUE; + HANDLE stdOutRead = INVALID_HANDLE_VALUE; + createStdoutPipe(&stdOutRead, &stdOutWrite); + + try { + core.prepareVFS(); + } catch (const UsvfsConnectorException &e) { + log::debug("{}", e.what()); + return false; + } catch (const std::exception &e) { + QMessageBox::warning(parent, QObject::tr("Error"), e.what()); + return false; + } + + spawn::SpawnParameters sp; + sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); + sp.arguments = parameters.join(" "); + sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); + sp.hooked = true; + sp.stdOut = stdOutWrite; + + HANDLE loot = spawn::startBinary(parent, sp); + + // we don't use the write end + ::CloseHandle(stdOutWrite); + + core.pluginList()->clearAdditionalInformation(); + + DWORD retLen; + JOBOBJECT_BASIC_PROCESS_ID_LIST info; + HANDLE processHandle = loot; + + if (loot != INVALID_HANDLE_VALUE) { + bool isJobHandle = true; + ULONG lastProcessID; + DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); + while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { + if (isJobHandle) { + if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { + if (info.NumberOfProcessIdsInList == 0) { + log::debug("no more processes in job"); + break; + } else { + if (lastProcessID != info.ProcessIdList[0]) { + lastProcessID = info.ProcessIdList[0]; + if (processHandle != loot) { + ::CloseHandle(processHandle); + } + processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID); + } + } + } else { + // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there + // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running. + // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without + // the right to break out. + if (::GetLastError() != ERROR_MORE_DATA) { + isJobHandle = false; + } + } + } + + if (dialog.wasCanceled()) { + if (isJobHandle) { + ::TerminateJobObject(loot, 1); + } else { + ::TerminateProcess(loot, 1); + } + } + + // keep processing events so the app doesn't appear dead + QCoreApplication::processEvents(); + std::string lootOut = readFromPipe(stdOutRead); + processLOOTOut(core, lootOut, errorMessages, dialog); + + res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); + } + + std::string remainder = readFromPipe(stdOutRead).c_str(); + if (remainder.length() > 0) { + processLOOTOut(core, remainder, errorMessages, dialog); + } + + DWORD exitCode = 0UL; + ::GetExitCodeProcess(processHandle, &exitCode); + ::CloseHandle(processHandle); + if (exitCode != 0UL) { + reportError(QObject::tr("loot failed. Exit code was: %1").arg(exitCode)); + return false; + } else { + success = true; + QFile outFile(outPath); + outFile.open(QIODevice::ReadOnly); + QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); + QJsonArray array = doc.array(); + for (auto iter = array.begin(); iter != array.end(); ++iter) { + QJsonObject pluginObj = (*iter).toObject(); + QJsonArray pluginMessages = pluginObj["messages"].toArray(); + for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { + QJsonObject msg = (*msgIter).toObject(); + core.pluginList()->addInformation(pluginObj["name"].toString(), + QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); + } + if (pluginObj["dirty"].toString() == "yes") + core.pluginList()->addInformation(pluginObj["name"].toString(), "dirty"); + } + + } + } else { + reportError(QObject::tr("failed to start loot")); + } + } catch (const std::exception &e) { + reportError(QObject::tr("failed to run loot: %1").arg(e.what())); + } + + if (errorMessages.length() > 0) { + QMessageBox *warn = new QMessageBox( + QMessageBox::Warning, QObject::tr("Errors occurred"), + errorMessages.c_str(), QMessageBox::Ok, parent); + + warn->setModal(false); + warn->show(); + } + + return success; +} -- cgit v1.3.1 From 272cb514839ebe931d735677026ee55d08bd949b Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 16 Nov 2019 20:34:55 -0500 Subject: added LootDialog, more flexible than QProgressDialog --- src/loot.cpp | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 7 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index b19c59ed..c0051e61 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -5,6 +5,74 @@ using namespace MOBase; + +class LootDialog : public QDialog +{ +public: + LootDialog(QWidget* parent) : + QDialog(parent), m_label(nullptr), m_progress(nullptr), m_buttons(nullptr), + m_cancelled(false) + { + createUI(); + } + + void setText(const QString& s) + { + m_label->setText(s); + } + + void setIndeterminate() + { + m_progress->setMaximum(0); + } + + bool cancelled() const + { + return m_cancelled; + } + +private: + QLabel* m_label; + QProgressBar* m_progress; + QDialogButtonBox* m_buttons; + bool m_cancelled; + + void createUI() + { + auto* root = new QWidget(this); + auto* ly = new QVBoxLayout(root); + + setLayout(new QVBoxLayout); + layout()->setContentsMargins(0, 0, 0, 0); + layout()->addWidget(root); + + m_label = new QLabel; + ly->addWidget(m_label); + + m_progress = new QProgressBar; + ly->addWidget(m_progress); + + ly->addStretch(1); + + m_buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); + connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); + ly->addWidget(m_buttons); + } + + void closeEvent(QCloseEvent* e) override + { + m_cancelled = true; + } + + void onButton(QAbstractButton* b) + { + if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { + m_cancelled = true; + } + } +}; + + void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) { SECURITY_ATTRIBUTES secAttributes; @@ -47,8 +115,7 @@ std::string readFromPipe(HANDLE stdOutRead) void processLOOTOut( OrganizerCore& core, - const std::string &lootOut, std::string &errorMessages, - QProgressDialog &dialog) + const std::string &lootOut, std::string &errorMessages, LootDialog& dialog) { std::vector lines; boost::split(lines, lootOut, boost::is_any_of("\r\n")); @@ -61,7 +128,7 @@ void processLOOTOut( size_t progidx = line.find("[progress]"); size_t erroridx = line.find("[error]"); if (progidx != std::string::npos) { - dialog.setLabelText(line.substr(progidx + 11).c_str()); + dialog.setText(line.substr(progidx + 11).c_str()); } else if (erroridx != std::string::npos) { log::warn("{}", line); errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); @@ -97,10 +164,10 @@ bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) bool success = false; try { - QProgressDialog dialog(parent); + LootDialog dialog(parent); - dialog.setLabelText(QObject::tr("Please wait while LOOT is running")); - dialog.setMaximum(0); + dialog.setText(QObject::tr("Please wait while LOOT is running")); + dialog.setIndeterminate(); dialog.show(); QString outPath = QDir::temp().absoluteFilePath("lootreport.json"); @@ -178,7 +245,7 @@ bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) } } - if (dialog.wasCanceled()) { + if (dialog.cancelled()) { if (isJobHandle) { ::TerminateJobObject(loot, 1); } else { -- cgit v1.3.1 From 9688f37bbe54568aba09e2dd755e0ea35eef2bb1 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 16 Nov 2019 20:47:30 -0500 Subject: added loot output --- src/loot.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index c0051e61..a9e716ae 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -26,6 +26,19 @@ public: m_progress->setMaximum(0); } + void addOutput(const QString& s) + { + const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); + + for (auto&& line : lines) { + if (line.isEmpty()) { + continue; + } + + addLineOutput(line); + } + } + bool cancelled() const { return m_cancelled; @@ -35,6 +48,8 @@ private: QLabel* m_label; QProgressBar* m_progress; QDialogButtonBox* m_buttons; + QPlainTextEdit* m_output; + QString m_lastLine; bool m_cancelled; void createUI() @@ -52,7 +67,8 @@ private: m_progress = new QProgressBar; ly->addWidget(m_progress); - ly->addStretch(1); + m_output = new QPlainTextEdit; + ly->addWidget(m_output); m_buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); @@ -70,6 +86,16 @@ private: m_cancelled = true; } } + + void addLineOutput(const QString& line) + { + if (line == m_lastLine) { + return; + } + + m_output->appendPlainText(line); + m_lastLine = line; + } }; @@ -117,6 +143,8 @@ void processLOOTOut( OrganizerCore& core, const std::string &lootOut, std::string &errorMessages, LootDialog& dialog) { + dialog.addOutput(QString::fromStdString(lootOut)); + std::vector lines; boost::split(lines, lootOut, boost::is_any_of("\r\n")); -- cgit v1.3.1 From b8eab5d248272cab886bc800f2d619a49d8af54a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 16 Nov 2019 22:25:55 -0500 Subject: threaded loot --- src/loot.cpp | 317 +++++++++++++++++++++++++++++++++++------------------ src/loot.h | 37 +++++++ src/mainwindow.cpp | 3 +- 3 files changed, 249 insertions(+), 108 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index a9e716ae..ab152b80 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -1,3 +1,4 @@ +#include "loot.h" #include "spawn.h" #include "organizercore.h" #include @@ -9,11 +10,35 @@ using namespace MOBase; class LootDialog : public QDialog { public: - LootDialog(QWidget* parent) : - QDialog(parent), m_label(nullptr), m_progress(nullptr), m_buttons(nullptr), - m_cancelled(false) + LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : + QDialog(parent), m_core(core), m_loot(loot), + m_label(nullptr), m_progress(nullptr), m_buttons(nullptr), m_finished(false) { createUI(); + + QObject::connect( + &m_loot, &Loot::output, this, + [&](auto&& s){ addOutput(s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::progress, + this, [&](auto&& s){ setText(s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::information, this, + [&](auto&& mod, auto&& i){ setInfo(mod, i); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::errorMessage, this, + [&](auto&& s){ onErrorMessage(s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::error, this, + [&](auto&& s){ onError(s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::finished, this, + [&]{ onFinished(); }, Qt::QueuedConnection); } void setText(const QString& s) @@ -39,18 +64,52 @@ public: } } - bool cancelled() const + void setInfo(const QString& mod, const QString& info) { - return m_cancelled; + m_core.pluginList()->addInformation(mod.toStdString().c_str(), info); + } + + bool result() const + { + return m_loot.result(); + } + + void cancel() + { + m_loot.cancel(); + } + + int exec() override + { + m_loot.start(); + QDialog::exec(); + + if (m_errorMessages.length() > 0) { + QMessageBox *warn = new QMessageBox( + QMessageBox::Warning, QObject::tr("Errors occurred"), + m_errorMessages, QMessageBox::Ok, parentWidget()); + + warn->exec(); + } + + return 0; + } + + void onError(const QString& s) + { + reportError(s); } private: + OrganizerCore& m_core; + Loot& m_loot; QLabel* m_label; QProgressBar* m_progress; QDialogButtonBox* m_buttons; QPlainTextEdit* m_output; QString m_lastLine; - bool m_cancelled; + QString m_errorMessages; + bool m_finished; void createUI() { @@ -77,13 +136,18 @@ private: void closeEvent(QCloseEvent* e) override { - m_cancelled = true; + if (m_finished) { + QDialog::closeEvent(e); + } else { + cancel(); + e->ignore(); + } } void onButton(QAbstractButton* b) { if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { - m_cancelled = true; + cancel(); } } @@ -96,10 +160,83 @@ private: m_output->appendPlainText(line); m_lastLine = line; } + + void onFinished() + { + m_finished = true; + close(); + } + + void onErrorMessage(const QString& s) + { + m_errorMessages += s; + } }; -void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) +Loot::Loot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) : + m_thread(nullptr), m_cancel(false), m_result(false), + m_lootProcess(INVALID_HANDLE_VALUE), m_stdOutRead(INVALID_HANDLE_VALUE) +{ + m_outPath = QDir::temp().absoluteFilePath("lootreport.json"); + + QStringList parameters; + parameters + << "--game" << core.managedGame()->gameShortName() + << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) + << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) + << "--out" << QString("\"%1\"").arg(m_outPath); + + if (didUpdateMasterList) { + parameters << "--skipUpdateMasterlist"; + } + + HANDLE stdOutWrite = INVALID_HANDLE_VALUE; + createStdoutPipe(&m_stdOutRead, &stdOutWrite); + + core.prepareVFS(); + + spawn::SpawnParameters sp; + sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); + sp.arguments = parameters.join(" "); + sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); + sp.hooked = true; + sp.stdOut = stdOutWrite; + + m_lootProcess = spawn::startBinary(parent, sp); + + // we don't use the write end + ::CloseHandle(stdOutWrite); + + core.pluginList()->clearAdditionalInformation(); + + m_thread.reset(QThread::create([&]{ + lootThread(); + emit finished(); + })); +} + +Loot::~Loot() +{ + m_thread->wait(); +} + +void Loot::start() +{ + m_thread->start(); +} + +void Loot::cancel() +{ + m_cancel = true; +} + +bool Loot::result() const +{ + return m_result; +} + +void Loot::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) { SECURITY_ATTRIBUTES secAttributes; secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); @@ -116,7 +253,7 @@ void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) } } -std::string readFromPipe(HANDLE stdOutRead) +std::string Loot::readFromPipe(HANDLE stdOutRead) { static const int chunkSize = 128; std::string result; @@ -139,11 +276,9 @@ std::string readFromPipe(HANDLE stdOutRead) return result; } -void processLOOTOut( - OrganizerCore& core, - const std::string &lootOut, std::string &errorMessages, LootDialog& dialog) +void Loot::processLOOTOut(const std::string &lootOut) { - dialog.addOutput(QString::fromStdString(lootOut)); + emit output(QString::fromStdString(lootOut)); std::vector lines; boost::split(lines, lootOut, boost::is_any_of("\r\n")); @@ -156,20 +291,25 @@ void processLOOTOut( size_t progidx = line.find("[progress]"); size_t erroridx = line.find("[error]"); if (progidx != std::string::npos) { - dialog.setText(line.substr(progidx + 11).c_str()); + emit progress(line.substr(progidx + 11).c_str()); } else if (erroridx != std::string::npos) { log::warn("{}", line); - errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); + emit errorMessage(QString::fromStdString( + boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n")); } else { std::smatch match; if (std::regex_match(line, match, exRequires)) { std::string modName(match[1].first, match[1].second); std::string dependency(match[2].first, match[2].second); - core.pluginList()->addInformation(modName.c_str(), QObject::tr("depends on missing \"%1\"").arg(dependency.c_str())); + emit information( + QString::fromStdString(modName), + QObject::tr("depends on missing \"%1\"").arg(dependency.c_str())); } else if (std::regex_match(line, match, exIncompatible)) { std::string modName(match[1].first, match[1].second); std::string dependency(match[2].first, match[2].second); - core.pluginList()->addInformation(modName.c_str(), QObject::tr("incompatible with \"%1\"").arg(dependency.c_str())); + emit information( + QString::fromStdString(modName), + QObject::tr("incompatible with \"%1\"").arg(dependency.c_str())); } else { log::debug("[loot] {}", line); } @@ -178,85 +318,29 @@ void processLOOTOut( } } -bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) +void Loot::lootThread() { - std::string errorMessages; - - //m_OrganizerCore.currentProfile()->writeModlistNow(); - core.savePluginList(); - - //Create a backup of the load orders w/ LOOT in name - //to make sure that any sorting is easily undo-able. - //Need to figure out how I want to do that. - - bool success = false; - try { - LootDialog dialog(parent); - - dialog.setText(QObject::tr("Please wait while LOOT is running")); - dialog.setIndeterminate(); - dialog.show(); - - QString outPath = QDir::temp().absoluteFilePath("lootreport.json"); - - QStringList parameters; - parameters - << "--game" << core.managedGame()->gameShortName() - << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) - << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) - << "--out" << QString("\"%1\"").arg(outPath); - - if (didUpdateMasterList) { - parameters << "--skipUpdateMasterlist"; - } - - HANDLE stdOutWrite = INVALID_HANDLE_VALUE; - HANDLE stdOutRead = INVALID_HANDLE_VALUE; - createStdoutPipe(&stdOutRead, &stdOutWrite); - - try { - core.prepareVFS(); - } catch (const UsvfsConnectorException &e) { - log::debug("{}", e.what()); - return false; - } catch (const std::exception &e) { - QMessageBox::warning(parent, QObject::tr("Error"), e.what()); - return false; - } - - spawn::SpawnParameters sp; - sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); - sp.arguments = parameters.join(" "); - sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); - sp.hooked = true; - sp.stdOut = stdOutWrite; - - HANDLE loot = spawn::startBinary(parent, sp); - - // we don't use the write end - ::CloseHandle(stdOutWrite); - - core.pluginList()->clearAdditionalInformation(); + m_result = false; DWORD retLen; JOBOBJECT_BASIC_PROCESS_ID_LIST info; - HANDLE processHandle = loot; + HANDLE processHandle = m_lootProcess; - if (loot != INVALID_HANDLE_VALUE) { + if (m_lootProcess != INVALID_HANDLE_VALUE) { bool isJobHandle = true; ULONG lastProcessID; - DWORD res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); + DWORD res = ::MsgWaitForMultipleObjects(1, &m_lootProcess, false, 100, QS_KEY | QS_MOUSE); while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { if (isJobHandle) { - if (::QueryInformationJobObject(loot, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { + if (::QueryInformationJobObject(m_lootProcess, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { if (info.NumberOfProcessIdsInList == 0) { log::debug("no more processes in job"); break; } else { if (lastProcessID != info.ProcessIdList[0]) { lastProcessID = info.ProcessIdList[0]; - if (processHandle != loot) { + if (processHandle != m_lootProcess) { ::CloseHandle(processHandle); } processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID); @@ -273,36 +357,37 @@ bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) } } - if (dialog.cancelled()) { + if (m_cancel) { if (isJobHandle) { - ::TerminateJobObject(loot, 1); + ::TerminateJobObject(m_lootProcess, 1); } else { - ::TerminateProcess(loot, 1); + ::TerminateProcess(m_lootProcess, 1); } } // keep processing events so the app doesn't appear dead QCoreApplication::processEvents(); - std::string lootOut = readFromPipe(stdOutRead); - processLOOTOut(core, lootOut, errorMessages, dialog); + std::string lootOut = readFromPipe(m_stdOutRead); + processLOOTOut(lootOut); - res = ::MsgWaitForMultipleObjects(1, &loot, false, 100, QS_KEY | QS_MOUSE); + res = ::MsgWaitForMultipleObjects(1, &m_lootProcess, false, 100, QS_KEY | QS_MOUSE); } - std::string remainder = readFromPipe(stdOutRead).c_str(); + std::string remainder = readFromPipe(m_stdOutRead).c_str(); if (remainder.length() > 0) { - processLOOTOut(core, remainder, errorMessages, dialog); + processLOOTOut(remainder); } DWORD exitCode = 0UL; ::GetExitCodeProcess(processHandle, &exitCode); ::CloseHandle(processHandle); if (exitCode != 0UL) { - reportError(QObject::tr("loot failed. Exit code was: %1").arg(exitCode)); - return false; + emit error(QObject::tr("loot failed. Exit code was: %1").arg(exitCode)); + m_result = false; + return; } else { - success = true; - QFile outFile(outPath); + m_result = true; + QFile outFile(m_outPath); outFile.open(QIODevice::ReadOnly); QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); QJsonArray array = doc.array(); @@ -311,29 +396,47 @@ bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) QJsonArray pluginMessages = pluginObj["messages"].toArray(); for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { QJsonObject msg = (*msgIter).toObject(); - core.pluginList()->addInformation(pluginObj["name"].toString(), + emit information( + pluginObj["name"].toString(), QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); } - if (pluginObj["dirty"].toString() == "yes") - core.pluginList()->addInformation(pluginObj["name"].toString(), "dirty"); + if (pluginObj["dirty"].toString() == "yes") { + emit information(pluginObj["name"].toString(), "dirty"); + } } - } } else { - reportError(QObject::tr("failed to start loot")); + emit error(QObject::tr("failed to start loot")); } } catch (const std::exception &e) { - reportError(QObject::tr("failed to run loot: %1").arg(e.what())); + emit error(QObject::tr("failed to run loot: %1").arg(e.what())); } +} - if (errorMessages.length() > 0) { - QMessageBox *warn = new QMessageBox( - QMessageBox::Warning, QObject::tr("Errors occurred"), - errorMessages.c_str(), QMessageBox::Ok, parent); - warn->setModal(false); - warn->show(); - } +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) +{ + //m_OrganizerCore.currentProfile()->writeModlistNow(); + core.savePluginList(); - return success; + //Create a backup of the load orders w/ LOOT in name + //to make sure that any sorting is easily undo-able. + //Need to figure out how I want to do that. + + try { + Loot loot(parent, core, didUpdateMasterList); + LootDialog dialog(parent, core, loot); + + dialog.setText(QObject::tr("Please wait while LOOT is running")); + dialog.setIndeterminate(); + dialog.exec(); + + return dialog.result(); + } catch (const UsvfsConnectorException &e) { + log::debug("{}", e.what()); + return false; + } catch (const std::exception &e) { + reportError(QObject::tr("failed to run loot: %1").arg(e.what())); + return false; + } } diff --git a/src/loot.h b/src/loot.h index 2314e5b3..11f1dbc8 100644 --- a/src/loot.h +++ b/src/loot.h @@ -1,10 +1,47 @@ #ifndef MODORGANIZER_LOOT_H #define MODORGANIZER_LOOT_H +#include #include class OrganizerCore; +class Loot : public QObject +{ + Q_OBJECT; + +public: + Loot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); + ~Loot(); + + void start(); + void cancel(); + bool result() const; + +signals: + void output(const QString& s); + void progress(const QString& s); + void information(const QString& mod, const QString& info); + void errorMessage(const QString& s); + void error(const QString& s); + void finished(); + +private: + std::unique_ptr m_thread; + std::atomic m_cancel; + std::atomic m_result; + QString m_outPath; + HANDLE m_lootProcess; + HANDLE m_stdOutRead; + + void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite); + std::string readFromPipe(HANDLE stdOutRead); + + void processLOOTOut(const std::string &lootOut); + void lootThread(); +}; + + bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); #endif // MODORGANIZER_LOOT_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 11edfc81..9ea554a2 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -39,7 +39,6 @@ along with Mod Organizer. If not, see . #include "spawn.h" #include "versioninfo.h" #include "instancemanager.h" -#include "loot.h" #include "report.h" #include "modlist.h" #include "modlistsortproxy.h" @@ -191,6 +190,8 @@ const QSize SmallToolbarSize(24, 24); const QSize MediumToolbarSize(32, 32); const QSize LargeToolbarSize(42, 36); +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); + MainWindow::MainWindow(Settings &settings , OrganizerCore &organizerCore -- cgit v1.3.1 From d686a61a7a72a8d4f6e7df7a4136757397cae3ae Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 16 Nov 2019 23:05:24 -0500 Subject: removed unused job stuff don't emit error when the process terminates because it's cancelled --- src/loot.cpp | 206 ++++++++++++++++++++++++++--------------------------------- src/loot.h | 12 ++-- 2 files changed, 97 insertions(+), 121 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index ab152b80..36d8c8f9 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -76,12 +76,12 @@ public: void cancel() { + addOutput(QObject::tr("Stopping LOOT...")); m_loot.cancel(); } int exec() override { - m_loot.start(); QDialog::exec(); if (m_errorMessages.length() > 0) { @@ -174,9 +174,17 @@ private: }; -Loot::Loot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) : - m_thread(nullptr), m_cancel(false), m_result(false), - m_lootProcess(INVALID_HANDLE_VALUE), m_stdOutRead(INVALID_HANDLE_VALUE) +Loot::Loot() + : m_thread(nullptr), m_cancel(false), m_result(false) +{ +} + +Loot::~Loot() +{ + m_thread->wait(); +} + +bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { m_outPath = QDir::temp().absoluteFilePath("lootreport.json"); @@ -191,8 +199,28 @@ Loot::Loot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) : parameters << "--skipUpdateMasterlist"; } - HANDLE stdOutWrite = INVALID_HANDLE_VALUE; - createStdoutPipe(&m_stdOutRead, &stdOutWrite); + SECURITY_ATTRIBUTES secAttributes; + secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); + secAttributes.bInheritHandle = TRUE; + secAttributes.lpSecurityDescriptor = nullptr; + + env::HandlePtr readPipe, writePipe; + + { + HANDLE read = INVALID_HANDLE_VALUE; + HANDLE write = INVALID_HANDLE_VALUE; + + if (!::CreatePipe(&read, &write, &secAttributes, 0)) { + log::error("failed to create stdout reroute"); + } + + readPipe.reset(read); + writePipe.reset(write); + + if (!::SetHandleInformation(read, HANDLE_FLAG_INHERIT, 0)) { + log::error("failed to correctly set up the stdout reroute"); + } + } core.prepareVFS(); @@ -201,12 +229,17 @@ Loot::Loot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) : sp.arguments = parameters.join(" "); sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); sp.hooked = true; - sp.stdOut = stdOutWrite; + sp.stdOut = writePipe.get(); + + m_stdout = std::move(readPipe); - m_lootProcess = spawn::startBinary(parent, sp); + HANDLE lootHandle = spawn::startBinary(parent, sp); + if (lootHandle == INVALID_HANDLE_VALUE) { + emit error(QObject::tr("failed to start loot")); + return false; + } - // we don't use the write end - ::CloseHandle(stdOutWrite); + m_lootProcess.reset(lootHandle); core.pluginList()->clearAdditionalInformation(); @@ -214,16 +247,10 @@ Loot::Loot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) : lootThread(); emit finished(); })); -} -Loot::~Loot() -{ - m_thread->wait(); -} - -void Loot::start() -{ m_thread->start(); + + return true; } void Loot::cancel() @@ -236,24 +263,7 @@ bool Loot::result() const return m_result; } -void Loot::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) -{ - SECURITY_ATTRIBUTES secAttributes; - secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); - secAttributes.bInheritHandle = TRUE; - secAttributes.lpSecurityDescriptor = nullptr; - - if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) { - log::error("failed to create stdout reroute"); - } - - if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) { - log::error("failed to correctly set up the stdout reroute"); - *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE; - } -} - -std::string Loot::readFromPipe(HANDLE stdOutRead) +std::string Loot::readFromPipe() { static const int chunkSize = 128; std::string result; @@ -263,7 +273,7 @@ std::string Loot::readFromPipe(HANDLE stdOutRead) DWORD read = 1; while (read > 0) { - if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, nullptr)) { + if (!::ReadFile(m_stdout.get(), buffer, chunkSize, &read, nullptr)) { break; } if (read > 0) { @@ -323,90 +333,54 @@ void Loot::lootThread() try { m_result = false; - DWORD retLen; - JOBOBJECT_BASIC_PROCESS_ID_LIST info; - HANDLE processHandle = m_lootProcess; - - if (m_lootProcess != INVALID_HANDLE_VALUE) { - bool isJobHandle = true; - ULONG lastProcessID; - DWORD res = ::MsgWaitForMultipleObjects(1, &m_lootProcess, false, 100, QS_KEY | QS_MOUSE); - while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { - if (isJobHandle) { - if (::QueryInformationJobObject(m_lootProcess, JobObjectBasicProcessIdList, &info, sizeof(info), &retLen) > 0) { - if (info.NumberOfProcessIdsInList == 0) { - log::debug("no more processes in job"); - break; - } else { - if (lastProcessID != info.ProcessIdList[0]) { - lastProcessID = info.ProcessIdList[0]; - if (processHandle != m_lootProcess) { - ::CloseHandle(processHandle); - } - processHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lastProcessID); - } - } - } else { - // the info-object I passed only provides space for 1 process id. but since this code only cares about whether there - // is more than one that's good enough. ERROR_MORE_DATA simply signals there are at least two processes running. - // any other error probably means the handle is a regular process handle, probably caused by running MO in a job without - // the right to break out. - if (::GetLastError() != ERROR_MORE_DATA) { - isJobHandle = false; - } - } - } + HANDLE waitHandle = m_lootProcess.get(); + DWORD res = ::MsgWaitForMultipleObjects(1, &waitHandle, false, 100, QS_KEY | QS_MOUSE); - if (m_cancel) { - if (isJobHandle) { - ::TerminateJobObject(m_lootProcess, 1); - } else { - ::TerminateProcess(m_lootProcess, 1); - } - } + while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { + if (m_cancel) { + ::TerminateProcess(m_lootProcess.get(), 1); + } - // keep processing events so the app doesn't appear dead - QCoreApplication::processEvents(); - std::string lootOut = readFromPipe(m_stdOutRead); - processLOOTOut(lootOut); + std::string lootOut = readFromPipe(); + processLOOTOut(lootOut); - res = ::MsgWaitForMultipleObjects(1, &m_lootProcess, false, 100, QS_KEY | QS_MOUSE); - } + res = ::MsgWaitForMultipleObjects(1, &waitHandle, false, 100, QS_KEY | QS_MOUSE); + } - std::string remainder = readFromPipe(m_stdOutRead).c_str(); - if (remainder.length() > 0) { - processLOOTOut(remainder); - } + std::string remainder = readFromPipe(); + if (remainder.length() > 0) { + processLOOTOut(remainder); + } - DWORD exitCode = 0UL; - ::GetExitCodeProcess(processHandle, &exitCode); - ::CloseHandle(processHandle); - if (exitCode != 0UL) { + DWORD exitCode = 0UL; + ::GetExitCodeProcess(m_lootProcess.get(), &exitCode); + + if (exitCode != 0UL) { + if (!m_cancel) { emit error(QObject::tr("loot failed. Exit code was: %1").arg(exitCode)); - m_result = false; - return; - } else { - m_result = true; - QFile outFile(m_outPath); - outFile.open(QIODevice::ReadOnly); - QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); - QJsonArray array = doc.array(); - for (auto iter = array.begin(); iter != array.end(); ++iter) { - QJsonObject pluginObj = (*iter).toObject(); - QJsonArray pluginMessages = pluginObj["messages"].toArray(); - for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { - QJsonObject msg = (*msgIter).toObject(); - emit information( - pluginObj["name"].toString(), - QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); - } - if (pluginObj["dirty"].toString() == "yes") { - emit information(pluginObj["name"].toString(), "dirty"); - } - } } - } else { - emit error(QObject::tr("failed to start loot")); + + return; + } + + m_result = true; + QFile outFile(m_outPath); + outFile.open(QIODevice::ReadOnly); + QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); + QJsonArray array = doc.array(); + + for (auto iter = array.begin(); iter != array.end(); ++iter) { + QJsonObject pluginObj = (*iter).toObject(); + QJsonArray pluginMessages = pluginObj["messages"].toArray(); + for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { + QJsonObject msg = (*msgIter).toObject(); + emit information( + pluginObj["name"].toString(), + QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); + } + if (pluginObj["dirty"].toString() == "yes") { + emit information(pluginObj["name"].toString(), "dirty"); + } } } catch (const std::exception &e) { emit error(QObject::tr("failed to run loot: %1").arg(e.what())); @@ -424,9 +398,11 @@ bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) //Need to figure out how I want to do that. try { - Loot loot(parent, core, didUpdateMasterList); + Loot loot; LootDialog dialog(parent, core, loot); + loot.start(parent, core, didUpdateMasterList); + dialog.setText(QObject::tr("Please wait while LOOT is running")); dialog.setIndeterminate(); dialog.exec(); diff --git a/src/loot.h b/src/loot.h index 11f1dbc8..d0c22236 100644 --- a/src/loot.h +++ b/src/loot.h @@ -3,6 +3,7 @@ #include #include +#include "envmodule.h" class OrganizerCore; @@ -11,10 +12,10 @@ class Loot : public QObject Q_OBJECT; public: - Loot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); + Loot(); ~Loot(); - void start(); + bool start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); void cancel(); bool result() const; @@ -31,11 +32,10 @@ private: std::atomic m_cancel; std::atomic m_result; QString m_outPath; - HANDLE m_lootProcess; - HANDLE m_stdOutRead; + env::HandlePtr m_lootProcess; + env::HandlePtr m_stdout; - void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite); - std::string readFromPipe(HANDLE stdOutRead); + std::string readFromPipe(); void processLOOTOut(const std::string &lootOut); void lootThread(); -- cgit v1.3.1 From 1b6dc5e0a0ef365453abeaf52015d5eeeb6b7ae2 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 18 Nov 2019 11:15:09 -0500 Subject: moved things around --- src/loot.cpp | 142 ++++++++++++++++++++++++++++++++++------------------------- src/loot.h | 4 +- 2 files changed, 86 insertions(+), 60 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 36d8c8f9..e589b54c 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -76,7 +76,7 @@ public: void cancel() { - addOutput(QObject::tr("Stopping LOOT...")); + addOutput(tr("Stopping LOOT...")); m_loot.cancel(); } @@ -86,7 +86,7 @@ public: if (m_errorMessages.length() > 0) { QMessageBox *warn = new QMessageBox( - QMessageBox::Warning, QObject::tr("Errors occurred"), + QMessageBox::Warning, tr("Errors occurred"), m_errorMessages, QMessageBox::Ok, parentWidget()); warn->exec(); @@ -235,7 +235,7 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) HANDLE lootHandle = spawn::startBinary(parent, sp); if (lootHandle == INVALID_HANDLE_VALUE) { - emit error(QObject::tr("failed to start loot")); + emit error(tr("failed to start loot")); return false; } @@ -263,6 +263,66 @@ bool Loot::result() const return m_result; } +void Loot::lootThread() +{ + try { + m_result = false; + + if (!waitForCompletion()) { + return; + } + + m_result = true; + processOutputFile(); + } catch (const std::exception &e) { + emit error(tr("failed to run loot: %1").arg(e.what())); + } +} + +bool Loot::waitForCompletion() +{ + HANDLE waitHandle = m_lootProcess.get(); + DWORD res = ::MsgWaitForMultipleObjects(1, &waitHandle, false, 100, QS_KEY | QS_MOUSE); + + while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { + if (m_cancel) { + ::TerminateProcess(m_lootProcess.get(), 1); + } + + std::string lootOut = readFromPipe(); + processStdout(lootOut); + + res = ::MsgWaitForMultipleObjects(1, &waitHandle, false, 100, QS_KEY | QS_MOUSE); + } + + const std::string remainder = readFromPipe(); + if (!remainder.empty()) { + processStdout(remainder); + } + + if (m_cancel) { + return false; + } + + + // checking exit code + + DWORD exitCode = 0; + + if (!::GetExitCodeProcess(m_lootProcess.get(), &exitCode)) { + const auto e = GetLastError(); + log::error("failed to get exit code for loot, {}", formatSystemMessage(e)); + return false; + } + + if (exitCode != 0UL) { + emit error(tr("Loot failed. Exit code was: %1").arg(exitCode)); + return false; + } + + return true; +} + std::string Loot::readFromPipe() { static const int chunkSize = 128; @@ -286,7 +346,7 @@ std::string Loot::readFromPipe() return result; } -void Loot::processLOOTOut(const std::string &lootOut) +void Loot::processStdout(const std::string &lootOut) { emit output(QString::fromStdString(lootOut)); @@ -313,13 +373,13 @@ void Loot::processLOOTOut(const std::string &lootOut) std::string dependency(match[2].first, match[2].second); emit information( QString::fromStdString(modName), - QObject::tr("depends on missing \"%1\"").arg(dependency.c_str())); + tr("depends on missing \"%1\"").arg(dependency.c_str())); } else if (std::regex_match(line, match, exIncompatible)) { std::string modName(match[1].first, match[1].second); std::string dependency(match[2].first, match[2].second); emit information( QString::fromStdString(modName), - QObject::tr("incompatible with \"%1\"").arg(dependency.c_str())); + tr("incompatible with \"%1\"").arg(dependency.c_str())); } else { log::debug("[loot] {}", line); } @@ -328,62 +388,26 @@ void Loot::processLOOTOut(const std::string &lootOut) } } -void Loot::lootThread() +void Loot::processOutputFile() { - try { - m_result = false; - - HANDLE waitHandle = m_lootProcess.get(); - DWORD res = ::MsgWaitForMultipleObjects(1, &waitHandle, false, 100, QS_KEY | QS_MOUSE); - - while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { - if (m_cancel) { - ::TerminateProcess(m_lootProcess.get(), 1); - } - - std::string lootOut = readFromPipe(); - processLOOTOut(lootOut); - - res = ::MsgWaitForMultipleObjects(1, &waitHandle, false, 100, QS_KEY | QS_MOUSE); - } - - std::string remainder = readFromPipe(); - if (remainder.length() > 0) { - processLOOTOut(remainder); - } - - DWORD exitCode = 0UL; - ::GetExitCodeProcess(m_lootProcess.get(), &exitCode); - - if (exitCode != 0UL) { - if (!m_cancel) { - emit error(QObject::tr("loot failed. Exit code was: %1").arg(exitCode)); - } - - return; + QFile outFile(m_outPath); + outFile.open(QIODevice::ReadOnly); + + QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); + QJsonArray array = doc.array(); + + for (auto iter = array.begin(); iter != array.end(); ++iter) { + QJsonObject pluginObj = (*iter).toObject(); + QJsonArray pluginMessages = pluginObj["messages"].toArray(); + for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { + QJsonObject msg = (*msgIter).toObject(); + emit information( + pluginObj["name"].toString(), + QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); } - - m_result = true; - QFile outFile(m_outPath); - outFile.open(QIODevice::ReadOnly); - QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); - QJsonArray array = doc.array(); - - for (auto iter = array.begin(); iter != array.end(); ++iter) { - QJsonObject pluginObj = (*iter).toObject(); - QJsonArray pluginMessages = pluginObj["messages"].toArray(); - for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { - QJsonObject msg = (*msgIter).toObject(); - emit information( - pluginObj["name"].toString(), - QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); - } - if (pluginObj["dirty"].toString() == "yes") { - emit information(pluginObj["name"].toString(), "dirty"); - } + if (pluginObj["dirty"].toString() == "yes") { + emit information(pluginObj["name"].toString(), "dirty"); } - } catch (const std::exception &e) { - emit error(QObject::tr("failed to run loot: %1").arg(e.what())); } } diff --git a/src/loot.h b/src/loot.h index d0c22236..1f1c4353 100644 --- a/src/loot.h +++ b/src/loot.h @@ -37,8 +37,10 @@ private: std::string readFromPipe(); - void processLOOTOut(const std::string &lootOut); void lootThread(); + bool waitForCompletion(); + void processOutputFile(); + void processStdout(const std::string &lootOut); }; -- cgit v1.3.1 From 27dadd016422765acb774ed2ed9ddae480eda46d Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 18 Nov 2019 13:32:33 -0500 Subject:
  • in tooltip for information messages rewrote json output file handling to check for errors --- src/loot.cpp | 208 +++++++++++++++++++++++++++++++++++++++++++++++++---- src/loot.h | 24 ++++++- src/pluginlist.cpp | 6 +- 3 files changed, 220 insertions(+), 18 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index e589b54c..1fc0e438 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -388,27 +388,205 @@ void Loot::processStdout(const std::string &lootOut) } } +QString jsonType(const QJsonValue& v) +{ + if (v.isUndefined()) { + return "undefined"; + } else if (v.isNull()) { + return "null"; + } else if (v.isArray()) { + return "an array"; + } else if (v.isBool()) { + return "a bool"; + } else if (v.isDouble()) { + return "a double"; + } else if (v.isObject()) { + return "an object"; + } else if (v.isString()) { + return "a string"; + } else { + return "an unknown type"; + } +} + +QString jsonType(const QJsonDocument& doc) +{ + if (doc.isEmpty()) { + return "empty"; + } else if (doc.isNull()) { + return "null"; + } else if (doc.isArray()) { + return "an array"; + } else if (doc.isObject()) { + return "an object"; + } else { + return "an unknown type"; + } +} + void Loot::processOutputFile() { QFile outFile(m_outPath); - outFile.open(QIODevice::ReadOnly); - - QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll()); - QJsonArray array = doc.array(); - - for (auto iter = array.begin(); iter != array.end(); ++iter) { - QJsonObject pluginObj = (*iter).toObject(); - QJsonArray pluginMessages = pluginObj["messages"].toArray(); - for (auto msgIter = pluginMessages.begin(); msgIter != pluginMessages.end(); ++msgIter) { - QJsonObject msg = (*msgIter).toObject(); - emit information( - pluginObj["name"].toString(), - QString("%1: %2").arg(msg["type"].toString(), msg["message"].toString())); + if (!outFile.open(QIODevice::ReadOnly)) { + logJsonError( + "failed to open file, {} (error {})", + outFile.errorString(), outFile.error()); + + return; + } + + QJsonParseError e; + const QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll(), &e); + if (doc.isNull()) { + logJsonError("invalid json, {} (error {})", e.errorString(), e.error); + return; + } + + if (!doc.isArray()) { + logJsonError("root is {}, not an array", jsonType(doc)); + return; + } + + const QJsonArray array = doc.array(); + + for (auto pluginValue : array) { + processOutputPlugin(pluginValue); + } +} + +bool Loot::processOutputPlugin(const QJsonValue& pluginValue) +{ + if (!pluginValue.isObject()) { + logJsonError( + "value in root array is {}, not an object", jsonType(pluginValue)); + return false; + } + + const auto plugin = pluginValue.toObject(); + + + if (!plugin.contains("name")) { + logJsonError("plugin value doesn't have a 'name' property"); + return false; + } + + const auto pluginNameValue = plugin["name"]; + if (!pluginNameValue.isString()) { + logJsonError( + "plugin property 'name' is {}, not a string", jsonType(pluginNameValue)); + return false; + } + + const auto pluginName = pluginNameValue.toString(); + + processPluginMessages(pluginName, plugin); + processPluginDirty(pluginName, plugin); + + return true; +} + +bool Loot::processPluginMessages( + const QString& pluginName, const QJsonObject& plugin) +{ + if (!plugin.contains("messages")) { + return true; + } + + const auto messagesValue = plugin["messages"]; + + if (!messagesValue.isArray()) { + logJsonError( + "'messages' value for plugin '{}' is {}, not an array", + pluginName, jsonType(messagesValue)); + + return false; + } + + const auto messages = messagesValue.toArray(); + + + for (auto messageValue : messages) { + if (!messageValue.isObject()) { + logJsonError( + "plugin '{}' has a message that's {}, not an object", + pluginName, jsonType(messageValue)); + + continue; } - if (pluginObj["dirty"].toString() == "yes") { - emit information(pluginObj["name"].toString(), "dirty"); + + processPluginMessage(pluginName, messageValue.toObject()); + } + + return true; +} + +bool Loot::processPluginMessage( + const QString& pluginName, const QJsonObject& message) +{ + const auto messageType = message["type"].toString(); + const auto messageString = message["message"].toString(); + + if (messageType.isEmpty()) { + logJsonError( + "plugin '{}' has a message with no 'type' property", pluginName); + return false; + } + + if (messageString.isEmpty()) { + logJsonError( + "plugin '{}' has a message with no 'message' property", pluginName); + return false; + } + + const auto info = QString("%1: %2") + .arg(messageType) + .arg(messageString); + + emit information(pluginName, info); + return true; +} + + +bool Loot::processPluginDirty( + const QString& pluginName, const QJsonObject& plugin) +{ + if (!plugin.contains("dirty")) { + return true; + } + + const auto dirtyValue = plugin["dirty"]; + + if (!dirtyValue.isArray()) { + logJsonError( + "'dirty' value for plugin '{}' is {}, not an array", + pluginName, jsonType(dirtyValue)); + + return false; + } + + const auto dirty = dirtyValue.toArray(); + + + for (auto stringValue : dirty) { + if (!stringValue.isString()) { + logJsonError( + "'dirty' value for plugin '{}' is {}, not a string", + pluginName, jsonType(stringValue)); + + continue; + } + + const auto string = stringValue.toString(); + + if (string.isEmpty()) { + logJsonError("'dirty' string for plugin '{}' is empty", pluginName); + continue; } + + emit information(pluginName, string); } + + return true; } diff --git a/src/loot.h b/src/loot.h index 1f1c4353..11bb4987 100644 --- a/src/loot.h +++ b/src/loot.h @@ -1,9 +1,10 @@ #ifndef MODORGANIZER_LOOT_H #define MODORGANIZER_LOOT_H +#include "envmodule.h" +#include #include #include -#include "envmodule.h" class OrganizerCore; @@ -39,8 +40,27 @@ private: void lootThread(); bool waitForCompletion(); - void processOutputFile(); void processStdout(const std::string &lootOut); + + void processOutputFile(); + bool processOutputPlugin(const QJsonValue& pluginValue); + + bool processPluginMessages( + const QString& pluginName, const QJsonObject& plugin); + + bool processPluginMessage( + const QString& pluginName, const QJsonObject& message); + + bool processPluginDirty( + const QString& pluginName, const QJsonObject& plugin); + + template + void logJsonError(Format&& f, Args&&... args) + { + MOBase::log::error( + std::string("loot output file '{}': ") + f, + m_outPath, std::forward(args)...); + }; }; diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index f35f9409..b50a51d8 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -963,7 +963,11 @@ QVariant PluginList::data(const QModelIndex &modelIndex, int role) const QString toolTip; if (addInfoIter != m_AdditionalInfo.end()) { if (!addInfoIter->second.m_Messages.isEmpty()) { - toolTip += addInfoIter->second.m_Messages.join("
    ") + "

    "; + toolTip += "
      "; + for (auto&& message : addInfoIter->second.m_Messages) { + toolTip += "
    • " + message + "
    • "; + } + toolTip += "

    "; } } if (m_ESPs[index].m_ForceEnabled) { -- cgit v1.3.1 From 2b5747c19d942974295be18042fbdde8ddc4cc78 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 18 Nov 2019 17:04:01 -0500 Subject: handles changes in lootcli for a more formal communication protocol --- src/CMakeLists.txt | 2 + src/loot.cpp | 129 +++++++++++++++++++++++++++++++++++++---------------- src/loot.h | 12 +++-- 3 files changed, 101 insertions(+), 42 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 70c501ae..824ffa33 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -569,11 +569,13 @@ INCLUDE_DIRECTORIES(${project_path}/uibase/src ${project_path}/bsatk/src ${project_path}/esptk/src ${project_path}/archive/src + ${project_path}/lootcli/include ${dependency_project_path}/usvfs/include ${project_path}/game_gamebryo/src/gamebryo ${project_path}/game_gamebryo/src/creation ${project_path}/game_features/src ${project_path}/githubpp/src + ${SPDLOG_ROOT}/include ${LZ4_ROOT}/lib) INCLUDE_DIRECTORIES(shared ${ZLIB_INCLUDE_DIRS}) diff --git a/src/loot.cpp b/src/loot.cpp index 1fc0e438..7184cce8 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -22,19 +22,15 @@ public: QObject::connect( &m_loot, &Loot::progress, - this, [&](auto&& s){ setText(s); }, Qt::QueuedConnection); + this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection); QObject::connect( - &m_loot, &Loot::information, this, - [&](auto&& mod, auto&& i){ setInfo(mod, i); }, Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::errorMessage, this, - [&](auto&& s){ onErrorMessage(s); }, Qt::QueuedConnection); + &m_loot, &Loot::log, this, + [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); QObject::connect( - &m_loot, &Loot::error, this, - [&](auto&& s){ onError(s); }, Qt::QueuedConnection); + &m_loot, &Loot::information, this, + [&](auto&& mod, auto&& i){ setInfo(mod, i); }, Qt::QueuedConnection); QObject::connect( &m_loot, &Loot::finished, this, @@ -46,6 +42,29 @@ public: m_label->setText(s); } + void setProgress(lootcli::Progress p) + { + setText(progressToString(p)); + } + + QString progressToString(lootcli::Progress p) + { + using P = lootcli::Progress; + + switch (p) + { + case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); + case P::UpdatingMasterlist: return tr("Updating masterlist"); + case P::LoadingLists: return tr("Loading lists"); + case P::ReadingPlugins: return tr("Reading plugins"); + case P::SortingPlugins: return tr("Sorting plugins"); + case P::WritingLoadorder: return tr("Writing loadorder.txt"); + case P::ParsingLootMessages: return tr("Parsing loot messages"); + case P::Done: return tr("Done"); + default: return QString("unknown progress %1").arg(static_cast(p)); + } + } + void setIndeterminate() { m_progress->setMaximum(0); @@ -153,10 +172,6 @@ private: void addLineOutput(const QString& line) { - if (line == m_lastLine) { - return; - } - m_output->appendPlainText(line); m_lastLine = line; } @@ -164,12 +179,19 @@ private: void onFinished() { m_finished = true; - close(); } - void onErrorMessage(const QString& s) + void log(log::Levels lv, const QString& s) { - m_errorMessages += s; + if (lv == log::Levels::Error) { + MOBase::log::error("{}", s); + + if (!m_errorMessages.isEmpty()) { + m_errorMessages += "\n"; + } + + m_errorMessages += s; + } } }; @@ -235,7 +257,7 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) HANDLE lootHandle = spawn::startBinary(parent, sp); if (lootHandle == INVALID_HANDLE_VALUE) { - emit error(tr("failed to start loot")); + emit log(log::Levels::Error, tr("failed to start loot")); return false; } @@ -275,7 +297,7 @@ void Loot::lootThread() m_result = true; processOutputFile(); } catch (const std::exception &e) { - emit error(tr("failed to run loot: %1").arg(e.what())); + emit log(log::Levels::Error, tr("failed to run loot: %1").arg(e.what())); } } @@ -316,7 +338,7 @@ bool Loot::waitForCompletion() } if (exitCode != 0UL) { - emit error(tr("Loot failed. Exit code was: %1").arg(exitCode)); + emit log(log::Levels::Error, tr("Loot failed. Exit code was: %1").arg(exitCode)); return false; } @@ -350,40 +372,69 @@ void Loot::processStdout(const std::string &lootOut) { emit output(QString::fromStdString(lootOut)); - std::vector lines; - boost::split(lines, lootOut, boost::is_any_of("\r\n")); - - std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); - std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); - - for (const std::string &line : lines) { - if (line.length() > 0) { - size_t progidx = line.find("[progress]"); - size_t erroridx = line.find("[error]"); - if (progidx != std::string::npos) { - emit progress(line.substr(progidx + 11).c_str()); - } else if (erroridx != std::string::npos) { - log::warn("{}", line); - emit errorMessage(QString::fromStdString( - boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n")); - } else { + m_outputBuffer += lootOut; + std::size_t start = 0; + + for (;;) { + const auto newline = m_outputBuffer.find("\n", start); + if (newline == std::string::npos) { + break; + } + + const std::string_view line(m_outputBuffer.c_str() + start, newline - start); + const auto m = lootcli::parseMessage(line); + + if (m.type == lootcli::MessageType::None) { + log::error("unrecognised loot output: '{}'", line); + continue; + } + + processMessage(m); + + start = newline + 1; + } + + m_outputBuffer.erase(0, start); +} + +void Loot::processMessage(const lootcli::Message& m) +{ + static const std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); + static const std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); + + switch (m.type) + { + case lootcli::MessageType::Log: + { + if (m.logLevel == spdlog::level::err) { std::smatch match; - if (std::regex_match(line, match, exRequires)) { + + if (std::regex_match(m.log, match, exRequires)) { std::string modName(match[1].first, match[1].second); std::string dependency(match[2].first, match[2].second); emit information( QString::fromStdString(modName), tr("depends on missing \"%1\"").arg(dependency.c_str())); - } else if (std::regex_match(line, match, exIncompatible)) { + } else if (std::regex_match(m.log, match, exIncompatible)) { std::string modName(match[1].first, match[1].second); std::string dependency(match[2].first, match[2].second); emit information( QString::fromStdString(modName), tr("incompatible with \"%1\"").arg(dependency.c_str())); } else { - log::debug("[loot] {}", line); + emit log(log::levelFromSpdlog(m.logLevel), QString::fromStdString(m.log)); } + } else { + emit log(log::levelFromSpdlog(m.logLevel), QString::fromStdString(m.log)); } + + break; + } + + case lootcli::MessageType::Progress: + { + emit progress(m.progress); + break; } } } diff --git a/src/loot.h b/src/loot.h index 11bb4987..ab959f2e 100644 --- a/src/loot.h +++ b/src/loot.h @@ -3,9 +3,13 @@ #include "envmodule.h" #include +#include #include #include +Q_DECLARE_METATYPE(lootcli::Progress); +Q_DECLARE_METATYPE(MOBase::log::Levels); + class OrganizerCore; class Loot : public QObject @@ -22,10 +26,9 @@ public: signals: void output(const QString& s); - void progress(const QString& s); + void progress(const lootcli::Progress p); + void log(MOBase::log::Levels level, const QString& s); void information(const QString& mod, const QString& info); - void errorMessage(const QString& s); - void error(const QString& s); void finished(); private: @@ -35,12 +38,15 @@ private: QString m_outPath; env::HandlePtr m_lootProcess; env::HandlePtr m_stdout; + std::string m_outputBuffer; std::string readFromPipe(); void lootThread(); bool waitForCompletion(); + void processStdout(const std::string &lootOut); + void processMessage(const lootcli::Message& m); void processOutputFile(); bool processOutputPlugin(const QJsonValue& pluginValue); -- cgit v1.3.1 From 8fcaea9de32b888ec8839ef6b7f394556383275e Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 22 Nov 2019 07:44:09 -0500 Subject: added loot log level option --- src/loot.cpp | 79 +++++++++++++--------- src/settings.cpp | 11 +++ src/settings.h | 5 ++ src/settingsdialog.ui | 136 ++++++++++++++++++-------------------- src/settingsdialogdiagnostics.cpp | 36 +++++++++- src/settingsdialogdiagnostics.h | 3 +- 6 files changed, 164 insertions(+), 106 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 7184cce8..c5df8c9d 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -6,6 +6,30 @@ using namespace MOBase; +log::Levels levelFromLoot(lootcli::LogLevels level) +{ + using LC = lootcli::LogLevels; + + switch (level) + { + case LC::Trace: // fall-through + case LC::Debug: + return log::Debug; + + case LC::Info: + return log::Info; + + case LC::Warning: + return log::Warning; + + case LC::Error: + return log::Error; + + default: + return log::Info; + } +} + class LootDialog : public QDialog { @@ -15,6 +39,7 @@ public: m_label(nullptr), m_progress(nullptr), m_buttons(nullptr), m_finished(false) { createUI(); + m_progress->setMaximum(0); QObject::connect( &m_loot, &Loot::output, this, @@ -45,6 +70,11 @@ public: void setProgress(lootcli::Progress p) { setText(progressToString(p)); + + if (p == lootcli::Progress::Done) { + m_progress->setRange(0, 1); + m_progress->setValue(1); + } } QString progressToString(lootcli::Progress p) @@ -65,13 +95,12 @@ public: } } - void setIndeterminate() - { - m_progress->setMaximum(0); - } - void addOutput(const QString& s) { + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + return; + } + const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); for (auto&& line : lines) { @@ -101,17 +130,7 @@ public: int exec() override { - QDialog::exec(); - - if (m_errorMessages.length() > 0) { - QMessageBox *warn = new QMessageBox( - QMessageBox::Warning, tr("Errors occurred"), - m_errorMessages, QMessageBox::Ok, parentWidget()); - - warn->exec(); - } - - return 0; + return QDialog::exec(); } void onError(const QString& s) @@ -126,8 +145,6 @@ private: QProgressBar* m_progress; QDialogButtonBox* m_buttons; QPlainTextEdit* m_output; - QString m_lastLine; - QString m_errorMessages; bool m_finished; void createUI() @@ -146,11 +163,14 @@ private: ly->addWidget(m_progress); m_output = new QPlainTextEdit; + m_output->setWordWrapMode(QTextOption::NoWrap); ly->addWidget(m_output); m_buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); ly->addWidget(m_buttons); + + resize(700, 400); } void closeEvent(QCloseEvent* e) override @@ -173,7 +193,6 @@ private: void addLineOutput(const QString& line) { m_output->appendPlainText(line); - m_lastLine = line; } void onFinished() @@ -183,14 +202,12 @@ private: void log(log::Levels lv, const QString& s) { - if (lv == log::Levels::Error) { - MOBase::log::error("{}", s); - - if (!m_errorMessages.isEmpty()) { - m_errorMessages += "\n"; - } + if (lv >= log::Levels::Warning) { + log::log(lv, "{}", s); + } - m_errorMessages += s; + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); } } }; @@ -210,11 +227,14 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { m_outPath = QDir::temp().absoluteFilePath("lootreport.json"); + const auto logLevel = core.settings().diagnostics().lootLogLevel(); + QStringList parameters; parameters << "--game" << core.managedGame()->gameShortName() << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) + << "--logLevel" << QString::fromStdString(lootcli::logLevelToString(logLevel)) << "--out" << QString("\"%1\"").arg(m_outPath); if (didUpdateMasterList) { @@ -406,7 +426,7 @@ void Loot::processMessage(const lootcli::Message& m) { case lootcli::MessageType::Log: { - if (m.logLevel == spdlog::level::err) { + if (m.logLevel == lootcli::LogLevels::Error) { std::smatch match; if (std::regex_match(m.log, match, exRequires)) { @@ -422,10 +442,10 @@ void Loot::processMessage(const lootcli::Message& m) QString::fromStdString(modName), tr("incompatible with \"%1\"").arg(dependency.c_str())); } else { - emit log(log::levelFromSpdlog(m.logLevel), QString::fromStdString(m.log)); + emit log(levelFromLoot(m.logLevel), QString::fromStdString(m.log)); } } else { - emit log(log::levelFromSpdlog(m.logLevel), QString::fromStdString(m.log)); + emit log(levelFromLoot(m.logLevel), QString::fromStdString(m.log)); } break; @@ -657,7 +677,6 @@ bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) loot.start(parent, core, didUpdateMasterList); dialog.setText(QObject::tr("Please wait while LOOT is running")); - dialog.setIndeterminate(); dialog.exec(); return dialog.result(); diff --git a/src/settings.cpp b/src/settings.cpp index 5aeb82fe..b533b400 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1977,6 +1977,17 @@ void DiagnosticsSettings::setLogLevel(log::Levels level) set(m_Settings, "Settings", "log_level", level); } +lootcli::LogLevels DiagnosticsSettings::lootLogLevel() const +{ + return get( + m_Settings, "Settings", "loot_log_level", lootcli::LogLevels::Info); +} + +void DiagnosticsSettings::setLootLogLevel(lootcli::LogLevels level) +{ + set(m_Settings, "Settings", "loot_log_level", level); +} + CrashDumpsType DiagnosticsSettings::crashDumpsType() const { return get(m_Settings, diff --git a/src/settings.h b/src/settings.h index d604823a..d71fabf4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -21,6 +21,7 @@ along with Mod Organizer. If not, see . #define SETTINGS_H #include "loadmechanism.h" +#include #include #include #include @@ -619,6 +620,10 @@ public: MOBase::log::Levels logLevel() const; void setLogLevel(MOBase::log::Levels level); + // log level for loot + lootcli::LogLevels lootLogLevel() const; + void setLootLogLevel(lootcli::LogLevels level); + // crash dump type for both MO and usvfs // CrashDumpsType crashDumpsType() const; diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 0ecbd101..b88c8b71 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -1386,44 +1386,23 @@ programs you are intentionally running. Diagnostics - - - - - - Max Dumps To Keep - - - - - - - Qt::Horizontal - - - - 60 - 20 - - - - - - - - Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - - - - Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - Set "Crash Dumps" above to None to disable crash dump collection. - - - - - - + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 232 + + + + + Hint: right click link and copy link location @@ -1444,16 +1423,42 @@ programs you are intentionally running. - - - + + + + QFormLayout::ExpandingFieldsGrow + + + 12 + + + + + Log Level + + + + + + + Decides the amount of data printed to "ModOrganizer.log" + + + + Decides the amount of data printed to "ModOrganizer.log". + "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. + + + + + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. @@ -1469,46 +1474,36 @@ programs you are intentionally running. - - - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 20 - 232 - - - - - - - - + + - Log Level + Max Dumps To Keep - - + + - Decides the amount of data printed to "ModOrganizer.log" + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - Decides the amount of data printed to "ModOrganizer.log". - "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. + Set "Crash Dumps" above to None to disable crash dump collection. + + + + LOOT Log Level + + + + + + @@ -1589,9 +1584,6 @@ programs you are intentionally running. bsaDateBtn execBlacklistBtn resetGeometryBtn - logLevelBox - dumpsTypeBox - dumpsMaxEdit diff --git a/src/settingsdialogdiagnostics.cpp b/src/settingsdialogdiagnostics.cpp index 386c7425..74cadaa9 100644 --- a/src/settingsdialogdiagnostics.cpp +++ b/src/settingsdialogdiagnostics.cpp @@ -9,7 +9,8 @@ using namespace MOBase; DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d) { - setLevelsBox(); + setLogLevel(); + setLootLogLevel(); setCrashDumpTypesBox(); ui->dumpsMaxEdit->setValue(settings().diagnostics().crashDumpsMax()); @@ -26,7 +27,7 @@ DiagnosticsSettingsTab::DiagnosticsSettingsTab(Settings& s, SettingsDialog& d) ); } -void DiagnosticsSettingsTab::setLevelsBox() +void DiagnosticsSettingsTab::setLogLevel() { ui->logLevelBox->clear(); @@ -35,14 +36,40 @@ void DiagnosticsSettingsTab::setLevelsBox() ui->logLevelBox->addItem(QObject::tr("Warning"), log::Warning); ui->logLevelBox->addItem(QObject::tr("Error"), log::Error); + const auto sel = settings().diagnostics().logLevel(); + for (int i=0; ilogLevelBox->count(); ++i) { - if (ui->logLevelBox->itemData(i) == settings().diagnostics().logLevel()) { + if (ui->logLevelBox->itemData(i) == sel) { ui->logLevelBox->setCurrentIndex(i); break; } } } +void DiagnosticsSettingsTab::setLootLogLevel() +{ + using L = lootcli::LogLevels; + + auto v = [](L level) { return QVariant(static_cast(level)); }; + + ui->lootLogLevel->clear(); + + ui->lootLogLevel->addItem(QObject::tr("Trace"), v(L::Trace)); + ui->lootLogLevel->addItem(QObject::tr("Debug"), v(L::Debug)); + ui->lootLogLevel->addItem(QObject::tr("Info (recommended)"), v(L::Info)); + ui->lootLogLevel->addItem(QObject::tr("Warning"), v(L::Warning)); + ui->lootLogLevel->addItem(QObject::tr("Error"), v(L::Error)); + + const auto sel = settings().diagnostics().lootLogLevel(); + + for (int i=0; ilootLogLevel->count(); ++i) { + if (ui->lootLogLevel->itemData(i) == v(sel)) { + ui->lootLogLevel->setCurrentIndex(i); + break; + } + } +} + void DiagnosticsSettingsTab::setCrashDumpTypesBox() { ui->dumpsTypeBox->clear(); @@ -76,4 +103,7 @@ void DiagnosticsSettingsTab::update() static_cast(ui->dumpsTypeBox->currentData().toInt())); settings().diagnostics().setCrashDumpsMax(ui->dumpsMaxEdit->value()); + + settings().diagnostics().setLootLogLevel( + static_cast(ui->lootLogLevel->currentData().toInt())); } diff --git a/src/settingsdialogdiagnostics.h b/src/settingsdialogdiagnostics.h index f0fbf770..e01ee22f 100644 --- a/src/settingsdialogdiagnostics.h +++ b/src/settingsdialogdiagnostics.h @@ -12,7 +12,8 @@ public: void update(); private: - void setLevelsBox(); + void setLogLevel(); + void setLootLogLevel(); void setCrashDumpTypesBox(); }; -- cgit v1.3.1 From d05df3bb7c467bceca4b8b8f9fb863cdc51b0e38 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 22 Nov 2019 08:01:07 -0500 Subject: cancel/close button --- src/loot.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index c5df8c9d..8345fc63 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -36,7 +36,8 @@ class LootDialog : public QDialog public: LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : QDialog(parent), m_core(core), m_loot(loot), - m_label(nullptr), m_progress(nullptr), m_buttons(nullptr), m_finished(false) + m_label(nullptr), m_progress(nullptr), m_buttons(nullptr), + m_finished(false), m_cancelling(false) { createUI(); m_progress->setMaximum(0); @@ -124,8 +125,12 @@ public: void cancel() { - addOutput(tr("Stopping LOOT...")); - m_loot.cancel(); + if (!m_finished && !m_cancelling) { + addLineOutput(tr("Stopping LOOT...")); + m_loot.cancel(); + m_buttons->setEnabled(false); + m_cancelling = true; + } } int exec() override @@ -146,6 +151,7 @@ private: QDialogButtonBox* m_buttons; QPlainTextEdit* m_output; bool m_finished; + bool m_cancelling; void createUI() { @@ -186,7 +192,11 @@ private: void onButton(QAbstractButton* b) { if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { - cancel(); + if (m_finished) { + close(); + } else { + cancel(); + } } } @@ -198,6 +208,12 @@ private: void onFinished() { m_finished = true; + + if (m_cancelling) { + close(); + } else { + m_buttons->setStandardButtons(QDialogButtonBox::Close); + } } void log(log::Levels lv, const QString& s) -- cgit v1.3.1 From 0faf628e928a7caf46aeecff2a15dd2bcd2726fe Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 22 Nov 2019 08:26:29 -0500 Subject: open json report button --- src/loot.cpp | 36 +++++++++++++++++++++++++++++++++--- src/loot.h | 1 + 2 files changed, 34 insertions(+), 3 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 8345fc63..35ac9cff 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -37,6 +37,7 @@ public: LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : QDialog(parent), m_core(core), m_loot(loot), m_label(nullptr), m_progress(nullptr), m_buttons(nullptr), + m_report(nullptr), m_output(nullptr), m_finished(false), m_cancelling(false) { createUI(); @@ -138,9 +139,10 @@ public: return QDialog::exec(); } - void onError(const QString& s) + void openReport() { - reportError(s); + const auto path = m_loot.outPath(); + shell::Open(path); } private: @@ -149,6 +151,7 @@ private: QLabel* m_label; QProgressBar* m_progress; QDialogButtonBox* m_buttons; + QPushButton* m_report; QPlainTextEdit* m_output; bool m_finished; bool m_cancelling; @@ -168,6 +171,27 @@ private: m_progress = new QProgressBar; ly->addWidget(m_progress); + auto* more = createMoreUI(); + ly->addWidget(more); + + resize(700, 400); + } + + QWidget* createMoreUI() + { + auto* more = new QWidget; + auto* ly = new QVBoxLayout(more); + ly->setContentsMargins(0, 0, 0, 0); + + auto* buttons = new QHBoxLayout; + buttons->setContentsMargins(0, 0, 0, 0); + m_report = new QPushButton(tr("Open JSON report")); + m_report->setEnabled(false); + connect(m_report, &QPushButton::clicked, [&]{ openReport(); }); + buttons->addWidget(m_report); + buttons->addStretch(1); + ly->addLayout(buttons); + m_output = new QPlainTextEdit; m_output->setWordWrapMode(QTextOption::NoWrap); ly->addWidget(m_output); @@ -176,7 +200,7 @@ private: connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); ly->addWidget(m_buttons); - resize(700, 400); + return more; } void closeEvent(QCloseEvent* e) override @@ -212,6 +236,7 @@ private: if (m_cancelling) { close(); } else { + m_report->setEnabled(true); m_buttons->setStandardButtons(QDialogButtonBox::Close); } } @@ -321,6 +346,11 @@ bool Loot::result() const return m_result; } +const QString& Loot::outPath() const +{ + return m_outPath; +} + void Loot::lootThread() { try { diff --git a/src/loot.h b/src/loot.h index ab959f2e..6dc7c9f3 100644 --- a/src/loot.h +++ b/src/loot.h @@ -23,6 +23,7 @@ public: bool start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); void cancel(); bool result() const; + const QString& outPath() const; signals: void output(const QString& s); -- cgit v1.3.1 From 237825f5b6c77969376198ed60d81c26a6a8aded Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 22 Nov 2019 09:14:02 -0500 Subject: emit logs for general messages handle new json output file --- src/loot.cpp | 177 +++++++++++++++++++++++++++++++---------------------------- src/loot.h | 14 ++--- 2 files changed, 99 insertions(+), 92 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 35ac9cff..eef16b45 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -262,6 +262,16 @@ Loot::Loot() Loot::~Loot() { m_thread->wait(); + + if (!m_outPath.isEmpty()) { + const auto r = shell::Delete(m_outPath); + + if (!r) { + log::error( + "failed to remove temporary loot json report '{}': {}", + m_outPath, r.toString()); + } + } } bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) @@ -369,32 +379,39 @@ void Loot::lootThread() bool Loot::waitForCompletion() { - HANDLE waitHandle = m_lootProcess.get(); - DWORD res = ::MsgWaitForMultipleObjects(1, &waitHandle, false, 100, QS_KEY | QS_MOUSE); + bool terminating = false; - while ((res != WAIT_FAILED) && (res != WAIT_OBJECT_0)) { - if (m_cancel) { - ::TerminateProcess(m_lootProcess.get(), 1); + for (;;) { + DWORD res = WaitForSingleObject(m_lootProcess.get(), 100); + + if (res == WAIT_OBJECT_0) { + // done + break; } - std::string lootOut = readFromPipe(); - processStdout(lootOut); + if (res == WAIT_FAILED) { + const auto e = GetLastError(); + log::error("failed to wait on loot process, {}", formatSystemMessage(e)); + return false; + } - res = ::MsgWaitForMultipleObjects(1, &waitHandle, false, 100, QS_KEY | QS_MOUSE); - } + if (m_cancel) { + // terminate and wait to finish + ::TerminateProcess(m_lootProcess.get(), 1); + WaitForSingleObject(m_lootProcess.get(), INFINITE); + return false; + } - const std::string remainder = readFromPipe(); - if (!remainder.empty()) { - processStdout(remainder); + processStdout(readFromPipe()); } if (m_cancel) { return false; } + processStdout(readFromPipe()); // checking exit code - DWORD exitCode = 0; if (!::GetExitCodeProcess(m_lootProcess.get(), &exitCode)) { @@ -559,113 +576,107 @@ void Loot::processOutputFile() return; } - if (!doc.isArray()) { - logJsonError("root is {}, not an array", jsonType(doc)); + if (!doc.isObject()) { + logJsonError("root is {}, not an object", jsonType(doc)); return; } - const QJsonArray array = doc.array(); + const QJsonObject object = doc.object(); - for (auto pluginValue : array) { - processOutputPlugin(pluginValue); - } -} + if (object.contains("messages")) { + const auto messagesValue = object["messages"]; -bool Loot::processOutputPlugin(const QJsonValue& pluginValue) -{ - if (!pluginValue.isObject()) { - logJsonError( - "value in root array is {}, not an object", jsonType(pluginValue)); - return false; - } + if (messagesValue.isArray()) { + processMessages(messagesValue.toArray()); + } else { + logJsonError( + "'messages' property is {}, not an array", jsonType(messagesValue)); + } - const auto plugin = pluginValue.toObject(); + } + if (object.contains("plugins")) { + const auto pluginsValue = object["plugins"]; - if (!plugin.contains("name")) { - logJsonError("plugin value doesn't have a 'name' property"); - return false; + if (pluginsValue.isArray()) { + processPlugins(pluginsValue.toArray()); + } else { + logJsonError( + "'plugins' property is {}, not an array", jsonType(pluginsValue)); + } } +} - const auto pluginNameValue = plugin["name"]; - if (!pluginNameValue.isString()) { - logJsonError( - "plugin property 'name' is {}, not a string", jsonType(pluginNameValue)); - return false; +bool Loot::processMessages(const QJsonArray& messages) +{ + for (auto messageValue : messages) { + if (messageValue.isObject()) { + processMessage(messageValue.toObject()); + } else { + logJsonError("a message is {}, not an object", jsonType(messageValue)); + } } - const auto pluginName = pluginNameValue.toString(); - - processPluginMessages(pluginName, plugin); - processPluginDirty(pluginName, plugin); - return true; } -bool Loot::processPluginMessages( - const QString& pluginName, const QJsonObject& plugin) +bool Loot::processMessage(const QJsonObject& message) { - if (!plugin.contains("messages")) { - return true; - } - - const auto messagesValue = plugin["messages"]; - - if (!messagesValue.isArray()) { - logJsonError( - "'messages' value for plugin '{}' is {}, not an array", - pluginName, jsonType(messagesValue)); + const auto messageType = message["type"].toString(); + const auto messageString = message["message"].toString(); + if (messageType.isEmpty()) { + logJsonError("there's a message with no 'type' property"); return false; } - const auto messages = messagesValue.toArray(); + if (messageString.isEmpty()) { + logJsonError("there's a message with no 'message' property"); + return false; + } + emit log(levelFromLoot( + lootcli::logLevelFromString(messageType.toStdString())), + messageString); - for (auto messageValue : messages) { - if (!messageValue.isObject()) { - logJsonError( - "plugin '{}' has a message that's {}, not an object", - pluginName, jsonType(messageValue)); + return true; +} - continue; +bool Loot::processPlugins(const QJsonArray& plugins) +{ + for (auto pluginValue : plugins) { + if (pluginValue.isObject()) { + processPlugin(pluginValue.toObject()); + } else { + logJsonError("a plugin is {}, not an object", jsonType(pluginValue)); } - - processPluginMessage(pluginName, messageValue.toObject()); } return true; } -bool Loot::processPluginMessage( - const QString& pluginName, const QJsonObject& message) +bool Loot::processPlugin(const QJsonObject& plugin) { - const auto messageType = message["type"].toString(); - const auto messageString = message["message"].toString(); - - if (messageType.isEmpty()) { - logJsonError( - "plugin '{}' has a message with no 'type' property", pluginName); + if (!plugin.contains("name")) { + logJsonError("plugin missing 'name' property"); return false; } - if (messageString.isEmpty()) { - logJsonError( - "plugin '{}' has a message with no 'message' property", pluginName); + const auto nameValue = plugin["name"]; + if (!nameValue.isString()) { + logJsonError("plugin property 'name' is {}, not a string", jsonType(nameValue)); return false; } - const auto info = QString("%1: %2") - .arg(messageType) - .arg(messageString); + const auto name = nameValue.toString(); + + processPluginDirty(name, plugin); - emit information(pluginName, info); return true; } -bool Loot::processPluginDirty( - const QString& pluginName, const QJsonObject& plugin) +bool Loot::processPluginDirty(const QString& name, const QJsonObject& plugin) { if (!plugin.contains("dirty")) { return true; @@ -676,7 +687,7 @@ bool Loot::processPluginDirty( if (!dirtyValue.isArray()) { logJsonError( "'dirty' value for plugin '{}' is {}, not an array", - pluginName, jsonType(dirtyValue)); + name, jsonType(dirtyValue)); return false; } @@ -688,7 +699,7 @@ bool Loot::processPluginDirty( if (!stringValue.isString()) { logJsonError( "'dirty' value for plugin '{}' is {}, not a string", - pluginName, jsonType(stringValue)); + name, jsonType(stringValue)); continue; } @@ -696,11 +707,11 @@ bool Loot::processPluginDirty( const auto string = stringValue.toString(); if (string.isEmpty()) { - logJsonError("'dirty' string for plugin '{}' is empty", pluginName); + logJsonError("'dirty' string for plugin '{}' is empty", name); continue; } - emit information(pluginName, string); + emit information(name, string); } return true; diff --git a/src/loot.h b/src/loot.h index 6dc7c9f3..54fc6fd1 100644 --- a/src/loot.h +++ b/src/loot.h @@ -50,16 +50,12 @@ private: void processMessage(const lootcli::Message& m); void processOutputFile(); - bool processOutputPlugin(const QJsonValue& pluginValue); + bool processMessages(const QJsonArray& messages); + bool processMessage(const QJsonObject& message); + bool processPlugins(const QJsonArray& plugins); + bool processPlugin(const QJsonObject& plugin); - bool processPluginMessages( - const QString& pluginName, const QJsonObject& plugin); - - bool processPluginMessage( - const QString& pluginName, const QJsonObject& message); - - bool processPluginDirty( - const QString& pluginName, const QJsonObject& plugin); + bool processPluginDirty(const QString& name, const QJsonObject& plugin); template void logJsonError(Format&& f, Args&&... args) -- cgit v1.3.1 From 5c9de17b6376ce94b1cdff4f4edbba798e9bfd08 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 23 Nov 2019 22:08:53 -0500 Subject: rewrite of json report parsing added json.h with some utilities --- src/CMakeLists.txt | 2 + src/json.h | 190 ++++++++++++++++++++++++++++++ src/loot.cpp | 332 ++++++++++++++++++++++++++++++++--------------------- src/loot.h | 32 +++--- 4 files changed, 412 insertions(+), 144 deletions(-) create mode 100644 src/json.h (limited to 'src/loot.cpp') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 824ffa33..3b74ea37 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -265,6 +265,7 @@ SET(organizer_HDRS processrunner.h uilocker.h loot.h + json.h shared/windows_error.h shared/error_report.h @@ -468,6 +469,7 @@ set(utilities usvfsconnector shared/windows_error loot + json ) set(widgets diff --git a/src/json.h b/src/json.h new file mode 100644 index 00000000..d182f330 --- /dev/null +++ b/src/json.h @@ -0,0 +1,190 @@ +#ifndef MODORGANIZER_JSON_INCLUDED +#define MODORGANIZER_JSON_INCLUDED + +#include +#include +#include +#include +#include + +namespace json +{ + +class failed {}; + + +namespace details +{ + +QString typeName(const QJsonValue& v) +{ + if (v.isUndefined()) { + return "undefined"; + } else if (v.isNull()) { + return "null"; + } else if (v.isArray()) { + return "an array"; + } else if (v.isBool()) { + return "a bool"; + } else if (v.isDouble()) { + return "a double"; + } else if (v.isObject()) { + return "an object"; + } else if (v.isString()) { + return "a string"; + } else { + return "an unknown type"; + } +} + +QString typeName(const QJsonDocument& doc) +{ + if (doc.isEmpty()) { + return "empty"; + } else if (doc.isNull()) { + return "null"; + } else if (doc.isArray()) { + return "an array"; + } else if (doc.isObject()) { + return "an object"; + } else { + return "an unknown type"; + } +} + + +template +T convert(const QJsonValue& v) = delete; + +template <> +bool convert(const QJsonValue& v) +{ + if (!v.isBool()) { + throw failed(); + } + + return v.toBool(); +} + +template <> +QJsonObject convert(const QJsonValue& v) +{ + if (!v.isObject()) { + throw failed(); + } + + return v.toObject(); +} + +template <> +QString convert(const QJsonValue& v) +{ + if (!v.isString()) { + throw failed(); + } + + return v.toString(); +} + +template <> +QJsonArray convert(const QJsonValue& v) +{ + if (!v.isArray()) { + throw failed(); + } + + return v.toArray(); +} + +template <> +qint64 convert(const QJsonValue& v) +{ + if (!v.isDouble()) { + throw failed(); + } + + return static_cast(v.toDouble()); +} + +} // namespace + + +template +T convert(const QJsonValue& value, const char* what) +{ + try + { + return details::convert(value); + } + catch(failed&) + { + MOBase::log::error( + "'{}' is a {}, not a {}", + what, details::typeName(value), typeid(T).name); + + throw; + } +} + +template +T convertWarn(const QJsonValue& value, const char* what, T def={}) +{ + try + { + return details::convert(value); + } + catch(failed&) + { + MOBase::log::warn( + "'{}' is a {}, not a {}", + what, details::typeName(value), typeid(T).name()); + + return def; + } +} + +template +T get(const QJsonObject& o, const char* e) +{ + if (!o.contains(e)) { + MOBase::log::error("property '{}' is missing", e); + throw failed(); + } + + return convert(o[e], e); +} + +template +T getWarn(const QJsonObject& o, const char* e, T def={}) +{ + if (!o.contains(e)) { + MOBase::log::warn("property '{}' is missing", e); + return def; + } + + return convertWarn(o[e], e); +} + +template +T getOpt(const QJsonObject& o, const char* e, T def={}) +{ + if (!o.contains(e)) { + return def; + } + + return convertWarn(o[e], e); +} + + +template +void requireObject(const Value& v, const char* what) +{ + if (!v.isObject()) { + MOBase::log::error("{} is {}, not an object", what, details::typeName(v)); + throw failed(); + } +} + +} // namespace + +#endif // MODORGANIZER_JSON_INCLUDED diff --git a/src/loot.cpp b/src/loot.cpp index eef16b45..88ea8ce8 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -1,10 +1,12 @@ #include "loot.h" #include "spawn.h" #include "organizercore.h" +#include "json.h" #include #include using namespace MOBase; +using namespace json; log::Levels levelFromLoot(lootcli::LogLevels level) { @@ -193,7 +195,6 @@ private: ly->addLayout(buttons); m_output = new QPlainTextEdit; - m_output->setWordWrapMode(QTextOption::NoWrap); ly->addWidget(m_output); m_buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); @@ -254,6 +255,81 @@ private: }; +struct Loot::Message +{ + QString type; + QString text; +}; + +struct Loot::File +{ + QString name; + QString displayName; +}; + +struct Loot::Dirty +{ + qint64 crc=0; + qint64 itm=0; + qint64 deletedReferences=0; + qint64 deletedNavmesh=0; + QString cleaningUtility; + QString info; + + QString toString(bool isClean) const + { + if (isClean) { + return QObject::tr("Verified clean by %1") + .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility); + } + + QString s = cleaningString(); + + if (!info.isEmpty()) { + s += " " + info; + } + + return s; + } + + QString cleaningString() const + { + return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).") + .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility) + .arg(itm) + .arg(deletedReferences) + .arg(deletedNavmesh); + } +}; + +struct Loot::Plugin +{ + QString name; + std::vector incompatibilities; + std::vector messages; + std::vector dirty, clean; + std::vector missingMasters; + bool loadsArchive = false; + bool isMaster = false; + bool isLightMaster = false; +}; + +struct Loot::Stats +{ + qint64 time = 0; + QString version; +}; + +struct Loot::Report +{ + std::vector messages; + std::vector plugins; + Stats stats; +}; + + +class ReportFailed {}; + Loot::Loot() : m_thread(nullptr), m_cancel(false), m_result(false) { @@ -522,49 +598,16 @@ void Loot::processMessage(const lootcli::Message& m) } } -QString jsonType(const QJsonValue& v) -{ - if (v.isUndefined()) { - return "undefined"; - } else if (v.isNull()) { - return "null"; - } else if (v.isArray()) { - return "an array"; - } else if (v.isBool()) { - return "a bool"; - } else if (v.isDouble()) { - return "a double"; - } else if (v.isObject()) { - return "an object"; - } else if (v.isString()) { - return "a string"; - } else { - return "an unknown type"; - } -} - -QString jsonType(const QJsonDocument& doc) -{ - if (doc.isEmpty()) { - return "empty"; - } else if (doc.isNull()) { - return "null"; - } else if (doc.isArray()) { - return "an array"; - } else if (doc.isObject()) { - return "an object"; - } else { - return "an unknown type"; - } -} - void Loot::processOutputFile() { + log::info("parsing json output file at '{}'", m_outPath); + QFile outFile(m_outPath); if (!outFile.open(QIODevice::ReadOnly)) { - logJsonError( - "failed to open file, {} (error {})", - outFile.errorString(), outFile.error()); + emit log( + MOBase::log::Error, + QString("failed to open file, %1 (error %2)") + .arg(outFile.errorString()).arg(outFile.error())); return; } @@ -572,149 +615,178 @@ void Loot::processOutputFile() QJsonParseError e; const QJsonDocument doc = QJsonDocument::fromJson(outFile.readAll(), &e); if (doc.isNull()) { - logJsonError("invalid json, {} (error {})", e.errorString(), e.error); - return; - } + emit log( + MOBase::log::Error, + QString("invalid json, %1 (error %2)") + .arg(e.errorString()).arg(e.error)); - if (!doc.isObject()) { - logJsonError("root is {}, not an object", jsonType(doc)); return; } - const QJsonObject object = doc.object(); + const auto report = createReport(doc); - if (object.contains("messages")) { - const auto messagesValue = object["messages"]; + for (auto&& m : report.messages) { + emit log(levelFromLoot( + lootcli::logLevelFromString(m.type.toStdString())), + m.text); + } - if (messagesValue.isArray()) { - processMessages(messagesValue.toArray()); - } else { - logJsonError( - "'messages' property is {}, not an array", jsonType(messagesValue)); + for (auto&& p : report.plugins) { + for (auto&& d : p.dirty) { + emit information(p.name, d.toString(false)); } - } +} - if (object.contains("plugins")) { - const auto pluginsValue = object["plugins"]; +Loot::Report Loot::createReport(const QJsonDocument& doc) const +{ + requireObject(doc, "root"); - if (pluginsValue.isArray()) { - processPlugins(pluginsValue.toArray()); - } else { - logJsonError( - "'plugins' property is {}, not an array", jsonType(pluginsValue)); - } - } + Report r; + const QJsonObject object = doc.object(); + + r.messages = reportMessages(getOpt(object, "messages")); + r.plugins = reportPlugins(getOpt(object, "plugins")); + + return r; } -bool Loot::processMessages(const QJsonArray& messages) +std::vector Loot::reportPlugins(const QJsonArray& plugins) const { - for (auto messageValue : messages) { - if (messageValue.isObject()) { - processMessage(messageValue.toObject()); - } else { - logJsonError("a message is {}, not an object", jsonType(messageValue)); + std::vector v; + + for (auto pluginValue : plugins) { + const auto o = convertWarn(pluginValue, "plugin"); + if (o.isEmpty()) { + continue; + } + + auto p = reportPlugin(o); + if (!p.name.isEmpty()) { + v.emplace_back(std::move(p)); } } - return true; + return v; } -bool Loot::processMessage(const QJsonObject& message) +Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const { - const auto messageType = message["type"].toString(); - const auto messageString = message["message"].toString(); + Plugin p; - if (messageType.isEmpty()) { - logJsonError("there's a message with no 'type' property"); - return false; + p.name = getWarn(plugin, "name"); + if (p.name.isEmpty()) { + return {}; } - if (messageString.isEmpty()) { - logJsonError("there's a message with no 'message' property"); - return false; + if (plugin.contains("incompatibilities")) { + p.incompatibilities = reportFiles(getOpt(plugin, "incompatibilities")); } - emit log(levelFromLoot( - lootcli::logLevelFromString(messageType.toStdString())), - messageString); + if (plugin.contains("messages")) { + p.messages = reportMessages(getOpt(plugin, "messages")); + } - return true; -} + if (plugin.contains("dirty")) { + p.dirty = reportDirty(getOpt(plugin, "dirty")); + } -bool Loot::processPlugins(const QJsonArray& plugins) -{ - for (auto pluginValue : plugins) { - if (pluginValue.isObject()) { - processPlugin(pluginValue.toObject()); - } else { - logJsonError("a plugin is {}, not an object", jsonType(pluginValue)); - } + if (plugin.contains("clean")) { + p.clean = reportDirty(getOpt(plugin, "clean")); } - return true; + if (plugin.contains("missingMasters")) { + p.missingMasters = reportStringArray(getOpt(plugin, "missingMasters")); + } + + p.loadsArchive = getOpt(plugin, "loadsArchive", false); + p.isMaster = getOpt(plugin, "isMaster", false); + p.isLightMaster = getOpt(plugin, "isLightMaster", false); + + return p; } -bool Loot::processPlugin(const QJsonObject& plugin) +std::vector Loot::reportMessages(const QJsonArray& array) const { - if (!plugin.contains("name")) { - logJsonError("plugin missing 'name' property"); - return false; - } + std::vector v; - const auto nameValue = plugin["name"]; - if (!nameValue.isString()) { - logJsonError("plugin property 'name' is {}, not a string", jsonType(nameValue)); - return false; - } + for (auto messageValue : array) { + const auto o = convertWarn(messageValue, "message"); + if (o.isEmpty()) { + continue; + } - const auto name = nameValue.toString(); + Message m; + m.type = getWarn(o, "type"); + m.text = getWarn(o, "text"); - processPluginDirty(name, plugin); + if (!m.text.isEmpty()) { + v.emplace_back(std::move(m)); + } + } - return true; + return v; } - -bool Loot::processPluginDirty(const QString& name, const QJsonObject& plugin) +std::vector Loot::reportFiles(const QJsonArray& array) const { - if (!plugin.contains("dirty")) { - return true; - } + std::vector v; + + for (auto&& fileValue : array) { + const auto o = convertWarn(fileValue, "file"); + if (o.isEmpty()) { + continue; + } - const auto dirtyValue = plugin["dirty"]; + File f; - if (!dirtyValue.isArray()) { - logJsonError( - "'dirty' value for plugin '{}' is {}, not an array", - name, jsonType(dirtyValue)); + f.name = getWarn(o, "name"); + f.displayName = getOpt(o, "displayName"); - return false; + if (!f.name.isEmpty()) { + v.emplace_back(std::move(f)); + } } - const auto dirty = dirtyValue.toArray(); + return v; +} + +std::vector Loot::reportDirty(const QJsonArray& array) const +{ + std::vector v; + for (auto&& dirtyValue : array) { + const auto o = convertWarn(dirtyValue, "dirty"); - for (auto stringValue : dirty) { - if (!stringValue.isString()) { - logJsonError( - "'dirty' value for plugin '{}' is {}, not a string", - name, jsonType(stringValue)); + Dirty d; - continue; - } + d.crc = getWarn(o, "crc"); + d.itm = getOpt(o, "itm"); + d.deletedReferences = getOpt(o, "deletedReferences"); + d.deletedNavmesh = getOpt(o, "deletedNavmesh"); + d.cleaningUtility = getOpt(o, "cleaningUtility"); + d.info = getOpt(o, "info"); - const auto string = stringValue.toString(); + v.emplace_back(std::move(d)); + } + + return v; +} - if (string.isEmpty()) { - logJsonError("'dirty' string for plugin '{}' is empty", name); +std::vector Loot::reportStringArray(const QJsonArray& array) const +{ + std::vector v; + + for (auto&& sv : array) { + auto s = convertWarn(sv, "string"); + if (s.isEmpty()) { continue; } - emit information(name, string); + v.emplace_back(std::move(s)); } - return true; + return v; } diff --git a/src/loot.h b/src/loot.h index 54fc6fd1..95dbbe50 100644 --- a/src/loot.h +++ b/src/loot.h @@ -33,6 +33,14 @@ signals: void finished(); private: + struct Report; + struct Stats; + struct Message; + struct Plugin; + struct Dirty; + struct File; + class BadReport {}; + std::unique_ptr m_thread; std::atomic m_cancel; std::atomic m_result; @@ -50,20 +58,16 @@ private: void processMessage(const lootcli::Message& m); void processOutputFile(); - bool processMessages(const QJsonArray& messages); - bool processMessage(const QJsonObject& message); - bool processPlugins(const QJsonArray& plugins); - bool processPlugin(const QJsonObject& plugin); - - bool processPluginDirty(const QString& name, const QJsonObject& plugin); - - template - void logJsonError(Format&& f, Args&&... args) - { - MOBase::log::error( - std::string("loot output file '{}': ") + f, - m_outPath, std::forward(args)...); - }; + + Report createReport(const QJsonDocument& doc) const; + Message reportMessage(const QJsonObject& message) const; + std::vector reportPlugins(const QJsonArray& plugins) const; + Loot::Plugin reportPlugin(const QJsonObject& plugin) const; + + std::vector reportMessages(const QJsonArray& array) const; + std::vector reportFiles(const QJsonArray& array) const; + std::vector reportDirty(const QJsonArray& array) const; + std::vector reportStringArray(const QJsonArray& array) const; }; -- cgit v1.3.1 From 3a085212c939ae8c5e6022a4c9bddfb7df95400f Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 23 Nov 2019 22:35:08 -0500 Subject: added loot report to the plugin list, not used yet split PluginList::data() into individual functions disabled loot message processing, will use report instead --- src/loot.cpp | 131 ++++++++---------------- src/loot.h | 62 ++++++++++-- src/pluginlist.cpp | 293 +++++++++++++++++++++++++++++++++-------------------- src/pluginlist.h | 17 ++++ 4 files changed, 295 insertions(+), 208 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 88ea8ce8..66e8a01d 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -57,10 +57,6 @@ public: &m_loot, &Loot::log, this, [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); - QObject::connect( - &m_loot, &Loot::information, this, - [&](auto&& mod, auto&& i){ setInfo(mod, i); }, Qt::QueuedConnection); - QObject::connect( &m_loot, &Loot::finished, this, [&]{ onFinished(); }, Qt::QueuedConnection); @@ -116,11 +112,6 @@ public: } } - void setInfo(const QString& mod, const QString& info) - { - m_core.pluginList()->addInformation(mod.toStdString().c_str(), info); - } - bool result() const { return m_loot.result(); @@ -237,6 +228,7 @@ private: if (m_cancelling) { close(); } else { + handleReport(); m_report->setEnabled(true); m_buttons->setStandardButtons(QDialogButtonBox::Close); } @@ -252,83 +244,55 @@ private: addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); } } -}; - -struct Loot::Message -{ - QString type; - QString text; -}; - -struct Loot::File -{ - QString name; - QString displayName; -}; - -struct Loot::Dirty -{ - qint64 crc=0; - qint64 itm=0; - qint64 deletedReferences=0; - qint64 deletedNavmesh=0; - QString cleaningUtility; - QString info; - - QString toString(bool isClean) const + void handleReport() { - if (isClean) { - return QObject::tr("Verified clean by %1") - .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility); - } - - QString s = cleaningString(); + const auto& report = m_loot.report(); - if (!info.isEmpty()) { - s += " " + info; + if (!report.messages.empty()) { + addLineOutput(""); } - return s; - } + for (auto&& m : report.messages) { + log(levelFromLoot( + lootcli::logLevelFromString(m.type.toStdString())), + m.text); + } - QString cleaningString() const - { - return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).") - .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility) - .arg(itm) - .arg(deletedReferences) - .arg(deletedNavmesh); + for (auto&& p : report.plugins) { + for (auto&& d : p.dirty) { + m_core.pluginList()->addInformation(p.name, d.toString(false)); + } + } } }; -struct Loot::Plugin -{ - QString name; - std::vector incompatibilities; - std::vector messages; - std::vector dirty, clean; - std::vector missingMasters; - bool loadsArchive = false; - bool isMaster = false; - bool isLightMaster = false; -}; -struct Loot::Stats +QString Loot::Dirty::toString(bool isClean) const { - qint64 time = 0; - QString version; -}; + if (isClean) { + return QObject::tr("Verified clean by %1") + .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility); + } -struct Loot::Report -{ - std::vector messages; - std::vector plugins; - Stats stats; -}; + QString s = cleaningString(); + if (!info.isEmpty()) { + s += " " + info; + } + + return s; +} + +QString Loot::Dirty::cleaningString() const +{ + return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).") + .arg(cleaningUtility.isEmpty() ? "?" : cleaningUtility) + .arg(itm) + .arg(deletedReferences) + .arg(deletedNavmesh); +} -class ReportFailed {}; Loot::Loot() : m_thread(nullptr), m_cancel(false), m_result(false) @@ -437,6 +401,11 @@ const QString& Loot::outPath() const return m_outPath; } +const Loot::Report& Loot::report() const +{ + return m_report; +} + void Loot::lootThread() { try { @@ -558,7 +527,7 @@ void Loot::processStdout(const std::string &lootOut) void Loot::processMessage(const lootcli::Message& m) { - static const std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); + /*static const std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); static const std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); switch (m.type) @@ -595,7 +564,7 @@ void Loot::processMessage(const lootcli::Message& m) emit progress(m.progress); break; } - } + }*/ } void Loot::processOutputFile() @@ -623,19 +592,7 @@ void Loot::processOutputFile() return; } - const auto report = createReport(doc); - - for (auto&& m : report.messages) { - emit log(levelFromLoot( - lootcli::logLevelFromString(m.type.toStdString())), - m.text); - } - - for (auto&& p : report.plugins) { - for (auto&& d : p.dirty) { - emit information(p.name, d.toString(false)); - } - } + m_report = createReport(doc); } Loot::Report Loot::createReport(const QJsonDocument& doc) const diff --git a/src/loot.h b/src/loot.h index 95dbbe50..dc9b0d7b 100644 --- a/src/loot.h +++ b/src/loot.h @@ -17,6 +17,57 @@ class Loot : public QObject Q_OBJECT; public: + struct Message + { + QString type; + QString text; + }; + + struct File + { + QString name; + QString displayName; + }; + + struct Dirty + { + qint64 crc=0; + qint64 itm=0; + qint64 deletedReferences=0; + qint64 deletedNavmesh=0; + QString cleaningUtility; + QString info; + + QString toString(bool isClean) const; + QString cleaningString() const; + }; + + struct Plugin + { + QString name; + std::vector incompatibilities; + std::vector messages; + std::vector dirty, clean; + std::vector missingMasters; + bool loadsArchive = false; + bool isMaster = false; + bool isLightMaster = false; + }; + + struct Stats + { + qint64 time = 0; + QString version; + }; + + struct Report + { + std::vector messages; + std::vector plugins; + Stats stats; + }; + + Loot(); ~Loot(); @@ -24,23 +75,15 @@ public: void cancel(); bool result() const; const QString& outPath() const; + const Report& report() const; signals: void output(const QString& s); void progress(const lootcli::Progress p); void log(MOBase::log::Levels level, const QString& s); - void information(const QString& mod, const QString& info); void finished(); private: - struct Report; - struct Stats; - struct Message; - struct Plugin; - struct Dirty; - struct File; - class BadReport {}; - std::unique_ptr m_thread; std::atomic m_cancel; std::atomic m_result; @@ -48,6 +91,7 @@ private: env::HandlePtr m_lootProcess; env::HandlePtr m_stdout; std::string m_outputBuffer; + Report m_report; std::string readFromPipe(); diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index b50a51d8..ab421f2b 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -423,6 +423,17 @@ void PluginList::addInformation(const QString &name, const QString &message) } } +void PluginList::addLootReport(const QString& name, Loot::Plugin plugin) +{ + auto iter = m_ESPsByName.find(name.toLower()); + + if (iter != m_ESPsByName.end()) { + m_AdditionalInfo[name.toLower()].m_Loot = std::move(plugin); + } else { + log::warn("failed to associate loot report for \"{}\"", name); + } +} + bool PluginList::isEnabled(int index) { return m_ESPs.at(index).m_Enabled; @@ -908,137 +919,195 @@ void PluginList::testMasters() QVariant PluginList::data(const QModelIndex &modelIndex, int role) const { int index = modelIndex.row(); - if ((role == Qt::DisplayRole) - || (role == Qt::EditRole)) { - switch (modelIndex.column()) { - case COL_NAME: { - return m_ESPs[index].m_Name; - } break; - case COL_PRIORITY: { - return m_ESPs[index].m_Priority; - } break; - case COL_MODINDEX: { - return m_ESPs[index].m_Index; - } break; - default: { - return QVariant(); - } break; - } + + if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) { + return displayData(modelIndex); } else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) { - if (m_ESPs[index].m_ForceEnabled) { - return QVariant(); - } else { - return m_ESPs[index].m_Enabled ? Qt::Checked : Qt::Unchecked; - } + return checkstateData(modelIndex); } else if (role == Qt::ForegroundRole) { - if ((modelIndex.column() == COL_NAME) && - m_ESPs[index].m_ForceEnabled) { - return QBrush(Qt::gray); - } - } else if (role == Qt::BackgroundRole - || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { - if (m_ESPs[index].m_ModSelected) { - return Settings::instance().colors().pluginListContained(); - } else { - return QVariant(); - } + return foregroundData(modelIndex); + } else if (role == Qt::BackgroundRole || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { + return backgroundData(modelIndex); } else if (role == Qt::FontRole) { - QFont result; - if (m_ESPs[index].m_IsMaster) { - result.setItalic(true); - result.setWeight(QFont::Bold); - } else if (m_ESPs[index].m_IsLight || m_ESPs[index].m_IsLightFlagged) { - result.setItalic(true); - } - return result; + return fontData(modelIndex); } else if (role == Qt::TextAlignmentRole) { - if (modelIndex.column() == 0) { - return QVariant(Qt::AlignLeft | Qt::AlignVCenter); - } else { - return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); - } + return alignmentData(modelIndex); } else if (role == Qt::ToolTipRole) { - QString name = m_ESPs[index].m_Name.toLower(); - auto addInfoIter = m_AdditionalInfo.find(name); - QString toolTip; - if (addInfoIter != m_AdditionalInfo.end()) { - if (!addInfoIter->second.m_Messages.isEmpty()) { - toolTip += "
      "; - for (auto&& message : addInfoIter->second.m_Messages) { - toolTip += "
    • " + message + "
    • "; - } - toolTip += "

    "; + return tooltipData(modelIndex); + } else if (role == Qt::UserRole + 1) { + return iconData(modelIndex); + } + return QVariant(); +} + +QVariant PluginList::displayData(const QModelIndex &modelIndex) const +{ + int index = modelIndex.row(); + + switch (modelIndex.column()) { + case COL_NAME: { + return m_ESPs[index].m_Name; + } break; + case COL_PRIORITY: { + return m_ESPs[index].m_Priority; + } break; + case COL_MODINDEX: { + return m_ESPs[index].m_Index; + } break; + default: { + return QVariant(); + } break; + } +} + +QVariant PluginList::checkstateData(const QModelIndex &modelIndex) const +{ + int index = modelIndex.row(); + + if (m_ESPs[index].m_ForceEnabled) { + return QVariant(); + } else { + return m_ESPs[index].m_Enabled ? Qt::Checked : Qt::Unchecked; + } +} + +QVariant PluginList::foregroundData(const QModelIndex &modelIndex) const +{ + int index = modelIndex.row(); + + if ((modelIndex.column() == COL_NAME) && + m_ESPs[index].m_ForceEnabled) { + return QBrush(Qt::gray); + } + + return {}; +} + +QVariant PluginList::backgroundData(const QModelIndex &modelIndex) const +{ + int index = modelIndex.row(); + + if (m_ESPs[index].m_ModSelected) { + return Settings::instance().colors().pluginListContained(); + } else { + return QVariant(); + } +} + +QVariant PluginList::fontData(const QModelIndex &modelIndex) const +{ + int index = modelIndex.row(); + + QFont result; + + if (m_ESPs[index].m_IsMaster) { + result.setItalic(true); + result.setWeight(QFont::Bold); + } else if (m_ESPs[index].m_IsLight || m_ESPs[index].m_IsLightFlagged) { + result.setItalic(true); + } + + return result; +} + +QVariant PluginList::alignmentData(const QModelIndex &modelIndex) const +{ + int index = modelIndex.row(); + + if (modelIndex.column() == 0) { + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + } else { + return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); + } +} + +QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const +{ + int index = modelIndex.row(); + + QString name = m_ESPs[index].m_Name.toLower(); + auto addInfoIter = m_AdditionalInfo.find(name); + QString toolTip; + if (addInfoIter != m_AdditionalInfo.end()) { + if (!addInfoIter->second.m_Messages.isEmpty()) { + toolTip += "
      "; + for (auto&& message : addInfoIter->second.m_Messages) { + toolTip += "
    • " + message + "
    • "; } + toolTip += "

    "; } - if (m_ESPs[index].m_ForceEnabled) { - QString text = tr("Origin: %1").arg(m_ESPs[index].m_OriginName); - text += tr("
    This plugin can't be disabled (enforced by the game)."); - toolTip += text; - } else { - QString text = tr("Origin: %1").arg(m_ESPs[index].m_OriginName); - if (m_ESPs[index].m_Author.size() > 0) { - text += "
    " + tr("Author") + ": " + TruncateString(m_ESPs[index].m_Author); - } - if (m_ESPs[index].m_Description.size() > 0) { - text += "
    " + tr("Description") + ": " + TruncateString(m_ESPs[index].m_Description); - } - if (m_ESPs[index].m_MasterUnset.size() > 0) { - text += "
    " + tr("Missing Masters") + ": " + TruncateString(SetJoin(m_ESPs[index].m_MasterUnset, ", ")) + ""; - } - std::set enabledMasters; - std::set_difference(m_ESPs[index].m_Masters.begin(), m_ESPs[index].m_Masters.end(), - m_ESPs[index].m_MasterUnset.begin(), m_ESPs[index].m_MasterUnset.end(), - std::inserter(enabledMasters, enabledMasters.end())); - if (!enabledMasters.empty()) { - text += "
    " + tr("Enabled Masters") + ": " + TruncateString(SetJoin(enabledMasters, ", ")); - } - if (!m_ESPs[index].m_Archives.empty()) { - text += "
    " + tr("Loads Archives") + ": " + TruncateString(SetJoin(m_ESPs[index].m_Archives, ", ")); - text += "
    " + tr("There are Archives connected to this plugin. " - "Their assets will be added to your game, overwriting in case of conflicts following the plugin order. " - "Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin)"); - } - if (m_ESPs[index].m_HasIni) { - text += "
    " + tr("Loads INI settings") + ": "; - text += "
    " + tr("There is an ini file connected to this plugin. " - "Its settings will be added to your game settings, overwriting in case of conflicts."); - } - if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) { - text += "

    " + tr("This ESP is flagged as an ESL. " - "It will adhere to the ESP load order but the records will be loaded in ESL space."); - } - toolTip += text; + } + if (m_ESPs[index].m_ForceEnabled) { + QString text = tr("Origin: %1").arg(m_ESPs[index].m_OriginName); + text += tr("
    This plugin can't be disabled (enforced by the game)."); + toolTip += text; + } else { + QString text = tr("Origin: %1").arg(m_ESPs[index].m_OriginName); + if (m_ESPs[index].m_Author.size() > 0) { + text += "
    " + tr("Author") + ": " + TruncateString(m_ESPs[index].m_Author); + } + if (m_ESPs[index].m_Description.size() > 0) { + text += "
    " + tr("Description") + ": " + TruncateString(m_ESPs[index].m_Description); } - return toolTip; - } else if (role == Qt::UserRole + 1) { - QVariantList result; - QString nameLower = m_ESPs[index].m_Name.toLower(); if (m_ESPs[index].m_MasterUnset.size() > 0) { - result.append(":/MO/gui/warning"); + text += "
    " + tr("Missing Masters") + ": " + TruncateString(SetJoin(m_ESPs[index].m_MasterUnset, ", ")) + ""; } - if (m_LockedOrder.find(nameLower) != m_LockedOrder.end()) { - result.append(":/MO/gui/locked"); + std::set enabledMasters; + std::set_difference(m_ESPs[index].m_Masters.begin(), m_ESPs[index].m_Masters.end(), + m_ESPs[index].m_MasterUnset.begin(), m_ESPs[index].m_MasterUnset.end(), + std::inserter(enabledMasters, enabledMasters.end())); + if (!enabledMasters.empty()) { + text += "
    " + tr("Enabled Masters") + ": " + TruncateString(SetJoin(enabledMasters, ", ")); } - auto bossInfoIter = m_AdditionalInfo.find(nameLower); - if (bossInfoIter != m_AdditionalInfo.end()) { - if (!bossInfoIter->second.m_Messages.isEmpty()) { - result.append(":/MO/gui/information"); - } + if (!m_ESPs[index].m_Archives.empty()) { + text += "
    " + tr("Loads Archives") + ": " + TruncateString(SetJoin(m_ESPs[index].m_Archives, ", ")); + text += "
    " + tr("There are Archives connected to this plugin. " + "Their assets will be added to your game, overwriting in case of conflicts following the plugin order. " + "Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin)"); } if (m_ESPs[index].m_HasIni) { - result.append(":/MO/gui/attachment"); - } - if (!m_ESPs[index].m_Archives.empty()) { - result.append(":/MO/gui/archive_conflict_neutral"); + text += "
    " + tr("Loads INI settings") + ": "; + text += "
    " + tr("There is an ini file connected to this plugin. " + "Its settings will be added to your game settings, overwriting in case of conflicts."); } if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) { - result.append(":/MO/gui/awaiting"); + text += "

    " + tr("This ESP is flagged as an ESL. " + "It will adhere to the ESP load order but the records will be loaded in ESL space."); } - return result; + toolTip += text; } - return QVariant(); + return toolTip; } +QVariant PluginList::iconData(const QModelIndex &modelIndex) const +{ + int index = modelIndex.row(); + + QVariantList result; + QString nameLower = m_ESPs[index].m_Name.toLower(); + if (m_ESPs[index].m_MasterUnset.size() > 0) { + result.append(":/MO/gui/warning"); + } + if (m_LockedOrder.find(nameLower) != m_LockedOrder.end()) { + result.append(":/MO/gui/locked"); + } + auto bossInfoIter = m_AdditionalInfo.find(nameLower); + if (bossInfoIter != m_AdditionalInfo.end()) { + if (!bossInfoIter->second.m_Messages.isEmpty()) { + result.append(":/MO/gui/information"); + } + } + if (m_ESPs[index].m_HasIni) { + result.append(":/MO/gui/attachment"); + } + if (!m_ESPs[index].m_Archives.empty()) { + result.append(":/MO/gui/archive_conflict_neutral"); + } + if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) { + result.append(":/MO/gui/awaiting"); + } + return result; +} bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role) { diff --git a/src/pluginlist.h b/src/pluginlist.h index 092ba378..5cbe0a17 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -23,6 +23,8 @@ along with Mod Organizer. If not, see . #include #include #include "profile.h" +#include "loot.h" + namespace MOBase { class IPluginGame; } #include @@ -154,6 +156,11 @@ public: */ void addInformation(const QString &name, const QString &message); + /** + * adds information from a loot report + */ + void addLootReport(const QString& name, Loot::Plugin plugin); + /** * @brief test if a plugin is enabled * @@ -324,6 +331,7 @@ private: struct AdditionalInfo { QStringList m_Messages; + Loot::Plugin m_Loot; }; friend bool ByName(const ESPInfo& LHS, const ESPInfo& RHS); @@ -372,6 +380,15 @@ private: const MOBase::IPluginGame *m_GamePlugin; + + QVariant displayData(const QModelIndex &modelIndex) const; + QVariant checkstateData(const QModelIndex &modelIndex) const; + QVariant foregroundData(const QModelIndex &modelIndex) const; + QVariant backgroundData(const QModelIndex &modelIndex) const; + QVariant fontData(const QModelIndex &modelIndex) const; + QVariant alignmentData(const QModelIndex &modelIndex) const; + QVariant tooltipData(const QModelIndex &modelIndex) const; + QVariant iconData(const QModelIndex &modelIndex) const; }; #pragma warning(pop) -- cgit v1.3.1 From 54b18e88159738a3054c71d5a4827646caad4ded Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sat, 23 Nov 2019 23:54:19 -0500 Subject: added loot info to tooltip --- src/loot.cpp | 51 +++++-------- src/loot.h | 2 +- src/pluginlist.cpp | 212 +++++++++++++++++++++++++++++++++++++++++++---------- src/pluginlist.h | 4 + 4 files changed, 195 insertions(+), 74 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 66e8a01d..c315056b 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -254,15 +254,11 @@ private: } for (auto&& m : report.messages) { - log(levelFromLoot( - lootcli::logLevelFromString(m.type.toStdString())), - m.text); + log(m.type, m.text); } for (auto&& p : report.plugins) { - for (auto&& d : p.dirty) { - m_core.pluginList()->addInformation(p.name, d.toString(false)); - } + m_core.pluginList()->addLootReport(p.name, p); } } }; @@ -527,35 +523,11 @@ void Loot::processStdout(const std::string &lootOut) void Loot::processMessage(const lootcli::Message& m) { - /*static const std::regex exRequires("\"([^\"]*)\" requires \"([^\"]*)\", but it is missing\\."); - static const std::regex exIncompatible("\"([^\"]*)\" is incompatible with \"([^\"]*)\", but both are present\\."); - switch (m.type) { case lootcli::MessageType::Log: { - if (m.logLevel == lootcli::LogLevels::Error) { - std::smatch match; - - if (std::regex_match(m.log, match, exRequires)) { - std::string modName(match[1].first, match[1].second); - std::string dependency(match[2].first, match[2].second); - emit information( - QString::fromStdString(modName), - tr("depends on missing \"%1\"").arg(dependency.c_str())); - } else if (std::regex_match(m.log, match, exIncompatible)) { - std::string modName(match[1].first, match[1].second); - std::string dependency(match[2].first, match[2].second); - emit information( - QString::fromStdString(modName), - tr("incompatible with \"%1\"").arg(dependency.c_str())); - } else { - emit log(levelFromLoot(m.logLevel), QString::fromStdString(m.log)); - } - } else { - emit log(levelFromLoot(m.logLevel), QString::fromStdString(m.log)); - } - + emit log(levelFromLoot(m.logLevel), QString::fromStdString(m.log)); break; } @@ -564,7 +536,7 @@ void Loot::processMessage(const lootcli::Message& m) emit progress(m.progress); break; } - }*/ + } } void Loot::processOutputFile() @@ -674,7 +646,20 @@ std::vector Loot::reportMessages(const QJsonArray& array) const } Message m; - m.type = getWarn(o, "type"); + + const auto type = getWarn(o, "type"); + + if (type == "info") { + m.type = log::Info; + } else if (type == "warn") { + m.type = log::Warning; + } else if (type == "error") { + m.type = log::Error; + } else { + log::error("unknown message type '{}'", type); + m.type = log::Info; + } + m.text = getWarn(o, "text"); if (!m.text.isEmpty()) { diff --git a/src/loot.h b/src/loot.h index dc9b0d7b..3a7c6aa9 100644 --- a/src/loot.h +++ b/src/loot.h @@ -19,7 +19,7 @@ class Loot : public QObject public: struct Message { - QString type; + MOBase::log::Levels type; QString text; }; diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index 52c3fc3c..e91d820d 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -1034,36 +1034,30 @@ QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const QString toolTip; - // additional info - auto itor = m_AdditionalInfo.find(esp.name.toLower()); - - if (itor != m_AdditionalInfo.end()) { - if (!itor->second.messages.isEmpty()) { - toolTip += "
      "; - - for (auto&& message : itor->second.messages) { - toolTip += "
    • " + message + "
    • "; - } - - toolTip += "

    "; - } - } - - toolTip += tr("Origin: %1").arg(esp.originName); + toolTip += "" + tr("Origin") + ": " + esp.originName; if (esp.forceEnabled) { - toolTip += tr("
    This plugin can't be disabled (enforced by the game)."); + toolTip += + "
    " + + tr("This plugin can't be disabled (enforced by the game).") + + ""; } else { if (!esp.author.isEmpty()) { - toolTip += "
    " + tr("Author") + ": " + TruncateString(esp.author); + toolTip += + "
    " + tr("Author") + ": " + + TruncateString(esp.author); } if (esp.description.size() > 0) { - toolTip += "
    " + tr("Description") + ": " + TruncateString(esp.description); + toolTip += + "
    " + tr("Description") + ": " + + TruncateString(esp.description); } if (esp.masterUnset.size() > 0) { - toolTip += "
    " + tr("Missing Masters") + ": " + TruncateString(SetJoin(esp.masterUnset, ", ")) + ""; + toolTip += + "
    " + tr("Missing Masters") + ": " + + "" + TruncateString(SetJoin(esp.masterUnset, ", ")) + ""; } std::set enabledMasters; @@ -1072,61 +1066,199 @@ QVariant PluginList::tooltipData(const QModelIndex &modelIndex) const std::inserter(enabledMasters, enabledMasters.end())); if (!enabledMasters.empty()) { - toolTip += "
    " + tr("Enabled Masters") + ": " + TruncateString(SetJoin(enabledMasters, ", ")); + toolTip += + "
    " + tr("Enabled Masters") + ": " + + TruncateString(SetJoin(enabledMasters, ", ")); } if (!esp.archives.empty()) { - toolTip += "
    " + tr("Loads Archives") + ": " + TruncateString(SetJoin(esp.archives, ", ")); - toolTip += "
    " + tr("There are Archives connected to this plugin. " - "Their assets will be added to your game, overwriting in case of conflicts following the plugin order. " - "Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin)"); + toolTip += + "
    " + tr("Loads Archives") + ": " + + TruncateString(SetJoin(esp.archives, ", ")) + + "
    " + tr( + "There are Archives connected to this plugin. Their assets will be " + "added to your game, overwriting in case of conflicts following the " + "plugin order. Loose files will always overwrite assets from " + "Archives. (This flag only checks for Archives from the same mod as " + "the plugin)"); } if (esp.hasIni) { - toolTip += "
    " + tr("Loads INI settings") + ": "; - toolTip += "
    " + tr("There is an ini file connected to this plugin. " - "Its settings will be added to your game settings, overwriting in case of conflicts."); + toolTip += + "
    " + tr("Loads INI settings") + ": " + "
    " + tr( + "There is an ini file connected to this plugin. Its settings will " + "be added to your game settings, overwriting in case of conflicts."); } if (esp.isLightFlagged && !esp.isLight) { - toolTip += "

    " + tr("This ESP is flagged as an ESL. " - "It will adhere to the ESP load order but the records will be loaded in ESL space."); + toolTip += + "

    " + tr( + "This ESP is flagged as an ESL. It will adhere to the ESP load " + "order but the records will be loaded in ESL space."); } } + + // additional info + auto itor = m_AdditionalInfo.find(esp.name.toLower()); + + if (itor != m_AdditionalInfo.end()) { + if (!itor->second.messages.isEmpty()) { + toolTip += "
      "; + + for (auto&& message : itor->second.messages) { + toolTip += "
    • " + message + "
    • "; + } + + toolTip += "
    "; + } + + // loot + toolTip += makeLootTooltip(itor->second.loot); + } + return toolTip; } +QString PluginList::makeLootTooltip(const Loot::Plugin& loot) const +{ + QString s; + + for (auto&& f : loot.incompatibilities) { + s += + "
  • " + tr("Incompatible with %1") + .arg(f.displayName.isEmpty() ? f.name : f.displayName) + + "
  • "; + } + + for (auto&& m : loot.missingMasters) { + s += "
  • " + tr("Depends on missing %1").arg(m) + "
  • "; + } + + for (auto&& m : loot.messages) { + s += "
  • "; + + switch (m.type) + { + case log::Warning: + s += tr("Warning") + ": "; + break; + + case log::Error: + s += tr("Error") + ": "; + break; + + case log::Info: // fall-through + case log::Debug: + default: + // nothing + break; + } + + s += m.text + "
  • "; + } + + for (auto&& d : loot.dirty) { + s += "
  • " + d.toString(false) + "
  • "; + } + + for (auto&& c : loot.clean) { + s += "
  • " + c.toString(true) + "
  • "; + } + + if (!s.isEmpty()) { + s = + "
    " + "
      " + + s + + "
    "; + } + + return s; +} + QVariant PluginList::iconData(const QModelIndex &modelIndex) const { int index = modelIndex.row(); QVariantList result; - QString nameLower = m_ESPs[index].name.toLower(); - if (m_ESPs[index].masterUnset.size() > 0) { + + const auto& esp = m_ESPs[index]; + const QString nameLower = esp.name.toLower(); + + auto infoItor = m_AdditionalInfo.find(nameLower); + + const AdditionalInfo* info = nullptr; + if (infoItor != m_AdditionalInfo.end()) { + info = &infoItor->second; + } + + if (isProblematic(esp, info)) { result.append(":/MO/gui/warning"); } + if (m_LockedOrder.find(nameLower) != m_LockedOrder.end()) { result.append(":/MO/gui/locked"); } - auto bossInfoIter = m_AdditionalInfo.find(nameLower); - if (bossInfoIter != m_AdditionalInfo.end()) { - if (!bossInfoIter->second.messages.isEmpty()) { - result.append(":/MO/gui/information"); - } + + if (hasInfo(esp, info)) { + result.append(":/MO/gui/information"); } - if (m_ESPs[index].hasIni) { + + if (esp.hasIni) { result.append(":/MO/gui/attachment"); } - if (!m_ESPs[index].archives.empty()) { + + if (!esp.archives.empty()) { result.append(":/MO/gui/archive_conflict_neutral"); } - if (m_ESPs[index].isLightFlagged && !m_ESPs[index].isLight) { + + if (esp.isLightFlagged && !m_ESPs[index].isLight) { result.append(":/MO/gui/awaiting"); } + return result; } +bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const +{ + if (esp.masterUnset.size() > 0) { + return true; + } + + if (info) { + if (!info->loot.incompatibilities.empty()) { + return true; + } + + if (!info->loot.missingMasters.empty()) { + return true; + } + } + + return false; +} + +bool PluginList::hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const +{ + if (info) { + if (!info->messages.empty()) { + return true; + } + + if (!info->loot.messages.empty()) { + return true; + } + + if (!info->loot.dirty.empty()) { + return true; + } + } + + return false; +} + bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int role) { QString modName = modIndex.data().toString(); diff --git a/src/pluginlist.h b/src/pluginlist.h index 8b1ce90c..004b1590 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -394,6 +394,10 @@ private: QVariant alignmentData(const QModelIndex &modelIndex) const; QVariant tooltipData(const QModelIndex &modelIndex) const; QVariant iconData(const QModelIndex &modelIndex) const; + + QString makeLootTooltip(const Loot::Plugin& loot) const; + bool isProblematic(const ESPInfo& esp, const AdditionalInfo* info) const; + bool hasInfo(const ESPInfo& esp, const AdditionalInfo* info) const; }; #pragma warning(pop) -- cgit v1.3.1 From f5476531ae39fdae8c3adc63d4c4a11f92600ff8 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 19:39:26 -0500 Subject: basic html loot report --- src/loot.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 7 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index c315056b..c73dcaa7 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -32,14 +32,13 @@ log::Levels levelFromLoot(lootcli::LogLevels level) } } - class LootDialog : public QDialog { public: LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : QDialog(parent), m_core(core), m_loot(loot), - m_label(nullptr), m_progress(nullptr), m_buttons(nullptr), - m_report(nullptr), m_output(nullptr), + m_label(nullptr), m_progress(nullptr), + m_report(nullptr), m_output(nullptr), m_buttons(nullptr), m_finished(false), m_cancelling(false) { createUI(); @@ -143,9 +142,10 @@ private: Loot& m_loot; QLabel* m_label; QProgressBar* m_progress; - QDialogButtonBox* m_buttons; + QTextEdit* m_messages; QPushButton* m_report; QPlainTextEdit* m_output; + QDialogButtonBox* m_buttons; bool m_finished; bool m_cancelling; @@ -164,6 +164,9 @@ private: m_progress = new QProgressBar; ly->addWidget(m_progress); + m_messages = new QTextEdit; + ly->addWidget(m_messages); + auto* more = createMoreUI(); ly->addWidget(more); @@ -253,10 +256,46 @@ private: addLineOutput(""); } - for (auto&& m : report.messages) { - log(m.type, m.text); + QString html; + + if (!report.messages.empty()) { + html += "
      "; + + for (auto&& m : report.messages) { + log(m.type, m.text); + + html += "
    • "; + + switch (m.type) + { + case log::Error: + { + html += "" + QObject::tr("Error") + ": "; + break; + } + + case log::Warning: + { + html += "" + QObject::tr("Warning") + ": "; + break; + } + + default: + { + break; + } + } + + html += m.text + "
    • "; + } + + html + "
    "; + } else { + html += QObject::tr("No messages."); } + m_messages->setHtml(html); + for (auto&& p : report.plugins) { m_core.pluginList()->addLootReport(p.name, p); } @@ -299,7 +338,7 @@ Loot::~Loot() { m_thread->wait(); - if (!m_outPath.isEmpty()) { + if (!m_outPath.isEmpty() && QFile::exists(m_outPath)) { const auto r = shell::Delete(m_outPath); if (!r) { -- cgit v1.3.1 From cd876a0f9ffd03c711812c2ade92836e5d6c0203 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 20:11:50 -0500 Subject: split loot dialog, added ui file --- src/CMakeLists.txt | 10 +- src/loot.cpp | 271 +---------------------------------------------------- src/lootdialog.cpp | 222 +++++++++++++++++++++++++++++++++++++++++++ src/lootdialog.h | 47 ++++++++++ src/lootdialog.ui | 214 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 492 insertions(+), 272 deletions(-) create mode 100644 src/lootdialog.cpp create mode 100644 src/lootdialog.h create mode 100644 src/lootdialog.ui (limited to 'src/loot.cpp') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3b74ea37..db3bda73 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -143,6 +143,7 @@ SET(organizer_SRCS processrunner.cpp uilocker.cpp loot.cpp + lootdialog.cpp shared/windows_error.cpp shared/error_report.cpp @@ -265,6 +266,7 @@ SET(organizer_HDRS processrunner.h uilocker.h loot.h + lootdialog.h json.h shared/windows_error.h @@ -392,6 +394,11 @@ set(executables editexecutablesdialog ) +set(loot + loot + lootdialog +) + set(modinfo modinfo modinfobackup @@ -468,7 +475,6 @@ set(utilities shared/util usvfsconnector shared/windows_error - loot json ) @@ -490,7 +496,7 @@ set(widgets ) set(src_filters - application core browser dialogs downloads env executables modinfo + application core browser dialogs downloads env executables loot modinfo modinfo\\dialog modlist plugins previews profiles settings settingsdialog utilities widgets ) diff --git a/src/loot.cpp b/src/loot.cpp index c73dcaa7..f0618b0b 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -1,4 +1,5 @@ #include "loot.h" +#include "lootdialog.h" #include "spawn.h" #include "organizercore.h" #include "json.h" @@ -32,276 +33,6 @@ log::Levels levelFromLoot(lootcli::LogLevels level) } } -class LootDialog : public QDialog -{ -public: - LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : - QDialog(parent), m_core(core), m_loot(loot), - m_label(nullptr), m_progress(nullptr), - m_report(nullptr), m_output(nullptr), m_buttons(nullptr), - m_finished(false), m_cancelling(false) - { - createUI(); - m_progress->setMaximum(0); - - QObject::connect( - &m_loot, &Loot::output, this, - [&](auto&& s){ addOutput(s); }, Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::progress, - this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::log, this, - [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); - - QObject::connect( - &m_loot, &Loot::finished, this, - [&]{ onFinished(); }, Qt::QueuedConnection); - } - - void setText(const QString& s) - { - m_label->setText(s); - } - - void setProgress(lootcli::Progress p) - { - setText(progressToString(p)); - - if (p == lootcli::Progress::Done) { - m_progress->setRange(0, 1); - m_progress->setValue(1); - } - } - - QString progressToString(lootcli::Progress p) - { - using P = lootcli::Progress; - - switch (p) - { - case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); - case P::UpdatingMasterlist: return tr("Updating masterlist"); - case P::LoadingLists: return tr("Loading lists"); - case P::ReadingPlugins: return tr("Reading plugins"); - case P::SortingPlugins: return tr("Sorting plugins"); - case P::WritingLoadorder: return tr("Writing loadorder.txt"); - case P::ParsingLootMessages: return tr("Parsing loot messages"); - case P::Done: return tr("Done"); - default: return QString("unknown progress %1").arg(static_cast(p)); - } - } - - void addOutput(const QString& s) - { - if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { - return; - } - - const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); - - for (auto&& line : lines) { - if (line.isEmpty()) { - continue; - } - - addLineOutput(line); - } - } - - bool result() const - { - return m_loot.result(); - } - - void cancel() - { - if (!m_finished && !m_cancelling) { - addLineOutput(tr("Stopping LOOT...")); - m_loot.cancel(); - m_buttons->setEnabled(false); - m_cancelling = true; - } - } - - int exec() override - { - return QDialog::exec(); - } - - void openReport() - { - const auto path = m_loot.outPath(); - shell::Open(path); - } - -private: - OrganizerCore& m_core; - Loot& m_loot; - QLabel* m_label; - QProgressBar* m_progress; - QTextEdit* m_messages; - QPushButton* m_report; - QPlainTextEdit* m_output; - QDialogButtonBox* m_buttons; - bool m_finished; - bool m_cancelling; - - void createUI() - { - auto* root = new QWidget(this); - auto* ly = new QVBoxLayout(root); - - setLayout(new QVBoxLayout); - layout()->setContentsMargins(0, 0, 0, 0); - layout()->addWidget(root); - - m_label = new QLabel; - ly->addWidget(m_label); - - m_progress = new QProgressBar; - ly->addWidget(m_progress); - - m_messages = new QTextEdit; - ly->addWidget(m_messages); - - auto* more = createMoreUI(); - ly->addWidget(more); - - resize(700, 400); - } - - QWidget* createMoreUI() - { - auto* more = new QWidget; - auto* ly = new QVBoxLayout(more); - ly->setContentsMargins(0, 0, 0, 0); - - auto* buttons = new QHBoxLayout; - buttons->setContentsMargins(0, 0, 0, 0); - m_report = new QPushButton(tr("Open JSON report")); - m_report->setEnabled(false); - connect(m_report, &QPushButton::clicked, [&]{ openReport(); }); - buttons->addWidget(m_report); - buttons->addStretch(1); - ly->addLayout(buttons); - - m_output = new QPlainTextEdit; - ly->addWidget(m_output); - - m_buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); - connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); - ly->addWidget(m_buttons); - - return more; - } - - void closeEvent(QCloseEvent* e) override - { - if (m_finished) { - QDialog::closeEvent(e); - } else { - cancel(); - e->ignore(); - } - } - - void onButton(QAbstractButton* b) - { - if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { - if (m_finished) { - close(); - } else { - cancel(); - } - } - } - - void addLineOutput(const QString& line) - { - m_output->appendPlainText(line); - } - - void onFinished() - { - m_finished = true; - - if (m_cancelling) { - close(); - } else { - handleReport(); - m_report->setEnabled(true); - m_buttons->setStandardButtons(QDialogButtonBox::Close); - } - } - - void log(log::Levels lv, const QString& s) - { - if (lv >= log::Levels::Warning) { - log::log(lv, "{}", s); - } - - if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { - addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); - } - } - - void handleReport() - { - const auto& report = m_loot.report(); - - if (!report.messages.empty()) { - addLineOutput(""); - } - - QString html; - - if (!report.messages.empty()) { - html += "
      "; - - for (auto&& m : report.messages) { - log(m.type, m.text); - - html += "
    • "; - - switch (m.type) - { - case log::Error: - { - html += "" + QObject::tr("Error") + ": "; - break; - } - - case log::Warning: - { - html += "" + QObject::tr("Warning") + ": "; - break; - } - - default: - { - break; - } - } - - html += m.text + "
    • "; - } - - html + "
    "; - } else { - html += QObject::tr("No messages."); - } - - m_messages->setHtml(html); - - for (auto&& p : report.plugins) { - m_core.pluginList()->addLootReport(p.name, p); - } - } -}; - QString Loot::Dirty::toString(bool isClean) const { diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp new file mode 100644 index 00000000..db41959d --- /dev/null +++ b/src/lootdialog.cpp @@ -0,0 +1,222 @@ +#include "lootdialog.h" +#include "ui_lootdialog.h" +#include "loot.h" +#include "organizercore.h" +#include +#include + +using namespace MOBase; + +LootDialog::LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : + QDialog(parent), ui(new Ui::LootDialog), m_core(core), m_loot(loot), + m_finished(false), m_cancelling(false) +{ + createUI(); + + QObject::connect( + &m_loot, &Loot::output, this, + [&](auto&& s){ addOutput(s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::progress, + this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::log, this, + [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::finished, this, + [&]{ onFinished(); }, Qt::QueuedConnection); +} + +LootDialog::~LootDialog() = default; + +void LootDialog::setText(const QString& s) +{ + ui->progressText->setText(s); +} + +void LootDialog::setProgress(lootcli::Progress p) +{ + setText(progressToString(p)); + + if (p == lootcli::Progress::Done) { + ui->progressBar->setRange(0, 1); + ui->progressBar->setValue(1); + } +} + +QString LootDialog::progressToString(lootcli::Progress p) +{ + using P = lootcli::Progress; + + switch (p) + { + case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); + case P::UpdatingMasterlist: return tr("Updating masterlist"); + case P::LoadingLists: return tr("Loading lists"); + case P::ReadingPlugins: return tr("Reading plugins"); + case P::SortingPlugins: return tr("Sorting plugins"); + case P::WritingLoadorder: return tr("Writing loadorder.txt"); + case P::ParsingLootMessages: return tr("Parsing loot messages"); + case P::Done: return tr("Done"); + default: return QString("unknown progress %1").arg(static_cast(p)); + } +} + +void LootDialog::addOutput(const QString& s) +{ + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + return; + } + + const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); + + for (auto&& line : lines) { + if (line.isEmpty()) { + continue; + } + + addLineOutput(line); + } +} + +bool LootDialog::result() const +{ + return m_loot.result(); +} + +void LootDialog::cancel() +{ + if (!m_finished && !m_cancelling) { + addLineOutput(tr("Stopping LOOT...")); + m_loot.cancel(); + ui->buttons->setEnabled(false); + m_cancelling = true; + } +} + +void LootDialog::openReport() +{ + const auto path = m_loot.outPath(); + shell::Open(path); +} + +void LootDialog::createUI() +{ + ui->setupUi(this); + ui->progressBar->setMaximum(0); + + ui->openJsonReport->setEnabled(false); + connect(ui->openJsonReport, &QPushButton::clicked, [&]{ openReport(); }); + + new ExpanderWidget(ui->details, ui->detailsPanel); + + connect(ui->buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); + + resize(480, 275); +} + +void LootDialog::closeEvent(QCloseEvent* e) +{ + if (m_finished) { + QDialog::closeEvent(e); + } else { + cancel(); + e->ignore(); + } +} + +void LootDialog::onButton(QAbstractButton* b) +{ + if (ui->buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { + if (m_finished) { + close(); + } else { + cancel(); + } + } +} + +void LootDialog::addLineOutput(const QString& line) +{ + ui->output->appendPlainText(line); +} + +void LootDialog::onFinished() +{ + m_finished = true; + + if (m_cancelling) { + close(); + } else { + handleReport(); + ui->openJsonReport->setEnabled(true); + ui->buttons->setStandardButtons(QDialogButtonBox::Close); + } +} + +void LootDialog::log(log::Levels lv, const QString& s) +{ + if (lv >= log::Levels::Warning) { + log::log(lv, "{}", s); + } + + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); + } +} + +void LootDialog::handleReport() +{ + const auto& report = m_loot.report(); + + if (!report.messages.empty()) { + addLineOutput(""); + } + + QString html; + + if (!report.messages.empty()) { + html += "
      "; + + for (auto&& m : report.messages) { + log(m.type, m.text); + + html += "
    • "; + + switch (m.type) + { + case log::Error: + { + html += "" + QObject::tr("Error") + ": "; + break; + } + + case log::Warning: + { + html += "" + QObject::tr("Warning") + ": "; + break; + } + + default: + { + break; + } + } + + html += m.text + "
    • "; + } + + html + "
    "; + } else { + html += QObject::tr("No messages."); + } + + ui->report->setHtml(html); + + for (auto&& p : report.plugins) { + m_core.pluginList()->addLootReport(p.name, p); + } +} diff --git a/src/lootdialog.h b/src/lootdialog.h new file mode 100644 index 00000000..e4647b5c --- /dev/null +++ b/src/lootdialog.h @@ -0,0 +1,47 @@ +#ifndef MODORGANIZER_LOOTDIALOG_H +#define MODORGANIZER_LOOTDIALOG_H + +#include +#include + +namespace Ui { class LootDialog; } + +class OrganizerCore; +class Loot; + +class LootDialog : public QDialog +{ +public: + LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot); + ~LootDialog(); + + void setText(const QString& s); + void setProgress(lootcli::Progress p); + + QString progressToString(lootcli::Progress p); + + void addOutput(const QString& s); + + bool result() const; + + void cancel(); + + void openReport(); + +private: + std::unique_ptr ui; + OrganizerCore& m_core; + Loot& m_loot; + bool m_finished; + bool m_cancelling; + + void createUI(); + void closeEvent(QCloseEvent* e) override; + void onButton(QAbstractButton* b); + void addLineOutput(const QString& line); + void onFinished(); + void log(MOBase::log::Levels lv, const QString& s); + void handleReport(); +}; + +#endif // MODORGANIZER_LOOTDIALOG_H diff --git a/src/lootdialog.ui b/src/lootdialog.ui new file mode 100644 index 00000000..7e10b1db --- /dev/null +++ b/src/lootdialog.ui @@ -0,0 +1,214 @@ + + + LootDialog + + + + 0 + 0 + 457 + 343 + + + + LOOT + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Progress + + + + + + + 24 + + + false + + + + + + + true + + + Qt::TextBrowserInteraction + + + LOOT Report + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Details + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Open JSON report + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttons + accepted() + LootDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttons + rejected() + LootDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + -- cgit v1.3.1 From 70ee786102c8436c7f8f9e8a5d2ea71035d8d572 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 22:19:23 -0500 Subject: changed loot report to webengine with markdown support --- src/loot.cpp | 4 +- src/lootdialog.cpp | 74 +++++++++-- src/lootdialog.h | 31 +++++ src/lootdialog.ui | 49 ++++++-- src/resources/markdown.html | 299 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 434 insertions(+), 23 deletions(-) create mode 100644 src/resources/markdown.html (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index f0618b0b..5d75eeaa 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -67,7 +67,9 @@ Loot::Loot() Loot::~Loot() { - m_thread->wait(); + if (m_thread) { + m_thread->wait(); + } if (!m_outPath.isEmpty() && QFile::exists(m_outPath)) { const auto r = shell::Delete(m_outPath); diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index db41959d..9c7b482e 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -4,9 +4,44 @@ #include "organizercore.h" #include #include +#include using namespace MOBase; + +MarkdownDocument::MarkdownDocument(QObject* parent) + : QObject(parent) +{ +} + +void MarkdownDocument::setText(const QString& text) +{ + if (m_text == text) + return; + + m_text = text; + emit textChanged(m_text); +} + + +MarkdownPage::MarkdownPage(QObject* parent) + : QWebEnginePage(parent) +{ +} + +bool MarkdownPage::acceptNavigationRequest(const QUrl &url, NavigationType, bool) +{ + static const QStringList allowed = {"qrc", "data"}; + + if (!allowed.contains(url.scheme())) { + QDesktopServices::openUrl(url); + return false; + } + + return true; +} + + LootDialog::LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : QDialog(parent), ui(new Ui::LootDialog), m_core(core), m_loot(loot), m_finished(false), m_cancelling(false) @@ -108,6 +143,27 @@ void LootDialog::createUI() ui->setupUi(this); ui->progressBar->setMaximum(0); + auto* page = new MarkdownPage(this); + ui->report->setPage(page); + + auto* channel = new QWebChannel(this); + channel->registerObject("content", &m_report); + page->setWebChannel(channel); + + const QString path = QApplication::applicationDirPath() + "/resources/markdown.html"; + QFile f(path); + + if (f.open(QFile::ReadOnly)) { + const QString html = f.readAll(); + if (!html.isEmpty()) { + ui->report->setHtml(html); + } else { + log::error("failed to read '{}', {}", path, f.errorString()); + } + } else { + log::error("can't open '{}', {}", path, f.errorString()); + } + ui->openJsonReport->setEnabled(false); connect(ui->openJsonReport, &QPushButton::clicked, [&]{ openReport(); }); @@ -176,27 +232,25 @@ void LootDialog::handleReport() addLineOutput(""); } - QString html; + QString md; if (!report.messages.empty()) { - html += "
      "; - for (auto&& m : report.messages) { log(m.type, m.text); - html += "
    • "; + md += " - "; switch (m.type) { case log::Error: { - html += "" + QObject::tr("Error") + ": "; + md += "**" + QObject::tr("Error") + "**: "; break; } case log::Warning: { - html += "" + QObject::tr("Warning") + ": "; + md += "**" + QObject::tr("Warning") + "**: "; break; } @@ -206,15 +260,13 @@ void LootDialog::handleReport() } } - html += m.text + "
    • "; + md += m.text + "\n"; } - - html + "
    "; } else { - html += QObject::tr("No messages."); + md += QObject::tr("**No messages.**"); } - ui->report->setHtml(html); + m_report.setText(md); for (auto&& p : report.plugins) { m_core.pluginList()->addLootReport(p.name, p); diff --git a/src/lootdialog.h b/src/lootdialog.h index e4647b5c..df9e546d 100644 --- a/src/lootdialog.h +++ b/src/lootdialog.h @@ -9,6 +9,36 @@ namespace Ui { class LootDialog; } class OrganizerCore; class Loot; + +class MarkdownDocument : public QObject +{ + Q_OBJECT; + Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged FINAL); + +public: + explicit MarkdownDocument(QObject* parent=nullptr); + void setText(const QString& text); + +signals: + void textChanged(const QString &text); + +private: + QString m_text; +}; + + +class MarkdownPage : public QWebEnginePage +{ + Q_OBJECT; + +public: + explicit MarkdownPage(QObject* parent=nullptr); + +protected: + bool acceptNavigationRequest(const QUrl &url, NavigationType, bool) override; +}; + + class LootDialog : public QDialog { public: @@ -34,6 +64,7 @@ private: Loot& m_loot; bool m_finished; bool m_cancelling; + MarkdownDocument m_report; void createUI(); void closeEvent(QCloseEvent* e) override; diff --git a/src/lootdialog.ui b/src/lootdialog.ui index 7e10b1db..e366ce41 100644 --- a/src/lootdialog.ui +++ b/src/lootdialog.ui @@ -7,7 +7,7 @@ 0 0 457 - 343 + 600 @@ -16,7 +16,7 @@ - + 0 @@ -31,7 +31,7 @@ - + 0 @@ -62,16 +62,36 @@ - - - true + + + QFrame::StyledPanel - - Qt::TextBrowserInteraction - - - LOOT Report + + QFrame::Sunken + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + about:blank + + + + + @@ -176,6 +196,13 @@ + + + QWebEngineView + QWidget +
    QtWebEngineWidgets/QWebEngineView
    +
    +
    diff --git a/src/resources/markdown.html b/src/resources/markdown.html new file mode 100644 index 00000000..a09b2209 --- /dev/null +++ b/src/resources/markdown.html @@ -0,0 +1,299 @@ + + + + + + + + + +
    + + + \ No newline at end of file -- cgit v1.3.1 From 64304a6368cf8357bb04d1c0057aec637ebb479a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 23:23:15 -0500 Subject: tweaked css, finished markdown report copy markdown.html to resources/ --- src/CMakeLists.txt | 4 +- src/loot.cpp | 139 ++++++++++++ src/loot.h | 13 +- src/lootdialog.cpp | 44 +--- src/resources/markdown.html | 528 ++++++++++++++++++++++---------------------- 5 files changed, 424 insertions(+), 304 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index db3bda73..5a510806 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -676,4 +676,6 @@ INSTALL( ) # qdds.dll needs installing manually as Qt no longer ships with it by default. -INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) \ No newline at end of file +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) + +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/markdown.html DESTINATION bin/resources) diff --git a/src/loot.cpp b/src/loot.cpp index 5d75eeaa..2faf9c2e 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -34,6 +34,99 @@ log::Levels levelFromLoot(lootcli::LogLevels level) } +QString Loot::Report::toMarkdown() const +{ + QString s; + + if (!messages.empty()) { + s += "### " + QObject::tr("General messages") + "\n"; + + for (auto&& m : messages) { + s += " - " + m.toMarkdown() + "\n"; + } + } + + if (!plugins.empty()) { + if (!s.isEmpty()) { + s += "\n"; + } + + s += "### " + QObject::tr("Plugins") + "\n"; + + for (auto&& p : plugins) { + const auto ps = p.toMarkdown(); + if (!ps.isEmpty()) { + s += ps + "\n"; + } + } + } + + if (s.isEmpty()) { + s += "**" + QObject::tr("No messages.") + "**"; + } + + s += stats.toMarkdown(); + + return s; +} + +QString Loot::Stats::toMarkdown() const +{ + return QString("`stats: %1s, lootcli %2, loot %3`") + .arg(QString::number(time / 1000.0, 'f', 2)) + .arg(lootcliVersion) + .arg(lootVersion); +} + +QString Loot::Plugin::toMarkdown() const +{ + QString s; + + if (!incompatibilities.empty()) { + s += " - **" + QObject::tr("Incompatibilities") + ": "; + + QString fs; + for (auto&& f : incompatibilities) { + if (!fs.isEmpty()) { + fs += ", "; + } + + fs += f.displayName.isEmpty() ? f.name : f.displayName; + } + + s += fs + "**\n"; + } + + if (!missingMasters.empty()) { + s += " - **" + QObject::tr("Missing masters") + ": "; + + QString ms; + for (auto&& m : missingMasters) { + if (!ms.isEmpty()) { + ms += ", "; + } + + ms += m; + } + + s += ms + "**\n"; + } + + for (auto&& m : messages) { + s += " - " + m.toMarkdown() + "\n"; + } + + for (auto&& d : dirty) { + s += " - " + d.toMarkdown(false) + "\n"; + } + + if (!s.isEmpty()) { + s = "#### " + name + "\n" + s; + } + + return s; +} + QString Loot::Dirty::toString(bool isClean) const { if (isClean) { @@ -50,6 +143,11 @@ QString Loot::Dirty::toString(bool isClean) const return s; } +QString Loot::Dirty::toMarkdown(bool isClean) const +{ + return toString(isClean); +} + QString Loot::Dirty::cleaningString() const { return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).") @@ -59,6 +157,35 @@ QString Loot::Dirty::cleaningString() const .arg(deletedNavmesh); } +QString Loot::Message::toMarkdown() const +{ + QString s; + + switch (type) + { + case log::Error: + { + s += "**" + QObject::tr("Error") + "**: "; + break; + } + + case log::Warning: + { + s += "**" + QObject::tr("Warning") + "**: "; + break; + } + + default: + { + break; + } + } + + s += text; + + return s; +} + Loot::Loot() : m_thread(nullptr), m_cancel(false), m_result(false) @@ -348,6 +475,7 @@ Loot::Report Loot::createReport(const QJsonDocument& doc) const r.messages = reportMessages(getOpt(object, "messages")); r.plugins = reportPlugins(getOpt(object, "plugins")); + r.stats = reportStats(getWarn(object, "stats")); return r; } @@ -407,6 +535,17 @@ Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const return p; } +Loot::Stats Loot::reportStats(const QJsonObject& stats) const +{ + Stats s; + + s.time = getWarn(stats, "time"); + s.lootcliVersion = getWarn(stats, "lootcliVersion"); + s.lootVersion = getWarn(stats, "lootVersion"); + + return s; +} + std::vector Loot::reportMessages(const QJsonArray& array) const { std::vector v; diff --git a/src/loot.h b/src/loot.h index 3a7c6aa9..30ef4b60 100644 --- a/src/loot.h +++ b/src/loot.h @@ -21,6 +21,8 @@ public: { MOBase::log::Levels type; QString text; + + QString toMarkdown() const; }; struct File @@ -39,6 +41,7 @@ public: QString info; QString toString(bool isClean) const; + QString toMarkdown(bool isClean) const; QString cleaningString() const; }; @@ -52,12 +55,17 @@ public: bool loadsArchive = false; bool isMaster = false; bool isLightMaster = false; + + QString toMarkdown() const; }; struct Stats { qint64 time = 0; - QString version; + QString lootcliVersion; + QString lootVersion; + + QString toMarkdown() const; }; struct Report @@ -65,6 +73,8 @@ public: std::vector messages; std::vector plugins; Stats stats; + + QString toMarkdown() const; }; @@ -107,6 +117,7 @@ private: Message reportMessage(const QJsonObject& message) const; std::vector reportPlugins(const QJsonArray& plugins) const; Loot::Plugin reportPlugin(const QJsonObject& plugin) const; + Loot::Stats reportStats(const QJsonObject& stats) const; std::vector reportMessages(const QJsonArray& array) const; std::vector reportFiles(const QJsonArray& array) const; diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index 9c7b482e..c7cdfecd 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -226,49 +226,19 @@ void LootDialog::log(log::Levels lv, const QString& s) void LootDialog::handleReport() { - const auto& report = m_loot.report(); + const auto& lootReport = m_loot.report(); - if (!report.messages.empty()) { + if (!lootReport.messages.empty()) { addLineOutput(""); } - QString md; - - if (!report.messages.empty()) { - for (auto&& m : report.messages) { - log(m.type, m.text); - - md += " - "; - - switch (m.type) - { - case log::Error: - { - md += "**" + QObject::tr("Error") + "**: "; - break; - } - - case log::Warning: - { - md += "**" + QObject::tr("Warning") + "**: "; - break; - } - - default: - { - break; - } - } - - md += m.text + "\n"; - } - } else { - md += QObject::tr("**No messages.**"); + for (auto&& m : lootReport.messages) { + log(m.type, m.text); } - m_report.setText(md); - - for (auto&& p : report.plugins) { + for (auto&& p : lootReport.plugins) { m_core.pluginList()->addLootReport(p.name, p); } + + m_report.setText(lootReport.toMarkdown()); } diff --git a/src/resources/markdown.html b/src/resources/markdown.html index a09b2209..1ecaf1b9 100644 --- a/src/resources/markdown.html +++ b/src/resources/markdown.html @@ -2,278 +2,276 @@ - - - -- cgit v1.3.1 From 0e45044dbd724e9050bea00511585dc023afe144 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 25 Nov 2019 00:47:45 -0500 Subject: fixed cancel button debug logs, some cleanup --- src/loot.cpp | 47 +++++++++++++++++++++------- src/lootdialog.cpp | 92 ++++++++++++++++++++++++++++++++++-------------------- src/lootdialog.h | 11 +++---- 3 files changed, 98 insertions(+), 52 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 2faf9c2e..9c8cf8c4 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -199,6 +199,7 @@ Loot::~Loot() } if (!m_outPath.isEmpty() && QFile::exists(m_outPath)) { + log::debug("deleting temporary loot report '{}'", m_outPath); const auto r = shell::Delete(m_outPath); if (!r) { @@ -211,6 +212,8 @@ Loot::~Loot() bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { + log::debug("starting loot"); + m_outPath = QDir::temp().absoluteFilePath("lootreport.json"); const auto logLevel = core.settings().diagnostics().lootLogLevel(); @@ -271,8 +274,19 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) core.pluginList()->clearAdditionalInformation(); + log::debug("starting loot thread"); + m_thread.reset(QThread::create([&]{ - lootThread(); + try + { + lootThread(); + } + catch(...) + { + log::error("unhandled exception in loot thread"); + } + + log::debug("finishing loot thread"); emit finished(); })); @@ -283,7 +297,10 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) void Loot::cancel() { - m_cancel = true; + if (!m_cancel) { + log::debug("loot received cancel request"); + m_cancel = true; + } } bool Loot::result() const @@ -303,6 +320,8 @@ const Loot::Report& Loot::report() const void Loot::lootThread() { + ::SetThreadDescription(GetCurrentThread(), L"loot"); + try { m_result = false; @@ -321,10 +340,13 @@ bool Loot::waitForCompletion() { bool terminating = false; + log::debug("loot thread waiting for completion on lootcli"); + for (;;) { DWORD res = WaitForSingleObject(m_lootProcess.get(), 100); if (res == WAIT_OBJECT_0) { + log::debug("lootcli has completed"); // done break; } @@ -336,9 +358,13 @@ bool Loot::waitForCompletion() } if (m_cancel) { - // terminate and wait to finish + log::debug("terminating lootcli process"); ::TerminateProcess(m_lootProcess.get(), 1); + + log::debug("waiting for loocli process to terminate"); WaitForSingleObject(m_lootProcess.get(), INFINITE); + + log::debug("lootcli terminated"); return false; } @@ -396,6 +422,12 @@ void Loot::processStdout(const std::string &lootOut) emit output(QString::fromStdString(lootOut)); m_outputBuffer += lootOut; + if (m_outputBuffer.empty()) { + return; + } + + log::debug("loot: processing stdout ({} bytes)", m_outputBuffer.size()); + std::size_t start = 0; for (;;) { @@ -440,7 +472,7 @@ void Loot::processMessage(const lootcli::Message& m) void Loot::processOutputFile() { - log::info("parsing json output file at '{}'", m_outPath); + log::debug("parsing json output file at '{}'", m_outPath); QFile outFile(m_outPath); if (!outFile.open(QIODevice::ReadOnly)) { @@ -645,20 +677,13 @@ std::vector Loot::reportStringArray(const QJsonArray& array) const bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { - //m_OrganizerCore.currentProfile()->writeModlistNow(); core.savePluginList(); - //Create a backup of the load orders w/ LOOT in name - //to make sure that any sorting is easily undo-able. - //Need to figure out how I want to do that. - try { Loot loot; LootDialog dialog(parent, core, loot); loot.start(parent, core, didUpdateMasterList); - - dialog.setText(QObject::tr("Please wait while LOOT is running")); dialog.exec(); return dialog.result(); diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index 80664415..43929c00 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -8,6 +8,29 @@ using namespace MOBase; +QString progressToString(lootcli::Progress p) +{ + using P = lootcli::Progress; + + static const std::map map = { + {P::CheckingMasterlistExistence, QObject::tr("Checking masterlist existence")}, + {P::UpdatingMasterlist, QObject::tr("Updating masterlist")}, + {P::LoadingLists, QObject::tr("Loading lists")}, + {P::ReadingPlugins, QObject::tr("Reading plugins")}, + {P::SortingPlugins, QObject::tr("Sorting plugins")}, + {P::WritingLoadorder, QObject::tr("Writing loadorder.txt")}, + {P::ParsingLootMessages, QObject::tr("Parsing loot messages")}, + {P::Done, QObject::tr("Done")} + }; + + auto itor = map.find(p); + if (itor == map.end()) { + return QString("unknown progress %1").arg(static_cast(p)); + } else { + return itor->second; + } +} + MarkdownDocument::MarkdownDocument(QObject* parent) : QObject(parent) @@ -74,7 +97,11 @@ void LootDialog::setText(const QString& s) void LootDialog::setProgress(lootcli::Progress p) { - setText(progressToString(p)); + // don't overwrite the "stopping loot" message even if lootcli generates a new + // progress message + if (!m_cancelling) { + setText(progressToString(p)); + } if (p == lootcli::Progress::Done) { ui->progressBar->setRange(0, 1); @@ -82,24 +109,6 @@ void LootDialog::setProgress(lootcli::Progress p) } } -QString LootDialog::progressToString(lootcli::Progress p) -{ - using P = lootcli::Progress; - - switch (p) - { - case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); - case P::UpdatingMasterlist: return tr("Updating masterlist"); - case P::LoadingLists: return tr("Loading lists"); - case P::ReadingPlugins: return tr("Reading plugins"); - case P::SortingPlugins: return tr("Sorting plugins"); - case P::WritingLoadorder: return tr("Writing loadorder.txt"); - case P::ParsingLootMessages: return tr("Parsing loot messages"); - case P::Done: return tr("Done"); - default: return QString("unknown progress %1").arg(static_cast(p)); - } -} - void LootDialog::addOutput(const QString& s) { if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { @@ -125,8 +134,12 @@ bool LootDialog::result() const void LootDialog::cancel() { if (!m_finished && !m_cancelling) { - addLineOutput(tr("Stopping LOOT...")); + log::debug("loot dialog: cancelling"); m_loot.cancel(); + + setText(tr("Stopping LOOT...")); + addLineOutput("stopping loot"); + ui->buttons->setEnabled(false); m_cancelling = true; } @@ -138,6 +151,22 @@ void LootDialog::openReport() shell::Open(path); } +void LootDialog::accept() +{ + // no-op +} + +void LootDialog::reject() +{ + if (m_finished) { + log::debug("loot dialog reject: loot finished, closing"); + QDialog::reject(); + } else { + log::debug("loot dialog reject: not finished, cancelling"); + cancel(); + } +} + void LootDialog::createUI() { ui->setupUi(this); @@ -169,7 +198,7 @@ void LootDialog::createUI() new ExpanderWidget(ui->details, ui->detailsPanel); - connect(ui->buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); + ui->buttons->setStandardButtons(QDialogButtonBox::Cancel); resize(650, 450); } @@ -177,24 +206,15 @@ void LootDialog::createUI() void LootDialog::closeEvent(QCloseEvent* e) { if (m_finished) { + log::debug("loot dialog close event: finished, closing"); QDialog::closeEvent(e); } else { + log::debug("loot dialog close event: not finished, cancelling"); cancel(); e->ignore(); } } -void LootDialog::onButton(QAbstractButton* b) -{ - if (ui->buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { - if (m_finished) { - close(); - } else { - cancel(); - } - } -} - void LootDialog::addLineOutput(const QString& line) { ui->output->appendPlainText(line); @@ -202,12 +222,16 @@ void LootDialog::addLineOutput(const QString& line) void LootDialog::onFinished() { + log::debug("loot dialog: loot is finished"); + m_finished = true; if (m_cancelling) { + log::debug("loot dialog: was cancelling, closing"); close(); } else { - handleReport(); + log::debug("loot dialog: showing report"); + showReport(); ui->openJsonReport->setEnabled(true); ui->buttons->setStandardButtons(QDialogButtonBox::Close); } @@ -224,7 +248,7 @@ void LootDialog::log(log::Levels lv, const QString& s) } } -void LootDialog::handleReport() +void LootDialog::showReport() { const auto& lootReport = m_loot.report(); diff --git a/src/lootdialog.h b/src/lootdialog.h index df9e546d..fcdeb304 100644 --- a/src/lootdialog.h +++ b/src/lootdialog.h @@ -48,16 +48,14 @@ public: void setText(const QString& s); void setProgress(lootcli::Progress p); - QString progressToString(lootcli::Progress p); - void addOutput(const QString& s); - bool result() const; - void cancel(); - void openReport(); + void accept() override; + void reject() override; + private: std::unique_ptr ui; OrganizerCore& m_core; @@ -68,11 +66,10 @@ private: void createUI(); void closeEvent(QCloseEvent* e) override; - void onButton(QAbstractButton* b); void addLineOutput(const QString& line); void onFinished(); void log(MOBase::log::Levels lv, const QString& s); - void handleReport(); + void showReport(); }; #endif // MODORGANIZER_LOOTDIALOG_H -- cgit v1.3.1 From 0c69619dbe4fae24794b2539a331ca7ac66f1f93 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 25 Nov 2019 02:10:59 -0500 Subject: switched to named pipes --- src/loot.cpp | 178 ++++++++++++++++++++++++++++++++--------------------- src/loot.h | 6 +- src/lootdialog.cpp | 1 + 3 files changed, 115 insertions(+), 70 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 9c8cf8c4..d9399ab6 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -9,6 +9,8 @@ using namespace MOBase; using namespace json; +static QString LootReportPath = QDir::temp().absoluteFilePath("lootreport.json"); + log::Levels levelFromLoot(lootcli::LogLevels level) { using LC = lootcli::LogLevels; @@ -198,14 +200,14 @@ Loot::~Loot() m_thread->wait(); } - if (!m_outPath.isEmpty() && QFile::exists(m_outPath)) { - log::debug("deleting temporary loot report '{}'", m_outPath); - const auto r = shell::Delete(m_outPath); + if (QFile::exists(LootReportPath)) { + log::debug("deleting temporary loot report '{}'", LootReportPath); + const auto r = shell::Delete(LootReportPath); if (!r) { log::error( "failed to remove temporary loot json report '{}': {}", - m_outPath, r.toString()); + LootReportPath, r.toString()); } } } @@ -214,8 +216,32 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { log::debug("starting loot"); - m_outPath = QDir::temp().absoluteFilePath("lootreport.json"); + // creating pipe + env::HandlePtr out(createPipe()); + if (out.get() == INVALID_HANDLE_VALUE) { + return false; + } + + // vfs + core.prepareVFS(); + // spawning + if (!spawnLootcli(parent, core, didUpdateMasterList, out.get())) { + return false; + } + + // starting thread + log::debug("starting loot thread"); + m_thread.reset(QThread::create([&]{ lootThread(); })); + m_thread->start(); + + return true; +} + +bool Loot::spawnLootcli( + QWidget* parent, OrganizerCore& core, bool didUpdateMasterList, + HANDLE stdoutHandle) +{ const auto logLevel = core.settings().diagnostics().lootLogLevel(); QStringList parameters; @@ -224,45 +250,18 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) << "--logLevel" << QString::fromStdString(lootcli::logLevelToString(logLevel)) - << "--out" << QString("\"%1\"").arg(m_outPath); + << "--out" << QString("\"%1\"").arg(LootReportPath); if (didUpdateMasterList) { parameters << "--skipUpdateMasterlist"; } - SECURITY_ATTRIBUTES secAttributes; - secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); - secAttributes.bInheritHandle = TRUE; - secAttributes.lpSecurityDescriptor = nullptr; - - env::HandlePtr readPipe, writePipe; - - { - HANDLE read = INVALID_HANDLE_VALUE; - HANDLE write = INVALID_HANDLE_VALUE; - - if (!::CreatePipe(&read, &write, &secAttributes, 0)) { - log::error("failed to create stdout reroute"); - } - - readPipe.reset(read); - writePipe.reset(write); - - if (!::SetHandleInformation(read, HANDLE_FLAG_INHERIT, 0)) { - log::error("failed to correctly set up the stdout reroute"); - } - } - - core.prepareVFS(); - spawn::SpawnParameters sp; sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); sp.arguments = parameters.join(" "); sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); sp.hooked = true; - sp.stdOut = writePipe.get(); - - m_stdout = std::move(readPipe); + sp.stdOut = stdoutHandle; HANDLE lootHandle = spawn::startBinary(parent, sp); if (lootHandle == INVALID_HANDLE_VALUE) { @@ -272,27 +271,64 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) m_lootProcess.reset(lootHandle); - core.pluginList()->clearAdditionalInformation(); + return true; +} + +HANDLE Loot::createPipe() +{ + static const wchar_t* PipeName = L"\\\\.\\pipe\\lootcli_pipe"; - log::debug("starting loot thread"); + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; - m_thread.reset(QThread::create([&]{ - try - { - lootThread(); + env::HandlePtr pipe; + + // creating pipe + { + HANDLE pipeHandle = ::CreateNamedPipe( + PipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, + 1, 50'000, 50'000, 0, &sa); + + if (pipeHandle == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + log::error("CreateNamedPipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; } - catch(...) - { - log::error("unhandled exception in loot thread"); + + pipe.reset(pipeHandle); + } + + { + // duplicating the handle to read from it + HANDLE outputRead = INVALID_HANDLE_VALUE; + + const auto r = DuplicateHandle( + GetCurrentProcess(), pipe.get(), GetCurrentProcess(), &outputRead, + 0, TRUE, DUPLICATE_SAME_ACCESS); + + if (!r) { + const auto e = GetLastError(); + log::error("DuplicateHandle for pipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; } - log::debug("finishing loot thread"); - emit finished(); - })); + m_stdout.reset(outputRead); + } - m_thread->start(); - return true; + // creating handle to pipe which is passed to CreateProcess() + HANDLE outputWrite = ::CreateFileW( + PipeName, FILE_WRITE_DATA|SYNCHRONIZE, 0, + &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + + if (outputWrite == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + log::error("CreateFileW for pipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; + } + + return outputWrite; } void Loot::cancel() @@ -310,7 +346,7 @@ bool Loot::result() const const QString& Loot::outPath() const { - return m_outPath; + return LootReportPath; } const Loot::Report& Loot::report() const @@ -322,7 +358,8 @@ void Loot::lootThread() { ::SetThreadDescription(GetCurrentThread(), L"loot"); - try { + try + { m_result = false; if (!waitForCompletion()) { @@ -331,9 +368,14 @@ void Loot::lootThread() m_result = true; processOutputFile(); - } catch (const std::exception &e) { - emit log(log::Levels::Error, tr("failed to run loot: %1").arg(e.what())); } + catch(...) + { + log::error("unhandled exception in loot thread"); + } + + log::debug("finishing loot thread"); + emit finished(); } bool Loot::waitForCompletion() @@ -396,25 +438,23 @@ bool Loot::waitForCompletion() std::string Loot::readFromPipe() { - static const int chunkSize = 128; - std::string result; + static const std::size_t bufferSize = 50'000; - char buffer[chunkSize + 1]; - buffer[chunkSize] = '\0'; + char buffer[bufferSize] = {}; - DWORD read = 1; - while (read > 0) { - if (!::ReadFile(m_stdout.get(), buffer, chunkSize, &read, nullptr)) { - break; - } - if (read > 0) { - result.append(buffer, read); - if (read < chunkSize) { - break; - } + DWORD bytesRead = 0; + if (::ReadFile(m_stdout.get(), buffer, bufferSize, &bytesRead, nullptr)) { + return {buffer, buffer + bytesRead}; + } else { + const auto e = GetLastError(); + + // broken pipe probably means lootcli is finished + if (e != ERROR_BROKEN_PIPE) { + log::error("{}", formatSystemMessage(e)); } + + return {}; } - return result; } void Loot::processStdout(const std::string &lootOut) @@ -472,9 +512,9 @@ void Loot::processMessage(const lootcli::Message& m) void Loot::processOutputFile() { - log::debug("parsing json output file at '{}'", m_outPath); + log::debug("parsing json output file at '{}'", LootReportPath); - QFile outFile(m_outPath); + QFile outFile(LootReportPath); if (!outFile.open(QIODevice::ReadOnly)) { emit log( MOBase::log::Error, diff --git a/src/loot.h b/src/loot.h index 30ef4b60..a67b9ed8 100644 --- a/src/loot.h +++ b/src/loot.h @@ -97,12 +97,16 @@ private: std::unique_ptr m_thread; std::atomic m_cancel; std::atomic m_result; - QString m_outPath; env::HandlePtr m_lootProcess; env::HandlePtr m_stdout; std::string m_outputBuffer; Report m_report; + HANDLE createPipe(); + bool spawnLootcli( + QWidget* parent, OrganizerCore& core, bool didUpdateMasterList, + HANDLE stdoutHandle); + std::string readFromPipe(); void lootThread(); diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index 43929c00..a8f73cdc 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -252,6 +252,7 @@ void LootDialog::showReport() { const auto& lootReport = m_loot.report(); + m_core.pluginList()->clearAdditionalInformation(); for (auto&& p : lootReport.plugins) { m_core.pluginList()->addLootReport(p.name, p); } -- cgit v1.3.1 From 23e917df454ba1588fcbe08adfa47422a443b9da Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 25 Nov 2019 03:08:27 -0500 Subject: switched to overlapped io so lootcli can still be terminated even if it's not writing anything to stdout --- src/loot.cpp | 295 ++++++++++++++++++++++++++++++++++++++++------------------- src/loot.h | 8 +- 2 files changed, 205 insertions(+), 98 deletions(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index d9399ab6..d2059d7c 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -10,6 +10,194 @@ using namespace MOBase; using namespace json; static QString LootReportPath = QDir::temp().absoluteFilePath("lootreport.json"); +static const DWORD PipeTimeout = 500; + + +class AsyncPipe +{ +public: + AsyncPipe() + : m_ioPending(false) + { + std::fill(std::begin(m_buffer), std::end(m_buffer), 0); + std::memset(&m_ov, 0, sizeof(m_ov)); + } + + env::HandlePtr create() + { + // creating pipe + env::HandlePtr out(createPipe()); + if (out.get() == INVALID_HANDLE_VALUE) { + return {}; + } + + HANDLE readEventHandle = ::CreateEvent(nullptr, TRUE, FALSE, nullptr); + + if (readEventHandle == NULL) { + const auto e = GetLastError(); + log::error("CreateEvent failed for loot, {}", formatSystemMessage(e)); + return {}; + } + + m_ov.hEvent = readEventHandle; + m_readEvent.reset(readEventHandle); + + return out; + } + + std::string read() + { + if (m_ioPending) { + return checkPending(); + } else { + return tryRead(); + } + } + +private: + static const std::size_t bufferSize = 50'000; + + env::HandlePtr m_stdout; + env::HandlePtr m_readEvent; + char m_buffer[bufferSize]; + OVERLAPPED m_ov; + bool m_ioPending; + + HANDLE createPipe() + { + static const wchar_t* PipeName = L"\\\\.\\pipe\\lootcli_pipe"; + + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; + + env::HandlePtr pipe; + + // creating pipe + { + HANDLE pipeHandle = ::CreateNamedPipe( + PipeName, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, + 1, 50'000, 50'000, PipeTimeout, &sa); + + if (pipeHandle == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + log::error("CreateNamedPipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; + } + + pipe.reset(pipeHandle); + } + + { + // duplicating the handle to read from it + HANDLE outputRead = INVALID_HANDLE_VALUE; + + const auto r = DuplicateHandle( + GetCurrentProcess(), pipe.get(), GetCurrentProcess(), &outputRead, + 0, TRUE, DUPLICATE_SAME_ACCESS); + + if (!r) { + const auto e = GetLastError(); + log::error("DuplicateHandle for pipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; + } + + m_stdout.reset(outputRead); + } + + + // creating handle to pipe which is passed to CreateProcess() + HANDLE outputWrite = ::CreateFileW( + PipeName, FILE_WRITE_DATA|SYNCHRONIZE, 0, + &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + + if (outputWrite == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + log::error("CreateFileW for pipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; + } + + return outputWrite; + } + + std::string tryRead() + { + DWORD bytesRead = 0; + + if (!::ReadFile(m_stdout.get(), m_buffer, bufferSize, &bytesRead, &m_ov)) { + const auto e = GetLastError(); + + switch (e) + { + case ERROR_IO_PENDING: + { + m_ioPending = true; + break; + } + + case ERROR_BROKEN_PIPE: + { + // broken pipe probably means lootcli is finished + break; + } + + default: + { + log::error("{}", formatSystemMessage(e)); + break; + } + } + + return {}; + } + + return {m_buffer, m_buffer + bytesRead}; + } + + std::string checkPending() + { + DWORD bytesRead = 0; + + if (!::GetOverlappedResultEx(m_stdout.get(), &m_ov, &bytesRead, PipeTimeout, FALSE)) { + const auto e = GetLastError(); + + switch (e) + { + case ERROR_IO_INCOMPLETE: + { + break; + } + + case WAIT_TIMEOUT: + { + break; + } + + case ERROR_BROKEN_PIPE: + { + // broken pipe probably means lootcli is finished + break; + } + + default: + { + log::error("GetOverlappedResult failed, {}", formatSystemMessage(e)); + break; + } + } + + return {}; + } + + ::ResetEvent(m_readEvent.get()); + m_ioPending = false; + + return {m_buffer, m_buffer + bytesRead}; + } +}; + + log::Levels levelFromLoot(lootcli::LogLevels level) { @@ -216,9 +404,10 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { log::debug("starting loot"); - // creating pipe - env::HandlePtr out(createPipe()); - if (out.get() == INVALID_HANDLE_VALUE) { + m_pipe.reset(new AsyncPipe); + + env::HandlePtr stdoutHandle = m_pipe->create(); + if (!stdoutHandle) { return false; } @@ -226,7 +415,7 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) core.prepareVFS(); // spawning - if (!spawnLootcli(parent, core, didUpdateMasterList, out.get())) { + if (!spawnLootcli(parent, core, didUpdateMasterList, std::move(stdoutHandle))) { return false; } @@ -240,7 +429,7 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) bool Loot::spawnLootcli( QWidget* parent, OrganizerCore& core, bool didUpdateMasterList, - HANDLE stdoutHandle) + env::HandlePtr stdoutHandle) { const auto logLevel = core.settings().diagnostics().lootLogLevel(); @@ -261,9 +450,10 @@ bool Loot::spawnLootcli( sp.arguments = parameters.join(" "); sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); sp.hooked = true; - sp.stdOut = stdoutHandle; + sp.stdOut = stdoutHandle.get(); HANDLE lootHandle = spawn::startBinary(parent, sp); + if (lootHandle == INVALID_HANDLE_VALUE) { emit log(log::Levels::Error, tr("failed to start loot")); return false; @@ -274,63 +464,6 @@ bool Loot::spawnLootcli( return true; } -HANDLE Loot::createPipe() -{ - static const wchar_t* PipeName = L"\\\\.\\pipe\\lootcli_pipe"; - - SECURITY_ATTRIBUTES sa = {}; - sa.nLength = sizeof(SECURITY_ATTRIBUTES); - sa.bInheritHandle = TRUE; - - env::HandlePtr pipe; - - // creating pipe - { - HANDLE pipeHandle = ::CreateNamedPipe( - PipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, - 1, 50'000, 50'000, 0, &sa); - - if (pipeHandle == INVALID_HANDLE_VALUE) { - const auto e = GetLastError(); - log::error("CreateNamedPipe failed, {}", formatSystemMessage(e)); - return INVALID_HANDLE_VALUE; - } - - pipe.reset(pipeHandle); - } - - { - // duplicating the handle to read from it - HANDLE outputRead = INVALID_HANDLE_VALUE; - - const auto r = DuplicateHandle( - GetCurrentProcess(), pipe.get(), GetCurrentProcess(), &outputRead, - 0, TRUE, DUPLICATE_SAME_ACCESS); - - if (!r) { - const auto e = GetLastError(); - log::error("DuplicateHandle for pipe failed, {}", formatSystemMessage(e)); - return INVALID_HANDLE_VALUE; - } - - m_stdout.reset(outputRead); - } - - - // creating handle to pipe which is passed to CreateProcess() - HANDLE outputWrite = ::CreateFileW( - PipeName, FILE_WRITE_DATA|SYNCHRONIZE, 0, - &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); - - if (outputWrite == INVALID_HANDLE_VALUE) { - const auto e = GetLastError(); - log::error("CreateFileW for pipe failed, {}", formatSystemMessage(e)); - return INVALID_HANDLE_VALUE; - } - - return outputWrite; -} - void Loot::cancel() { if (!m_cancel) { @@ -362,12 +495,10 @@ void Loot::lootThread() { m_result = false; - if (!waitForCompletion()) { - return; + if (waitForCompletion()) { + m_result = true; + processOutputFile(); } - - m_result = true; - processOutputFile(); } catch(...) { @@ -378,6 +509,7 @@ void Loot::lootThread() emit finished(); } + bool Loot::waitForCompletion() { bool terminating = false; @@ -410,14 +542,14 @@ bool Loot::waitForCompletion() return false; } - processStdout(readFromPipe()); + processStdout(m_pipe->read()); } if (m_cancel) { return false; } - processStdout(readFromPipe()); + processStdout(m_pipe->read()); // checking exit code DWORD exitCode = 0; @@ -436,27 +568,6 @@ bool Loot::waitForCompletion() return true; } -std::string Loot::readFromPipe() -{ - static const std::size_t bufferSize = 50'000; - - char buffer[bufferSize] = {}; - - DWORD bytesRead = 0; - if (::ReadFile(m_stdout.get(), buffer, bufferSize, &bytesRead, nullptr)) { - return {buffer, buffer + bytesRead}; - } else { - const auto e = GetLastError(); - - // broken pipe probably means lootcli is finished - if (e != ERROR_BROKEN_PIPE) { - log::error("{}", formatSystemMessage(e)); - } - - return {}; - } -} - void Loot::processStdout(const std::string &lootOut) { emit output(QString::fromStdString(lootOut)); @@ -466,8 +577,6 @@ void Loot::processStdout(const std::string &lootOut) return; } - log::debug("loot: processing stdout ({} bytes)", m_outputBuffer.size()); - std::size_t start = 0; for (;;) { diff --git a/src/loot.h b/src/loot.h index a67b9ed8..7e5e01bd 100644 --- a/src/loot.h +++ b/src/loot.h @@ -11,6 +11,7 @@ Q_DECLARE_METATYPE(lootcli::Progress); Q_DECLARE_METATYPE(MOBase::log::Levels); class OrganizerCore; +class AsyncPipe; class Loot : public QObject { @@ -98,16 +99,13 @@ private: std::atomic m_cancel; std::atomic m_result; env::HandlePtr m_lootProcess; - env::HandlePtr m_stdout; + std::unique_ptr m_pipe; std::string m_outputBuffer; Report m_report; - HANDLE createPipe(); bool spawnLootcli( QWidget* parent, OrganizerCore& core, bool didUpdateMasterList, - HANDLE stdoutHandle); - - std::string readFromPipe(); + env::HandlePtr stdoutHandle); void lootThread(); bool waitForCompletion(); -- cgit v1.3.1 From bd191f3a71fbdcb706de9db5c29d615bfa13c174 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 25 Nov 2019 03:28:27 -0500 Subject: now passes --language to lootcli --- src/loot.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/loot.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index d2059d7c..11eb4f4e 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -439,7 +439,8 @@ bool Loot::spawnLootcli( << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) << "--logLevel" << QString::fromStdString(lootcli::logLevelToString(logLevel)) - << "--out" << QString("\"%1\"").arg(LootReportPath); + << "--out" << QString("\"%1\"").arg(LootReportPath) + << "--language" << core.settings().interface().language(); if (didUpdateMasterList) { parameters << "--skipUpdateMasterlist"; -- cgit v1.3.1