/* 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 "report.h" #include "messagedialog.h" #include "downloadmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace MOBase; using namespace MOShared; typedef Archive* (*CreateArchiveType)(); template T resolveFunction(QLibrary &lib, const char *name) { T temp = reinterpret_cast(lib.resolve(name)); if (temp == NULL) { throw std::runtime_error(QObject::tr("invalid 7-zip32.dll: %1").arg(lib.errorString()).toLatin1().constData()); } return temp; } SelfUpdater::SelfUpdater(NexusInterface *nexusInterface, QWidget *parent) : QObject(parent), m_Parent(parent), m_Interface(nexusInterface), m_UpdateRequestID(-1), m_Reply(NULL), m_Progress(parent), m_Attempts(3) { m_Progress.setMaximum(100); 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_CurrentArchive = CreateArchiveFunc(); if (!m_CurrentArchive->isValid()) { throw MyException(InstallationManager::getErrorString(m_CurrentArchive->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_CurrentArchive; } 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(), ToQString(SkyrimInfo::getNexusInfoUrlStatic())); } } 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::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); m_Progress.setModal(true); m_Progress.show(); m_Progress.setValue(0); m_Progress.setWindowTitle(tr("Update")); m_Progress.setLabelText(tr("Download in progress")); 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 != NULL) { if (m_Canceled) { m_Reply->abort(); } else { if (bytesTotal != 0) { m_Progress.setValue((bytesReceived * 100) / bytesTotal); } } } } void SelfUpdater::downloadReadyRead() { if (m_Reply != NULL) { m_UpdateFile.write(m_Reply->readAll()); } } void SelfUpdater::downloadFinished() { int error = QNetworkReply::NoError; if (m_Reply != NULL) { m_UpdateFile.write(m_Reply->readAll()); error = m_Reply->error(); if (m_Reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive)) { m_Canceled = true; } m_Progress.hide(); m_Reply->close(); m_Reply->deleteLater(); m_Reply = NULL; } 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.mid(0).append("/update_backup"); QDir().mkdir(backupPath); // rename files that are currently open so we can unpack the update if (!m_CurrentArchive->open(ToWString(QDir::toNativeSeparators(m_UpdateFile.fileName())).c_str(), new MethodCallback(this, &SelfUpdater::queryPassword))) { throw MyException(tr("failed to open archive \"%1\": %2") .arg(m_UpdateFile.fileName()) .arg(InstallationManager::getErrorString(m_CurrentArchive->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_CurrentArchive->getFileList(data, size); for (size_t i = 0; i < size; ++i) { QString outputName = ToQString(data[i]->getFileName()); if (outputName.startsWith("ModOrganizer\\", Qt::CaseInsensitive)) { outputName = outputName.mid(13); data[i]->setOutputFileName(ToWString(outputName).c_str()); } else if (outputName == "ModOrganizer") { data[i]->setSkip(true); } QFileInfo file(mopath.mid(0).append("/").append(outputName)); if (file.exists() && file.isFile()) { if (!shellMove(QStringList(mopath.mid(0).append("/").append(outputName)), QStringList(backupPath.mid(0).append("/").append(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_CurrentArchive->extract(GameInfo::instance().getOrganizerDirectory().c_str(), new MethodCallback(this, &SelfUpdater::updateProgress), new MethodCallback(this, &SelfUpdater::updateProgressFile), new MethodCallback(this, &SelfUpdater::report7ZipError))) { throw std::runtime_error("extracting failed"); } m_CurrentArchive->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.mid(0).append("/ModOrganizer.exe"))) { newProcess.startDetached(mopath.mid(0).append("/ModOrganizer.exe"), QStringList("update")); } else { newProcess.startDetached(mopath.mid(0).append("/ModOrganiser.exe"), QStringList("update")); } emit restart(); } void SelfUpdater::queryPassword(LPSTR) { // nop } void SelfUpdater::updateProgress(float) { // nop } void SelfUpdater::updateProgressFile(LPCWSTR) { // nop } void SelfUpdater::report7ZipError(LPCWSTR errorMessage) { QMessageBox::critical(m_Parent, tr("Error"), ToQString(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; foreach(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); m_Progress.hide(); } } void SelfUpdater::nxmRequestFailed(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 { MessageDialog::showMessage(tr("Failed to retrieve update information: %1").arg(errorMessage), m_Parent); } } } 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); m_Progress.hide(); } } }