diff options
| author | Tannin <Tannin@Serenity> | 2013-02-03 12:49:25 +0100 |
|---|---|---|
| committer | Tannin <Tannin@Serenity> | 2013-02-03 12:49:25 +0100 |
| commit | 981f8b3acf7e76f27c02f4ced80d55b424cc7497 (patch) | |
| tree | 0f504d99d1e5fe197258febff3c147d0725abda4 /src/installationmanager.cpp | |
initial commit to mercurial repository.
Corresponds to MO version 0.12.6
Diffstat (limited to 'src/installationmanager.cpp')
| -rw-r--r-- | src/installationmanager.cpp | 1302 |
1 files changed, 1302 insertions, 0 deletions
diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp new file mode 100644 index 00000000..e7784771 --- /dev/null +++ b/src/installationmanager.cpp @@ -0,0 +1,1302 @@ +/* +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 <http://www.gnu.org/licenses/>. +*/ + +#include "installationmanager.h" + +#include "utility.h" +#include "installdialog.h" +#include "simpleinstalldialog.h" +#include "baincomplexinstallerdialog.h" +#include "fomodinstallerdialog.h" +#include "report.h" +#include "categories.h" +#include <scopeguard.h> +#include "questionboxmemory.h" +#include "settings.h" +#include "queryoverwritedialog.h" +#include "messagedialog.h" +#include "iplugininstallersimple.h" +#include "iplugininstallercustom.h" +#include "nexusinterface.h" +#include "selectiondialog.h" +#include <installationtester.h> +#include <gameinfo.h> +#include <QFileInfo> +#include <QLibrary> +#include <QInputDialog> +#include <QRegExp> +#include <QDir> +#include <QMessageBox> +#include <QSettings> +#include <Shellapi.h> +#include <QPushButton> +#include <QApplication> +#include <QDirIterator> +#include <boost/assign.hpp> + + +typedef Archive* (*CreateArchiveType)(); + + + +template <typename T> T resolveFunction(QLibrary &lib, const char *name) +{ + T temp = reinterpret_cast<T>(lib.resolve(name)); + if (temp == NULL) { + throw std::runtime_error(QObject::tr("invalid 7-zip32.dll: %1").arg(lib.errorString()).toLatin1().constData()); + } + return temp; +} + + +InstallationManager::InstallationManager(QWidget *parent) + : QObject(parent), m_ParentWidget(parent), + m_NCCPath(QApplication::applicationDirPath() + "/NCC/NexusClientCLI.exe"), + m_InstallationProgress(parent), m_SupportedExtensions(boost::assign::list_of("zip")("rar")("7z")("fomod")) +{ + QLibrary archiveLib("dlls\\archive.dll"); + if (!archiveLib.load()) { + throw MyException(tr("archive.dll not loaded: \"%1\"").arg(archiveLib.errorString())); + } + + CreateArchiveType CreateArchiveFunc = resolveFunction<CreateArchiveType>(archiveLib, "CreateArchive"); + + m_CurrentArchive = CreateArchiveFunc(); + if (!m_CurrentArchive->isValid()) { + throw MyException(getErrorString(m_CurrentArchive->getLastError())); + } + +} + + +InstallationManager::~InstallationManager() +{ + delete m_CurrentArchive; +} + + +void InstallationManager::queryPassword(LPSTR password) +{ + QString result = QInputDialog::getText(NULL, tr("Password required"), tr("Password"), QLineEdit::Password); + strncpy(password, result.toLocal8Bit().constData(), MAX_PASSWORD_LENGTH); +} + + +void InstallationManager::mapToArchive(const DirectoryTree::Node *node, std::wstring path, FileData * const *data) +{ + if (path.length() > 0) { + path.append(L"\\"); + } + + for (DirectoryTree::const_leaf_iterator iter = node->leafsBegin(); iter != node->leafsEnd(); ++iter) { + data[iter->getIndex()]->setSkip(false); + data[iter->getIndex()]->setOutputFileName(path.substr().append(ToWString(iter->getName())).c_str()); + } + + for (DirectoryTree::const_node_iterator iter = node->nodesBegin(); iter != node->nodesEnd(); ++iter) { + if ((*iter)->getData().index != -1) { + data[(*iter)->getData().index]->setSkip(false); + data[(*iter)->getData().index]->setOutputFileName(path.substr().append(ToWString((*iter)->getData().name)).c_str()); + } + mapToArchive(*iter, path.substr().append(ToWString((*iter)->getData().name)), data); + } +} + + +void InstallationManager::mapToArchive(const DirectoryTree::Node *baseNode) +{ + FileData* const *data; + size_t size; + m_CurrentArchive->getFileList(data, size); + + // first disable all files + folders, we will re-enable those present in baseNode + for (size_t i = 0; i < size; ++i) { + data[i]->setSkip(true); + } + + std::wstring currentPath; + + mapToArchive(baseNode, currentPath, data); +} + + +bool InstallationManager::unpackPackageTXT() +{ + return unpackSingleFile("package.txt"); +} + + +bool InstallationManager::unpackSingleFile(const QString &fileName) +{ + FileData* const *data; + size_t size; + m_CurrentArchive->getFileList(data, size); + + QString baseName = QFileInfo(fileName).fileName(); + + bool available = false; + for (size_t i = 0; i < size; ++i) { + if (_wcsicmp(data[i]->getFileName(), ToWString(fileName).c_str()) == 0) { + available = true; + data[i]->setSkip(false); + data[i]->setOutputFileName(ToWString(baseName).c_str()); + m_TempFilesToDelete.insert(baseName); + } else { + data[i]->setSkip(true); + } + } + + if (available) { + m_InstallationProgress.setWindowTitle(tr("Extracting files")); + m_InstallationProgress.setLabelText(QString()); + m_InstallationProgress.setValue(0); + m_InstallationProgress.setWindowModality(Qt::WindowModal); + m_InstallationProgress.show(); + + bool res = m_CurrentArchive->extract(ToWString(QDir::toNativeSeparators(QDir::tempPath())).c_str(), + new MethodCallback<InstallationManager, void, float>(this, &InstallationManager::updateProgress), + new MethodCallback<InstallationManager, void, LPCWSTR>(this, &InstallationManager::dummyProgressFile), + new MethodCallback<InstallationManager, void, LPCWSTR>(this, &InstallationManager::report7ZipError)); + + m_InstallationProgress.hide(); + + return res; + } else { + return false; + } +} + + +QString InstallationManager::extractFile(const QString &fileName) +{ + if (unpackSingleFile(fileName)) { + QString tempFileName = QDir::tempPath().append("/").append(QFileInfo(fileName).fileName()); + + m_FilesToDelete.insert(tempFileName); + + return tempFileName; + } else { + return QString(); + } +} + + +QString canonicalize(const QString &name) +{ + QString result(name); + if ((result.startsWith('/')) || + (result.startsWith('\\'))) { + result.remove(0, 1); + } + result.replace('/', '\\'); + + return result; +} + + +QStringList InstallationManager::extractFiles(const QStringList &filesOrig) +{ + QStringList files; + + foreach (const QString &file, filesOrig) { + files.append(canonicalize(file)); + } + + QStringList result; + + FileData* const *data; + size_t size; + m_CurrentArchive->getFileList(data, size); + + for (size_t i = 0; i < size; ++i) { + if (files.contains(ToQString(data[i]->getFileName()), Qt::CaseInsensitive)) { + const wchar_t *baseName = wcsrchr(data[i]->getFileName(), '\\'); + if (baseName == NULL) { + baseName = wcsrchr(data[i]->getFileName(), '/'); + } + if (baseName == NULL) { + qCritical("failed to find backslash in %ls", data[i]->getFileName()); + continue; + } + data[i]->setOutputFileName(baseName); + + result.append(QDir::tempPath().append("/").append(ToQString(baseName))); + + data[i]->setSkip(false); + m_TempFilesToDelete.insert(ToQString(baseName)); + } else { + data[i]->setSkip(true); + } + } + + m_InstallationProgress.setWindowTitle(tr("Extracting files")); + m_InstallationProgress.setLabelText(QString()); + m_InstallationProgress.setValue(0); + m_InstallationProgress.setWindowModality(Qt::WindowModal); + m_InstallationProgress.show(); + + // unpack only the files we need for the installer + if (!m_CurrentArchive->extract(ToWString(QDir::toNativeSeparators(QDir::tempPath())).c_str(), + new MethodCallback<InstallationManager, void, float>(this, &InstallationManager::updateProgress), + new MethodCallback<InstallationManager, void, LPCWSTR>(this, &InstallationManager::dummyProgressFile), + new MethodCallback<InstallationManager, void, LPCWSTR>(this, &InstallationManager::report7ZipError))) { + throw std::runtime_error("extracting failed"); + } + + m_InstallationProgress.hide(); + return result; +} + +IPluginInstaller::EInstallResult InstallationManager::installArchive(const QString &modName, const QString &archiveName) +{ + QString temp = modName; + bool iniTweaks; + if (install(archiveName, "", "modsdir", false, true, temp, iniTweaks)) { + return IPluginInstaller::RESULT_SUCCESS; + } else { + return IPluginInstaller::RESULT_FAILED; + } +} + + +DirectoryTree *InstallationManager::createFilesTree() +{ + FileData* const *data; + size_t size; + m_CurrentArchive->getFileList(data, size); + + QScopedPointer<DirectoryTree> result(new DirectoryTree); + + for (size_t i = 0; i < size; ++i) { + // the files are in a flat list where each file has a a full path relative to the archive root + // to create a tree, we have to iterate over each path component of each. This could be sped up by + // grouping the filenames first, but so far there doesn't seem to be an actual performance problem + DirectoryTree::Node *currentNode = result.data(); + + QString fileName = ToQString(data[i]->getFileName()); + QStringList components = fileName.split("\\"); + + // iterate over all path-components of this filename (including the filename itself) + for (QStringList::iterator componentIter = components.begin(); componentIter != components.end(); ++componentIter) { + if (componentIter->size() == 0) { + // empty string indicates fileName is actually only a directory name and we have + // completely processed it already. + break; + } + + bool exists = false; + // test if this path is already in the tree + for (DirectoryTree::node_iterator nodeIter = currentNode->nodesBegin(); nodeIter != currentNode->nodesEnd(); ++nodeIter) { + if ((*nodeIter)->getData().name == *componentIter) { + currentNode = *nodeIter; + exists = true; + break; + } + } + + if (!exists) { + if (componentIter + 1 == components.end()) { + // last path component. directory or file? + if (data[i]->isDirectory()) { + // this is a bit problematic. archives will often only list directories if they are empty, + // otherwise the dir only appears in the path of a file. In the UI however we allow the user + // to uncheck all files in a directory while keeping the dir checked. Those directories are + // currently not installed. + DirectoryTree::Node *newNode = new DirectoryTree::Node; + newNode->setData(DirectoryTreeInformation(*componentIter, i)); + currentNode->addNode(newNode, false); + currentNode = newNode; + } else { + currentNode->addLeaf(FileTreeInformation(*componentIter, i)); + } + } else { + DirectoryTree::Node *newNode = new DirectoryTree::Node; + newNode->setData(DirectoryTreeInformation(*componentIter, -1)); + currentNode->addNode(newNode, false); + currentNode = newNode; + } + } + } + } + + return result.take(); +} + + +bool InstallationManager::isSimpleArchiveTopLayer(const DirectoryTree::Node *node, bool bainStyle) +{ + // see if there is at least one directory that makes sense on the top level + for (DirectoryTree::const_node_iterator iter = node->nodesBegin(); iter != node->nodesEnd(); ++iter) { + if ((bainStyle && InstallationTester::isTopLevelDirectoryBain((*iter)->getData().name)) || + (!bainStyle && InstallationTester::isTopLevelDirectory((*iter)->getData().name))) { + qDebug("%s on the top level", (*iter)->getData().name.toUtf8().constData()); + return true; + } + } + + // see if there is a file that makes sense on the top level + for (DirectoryTree::const_leaf_iterator iter = node->leafsBegin(); iter != node->leafsEnd(); ++iter) { + if (InstallationTester::isTopLevelSuffix(iter->getName())) { + return true; + } + } + return false; +} + + +DirectoryTree::Node *InstallationManager::getSimpleArchiveBase(DirectoryTree *dataTree) +{ + DirectoryTree::Node *currentNode = dataTree; + + while (true) { + if (isSimpleArchiveTopLayer(currentNode, false)) { + return currentNode; + } else if ((currentNode->numLeafs() == 0) && + (currentNode->numNodes() == 1)) { + currentNode = *currentNode->nodesBegin(); + } else { + qDebug("not a simple archive"); + return NULL; + } + } +} + + +bool InstallationManager::checkBainPackage(DirectoryTree *dataTree) +{ + int numDirs = dataTree->numNodes(); + // each directory would have to serve as a top-level directory + for (DirectoryTree::const_node_iterator iter = dataTree->nodesBegin(); + iter != dataTree->nodesEnd(); ++iter) { + const QString &dirName = (*iter)->getData().name; + if ((dirName.compare("fomod", Qt::CaseInsensitive) == 0) || + (dirName.startsWith("--"))) { + --numDirs; + continue; + } + if (!isSimpleArchiveTopLayer(*iter, true)) { + qDebug("%s is not a top layer directory", (*iter)->getData().name.toUtf8().constData()); + return false; + } + } + + if (numDirs < 2) { + // a complex bain package contains at least 2 directories to choose from + qDebug("only %d dirs", numDirs); + return false; + } + + return true; +} + + +bool InstallationManager::checkNMMInstaller() +{ + return QFile::exists(m_NCCPath); +} + + +bool InstallationManager::checkFomodPackage(DirectoryTree *dataTree, QString &offset, bool &xmlInstaller) +{ + bool scriptInstaller = false; + xmlInstaller = false; + for (DirectoryTree::const_node_iterator iter = dataTree->nodesBegin(); + iter != dataTree->nodesEnd(); ++iter) { + const QString &dirName = (*iter)->getData().name; + if (dirName.compare("fomod", Qt::CaseInsensitive) == 0) { + for (DirectoryTree::const_leaf_iterator fileIter = (*iter)->leafsBegin(); + fileIter != (*iter)->leafsEnd(); ++fileIter) { + if (fileIter->getName().compare("ModuleConfig.xml", Qt::CaseInsensitive) == 0) { + xmlInstaller = true; + } else if (fileIter->getName().compare("script.cs", Qt::CaseInsensitive) == 0) { + scriptInstaller = true; + } + } + break; + } + } + if (!xmlInstaller && !scriptInstaller && (dataTree->numNodes() == 1) && (dataTree->numLeafs() == 0)) { + DirectoryTree::Node *node = *dataTree->nodesBegin(); + offset = node->getData().name; + return checkFomodPackage(node, offset, xmlInstaller); + } + + return (xmlInstaller || scriptInstaller); +} + + +void InstallationManager::updateProgress(float percentage) +{ + m_InstallationProgress.setValue(static_cast<int>(percentage * 100.0)); + if (m_InstallationProgress.wasCanceled()) { + m_CurrentArchive->cancel(); + m_InstallationProgress.reset(); + } +} + + +void InstallationManager::updateProgressFile(LPCWSTR fileName) +{ + m_InstallationProgress.setLabelText(QString::fromUtf16(fileName)); +} + + +void InstallationManager::report7ZipError(LPCWSTR errorMessage) +{ + reportError(QString::fromUtf16(errorMessage)); +} + + +QString InstallationManager::generateBackupName(const QString &directoryName) +{ + QString backupName = directoryName + "_backup"; + if (QDir(backupName).exists()) { + int idx = 2; + QString temp = backupName + QString::number(idx); + while (QDir(temp).exists()) { + ++idx; + temp = backupName + QString::number(idx); + } + backupName = temp; + } + return backupName; +} + + +bool InstallationManager::testOverwrite(const QString &modsDirectory, QString &modName) +{ + QString targetDirectory = QDir::fromNativeSeparators(modsDirectory.mid(0).append("\\").append(modName)); + + while (QDir(targetDirectory).exists()) { + QueryOverwriteDialog overwriteDialog(m_ParentWidget); + if (overwriteDialog.exec()) { + if (overwriteDialog.backup()) { + QString backupDirectory = generateBackupName(targetDirectory); + if (!copyDir(targetDirectory, backupDirectory, false)) { + reportError(tr("failed to create backup")); + return false; + } + } + if (overwriteDialog.action() == QueryOverwriteDialog::ACT_RENAME) { + bool ok = false; + QString name = QInputDialog::getText(m_ParentWidget, tr("Mod Name"), tr("Name"), + QLineEdit::Normal, modName, &ok); + if (ok && !name.isEmpty()) { + modName = name; + if (!ensureValidModName(modName)) {
+ return false;
+ }
+ targetDirectory = QDir::fromNativeSeparators(modsDirectory.mid(0).append("\\").append(modName)); + } + } else if (overwriteDialog.action() == QueryOverwriteDialog::ACT_REPLACE) { + // save original settings like categories. Because it makes sense + QString metaFilename = targetDirectory.mid(0).append("/meta.ini"); + QFile settingsFile(metaFilename); + QByteArray originalSettings; + if (settingsFile.open(QIODevice::ReadOnly)) { + originalSettings = settingsFile.readAll(); + settingsFile.close(); + } + + // remove the directory with all content, then recreate it empty + removeDir(targetDirectory); + if (!QDir().mkdir(targetDirectory)) { + // windows may keep the directory around for a moment, preventing its re-creation + Sleep(100); + QDir().mkdir(targetDirectory); + } + // restore the saved settings + if (settingsFile.open(QIODevice::WriteOnly)) { + settingsFile.write(originalSettings); + settingsFile.close(); + } else { + qCritical("failed to restore original settings: %s", metaFilename.toUtf8().constData()); + } + return true; + } else if (overwriteDialog.action() == QueryOverwriteDialog::ACT_MERGE) { + return true; + } + } else { + return false; + } + } + + QDir().mkdir(targetDirectory); + + return true; +} + + +void InstallationManager::fixModName(QString &name) +{ +// name = name.remove("^[ ]*").trimmed(); + name = name.simplified(); + while (name.endsWith('.')) name.chop(1);
+
+ name.replace(QRegExp("[<>:\"/\\|?*]"), "");
+
+ static QString invalidNames[] = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
+ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
+ for (int i = 0; i < sizeof(invalidNames) / sizeof(QString); ++i) {
+ if (name == invalidNames[i]) {
+ name = "";
+ break;
+ }
+ }
+} + + +bool InstallationManager::ensureValidModName(QString &name)
+{
+ fixModName(name);
+
+ while (name.isEmpty()) {
+ bool ok;
+ name = QInputDialog::getText(m_ParentWidget, tr("Invalid name"),
+ tr("The name you entered is invalid, please enter a different one."),
+ QLineEdit::Normal, "", &ok);
+ if (!ok) {
+ return false;
+ }
+ fixModName(name);
+ }
+ return true;
+}
+
+
+bool InstallationManager::doInstall(const QString &modsDirectory, QString &modName, int modID,
+ const QString &version, const QString &newestVersion, int categoryID) +{ + if (!ensureValidModName(modName)) {
+ return false;
+ }
+
+ // determine target directory + if (!testOverwrite(modsDirectory, modName)) { + return false; + } + + QString targetDirectory = QDir::fromNativeSeparators(modsDirectory.mid(0).append("\\").append(modName)); + + qDebug("installing to \"%s\"", targetDirectory.toUtf8().constData()); + + m_InstallationProgress.setWindowTitle(tr("Extracting files")); + m_InstallationProgress.setLabelText(QString()); + m_InstallationProgress.setValue(0); + m_InstallationProgress.setWindowModality(Qt::WindowModal); + m_InstallationProgress.show(); + + if (!m_CurrentArchive->extract(ToWString(QDir::toNativeSeparators(targetDirectory)).c_str(), + new MethodCallback<InstallationManager, void, float>(this, &InstallationManager::updateProgress), + new MethodCallback<InstallationManager, void, LPCWSTR>(this, &InstallationManager::updateProgressFile), + new MethodCallback<InstallationManager, void, LPCWSTR>(this, &InstallationManager::report7ZipError))) { + if (m_CurrentArchive->getLastError() == Archive::ERROR_EXTRACT_CANCELLED) { + return false; + } else { + throw std::runtime_error("extracting failed"); + } + } + + m_InstallationProgress.hide(); + + QSettings settingsFile(targetDirectory.mid(0).append("/meta.ini"), QSettings::IniFormat); + + // overwrite settings only if they are actually are available or haven't been set before + if ((modID != 0) || !settingsFile.contains("modid")) { + settingsFile.setValue("modid", modID); + } + if (!version.isEmpty() || !settingsFile.contains("version")) { + settingsFile.setValue("version", version); + } + if (!newestVersion.isEmpty() || !settingsFile.contains("newestVersion")) { + settingsFile.setValue("newestVersion", newestVersion); + } + // issue #51 used to overwrite the manually set categories + if (!settingsFile.contains("category")) { + settingsFile.setValue("category", QString::number(categoryID)); + } + settingsFile.setValue("installationFile", m_CurrentFile); + + return true; +} + + +void InstallationManager::openFile(const QString &fileName) +{ + unpackSingleFile(fileName); + + QString tempFileName = QDir::tempPath().append("/").append(QFileInfo(fileName).fileName()); + + SHELLEXECUTEINFOW execInfo; + memset(&execInfo, 0, sizeof(SHELLEXECUTEINFOW)); + execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); + execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + execInfo.lpVerb = L"open"; + std::wstring fileNameW = ToWString(tempFileName); + execInfo.lpFile = fileNameW.c_str(); + execInfo.nShow = SW_SHOWNORMAL; + if (!::ShellExecuteExW(&execInfo)) { + qCritical("failed to spawn %s: %d", tempFileName.toUtf8().constData(), ::GetLastError()); + } + + m_FilesToDelete.insert(tempFileName); +} + + +// copy and pasted from mo_dll +bool EndsWith(LPCWSTR string, LPCWSTR subString) +{ + size_t slen = wcslen(string); + size_t len = wcslen(subString); + if (slen < len) { + return false; + } + + for (size_t i = 0; i < len; ++i) { + if (towlower(string[slen - len + i]) != towlower(subString[i])) { + return false; + } + } + return true; +} + + +bool InstallationManager::installFomodInternal(DirectoryTree *&baseNode, const QString &fomodPath, const QString &modsDirectory, + int modID, const QString &version, const QString &newestVersion, int categoryID, + QString &modName, bool &manualRequest) +{ + qDebug("treating as fomod archive"); + + FileData* const *data; + size_t size; + m_CurrentArchive->getFileList(data, size); + wchar_t *installerFiles[] = { L"fomod\\info.xml", L"fomod\\ModuleConfig.xml", + L"fomod\\script.cs", L"fomod\\screenshot.png", NULL }; + for (size_t i = 0; i < size; ++i) { + data[i]->setSkip(true); + if (data[i]->getFileName() == NULL) { + qCritical("invalid archive file name"); + } + for (int fileIdx = 0; installerFiles[fileIdx] != NULL; ++fileIdx) { + if (EndsWith(data[i]->getFileName(), installerFiles[fileIdx])) { + wchar_t *baseName = wcsrchr(installerFiles[fileIdx], '\\'); + if (baseName != NULL) { + data[i]->setSkip(false); + data[i]->setOutputFileName(baseName); + m_TempFilesToDelete.insert(ToQString(baseName)); + } else { + qCritical("failed to find backslash in %ls", installerFiles[fileIdx]); + } + break; + } + } + if (EndsWith(data[i]->getFileName(), L".png") || + EndsWith(data[i]->getFileName(), L".jpg") || + EndsWith(data[i]->getFileName(), L".gif") || + EndsWith(data[i]->getFileName(), L".bmp")) { + const wchar_t *baseName = wcsrchr(data[i]->getFileName(), '\\'); + if (baseName == NULL) { + baseName = data[i]->getFileName(); + } else { + ++baseName; + } + data[i]->setSkip(false); + data[i]->setOutputFileName(baseName); + m_TempFilesToDelete.insert(ToQString(baseName)); + } + } + + m_InstallationProgress.setWindowTitle(tr("Preparing installer")); + m_InstallationProgress.setLabelText(QString()); + m_InstallationProgress.setValue(0); + m_InstallationProgress.setWindowModality(Qt::WindowModal); + m_InstallationProgress.show(); + + // unpack only the files we need for the installer + if (!m_CurrentArchive->extract(ToWString(QDir::toNativeSeparators(QDir::tempPath())).c_str(), + new MethodCallback<InstallationManager, void, float>(this, &InstallationManager::updateProgress), + new MethodCallback<InstallationManager, void, LPCWSTR>(this, &InstallationManager::dummyProgressFile), + new MethodCallback<InstallationManager, void, LPCWSTR>(this, &InstallationManager::report7ZipError))) { + throw std::runtime_error("extracting failed"); + } + + m_InstallationProgress.hide(); + + bool success = false; + try { + FomodInstallerDialog dialog(modName, fomodPath); + + FileData* const *data; + size_t size; + m_CurrentArchive->getFileList(data, size); + + // the installer will want to unpack screenshots... + for (size_t i = 0; i < size; ++i) { + data[i]->setSkip(true); + } + + dialog.initData(); + if (dialog.exec() == QDialog::Accepted) { + modName = dialog.getName(); + baseNode = dialog.updateTree(baseNode); + mapToArchive(baseNode); + + if (doInstall(modsDirectory, modName, modID, version, newestVersion, categoryID)) { + success = true; + } + } else { + if (dialog.manualRequested()) { + manualRequest = true; + modName = dialog.getName(); + } + } + } catch (const std::exception &e) { + reportError(tr("Installation as fomod failed: %1").arg(e.what())); + manualRequest = true; + } + return success; +} + + +bool InstallationManager::installFomodExternal(const QString &fileName, const QString &pluginsFileName, const QString &modDirectory) +{ + wchar_t binary[MAX_PATH]; + wchar_t parameters[1024]; // maximum: 2xMAX_PATH + approx 20 characters + wchar_t currentDirectory[MAX_PATH]; + + _snwprintf(binary, MAX_PATH, L"%ls", ToWString(QDir::toNativeSeparators(m_NCCPath)).c_str()); + _snwprintf(parameters, 1024, L"-g %ls -p \"%ls\" -i \"%ls\" \"%ls\"", + GameInfo::instance().getGameShortName().c_str(), + ToWString(QDir::toNativeSeparators(pluginsFileName)).c_str(), + ToWString(QDir::toNativeSeparators(fileName)).c_str(), + ToWString(QDir::toNativeSeparators(modDirectory)).c_str()); + _snwprintf(currentDirectory, MAX_PATH, L"%ls", ToWString(QFileInfo(m_NCCPath).absolutePath()).c_str()); + + GameInfo &gameInfo = GameInfo::instance(); + + QString binaryDestination = modDirectory.mid(0).append("/").append(ToQString(gameInfo.getBinaryName())); + + // NCC assumes the installation directory is the game directory and may try to access the binary to determine version information + QFile::copy(QDir::fromNativeSeparators(ToQString(gameInfo.getGameDirectory().append(L"\\").append(gameInfo.getBinaryName()))), + binaryDestination); + + ON_BLOCK_EXIT([&binaryDestination] { QFile::remove(binaryDestination); } ); + + SHELLEXECUTEINFOW execInfo = {0}; + execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); + execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + execInfo.hwnd = NULL; + execInfo.lpVerb = L"open"; + execInfo.lpFile = binary; + execInfo.lpParameters = parameters; + execInfo.lpDirectory = currentDirectory; + execInfo.nShow = SW_SHOW; + + if (!::ShellExecuteExW(&execInfo)) { + reportError(tr("failed to start %1").arg(m_NCCPath)); + return false; + } + + QProgressDialog busyDialog(tr("Running external installer.\nNote: This installer will not be aware of other installed mods!"), tr("Force Close"), 0, 0, m_ParentWidget); + busyDialog.setWindowModality(Qt::WindowModal); + bool confirmCancel = false; + busyDialog.show(); + bool finished = false; + while (true) { + QCoreApplication::processEvents(); + DWORD res = ::WaitForSingleObject(execInfo.hProcess, 100); + if (res == WAIT_OBJECT_0) { + finished = true; + break; + } else if ((busyDialog.wasCanceled()) || (res != WAIT_TIMEOUT)) { + if (!confirmCancel) { + confirmCancel = true; + busyDialog.hide(); + busyDialog.reset(); + busyDialog.show(); + busyDialog.setCancelButtonText(tr("Confirm")); + } else { + break; + } + } + } + + if (!finished) { + ::TerminateProcess(execInfo.hProcess, 1); + return false; + } + + DWORD exitCode = 128; + ::GetExitCodeProcess(execInfo.hProcess, &exitCode); + + ::CloseHandle(execInfo.hProcess); + + + if ((exitCode == 0) || (exitCode == 10)) { // 0 = success, 10 = incomplete installation + bool errorOccured = false; + { // move all installed files from the data directory one directory up + QDirIterator dirIter(QDir(modDirectory).absoluteFilePath("Data"), QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); + bool hasFiles = false; + while (dirIter.hasNext()) { + dirIter.next(); + QFileInfo fileInfo = dirIter.fileInfo(); + + QDir dir(fileInfo.absolutePath()); + dir.cdUp(); + QString newName = dir.absoluteFilePath(fileInfo.fileName()); + if (QFile::exists(newName)) { + if (!QFile::remove(newName)) { + qCritical("failed to overwrite %s", qPrintable(newName)); + errorOccured = true; + } + } + if (!QFile::rename(fileInfo.absoluteFilePath(), newName)) { + qCritical("failed to move %s to %s", qPrintable(fileInfo.absoluteFilePath()), qPrintable(newName)); + errorOccured = true; + } + hasFiles = true; + } + // recognition of canceled installation in the external installer is broken so we assume the installation was + // canceled if no files were installed + if (!hasFiles) { + exitCode = 11; + } + } + + QString dataDir = modDirectory.mid(0).append("/Data"); + if (!removeDir(dataDir)) { + qCritical("failed to remove data directory from %s", dataDir.toUtf8().constData()); + errorOccured = true; + } + if (errorOccured) { + reportError(tr("Finalization of the installation failed. The mod may or may not work correctly. See mo_interface.log for details")); + } + } else if (exitCode != 11) { // 11 = manually canceled + reportError(tr("installation failed (errorcode %1)").arg(exitCode)); + } + + if ((exitCode == 0) || (exitCode == 10)) { + return true; + } else { + // after cancelation or error the installer may leave the empty mod directory + if (!removeDir(modDirectory)) { + qCritical ("failed to remove empty mod directory %s", modDirectory.toUtf8().constData()); + } + return false; + } +} + + +bool InstallationManager::wasCancelled() +{ + return m_CurrentArchive->getLastError() == Archive::ERROR_EXTRACT_CANCELLED; +} + + +bool InstallationManager::install(const QString &fileName, const QString &pluginsFileName, const QString &modsDirectory, + bool preferIntegrated, bool enableQuickInstall, QString &modName, bool &hasIniTweaks) +{ + QFileInfo fileInfo(fileName); + bool success = false; + if (m_SupportedExtensions.find(fileInfo.suffix()) == m_SupportedExtensions.end()) { + reportError(tr("File format \"%1\" not supported").arg(fileInfo.completeSuffix())); + return false; + } + + // read out meta information from the download if available + int modID = 0; + QString version = ""; + QString newestVersion = ""; + int categoryID = 0; + + QString metaName = fileName.mid(0).append(".meta"); + if (QFile(metaName).exists()) { + QSettings metaFile(metaName, QSettings::IniFormat); + modID = metaFile.value("modID", 0).toInt(); + if (modName.isEmpty()) { + modName = metaFile.value("modName", "").toString(); + // it is possible we have a file-name but not the correct mod name. in this case, + // the stored mod name may be "\0" + if (modName.isEmpty() || (modName.length() < 2)) { + modName = metaFile.value("name", "").toString(); + } + } + version = metaFile.value("version", "").toString(); + newestVersion = metaFile.value("newestVersion", "").toString(); + unsigned int categoryIndex = CategoryFactory::instance().resolveNexusID(metaFile.value("category", 0).toInt()); + categoryID = CategoryFactory::instance().getCategoryID(categoryIndex); + } + + { // guess the mod name and mod if from the file name if there was no meta information + QString guessedModName; + int guessedModID = modID;
+ NexusInterface::interpretNexusFileName(QFileInfo(fileName).baseName(), guessedModName, guessedModID, false);
+ if ((modID == 0) && (guessedModID != -1)) {
+ modID = guessedModID; + } else if (modID != guessedModID) { + qDebug("passed mod id: %d, guessed id: %d", modID, guessedModID); + } + + if (modName.isEmpty()) { + modName = guessedModName; + } + } + fixModName(modName); + + qDebug("using mod name \"%s\" (id %d)", modName.toUtf8().constData(), modID); + m_CurrentFile = fileInfo.fileName(); + + // open the archive and construct the directory tree the installers work on + bool archiveOpen = m_CurrentArchive->open(ToWString(QDir::toNativeSeparators(fileName)).c_str(), + new MethodCallback<InstallationManager, void, LPSTR>(this, &InstallationManager::queryPassword)); + + DirectoryTree *filesTree = archiveOpen ? createFilesTree() : NULL; + +/* IPluginInstaller::EInstallResult installResult = IPluginInstaller::RESULT_NOTATTEMPTED; + + std::sort(m_Installers.begin(), m_Installers.end(), [] (IPluginInstaller *LHS, IPluginInstaller *RHS) { + return LHS->priority() > RHS->priority(); + }); + + foreach (IPluginInstaller *installer, m_Installers) { + // don't use inactive installers + if (!installer->isActive()) { + continue; + } + + // try only manual installers if that was requested + if ((installResult == IPluginInstaller::RESULT_MANUALREQUESTED) && !installer->isManualInstaller()) { + continue; + } + + try { + { // simple case + IPluginInstallerSimple *installerSimple = dynamic_cast<IPluginInstallerSimple*>(installer); + + if ((installerSimple != NULL) && + (filesTree != NULL) && (installer->isArchiveSupported(*filesTree))) { + installResult = installerSimple->install(modName, *filesTree); + if (installResult == IPluginInstaller::RESULT_SUCCESS) { + mapToArchive(filesTree); + if (!doInstall(modsDirectory, modName, modID, version, newestVersion, categoryID)) { + installResult = IPluginInstaller::RESULT_FAILED; + } + } + } + } + + { // custom case + IPluginInstallerCustom *installerCustom = dynamic_cast<IPluginInstallerCustom*>(installer); + if ((installerCustom != NULL) && + (((filesTree != NULL) && installer->isArchiveSupported(*filesTree)) || + ((filesTree == NULL) && installerCustom->isArchiveSupported(fileName)))) { + std::set<QString> installerExtensions = installerCustom->supportedExtensions(); + if (installerExtensions.find(fileInfo.suffix()) != installerExtensions.end()) { + installResult = installerCustom->install(modName, fileName); + } + } + } + } catch (const IncompatibilityException &e) { + qCritical("plugin \"%s\" incompatible: %s", + qPrintable(installer->name()), e.what()); + } + + // act upon the installation result. at this point the files have already been + // extracted to the correct location + switch (installResult) { + case IPluginInstaller::RESULT_CANCELED: + case IPluginInstaller::RESULT_FAILED: { + return false; + } break; + case IPluginInstaller::RESULT_SUCCESS: { + DirectoryTree::node_iterator iniTweakNode = filesTree->nodeFind(DirectoryTreeInformation("INI Tweaks")); + hasIniTweaks = (iniTweakNode != filesTree->nodesEnd()) && + ((*iniTweakNode)->numLeafs() != 0); + return true; + } break; + } + } + + reportError(tr("None of the available installer plugins were able to handle that archive")); + return false;*/ + + + + hasIniTweaks = false; + + + DirectoryTree::Node *baseNode = NULL; + bool manualRequest = false; + + if (!archiveOpen) { + reportError(tr("Failed to open \"%1\": %2").arg(QDir::toNativeSeparators(fileName)).arg(getErrorString(m_CurrentArchive->getLastError()))); + return false; + } + + // bundled fomod? + if ((baseNode == NULL) && !manualRequest) { + QStringList bundledFomods; + for (DirectoryTree::const_leaf_iterator fileIter = filesTree->leafsBegin(); fileIter != filesTree->leafsEnd(); ++fileIter) { + if (fileIter->getName().endsWith(".fomod", Qt::CaseInsensitive)) { + bundledFomods.append(fileIter->getName()); + } + } + QString bundledFomodInst; + if (bundledFomods.count() > 1) { + SelectionDialog selection(tr("This seems like a bundle of fomods, which one do you want to install?"), m_ParentWidget); + foreach (const QString &fomod, bundledFomods) { + selection.addChoice(fomod, fomod, QVariant()); + } + if (selection.exec() == QDialog::Accepted) { + bundledFomodInst = selection.getChoiceString(); + } else { + return false; + } + } else if (bundledFomods.count() == 1) { + bundledFomodInst = bundledFomods.at(0); + qDebug("archive contains fomod: %s", qPrintable(bundledFomodInst)); + } + if (!bundledFomodInst.isEmpty()) { + unpackSingleFile(bundledFomodInst); + m_CurrentArchive->close(); + return install(QDir::tempPath().append("/").append(bundledFomodInst), pluginsFileName, modsDirectory, preferIntegrated, + enableQuickInstall, modName, hasIniTweaks); + } + } + + // fomod installer? + if ((baseNode == NULL) && !manualRequest) { + QString fomodPath; + bool xmlInstaller = false; + if (checkFomodPackage(filesTree, fomodPath, xmlInstaller)) { + baseNode = filesTree; + bool nmmInstaller = checkNMMInstaller(); + + if (xmlInstaller || nmmInstaller) { + if (!xmlInstaller || (nmmInstaller && !preferIntegrated)) { + if (!ensureValidModName(modName) ||
+ !testOverwrite(modsDirectory, modName)) {
+ return false; + } + + QString targetDirectory = QDir::fromNativeSeparators(modsDirectory.mid(0).append("\\").append(modName)); + + if (installFomodExternal(fileName, pluginsFileName, targetDirectory)) { + QSettings settingsFile(targetDirectory.mid(0).append("/meta.ini"), QSettings::IniFormat); + + // overwrite settings only if they are actually are available or haven't been set before + if ((modID != 0) || !settingsFile.contains("modid")) { + settingsFile.setValue("modid", modID); + } + if (!version.isEmpty() || !settingsFile.contains("version")) { + settingsFile.setValue("version", version); + } + if (!newestVersion.isEmpty() || !settingsFile.contains("newestVersion")) { + settingsFile.setValue("newestVersion", newestVersion); + } + if (!settingsFile.contains("category")) { + settingsFile.setValue("category", QString::number(categoryID)); + } + settingsFile.setValue("installationFile", m_CurrentFile); + + success = true; + } + } else { + if (installFomodInternal(baseNode, fomodPath, modsDirectory, + modID, version, newestVersion, categoryID, + modName, manualRequest)) { + success = true; + } + } + if (success) { + DirectoryTree::node_iterator iniTweakNode = baseNode->nodeFind(DirectoryTreeInformation("INI Tweaks")); + hasIniTweaks = (iniTweakNode != baseNode->nodesEnd()) && + ((*iniTweakNode)->numLeafs() != 0); + } + if (baseNode != filesTree) { + delete baseNode; + } + } else { + if (QuestionBoxMemory::query(m_ParentWidget, Settings::instance().directInterface(), "missingNCC", + tr("Installer missing"), + tr("This package contains a scripted installer. To use this installer " + "you need the optional \"NCC\"-package and the .net runtime. " + "Do you want to continue, treating this as a manual installer?"), + QDialogButtonBox::Yes | QDialogButtonBox::Cancel) == QMessageBox::Yes) { + manualRequest = true; + } else { + MessageDialog::showMessage(tr("Please install NCC"), m_ParentWidget); + } + } + } + } + + // simple installer? + if ((baseNode == NULL) && enableQuickInstall && !manualRequest) { + baseNode = getSimpleArchiveBase(filesTree); + if (baseNode != NULL) { + qDebug("treating as simple archive (%d)", baseNode->numLeafs()); + SimpleInstallDialog dialog(modName, m_ParentWidget); + if (dialog.exec() == QDialog::Accepted) { + mapToArchive(baseNode); + modName = dialog.getName(); + if (doInstall(modsDirectory, modName, modID, version, newestVersion, categoryID)) { + success = true; + + DirectoryTree::node_iterator iniTweakNode = baseNode->nodeFind(DirectoryTreeInformation("INI Tweaks")); + hasIniTweaks = (iniTweakNode != baseNode->nodesEnd()) && + ((*iniTweakNode)->numLeafs() != 0); + } + } else { + if (dialog.manualRequested()) { + manualRequest = true; + modName = dialog.getName(); + } + } + } + } + + // bain complex package? + if ((baseNode == NULL) && !manualRequest) { + if (checkBainPackage(filesTree)) { + bool hasPackageTXT = unpackPackageTXT(); + + baseNode = filesTree; + qDebug("treating as complex archive (%d)", filesTree->numNodes()); + BainComplexInstallerDialog dialog(filesTree, modName, hasPackageTXT, m_ParentWidget); + if (dialog.exec() == QDialog::Accepted) { + modName = dialog.getName(); + // create a new tree with the selected directories mapped to the + // base directory. This is destructive on the original tree + baseNode = dialog.updateTree(baseNode); + mapToArchive(baseNode); + + if (doInstall(modsDirectory, modName, modID, version, newestVersion, categoryID)) { + success = true; + + DirectoryTree::node_iterator iniTweakNode = baseNode->nodeFind(DirectoryTreeInformation("INI Tweaks")); + hasIniTweaks = (iniTweakNode != baseNode->nodesEnd()) && + ((*iniTweakNode)->numLeafs() != 0); + } + delete baseNode; + } else { + if (dialog.manualRequested()) { + manualRequest = true; + modName = dialog.getName(); + } + } + QFile::remove(QDir::tempPath().append("/package.txt")); + } + } + + // final option: manual installer + if ((baseNode == NULL) || manualRequest) { + qDebug("offering installation dialog"); + InstallDialog dialog(filesTree, modName, m_ParentWidget); + connect(&dialog, SIGNAL(openFile(QString)), this, SLOT(openFile(QString))); + if (dialog.exec() == QDialog::Accepted) { + modName = dialog.getModName(); + baseNode = dialog.getDataTree(); + mapToArchive(baseNode); + if (doInstall(modsDirectory, modName, modID, version, newestVersion, categoryID)) { + success = true; + + DirectoryTree::node_iterator iniTweakNode = baseNode->nodeFind(DirectoryTreeInformation("INI Tweaks")); + hasIniTweaks = (iniTweakNode != baseNode->nodesEnd()) && + ((*iniTweakNode)->numLeafs() != 0); + } + delete baseNode; // baseNode is a new tree, independent of filesTree + } + } + + delete filesTree; + m_CurrentArchive->close(); + + for (std::set<QString>::iterator iter = m_FilesToDelete.begin(); + iter != m_FilesToDelete.end(); ++iter) { + QFile(*iter).remove(); + } + m_FilesToDelete.clear(); + + for (std::set<QString>::iterator iter = m_TempFilesToDelete.begin(); + iter != m_TempFilesToDelete.end(); ++iter) { + QFile(QDir::tempPath().append("/").append(*iter)).remove(); + } + + m_TempFilesToDelete.clear(); + + return success; +} + + + +QString InstallationManager::getErrorString(Archive::Error errorCode) +{ + switch (errorCode) { + case Archive::ERROR_NONE: { + return tr("no error"); + } break; + case Archive::ERROR_LIBRARY_NOT_FOUND: { + return tr("7z.dll not found"); + } break; + case Archive::ERROR_LIBRARY_INVALID: { + return tr("7z.dll isn't valid"); + } break; + case Archive::ERROR_ARCHIVE_NOT_FOUND: { + return tr("archive not found"); + } break; + case Archive::ERROR_FAILED_TO_OPEN_ARCHIVE: { + return tr("failed to open archive"); + } break; + case Archive::ERROR_INVALID_ARCHIVE_FORMAT: { + return tr("unsupported archive type"); + } break; + case Archive::ERROR_LIBRARY_ERROR: { + return tr("internal library error"); + } break; + case Archive::ERROR_ARCHIVE_INVALID: { + return tr("archive invalid"); + } break; + default: { + // this probably means the archiver.dll is newer than this + return tr("unknown archive error"); + } break; + } +} + + +void InstallationManager::registerInstaller(IPluginInstaller *installer) +{ + m_Installers.push_back(installer); + installer->setInstallationManager(this); + IPluginInstallerCustom *installerCustom = dynamic_cast<IPluginInstallerCustom*>(installer); + if (installerCustom != NULL) { + std::set<QString> extensions = installerCustom->supportedExtensions(); + m_SupportedExtensions.insert(extensions.begin(), extensions.end()); + } +} + +QStringList InstallationManager::getSupportedExtensions() const +{ + QStringList result; + foreach (const QString &extension, m_SupportedExtensions) { + result.append(extension); + } + return result; +} |
