/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "selfupdater.h" #include "utility.h" #include "installationmanager.h" #include "messagedialog.h" #include "downloadmanager.h" #include "nexusinterface.h" #include "nxmaccessmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MOBase; using namespace MOShared; typedef Archive* (*CreateArchiveType)(); template static T resolveFunction(QLibrary &lib, const char *name) { T temp = reinterpret_cast(lib.resolve(name)); if (temp == nullptr) { throw std::runtime_error(QObject::tr("invalid 7-zip32.dll: %1").arg(lib.errorString()).toLatin1().constData()); } return temp; } SelfUpdater::SelfUpdater(NexusInterface *nexusInterface) : m_Parent(nullptr) , m_Interface(nexusInterface) , m_UpdateRequestID(-1) , m_Reply(nullptr) , m_Attempts(3) { QLibrary archiveLib("dlls\\archive.dll"); if (!archiveLib.load()) { throw MyException(tr("archive.dll not loaded: \"%1\"").arg(archiveLib.errorString())); } CreateArchiveType CreateArchiveFunc = resolveFunction(archiveLib, "CreateArchive"); m_ArchiveHandler = CreateArchiveFunc(); if (!m_ArchiveHandler->isValid()) { throw MyException(InstallationManager::getErrorString(m_ArchiveHandler->getLastError())); } connect(m_Progress, SIGNAL(canceled()), this, SLOT(downloadCancel())); VS_FIXEDFILEINFO version = GetFileVersion(ToWString(QApplication::applicationFilePath())); m_MOVersion = VersionInfo(version.dwFileVersionMS >> 16, version.dwFileVersionMS & 0xFFFF, version.dwFileVersionLS >> 16); } SelfUpdater::~SelfUpdater() { delete m_ArchiveHandler; } void SelfUpdater::setUserInterface(QWidget *widget) { m_Parent = widget; } void SelfUpdater::testForUpdate() { if (QFile::exists(QCoreApplication::applicationDirPath() + "/mo_test_update.7z")) { emit updateAvailable(); return; } if (m_UpdateRequestID == -1) { m_UpdateRequestID = m_Interface->requestDescription( SkyrimInfo::getNexusModIDStatic(), this, QVariant(), QString(), ToQString(SkyrimInfo::getNexusInfoUrlStatic()), SkyrimInfo::getNexusGameIDStatic()); } } void SelfUpdater::startUpdate() { if (QFile::exists(QCoreApplication::applicationDirPath() + "/mo_test_update.7z")) { m_UpdateFile.setFileName(QCoreApplication::applicationDirPath() + "/mo_test_update.7z"); installUpdate(); return; } if ((m_UpdateRequestID == -1) && (!m_NewestVersion.isEmpty())) { if (QMessageBox::question(m_Parent, tr("Update"), tr("An update is available (newest version: %1), do you want to install it?").arg(m_NewestVersion), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { m_UpdateRequestID = m_Interface->requestFiles(SkyrimInfo::getNexusModIDStatic(), this, m_NewestVersion, ToQString(SkyrimInfo::getNexusInfoUrlStatic())); } } } void SelfUpdater::showProgress() { if (m_Progress == nullptr) { m_Progress = new QProgressDialog(m_Parent, Qt::Dialog); } m_Progress->setModal(true); m_Progress->show(); m_Progress->setValue(0); m_Progress->setWindowTitle(tr("Update")); m_Progress->setLabelText(tr("Download in progress")); } void SelfUpdater::closeProgress() { if (m_Progress != nullptr) { m_Progress->hide(); m_Progress->deleteLater(); m_Progress = nullptr; } } void SelfUpdater::download(const QString &downloadLink, const QString &fileName) { QNetworkAccessManager *accessManager = m_Interface->getAccessManager(); QUrl dlUrl(downloadLink); QNetworkRequest request(dlUrl); m_Canceled = false; m_Reply = accessManager->get(request); m_UpdateFile.setFileName(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOrganizerDirectory()).append("\\").append(fileName))); m_UpdateFile.open(QIODevice::WriteOnly); showProgress(); connect(m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64))); connect(m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); connect(m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead())); } void SelfUpdater::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { if (m_Reply != nullptr) { if (m_Canceled) { m_Reply->abort(); } else { if (bytesTotal != 0) { if (m_Progress != nullptr) { m_Progress->setValue((bytesReceived * 100) / bytesTotal); } } } } } void SelfUpdater::downloadReadyRead() { if (m_Reply != nullptr) { m_UpdateFile.write(m_Reply->readAll()); } } void SelfUpdater::downloadFinished() { int error = QNetworkReply::NoError; if (m_Reply != nullptr) { m_UpdateFile.write(m_Reply->readAll()); error = m_Reply->error(); if (m_Reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive)) { m_Canceled = true; } closeProgress(); m_Reply->close(); m_Reply->deleteLater(); m_Reply = nullptr; } m_UpdateFile.close(); if ((m_UpdateFile.size() == 0) || (error != QNetworkReply::NoError) || m_Canceled) { if (!m_Canceled) { reportError(tr("Download failed: %1").arg(error)); } m_UpdateFile.remove(); return; } qDebug("download: %s", m_UpdateFile.fileName().toUtf8().constData()); try { installUpdate(); } catch (const std::exception &e) { reportError(tr("Failed to install update: %1").arg(e.what())); } } void SelfUpdater::downloadCancel() { m_Canceled = true; } void SelfUpdater::installUpdate() { const QString mopath = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOrganizerDirectory())); QString backupPath = mopath + "/update_backup"; QDir().mkdir(backupPath); // rename files that are currently open so we can unpack the update if (!m_ArchiveHandler->open(m_UpdateFile.fileName(), nullptr)) { throw MyException(tr("failed to open archive \"%1\": %2") .arg(m_UpdateFile.fileName()) .arg(InstallationManager::getErrorString(m_ArchiveHandler->getLastError()))); } // move all files contained in the archive out of the way, // otherwise we can't overwrite everything FileData* const *data; size_t size; m_ArchiveHandler->getFileList(data, size); for (size_t i = 0; i < size; ++i) { QString outputName = data[i]->getFileName(); if (outputName.startsWith("ModOrganizer\\", Qt::CaseInsensitive)) { outputName = outputName.mid(13); data[i]->addOutputFileName(outputName); } else if (outputName != "ModOrganizer") { data[i]->addOutputFileName(outputName); } QFileInfo file(mopath + "/" + outputName); if (file.exists() && file.isFile()) { if (!shellMove(QStringList(mopath + "/" + outputName), QStringList(backupPath + "/" + outputName))) { reportError(tr("failed to move outdated files: %1. Please update manually.").arg(windowsErrorString(::GetLastError()))); return; } } } // now unpack the archive into the mo directory if (!m_ArchiveHandler->extract(QString::fromStdWString(GameInfo::instance().getOrganizerDirectory()), nullptr, nullptr, new MethodCallback(this, &SelfUpdater::report7ZipError))) { throw std::runtime_error("extracting failed"); } m_ArchiveHandler->close(); m_UpdateFile.remove(); QMessageBox::information(m_Parent, tr("Update"), tr("Update installed, Mod Organizer will now be restarted.")); QProcess newProcess; if (QFile::exists(mopath + "/ModOrganizer.exe")) { newProcess.startDetached(mopath + "/ModOrganizer.exe", QStringList("update")); } else { newProcess.startDetached(mopath + "/ModOrganiser.exe", QStringList("update")); } emit restart(); } void SelfUpdater::report7ZipError(QString const &errorMessage) { QMessageBox::critical(m_Parent, tr("Error"), errorMessage); } QString SelfUpdater::retrieveNews(const QString &description) { QStringList temp = description.split("[s][/s]"); if (temp.length() < 2) { return QString(); } else { return temp.at(1); } } void SelfUpdater::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID) { if (requestID == m_UpdateRequestID) { m_UpdateRequestID = -1; QVariantMap result = resultData.toMap(); QString motd = retrieveNews(result["description"].toString()).trimmed(); if (motd.length() != 0) { emit motdAvailable(motd); } m_NewestVersion = result["version"].toString(); if (m_NewestVersion.isEmpty()) { QTimer::singleShot(5000, this, SLOT(testForUpdate())); } VersionInfo currentVersion(m_MOVersion); VersionInfo newestVersion(m_NewestVersion); if (!m_NewestVersion.isEmpty() && (currentVersion < newestVersion)) { emit updateAvailable(); } else if (newestVersion < currentVersion) { qDebug("this version is newer than the current version on nexus (%s vs %s)", currentVersion.canonicalString().toUtf8().constData(), newestVersion.canonicalString().toUtf8().constData()); } } } void SelfUpdater::nxmFilesAvailable(int, QVariant userData, QVariant resultData, int requestID) { if (requestID != m_UpdateRequestID) { return; } QString version = userData.toString(); m_UpdateRequestID = -1; if (!resultData.canConvert()) { qCritical("invalid files result: %s", resultData.toString().toUtf8().constData()); reportError(tr("Failed to parse response. Please report this as a bug and include the file mo_interface.log.")); return; } QVariantList result = resultData.toList(); QRegExp updateExpList(QString("updates version ([0-9., ]*) to %1").arg(version)); QRegExp updateExpRange(QString("updates version ([0-9.]*) - ([0-9.]*) to %1").arg(version)); int updateFileID = -1; QString updateFileName; int mainFileID = -1; QString mainFileName; int mainFileSize = 0; for(QVariant file : result) { QVariantMap fileInfo = file.toMap(); if (!fileInfo["uri"].toString().endsWith(".7z")) { continue; } if (fileInfo["version"].toString() == version) { if (fileInfo["category_id"].toInt() == 2) { QString description = fileInfo["description"].toString(); // update if (updateExpList.indexIn(description) != -1) { // there is an update for the newest version of MO, but does // it apply to the current version? QStringList supportedVersions = updateExpList.cap(1).split(QRegExp(",[ ]*"), QString::SkipEmptyParts); if (supportedVersions.contains(m_MOVersion.canonicalString())) { updateFileID = fileInfo["id"].toInt(); updateFileName = fileInfo["uri"].toString(); } else { qDebug("update not supported from %s", m_MOVersion.canonicalString().toUtf8().constData()); } } else if (updateExpRange.indexIn(description) != -1) { VersionInfo rangeLowEnd(updateExpRange.cap(1)); VersionInfo rangeHighEnd(updateExpRange.cap(2)); if ((rangeLowEnd <= m_MOVersion) && (m_MOVersion <= rangeHighEnd)) { updateFileID = fileInfo["id"].toInt(); updateFileName = fileInfo["uri"].toString(); break; } else { qDebug("update not supported from %s", m_MOVersion.canonicalString().toUtf8().constData()); } } else { qWarning("invalid update description: %s", description.toUtf8().constData()); } } else if (fileInfo["category_id"].toInt() == 1) { mainFileID = fileInfo["id"].toInt(); mainFileName = fileInfo["uri"].toString(); mainFileSize = fileInfo["size"].toInt(); } } } if (updateFileID != -1) { qDebug("update available: %d", updateFileID); m_UpdateRequestID = m_Interface->requestDownloadURL(SkyrimInfo::getNexusModIDStatic(), updateFileID, this, updateFileName, ToQString(SkyrimInfo::getNexusInfoUrlStatic())); } else if (mainFileID != -1) { qDebug("full download required: %d", mainFileID); if (QMessageBox::question(m_Parent, tr("Update"), tr("No incremental update available for this version, " "the complete package needs to be downloaded (%1 kB)").arg(mainFileSize), QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Ok) { m_UpdateRequestID = m_Interface->requestDownloadURL(SkyrimInfo::getNexusModIDStatic(), mainFileID, this, mainFileName, ToQString(SkyrimInfo::getNexusInfoUrlStatic())); } } else { qCritical("no file for update found"); MessageDialog::showMessage(tr("no file for update found. Please update manually."), m_Parent); closeProgress(); } } void SelfUpdater::nxmRequestFailed(int, int, QVariant, int requestID, const QString &errorMessage) { if (requestID == m_UpdateRequestID) { m_UpdateRequestID = -1; if (m_Attempts > 0) { QTimer::singleShot(60000, this, SLOT(testForUpdate())); --m_Attempts; } else { qWarning("Failed to retrieve update information: %s", qPrintable(errorMessage)); MessageDialog::showMessage(tr("Failed to retrieve update information: %1").arg(errorMessage), m_Parent, false); } } } void SelfUpdater::nxmDownloadURLsAvailable(int, int, QVariant userData, QVariant resultData, int requestID) { if (requestID == m_UpdateRequestID) { m_UpdateRequestID = -1; QVariantList serverList = resultData.toList(); if (serverList.count() != 0) { std::map dummy; qSort(serverList.begin(), serverList.end(), boost::bind(&DownloadManager::ServerByPreference, dummy, _1, _2)); QVariantMap dlServer = serverList.first().toMap(); download(dlServer["URI"].toString(), userData.toString()); } else { MessageDialog::showMessage(tr("No download server available. Please try again later."), m_Parent); closeProgress(); } } }