diff options
author | Jan Dalheimer <jan@dalheimer.de> | 2013-12-16 21:17:50 +0100 |
---|---|---|
committer | Jan Dalheimer <jan@dalheimer.de> | 2013-12-16 21:17:50 +0100 |
commit | 47bf7fff27665496bc4212bcaccc80350f665f97 (patch) | |
tree | 878fd220464c5280fcbe589d3ed738ce0306ab2a | |
parent | ae68adc3a536525990b0668703fd74eded8ccfde (diff) | |
parent | dff00a6d2abb84a93e48ff00dda16444550d859f (diff) | |
download | MultiMC-47bf7fff27665496bc4212bcaccc80350f665f97.tar MultiMC-47bf7fff27665496bc4212bcaccc80350f665f97.tar.gz MultiMC-47bf7fff27665496bc4212bcaccc80350f665f97.tar.lz MultiMC-47bf7fff27665496bc4212bcaccc80350f665f97.tar.xz MultiMC-47bf7fff27665496bc4212bcaccc80350f665f97.zip |
Merge remote-tracking branch 'upstream/develop' into updater_tests
Conflicts:
mmc_updater/src/tests/CMakeLists.txt
31 files changed, 284 insertions, 710 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/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/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/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/tests/CMakeLists.txt b/mmc_updater/src/tests/CMakeLists.txt index 85864791..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> |