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.h | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/loot.h (limited to 'src/loot.h') diff --git a/src/loot.h b/src/loot.h new file mode 100644 index 00000000..2314e5b3 --- /dev/null +++ b/src/loot.h @@ -0,0 +1,10 @@ +#ifndef MODORGANIZER_LOOT_H +#define MODORGANIZER_LOOT_H + +#include + +class OrganizerCore; + +bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList); + +#endif // MODORGANIZER_LOOT_H -- 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.h') 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.h') 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.h') 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.h') 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.h') 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 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.h') 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.h') 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.h') 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.h') 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.h') 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 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.h') 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 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.h') 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.h') 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