summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt6
-rw-r--r--MultiMC.cpp3
-rw-r--r--MultiMC.h3
-rw-r--r--MultiMC.manifest27
-rw-r--r--gui/ConsoleWindow.cpp15
-rw-r--r--gui/ConsoleWindow.ui5
-rw-r--r--gui/MainWindow.cpp165
-rw-r--r--gui/dialogs/AccountListDialog.cpp6
-rw-r--r--gui/dialogs/InstanceSettings.cpp16
-rw-r--r--gui/dialogs/InstanceSettings.ui26
-rw-r--r--gui/dialogs/SettingsDialog.cpp6
-rw-r--r--gui/dialogs/SettingsDialog.ui16
-rw-r--r--logic/BaseInstance.cpp5
-rw-r--r--logic/BaseInstance.h2
-rw-r--r--logic/OneSixUpdate.cpp9
-rw-r--r--logic/auth/MojangAccountList.cpp27
-rw-r--r--logic/auth/MojangAccountList.h5
-rw-r--r--logic/lists/InstanceList.cpp14
-rw-r--r--logic/lists/InstanceList.h5
-rw-r--r--logic/net/ByteArrayDownload.cpp8
-rw-r--r--logic/net/CacheDownload.cpp14
-rw-r--r--logic/net/ForgeMirrors.cpp8
-rw-r--r--logic/net/ForgeXzDownload.cpp18
-rw-r--r--logic/net/MD5EtagDownload.cpp16
-rw-r--r--logic/net/NetAction.h23
-rw-r--r--logic/net/NetJob.h12
-rw-r--r--logic/net/PasteUpload.cpp2
-rw-r--r--logic/net/S3ListBucket.cpp175
-rw-r--r--logic/net/S3ListBucket.h57
-rw-r--r--logic/updater/DownloadUpdateTask.cpp172
-rw-r--r--logic/updater/DownloadUpdateTask.h34
-rw-r--r--logic/updater/UpdateChecker.cpp2
-rw-r--r--logic/updater/UpdateChecker.h5
-rw-r--r--mmc_updater/CMakeLists.txt8
-rw-r--r--mmc_updater/src/UpdateScript.h4
-rw-r--r--mmc_updater/src/UpdaterOptions.cpp3
-rw-r--r--mmc_updater/src/resources/updater.manifest27
-rw-r--r--mmc_updater/src/resources/updater.rc30
-rw-r--r--mmc_updater/src/tests/CMakeLists.txt28
-rw-r--r--mmc_updater/src/tests/TestParseScript.cpp24
-rw-r--r--mmc_updater/src/tests/TestParseScript.h8
-rw-r--r--mmc_updater/src/tests/TestUpdateScript.cpp27
-rw-r--r--mmc_updater/src/tests/TestUpdateScript.h8
-rw-r--r--mmc_updater/src/tests/file_list.xml15
-rwxr-xr-xmmc_updater/src/tests/test-update.rb218
-rw-r--r--mmc_updater/src/tests/v2_file_list.xml67
-rw-r--r--multimc.rc28
-rwxr-xr-xpackage/linux/MultiMC47
-rw-r--r--tests/CMakeLists.txt2
-rw-r--r--tests/TestUtil.h16
-rw-r--r--tests/data/1.json43
-rw-r--r--tests/data/2.json31
-rw-r--r--tests/data/channels.json23
-rw-r--r--tests/data/errorChannels.json23
-rw-r--r--tests/data/fileOneA1
-rw-r--r--tests/data/fileOneB3
-rw-r--r--tests/data/fileThree1
-rw-r--r--tests/data/fileTwo1
-rw-r--r--tests/data/garbageChannels.json22
-rw-r--r--tests/data/index.json9
-rw-r--r--tests/data/noChannels.json5
-rw-r--r--tests/data/oneChannel.json11
-rw-r--r--tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml17
-rw-r--r--tests/tst_DownloadUpdateTask.cpp200
-rw-r--r--tests/tst_UpdateChecker.cpp163
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));
diff --git a/MultiMC.h b/MultiMC.h
index 7bfa0023..4a33fb69 100644
--- a/MultiMC.h
+++ b/MultiMC.h
@@ -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 &currentVersion, 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 &currentVersion, 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>
diff --git a/multimc.rc b/multimc.rc
index decf7d3a..2140e3f4 100644
--- a/multimc.rc
+++ b/multimc.rc
@@ -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"