diff --git a/application/InstancePageProvider.h b/application/InstancePageProvider.h index cff0b6d2..79bdb71b 100644 --- a/application/InstancePageProvider.h +++ b/application/InstancePageProvider.h @@ -41,12 +41,10 @@ public: values.append(new NotesPage(onesix.get())); values.append(new ScreenshotsPage(PathCombine(onesix->minecraftRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix.get())); - values.append(new OtherLogsPage(onesix->minecraftRoot())); } std::shared_ptr legacy = std::dynamic_pointer_cast(inst); if(legacy) { - QList values; // FIXME: actually implement the legacy instance upgrade, then enable this. //values.append(new LegacyUpgradePage(this)); values.append(new LegacyJarModPage(legacy.get())); @@ -58,8 +56,12 @@ public: values.append(new NotesPage(legacy.get())); values.append(new ScreenshotsPage(PathCombine(legacy->minecraftRoot(), "screenshots"))); values.append(new InstanceSettingsPage(legacy.get())); - values.append(new OtherLogsPage(legacy->minecraftRoot())); - return values; + values.append(new OtherLogsPage(legacy->minecraftRoot(), inst->getLogFileMatcher())); + } + auto logMatcher = inst->getLogFileMatcher(); + if(logMatcher) + { + values.append(new OtherLogsPage(onesix->minecraftRoot(), logMatcher)); } return values; } diff --git a/application/pages/OtherLogsPage.cpp b/application/pages/OtherLogsPage.cpp index 2f9a800c..d59f0451 100644 --- a/application/pages/OtherLogsPage.cpp +++ b/application/pages/OtherLogsPage.cpp @@ -20,20 +20,20 @@ #include "GuiUtil.h" #include "RecursiveFileSystemWatcher.h" +#include #include -OtherLogsPage::OtherLogsPage(QString path, QWidget *parent) - : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), +OtherLogsPage::OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent) + : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), m_fileFilter(fileFilter), m_watcher(new RecursiveFileSystemWatcher(this)) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); - m_watcher->setFileExpression("(.*\\.log(\\.[0-9]*)?$)|(crash-.*\\.txt)"); + m_watcher->setMatcher(fileFilter); m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path)); - connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, - &OtherLogsPage::populateSelectLogBox); + connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, &OtherLogsPage::populateSelectLogBox); populateSelectLogBox(); } @@ -55,15 +55,23 @@ void OtherLogsPage::populateSelectLogBox() { ui->selectLogBox->clear(); ui->selectLogBox->addItems(m_watcher->files()); - if (m_currentFile.isNull()) + if (m_currentFile.isEmpty()) { + setControlsEnabled(false); ui->selectLogBox->setCurrentIndex(-1); } else { const int index = ui->selectLogBox->findText(m_currentFile); if (index != -1) + { ui->selectLogBox->setCurrentIndex(index); + setControlsEnabled(true); + } + else + { + setControlsEnabled(false); + } } } @@ -91,6 +99,11 @@ void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) void OtherLogsPage::on_btnReload_clicked() { + if(m_currentFile.isEmpty()) + { + setControlsEnabled(false); + return; + } QFile file(PathCombine(m_path, m_currentFile)); if (!file.open(QFile::ReadOnly)) { @@ -102,16 +115,39 @@ void OtherLogsPage::on_btnReload_clicked() } else { - if (file.size() < 10000000ll) - { - ui->text->setPlainText(QString::fromUtf8(file.readAll())); - } - else + auto showTooBig = [&]() { ui->text->setPlainText( tr("The file (%1) is too big. You may want to open it in a viewer optimized " "for large files.").arg(file.fileName())); + }; + if(file.size() >= 10000000ll) + { + showTooBig(); + return; } + QString content; + if(file.fileName().endsWith(".gz")) + { + QByteArray temp; + if(!GZip::inflate(file.readAll(), temp)) + { + ui->text->setPlainText( + tr("The file (%1) is not readable.").arg(file.fileName())); + return; + } + content = QString::fromUtf8(temp); + } + else + { + content = QString::fromUtf8(file.readAll()); + } + if (content.size() >= 50000000ll) + { + showTooBig(); + return; + } + ui->text->setPlainText(content); } } @@ -119,12 +155,19 @@ void OtherLogsPage::on_btnPaste_clicked() { GuiUtil::uploadPaste(ui->text->toPlainText(), this); } + void OtherLogsPage::on_btnCopy_clicked() { GuiUtil::setClipboardText(ui->text->toPlainText()); } + void OtherLogsPage::on_btnDelete_clicked() { + if(m_currentFile.isEmpty()) + { + setControlsEnabled(false); + return; + } if (QMessageBox::question(this, tr("Delete"), tr("Do you really want to delete %1?").arg(m_currentFile), QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) @@ -139,6 +182,68 @@ void OtherLogsPage::on_btnDelete_clicked() } } + + +void OtherLogsPage::on_btnClean_clicked() +{ + auto toDelete = m_watcher->files(); + if(toDelete.isEmpty()) + { + return; + } + QMessageBox *messageBox = new QMessageBox(this); + messageBox->setWindowTitle(tr("Clean up")); + if(toDelete.size() > 5) + { + messageBox->setText(tr("Do you really want to delete all log files?")); + messageBox->setDetailedText(toDelete.join('\n')); + } + else + { + messageBox->setText(tr("Do you really want to these files?\n%1").arg(toDelete.join('\n'))); + } + messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + messageBox->setDefaultButton(QMessageBox::Ok); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(QMessageBox::Question); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + + if (messageBox->exec() != QMessageBox::Ok) + { + return; + } + QStringList failed; + for(auto item: toDelete) + { + QFile file(PathCombine(m_path, item)); + if (!file.remove()) + { + failed.push_back(item); + } + } + if(!failed.empty()) + { + QMessageBox *messageBox = new QMessageBox(this); + messageBox->setWindowTitle(tr("Error")); + if(failed.size() > 5) + { + messageBox->setText(tr("Couldn't delete some files!")); + messageBox->setDetailedText(failed.join('\n')); + } + else + { + messageBox->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); + } + messageBox->setStandardButtons(QMessageBox::Ok); + messageBox->setDefaultButton(QMessageBox::Ok); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(QMessageBox::Critical); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + messageBox->exec(); + } +} + + void OtherLogsPage::setControlsEnabled(const bool enabled) { ui->btnReload->setEnabled(enabled); @@ -146,4 +251,5 @@ void OtherLogsPage::setControlsEnabled(const bool enabled) ui->btnCopy->setEnabled(enabled); ui->btnPaste->setEnabled(enabled); ui->text->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); } diff --git a/application/pages/OtherLogsPage.h b/application/pages/OtherLogsPage.h index d6e4ec9f..9a5a6ed4 100644 --- a/application/pages/OtherLogsPage.h +++ b/application/pages/OtherLogsPage.h @@ -19,6 +19,7 @@ #include "BasePage.h" #include +#include namespace Ui { @@ -32,7 +33,7 @@ class OtherLogsPage : public QWidget, public BasePage Q_OBJECT public: - explicit OtherLogsPage(QString path, QWidget *parent = 0); + explicit OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent = 0); ~OtherLogsPage(); QString id() const override @@ -61,12 +62,15 @@ private slots: void on_btnPaste_clicked(); void on_btnCopy_clicked(); void on_btnDelete_clicked(); + void on_btnClean_clicked(); + +private: + void setControlsEnabled(const bool enabled); private: Ui::OtherLogsPage *ui; QString m_path; RecursiveFileSystemWatcher *m_watcher; QString m_currentFile; - - void setControlsEnabled(const bool enabled); + IPathMatcher::Ptr m_fileFilter; }; diff --git a/application/pages/OtherLogsPage.ui b/application/pages/OtherLogsPage.ui index 08200684..755de6df 100644 --- a/application/pages/OtherLogsPage.ui +++ b/application/pages/OtherLogsPage.ui @@ -37,25 +37,8 @@ - - - - - - 0 - 0 - - - - - - - - Reload - - - - + + Copy the whole log into the clipboard @@ -65,7 +48,17 @@ - + + + + Clear the log + + + Delete + + + + Upload the log to paste.ee - it will stay online for a month @@ -75,13 +68,30 @@ - - + + + + Reload + + + + + + + + 0 + 0 + + + + + + Clear the log - Delete + Clean diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h index 9c282bbc..bcd1e0da 100644 --- a/logic/BaseInstance.h +++ b/logic/BaseInstance.h @@ -26,6 +26,7 @@ #include "BaseVersionList.h" #include "auth/MojangAccount.h" #include "launch/MessageLevel.h" +#include "pathmatcher/IPathMatcher.h" class QDir; class Task; @@ -158,12 +159,16 @@ public: */ virtual std::shared_ptr createJarModdingTask() = 0; - /*! * Create envrironment variables for running the instance */ virtual QProcessEnvironment createEnvironment() = 0; + /*! + * Returns a matcher that can maps relative paths within the instance to whether they are 'log files' + */ + virtual IPathMatcher::Ptr getLogFileMatcher() = 0; + /*! * does any necessary cleanups after the instance finishes. also runs before\ * TODO: turn into a task that can run asynchronously diff --git a/logic/CMakeLists.txt b/logic/CMakeLists.txt index fe445c05..fce89ac8 100644 --- a/logic/CMakeLists.txt +++ b/logic/CMakeLists.txt @@ -59,6 +59,16 @@ set(LOGIC_SOURCES resources/ResourceProxyModel.h resources/ResourceProxyModel.cpp + # Path matchers + pathmatcher/FSTreeMatcher.h + pathmatcher/IPathMatcher.h + pathmatcher/MultiMatcher.h + pathmatcher/RegexpMatcher.h + + # Compression support + GZip.h + GZip.cpp + # network stuffs net/NetAction.h net/MD5EtagDownload.h @@ -293,12 +303,28 @@ set(LOGIC_SOURCES ) ################################ COMPILE ################################ +if(WIN32) + add_definitions(-DZ_PREFIX) +endif() + # Add common library add_library(MultiMC_logic STATIC ${LOGIC_SOURCES}) +# Use system zlib on unix and Qt ZLIB on Windows +if(UNIX) + find_package(ZLIB REQUIRED) +else(UNIX) + get_filename_component(ZLIB_FOUND_DIR "${Qt5Core_DIR}/../../../include/QtZlib" ABSOLUTE) + set(ZLIB_INCLUDE_DIRS ${ZLIB_FOUND_DIR} CACHE PATH "Path to ZLIB headers of Qt") + set(ZLIB_LIBRARIES "") + if(NOT EXISTS "${ZLIB_INCLUDE_DIRS}/zlib.h") + message("Please specify a valid zlib include dir") + endif(NOT EXISTS "${ZLIB_INCLUDE_DIRS}/zlib.h") +endif(UNIX) + # Link target_link_libraries(MultiMC_logic xz-embedded unpack200 iconfix libUtil LogicalGui ${QUAZIP_LIBRARIES} Qt5::Core Qt5::Xml Qt5::Widgets Qt5::Network Qt5::Concurrent - ${MultiMC_LINK_ADDITIONAL_LIBS}) + ${ZLIB_LIBRARIES} ${MultiMC_LINK_ADDITIONAL_LIBS}) add_dependencies(MultiMC_logic QuaZIP) diff --git a/logic/GZip.cpp b/logic/GZip.cpp new file mode 100644 index 00000000..96d58ea8 --- /dev/null +++ b/logic/GZip.cpp @@ -0,0 +1,71 @@ +#include "GZip.h" +#include +#include + +// HACK: workaround for terrible macro crap on Windows +int wrap_inflate (z_streamp strm, int flush) +{ + return inflate(strm, flush); +} + +#ifdef inflate + #undef inflate +#endif + +bool GZip::inflate(const QByteArray &compressedBytes, QByteArray &uncompressedBytes) +{ + if (compressedBytes.size() == 0) + { + uncompressedBytes = compressedBytes; + return true; + } + + unsigned uncompLength = compressedBytes.size(); + unsigned half_length = compressedBytes.size() / 2; + uncompressedBytes.clear(); + uncompressedBytes.resize(uncompLength); + + z_stream strm; + strm.next_in = (Bytef *)compressedBytes.data(); + strm.avail_in = compressedBytes.size(); + strm.total_out = 0; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + + bool done = false; + + if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK) + { + return false; + } + + while (!done) + { + // If our output buffer is too small + if (strm.total_out >= uncompLength) + { + uncompressedBytes.resize(uncompLength + half_length); + uncompLength += half_length; + } + + strm.next_out = (Bytef *)(uncompressedBytes.data() + strm.total_out); + strm.avail_out = uncompLength - strm.total_out; + + // Inflate another chunk. + int err = wrap_inflate(&strm, Z_SYNC_FLUSH); + if (err == Z_STREAM_END) + done = true; + else if (err != Z_OK) + { + break; + } + } + + if (inflateEnd(&strm) != Z_OK) + { + return false; + } + + uncompressedBytes.resize(strm.total_out); + return true; +} diff --git a/logic/GZip.h b/logic/GZip.h new file mode 100644 index 00000000..cd9f53d8 --- /dev/null +++ b/logic/GZip.h @@ -0,0 +1,9 @@ +#pragma once +#include + +class GZip +{ +public: + static bool inflate(const QByteArray &compressedBytes, QByteArray &uncompressedBytes); +}; + diff --git a/logic/NullInstance.h b/logic/NullInstance.h index aba3e484..9ddbc2b2 100644 --- a/logic/NullInstance.h +++ b/logic/NullInstance.h @@ -70,4 +70,8 @@ public: { return QMap(); } + virtual IPathMatcher::Ptr getLogFileMatcher() + { + return nullptr; + } }; diff --git a/logic/RecursiveFileSystemWatcher.cpp b/logic/RecursiveFileSystemWatcher.cpp index 39985699..1253870b 100644 --- a/logic/RecursiveFileSystemWatcher.cpp +++ b/logic/RecursiveFileSystemWatcher.cpp @@ -4,7 +4,7 @@ #include RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject *parent) - : QObject(parent), m_exp(".*"), m_watcher(new QFileSystemWatcher(this)) + : QObject(parent), m_watcher(new QFileSystemWatcher(this)) { connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RecursiveFileSystemWatcher::fileChange); @@ -82,16 +82,20 @@ void RecursiveFileSystemWatcher::addFilesToWatcherRecursive(const QDir &dir) QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir &directory) { QStringList ret; - QRegularExpression exp(m_exp); + if(!m_matcher) + { + return {}; + } for (const QString &dir : directory.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { ret.append(scanRecursive(directory.absoluteFilePath(dir))); } for (const QString &file : directory.entryList(QDir::Files)) { - if (exp.match(file).hasMatch()) + auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); + if (m_matcher->matches(relPath)) { - ret.append(m_root.relativeFilePath(directory.absoluteFilePath(file))); + ret.append(relPath); } } return ret; diff --git a/logic/RecursiveFileSystemWatcher.h b/logic/RecursiveFileSystemWatcher.h index 339172bb..a2a9e7e3 100644 --- a/logic/RecursiveFileSystemWatcher.h +++ b/logic/RecursiveFileSystemWatcher.h @@ -2,6 +2,7 @@ #include #include +#include "pathmatcher/IPathMatcher.h" class RecursiveFileSystemWatcher : public QObject { @@ -10,16 +11,27 @@ public: RecursiveFileSystemWatcher(QObject *parent); void setRootDir(const QDir &root); - QDir rootDir() const { return m_root; } + QDir rootDir() const + { + return m_root; + } // WARNING: setting this to true may be bad for performance void setWatchFiles(const bool watchFiles); - bool watchFiles() const { return m_watchFiles; } + bool watchFiles() const + { + return m_watchFiles; + } - void setFileExpression(const QString &exp) { m_exp = exp; } - QString fileExpression() const { return m_exp; } + void setMatcher(IPathMatcher::Ptr matcher) + { + m_matcher = matcher; + } - QStringList files() const { return m_files; } + QStringList files() const + { + return m_files; + } signals: void filesChanged(); @@ -33,7 +45,7 @@ private: QDir m_root; bool m_watchFiles = false; bool m_isEnabled = false; - QString m_exp; + IPathMatcher::Ptr m_matcher; QFileSystemWatcher *m_watcher; diff --git a/logic/minecraft/MinecraftInstance.cpp b/logic/minecraft/MinecraftInstance.cpp index d576ef4f..50962452 100644 --- a/logic/minecraft/MinecraftInstance.cpp +++ b/logic/minecraft/MinecraftInstance.cpp @@ -5,6 +5,8 @@ #include "Env.h" #include "minecraft/MinecraftVersionList.h" #include +#include +#include #define IBUS "@im=ibus" @@ -277,4 +279,12 @@ MessageLevel::Enum MinecraftInstance::guessLevel(const QString &line, MessageLev return level; } +IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher() +{ + auto combined = std::make_shared(); + combined->add(std::make_shared(".*\\.log(\\.[0-9]*)?(\\.gz)?$")); + combined->add(std::make_shared("crash-.*\\.txt")); + return combined; +} + #include "MinecraftInstance.moc" diff --git a/logic/minecraft/MinecraftInstance.h b/logic/minecraft/MinecraftInstance.h index 14a0f097..a09e44db 100644 --- a/logic/minecraft/MinecraftInstance.h +++ b/logic/minecraft/MinecraftInstance.h @@ -44,6 +44,8 @@ public: /// guess log level from a line of minecraft log virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level); + virtual IPathMatcher::Ptr getLogFileMatcher() override; + protected: QMap createCensorFilterFromSession(AuthSessionPtr session); }; diff --git a/logic/pathmatcher/FSTreeMatcher.h b/logic/pathmatcher/FSTreeMatcher.h new file mode 100644 index 00000000..f99e45c9 --- /dev/null +++ b/logic/pathmatcher/FSTreeMatcher.h @@ -0,0 +1,19 @@ +#include "IPathMatcher.h" +#include +#include + +class FSTreeMatcher : public IPathMatcher +{ +public: + virtual ~FSTreeMatcher() {}; + FSTreeMatcher(SeparatorPrefixTree<'/'> & tree) : m_fsTree(tree) + { + } + + virtual bool matches(const QString &string) override + { + return m_fsTree.covers(string); + } + + SeparatorPrefixTree<'/'> & m_fsTree; +}; diff --git a/logic/pathmatcher/IPathMatcher.h b/logic/pathmatcher/IPathMatcher.h new file mode 100644 index 00000000..806a750a --- /dev/null +++ b/logic/pathmatcher/IPathMatcher.h @@ -0,0 +1,12 @@ +#pragma once +#include + +class IPathMatcher +{ +public: + typedef std::shared_ptr Ptr; + +public: + virtual ~IPathMatcher(){}; + virtual bool matches(const QString &string) = 0; +}; diff --git a/logic/pathmatcher/MultiMatcher.h b/logic/pathmatcher/MultiMatcher.h new file mode 100644 index 00000000..e018967c --- /dev/null +++ b/logic/pathmatcher/MultiMatcher.h @@ -0,0 +1,31 @@ +#include "IPathMatcher.h" +#include +#include + +class MultiMatcher : public IPathMatcher +{ +public: + virtual ~MultiMatcher() {}; + MultiMatcher() + { + } + MultiMatcher &add(Ptr add) + { + m_matchers.append(add); + return *this; + } + + virtual bool matches(const QString &string) override + { + for(auto iter: m_matchers) + { + if(iter->matches(string)) + { + return true; + } + } + return false; + } + + QList m_matchers; +}; diff --git a/logic/pathmatcher/RegexpMatcher.h b/logic/pathmatcher/RegexpMatcher.h new file mode 100644 index 00000000..f3cf90b1 --- /dev/null +++ b/logic/pathmatcher/RegexpMatcher.h @@ -0,0 +1,29 @@ +#include "IPathMatcher.h" +#include + +class RegexpMatcher : public IPathMatcher +{ +public: + virtual ~RegexpMatcher() {}; + RegexpMatcher(QString regexp) + { + m_regexp.setPattern(regexp); + m_onlyFilenamePart = !regexp.contains('/'); + } + + virtual bool matches(const QString &string) override + { + if(m_onlyFilenamePart) + { + auto slash = string.lastIndexOf('/'); + if(slash != -1) + { + auto part = string.mid(slash + 1); + return m_regexp.match(part).hasMatch(); + } + } + return m_regexp.match(string).hasMatch(); + } + QRegularExpression m_regexp; + bool m_onlyFilenamePart = false; +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6e66c834..238accf9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,6 +29,7 @@ add_unit_test(modutils tst_modutils.cpp) add_unit_test(inifile tst_inifile.cpp) add_unit_test(UpdateChecker tst_UpdateChecker.cpp) add_unit_test(DownloadTask tst_DownloadTask.cpp) +add_unit_test(filematchers tst_filematchers.cpp) add_unit_test(Resource tst_Resource.cpp) # Tests END # diff --git a/tests/tst_filematchers.cpp b/tests/tst_filematchers.cpp new file mode 100644 index 00000000..36286070 --- /dev/null +++ b/tests/tst_filematchers.cpp @@ -0,0 +1,37 @@ +#include +#include "TestUtil.h" + +class IniFileTest : public QObject +{ + Q_OBJECT +private +slots: + + void test_FSTree() + { + /* + QTest::addColumn("through"); + + QTest::newRow("unix path") << "/abc/def/ghi/jkl"; + QTest::newRow("windows path") << "C:\\Program files\\terrible\\name\\of something\\"; + QTest::newRow("Plain text") << "Lorem ipsum dolor sit amet."; + QTest::newRow("Escape sequences") << "Lorem\n\t\n\\n\\tAAZ\nipsum dolor\n\nsit amet."; + QTest::newRow("Escape sequences 2") << "\"\n\n\""; + */ + } + void test_Regexp() + { + /* + QFETCH(QString, through); + + QString there = INIFile::escape(through); + QString back = INIFile::unescape(there); + + QCOMPARE(back, through); + */ + } +}; + +QTEST_GUILESS_MAIN(IniFileTest) + +#include "tst_filematchers.moc"