diff options
65 files changed, 1130 insertions, 890 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ea5803d..33f74a8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -300,8 +300,6 @@ logic/net/NetJob.h logic/net/NetJob.cpp logic/net/HttpMetaCache.h logic/net/HttpMetaCache.cpp -logic/net/S3ListBucket.h -logic/net/S3ListBucket.cpp logic/net/PasteUpload.h logic/net/PasteUpload.cpp logic/net/URLConstants.h @@ -482,10 +480,10 @@ QT5_ADD_RESOURCES(GENERATED_QRC ${CMAKE_CURRENT_BINARY_DIR}/generated.qrc) QT5_ADD_RESOURCES(GRAPHICS_QRC graphics.qrc) # Add common library -ADD_LIBRARY(MultiMC_common STATIC ${MULTIMC_SOURCES} ${MULTIMC_UI} ${GENERATED_QRC} ${GRAPHICS_QRC} ${MULTIMC_RCS}) +ADD_LIBRARY(MultiMC_common STATIC ${MULTIMC_SOURCES} ${MULTIMC_UI} ${GENERATED_QRC} ${GRAPHICS_QRC}) # Add executable -ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 main.cpp) +ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 main.cpp ${MULTIMC_RCS}) # Link TARGET_LINK_LIBRARIES(MultiMC MultiMC_common) diff --git a/MultiMC.cpp b/MultiMC.cpp index bf0d9d99..65c24087 100644 --- a/MultiMC.cpp +++ b/MultiMC.cpp @@ -319,9 +319,6 @@ void MultiMC::initGlobalSettings() m_settings->registerSetting(new Setting("MinecraftWinWidth", 854)); m_settings->registerSetting(new Setting("MinecraftWinHeight", 480)); - // Auto login - m_settings->registerSetting(new Setting("AutoLogin", false)); - // Memory m_settings->registerSetting(new Setting("MinMemAlloc", 512)); m_settings->registerSetting(new Setting("MaxMemAlloc", 1024)); @@ -124,6 +124,9 @@ private: void initTranslations(); private: + friend class UpdateCheckerTest; + friend class DownloadUpdateTaskTest; + std::shared_ptr<QTranslator> m_qt_translator; std::shared_ptr<QTranslator> m_mmc_translator; std::shared_ptr<SettingsObject> m_settings; diff --git a/MultiMC.manifest b/MultiMC.manifest new file mode 100644 index 00000000..3acf8f7f --- /dev/null +++ b/MultiMC.manifest @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> + <assemblyIdentity name="MultiMC.Application.5" type="win32" version="5.0.0.0" /> + <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> + <security> + <requestedPrivileges> + <requestedExecutionLevel level="asInvoker" uiAccess="false"/> + </requestedPrivileges> + </security> + </trustInfo> + <dependency> + <dependentAssembly> + <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="x86" publicKeyToken="6595b64144ccf1df" language="*"/> + </dependentAssembly> + </dependency> + <description>Custom Minecraft launcher for managing multiple installs.</description> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!--The ID below indicates app support for Windows Vista --> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + <!--The ID below indicates app support for Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + <!--The ID below indicates app support for Windows Developer Preview / Windows 8 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + </application> + </compatibility> +</assembly>
\ No newline at end of file diff --git a/gui/ConsoleWindow.cpp b/gui/ConsoleWindow.cpp index 24afbc0a..e640d261 100644 --- a/gui/ConsoleWindow.cpp +++ b/gui/ConsoleWindow.cpp @@ -58,10 +58,17 @@ ConsoleWindow::~ConsoleWindow() void ConsoleWindow::writeColor(QString text, const char *color) { // append a paragraph - if (color != nullptr) - ui->text->appendHtml(QString("<font color=\"%1\">%2</font>").arg(color).arg(text)); - else - ui->text->appendPlainText(text); + QString newtext; + newtext += "<span style=\""; + { + if(color) + newtext += QString("color:") + color + ";"; + newtext += "font-family: monospace;"; + } + newtext += "\">"; + newtext += text.toHtmlEscaped(); + newtext += "</span>"; + ui->text->appendHtml(newtext); } void ConsoleWindow::write(QString data, MessageLevel::Enum mode) diff --git a/gui/ConsoleWindow.ui b/gui/ConsoleWindow.ui index 62cc89ac..c2307ecc 100644 --- a/gui/ConsoleWindow.ui +++ b/gui/ConsoleWindow.ui @@ -17,11 +17,6 @@ <layout class="QVBoxLayout" name="verticalLayout"> <item> <widget class="QPlainTextEdit" name="text"> - <property name="font"> - <font> - <pointsize>10</pointsize> - </font> - </property> <property name="undoRedoEnabled"> <bool>false</bool> </property> diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index f6ef6bad..d91fc862 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -20,7 +20,6 @@ #include "MainWindow.h" #include "ui_MainWindow.h" -#include "keyring.h" #include <QMenu> #include <QMessageBox> @@ -177,14 +176,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi statusBar()->addPermanentWidget(m_statusRight, 0); // Add "manage accounts" button, right align - QWidget* spacer = new QWidget(); + QWidget *spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); ui->mainToolBar->addWidget(spacer); accountMenu = new QMenu(this); manageAccountsAction = new QAction(tr("Manage Accounts"), this); manageAccountsAction->setCheckable(false); - connect(manageAccountsAction, SIGNAL(triggered(bool)), this, SLOT(on_actionManageAccounts_triggered())); + connect(manageAccountsAction, SIGNAL(triggered(bool)), this, + SLOT(on_actionManageAccounts_triggered())); repopulateAccountsMenu(); @@ -193,7 +193,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi accountMenuButton->setMenu(accountMenu); accountMenuButton->setPopupMode(QToolButton::InstantPopup); accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - accountMenuButton->setIcon(QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio)); + accountMenuButton->setIcon( + QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio)); QWidgetAction *accountMenuButtonAction = new QWidgetAction(this); accountMenuButtonAction->setDefaultWidget(accountMenuButton); @@ -201,26 +202,28 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi ui->mainToolBar->addAction(accountMenuButtonAction); // Update the menu when the active account changes. - // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. Template hell sucks... - connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this] { activeAccountChanged(); }); - connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this] { repopulateAccountsMenu(); }); + // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. + // Template hell sucks... + connect(MMC->accounts().get(), &MojangAccountList::activeAccountChanged, [this] + { activeAccountChanged(); }); + connect(MMC->accounts().get(), &MojangAccountList::listChanged, [this] + { repopulateAccountsMenu(); }); std::shared_ptr<MojangAccountList> accounts = MMC->accounts(); // TODO: Nicer way to iterate? - for(int i = 0; i < accounts->count(); i++) + for (int i = 0; i < accounts->count(); i++) { MojangAccountPtr account = accounts->at(i); - if(account != nullptr) + if (account != nullptr) { auto job = new NetJob("Startup player skins: " + account->username()); - for(AccountProfile profile : account->profiles()) + for (AccountProfile profile : account->profiles()) { auto meta = MMC->metacache()->resolveEntry("skins", profile.name + ".png"); auto action = CacheDownload::make( - QUrl("http://" + URLConstants::SKINS_BASE + profile.name + ".png"), - meta); + QUrl("http://" + URLConstants::SKINS_BASE + profile.name + ".png"), meta); job->addNetAction(action); meta->stale = true; } @@ -245,9 +248,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // set up the updater object. auto updater = MMC->updateChecker(); - QObject::connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable); + QObject::connect(updater.get(), &UpdateChecker::updateAvailable, this, + &MainWindow::updateAvailable); // if automatic update checks are allowed, start one. - if(MMC->settings()->get("AutoUpdate").toBool()) + if (MMC->settings()->get("AutoUpdate").toBool()) on_actionCheckUpdate_triggered(); } @@ -321,7 +325,7 @@ void MainWindow::repopulateAccountsMenu() QAction *action = new QAction(profile.name, this); action->setData(account->username()); action->setCheckable(true); - if(active_username == account->username()) + if (active_username == account->username()) { action->setChecked(true); } @@ -339,7 +343,7 @@ void MainWindow::repopulateAccountsMenu() action->setCheckable(true); action->setIcon(QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio)); action->setData(""); - if(active_username.isEmpty()) + if (active_username.isEmpty()) { action->setChecked(true); } @@ -356,10 +360,11 @@ void MainWindow::repopulateAccountsMenu() */ void MainWindow::changeActiveAccount() { - QAction* sAction = (QAction*) sender(); + QAction *sAction = (QAction *)sender(); // Profile's associated Mojang username // Will need to change when profiles are properly implemented - if (sAction->data().type() != QVariant::Type::String) return; + if (sAction->data().type() != QVariant::Type::String) + return; QVariant data = sAction->data(); QString id = ""; @@ -390,7 +395,8 @@ void MainWindow::activeAccountChanged() } // Set the icon to the "no account" icon. - accountMenuButton->setIcon(QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio)); + accountMenuButton->setIcon( + QPixmap(":/icons/toolbar/noaccount").scaled(48, 48, Qt::KeepAspectRatio)); } bool MainWindow::eventFilter(QObject *obj, QEvent *ev) @@ -426,26 +432,28 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *ev) void MainWindow::updateAvailable(QString repo, QString versionName, int versionId) { UpdateDialog dlg; - UpdateAction action = (UpdateAction) dlg.exec(); - switch(action) + UpdateAction action = (UpdateAction)dlg.exec(); + switch (action) { - case UPDATE_LATER: - QLOG_INFO() << "Update will be installed later."; - break; - case UPDATE_NOW: - downloadUpdates(repo, versionId); - break; - case UPDATE_ONEXIT: - downloadUpdates(repo, versionId, true); - break; + case UPDATE_LATER: + QLOG_INFO() << "Update will be installed later."; + break; + case UPDATE_NOW: + downloadUpdates(repo, versionId); + break; + case UPDATE_ONEXIT: + downloadUpdates(repo, versionId, true); + break; } } void MainWindow::downloadUpdates(QString repo, int versionId, bool installOnExit) { QLOG_INFO() << "Downloading updates."; - // TODO: If the user chooses to update on exit, we should download updates in the background. - // Doing so is a bit complicated, because we'd have to make sure it finished downloading before actually exiting MultiMC. + // TODO: If the user chooses to update on exit, we should download updates in the + // background. + // Doing so is a bit complicated, because we'd have to make sure it finished downloading + // before actually exiting MultiMC. ProgressDialog updateDlg(this); DownloadUpdateTask updateTask(repo, versionId, &updateDlg); // If the task succeeds, install the updates. @@ -521,36 +529,44 @@ void MainWindow::on_actionAddInstance_triggered() { errorMsg += "An instance with the given directory name already exists."; CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - break; + return; } case InstanceFactory::CantCreateDir: { errorMsg += "Failed to create the instance directory."; CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - break; + return; } default: { errorMsg += QString("Unknown instance loader error %1").arg(error); CustomMessageBox::selectable(this, tr("Error"), errorMsg, QMessageBox::Warning)->show(); - break; + return; } } - std::shared_ptr<MojangAccountList> accounts = MMC->accounts(); - MojangAccountPtr account = accounts->activeAccount(); - if(account.get() != nullptr && account->accountStatus() != NotVerified) + if (MMC->accounts()->anyAccountIsValid()) { ProgressDialog loadDialog(this); auto update = newInstance->doUpdate(false); - connect(update.get(), &Task::failed , [this](QString reason) { + connect(update.get(), &Task::failed, [this](QString reason) + { QString error = QString("Instance load failed: %1").arg(reason); - CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); + CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning) + ->show(); }); loadDialog.exec(update.get()); } + else + { + CustomMessageBox::selectable( + this, tr("Error"), + tr("MultiMC cannot download Minecraft or update instances unless you have at least " + "one account added.\nPlease add your Mojang or Minecraft account."), + QMessageBox::Warning)->show(); + } } void MainWindow::on_actionCopyInstance_triggered() @@ -625,8 +641,14 @@ void MainWindow::on_actionChangeInstGroup_triggered() bool ok = false; QString name(m_selectedInstance->group()); - name = QInputDialog::getText(this, tr("Group name"), tr("Enter a new group name."), - QLineEdit::Normal, name, &ok); + auto groups = MMC->instances()->getGroups(); + groups.insert(0, ""); + groups.sort(Qt::CaseInsensitive); + int foo = groups.indexOf(name); + + name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, + foo, true, &ok); + name = name.simplified(); if (ok) m_selectedInstance->setGroupPost(name); } @@ -810,9 +832,11 @@ void MainWindow::doLaunch() if (accounts->count() <= 0) { // Tell the user they need to log in at least one account in order to play. - auto reply = CustomMessageBox::selectable(this, tr("No Accounts"), - tr("In order to play Minecraft, you must have at least one Mojang or Minecraft account logged in to MultiMC." - "Would you like to open the account manager to add an account now?"), + auto reply = CustomMessageBox::selectable( + this, tr("No Accounts"), + tr("In order to play Minecraft, you must have at least one Mojang or Minecraft " + "account logged in to MultiMC." + "Would you like to open the account manager to add an account now?"), QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec(); if (reply == QMessageBox::Yes) @@ -825,7 +849,7 @@ void MainWindow::doLaunch() { // If no default account is set, ask the user which one to use. AccountSelectDialog selectDialog(tr("Which account would you like to use?"), - AccountSelectDialog::GlobalDefaultCheckbox, this); + AccountSelectDialog::GlobalDefaultCheckbox, this); selectDialog.exec(); @@ -842,7 +866,7 @@ void MainWindow::doLaunch() return; // do the login. if the account has an access token, try to refresh it first. - if(account->accountStatus() != NotVerified) + if (account->accountStatus() != NotVerified) { // We'll need to validate the access token to make sure the account is still logged in. ProgressDialog progDialog(this); @@ -851,7 +875,7 @@ void MainWindow::doLaunch() progDialog.exec(task.get()); auto status = account->accountStatus(); - if(status != NotVerified) + if (status != NotVerified) { updateInstance(m_selectedInstance, account); } @@ -859,20 +883,22 @@ void MainWindow::doLaunch() account->downgrade(); return; } - if (loginWithPassword(account, tr("Your account is currently not logged in. Please enter your password to log in again."))) + if (loginWithPassword(account, tr("Your account is currently not logged in. Please enter " + "your password to log in again."))) updateInstance(m_selectedInstance, account); } -bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString& errorMsg) +bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString &errorMsg) { EditAccountDialog passDialog(errorMsg, this, EditAccountDialog::PasswordField); if (passDialog.exec() == QDialog::Accepted) { - // To refresh the token, we just create an authenticate task with the given account and the user's password. + // To refresh the token, we just create an authenticate task with the given account and + // the user's password. ProgressDialog progDialog(this); auto task = account->login(passDialog.password()); progDialog.exec(task.get()); - if(task->successful()) + if (task->successful()) return true; else { @@ -883,21 +909,20 @@ bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString& erro return false; } -void MainWindow::updateInstance(BaseInstance* instance, MojangAccountPtr account) +void MainWindow::updateInstance(BaseInstance *instance, MojangAccountPtr account) { bool only_prepare = account->accountStatus() != Online; auto updateTask = instance->doUpdate(only_prepare); if (!updateTask) { launchInstance(instance, account); + return; } - else - { - ProgressDialog tDialog(this); - connect(updateTask.get(), &Task::succeeded, [this, instance, account] { launchInstance(instance, account); }); - connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString))); - tDialog.exec(updateTask.get()); - } + ProgressDialog tDialog(this); + connect(updateTask.get(), &Task::succeeded, [this, instance, account] + { launchInstance(instance, account); }); + connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString))); + tDialog.exec(updateTask.get()); } void MainWindow::launchInstance(BaseInstance *instance, MojangAccountPtr account) @@ -985,13 +1010,31 @@ void MainWindow::on_actionChangeInstMCVersion_triggered() this, tr("Are you sure?"), tr("This will remove any library/version customization you did previously. " "This includes things like Forge install and similar."), - QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Abort)->exec(); + QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Abort, + QMessageBox::Abort)->exec(); if (result != QMessageBox::Ok) return; } m_selectedInstance->setIntendedVersionId(vselect.selectedVersion()->descriptor()); } + if (!MMC->accounts()->anyAccountIsValid()) + { + CustomMessageBox::selectable( + this, tr("Error"), + tr("MultiMC cannot download Minecraft or update instances unless you have at least " + "one account added.\nPlease add your Mojang or Minecraft account."), + QMessageBox::Warning)->show(); + return; + } + auto updateTask = m_selectedInstance->doUpdate(false /*only_prepare*/); + if (!updateTask) + { + return; + } + ProgressDialog tDialog(this); + connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString))); + tDialog.exec(updateTask.get()); } void MainWindow::on_actionChangeInstLWJGLVersion_triggered() diff --git a/gui/dialogs/AccountListDialog.cpp b/gui/dialogs/AccountListDialog.cpp index 91b2ac55..242590fb 100644 --- a/gui/dialogs/AccountListDialog.cpp +++ b/gui/dialogs/AccountListDialog.cpp @@ -36,8 +36,12 @@ AccountListDialog::AccountListDialog(QWidget *parent) ui->setupUi(this); m_accounts = MMC->accounts(); - // TODO: Make the "Active?" column show checkboxes or radio buttons. + ui->listView->setModel(m_accounts.get()); + ui->listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + + // Expand the account column + ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch); QItemSelectionModel* selectionModel = ui->listView->selectionModel(); diff --git a/gui/dialogs/InstanceSettings.cpp b/gui/dialogs/InstanceSettings.cpp index 641c7fab..e3f8a66b 100644 --- a/gui/dialogs/InstanceSettings.cpp +++ b/gui/dialogs/InstanceSettings.cpp @@ -98,18 +98,6 @@ void InstanceSettings::applySettings() m_obj->reset("MinecraftWinHeight"); } - // Auto Login - bool login = ui->accountSettingsBox->isChecked(); - m_obj->set("OverrideLogin", login); - if (login) - { - m_obj->set("AutoLogin", ui->autoLoginCheckBox->isChecked()); - } - else - { - m_obj->reset("AutoLogin"); - } - // Memory bool memory = ui->memoryGroupBox->isChecked(); m_obj->set("OverrideMemory", memory); @@ -170,10 +158,6 @@ void InstanceSettings::loadSettings() ui->windowWidthSpinBox->setValue(m_obj->get("MinecraftWinWidth").toInt()); ui->windowHeightSpinBox->setValue(m_obj->get("MinecraftWinHeight").toInt()); - // Auto Login - ui->accountSettingsBox->setChecked(m_obj->get("OverrideLogin").toBool()); - ui->autoLoginCheckBox->setChecked(m_obj->get("AutoLogin").toBool()); - // Memory ui->memoryGroupBox->setChecked(m_obj->get("OverrideMemory").toBool()); ui->minMemSpinBox->setValue(m_obj->get("MinMemAlloc").toInt()); diff --git a/gui/dialogs/InstanceSettings.ui b/gui/dialogs/InstanceSettings.ui index c4a7d6ed..9260caea 100644 --- a/gui/dialogs/InstanceSettings.ui +++ b/gui/dialogs/InstanceSettings.ui @@ -132,31 +132,6 @@ </widget> </item> <item> - <widget class="QGroupBox" name="accountSettingsBox"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="title"> - <string>Account Settings</string> - </property> - <property name="checkable"> - <bool>true</bool> - </property> - <property name="checked"> - <bool>false</bool> - </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> - <widget class="QCheckBox" name="autoLoginCheckBox"> - <property name="text"> - <string>Login automatically when an instance icon is double clicked?</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> <spacer name="verticalSpacerMinecraft"> <property name="orientation"> <enum>Qt::Vertical</enum> @@ -411,7 +386,6 @@ <tabstop>consoleSettingsBox</tabstop> <tabstop>showConsoleCheck</tabstop> <tabstop>autoCloseConsoleCheck</tabstop> - <tabstop>accountSettingsBox</tabstop> <tabstop>memoryGroupBox</tabstop> <tabstop>minMemSpinBox</tabstop> <tabstop>maxMemSpinBox</tabstop> diff --git a/gui/dialogs/SettingsDialog.cpp b/gui/dialogs/SettingsDialog.cpp index e7f537e3..b960483a 100644 --- a/gui/dialogs/SettingsDialog.cpp +++ b/gui/dialogs/SettingsDialog.cpp @@ -150,9 +150,6 @@ void SettingsDialog::applySettings(SettingsObject *s) s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); - // Auto Login - s->set("AutoLogin", ui->autoLoginCheckBox->isChecked()); - // Memory s->set("MinMemAlloc", ui->minMemSpinBox->value()); s->set("MaxMemAlloc", ui->maxMemSpinBox->value()); @@ -202,9 +199,6 @@ void SettingsDialog::loadSettings(SettingsObject *s) ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt()); ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt()); - // Auto Login - ui->autoLoginCheckBox->setChecked(s->get("AutoLogin").toBool()); - // Memory ui->minMemSpinBox->setValue(s->get("MinMemAlloc").toInt()); ui->maxMemSpinBox->setValue(s->get("MaxMemAlloc").toInt()); diff --git a/gui/dialogs/SettingsDialog.ui b/gui/dialogs/SettingsDialog.ui index 0dbc8def..17320b48 100644 --- a/gui/dialogs/SettingsDialog.ui +++ b/gui/dialogs/SettingsDialog.ui @@ -262,22 +262,6 @@ </widget> </item> <item> - <widget class="QGroupBox" name="accountSettingsBox"> - <property name="title"> - <string>Account Settings</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QCheckBox" name="autoLoginCheckBox"> - <property name="text"> - <string>Login automatically when an instance icon is double clicked?</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> <spacer name="verticalSpacerMinecraft"> <property name="orientation"> <enum>Qt::Vertical</enum> diff --git a/logic/BaseInstance.cpp b/logic/BaseInstance.cpp index 6f8222b7..bc82fee1 100644 --- a/logic/BaseInstance.cpp +++ b/logic/BaseInstance.cpp @@ -85,11 +85,6 @@ BaseInstance::BaseInstance(BaseInstancePrivate *d_in, const QString &rootDir, settings().registerSetting( new OverrideSetting("PermGen", globalSettings->getSetting("PermGen"))); - // Auto login - settings().registerSetting(new Setting("OverrideLogin", false)); - settings().registerSetting( - new OverrideSetting("AutoLogin", globalSettings->getSetting("AutoLogin"))); - // Console settings().registerSetting(new Setting("OverrideConsole", false)); settings().registerSetting( diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h index 93e57414..5f426676 100644 --- a/logic/BaseInstance.h +++ b/logic/BaseInstance.h @@ -149,7 +149,7 @@ public: */ virtual SettingsObject &settings() const; - /// returns a valid update task if update is needed, NULL otherwise + /// returns a valid update task virtual std::shared_ptr<Task> doUpdate(bool only_prepare) = 0; /// returns a valid minecraft process, ready for launch with the given account. diff --git a/logic/OneSixUpdate.cpp b/logic/OneSixUpdate.cpp index 66950fc4..696eeff0 100644 --- a/logic/OneSixUpdate.cpp +++ b/logic/OneSixUpdate.cpp @@ -54,11 +54,9 @@ void OneSixUpdate::executeTask() if (m_only_prepare) { - if (m_inst->shouldUpdate()) - { - emitFailed("Unable to update instance in offline mode."); - return; - } + /* + * FIXME: in offline mode, do not proceed! + */ setStatus("Testing the Java installation."); QString java_path = m_inst->settings().get("JavaPath").toString(); @@ -243,6 +241,7 @@ void OneSixUpdate::assetIndexFinished() auto objectDL = MD5EtagDownload::make( QUrl("http://" + URLConstants::RESOURCE_BASE + objectName), objectFile.filePath()); + objectDL->m_total_progress = object.size; dls.append(objectDL); } } diff --git a/logic/auth/MojangAccountList.cpp b/logic/auth/MojangAccountList.cpp index 33990662..70bc0cf2 100644 --- a/logic/auth/MojangAccountList.cpp +++ b/logic/auth/MojangAccountList.cpp @@ -27,6 +27,7 @@ #include "logger/QsLog.h" #include "logic/auth/MojangAccount.h" +#include <pathutils.h> #define ACCOUNT_LIST_FORMAT_VERSION 2 @@ -265,11 +266,6 @@ bool MojangAccountList::loadList(const QString &filePath) return false; } - if (!QDir::current().exists(path)) - { - QDir::current().mkpath(path); - } - QFile file(path); // Try to open the file and fail if we can't. @@ -351,9 +347,16 @@ bool MojangAccountList::saveList(const QString &filePath) return false; } - if (!QDir::current().exists(path)) + // make sure the parent folder exists + if(!ensureFilePathExists(path)) + return false; + + // make sure the file wasn't overwritten with a folder before (fixes a bug) + QFileInfo finfo(path); + if(finfo.isDir()) { - QDir::current().mkpath(path); + QDir badDir(path); + badDir.removeRecursively(); } QLOG_INFO() << "Writing account list to" << path; @@ -411,3 +414,13 @@ void MojangAccountList::setListFilePath(QString path, bool autosave) m_listFilePath = path; m_autosave = autosave; } + +bool MojangAccountList::anyAccountIsValid() +{ + for(auto account:m_accounts) + { + if(account->accountStatus() != NotVerified) + return true; + } + return false; +} diff --git a/logic/auth/MojangAccountList.h b/logic/auth/MojangAccountList.h index c7e30958..6f4fbb17 100644 --- a/logic/auth/MojangAccountList.h +++ b/logic/auth/MojangAccountList.h @@ -126,6 +126,11 @@ public: * If the username given is an empty string, sets the active account to nothing. */ virtual void setActiveAccount(const QString &username); + + /*! + * Returns true if any of the account is at least Validated + */ + bool anyAccountIsValid(); signals: /*! diff --git a/logic/lists/InstanceList.cpp b/logic/lists/InstanceList.cpp index 94481fb9..15fd10ba 100644 --- a/logic/lists/InstanceList.cpp +++ b/logic/lists/InstanceList.cpp @@ -117,6 +117,11 @@ void InstanceList::groupChanged() saveGroupList(); } +QStringList InstanceList::getGroups() +{ + return m_groups.toList(); +} + void InstanceList::saveGroupList() { QString groupFileName = m_instDir + "/instgroups.json"; @@ -126,7 +131,7 @@ void InstanceList::saveGroupList() if (!groupFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { // An error occurred. Ignore it. - QLOG_ERROR() << "Failed to read instance group file."; + QLOG_ERROR() << "Failed to save instance group file."; return; } QTextStream out(&groupFile); @@ -137,6 +142,10 @@ void InstanceList::saveGroupList() QString group = instance->group(); if (group.isEmpty()) continue; + + // keep a list/set of groups for choosing + m_groups.insert(group); + if (!groupMap.count(group)) { QSet<QString> set; @@ -253,6 +262,9 @@ void InstanceList::loadGroupList(QMap<QString, QString> &groupMap) continue; } + // keep a list/set of groups for choosing + m_groups.insert(groupName); + // Iterate through the list of instances in the group. QJsonArray instancesArray = groupObj.value("instances").toArray(); diff --git a/logic/lists/InstanceList.h b/logic/lists/InstanceList.h index c3bb74cd..f23b7763 100644 --- a/logic/lists/InstanceList.h +++ b/logic/lists/InstanceList.h @@ -17,6 +17,7 @@ #include <QObject> #include <QAbstractListModel> +#include <QSet> #include "categorizedsortfilterproxymodel.h" #include <QIcon> @@ -97,6 +98,9 @@ public: InstancePtr getInstanceById(QString id) const; QModelIndex getInstanceIndexById(const QString &id) const; + + // FIXME: instead of iterating through all instances and forming a set, keep the set around + QStringList getGroups(); signals: void dataIsInvalid(); @@ -116,6 +120,7 @@ private: protected: QString m_instDir; QList<InstancePtr> m_instances; + QSet<QString> m_groups; }; class InstanceProxyModel : public KCategorizedSortFilterProxyModel diff --git a/logic/net/ByteArrayDownload.cpp b/logic/net/ByteArrayDownload.cpp index af5af8e9..27d2a250 100644 --- a/logic/net/ByteArrayDownload.cpp +++ b/logic/net/ByteArrayDownload.cpp @@ -42,7 +42,9 @@ void ByteArrayDownload::start() void ByteArrayDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - emit progress(index_within_job, bytesReceived, bytesTotal); + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit progress(m_index_within_job, bytesReceived, bytesTotal); } void ByteArrayDownload::downloadError(QNetworkReply::NetworkError error) @@ -62,14 +64,14 @@ void ByteArrayDownload::downloadFinished() m_status = Job_Finished; m_data = m_reply->readAll(); m_reply.reset(); - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); return; } // else the download failed else { m_reply.reset(); - emit failed(index_within_job); + emit failed(m_index_within_job); return; } } diff --git a/logic/net/CacheDownload.cpp b/logic/net/CacheDownload.cpp index 873d3a2e..6eadae39 100644 --- a/logic/net/CacheDownload.cpp +++ b/logic/net/CacheDownload.cpp @@ -35,14 +35,14 @@ void CacheDownload::start() { if (!m_entry->stale) { - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); return; } m_output_file.setFileName(m_target_path); // if there already is a file and md5 checking is in effect and it can be opened if (!ensureFilePathExists(m_target_path)) { - emit failed(index_within_job); + emit failed(m_index_within_job); return; } QLOG_INFO() << "Downloading " << m_url.toString(); @@ -69,7 +69,9 @@ void CacheDownload::start() void CacheDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - emit progress(index_within_job, bytesReceived, bytesTotal); + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit progress(m_index_within_job, bytesReceived, bytesTotal); } void CacheDownload::downloadError(QNetworkReply::NetworkError error) @@ -116,7 +118,7 @@ void CacheDownload::downloadFinished() MMC->metacache()->updateEntry(m_entry); m_reply.reset(); - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); return; } // else the download failed @@ -125,7 +127,7 @@ void CacheDownload::downloadFinished() m_output_file.close(); m_output_file.remove(); m_reply.reset(); - emit failed(index_within_job); + emit failed(m_index_within_job); return; } } @@ -140,7 +142,7 @@ void CacheDownload::downloadReadyRead() * Can't open the file... the job failed */ m_reply->abort(); - emit failed(index_within_job); + emit failed(m_index_within_job); return; } } diff --git a/logic/net/ForgeMirrors.cpp b/logic/net/ForgeMirrors.cpp index fd7eccca..b224306f 100644 --- a/logic/net/ForgeMirrors.cpp +++ b/logic/net/ForgeMirrors.cpp @@ -68,7 +68,7 @@ void ForgeMirrors::deferToFixedList() "https://www.creeperhost.net/link.php?id=1", "http://new.creeperrepo.net/forge/maven/"}); injectDownloads(); - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); } void ForgeMirrors::parseMirrorList() @@ -88,7 +88,7 @@ void ForgeMirrors::parseMirrorList() if(!m_mirrors.size()) deferToFixedList(); injectDownloads(); - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); } void ForgeMirrors::injectDownloads() @@ -108,7 +108,9 @@ void ForgeMirrors::injectDownloads() void ForgeMirrors::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - emit progress(index_within_job, bytesReceived, bytesTotal); + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit progress(m_index_within_job, bytesReceived, bytesTotal); } void ForgeMirrors::downloadReadyRead() diff --git a/logic/net/ForgeXzDownload.cpp b/logic/net/ForgeXzDownload.cpp index f119878a..1771d304 100644 --- a/logic/net/ForgeXzDownload.cpp +++ b/logic/net/ForgeXzDownload.cpp @@ -44,20 +44,20 @@ void ForgeXzDownload::start() if (!m_entry->stale) { m_status = Job_Finished; - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); return; } // can we actually create the real, final file? if (!ensureFilePathExists(m_target_path)) { m_status = Job_Failed; - emit failed(index_within_job); + emit failed(m_index_within_job); return; } if (m_mirrors.empty()) { m_status = Job_Failed; - emit failed(index_within_job); + emit failed(m_index_within_job); return; } @@ -80,7 +80,9 @@ void ForgeXzDownload::start() void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - emit progress(index_within_job, bytesReceived, bytesTotal); + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit progress(m_index_within_job, bytesReceived, bytesTotal); } void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error) @@ -100,7 +102,7 @@ void ForgeXzDownload::failAndTryNextMirror() m_mirror_index = next; updateUrl(); - emit failed(index_within_job); + emit failed(m_index_within_job); } void ForgeXzDownload::updateUrl() @@ -148,7 +150,7 @@ void ForgeXzDownload::downloadFinished() m_status = Job_Failed; m_pack200_xz_file.remove(); m_reply.reset(); - emit failed(index_within_job); + emit failed(m_index_within_job); return; } } @@ -175,7 +177,7 @@ void ForgeXzDownload::downloadReadyRead() * Can't open the file... the job failed */ m_reply->abort(); - emit failed(index_within_job); + emit failed(m_index_within_job); return; } } @@ -347,5 +349,5 @@ void ForgeXzDownload::decompressAndInstall() MMC->metacache()->updateEntry(m_entry); m_reply.reset(); - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); } diff --git a/logic/net/MD5EtagDownload.cpp b/logic/net/MD5EtagDownload.cpp index 4b9f52af..435e854e 100644 --- a/logic/net/MD5EtagDownload.cpp +++ b/logic/net/MD5EtagDownload.cpp @@ -44,7 +44,7 @@ void MD5EtagDownload::start() if (m_check_md5 && hash == m_expected_md5) { QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match."; - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); return; } else @@ -54,7 +54,7 @@ void MD5EtagDownload::start() } if (!ensureFilePathExists(filename)) { - emit failed(index_within_job); + emit failed(m_index_within_job); return; } @@ -68,7 +68,7 @@ void MD5EtagDownload::start() // Plus, this way, we don't end up starting a download for a file we can't open. if (!m_output_file.open(QIODevice::WriteOnly)) { - emit failed(index_within_job); + emit failed(m_index_within_job); return; } @@ -86,7 +86,9 @@ void MD5EtagDownload::start() void MD5EtagDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - emit progress(index_within_job, bytesReceived, bytesTotal); + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit progress(m_index_within_job, bytesReceived, bytesTotal); } void MD5EtagDownload::downloadError(QNetworkReply::NetworkError error) @@ -107,7 +109,7 @@ void MD5EtagDownload::downloadFinished() QLOG_INFO() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData(); m_reply.reset(); - emit succeeded(index_within_job); + emit succeeded(m_index_within_job); return; } // else the download failed @@ -115,7 +117,7 @@ void MD5EtagDownload::downloadFinished() { m_output_file.close(); m_reply.reset(); - emit failed(index_within_job); + emit failed(m_index_within_job); return; } } @@ -130,7 +132,7 @@ void MD5EtagDownload::downloadReadyRead() * Can't open the file... the job failed */ m_reply->abort(); - emit failed(index_within_job); + emit failed(m_index_within_job); return; } } diff --git a/logic/net/NetAction.h b/logic/net/NetAction.h index c96d8f8f..97c96e5d 100644 --- a/logic/net/NetAction.h +++ b/logic/net/NetAction.h @@ -39,6 +39,19 @@ public: virtual ~NetAction() {}; public: + virtual qint64 totalProgress() const + { + return m_total_progress; + } + virtual qint64 currentProgress() const + { + return m_progress; + } + virtual qint64 numberOfFailures() const + { + return m_failures; + } +public: /// the network reply std::shared_ptr<QNetworkReply> m_reply; @@ -46,10 +59,16 @@ public: QUrl m_url; /// The file's status - JobStatus m_status; + JobStatus m_status = Job_NotStarted; /// index within the parent job - int index_within_job = 0; + int m_index_within_job = 0; + + qint64 m_progress = 0; + qint64 m_total_progress = 1; + + /// number of failures up to this point + int m_failures = 0; signals: void started(int index); diff --git a/logic/net/NetJob.h b/logic/net/NetJob.h index 6e2e7607..68c4c408 100644 --- a/logic/net/NetJob.h +++ b/logic/net/NetJob.h @@ -36,10 +36,16 @@ public: template <typename T> bool addNetAction(T action) { NetActionPtr base = std::static_pointer_cast<NetAction>(action); - base->index_within_job = downloads.size(); + base->m_index_within_job = downloads.size(); downloads.append(action); - parts_progress.append(part_info()); - total_progress++; + part_info pi; + { + pi.current_progress = base->currentProgress(); + pi.total_progress = base->totalProgress(); + pi.failures = base->numberOfFailures(); + } + parts_progress.append(pi); + total_progress += pi.total_progress; // if this is already running, the action needs to be started right away! if (isRunning()) { diff --git a/logic/net/PasteUpload.cpp b/logic/net/PasteUpload.cpp index acf09291..fa54d084 100644 --- a/logic/net/PasteUpload.cpp +++ b/logic/net/PasteUpload.cpp @@ -78,7 +78,9 @@ bool PasteUpload::parseResult(QJsonDocument doc, QString *parseError) parseError = new QString(object.value("error").toString()); return false; } + // FIXME: not the place for GUI things. QString pasteUrl = object.value("paste").toObject().value("link").toString(); QDesktopServices::openUrl(pasteUrl); return true; } + diff --git a/logic/net/S3ListBucket.cpp b/logic/net/S3ListBucket.cpp deleted file mode 100644 index 439b7086..00000000 --- a/logic/net/S3ListBucket.cpp +++ /dev/null @@ -1,175 +0,0 @@ -/* Copyright 2013 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "S3ListBucket.h" -#include "MultiMC.h" -#include "logger/QsLog.h" -#include <QUrlQuery> -#include <qxmlstream.h> -#include <QDomDocument> - -inline QDomElement getDomElementByTagName(QDomElement parent, QString tagname) -{ - QDomNodeList elementList = parent.elementsByTagName(tagname); - if (elementList.count()) - return elementList.at(0).toElement(); - else - return QDomElement(); -} - -S3ListBucket::S3ListBucket(QUrl url) : NetAction() -{ - m_url = url; - m_status = Job_NotStarted; -} - -void S3ListBucket::start() -{ - QUrl finalUrl = m_url; - if (current_marker.size()) - { - QUrlQuery query; - query.addQueryItem("marker", current_marker); - finalUrl.setQuery(query); - } - QNetworkRequest request(finalUrl); - request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); - auto worker = MMC->qnam(); - QNetworkReply *rep = worker->get(request); - - m_reply = std::shared_ptr<QNetworkReply>(rep); - connect(rep, SIGNAL(downloadProgress(qint64, qint64)), - SLOT(downloadProgress(qint64, qint64))); - connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), - SLOT(downloadError(QNetworkReply::NetworkError))); - connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); -} - -void S3ListBucket::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - emit progress(index_within_job, bytesSoFar + bytesReceived, bytesSoFar + bytesTotal); -} - -void S3ListBucket::downloadError(QNetworkReply::NetworkError error) -{ - // error happened during download. - QLOG_ERROR() << "Error getting URL:" << m_url.toString().toLocal8Bit() - << "Network error: " << error; - m_status = Job_Failed; -} - -void S3ListBucket::processValidReply() -{ - QLOG_TRACE() << "GOT: " << m_url.toString() << " marker:" << current_marker; - auto readContents = [&](QXmlStreamReader & xml) - { - QString Key, ETag, Size; - while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == "Contents")) - { - if (xml.tokenType() == QXmlStreamReader::StartElement) - { - if (xml.name() == "Key") - { - Key = xml.readElementText(); - } - if (xml.name() == "ETag") - { - ETag = xml.readElementText(); - } - if (xml.name() == "Size") - { - Size = xml.readElementText(); - } - } - xml.readNext(); - } - if (xml.error() != QXmlStreamReader::NoError) - return; - objects.append({Key, ETag, Size.toLongLong()}); - }; - - // nothing went wrong... - QByteArray ba = m_reply->readAll(); - - QString xmlErrorMsg; - - bool is_truncated = false; - QXmlStreamReader xml(ba); - while (!xml.atEnd() && !xml.hasError()) - { - /* Read next element.*/ - QXmlStreamReader::TokenType token = xml.readNext(); - /* If token is just StartDocument, we'll go to next.*/ - if (token == QXmlStreamReader::StartDocument) - { - continue; - } - if (token == QXmlStreamReader::StartElement) - { - /* If it's named person, we'll dig the information from there.*/ - if (xml.name() == "Contents") - { - readContents(xml); - } - else if (xml.name() == "IsTruncated") - { - is_truncated = (xml.readElementText() == "true"); - } - } - } - if (xml.hasError()) - { - QLOG_ERROR() << "Failed to process s3.amazonaws.com/Minecraft.Resources. XML error:" - << xml.errorString() << ba; - emit failed(index_within_job); - return; - } - if (is_truncated) - { - current_marker = objects.last().Key; - bytesSoFar += m_reply->size(); - m_reply.reset(); - start(); - } - else - { - m_status = Job_Finished; - m_reply.reset(); - emit succeeded(index_within_job); - } - return; -} - -void S3ListBucket::downloadFinished() -{ - // if the download succeeded - if (m_status != Job_Failed) - { - processValidReply(); - } - // else the download failed - else - { - m_reply.reset(); - emit failed(index_within_job); - return; - } -} - -void S3ListBucket::downloadReadyRead() -{ - // ~_~ -} diff --git a/logic/net/S3ListBucket.h b/logic/net/S3ListBucket.h deleted file mode 100644 index e7c5e05c..00000000 --- a/logic/net/S3ListBucket.h +++ /dev/null @@ -1,57 +0,0 @@ -/* Copyright 2013 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once -#include "NetAction.h" - -struct S3Object -{ - QString Key; - QString ETag; - qlonglong size; -}; - -typedef std::shared_ptr<class S3ListBucket> S3ListBucketPtr; -class S3ListBucket : public NetAction -{ - Q_OBJECT -public: - S3ListBucket(QUrl url); - static S3ListBucketPtr make(QUrl url) - { - return S3ListBucketPtr(new S3ListBucket(url)); - } - -public: - QList<S3Object> objects; - -public -slots: - virtual void start() override; - -protected -slots: - virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; - virtual void downloadError(QNetworkReply::NetworkError error) override; - virtual void downloadFinished() override; - virtual void downloadReadyRead() override; - -private: - void processValidReply(); - -private: - qint64 bytesSoFar = 0; - QString current_marker; -}; diff --git a/logic/updater/DownloadUpdateTask.cpp b/logic/updater/DownloadUpdateTask.cpp index d9aab826..d72cfcf6 100644 --- a/logic/updater/DownloadUpdateTask.cpp +++ b/logic/updater/DownloadUpdateTask.cpp @@ -45,55 +45,54 @@ void DownloadUpdateTask::executeTask() findCurrentVersionInfo(); } -void DownloadUpdateTask::findCurrentVersionInfo() +void DownloadUpdateTask::processChannels() { - setStatus(tr("Finding information about the current version.")); - auto checker = MMC->updateChecker(); - // This runs after we've tried loading the channel list. - // If the channel list doesn't need to be loaded, this will be called immediately. - // If the channel list does need to be loaded, this will be called when it's done. - auto processFunc = [this, &checker] () -> void + // Now, check the channel list again. + if (!checker->hasChannels()) { - // Now, check the channel list again. - if (checker->hasChannels()) - { - // We still couldn't load the channel list. Give up. Call loadVersionInfo and return. - QLOG_INFO() << "Reloading the channel list didn't work. Giving up."; - loadVersionInfo(); - return; - } + // We still couldn't load the channel list. Give up. Call loadVersionInfo and return. + QLOG_INFO() << "Reloading the channel list didn't work. Giving up."; + loadVersionInfo(); + return; + } - QList<UpdateChecker::ChannelListEntry> channels = checker->getChannelList(); - QString channelId = MMC->version().channel; + QList<UpdateChecker::ChannelListEntry> channels = checker->getChannelList(); + QString channelId = MMC->version().channel; - // Search through the channel list for a channel with the correct ID. - for (auto channel : channels) + // Search through the channel list for a channel with the correct ID. + for (auto channel : channels) + { + if (channel.id == channelId) { - if (channel.id == channelId) - { - QLOG_INFO() << "Found matching channel."; - m_cRepoUrl = channel.url; - break; - } + QLOG_INFO() << "Found matching channel."; + m_cRepoUrl = preparePath(channel.url); + break; } + } - // Now that we've done that, load version info. - loadVersionInfo(); - }; + // Now that we've done that, load version info. + loadVersionInfo(); +} + +void DownloadUpdateTask::findCurrentVersionInfo() +{ + setStatus(tr("Finding information about the current version.")); - if (checker->hasChannels()) + auto checker = MMC->updateChecker(); + + if (!checker->hasChannels()) { // Load the channel list and wait for it to finish loading. QLOG_INFO() << "No channel list entries found. Will try reloading it."; - QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, processFunc); + QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, this, &DownloadUpdateTask::processChannels); checker->updateChanList(); } else { - processFunc(); + processChannels(); } } @@ -152,12 +151,24 @@ void DownloadUpdateTask::parseDownloadedVersionInfo() { setStatus(tr("Reading file lists.")); - parseVersionInfo(NEW_VERSION, &m_nVersionFileList); + setStatus(tr("Reading file list for new version.")); + QLOG_DEBUG() << "Reading file list for new version."; + QString error; + if (!parseVersionInfo(std::dynamic_pointer_cast<ByteArrayDownload>( + m_vinfoNetJob->first())->m_data, &m_nVersionFileList, &error)) + { + emitFailed(error); + return; + } // If there is a second entry in the network job's list, load it as the current version's info. if (m_vinfoNetJob->size() >= 2 && m_vinfoNetJob->operator[](1)->m_status != Job_Failed) { - parseVersionInfo(CURRENT_VERSION, &m_cVersionFileList); + setStatus(tr("Reading file list for current version.")); + QLOG_DEBUG() << "Reading file list for current version."; + QString error; + parseVersionInfo(std::dynamic_pointer_cast<ByteArrayDownload>( + m_vinfoNetJob->operator[](1))->m_data, &m_cVersionFileList, &error); } // We don't need this any more. @@ -167,26 +178,15 @@ void DownloadUpdateTask::parseDownloadedVersionInfo() processFileLists(); } -void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list) +bool DownloadUpdateTask::parseVersionInfo(const QByteArray &data, VersionFileList* list, QString *error) { - if (vfile == CURRENT_VERSION) setStatus(tr("Reading file list for current version.")); - else if (vfile == NEW_VERSION) setStatus(tr("Reading file list for new version.")); - - QLOG_DEBUG() << "Reading file list for" << (vfile == NEW_VERSION ? "new" : "current") << "version."; - - QByteArray data; - { - ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>( - vfile == NEW_VERSION ? m_vinfoNetJob->first() : m_vinfoNetJob->operator[](1)); - data = dl->m_data; - } - QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error != QJsonParseError::NoError) { - QLOG_ERROR() << "Failed to parse version info JSON:" << jsonError.errorString() << "at" << jsonError.offset; - return; + *error = QString("Failed to parse version info JSON: %1 at %2").arg(jsonError.errorString()).arg(jsonError.offset); + QLOG_ERROR() << error; + return false; } QJsonObject json = jsonDoc.object(); @@ -213,11 +213,11 @@ void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFile QString type = sourceObj.value("SourceType").toString(); if (type == "http") { - file.sources.append(FileSource("http", sourceObj.value("Url").toString())); + file.sources.append(FileSource("http", preparePath(sourceObj.value("Url").toString()))); } else if (type == "httpc") { - file.sources.append(FileSource("httpc", sourceObj.value("Url").toString(), sourceObj.value("CompressionType").toString())); + file.sources.append(FileSource("httpc", preparePath(sourceObj.value("Url").toString()), sourceObj.value("CompressionType").toString())); } else { @@ -229,18 +229,41 @@ void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFile list->append(file); } + + return true; } void DownloadUpdateTask::processFileLists() { + // Create a network job for downloading files. + NetJob* netJob = new NetJob("Update Files"); + + processFileLists(netJob, m_cVersionFileList, m_nVersionFileList, m_operationList); + + // Add listeners to wait for the downloads to finish. + QObject::connect(netJob, &NetJob::succeeded, this, &DownloadUpdateTask::fileDownloadFinished); + QObject::connect(netJob, &NetJob::progress, this, &DownloadUpdateTask::fileDownloadProgressChanged); + QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::fileDownloadFailed); + + // Now start the download. + setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size()))); + QLOG_DEBUG() << "Begin downloading update files to" << m_updateFilesDir.path(); + m_filesNetJob.reset(netJob); + netJob->start(); + + writeInstallScript(m_operationList, PathCombine(m_updateFilesDir.path(), "file_list.xml")); +} + +void DownloadUpdateTask::processFileLists(NetJob *job, const VersionFileList ¤tVersion, const VersionFileList &newVersion, DownloadUpdateTask::UpdateOperationList &ops) +{ setStatus(tr("Processing file lists. Figuring out how to install the update.")); // First, if we've loaded the current version's file list, we need to iterate through it and // delete anything in the current one version's list that isn't in the new version's list. - for (VersionFileEntry entry : m_cVersionFileList) + for (VersionFileEntry entry : currentVersion) { bool keep = false; - for (VersionFileEntry newEntry : m_nVersionFileList) + for (VersionFileEntry newEntry : newVersion) { if (newEntry.path == entry.path) { @@ -251,14 +274,11 @@ void DownloadUpdateTask::processFileLists() } // If the loop reaches the end and we didn't find a match, delete the file. if(!keep) - m_operationList.append(UpdateOperation::DeleteOp(entry.path)); + ops.append(UpdateOperation::DeleteOp(entry.path)); } - // Create a network job for downloading files. - NetJob* netJob = new NetJob("Update Files"); - // Next, check each file in MultiMC's folder and see if we need to update them. - for (VersionFileEntry entry : m_nVersionFileList) + for (VersionFileEntry entry : newVersion) { // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a way to do this in the background. QString fileMD5; @@ -285,34 +305,24 @@ void DownloadUpdateTask::processFileLists() // Download it to updatedir/<filepath>-<md5> where filepath is the file's path with slashes replaced by underscores. QString dlPath = PathCombine(m_updateFilesDir.path(), QString(entry.path).replace("/", "_")); - // We need to download the file to the updatefiles folder and add a task to copy it to its install path. - auto download = MD5EtagDownload::make(source.url, dlPath); - download->m_check_md5 = true; - download->m_expected_md5 = entry.md5; - netJob->addNetAction(download); + if (job) + { + // We need to download the file to the updatefiles folder and add a task to copy it to its install path. + auto download = MD5EtagDownload::make(source.url, dlPath); + download->m_check_md5 = true; + download->m_expected_md5 = entry.md5; + job->addNetAction(download); + } // Now add a copy operation to our operations list to install the file. - m_operationList.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode)); + ops.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode)); } } } } - - // Add listeners to wait for the downloads to finish. - QObject::connect(netJob, &NetJob::succeeded, this, &DownloadUpdateTask::fileDownloadFinished); - QObject::connect(netJob, &NetJob::progress, this, &DownloadUpdateTask::fileDownloadProgressChanged); - QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::fileDownloadFailed); - - // Now start the download. - setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size()))); - QLOG_DEBUG() << "Begin downloading update files to" << m_updateFilesDir.path(); - m_filesNetJob.reset(netJob); - netJob->start(); - - writeInstallScript(m_operationList, PathCombine(m_updateFilesDir.path(), "file_list.xml")); } -void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QString scriptFile) +bool DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QString scriptFile) { // Build the base structure of the XML document. QDomDocument doc; @@ -377,7 +387,15 @@ void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QStrin else { emitFailed(tr("Failed to write update script file.")); + return false; } + + return true; +} + +QString DownloadUpdateTask::preparePath(const QString &path) +{ + return QString(path).replace("$PWD", qApp->applicationDirPath()); } void DownloadUpdateTask::fileDownloadFinished() diff --git a/logic/updater/DownloadUpdateTask.h b/logic/updater/DownloadUpdateTask.h index f5b23d12..8530be77 100644 --- a/logic/updater/DownloadUpdateTask.h +++ b/logic/updater/DownloadUpdateTask.h @@ -34,7 +34,8 @@ public: */ QString updateFilesDir(); -protected: +public: + // TODO: We should probably put these data structures into a separate header... /*! @@ -53,7 +54,6 @@ protected: QString url; QString compressionType; }; - typedef QList<FileSource> FileSourceList; /*! @@ -66,10 +66,8 @@ protected: FileSourceList sources; QString md5; }; - typedef QList<VersionFileEntry> VersionFileList; - /*! * Structure that describes an operation to perform when installing updates. */ @@ -100,9 +98,12 @@ protected: // Yeah yeah, polymorphism blah blah inheritance, blah blah object oriented. I'm lazy, OK? }; - typedef QList<UpdateOperation> UpdateOperationList; +protected: + friend class DownloadUpdateTaskTest; + + /*! * Used for arguments to parseVersionInfo and friends to specify which version info file to parse. */ @@ -120,6 +121,13 @@ protected: virtual void findCurrentVersionInfo(); /*! + * This runs after we've tried loading the channel list. + * If the channel list doesn't need to be loaded, this will be called immediately. + * If the channel list does need to be loaded, this will be called when it's done. + */ + void processChannels(); + + /*! * Downloads the version info files from the repository. * The files for both the current build, and the build that we're updating to need to be downloaded. * If the current version's info file can't be found, MultiMC will not delete files that @@ -142,20 +150,25 @@ protected: /*! * Loads the file list from the given version info JSON object into the given list. */ - virtual void parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list); + virtual bool parseVersionInfo(const QByteArray &data, VersionFileList* list, QString *error); /*! * Takes a list of file entries for the current version's files and the new version's files * and populates the downloadList and operationList with information about how to download and install the update. */ + virtual void processFileLists(NetJob *job, const VersionFileList ¤tVersion, const VersionFileList &newVersion, UpdateOperationList &ops); + + /*! + * Calls \see processFileLists to populate the \see m_operationList and a NetJob, and then executes + * the NetJob to fetch all needed files + */ virtual void processFileLists(); /*! * Takes the operations list and writes an install script for the updater to the update files directory. */ - virtual void writeInstallScript(UpdateOperationList& opsList, QString scriptFile); + virtual bool writeInstallScript(UpdateOperationList& opsList, QString scriptFile); - VersionFileList m_downloadList; UpdateOperationList m_operationList; VersionFileList m_nVersionFileList; @@ -181,6 +194,11 @@ protected: */ QTemporaryDir m_updateFilesDir; + /*! + * Substitutes $PWD for the application directory + */ + static QString preparePath(const QString &path); + protected slots: void vinfoDownloadFinished(); void vinfoDownloadFailed(); diff --git a/logic/updater/UpdateChecker.cpp b/logic/updater/UpdateChecker.cpp index 5ff1898e..af56288c 100644 --- a/logic/updater/UpdateChecker.cpp +++ b/logic/updater/UpdateChecker.cpp @@ -44,7 +44,7 @@ QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const bool UpdateChecker::hasChannels() const { - return m_channels.isEmpty(); + return !m_channels.isEmpty(); } void UpdateChecker::checkForUpdate() diff --git a/logic/updater/UpdateChecker.h b/logic/updater/UpdateChecker.h index 59fb8e47..5b7efc05 100644 --- a/logic/updater/UpdateChecker.h +++ b/logic/updater/UpdateChecker.h @@ -27,6 +27,9 @@ public: UpdateChecker(); void checkForUpdate(); + void setCurrentChannel(const QString &channel) { m_currentChannel = channel; } + void setChannelListUrl(const QString &url) { m_channelListUrl = url; } + /*! * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake). * If this isn't called before checkForUpdate(), it will automatically be called. @@ -70,6 +73,8 @@ private slots: void chanListDownloadFailed(); private: + friend class UpdateCheckerTest; + NetJobPtr indexJob; NetJobPtr chanListJob; diff --git a/mmc_updater/CMakeLists.txt b/mmc_updater/CMakeLists.txt index 61c8cd09..971ac153 100644 --- a/mmc_updater/CMakeLists.txt +++ b/mmc_updater/CMakeLists.txt @@ -9,6 +9,14 @@ include_directories(depends) if (WIN32) include_directories(depends/win32cpp) + # static all the things. The updater must have no dependencies, or it will fail. + if (MINGW) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static-libgcc -static") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libgcc -static-libstdc++ -static") +#set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "${CMAKE_SHARED_LIBRARY_LINK_C_FLAGS} -static-libgcc -s") +#set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "${CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS} -static-libgcc -static-libstdc++ -s") + endif() + if(MSVC) # - Link the updater binary statically with the Visual C++ runtime # so that the executable can function standalone. diff --git a/mmc_updater/src/UpdateScript.h b/mmc_updater/src/UpdateScript.h index c825e35d..5c863ff4 100644 --- a/mmc_updater/src/UpdateScript.h +++ b/mmc_updater/src/UpdateScript.h @@ -56,9 +56,7 @@ class UpdateScriptFile } }; -/** Stores information about the packages and files included - * in an update, parsed from an XML file. - */ +/** Stores information about the files included in an update, parsed from an XML file. */ class UpdateScript { public: diff --git a/mmc_updater/src/UpdaterOptions.cpp b/mmc_updater/src/UpdaterOptions.cpp index ae34562d..0945431b 100644 --- a/mmc_updater/src/UpdaterOptions.cpp +++ b/mmc_updater/src/UpdaterOptions.cpp @@ -142,7 +142,7 @@ void UpdaterOptions::parse(int argc, char** argv) showVersion = parser.getFlag("version"); forceElevated = parser.getFlag("force-elevated"); autoClose = parser.getFlag("auto-close"); - + if (installDir.empty()) { // if no --install-dir argument is present, try parsing @@ -152,3 +152,4 @@ void UpdaterOptions::parse(int argc, char** argv) } } + diff --git a/mmc_updater/src/resources/updater.manifest b/mmc_updater/src/resources/updater.manifest new file mode 100644 index 00000000..cafc47d3 --- /dev/null +++ b/mmc_updater/src/resources/updater.manifest @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> + <assemblyIdentity name="MultiMC.Updater.1" type="win32" version="1.0.0.0" /> + <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> + <security> + <requestedPrivileges> + <requestedExecutionLevel level="asInvoker" uiAccess="false"/> + </requestedPrivileges> + </security> + </trustInfo> + <dependency> + <dependentAssembly> + <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="x86" publicKeyToken="6595b64144ccf1df" language="*"/> + </dependentAssembly> + </dependency> + <description>Software updater for MultiMC.</description> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!--The ID below indicates app support for Windows Vista --> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + <!--The ID below indicates app support for Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + <!--The ID below indicates app support for Windows Developer Preview / Windows 8 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + </application> + </compatibility> +</assembly>
\ No newline at end of file diff --git a/mmc_updater/src/resources/updater.rc b/mmc_updater/src/resources/updater.rc index 550970a8..9c7c5711 100644 --- a/mmc_updater/src/resources/updater.rc +++ b/mmc_updater/src/resources/updater.rc @@ -1,30 +1,30 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include <windows.h> + IDI_APPICON ICON DISCARDABLE "updater.ico" -1 VERSIONINFO -FILEVERSION 0,0,1,0 -PRODUCTVERSION 1,0,1,0 -FILEFLAGSMASK 0X3FL -FILEFLAGS 0X8L -FILEOS 0X40004L -FILETYPE 0X1 -FILESUBTYPE 0 +1 RT_MANIFEST "updater.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "000004b0" BEGIN - VALUE "FileVersion", "0.0.1.0" - VALUE "ProductVersion", "1.0.1.0" - VALUE "OriginalFilename", "updater.exe" - VALUE "InternalName", "updater.exe" - VALUE "FileDescription", "Software Update Tool" VALUE "CompanyName", "MultiMC Contributors" + VALUE "FileDescription", "Software Update Tool" + VALUE "FileVersion", "1.0.0.0" VALUE "ProductName", "MultiMC Software Updater" - VALUE "PrivateBuild", "Built by BuildBot" + VALUE "ProductVersion", "1.0" END END BLOCK "VarFileInfo" BEGIN - VALUE "Translation", 0x0000, 0x04b0 + VALUE "Translation", 0x0000, 0x04b0 // Unicode END END
\ No newline at end of file diff --git a/mmc_updater/src/tests/CMakeLists.txt b/mmc_updater/src/tests/CMakeLists.txt index 5de9d096..1d62214e 100644 --- a/mmc_updater/src/tests/CMakeLists.txt +++ b/mmc_updater/src/tests/CMakeLists.txt @@ -5,21 +5,19 @@ if (APPLE) set(HELPER_SHARED_SOURCES ../StlSymbolsLeopard.cpp) endif() -# Create helper binaries for unit tests -add_executable(oldapp - old_app.cpp - ${HELPER_SHARED_SOURCES} -) -add_executable(newapp - new_app.cpp - ${HELPER_SHARED_SOURCES} -) +# # Create helper binaries for unit tests +# add_executable(oldapp +# old_app.cpp +# ${HELPER_SHARED_SOURCES} +# ) +# add_executable(newapp +# new_app.cpp +# ${HELPER_SHARED_SOURCES} +# ) # Install data files required by unit tests set(TEST_FILES file_list.xml - v2_file_list.xml - test-update.rb ) foreach(TEST_FILE ${TEST_FILES}) @@ -40,12 +38,6 @@ macro(ADD_UPDATER_TEST CLASS) endif() endmacro() -add_updater_test(TestUpdateScript) +add_updater_test(TestParseScript) add_updater_test(TestUpdaterOptions) add_updater_test(TestFileUtils) - -# Add updater that that performs a complete update install -# and checks the result -find_program(RUBY_BIN ruby) -add_test(updater_TestUpdateInstall ${RUBY_BIN} test-update.rb) - diff --git a/mmc_updater/src/tests/TestParseScript.cpp b/mmc_updater/src/tests/TestParseScript.cpp new file mode 100644 index 00000000..f4453957 --- /dev/null +++ b/mmc_updater/src/tests/TestParseScript.cpp @@ -0,0 +1,24 @@ +#include "TestParseScript.h" + +#include "TestUtils.h" +#include "UpdateScript.h" + +#include <iostream> +#include <algorithm> + +void TestParseScript::testParse() +{ + UpdateScript script; + + script.parse("file_list.xml"); + + TEST_COMPARE(script.isValid(),true); +} + +int main(int,char**) +{ + TestList<TestParseScript> tests; + tests.addTest(&TestParseScript::testParse); + return TestUtils::runTest(tests); +} + diff --git a/mmc_updater/src/tests/TestParseScript.h b/mmc_updater/src/tests/TestParseScript.h new file mode 100644 index 00000000..528e97a8 --- /dev/null +++ b/mmc_updater/src/tests/TestParseScript.h @@ -0,0 +1,8 @@ +#pragma once + +class TestParseScript +{ + public: + void testParse(); +}; + diff --git a/mmc_updater/src/tests/TestUpdateScript.cpp b/mmc_updater/src/tests/TestUpdateScript.cpp deleted file mode 100644 index 30a7572a..00000000 --- a/mmc_updater/src/tests/TestUpdateScript.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "TestUpdateScript.h" - -#include "TestUtils.h" -#include "UpdateScript.h" - -#include <iostream> -#include <algorithm> - -void TestUpdateScript::testV2Script() -{ - UpdateScript newFormat; - UpdateScript oldFormat; - - newFormat.parse("file_list.xml"); - oldFormat.parse("v2_file_list.xml"); - - TEST_COMPARE(newFormat.filesToInstall(),oldFormat.filesToInstall()); - TEST_COMPARE(newFormat.filesToUninstall(),oldFormat.filesToUninstall()); -} - -int main(int,char**) -{ - TestList<TestUpdateScript> tests; - tests.addTest(&TestUpdateScript::testV2Script); - return TestUtils::runTest(tests); -} - diff --git a/mmc_updater/src/tests/TestUpdateScript.h b/mmc_updater/src/tests/TestUpdateScript.h deleted file mode 100644 index 513513d5..00000000 --- a/mmc_updater/src/tests/TestUpdateScript.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -class TestUpdateScript -{ - public: - void testV2Script(); -}; - diff --git a/mmc_updater/src/tests/file_list.xml b/mmc_updater/src/tests/file_list.xml index dff4b54f..06ba501d 100644 --- a/mmc_updater/src/tests/file_list.xml +++ b/mmc_updater/src/tests/file_list.xml @@ -1,20 +1,5 @@ <?xml version="1.0"?> <update version="3"> - <targetVersion>2.0</targetVersion> - <platform>Test</platform> - <dependencies> - <!-- The new updater is standalone and has no dependencies, - except for standard system libraries. - !--> - </dependencies> - <packages> - <package> - <name>app-pkg</name> - <hash>$APP_PACKAGE_HASH</hash> - <size>$APP_PACKAGE_SIZE</size> - <source>http://some/dummy/URL</source> - </package> - </packages> <install> <file> <name>$APP_FILENAME</name> diff --git a/mmc_updater/src/tests/test-update.rb b/mmc_updater/src/tests/test-update.rb deleted file mode 100755 index 82965cf4..00000000 --- a/mmc_updater/src/tests/test-update.rb +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/ruby - -require 'fileutils.rb' -require 'find' -require 'rbconfig' -require 'optparse' - -# Install directory - this contains a space to check -# for correct escaping of paths when passing comamnd -# line arguments under Windows -INSTALL_DIR = File.expand_path("install dir/") -PACKAGE_DIR = File.expand_path("package-dir/") -PACKAGE_SRC_DIR = File.expand_path("package-src-dir/") -IS_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ - -if IS_WINDOWS - OLDAPP_NAME = "oldapp.exe" - NEWAPP_NAME = "newapp.exe" - APP_NAME = "app.exe" - UPDATER_NAME = "updater.exe" - ZIP_TOOL = File.expand_path("../zip-tool.exe") -else - OLDAPP_NAME = "oldapp" - NEWAPP_NAME = "newapp" - APP_NAME = "app" - UPDATER_NAME = "updater" - ZIP_TOOL = File.expand_path("../zip-tool") -end - -file_list_vars = { - "APP_FILENAME" => APP_NAME, - "UPDATER_FILENAME" => UPDATER_NAME -} - -def replace_vars(src_file,dest_file,vars) - content = File.read(src_file) - vars.each do |key,value| - content.gsub! "$#{key}",value - end - File.open(dest_file,'w') do |file| - file.print content - end -end - -# Returns true if |src_file| and |dest_file| have the same contents, type -# and permissions or false otherwise -def compare_files(src_file, dest_file) - if File.ftype(src_file) != File.ftype(dest_file) - $stderr.puts "Type of file #{src_file} and #{dest_file} differ" - return false - end - - if File.file?(src_file) && !FileUtils.identical?(src_file, dest_file) - $stderr.puts "Contents of file #{src_file} and #{dest_file} differ" - return false - end - - src_stat = File.stat(src_file) - dest_stat = File.stat(dest_file) - - if src_stat.mode != dest_stat.mode - $stderr.puts "Permissions of #{src_file} and #{dest_file} differ" - return false - end - - return true -end - -# Compares the contents of two directories and returns a map of (file path => change type) -# for files and directories which differ between the two -def compare_dirs(src_dir, dest_dir) - src_dir += '/' if !src_dir.end_with?('/') - dest_dir += '/' if !dest_dir.end_with?('/') - - src_file_map = {} - Find.find(src_dir) do |src_file| - src_file = src_file[src_dir.length..-1] - src_file_map[src_file] = nil - end - - change_map = {} - Find.find(dest_dir) do |dest_file| - dest_file = dest_file[dest_dir.length..-1] - - if !src_file_map.include?(dest_file) - change_map[dest_file] = :deleted - elsif !compare_files("#{src_dir}/#{dest_file}", "#{dest_dir}/#{dest_file}") - change_map[dest_file] = :updated - end - - src_file_map.delete(dest_file) - end - - src_file_map.each do |file| - change_map[file] = :added - end - - return change_map -end - -def create_test_file(name, content) - File.open(name, 'w') do |file| - file.puts content - end - return name -end - -force_elevation = false -run_in_debugger = false - -OptionParser.new do |parser| - parser.on("-f","--force-elevated","Force the updater to elevate itself") do - force_elevation = true - end - parser.on("-d","--debug","Run the updater under GDB") do - run_in_debugger = true - end -end.parse! - -# copy 'src' to 'dest', preserving the attributes -# of 'src' -def copy_file(src, dest) - FileUtils.cp src, dest, :preserve => true -end - -# Remove the install and package dirs if they -# already exist -FileUtils.rm_rf(INSTALL_DIR) -FileUtils.rm_rf(PACKAGE_DIR) -FileUtils.rm_rf(PACKAGE_SRC_DIR) - -# Create the install directory with the old app -Dir.mkdir(INSTALL_DIR) -copy_file OLDAPP_NAME, "#{INSTALL_DIR}/#{APP_NAME}" - -# Create a dummy file to uninstall -uninstall_test_file = create_test_file("#{INSTALL_DIR}/file-to-uninstall.txt", "this file should be removed after the update") -uninstall_test_symlink = if not IS_WINDOWS - FileUtils.ln_s("#{INSTALL_DIR}/file-to-uninstall.txt", "#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt") -else - create_test_file("#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt", "dummy file. this is a symlink on Unix") -end - -# Populate package source dir with files to install -Dir.mkdir(PACKAGE_SRC_DIR) -nested_dir_path = "#{PACKAGE_SRC_DIR}/new-dir/new-dir2" -FileUtils.mkdir_p(nested_dir_path) -FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir" -FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir/new-dir2" -nested_dir_test_file = "#{nested_dir_path}/new-file.txt" -File.open(nested_dir_test_file,'w') do |file| - file.puts "this is a new file in a new nested dir" -end -FileUtils::chmod 0644, nested_dir_test_file -copy_file NEWAPP_NAME, "#{PACKAGE_SRC_DIR}/#{APP_NAME}" -FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/#{APP_NAME}" - -# Create .zip packages from source files -Dir.mkdir(PACKAGE_DIR) -Dir.chdir(PACKAGE_SRC_DIR) do - if !system("#{ZIP_TOOL} #{PACKAGE_DIR}/app-pkg.zip .") - raise "Unable to create update package" - end -end - -# Copy the install script and updater to the target -# directory -replace_vars("file_list.xml","#{PACKAGE_DIR}/file_list.xml",file_list_vars) -copy_file "../#{UPDATER_NAME}", "#{PACKAGE_DIR}/#{UPDATER_NAME}" - -# Run the updater using the new syntax -# -# Run the application from the install directory to -# make sure that it looks in the correct directory for -# the file_list.xml file and packages -# -install_path = File.expand_path(INSTALL_DIR) -Dir.chdir(INSTALL_DIR) do - flags = "--force-elevated" if force_elevation - debug_flags = "gdb --args" if run_in_debugger - cmd = "#{debug_flags} #{PACKAGE_DIR}/#{UPDATER_NAME} #{flags} --install-dir \"#{install_path}\" --package-dir \"#{PACKAGE_DIR}\" --script file_list.xml --auto-close" - puts "Running '#{cmd}'" - system(cmd) -end - -# TODO - Correctly wait until updater has finished -sleep(1) - -# Check that the app was updated -app_path = "#{INSTALL_DIR}/#{APP_NAME}" -output = `"#{app_path}"` -if (output.strip != "new app starting") - throw "Updated app produced unexpected output: #{output}" -end - -# Check that the packaged dir and install dir match -dir_diff = compare_dirs(PACKAGE_SRC_DIR, INSTALL_DIR) -ignored_files = ["test-dir", "test-dir/app-symlink", UPDATER_NAME] -have_unexpected_change = false -dir_diff.each do |path, change_type| - if !ignored_files.include?(path) - case change_type - when :added - $stderr.puts "File #{path} was not installed" - when :changed - $stderr.puts "File #{path} differs between install and package dir" - when :deleted - $stderr.puts "File #{path} was not uninstalled" - end - have_unexpected_change = true - end -end - -if have_unexpected_change - throw "Unexpected differences between packaging and update dir" -end - -puts "Test passed" diff --git a/mmc_updater/src/tests/v2_file_list.xml b/mmc_updater/src/tests/v2_file_list.xml deleted file mode 100644 index 202e5bbe..00000000 --- a/mmc_updater/src/tests/v2_file_list.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0"?> - -<!-- The v2-compatible attribute lets the update script parser - know that it is dealing with a script structured for backwards - compatibility with the MD <= 1.0 updater. -!--> -<update version="3" v2-compatible="true"> - <targetVersion>2.0</targetVersion> - <platform>Test</platform> - <dependencies> - <!-- The new updater is standalone and has no dependencies, - except for standard system libraries and itself. - !--> - </dependencies> - <packages> - <package> - <name>app-pkg</name> - <hash>$APP_PACKAGE_HASH</hash> - <size>$APP_PACKAGE_SIZE</size> - <source>http://some/dummy/URL</source> - </package> - </packages> - - <!-- For compatibility with the update download in MD <= 1.0, - an <install> section lists the packages to download and - the real list of files to install is in the <install-v3> - section. !--> - <install> - <!-- A duplicate of the <packages> section should appear here, - except that each package is listed using the same structure - as files in the install-v3/files section. - !--> - </install> - <install-v3> - <file> - <name>$APP_FILENAME</name> - <hash>$UPDATED_APP_HASH</hash> - <size>$UPDATED_APP_SIZE</size> - <permissions>0755</permissions> - <package>app-pkg</package> - <is-main-binary>true</is-main-binary> - </file> - <file> - <name>$UPDATER_FILENAME</name> - <hash>$UPDATER_HASH</hash> - <size>$UPDATER_SIZE</size> - <permissions>0755</permissions> - </file> - <!-- Test symlink !--> - <file> - <name>test-dir/app-symlink</name> - <target>../app</target> - </file> - <file> - <name>new-dir/new-dir2/new-file.txt</name> - <hash>$TEST_FILENAME</hash> - <size>$TEST_SIZE</size> - <package>app-pkg</package> - <permissions>0644</permissions> - </file> - </install-v3> - <uninstall> - <!-- TODO - List some files to uninstall here !--> - <file>file-to-uninstall.txt</file> - <file>symlink-to-file-to-uninstall.txt</file> - </uninstall> -</update> @@ -1 +1,29 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include <windows.h> + IDI_ICON1 ICON DISCARDABLE "resources/icons/MultiMC.ico" +1 RT_MANIFEST "MultiMC.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000004b0" + BEGIN + VALUE "CompanyName", "MultiMC Contributors" + VALUE "FileDescription", "Minecraft Launcher" + VALUE "FileVersion", "1.0.0.0" + VALUE "ProductName", "MultiMC" + VALUE "ProductVersion", "5" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0000, 0x04b0 // Unicode + END +END diff --git a/package/linux/MultiMC b/package/linux/MultiMC index d8d620aa..8e854676 100755 --- a/package/linux/MultiMC +++ b/package/linux/MultiMC @@ -12,7 +12,7 @@ export QT_FONTPATH="${MMC_DIR}/fonts" # Detect missing dependencies... DEPS_LIST=`ldd "${MMC_DIR}"/plugins/*/*.so | grep "not found" | awk -vORS=", " '{ print $1 }'` -if [ -z $DEPS_LIST ]; then +if [ "x$DEPS_LIST" = "x" ]; then # We have all our dependencies. Run MultiMC. echo "No missing dependencies found." @@ -25,9 +25,46 @@ if [ -z $DEPS_LIST ]; then # Exit with MultiMC's exit code. exit $? else - echo "Error: MultiMC is missing the following libraries that it needs to work correctly:" - echo "\t${DEPS_LIST}" - echo "Please install them from your distribution's package manager." + # apt + if which apt-file >/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do apt-file -l search $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo apt-get install $COMMAND_LIBS" + # pacman + elif which pkgfile >/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pkgfile $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo pacman -S $COMMAND_LIBS" + # yum + elif which yum >/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do yum whatprovides $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo yum install $COMMAND_LIBS" + # zypper + elif which zypper >/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do zypper wp $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo zypper install $COMMAND_LIBS" + # emerge + elif which pfl >/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pfl $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo emerge $COMMAND_LIBS" + fi + + MESSAGE="Error: MultiMC is missing the following libraries that it needs to work correctly:\n\t${DEPS_LIST}\nPlease install them from your distribution's package manager." + MESSAGE="$MESSAGE\n\nHint: $INSTALL_CMD" + + echo $MESSAGE + + if which zenity >/dev/null; then zenity --error --text="$MESSAGE"; + elif which kdialog >/dev/null; then kdialog --error "$MESSAGE"; + fi + exit 1 fi - diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e115af17..14670fbd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -20,6 +20,8 @@ endmacro() add_unit_test(pathutils tst_pathutils.cpp) add_unit_test(userutils tst_userutils.cpp) +add_unit_test(UpdateChecker tst_UpdateChecker.cpp) +add_unit_test(DownloadUpdateTask tst_DownloadUpdateTask.cpp) # Tests END # diff --git a/tests/TestUtil.h b/tests/TestUtil.h index 64ee1675..5de8c4f2 100644 --- a/tests/TestUtil.h +++ b/tests/TestUtil.h @@ -15,15 +15,27 @@ struct TestsInternal f.open(QFile::ReadOnly); return f.readAll(); } + static QString readFileUtf8(const QString &fileName) + { + return QString::fromUtf8(readFile(fileName)); + } }; #define MULTIMC_GET_TEST_FILE(file) TestsInternal::readFile(QFINDTESTDATA( file )) +#define MULTIMC_GET_TEST_FILE_UTF8(file) TestsInternal::readFileUtf8(QFINDTESTDATA( file )) +#ifdef Q_OS_LINUX +# define _MMC_EXTRA_ARGV , "-platform", "offscreen" +# define _MMC_EXTRA_ARGC 2 +#else +# define _MMC_EXTRA_ARGV +# define _MMC_EXTRA_ARGC 0 +#endif #define QTEST_GUILESS_MAIN_MULTIMC(TestObject) \ int main(int argc, char *argv[]) \ { \ - char *argv_[] = { argv[0] }; \ - int argc_ = 1; \ + char *argv_[] = { argv[0] _MMC_EXTRA_ARGV }; \ + int argc_ = 1 + _MMC_EXTRA_ARGC; \ MultiMC app(argc_, argv_, QDir::temp().absoluteFilePath("MultiMC_Test")); \ app.setAttribute(Qt::AA_Use96Dpi, true); \ TestObject tc; \ diff --git a/tests/data/1.json b/tests/data/1.json new file mode 100644 index 00000000..d5261d2c --- /dev/null +++ b/tests/data/1.json @@ -0,0 +1,43 @@ +{ + "ApiVersion": 0, + "Id": 1, + "Name": "1.0.1", + "Files": [ + { + "Path": "fileOne", + "Sources": [ + { + "SourceType": "http", + "Url": "file://$PWD/tests/data/fileOneA" + } + ], + "Executable": true, + "Perms": 493, + "MD5": "9eb84090956c484e32cb6c08455a667b" + }, + { + "Path": "fileTwo", + "Sources": [ + { + "SourceType": "http", + "Url": "file://$PWD/tests/data/fileTwo" + } + ], + "Executable": false, + "Perms": 644, + "MD5": "38f94f54fa3eb72b0ea836538c10b043" + }, + { + "Path": "fileThree", + "Sources": [ + { + "SourceType": "http", + "Url": "file://$PWD/tests/data/fileThree" + } + ], + "Executable": false, + "Perms": "750", + "MD5": "f12df554b21e320be6471d7154130e70" + } + ] +} diff --git a/tests/data/2.json b/tests/data/2.json new file mode 100644 index 00000000..a96aff79 --- /dev/null +++ b/tests/data/2.json @@ -0,0 +1,31 @@ +{ + "ApiVersion": 0, + "Id": 1, + "Name": "1.0.1", + "Files": [ + { + "Path": "fileOne", + "Sources": [ + { + "SourceType": "http", + "Url": "file://$PWD/tests/data/fileOneB" + } + ], + "Executable": true, + "Perms": 493, + "MD5": "42915a71277c9016668cce7b82c6b577" + }, + { + "Path": "fileTwo", + "Sources": [ + { + "SourceType": "http", + "Url": "file://$PWD/tests/data/fileTwo" + } + ], + "Executable": false, + "Perms": 644, + "MD5": "38f94f54fa3eb72b0ea836538c10b043" + } + ] +} diff --git a/tests/data/channels.json b/tests/data/channels.json new file mode 100644 index 00000000..e4f04bff --- /dev/null +++ b/tests/data/channels.json @@ -0,0 +1,23 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "file://$PWD/tests/data/" + }, + { + "id": "stable", + "name": "Stable", + "description": "It's stable at least", + "url": "ftp://username@host/path/to/stuff" + }, + { + "id": "42", + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "https://dent.me/tea" + } + ] +} diff --git a/tests/data/errorChannels.json b/tests/data/errorChannels.json new file mode 100644 index 00000000..333cd445 --- /dev/null +++ b/tests/data/errorChannels.json @@ -0,0 +1,23 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "http://example.org/stuff" + }, + { + "id": "stable", + "name": "", + "description": "It's stable at least", + "url": "ftp://username@host/path/to/stuff" + }, + { + "id": "42", + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "" + } + ] +} diff --git a/tests/data/fileOneA b/tests/data/fileOneA new file mode 100644 index 00000000..f2e41136 --- /dev/null +++ b/tests/data/fileOneA @@ -0,0 +1 @@ +stuff diff --git a/tests/data/fileOneB b/tests/data/fileOneB new file mode 100644 index 00000000..f9aba922 --- /dev/null +++ b/tests/data/fileOneB @@ -0,0 +1,3 @@ +stuff + +more stuff that came in the new version diff --git a/tests/data/fileThree b/tests/data/fileThree new file mode 100644 index 00000000..6353ff16 --- /dev/null +++ b/tests/data/fileThree @@ -0,0 +1 @@ +this is yet another file diff --git a/tests/data/fileTwo b/tests/data/fileTwo new file mode 100644 index 00000000..aad9a93a --- /dev/null +++ b/tests/data/fileTwo @@ -0,0 +1 @@ +some other stuff diff --git a/tests/data/garbageChannels.json b/tests/data/garbageChannels.json new file mode 100644 index 00000000..1450fb9c --- /dev/null +++ b/tests/data/garbageChannels.json @@ -0,0 +1,22 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", +aa "url": "http://example.org/stuff" + }, +a "id": "stable", + "name": "Stable", + "description": "It's stable at least", + "url": "ftp://username@host/path/to/stuff" + }, + { + "id": "42"f + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "https://dent.me/tea" + } + ] +} diff --git a/tests/data/index.json b/tests/data/index.json new file mode 100644 index 00000000..20ceb9f4 --- /dev/null +++ b/tests/data/index.json @@ -0,0 +1,9 @@ +{ + "ApiVersion": 0, + "Versions": [ + { "Id": 0, "Name": "1.0.0" }, + { "Id": 1, "Name": "1.0.1" }, + { "Id": 2, "Name": "1.0.2" }, + { "Id": 3, "Name": "1.0.3" } + ] +} diff --git a/tests/data/noChannels.json b/tests/data/noChannels.json new file mode 100644 index 00000000..bbb2cb70 --- /dev/null +++ b/tests/data/noChannels.json @@ -0,0 +1,5 @@ +{ + "format_version": 0, + "channels": [ + ] +} diff --git a/tests/data/oneChannel.json b/tests/data/oneChannel.json new file mode 100644 index 00000000..84727ac7 --- /dev/null +++ b/tests/data/oneChannel.json @@ -0,0 +1,11 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "http://example.org/stuff" + } + ] +} diff --git a/tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml b/tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml new file mode 100644 index 00000000..09c162ca --- /dev/null +++ b/tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml @@ -0,0 +1,17 @@ +<update version="3"> + <install> + <file> + <source>sourceOne</source> + <dest>destOne</dest> + <mode>0777</mode> + </file> + <file> + <source>MultiMC.exe</source> + <dest>M/u/l/t/i/M/C/e/x/e</dest> + <mode>0644</mode> + </file> + </install> + <uninstall> + <file>toDelete.abc</file> + </uninstall> +</update> diff --git a/tests/tst_DownloadUpdateTask.cpp b/tests/tst_DownloadUpdateTask.cpp new file mode 100644 index 00000000..d96e4cf1 --- /dev/null +++ b/tests/tst_DownloadUpdateTask.cpp @@ -0,0 +1,200 @@ +#include <QTest> +#include <QSignalSpy> + +#include "TestUtil.h" + +#include "logic/updater/DownloadUpdateTask.h" +#include "logic/updater/UpdateChecker.h" +#include "depends/util/include/pathutils.h" + +Q_DECLARE_METATYPE(DownloadUpdateTask::VersionFileList) +Q_DECLARE_METATYPE(DownloadUpdateTask::UpdateOperation) + +bool operator==(const DownloadUpdateTask::FileSource &f1, const DownloadUpdateTask::FileSource &f2) +{ + return f1.type == f2.type && + f1.url == f2.url && + f1.compressionType == f2.compressionType; +} +bool operator==(const DownloadUpdateTask::VersionFileEntry &v1, const DownloadUpdateTask::VersionFileEntry &v2) +{ + return v1.path == v2.path && + v1.mode == v2.mode && + v1.sources == v2.sources && + v1.md5 == v2.md5; +} +bool operator==(const DownloadUpdateTask::UpdateOperation &u1, const DownloadUpdateTask::UpdateOperation &u2) +{ + return u1.type == u2.type && + u1.file == u2.file && + u1.dest == u2.dest && + u1.mode == u2.mode; +} + +QDebug operator<<(QDebug dbg, const DownloadUpdateTask::FileSource &f) +{ + dbg.nospace() << "FileSource(type=" << f.type << " url=" << f.url << " comp=" << f.compressionType << ")"; + return dbg.maybeSpace(); +} +QDebug operator<<(QDebug dbg, const DownloadUpdateTask::VersionFileEntry &v) +{ + dbg.nospace() << "VersionFileEntry(path=" << v.path << " mode=" << v.mode << " md5=" << v.md5 << " sources=" << v.sources << ")"; + return dbg.maybeSpace(); +} +QDebug operator<<(QDebug dbg, const DownloadUpdateTask::UpdateOperation::Type &t) +{ + switch (t) + { + case DownloadUpdateTask::UpdateOperation::OP_COPY: dbg << "OP_COPY"; break; + case DownloadUpdateTask::UpdateOperation::OP_DELETE: dbg << "OP_DELETE"; break; + case DownloadUpdateTask::UpdateOperation::OP_MOVE: dbg << "OP_MOVE"; break; + case DownloadUpdateTask::UpdateOperation::OP_CHMOD: dbg << "OP_CHMOD"; break; + } + return dbg.maybeSpace(); +} +QDebug operator<<(QDebug dbg, const DownloadUpdateTask::UpdateOperation &u) +{ + dbg.nospace() << "UpdateOperation(type=" << u.type << " file=" << u.file << " dest=" << u.dest << " mode=" << u.mode << ")"; + return dbg.maybeSpace(); +} + +class DownloadUpdateTaskTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + + } + void cleanupTestCase() + { + + } + + void test_writeInstallScript() + { + DownloadUpdateTask task(QUrl::fromLocalFile(QDir::current().absoluteFilePath("tests/data/")).toString(), 0); + + DownloadUpdateTask::UpdateOperationList ops; + + ops << DownloadUpdateTask::UpdateOperation::CopyOp("sourceOne", "destOne", 0777) + << DownloadUpdateTask::UpdateOperation::CopyOp("MultiMC.exe", "M/u/l/t/i/M/C/e/x/e") + << DownloadUpdateTask::UpdateOperation::DeleteOp("toDelete.abc"); + + const QString script = QDir::temp().absoluteFilePath("MultiMCUpdateScript.xml"); + QVERIFY(task.writeInstallScript(ops, script)); + QCOMPARE(TestsInternal::readFileUtf8(script), MULTIMC_GET_TEST_FILE_UTF8("tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml")); + } + + void test_parseVersionInfo_data() + { + QTest::addColumn<QByteArray>("data"); + QTest::addColumn<DownloadUpdateTask::VersionFileList>("list"); + QTest::addColumn<QString>("error"); + QTest::addColumn<bool>("ret"); + + QTest::newRow("one") << MULTIMC_GET_TEST_FILE("tests/data/1.json") + << (DownloadUpdateTask::VersionFileList() + << DownloadUpdateTask::VersionFileEntry{"fileOne", 493, + (DownloadUpdateTask::FileSourceList() << DownloadUpdateTask::FileSource("http", "file://" + qApp->applicationDirPath() + "/tests/data/fileOneA")), + "9eb84090956c484e32cb6c08455a667b"} + << DownloadUpdateTask::VersionFileEntry{"fileTwo", 644, + (DownloadUpdateTask::FileSourceList() << DownloadUpdateTask::FileSource("http", "file://" + qApp->applicationDirPath() + "/tests/data/fileTwo")), + "38f94f54fa3eb72b0ea836538c10b043"} + << DownloadUpdateTask::VersionFileEntry{"fileThree", 750, + (DownloadUpdateTask::FileSourceList() << DownloadUpdateTask::FileSource("http", "file://" + qApp->applicationDirPath() + "/tests/data/fileThree")), + "f12df554b21e320be6471d7154130e70"}) + << QString() + << true; + QTest::newRow("two") << MULTIMC_GET_TEST_FILE("tests/data/2.json") + << (DownloadUpdateTask::VersionFileList() + << DownloadUpdateTask::VersionFileEntry{"fileOne", 493, + (DownloadUpdateTask::FileSourceList() << DownloadUpdateTask::FileSource("http", "file://" + qApp->applicationDirPath() + "/tests/data/fileOneB")), + "42915a71277c9016668cce7b82c6b577"} + << DownloadUpdateTask::VersionFileEntry{"fileTwo", 644, + (DownloadUpdateTask::FileSourceList() << DownloadUpdateTask::FileSource("http", "file://" + qApp->applicationDirPath() + "/tests/data/fileTwo")), + "38f94f54fa3eb72b0ea836538c10b043"}) + << QString() + << true; + } + void test_parseVersionInfo() + { + QFETCH(QByteArray, data); + QFETCH(DownloadUpdateTask::VersionFileList, list); + QFETCH(QString, error); + QFETCH(bool, ret); + + DownloadUpdateTask::VersionFileList outList; + QString outError; + bool outRet = DownloadUpdateTask("", 0).parseVersionInfo(data, &outList, &outError); + QCOMPARE(outRet, ret); + QCOMPARE(outList, list); + QCOMPARE(outError, error); + } + + void test_processFileLists_data() + { + QTest::addColumn<DownloadUpdateTask *>("downloader"); + QTest::addColumn<DownloadUpdateTask::VersionFileList>("currentVersion"); + QTest::addColumn<DownloadUpdateTask::VersionFileList>("newVersion"); + QTest::addColumn<DownloadUpdateTask::UpdateOperationList>("expectedOperations"); + + DownloadUpdateTask *downloader = new DownloadUpdateTask(QString(), -1); + + // update fileOne, keep fileTwo, remove fileThree + QTest::newRow("test 1") << downloader + << (DownloadUpdateTask::VersionFileList() + << DownloadUpdateTask::VersionFileEntry{QFINDTESTDATA("tests/data/fileOne"), 493, DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource("http", "http://host/path/fileOne-1"), "9eb84090956c484e32cb6c08455a667b"} + << DownloadUpdateTask::VersionFileEntry{QFINDTESTDATA("tests/data/fileTwo"), 644, DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource("http", "http://host/path/fileTwo-1"), "38f94f54fa3eb72b0ea836538c10b043"} + << DownloadUpdateTask::VersionFileEntry{QFINDTESTDATA("tests/data/fileThree"), 420, DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource("http", "http://host/path/fileThree-1"), "f12df554b21e320be6471d7154130e70"}) + << (DownloadUpdateTask::VersionFileList() + << DownloadUpdateTask::VersionFileEntry{QFINDTESTDATA("tests/data/fileOne"), 493, DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource("http", "http://host/path/fileOne-2"), "42915a71277c9016668cce7b82c6b577"} + << DownloadUpdateTask::VersionFileEntry{QFINDTESTDATA("tests/data/fileTwo"), 644, DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource("http", "http://host/path/fileTwo-2"), "38f94f54fa3eb72b0ea836538c10b043"}) + << (DownloadUpdateTask::UpdateOperationList() + << DownloadUpdateTask::UpdateOperation::DeleteOp(QFINDTESTDATA("tests/data/fileThree")) + << DownloadUpdateTask::UpdateOperation::CopyOp(PathCombine(downloader->updateFilesDir(), QFINDTESTDATA("tests/data/fileOne").replace("/", "_")), + QFINDTESTDATA("tests/data/fileOne"), 493)); + } + void test_processFileLists() + { + QFETCH(DownloadUpdateTask *, downloader); + QFETCH(DownloadUpdateTask::VersionFileList, currentVersion); + QFETCH(DownloadUpdateTask::VersionFileList, newVersion); + QFETCH(DownloadUpdateTask::UpdateOperationList, expectedOperations); + + DownloadUpdateTask::UpdateOperationList operations; + + downloader->processFileLists(new NetJob("Dummy"), currentVersion, newVersion, operations); + qDebug() << (operations == expectedOperations); + qDebug() << operations; + qDebug() << expectedOperations; + QCOMPARE(operations, expectedOperations); + } + + void test_masterTest() + { + QLOG_INFO() << "#####################"; + MMC->m_version.build = 1; + MMC->m_version.channel = "develop"; + MMC->updateChecker()->setChannelListUrl(QUrl::fromLocalFile(QDir::current().absoluteFilePath("tests/data/channels.json")).toString()); + MMC->updateChecker()->setCurrentChannel("develop"); + + DownloadUpdateTask task(QUrl::fromLocalFile(QDir::current().absoluteFilePath("tests/data/")).toString(), 2); + + QSignalSpy succeededSpy(&task, SIGNAL(succeeded())); + + task.start(); + + QVERIFY(succeededSpy.wait()); + } +}; + +QTEST_GUILESS_MAIN_MULTIMC(DownloadUpdateTaskTest) + +#include "tst_DownloadUpdateTask.moc" diff --git a/tests/tst_UpdateChecker.cpp b/tests/tst_UpdateChecker.cpp new file mode 100644 index 00000000..0f023f0e --- /dev/null +++ b/tests/tst_UpdateChecker.cpp @@ -0,0 +1,163 @@ +#include <QTest> +#include <QSignalSpy> + +#include "TestUtil.h" +#include "logic/updater/UpdateChecker.h" + +Q_DECLARE_METATYPE(UpdateChecker::ChannelListEntry) + +bool operator==(const UpdateChecker::ChannelListEntry &e1, const UpdateChecker::ChannelListEntry &e2) +{ + return e1.id == e2.id && + e1.name == e2.name && + e1.description == e2.description && + e1.url == e2.url; +} + +QDebug operator<<(QDebug dbg, const UpdateChecker::ChannelListEntry &c) +{ + dbg.nospace() << "ChannelListEntry(id=" << c.id << " name=" << c.name << " description=" << c.description << " url=" << c.url << ")"; + return dbg.maybeSpace(); +} + +class UpdateCheckerTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + + } + void cleanupTestCase() + { + + } + + static QString findTestDataUrl(const char *file) + { + return QUrl::fromLocalFile(QFINDTESTDATA(file)).toString(); + } + void tst_ChannelListParsing_data() + { + QTest::addColumn<QString>("channel"); + QTest::addColumn<QString>("channelUrl"); + QTest::addColumn<bool>("hasChannels"); + QTest::addColumn<bool>("valid"); + QTest::addColumn<QList<UpdateChecker::ChannelListEntry> >("result"); + + QTest::newRow("garbage") + << QString() + << findTestDataUrl("tests/data/garbageChannels.json") + << false + << false + << QList<UpdateChecker::ChannelListEntry>(); + QTest::newRow("errors") + << QString() + << findTestDataUrl("tests/data/errorChannels.json") + << false + << true + << QList<UpdateChecker::ChannelListEntry>(); + QTest::newRow("no channels") + << QString() + << findTestDataUrl("tests/data/noChannels.json") + << false + << true + << QList<UpdateChecker::ChannelListEntry>(); + QTest::newRow("one channel") + << QString("develop") + << findTestDataUrl("tests/data/oneChannel.json") + << true + << true + << (QList<UpdateChecker::ChannelListEntry>() << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "http://example.org/stuff"}); + QTest::newRow("several channels") + << QString("develop") + << findTestDataUrl("tests/data/channels.json") + << true + << true + << (QList<UpdateChecker::ChannelListEntry>() + << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "file://$PWD/tests/data/"} + << UpdateChecker::ChannelListEntry{"stable", "Stable", "It's stable at least", "ftp://username@host/path/to/stuff"} + << UpdateChecker::ChannelListEntry{"42", "The Channel", "This is the channel that is going to answer all of your questions", "https://dent.me/tea"}); + } + void tst_ChannelListParsing() + { + QFETCH(QString, channel); + QFETCH(QString, channelUrl); + QFETCH(bool, hasChannels); + QFETCH(bool, valid); + QFETCH(QList<UpdateChecker::ChannelListEntry>, result); + + UpdateChecker checker; + + QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); + QVERIFY(channelListLoadedSpy.isValid()); + + checker.setCurrentChannel(channel); + checker.setChannelListUrl(channelUrl); + + checker.updateChanList(); + + if (valid) + { + QVERIFY(channelListLoadedSpy.wait()); + QCOMPARE(channelListLoadedSpy.size(), 1); + } + else + { + channelListLoadedSpy.wait(); + QCOMPARE(channelListLoadedSpy.size(), 0); + } + + QCOMPARE(checker.hasChannels(), hasChannels); + QCOMPARE(checker.getChannelList(), result); + } + + void tst_UpdateChecking_data() + { + QTest::addColumn<QString>("channel"); + QTest::addColumn<QString>("channelUrl"); + QTest::addColumn<int>("currentBuild"); + QTest::addColumn<QList<QVariant> >("result"); + + QTest::newRow("valid channel") + << "develop" << findTestDataUrl("tests/data/channels.json") + << 2 + << (QList<QVariant>() << QString() << "1.0.3" << 3); + } + + void tst_UpdateChecking() + { + QFETCH(QString, channel); + QFETCH(QString, channelUrl); + QFETCH(int, currentBuild); + QFETCH(QList<QVariant>, result); + + MMC->m_version.build = currentBuild; + + UpdateChecker checker; + checker.setCurrentChannel(channel); + checker.setChannelListUrl(channelUrl); + + QSignalSpy updateAvailableSpy(&checker, SIGNAL(updateAvailable(QString,QString,int))); + QVERIFY(updateAvailableSpy.isValid()); + QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); + QVERIFY(channelListLoadedSpy.isValid()); + + checker.updateChanList(); + QVERIFY(channelListLoadedSpy.wait()); + + checker.m_channels[0].url = QUrl::fromLocalFile(QDir::current().absoluteFilePath("tests/data/")).toString(); + + checker.checkForUpdate(); + + QVERIFY(updateAvailableSpy.wait()); + QList<QVariant> res = result; + res[0] = checker.m_channels[0].url; + QCOMPARE(updateAvailableSpy.first(), res); + } +}; + +QTEST_GUILESS_MAIN_MULTIMC(UpdateCheckerTest) + +#include "tst_UpdateChecker.moc" |