From cd876a0f9ffd03c711812c2ade92836e5d6c0203 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 20:11:50 -0500 Subject: split loot dialog, added ui file --- src/lootdialog.cpp | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/lootdialog.cpp (limited to 'src/lootdialog.cpp') diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp new file mode 100644 index 00000000..db41959d --- /dev/null +++ b/src/lootdialog.cpp @@ -0,0 +1,222 @@ +#include "lootdialog.h" +#include "ui_lootdialog.h" +#include "loot.h" +#include "organizercore.h" +#include +#include + +using namespace MOBase; + +LootDialog::LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : + QDialog(parent), ui(new Ui::LootDialog), m_core(core), m_loot(loot), + m_finished(false), m_cancelling(false) +{ + createUI(); + + QObject::connect( + &m_loot, &Loot::output, this, + [&](auto&& s){ addOutput(s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::progress, + this, [&](auto&& p){ setProgress(p); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::log, this, + [&](auto&& lv, auto&& s){ log(lv, s); }, Qt::QueuedConnection); + + QObject::connect( + &m_loot, &Loot::finished, this, + [&]{ onFinished(); }, Qt::QueuedConnection); +} + +LootDialog::~LootDialog() = default; + +void LootDialog::setText(const QString& s) +{ + ui->progressText->setText(s); +} + +void LootDialog::setProgress(lootcli::Progress p) +{ + setText(progressToString(p)); + + if (p == lootcli::Progress::Done) { + ui->progressBar->setRange(0, 1); + ui->progressBar->setValue(1); + } +} + +QString LootDialog::progressToString(lootcli::Progress p) +{ + using P = lootcli::Progress; + + switch (p) + { + case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); + case P::UpdatingMasterlist: return tr("Updating masterlist"); + case P::LoadingLists: return tr("Loading lists"); + case P::ReadingPlugins: return tr("Reading plugins"); + case P::SortingPlugins: return tr("Sorting plugins"); + case P::WritingLoadorder: return tr("Writing loadorder.txt"); + case P::ParsingLootMessages: return tr("Parsing loot messages"); + case P::Done: return tr("Done"); + default: return QString("unknown progress %1").arg(static_cast(p)); + } +} + +void LootDialog::addOutput(const QString& s) +{ + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + return; + } + + const auto lines = s.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); + + for (auto&& line : lines) { + if (line.isEmpty()) { + continue; + } + + addLineOutput(line); + } +} + +bool LootDialog::result() const +{ + return m_loot.result(); +} + +void LootDialog::cancel() +{ + if (!m_finished && !m_cancelling) { + addLineOutput(tr("Stopping LOOT...")); + m_loot.cancel(); + ui->buttons->setEnabled(false); + m_cancelling = true; + } +} + +void LootDialog::openReport() +{ + const auto path = m_loot.outPath(); + shell::Open(path); +} + +void LootDialog::createUI() +{ + ui->setupUi(this); + ui->progressBar->setMaximum(0); + + ui->openJsonReport->setEnabled(false); + connect(ui->openJsonReport, &QPushButton::clicked, [&]{ openReport(); }); + + new ExpanderWidget(ui->details, ui->detailsPanel); + + connect(ui->buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); + + resize(480, 275); +} + +void LootDialog::closeEvent(QCloseEvent* e) +{ + if (m_finished) { + QDialog::closeEvent(e); + } else { + cancel(); + e->ignore(); + } +} + +void LootDialog::onButton(QAbstractButton* b) +{ + if (ui->buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { + if (m_finished) { + close(); + } else { + cancel(); + } + } +} + +void LootDialog::addLineOutput(const QString& line) +{ + ui->output->appendPlainText(line); +} + +void LootDialog::onFinished() +{ + m_finished = true; + + if (m_cancelling) { + close(); + } else { + handleReport(); + ui->openJsonReport->setEnabled(true); + ui->buttons->setStandardButtons(QDialogButtonBox::Close); + } +} + +void LootDialog::log(log::Levels lv, const QString& s) +{ + if (lv >= log::Levels::Warning) { + log::log(lv, "{}", s); + } + + if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { + addLineOutput(QString("[%1] %2").arg(log::levelToString(lv)).arg(s)); + } +} + +void LootDialog::handleReport() +{ + const auto& report = m_loot.report(); + + if (!report.messages.empty()) { + addLineOutput(""); + } + + QString html; + + if (!report.messages.empty()) { + html += "
    "; + + for (auto&& m : report.messages) { + log(m.type, m.text); + + html += "
  • "; + + switch (m.type) + { + case log::Error: + { + html += "" + QObject::tr("Error") + ": "; + break; + } + + case log::Warning: + { + html += "" + QObject::tr("Warning") + ": "; + break; + } + + default: + { + break; + } + } + + html += m.text + "
  • "; + } + + html + "
"; + } else { + html += QObject::tr("No messages."); + } + + ui->report->setHtml(html); + + for (auto&& p : report.plugins) { + m_core.pluginList()->addLootReport(p.name, p); + } +} -- cgit v1.3.1 From 70ee786102c8436c7f8f9e8a5d2ea71035d8d572 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 22:19:23 -0500 Subject: changed loot report to webengine with markdown support --- src/loot.cpp | 4 +- src/lootdialog.cpp | 74 +++++++++-- src/lootdialog.h | 31 +++++ src/lootdialog.ui | 49 ++++++-- src/resources/markdown.html | 299 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 434 insertions(+), 23 deletions(-) create mode 100644 src/resources/markdown.html (limited to 'src/lootdialog.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index f0618b0b..5d75eeaa 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -67,7 +67,9 @@ Loot::Loot() Loot::~Loot() { - m_thread->wait(); + if (m_thread) { + m_thread->wait(); + } if (!m_outPath.isEmpty() && QFile::exists(m_outPath)) { const auto r = shell::Delete(m_outPath); diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index db41959d..9c7b482e 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -4,9 +4,44 @@ #include "organizercore.h" #include #include +#include using namespace MOBase; + +MarkdownDocument::MarkdownDocument(QObject* parent) + : QObject(parent) +{ +} + +void MarkdownDocument::setText(const QString& text) +{ + if (m_text == text) + return; + + m_text = text; + emit textChanged(m_text); +} + + +MarkdownPage::MarkdownPage(QObject* parent) + : QWebEnginePage(parent) +{ +} + +bool MarkdownPage::acceptNavigationRequest(const QUrl &url, NavigationType, bool) +{ + static const QStringList allowed = {"qrc", "data"}; + + if (!allowed.contains(url.scheme())) { + QDesktopServices::openUrl(url); + return false; + } + + return true; +} + + LootDialog::LootDialog(QWidget* parent, OrganizerCore& core, Loot& loot) : QDialog(parent), ui(new Ui::LootDialog), m_core(core), m_loot(loot), m_finished(false), m_cancelling(false) @@ -108,6 +143,27 @@ void LootDialog::createUI() ui->setupUi(this); ui->progressBar->setMaximum(0); + auto* page = new MarkdownPage(this); + ui->report->setPage(page); + + auto* channel = new QWebChannel(this); + channel->registerObject("content", &m_report); + page->setWebChannel(channel); + + const QString path = QApplication::applicationDirPath() + "/resources/markdown.html"; + QFile f(path); + + if (f.open(QFile::ReadOnly)) { + const QString html = f.readAll(); + if (!html.isEmpty()) { + ui->report->setHtml(html); + } else { + log::error("failed to read '{}', {}", path, f.errorString()); + } + } else { + log::error("can't open '{}', {}", path, f.errorString()); + } + ui->openJsonReport->setEnabled(false); connect(ui->openJsonReport, &QPushButton::clicked, [&]{ openReport(); }); @@ -176,27 +232,25 @@ void LootDialog::handleReport() addLineOutput(""); } - QString html; + QString md; if (!report.messages.empty()) { - html += "
    "; - for (auto&& m : report.messages) { log(m.type, m.text); - html += "
  • "; + md += " - "; switch (m.type) { case log::Error: { - html += "" + QObject::tr("Error") + ": "; + md += "**" + QObject::tr("Error") + "**: "; break; } case log::Warning: { - html += "" + QObject::tr("Warning") + ": "; + md += "**" + QObject::tr("Warning") + "**: "; break; } @@ -206,15 +260,13 @@ void LootDialog::handleReport() } } - html += m.text + "
  • "; + md += m.text + "\n"; } - - html + "
"; } else { - html += QObject::tr("No messages."); + md += QObject::tr("**No messages.**"); } - ui->report->setHtml(html); + m_report.setText(md); for (auto&& p : report.plugins) { m_core.pluginList()->addLootReport(p.name, p); diff --git a/src/lootdialog.h b/src/lootdialog.h index e4647b5c..df9e546d 100644 --- a/src/lootdialog.h +++ b/src/lootdialog.h @@ -9,6 +9,36 @@ namespace Ui { class LootDialog; } class OrganizerCore; class Loot; + +class MarkdownDocument : public QObject +{ + Q_OBJECT; + Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged FINAL); + +public: + explicit MarkdownDocument(QObject* parent=nullptr); + void setText(const QString& text); + +signals: + void textChanged(const QString &text); + +private: + QString m_text; +}; + + +class MarkdownPage : public QWebEnginePage +{ + Q_OBJECT; + +public: + explicit MarkdownPage(QObject* parent=nullptr); + +protected: + bool acceptNavigationRequest(const QUrl &url, NavigationType, bool) override; +}; + + class LootDialog : public QDialog { public: @@ -34,6 +64,7 @@ private: Loot& m_loot; bool m_finished; bool m_cancelling; + MarkdownDocument m_report; void createUI(); void closeEvent(QCloseEvent* e) override; diff --git a/src/lootdialog.ui b/src/lootdialog.ui index 7e10b1db..e366ce41 100644 --- a/src/lootdialog.ui +++ b/src/lootdialog.ui @@ -7,7 +7,7 @@ 0 0 457 - 343 + 600 @@ -16,7 +16,7 @@ - + 0 @@ -31,7 +31,7 @@ - + 0 @@ -62,16 +62,36 @@ - - - true + + + QFrame::StyledPanel - - Qt::TextBrowserInteraction - - - LOOT Report + + QFrame::Sunken + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + about:blank + + + + + @@ -176,6 +196,13 @@ + + + QWebEngineView + QWidget +
QtWebEngineWidgets/QWebEngineView
+
+
diff --git a/src/resources/markdown.html b/src/resources/markdown.html new file mode 100644 index 00000000..a09b2209 --- /dev/null +++ b/src/resources/markdown.html @@ -0,0 +1,299 @@ + + + + + + + + + +
+ + + \ No newline at end of file -- cgit v1.3.1 From 64304a6368cf8357bb04d1c0057aec637ebb479a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 23:23:15 -0500 Subject: tweaked css, finished markdown report copy markdown.html to resources/ --- src/CMakeLists.txt | 4 +- src/loot.cpp | 139 ++++++++++++ src/loot.h | 13 +- src/lootdialog.cpp | 44 +--- src/resources/markdown.html | 528 ++++++++++++++++++++++---------------------- 5 files changed, 424 insertions(+), 304 deletions(-) (limited to 'src/lootdialog.cpp') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index db3bda73..5a510806 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -676,4 +676,6 @@ INSTALL( ) # qdds.dll needs installing manually as Qt no longer ships with it by default. -INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) \ No newline at end of file +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../qdds.dll DESTINATION bin/dlls/imageformats) + +INSTALL(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/markdown.html DESTINATION bin/resources) diff --git a/src/loot.cpp b/src/loot.cpp index 5d75eeaa..2faf9c2e 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -34,6 +34,99 @@ log::Levels levelFromLoot(lootcli::LogLevels level) } +QString Loot::Report::toMarkdown() const +{ + QString s; + + if (!messages.empty()) { + s += "### " + QObject::tr("General messages") + "\n"; + + for (auto&& m : messages) { + s += " - " + m.toMarkdown() + "\n"; + } + } + + if (!plugins.empty()) { + if (!s.isEmpty()) { + s += "\n"; + } + + s += "### " + QObject::tr("Plugins") + "\n"; + + for (auto&& p : plugins) { + const auto ps = p.toMarkdown(); + if (!ps.isEmpty()) { + s += ps + "\n"; + } + } + } + + if (s.isEmpty()) { + s += "**" + QObject::tr("No messages.") + "**"; + } + + s += stats.toMarkdown(); + + return s; +} + +QString Loot::Stats::toMarkdown() const +{ + return QString("`stats: %1s, lootcli %2, loot %3`") + .arg(QString::number(time / 1000.0, 'f', 2)) + .arg(lootcliVersion) + .arg(lootVersion); +} + +QString Loot::Plugin::toMarkdown() const +{ + QString s; + + if (!incompatibilities.empty()) { + s += " - **" + QObject::tr("Incompatibilities") + ": "; + + QString fs; + for (auto&& f : incompatibilities) { + if (!fs.isEmpty()) { + fs += ", "; + } + + fs += f.displayName.isEmpty() ? f.name : f.displayName; + } + + s += fs + "**\n"; + } + + if (!missingMasters.empty()) { + s += " - **" + QObject::tr("Missing masters") + ": "; + + QString ms; + for (auto&& m : missingMasters) { + if (!ms.isEmpty()) { + ms += ", "; + } + + ms += m; + } + + s += ms + "**\n"; + } + + for (auto&& m : messages) { + s += " - " + m.toMarkdown() + "\n"; + } + + for (auto&& d : dirty) { + s += " - " + d.toMarkdown(false) + "\n"; + } + + if (!s.isEmpty()) { + s = "#### " + name + "\n" + s; + } + + return s; +} + QString Loot::Dirty::toString(bool isClean) const { if (isClean) { @@ -50,6 +143,11 @@ QString Loot::Dirty::toString(bool isClean) const return s; } +QString Loot::Dirty::toMarkdown(bool isClean) const +{ + return toString(isClean); +} + QString Loot::Dirty::cleaningString() const { return QObject::tr("%1 found %2 ITM record(s), %3 deleted reference(s) and %4 deleted navmesh(es).") @@ -59,6 +157,35 @@ QString Loot::Dirty::cleaningString() const .arg(deletedNavmesh); } +QString Loot::Message::toMarkdown() const +{ + QString s; + + switch (type) + { + case log::Error: + { + s += "**" + QObject::tr("Error") + "**: "; + break; + } + + case log::Warning: + { + s += "**" + QObject::tr("Warning") + "**: "; + break; + } + + default: + { + break; + } + } + + s += text; + + return s; +} + Loot::Loot() : m_thread(nullptr), m_cancel(false), m_result(false) @@ -348,6 +475,7 @@ Loot::Report Loot::createReport(const QJsonDocument& doc) const r.messages = reportMessages(getOpt(object, "messages")); r.plugins = reportPlugins(getOpt(object, "plugins")); + r.stats = reportStats(getWarn(object, "stats")); return r; } @@ -407,6 +535,17 @@ Loot::Plugin Loot::reportPlugin(const QJsonObject& plugin) const return p; } +Loot::Stats Loot::reportStats(const QJsonObject& stats) const +{ + Stats s; + + s.time = getWarn(stats, "time"); + s.lootcliVersion = getWarn(stats, "lootcliVersion"); + s.lootVersion = getWarn(stats, "lootVersion"); + + return s; +} + std::vector Loot::reportMessages(const QJsonArray& array) const { std::vector v; diff --git a/src/loot.h b/src/loot.h index 3a7c6aa9..30ef4b60 100644 --- a/src/loot.h +++ b/src/loot.h @@ -21,6 +21,8 @@ public: { MOBase::log::Levels type; QString text; + + QString toMarkdown() const; }; struct File @@ -39,6 +41,7 @@ public: QString info; QString toString(bool isClean) const; + QString toMarkdown(bool isClean) const; QString cleaningString() const; }; @@ -52,12 +55,17 @@ public: bool loadsArchive = false; bool isMaster = false; bool isLightMaster = false; + + QString toMarkdown() const; }; struct Stats { qint64 time = 0; - QString version; + QString lootcliVersion; + QString lootVersion; + + QString toMarkdown() const; }; struct Report @@ -65,6 +73,8 @@ public: std::vector messages; std::vector plugins; Stats stats; + + QString toMarkdown() const; }; @@ -107,6 +117,7 @@ private: Message reportMessage(const QJsonObject& message) const; std::vector reportPlugins(const QJsonArray& plugins) const; Loot::Plugin reportPlugin(const QJsonObject& plugin) const; + Loot::Stats reportStats(const QJsonObject& stats) const; std::vector reportMessages(const QJsonArray& array) const; std::vector reportFiles(const QJsonArray& array) const; diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index 9c7b482e..c7cdfecd 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -226,49 +226,19 @@ void LootDialog::log(log::Levels lv, const QString& s) void LootDialog::handleReport() { - const auto& report = m_loot.report(); + const auto& lootReport = m_loot.report(); - if (!report.messages.empty()) { + if (!lootReport.messages.empty()) { addLineOutput(""); } - QString md; - - if (!report.messages.empty()) { - for (auto&& m : report.messages) { - log(m.type, m.text); - - md += " - "; - - switch (m.type) - { - case log::Error: - { - md += "**" + QObject::tr("Error") + "**: "; - break; - } - - case log::Warning: - { - md += "**" + QObject::tr("Warning") + "**: "; - break; - } - - default: - { - break; - } - } - - md += m.text + "\n"; - } - } else { - md += QObject::tr("**No messages.**"); + for (auto&& m : lootReport.messages) { + log(m.type, m.text); } - m_report.setText(md); - - for (auto&& p : report.plugins) { + for (auto&& p : lootReport.plugins) { m_core.pluginList()->addLootReport(p.name, p); } + + m_report.setText(lootReport.toMarkdown()); } diff --git a/src/resources/markdown.html b/src/resources/markdown.html index a09b2209..1ecaf1b9 100644 --- a/src/resources/markdown.html +++ b/src/resources/markdown.html @@ -2,278 +2,276 @@ - - - -- cgit v1.3.1 From 48fcf9521f796bc3b6e536545877a9ea2e37360d Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 24 Nov 2019 23:47:59 -0500 Subject: removed max-width, larger dialog removed messages in output now that there's a full report --- src/lootdialog.cpp | 10 +--------- src/resources/markdown.html | 5 ----- 2 files changed, 1 insertion(+), 14 deletions(-) (limited to 'src/lootdialog.cpp') diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index c7cdfecd..80664415 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -171,7 +171,7 @@ void LootDialog::createUI() connect(ui->buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); - resize(480, 275); + resize(650, 450); } void LootDialog::closeEvent(QCloseEvent* e) @@ -228,14 +228,6 @@ void LootDialog::handleReport() { const auto& lootReport = m_loot.report(); - if (!lootReport.messages.empty()) { - addLineOutput(""); - } - - for (auto&& m : lootReport.messages) { - log(m.type, m.text); - } - for (auto&& p : lootReport.plugins) { m_core.pluginList()->addLootReport(p.name, p); } diff --git a/src/resources/markdown.html b/src/resources/markdown.html index 1ecaf1b9..bd6d0f4d 100644 --- a/src/resources/markdown.html +++ b/src/resources/markdown.html @@ -18,7 +18,6 @@ font-family: Sans-serif; color: #F1F1F1; line-height: 1; - max-width: 960px; padding-left: 10px; padding-top: 0px; } @@ -72,11 +71,9 @@ p, ul, ol { font-size: 14px; line-height: 24px; - max-width: 540px; } pre { padding: 0px 24px; - max-width: 800px; white-space: pre-wrap; } code { @@ -93,7 +90,6 @@ border-left:.5em solid #eee; padding: 0 2em; margin-left:0; - max-width: 476px; } blockquote cite { font-size:14px; @@ -106,7 +102,6 @@ blockquote p { color: #666; - max-width: 460px; } hr { width: 540px; -- cgit v1.3.1 From 0e45044dbd724e9050bea00511585dc023afe144 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 25 Nov 2019 00:47:45 -0500 Subject: fixed cancel button debug logs, some cleanup --- src/loot.cpp | 47 +++++++++++++++++++++------- src/lootdialog.cpp | 92 ++++++++++++++++++++++++++++++++++-------------------- src/lootdialog.h | 11 +++---- 3 files changed, 98 insertions(+), 52 deletions(-) (limited to 'src/lootdialog.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 2faf9c2e..9c8cf8c4 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -199,6 +199,7 @@ Loot::~Loot() } if (!m_outPath.isEmpty() && QFile::exists(m_outPath)) { + log::debug("deleting temporary loot report '{}'", m_outPath); const auto r = shell::Delete(m_outPath); if (!r) { @@ -211,6 +212,8 @@ Loot::~Loot() bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { + log::debug("starting loot"); + m_outPath = QDir::temp().absoluteFilePath("lootreport.json"); const auto logLevel = core.settings().diagnostics().lootLogLevel(); @@ -271,8 +274,19 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) core.pluginList()->clearAdditionalInformation(); + log::debug("starting loot thread"); + m_thread.reset(QThread::create([&]{ - lootThread(); + try + { + lootThread(); + } + catch(...) + { + log::error("unhandled exception in loot thread"); + } + + log::debug("finishing loot thread"); emit finished(); })); @@ -283,7 +297,10 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) void Loot::cancel() { - m_cancel = true; + if (!m_cancel) { + log::debug("loot received cancel request"); + m_cancel = true; + } } bool Loot::result() const @@ -303,6 +320,8 @@ const Loot::Report& Loot::report() const void Loot::lootThread() { + ::SetThreadDescription(GetCurrentThread(), L"loot"); + try { m_result = false; @@ -321,10 +340,13 @@ bool Loot::waitForCompletion() { bool terminating = false; + log::debug("loot thread waiting for completion on lootcli"); + for (;;) { DWORD res = WaitForSingleObject(m_lootProcess.get(), 100); if (res == WAIT_OBJECT_0) { + log::debug("lootcli has completed"); // done break; } @@ -336,9 +358,13 @@ bool Loot::waitForCompletion() } if (m_cancel) { - // terminate and wait to finish + log::debug("terminating lootcli process"); ::TerminateProcess(m_lootProcess.get(), 1); + + log::debug("waiting for loocli process to terminate"); WaitForSingleObject(m_lootProcess.get(), INFINITE); + + log::debug("lootcli terminated"); return false; } @@ -396,6 +422,12 @@ void Loot::processStdout(const std::string &lootOut) emit output(QString::fromStdString(lootOut)); m_outputBuffer += lootOut; + if (m_outputBuffer.empty()) { + return; + } + + log::debug("loot: processing stdout ({} bytes)", m_outputBuffer.size()); + std::size_t start = 0; for (;;) { @@ -440,7 +472,7 @@ void Loot::processMessage(const lootcli::Message& m) void Loot::processOutputFile() { - log::info("parsing json output file at '{}'", m_outPath); + log::debug("parsing json output file at '{}'", m_outPath); QFile outFile(m_outPath); if (!outFile.open(QIODevice::ReadOnly)) { @@ -645,20 +677,13 @@ std::vector Loot::reportStringArray(const QJsonArray& array) const bool runLoot(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { - //m_OrganizerCore.currentProfile()->writeModlistNow(); core.savePluginList(); - //Create a backup of the load orders w/ LOOT in name - //to make sure that any sorting is easily undo-able. - //Need to figure out how I want to do that. - try { Loot loot; LootDialog dialog(parent, core, loot); loot.start(parent, core, didUpdateMasterList); - - dialog.setText(QObject::tr("Please wait while LOOT is running")); dialog.exec(); return dialog.result(); diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index 80664415..43929c00 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -8,6 +8,29 @@ using namespace MOBase; +QString progressToString(lootcli::Progress p) +{ + using P = lootcli::Progress; + + static const std::map map = { + {P::CheckingMasterlistExistence, QObject::tr("Checking masterlist existence")}, + {P::UpdatingMasterlist, QObject::tr("Updating masterlist")}, + {P::LoadingLists, QObject::tr("Loading lists")}, + {P::ReadingPlugins, QObject::tr("Reading plugins")}, + {P::SortingPlugins, QObject::tr("Sorting plugins")}, + {P::WritingLoadorder, QObject::tr("Writing loadorder.txt")}, + {P::ParsingLootMessages, QObject::tr("Parsing loot messages")}, + {P::Done, QObject::tr("Done")} + }; + + auto itor = map.find(p); + if (itor == map.end()) { + return QString("unknown progress %1").arg(static_cast(p)); + } else { + return itor->second; + } +} + MarkdownDocument::MarkdownDocument(QObject* parent) : QObject(parent) @@ -74,7 +97,11 @@ void LootDialog::setText(const QString& s) void LootDialog::setProgress(lootcli::Progress p) { - setText(progressToString(p)); + // don't overwrite the "stopping loot" message even if lootcli generates a new + // progress message + if (!m_cancelling) { + setText(progressToString(p)); + } if (p == lootcli::Progress::Done) { ui->progressBar->setRange(0, 1); @@ -82,24 +109,6 @@ void LootDialog::setProgress(lootcli::Progress p) } } -QString LootDialog::progressToString(lootcli::Progress p) -{ - using P = lootcli::Progress; - - switch (p) - { - case P::CheckingMasterlistExistence: return tr("Checking masterlist existence"); - case P::UpdatingMasterlist: return tr("Updating masterlist"); - case P::LoadingLists: return tr("Loading lists"); - case P::ReadingPlugins: return tr("Reading plugins"); - case P::SortingPlugins: return tr("Sorting plugins"); - case P::WritingLoadorder: return tr("Writing loadorder.txt"); - case P::ParsingLootMessages: return tr("Parsing loot messages"); - case P::Done: return tr("Done"); - default: return QString("unknown progress %1").arg(static_cast(p)); - } -} - void LootDialog::addOutput(const QString& s) { if (m_core.settings().diagnostics().lootLogLevel() > lootcli::LogLevels::Debug) { @@ -125,8 +134,12 @@ bool LootDialog::result() const void LootDialog::cancel() { if (!m_finished && !m_cancelling) { - addLineOutput(tr("Stopping LOOT...")); + log::debug("loot dialog: cancelling"); m_loot.cancel(); + + setText(tr("Stopping LOOT...")); + addLineOutput("stopping loot"); + ui->buttons->setEnabled(false); m_cancelling = true; } @@ -138,6 +151,22 @@ void LootDialog::openReport() shell::Open(path); } +void LootDialog::accept() +{ + // no-op +} + +void LootDialog::reject() +{ + if (m_finished) { + log::debug("loot dialog reject: loot finished, closing"); + QDialog::reject(); + } else { + log::debug("loot dialog reject: not finished, cancelling"); + cancel(); + } +} + void LootDialog::createUI() { ui->setupUi(this); @@ -169,7 +198,7 @@ void LootDialog::createUI() new ExpanderWidget(ui->details, ui->detailsPanel); - connect(ui->buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); + ui->buttons->setStandardButtons(QDialogButtonBox::Cancel); resize(650, 450); } @@ -177,24 +206,15 @@ void LootDialog::createUI() void LootDialog::closeEvent(QCloseEvent* e) { if (m_finished) { + log::debug("loot dialog close event: finished, closing"); QDialog::closeEvent(e); } else { + log::debug("loot dialog close event: not finished, cancelling"); cancel(); e->ignore(); } } -void LootDialog::onButton(QAbstractButton* b) -{ - if (ui->buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { - if (m_finished) { - close(); - } else { - cancel(); - } - } -} - void LootDialog::addLineOutput(const QString& line) { ui->output->appendPlainText(line); @@ -202,12 +222,16 @@ void LootDialog::addLineOutput(const QString& line) void LootDialog::onFinished() { + log::debug("loot dialog: loot is finished"); + m_finished = true; if (m_cancelling) { + log::debug("loot dialog: was cancelling, closing"); close(); } else { - handleReport(); + log::debug("loot dialog: showing report"); + showReport(); ui->openJsonReport->setEnabled(true); ui->buttons->setStandardButtons(QDialogButtonBox::Close); } @@ -224,7 +248,7 @@ void LootDialog::log(log::Levels lv, const QString& s) } } -void LootDialog::handleReport() +void LootDialog::showReport() { const auto& lootReport = m_loot.report(); diff --git a/src/lootdialog.h b/src/lootdialog.h index df9e546d..fcdeb304 100644 --- a/src/lootdialog.h +++ b/src/lootdialog.h @@ -48,16 +48,14 @@ public: void setText(const QString& s); void setProgress(lootcli::Progress p); - QString progressToString(lootcli::Progress p); - void addOutput(const QString& s); - bool result() const; - void cancel(); - void openReport(); + void accept() override; + void reject() override; + private: std::unique_ptr ui; OrganizerCore& m_core; @@ -68,11 +66,10 @@ private: void createUI(); void closeEvent(QCloseEvent* e) override; - void onButton(QAbstractButton* b); void addLineOutput(const QString& line); void onFinished(); void log(MOBase::log::Levels lv, const QString& s); - void handleReport(); + void showReport(); }; #endif // MODORGANIZER_LOOTDIALOG_H -- cgit v1.3.1 From 0c69619dbe4fae24794b2539a331ca7ac66f1f93 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 25 Nov 2019 02:10:59 -0500 Subject: switched to named pipes --- src/loot.cpp | 178 ++++++++++++++++++++++++++++++++--------------------- src/loot.h | 6 +- src/lootdialog.cpp | 1 + 3 files changed, 115 insertions(+), 70 deletions(-) (limited to 'src/lootdialog.cpp') diff --git a/src/loot.cpp b/src/loot.cpp index 9c8cf8c4..d9399ab6 100644 --- a/src/loot.cpp +++ b/src/loot.cpp @@ -9,6 +9,8 @@ using namespace MOBase; using namespace json; +static QString LootReportPath = QDir::temp().absoluteFilePath("lootreport.json"); + log::Levels levelFromLoot(lootcli::LogLevels level) { using LC = lootcli::LogLevels; @@ -198,14 +200,14 @@ Loot::~Loot() m_thread->wait(); } - if (!m_outPath.isEmpty() && QFile::exists(m_outPath)) { - log::debug("deleting temporary loot report '{}'", m_outPath); - const auto r = shell::Delete(m_outPath); + if (QFile::exists(LootReportPath)) { + log::debug("deleting temporary loot report '{}'", LootReportPath); + const auto r = shell::Delete(LootReportPath); if (!r) { log::error( "failed to remove temporary loot json report '{}': {}", - m_outPath, r.toString()); + LootReportPath, r.toString()); } } } @@ -214,8 +216,32 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) { log::debug("starting loot"); - m_outPath = QDir::temp().absoluteFilePath("lootreport.json"); + // creating pipe + env::HandlePtr out(createPipe()); + if (out.get() == INVALID_HANDLE_VALUE) { + return false; + } + + // vfs + core.prepareVFS(); + // spawning + if (!spawnLootcli(parent, core, didUpdateMasterList, out.get())) { + return false; + } + + // starting thread + log::debug("starting loot thread"); + m_thread.reset(QThread::create([&]{ lootThread(); })); + m_thread->start(); + + return true; +} + +bool Loot::spawnLootcli( + QWidget* parent, OrganizerCore& core, bool didUpdateMasterList, + HANDLE stdoutHandle) +{ const auto logLevel = core.settings().diagnostics().lootLogLevel(); QStringList parameters; @@ -224,45 +250,18 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) << "--gamePath" << QString("\"%1\"").arg(core.managedGame()->gameDirectory().absolutePath()) << "--pluginListPath" << QString("\"%1/loadorder.txt\"").arg(core.profilePath()) << "--logLevel" << QString::fromStdString(lootcli::logLevelToString(logLevel)) - << "--out" << QString("\"%1\"").arg(m_outPath); + << "--out" << QString("\"%1\"").arg(LootReportPath); if (didUpdateMasterList) { parameters << "--skipUpdateMasterlist"; } - SECURITY_ATTRIBUTES secAttributes; - secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); - secAttributes.bInheritHandle = TRUE; - secAttributes.lpSecurityDescriptor = nullptr; - - env::HandlePtr readPipe, writePipe; - - { - HANDLE read = INVALID_HANDLE_VALUE; - HANDLE write = INVALID_HANDLE_VALUE; - - if (!::CreatePipe(&read, &write, &secAttributes, 0)) { - log::error("failed to create stdout reroute"); - } - - readPipe.reset(read); - writePipe.reset(write); - - if (!::SetHandleInformation(read, HANDLE_FLAG_INHERIT, 0)) { - log::error("failed to correctly set up the stdout reroute"); - } - } - - core.prepareVFS(); - spawn::SpawnParameters sp; sp.binary = QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"); sp.arguments = parameters.join(" "); sp.currentDirectory.setPath(qApp->applicationDirPath() + "/loot"); sp.hooked = true; - sp.stdOut = writePipe.get(); - - m_stdout = std::move(readPipe); + sp.stdOut = stdoutHandle; HANDLE lootHandle = spawn::startBinary(parent, sp); if (lootHandle == INVALID_HANDLE_VALUE) { @@ -272,27 +271,64 @@ bool Loot::start(QWidget* parent, OrganizerCore& core, bool didUpdateMasterList) m_lootProcess.reset(lootHandle); - core.pluginList()->clearAdditionalInformation(); + return true; +} + +HANDLE Loot::createPipe() +{ + static const wchar_t* PipeName = L"\\\\.\\pipe\\lootcli_pipe"; - log::debug("starting loot thread"); + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; - m_thread.reset(QThread::create([&]{ - try - { - lootThread(); + env::HandlePtr pipe; + + // creating pipe + { + HANDLE pipeHandle = ::CreateNamedPipe( + PipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, + 1, 50'000, 50'000, 0, &sa); + + if (pipeHandle == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + log::error("CreateNamedPipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; } - catch(...) - { - log::error("unhandled exception in loot thread"); + + pipe.reset(pipeHandle); + } + + { + // duplicating the handle to read from it + HANDLE outputRead = INVALID_HANDLE_VALUE; + + const auto r = DuplicateHandle( + GetCurrentProcess(), pipe.get(), GetCurrentProcess(), &outputRead, + 0, TRUE, DUPLICATE_SAME_ACCESS); + + if (!r) { + const auto e = GetLastError(); + log::error("DuplicateHandle for pipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; } - log::debug("finishing loot thread"); - emit finished(); - })); + m_stdout.reset(outputRead); + } - m_thread->start(); - return true; + // creating handle to pipe which is passed to CreateProcess() + HANDLE outputWrite = ::CreateFileW( + PipeName, FILE_WRITE_DATA|SYNCHRONIZE, 0, + &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); + + if (outputWrite == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + log::error("CreateFileW for pipe failed, {}", formatSystemMessage(e)); + return INVALID_HANDLE_VALUE; + } + + return outputWrite; } void Loot::cancel() @@ -310,7 +346,7 @@ bool Loot::result() const const QString& Loot::outPath() const { - return m_outPath; + return LootReportPath; } const Loot::Report& Loot::report() const @@ -322,7 +358,8 @@ void Loot::lootThread() { ::SetThreadDescription(GetCurrentThread(), L"loot"); - try { + try + { m_result = false; if (!waitForCompletion()) { @@ -331,9 +368,14 @@ void Loot::lootThread() m_result = true; processOutputFile(); - } catch (const std::exception &e) { - emit log(log::Levels::Error, tr("failed to run loot: %1").arg(e.what())); } + catch(...) + { + log::error("unhandled exception in loot thread"); + } + + log::debug("finishing loot thread"); + emit finished(); } bool Loot::waitForCompletion() @@ -396,25 +438,23 @@ bool Loot::waitForCompletion() std::string Loot::readFromPipe() { - static const int chunkSize = 128; - std::string result; + static const std::size_t bufferSize = 50'000; - char buffer[chunkSize + 1]; - buffer[chunkSize] = '\0'; + char buffer[bufferSize] = {}; - DWORD read = 1; - while (read > 0) { - if (!::ReadFile(m_stdout.get(), buffer, chunkSize, &read, nullptr)) { - break; - } - if (read > 0) { - result.append(buffer, read); - if (read < chunkSize) { - break; - } + DWORD bytesRead = 0; + if (::ReadFile(m_stdout.get(), buffer, bufferSize, &bytesRead, nullptr)) { + return {buffer, buffer + bytesRead}; + } else { + const auto e = GetLastError(); + + // broken pipe probably means lootcli is finished + if (e != ERROR_BROKEN_PIPE) { + log::error("{}", formatSystemMessage(e)); } + + return {}; } - return result; } void Loot::processStdout(const std::string &lootOut) @@ -472,9 +512,9 @@ void Loot::processMessage(const lootcli::Message& m) void Loot::processOutputFile() { - log::debug("parsing json output file at '{}'", m_outPath); + log::debug("parsing json output file at '{}'", LootReportPath); - QFile outFile(m_outPath); + QFile outFile(LootReportPath); if (!outFile.open(QIODevice::ReadOnly)) { emit log( MOBase::log::Error, diff --git a/src/loot.h b/src/loot.h index 30ef4b60..a67b9ed8 100644 --- a/src/loot.h +++ b/src/loot.h @@ -97,12 +97,16 @@ private: std::unique_ptr m_thread; std::atomic m_cancel; std::atomic m_result; - QString m_outPath; env::HandlePtr m_lootProcess; env::HandlePtr m_stdout; std::string m_outputBuffer; Report m_report; + HANDLE createPipe(); + bool spawnLootcli( + QWidget* parent, OrganizerCore& core, bool didUpdateMasterList, + HANDLE stdoutHandle); + std::string readFromPipe(); void lootThread(); diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index 43929c00..a8f73cdc 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -252,6 +252,7 @@ void LootDialog::showReport() { const auto& lootReport = m_loot.report(); + m_core.pluginList()->clearAdditionalInformation(); for (auto&& p : lootReport.plugins) { m_core.pluginList()->addLootReport(p.name, p); } -- cgit v1.3.1 From f1b621d0babd33537cde97fc9d53e0dfa0ad5ea5 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 25 Nov 2019 03:34:51 -0500 Subject: save/restore state for loot dialog --- src/lootdialog.cpp | 18 +++++++++++++++--- src/lootdialog.h | 3 +++ 2 files changed, 18 insertions(+), 3 deletions(-) (limited to 'src/lootdialog.cpp') diff --git a/src/lootdialog.cpp b/src/lootdialog.cpp index a8f73cdc..9e269fef 100644 --- a/src/lootdialog.cpp +++ b/src/lootdialog.cpp @@ -3,7 +3,6 @@ #include "loot.h" #include "organizercore.h" #include -#include #include using namespace MOBase; @@ -151,6 +150,20 @@ void LootDialog::openReport() shell::Open(path); } +int LootDialog::exec() +{ + auto& s = m_core.settings(); + + GeometrySaver gs(s, this); + s.geometry().restoreState(&m_expander); + + const auto r = QDialog::exec(); + + s.geometry().saveState(&m_expander); + + return r; +} + void LootDialog::accept() { // no-op @@ -193,11 +206,10 @@ void LootDialog::createUI() log::error("can't open '{}', {}", path, f.errorString()); } + m_expander.set(ui->details, ui->detailsPanel); ui->openJsonReport->setEnabled(false); connect(ui->openJsonReport, &QPushButton::clicked, [&]{ openReport(); }); - new ExpanderWidget(ui->details, ui->detailsPanel); - ui->buttons->setStandardButtons(QDialogButtonBox::Cancel); resize(650, 450); diff --git a/src/lootdialog.h b/src/lootdialog.h index fcdeb304..bc8c01fb 100644 --- a/src/lootdialog.h +++ b/src/lootdialog.h @@ -3,6 +3,7 @@ #include #include +#include namespace Ui { class LootDialog; } @@ -53,11 +54,13 @@ public: void cancel(); void openReport(); + int exec() override; void accept() override; void reject() override; private: std::unique_ptr ui; + MOBase::ExpanderWidget m_expander; OrganizerCore& m_core; Loot& m_loot; bool m_finished; -- cgit v1.3.1