From 7de78b6697b60ac20add32b5b9140b04da973be8 Mon Sep 17 00:00:00 2001 From: Silarn Date: Sun, 27 Jan 2019 03:43:18 -0600 Subject: Several Nexus API-related changes * Added 30-second ping so Nexus doesn't close our connection * Set an overall 5 minute timeout before we give up listening for auth * Fixed some old signal and slot connections * Added a button to revoke the current API authorization * Updated bulk endorsement function with new API changes --- src/main.cpp | 1480 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 740 insertions(+), 740 deletions(-) (limited to 'src/main.cpp') diff --git a/src/main.cpp b/src/main.cpp index 631ec5b5..a8942b1a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,740 +1,740 @@ -/* -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 . -*/ - - -#ifdef LEAK_CHECK_WITH_VLD -#include -#include -#endif // LEAK_CHECK_WITH_VLD - -#define WIN32_LEAN_AND_MEAN -#include -#include - -#include -#include -#include -#include "mainwindow.h" -#include -#include "modlist.h" -#include "profile.h" -#include "spawn.h" -#include "executableslist.h" -#include "singleinstance.h" -#include "utility.h" -#include "helper.h" -#include "logbuffer.h" -#include "selectiondialog.h" -#include "moapplication.h" -#include "tutorialmanager.h" -#include "nxmaccessmanager.h" -#include "instancemanager.h" -#include "moshortcut.h" -#include "organizercore.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include - - -#pragma comment(linker, "/manifestDependency:\"name='dlls' processorArchitecture='x86' version='1.0.0.0' type='win32' \"") - - -using namespace MOBase; -using namespace MOShared; - -bool createAndMakeWritable(const std::wstring &subPath) { - QString const dataPath = qApp->property("dataPath").toString(); - QString fullPath = dataPath + "/" + QString::fromStdWString(subPath); - - if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) { - QMessageBox::critical(nullptr, QObject::tr("Error"), - QObject::tr("Failed to create \"%1\". Your user " - "account probably lacks permission.") - .arg(fullPath)); - return false; - } else { - return true; - } -} - -bool bootstrap() -{ - // remove the temporary backup directory in case we're restarting after an update - QString backupDirectory = qApp->applicationDirPath() + "/update_backup"; - if (QDir(backupDirectory).exists()) { - shellDelete(QStringList(backupDirectory)); - } - - // cycle logfile - removeOldFiles(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()), - "usvfs*.log", 5, QDir::Name); - - if (!createAndMakeWritable(AppConfig::logPath())) { - return false; - } - - return true; -} - -LPTOP_LEVEL_EXCEPTION_FILTER prevUnhandledExceptionFilter = nullptr; - -static LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs) -{ - const std::wstring& dumpPath = OrganizerCore::crashDumpsPath(); - int dumpRes = - CreateMiniDump(exceptionPtrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str()); - if (!dumpRes) - qCritical("ModOrganizer has crashed, crash dump created."); - else - qCritical("ModOrganizer has crashed, CreateMiniDump failed (%d, error %lu).", dumpRes, GetLastError()); - - if (prevUnhandledExceptionFilter) - return prevUnhandledExceptionFilter(exceptionPtrs); - else - return EXCEPTION_CONTINUE_SEARCH; -} - -// Parses the first parseArgCount arguments of the current process command line and returns -// them in parsedArgs, the rest of the command line is returned untouched. -LPCWSTR UntouchedCommandLineArguments(int parseArgCount, std::vector& parsedArgs) -{ - LPCWSTR cmd = GetCommandLineW(); - LPCWSTR arg = nullptr; // to skip executable name - for (; parseArgCount >= 0 && *cmd; ++cmd) - { - if (*cmd == '"') { - int escaped = 0; - for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd) - escaped = *cmd == '\\' ? escaped + 1 : 0; - } - if (*cmd == ' ') { - if (arg) - if (cmd-1 > arg && *arg == '"' && *(cmd-1) == '"') - parsedArgs.push_back(std::wstring(arg+1, cmd-1)); - else - parsedArgs.push_back(std::wstring(arg, cmd)); - arg = cmd + 1; - --parseArgCount; - } - } - return cmd; -} - -static int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) { - PROCESS_INFORMATION pi{ 0 }; - STARTUPINFO si{ 0 }; - si.cb = sizeof(si); - std::wstring commandLineCopy = commandLine; - - if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL, workingDirectory, &si, &pi)) { - // A bit of a problem where to log the error message here, at least this way you can get the message - // using a either DebugView or a live debugger: - std::wostringstream ost; - ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError(); - OutputDebugStringW(ost.str().c_str()); - return -1; - } - - WaitForSingleObject(pi.hProcess, INFINITE); - - DWORD exitCode = (DWORD)-1; - ::GetExitCodeProcess(pi.hProcess, &exitCode); - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - return static_cast(exitCode); -} - -static DWORD WaitForProcess() { - -} - -static bool HaveWriteAccess(const std::wstring &path) -{ - bool writable = false; - - const static SECURITY_INFORMATION requestedFileInformation = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; - - DWORD length = 0; - if (!::GetFileSecurityW(path.c_str(), requestedFileInformation, nullptr, 0UL, &length) - && (::GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { - std::string tempBuffer; - tempBuffer.reserve(length); - PSECURITY_DESCRIPTOR security = (PSECURITY_DESCRIPTOR)tempBuffer.data(); - if (security - && ::GetFileSecurity(path.c_str(), requestedFileInformation, security, length, &length)) { - HANDLE token = nullptr; - const static DWORD tokenDesiredAccess = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ; - if (!::OpenThreadToken(::GetCurrentThread(), tokenDesiredAccess, TRUE, &token)) { - if (!::OpenProcessToken(::GetCurrentProcess(), tokenDesiredAccess, &token)) { - throw std::runtime_error("Unable to get any thread or process token"); - } - } - - HANDLE impersonatedToken = nullptr; - if (::DuplicateToken(token, SecurityImpersonation, &impersonatedToken)) { - GENERIC_MAPPING mapping = { 0xFFFFFFFF }; - mapping.GenericRead = FILE_GENERIC_READ; - mapping.GenericWrite = FILE_GENERIC_WRITE; - mapping.GenericExecute = FILE_GENERIC_EXECUTE; - mapping.GenericAll = FILE_ALL_ACCESS; - - DWORD genericAccessRights = FILE_GENERIC_WRITE; - ::MapGenericMask(&genericAccessRights, &mapping); - - PRIVILEGE_SET privileges = { 0 }; - DWORD grantedAccess = 0; - DWORD privilegesLength = sizeof(privileges); - BOOL result = 0; - if (::AccessCheck(security, impersonatedToken, genericAccessRights, &mapping, &privileges, &privilegesLength, &grantedAccess, &result)) { - writable = result != 0; - } - ::CloseHandle(impersonatedToken); - } - - ::CloseHandle(token); - } - } - return writable; -} - - -QString determineProfile(QStringList &arguments, const QSettings &settings) -{ - QString selectedProfileName = QString::fromUtf8(settings.value("selected_profile", "").toByteArray()); - { // see if there is a profile on the command line - int profileIndex = arguments.indexOf("-p", 1); - if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) { - qDebug("profile overwritten on command line"); - selectedProfileName = arguments.at(profileIndex + 1); - } - arguments.removeAt(profileIndex); - arguments.removeAt(profileIndex); - } - if (selectedProfileName.isEmpty()) { - qDebug("no configured profile"); - selectedProfileName = "Default"; - } else { - qDebug("configured profile: %s", qUtf8Printable(selectedProfileName)); - } - - return selectedProfileName; -} - -MOBase::IPluginGame *selectGame(QSettings &settings, QDir const &gamePath, MOBase::IPluginGame *game) -{ - settings.setValue("gameName", game->gameName()); - //Sadly, hookdll needs gamePath in order to run. So following code block is - //commented out - /*if (gamePath == game->gameDirectory()) { - settings.remove("gamePath"); - } else*/ { - QString gameDir = gamePath.absolutePath(); - game->setGamePath(gameDir); - settings.setValue("gamePath", QDir::toNativeSeparators(gameDir).toUtf8().constData()); - } - return game; //Woot -} - - -MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &settings, PluginContainer const &plugins) -{ - //Determine what game we are running where. Be very paranoid in case the - //user has done something odd. - //If the game name has been set up, use that. - QString gameName = settings.value("gameName", "").toString(); - if (!gameName.isEmpty()) { - MOBase::IPluginGame *game = plugins.managedGame(gameName); - if (game == nullptr) { - reportError(QObject::tr("Plugin to handle %1 no longer installed").arg(gameName)); - return nullptr; - } - QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray()); - if (gamePath == "") { - gamePath = game->gameDirectory().absolutePath(); - } - QDir gameDir(gamePath); - if (game->looksValid(gameDir)) { - return selectGame(settings, gameDir, game); - } - } - - //gameName wasn't set, or otherwise can't be found. Try looking through all - //the plugins using the gamePath - QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray()); - if (!gamePath.isEmpty()) { - QDir gameDir(gamePath); - //Look to see if one of the installed games binary file exists in the current - //game directory. - for (IPluginGame * const game : plugins.plugins()) { - if (game->looksValid(gameDir)) { - return selectGame(settings, gameDir, game); - } - } - } - - //The following code would try to determine the right game to mange but it would usually find the wrong one - //so it was commented out. - /* - //OK, we are in a new setup or existing info is useless. - //See if MO has been installed inside a game directory - for (IPluginGame * const game : plugins.plugins()) { - if (game->isInstalled() && moPath.startsWith(game->gameDirectory().absolutePath())) { - //Found it. - return selectGame(settings, game->gameDirectory(), game); - } - } - - //Try walking up the directory tree to see if MO has been installed inside a game - { - QDir gameDir(moPath); - do { - //Look to see if one of the installed games binary file exists in the current - //directory. - for (IPluginGame * const game : plugins.plugins()) { - if (game->looksValid(gameDir)) { - return selectGame(settings, gameDir, game); - } - } - //OK, chop off the last directory and try again - } while (gameDir.cdUp()); - } - */ - - //Then try a selection dialogue. - if (!gamePath.isEmpty() || !gameName.isEmpty()) { - reportError(QObject::tr("Could not use configuration settings for game \"%1\", path \"%2\"."). - arg(gameName).arg(gamePath)); - } - - SelectionDialog selection(QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32)); - - for (IPluginGame *game : plugins.plugins()) { - if (game->isInstalled()) { - QString path = game->gameDirectory().absolutePath(); - selection.addChoice(game->gameIcon(), game->gameName(), path, QVariant::fromValue(game)); - } - } - - selection.addChoice(QString("Browse..."), QString(), QVariant::fromValue(static_cast(nullptr))); - - while (selection.exec() != QDialog::Rejected) { - IPluginGame * game = selection.getChoiceData().value(); - if (game != nullptr) { - return selectGame(settings, game->gameDirectory(), game); - } - - gamePath = QFileDialog::getExistingDirectory( - nullptr, QObject::tr("Please select the game to manage"), QString(), - QFileDialog::ShowDirsOnly); - - if (!gamePath.isEmpty()) { - QDir gameDir(gamePath); - QList possibleGames; - for (IPluginGame * const game : plugins.plugins()) { - if (game->looksValid(gameDir)) { - possibleGames.append(game); - //return selectGame(settings, gameDir, game); - } - } - if (possibleGames.count() > 1) { - SelectionDialog browseSelection(QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32)); - for (IPluginGame *game : possibleGames) { - QString path = game->gameDirectory().absolutePath(); - browseSelection.addChoice(game->gameIcon(), game->gameName(), path, QVariant::fromValue(game)); - } - if (browseSelection.exec() == QDialog::Accepted) { - return selectGame(settings, gameDir, browseSelection.getChoiceData().value()); - } else { - reportError(QObject::tr("Canceled finding game in \"%1\".").arg(gamePath)); - } - } else if(possibleGames.count() == 1) { - return selectGame(settings, gameDir, possibleGames[0]); - } else { - reportError(QObject::tr("No game identified in \"%1\". The directory is required to contain " - "the game binary.").arg(gamePath)); - } - } - } - - return nullptr; -} - - -// extend path to include dll directory so plugins don't need a manifest -// (using AddDllDirectory would be an alternative to this but it seems fairly -// complicated esp. -// since it isn't easily accessible on Windows < 8 -// SetDllDirectory replaces other search directories and this seems to -// propagate to child processes) -void setupPath() -{ - static const int BUFSIZE = 4096; - - qDebug("MO at: %s", qUtf8Printable(QDir::toNativeSeparators( - QCoreApplication::applicationDirPath()))); - - QCoreApplication::setLibraryPaths(QStringList(QCoreApplication::applicationDirPath() + "/dlls") + QCoreApplication::libraryPaths()); - - boost::scoped_array oldPath(new TCHAR[BUFSIZE]); - DWORD offset = ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), BUFSIZE); - if (offset > BUFSIZE) { - oldPath.reset(new TCHAR[offset]); - ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), offset); - } - - std::wstring newPath(ToWString(QDir::toNativeSeparators( - QCoreApplication::applicationDirPath())) + L"\\dlls"); - newPath += L";"; - newPath += oldPath.get(); - - ::SetEnvironmentVariableW(L"PATH", newPath.c_str()); -} - -static void preloadSsl() -{ - QString appPath = QDir::toNativeSeparators(QCoreApplication::applicationDirPath()); - if (GetModuleHandleA("libeay32.dll")) - qWarning("libeay32.dll already loaded?!"); - else { - QString libeay32 = appPath + "\\libeay32.dll"; - if (!QFile::exists(libeay32)) - qWarning("libeay32.dll not found: %s", qUtf8Printable(libeay32)); - else if (!LoadLibraryW(libeay32.toStdWString().c_str())) - qWarning("failed to load: %s, %d", qUtf8Printable(libeay32), GetLastError()); - } - if (GetModuleHandleA("ssleay32.dll")) - qWarning("ssleay32.dll already loaded?!"); - else { - QString ssleay32 = appPath + "\\ssleay32.dll"; - if (!QFile::exists(ssleay32)) - qWarning("ssleay32.dll not found: %s", qUtf8Printable(ssleay32)); - else if (!LoadLibraryW(ssleay32.toStdWString().c_str())) - qWarning("failed to load: %s, %d", qUtf8Printable(ssleay32), GetLastError()); - } -} - -static QString getVersionDisplayString() -{ - return createVersionInfo().displayString(3); -} - -int runApplication(MOApplication &application, SingleInstance &instance, - const QString &splashPath) -{ - - qDebug("Starting Mod Organizer version %s revision %s", qUtf8Printable(getVersionDisplayString()), -#if defined(HGID) - HGID -#elif defined(GITID) - GITID -#else - "unknown" -#endif - ); - -#if !defined(QT_NO_SSL) - preloadSsl(); - qDebug("ssl support: %d", QSslSocket::supportsSsl()); -#else - qDebug("non-ssl build"); -#endif - - QString dataPath = application.property("dataPath").toString(); - qDebug("data path: %s", qUtf8Printable(dataPath)); - - if (!bootstrap()) { - reportError("failed to set up data paths"); - return 1; - } - - QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow); - - QStringList arguments = application.arguments(); - - try { - qDebug("Working directory: %s", qUtf8Printable(QDir::toNativeSeparators(QDir::currentPath()))); - - QSettings settings(dataPath + "/" - + QString::fromStdWString(AppConfig::iniFileName()), - QSettings::IniFormat); - - // global crashDumpType sits in OrganizerCore to make a bit less ugly to update it when the settings are changed during runtime - OrganizerCore::setGlobalCrashDumpsType(settings.value("Settings/crash_dumps_type", static_cast(CrashDumpsType::Mini)).toInt()); - - qDebug("Loaded settings:"); - settings.beginGroup("Settings"); - for (auto k : settings.allKeys()) - if (!k.contains("username") && !k.contains("password")) - qDebug(" %s=%s", k.toUtf8().data(), settings.value(k).toString().toUtf8().data()); - settings.endGroup(); - - - qDebug("initializing core"); - OrganizerCore organizer(settings); - if (!organizer.bootstrap()) { - reportError("failed to set up data paths"); - return 1; - } - qDebug("initialize plugins"); - PluginContainer pluginContainer(&organizer); - pluginContainer.loadPlugins(); - - MOBase::IPluginGame *game = determineCurrentGame( - application.applicationDirPath(), settings, pluginContainer); - if (game == nullptr) { - return 1; - } - if (splashPath.startsWith(':')) { - // currently using MO splash, see if the plugin contains one - QString pluginSplash - = QString(":/%1/splash").arg(game->gameShortName()); - QImage image(pluginSplash); - if (!image.isNull()) { - image.save(dataPath + "/splash.png"); - } else { - qDebug("no plugin splash"); - } - } - - organizer.setManagedGame(game); - organizer.createDefaultProfile(); - - if (!settings.contains("game_edition")) { - QStringList editions = game->gameVariants(); - if (editions.size() > 1) { - SelectionDialog selection( - QObject::tr("Please select the game edition you have (MO can't " - "start the game correctly if this is set " - "incorrectly!)"), - nullptr); - selection.setWindowFlag(Qt::WindowStaysOnTopHint, true); - int index = 0; - for (const QString &edition : editions) { - selection.addChoice(edition, "", index++); - } - if (selection.exec() == QDialog::Rejected) { - return 1; - } else { - settings.setValue("game_edition", selection.getChoiceString()); - } - } - } - game->setGameVariant(settings.value("game_edition").toString()); - - qDebug("managing game at %s", qUtf8Printable(QDir::toNativeSeparators( - game->gameDirectory().absolutePath()))); - - organizer.updateExecutablesList(settings); - - QString selectedProfileName = determineProfile(arguments, settings); - organizer.setCurrentProfile(selectedProfileName); - - // if we have a command line parameter, it is either a nxm link or - // a binary to start - if (arguments.size() > 1) { - if (MOShortcut shortcut{ arguments.at(1) }) { - if (shortcut.hasExecutable()) { - try { - organizer.runShortcut(shortcut); - return 0; - } - catch (const std::exception &e) { - reportError( - QObject::tr("failed to start shortcut: %1").arg(e.what())); - return 1; - } - } - } - else if (OrganizerCore::isNxmLink(arguments.at(1))) { - qDebug("starting download from command line: %s", - qUtf8Printable(arguments.at(1))); - organizer.externalMessage(arguments.at(1)); - } - else { - QString exeName = arguments.at(1); - qDebug("starting %s from command line", qUtf8Printable(exeName)); - arguments.removeFirst(); // remove application name (ModOrganizer.exe) - arguments.removeFirst(); // remove binary name - // pass the remaining parameters to the binary - try { - organizer.startApplication(exeName, arguments, QString(), QString()); - return 0; - } - catch (const std::exception &e) { - reportError( - QObject::tr("failed to start application: %1").arg(e.what())); - return 1; - } - } - } - - QPixmap pixmap(splashPath); - QSplashScreen splash(pixmap); - splash.show(); - splash.activateWindow(); - - QString apiKey; - if (organizer.settings().getNexusApiKey(apiKey)) { - NexusInterface::instance(&pluginContainer)->getAccessManager()->apiCheck(apiKey); - } - - qDebug("initializing tutorials"); - TutorialManager::init( - qApp->applicationDirPath() + "/" - + QString::fromStdWString(AppConfig::tutorialsPath()) + "/", - &organizer); - - if (!application.setStyleFile(settings.value("Settings/style", "").toString())) { - // disable invalid stylesheet - settings.setValue("Settings/style", ""); - } - - int res = 1; - { // scope to control lifetime of mainwindow - // set up main window and its data structures - MainWindow mainWindow(settings, organizer, pluginContainer); - - QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application, - SLOT(setStyleFile(QString))); - QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer, - SLOT(externalMessage(QString))); - - mainWindow.processUpdates(); - mainWindow.readSettings(); - - qDebug("displaying main window"); - mainWindow.show(); - mainWindow.activateWindow(); - - splash.finish(&mainWindow); - return application.exec(); - } - } catch (const std::exception &e) { - reportError(e.what()); - return 1; - } -} - - -int main(int argc, char *argv[]) -{ - //Should allow for better scaling of ui with higher resolution displays - QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - - if (argc >= 4) { - std::vector arg; - auto args = UntouchedCommandLineArguments(2, arg); - if (arg[0] == L"launch") - return SpawnWaitProcess(arg[1].c_str(), args); - } - - MOApplication application(argc, argv); - QStringList arguments = application.arguments(); - - setupPath(); - - bool forcePrimary = false; - if (arguments.contains("update")) { - arguments.removeAll("update"); - forcePrimary = true; - } - - MOShortcut moshortcut{ arguments.size() > 1 ? arguments.at(1) : "" }; - - SingleInstance instance(forcePrimary); - if (!instance.primaryInstance()) { - if (moshortcut || - arguments.size() > 1 && OrganizerCore::isNxmLink(arguments.at(1))) - { - qDebug("not primary instance, sending shortcut/download message"); - instance.sendMessage(arguments.at(1)); - return 0; - } else if (arguments.size() == 1) { - QMessageBox::information( - nullptr, QObject::tr("Mod Organizer"), - QObject::tr("An instance of Mod Organizer is already running")); - return 0; - } - } // we continue for the primary instance OR if MO was called with parameters - - do { - QString dataPath; - - try { - InstanceManager& instanceManager = InstanceManager::instance(); - if (moshortcut && moshortcut.hasInstance()) - instanceManager.overrideInstance(moshortcut.instance()); - dataPath = instanceManager.determineDataPath(); - } catch (const std::exception &e) { - if (strcmp(e.what(),"Canceled")) - QMessageBox::critical(nullptr, QObject::tr("Failed to set up instance"), e.what()); - return 1; - } - application.setProperty("dataPath", dataPath); - - // initialize dump collection only after "dataPath" since the crashes are stored under it - prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); - - LogBuffer::init(1000, QtDebugMsg, qApp->property("dataPath").toString() + "/logs/mo_interface.log"); - - QString splash = dataPath + "/splash.png"; - if (!QFile::exists(dataPath + "/splash.png")) { - splash = ":/MO/gui/splash"; - } - - int result = runApplication(application, instance, splash); - if (result != INT_MAX) { - return result; - } - argc = 1; - moshortcut = MOShortcut(""); - } while (true); -} +/* +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 . +*/ + + +#ifdef LEAK_CHECK_WITH_VLD +#include +#include +#endif // LEAK_CHECK_WITH_VLD + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include +#include +#include "mainwindow.h" +#include +#include "modlist.h" +#include "profile.h" +#include "spawn.h" +#include "executableslist.h" +#include "singleinstance.h" +#include "utility.h" +#include "helper.h" +#include "logbuffer.h" +#include "selectiondialog.h" +#include "moapplication.h" +#include "tutorialmanager.h" +#include "nxmaccessmanager.h" +#include "instancemanager.h" +#include "moshortcut.h" +#include "organizercore.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + + +#pragma comment(linker, "/manifestDependency:\"name='dlls' processorArchitecture='x86' version='1.0.0.0' type='win32' \"") + + +using namespace MOBase; +using namespace MOShared; + +bool createAndMakeWritable(const std::wstring &subPath) { + QString const dataPath = qApp->property("dataPath").toString(); + QString fullPath = dataPath + "/" + QString::fromStdWString(subPath); + + if (!QDir(fullPath).exists() && !QDir().mkdir(fullPath)) { + QMessageBox::critical(nullptr, QObject::tr("Error"), + QObject::tr("Failed to create \"%1\". Your user " + "account probably lacks permission.") + .arg(fullPath)); + return false; + } else { + return true; + } +} + +bool bootstrap() +{ + // remove the temporary backup directory in case we're restarting after an update + QString backupDirectory = qApp->applicationDirPath() + "/update_backup"; + if (QDir(backupDirectory).exists()) { + shellDelete(QStringList(backupDirectory)); + } + + // cycle logfile + removeOldFiles(qApp->property("dataPath").toString() + "/" + QString::fromStdWString(AppConfig::logPath()), + "usvfs*.log", 5, QDir::Name); + + if (!createAndMakeWritable(AppConfig::logPath())) { + return false; + } + + return true; +} + +LPTOP_LEVEL_EXCEPTION_FILTER prevUnhandledExceptionFilter = nullptr; + +static LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs) +{ + const std::wstring& dumpPath = OrganizerCore::crashDumpsPath(); + int dumpRes = + CreateMiniDump(exceptionPtrs, OrganizerCore::getGlobalCrashDumpsType(), dumpPath.c_str()); + if (!dumpRes) + qCritical("ModOrganizer has crashed, crash dump created."); + else + qCritical("ModOrganizer has crashed, CreateMiniDump failed (%d, error %lu).", dumpRes, GetLastError()); + + if (prevUnhandledExceptionFilter) + return prevUnhandledExceptionFilter(exceptionPtrs); + else + return EXCEPTION_CONTINUE_SEARCH; +} + +// Parses the first parseArgCount arguments of the current process command line and returns +// them in parsedArgs, the rest of the command line is returned untouched. +LPCWSTR UntouchedCommandLineArguments(int parseArgCount, std::vector& parsedArgs) +{ + LPCWSTR cmd = GetCommandLineW(); + LPCWSTR arg = nullptr; // to skip executable name + for (; parseArgCount >= 0 && *cmd; ++cmd) + { + if (*cmd == '"') { + int escaped = 0; + for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd) + escaped = *cmd == '\\' ? escaped + 1 : 0; + } + if (*cmd == ' ') { + if (arg) + if (cmd-1 > arg && *arg == '"' && *(cmd-1) == '"') + parsedArgs.push_back(std::wstring(arg+1, cmd-1)); + else + parsedArgs.push_back(std::wstring(arg, cmd)); + arg = cmd + 1; + --parseArgCount; + } + } + return cmd; +} + +static int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine) { + PROCESS_INFORMATION pi{ 0 }; + STARTUPINFO si{ 0 }; + si.cb = sizeof(si); + std::wstring commandLineCopy = commandLine; + + if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL, workingDirectory, &si, &pi)) { + // A bit of a problem where to log the error message here, at least this way you can get the message + // using a either DebugView or a live debugger: + std::wostringstream ost; + ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError(); + OutputDebugStringW(ost.str().c_str()); + return -1; + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exitCode = (DWORD)-1; + ::GetExitCodeProcess(pi.hProcess, &exitCode); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return static_cast(exitCode); +} + +static DWORD WaitForProcess() { + +} + +static bool HaveWriteAccess(const std::wstring &path) +{ + bool writable = false; + + const static SECURITY_INFORMATION requestedFileInformation = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; + + DWORD length = 0; + if (!::GetFileSecurityW(path.c_str(), requestedFileInformation, nullptr, 0UL, &length) + && (::GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { + std::string tempBuffer; + tempBuffer.reserve(length); + PSECURITY_DESCRIPTOR security = (PSECURITY_DESCRIPTOR)tempBuffer.data(); + if (security + && ::GetFileSecurity(path.c_str(), requestedFileInformation, security, length, &length)) { + HANDLE token = nullptr; + const static DWORD tokenDesiredAccess = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ; + if (!::OpenThreadToken(::GetCurrentThread(), tokenDesiredAccess, TRUE, &token)) { + if (!::OpenProcessToken(::GetCurrentProcess(), tokenDesiredAccess, &token)) { + throw std::runtime_error("Unable to get any thread or process token"); + } + } + + HANDLE impersonatedToken = nullptr; + if (::DuplicateToken(token, SecurityImpersonation, &impersonatedToken)) { + GENERIC_MAPPING mapping = { 0xFFFFFFFF }; + mapping.GenericRead = FILE_GENERIC_READ; + mapping.GenericWrite = FILE_GENERIC_WRITE; + mapping.GenericExecute = FILE_GENERIC_EXECUTE; + mapping.GenericAll = FILE_ALL_ACCESS; + + DWORD genericAccessRights = FILE_GENERIC_WRITE; + ::MapGenericMask(&genericAccessRights, &mapping); + + PRIVILEGE_SET privileges = { 0 }; + DWORD grantedAccess = 0; + DWORD privilegesLength = sizeof(privileges); + BOOL result = 0; + if (::AccessCheck(security, impersonatedToken, genericAccessRights, &mapping, &privileges, &privilegesLength, &grantedAccess, &result)) { + writable = result != 0; + } + ::CloseHandle(impersonatedToken); + } + + ::CloseHandle(token); + } + } + return writable; +} + + +QString determineProfile(QStringList &arguments, const QSettings &settings) +{ + QString selectedProfileName = QString::fromUtf8(settings.value("selected_profile", "").toByteArray()); + { // see if there is a profile on the command line + int profileIndex = arguments.indexOf("-p", 1); + if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) { + qDebug("profile overwritten on command line"); + selectedProfileName = arguments.at(profileIndex + 1); + } + arguments.removeAt(profileIndex); + arguments.removeAt(profileIndex); + } + if (selectedProfileName.isEmpty()) { + qDebug("no configured profile"); + selectedProfileName = "Default"; + } else { + qDebug("configured profile: %s", qUtf8Printable(selectedProfileName)); + } + + return selectedProfileName; +} + +MOBase::IPluginGame *selectGame(QSettings &settings, QDir const &gamePath, MOBase::IPluginGame *game) +{ + settings.setValue("gameName", game->gameName()); + //Sadly, hookdll needs gamePath in order to run. So following code block is + //commented out + /*if (gamePath == game->gameDirectory()) { + settings.remove("gamePath"); + } else*/ { + QString gameDir = gamePath.absolutePath(); + game->setGamePath(gameDir); + settings.setValue("gamePath", QDir::toNativeSeparators(gameDir).toUtf8().constData()); + } + return game; //Woot +} + + +MOBase::IPluginGame *determineCurrentGame(QString const &moPath, QSettings &settings, PluginContainer const &plugins) +{ + //Determine what game we are running where. Be very paranoid in case the + //user has done something odd. + //If the game name has been set up, use that. + QString gameName = settings.value("gameName", "").toString(); + if (!gameName.isEmpty()) { + MOBase::IPluginGame *game = plugins.managedGame(gameName); + if (game == nullptr) { + reportError(QObject::tr("Plugin to handle %1 no longer installed").arg(gameName)); + return nullptr; + } + QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray()); + if (gamePath == "") { + gamePath = game->gameDirectory().absolutePath(); + } + QDir gameDir(gamePath); + if (game->looksValid(gameDir)) { + return selectGame(settings, gameDir, game); + } + } + + //gameName wasn't set, or otherwise can't be found. Try looking through all + //the plugins using the gamePath + QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray()); + if (!gamePath.isEmpty()) { + QDir gameDir(gamePath); + //Look to see if one of the installed games binary file exists in the current + //game directory. + for (IPluginGame * const game : plugins.plugins()) { + if (game->looksValid(gameDir)) { + return selectGame(settings, gameDir, game); + } + } + } + + //The following code would try to determine the right game to mange but it would usually find the wrong one + //so it was commented out. + /* + //OK, we are in a new setup or existing info is useless. + //See if MO has been installed inside a game directory + for (IPluginGame * const game : plugins.plugins()) { + if (game->isInstalled() && moPath.startsWith(game->gameDirectory().absolutePath())) { + //Found it. + return selectGame(settings, game->gameDirectory(), game); + } + } + + //Try walking up the directory tree to see if MO has been installed inside a game + { + QDir gameDir(moPath); + do { + //Look to see if one of the installed games binary file exists in the current + //directory. + for (IPluginGame * const game : plugins.plugins()) { + if (game->looksValid(gameDir)) { + return selectGame(settings, gameDir, game); + } + } + //OK, chop off the last directory and try again + } while (gameDir.cdUp()); + } + */ + + //Then try a selection dialogue. + if (!gamePath.isEmpty() || !gameName.isEmpty()) { + reportError(QObject::tr("Could not use configuration settings for game \"%1\", path \"%2\"."). + arg(gameName).arg(gamePath)); + } + + SelectionDialog selection(QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32)); + + for (IPluginGame *game : plugins.plugins()) { + if (game->isInstalled()) { + QString path = game->gameDirectory().absolutePath(); + selection.addChoice(game->gameIcon(), game->gameName(), path, QVariant::fromValue(game)); + } + } + + selection.addChoice(QString("Browse..."), QString(), QVariant::fromValue(static_cast(nullptr))); + + while (selection.exec() != QDialog::Rejected) { + IPluginGame * game = selection.getChoiceData().value(); + if (game != nullptr) { + return selectGame(settings, game->gameDirectory(), game); + } + + gamePath = QFileDialog::getExistingDirectory( + nullptr, QObject::tr("Please select the game to manage"), QString(), + QFileDialog::ShowDirsOnly); + + if (!gamePath.isEmpty()) { + QDir gameDir(gamePath); + QList possibleGames; + for (IPluginGame * const game : plugins.plugins()) { + if (game->looksValid(gameDir)) { + possibleGames.append(game); + //return selectGame(settings, gameDir, game); + } + } + if (possibleGames.count() > 1) { + SelectionDialog browseSelection(QObject::tr("Please select the game to manage"), nullptr, QSize(32, 32)); + for (IPluginGame *game : possibleGames) { + QString path = game->gameDirectory().absolutePath(); + browseSelection.addChoice(game->gameIcon(), game->gameName(), path, QVariant::fromValue(game)); + } + if (browseSelection.exec() == QDialog::Accepted) { + return selectGame(settings, gameDir, browseSelection.getChoiceData().value()); + } else { + reportError(QObject::tr("Canceled finding game in \"%1\".").arg(gamePath)); + } + } else if(possibleGames.count() == 1) { + return selectGame(settings, gameDir, possibleGames[0]); + } else { + reportError(QObject::tr("No game identified in \"%1\". The directory is required to contain " + "the game binary.").arg(gamePath)); + } + } + } + + return nullptr; +} + + +// extend path to include dll directory so plugins don't need a manifest +// (using AddDllDirectory would be an alternative to this but it seems fairly +// complicated esp. +// since it isn't easily accessible on Windows < 8 +// SetDllDirectory replaces other search directories and this seems to +// propagate to child processes) +void setupPath() +{ + static const int BUFSIZE = 4096; + + qDebug("MO at: %s", qUtf8Printable(QDir::toNativeSeparators( + QCoreApplication::applicationDirPath()))); + + QCoreApplication::setLibraryPaths(QStringList(QCoreApplication::applicationDirPath() + "/dlls") + QCoreApplication::libraryPaths()); + + boost::scoped_array oldPath(new TCHAR[BUFSIZE]); + DWORD offset = ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), BUFSIZE); + if (offset > BUFSIZE) { + oldPath.reset(new TCHAR[offset]); + ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), offset); + } + + std::wstring newPath(ToWString(QDir::toNativeSeparators( + QCoreApplication::applicationDirPath())) + L"\\dlls"); + newPath += L";"; + newPath += oldPath.get(); + + ::SetEnvironmentVariableW(L"PATH", newPath.c_str()); +} + +static void preloadSsl() +{ + QString appPath = QDir::toNativeSeparators(QCoreApplication::applicationDirPath()); + if (GetModuleHandleA("libeay32.dll")) + qWarning("libeay32.dll already loaded?!"); + else { + QString libeay32 = appPath + "\\libeay32.dll"; + if (!QFile::exists(libeay32)) + qWarning("libeay32.dll not found: %s", qUtf8Printable(libeay32)); + else if (!LoadLibraryW(libeay32.toStdWString().c_str())) + qWarning("failed to load: %s, %d", qUtf8Printable(libeay32), GetLastError()); + } + if (GetModuleHandleA("ssleay32.dll")) + qWarning("ssleay32.dll already loaded?!"); + else { + QString ssleay32 = appPath + "\\ssleay32.dll"; + if (!QFile::exists(ssleay32)) + qWarning("ssleay32.dll not found: %s", qUtf8Printable(ssleay32)); + else if (!LoadLibraryW(ssleay32.toStdWString().c_str())) + qWarning("failed to load: %s, %d", qUtf8Printable(ssleay32), GetLastError()); + } +} + +static QString getVersionDisplayString() +{ + return createVersionInfo().displayString(3); +} + +int runApplication(MOApplication &application, SingleInstance &instance, + const QString &splashPath) +{ + + qDebug("Starting Mod Organizer version %s revision %s", qUtf8Printable(getVersionDisplayString()), +#if defined(HGID) + HGID +#elif defined(GITID) + GITID +#else + "unknown" +#endif + ); + +#if !defined(QT_NO_SSL) + preloadSsl(); + qDebug("ssl support: %d", QSslSocket::supportsSsl()); +#else + qDebug("non-ssl build"); +#endif + + QString dataPath = application.property("dataPath").toString(); + qDebug("data path: %s", qUtf8Printable(dataPath)); + + if (!bootstrap()) { + reportError("failed to set up data paths"); + return 1; + } + + QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow); + + QStringList arguments = application.arguments(); + + try { + qDebug("Working directory: %s", qUtf8Printable(QDir::toNativeSeparators(QDir::currentPath()))); + + QSettings settings(dataPath + "/" + + QString::fromStdWString(AppConfig::iniFileName()), + QSettings::IniFormat); + + // global crashDumpType sits in OrganizerCore to make a bit less ugly to update it when the settings are changed during runtime + OrganizerCore::setGlobalCrashDumpsType(settings.value("Settings/crash_dumps_type", static_cast(CrashDumpsType::Mini)).toInt()); + + qDebug("Loaded settings:"); + settings.beginGroup("Settings"); + for (auto k : settings.allKeys()) + if (!k.contains("username") && !k.contains("password") && !k.contains("nexus_api_key")) + qDebug(" %s=%s", k.toUtf8().data(), settings.value(k).toString().toUtf8().data()); + settings.endGroup(); + + + qDebug("initializing core"); + OrganizerCore organizer(settings); + if (!organizer.bootstrap()) { + reportError("failed to set up data paths"); + return 1; + } + qDebug("initialize plugins"); + PluginContainer pluginContainer(&organizer); + pluginContainer.loadPlugins(); + + MOBase::IPluginGame *game = determineCurrentGame( + application.applicationDirPath(), settings, pluginContainer); + if (game == nullptr) { + return 1; + } + if (splashPath.startsWith(':')) { + // currently using MO splash, see if the plugin contains one + QString pluginSplash + = QString(":/%1/splash").arg(game->gameShortName()); + QImage image(pluginSplash); + if (!image.isNull()) { + image.save(dataPath + "/splash.png"); + } else { + qDebug("no plugin splash"); + } + } + + organizer.setManagedGame(game); + organizer.createDefaultProfile(); + + if (!settings.contains("game_edition")) { + QStringList editions = game->gameVariants(); + if (editions.size() > 1) { + SelectionDialog selection( + QObject::tr("Please select the game edition you have (MO can't " + "start the game correctly if this is set " + "incorrectly!)"), + nullptr); + selection.setWindowFlag(Qt::WindowStaysOnTopHint, true); + int index = 0; + for (const QString &edition : editions) { + selection.addChoice(edition, "", index++); + } + if (selection.exec() == QDialog::Rejected) { + return 1; + } else { + settings.setValue("game_edition", selection.getChoiceString()); + } + } + } + game->setGameVariant(settings.value("game_edition").toString()); + + qDebug("managing game at %s", qUtf8Printable(QDir::toNativeSeparators( + game->gameDirectory().absolutePath()))); + + organizer.updateExecutablesList(settings); + + QString selectedProfileName = determineProfile(arguments, settings); + organizer.setCurrentProfile(selectedProfileName); + + // if we have a command line parameter, it is either a nxm link or + // a binary to start + if (arguments.size() > 1) { + if (MOShortcut shortcut{ arguments.at(1) }) { + if (shortcut.hasExecutable()) { + try { + organizer.runShortcut(shortcut); + return 0; + } + catch (const std::exception &e) { + reportError( + QObject::tr("failed to start shortcut: %1").arg(e.what())); + return 1; + } + } + } + else if (OrganizerCore::isNxmLink(arguments.at(1))) { + qDebug("starting download from command line: %s", + qUtf8Printable(arguments.at(1))); + organizer.externalMessage(arguments.at(1)); + } + else { + QString exeName = arguments.at(1); + qDebug("starting %s from command line", qUtf8Printable(exeName)); + arguments.removeFirst(); // remove application name (ModOrganizer.exe) + arguments.removeFirst(); // remove binary name + // pass the remaining parameters to the binary + try { + organizer.startApplication(exeName, arguments, QString(), QString()); + return 0; + } + catch (const std::exception &e) { + reportError( + QObject::tr("failed to start application: %1").arg(e.what())); + return 1; + } + } + } + + QPixmap pixmap(splashPath); + QSplashScreen splash(pixmap); + splash.show(); + splash.activateWindow(); + + QString apiKey; + if (organizer.settings().getNexusApiKey(apiKey)) { + NexusInterface::instance(&pluginContainer)->getAccessManager()->apiCheck(apiKey); + } + + qDebug("initializing tutorials"); + TutorialManager::init( + qApp->applicationDirPath() + "/" + + QString::fromStdWString(AppConfig::tutorialsPath()) + "/", + &organizer); + + if (!application.setStyleFile(settings.value("Settings/style", "").toString())) { + // disable invalid stylesheet + settings.setValue("Settings/style", ""); + } + + int res = 1; + { // scope to control lifetime of mainwindow + // set up main window and its data structures + MainWindow mainWindow(settings, organizer, pluginContainer); + + QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application, + SLOT(setStyleFile(QString))); + QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer, + SLOT(externalMessage(QString))); + + mainWindow.processUpdates(); + mainWindow.readSettings(); + + qDebug("displaying main window"); + mainWindow.show(); + mainWindow.activateWindow(); + + splash.finish(&mainWindow); + return application.exec(); + } + } catch (const std::exception &e) { + reportError(e.what()); + return 1; + } +} + + +int main(int argc, char *argv[]) +{ + //Should allow for better scaling of ui with higher resolution displays + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + + if (argc >= 4) { + std::vector arg; + auto args = UntouchedCommandLineArguments(2, arg); + if (arg[0] == L"launch") + return SpawnWaitProcess(arg[1].c_str(), args); + } + + MOApplication application(argc, argv); + QStringList arguments = application.arguments(); + + setupPath(); + + bool forcePrimary = false; + if (arguments.contains("update")) { + arguments.removeAll("update"); + forcePrimary = true; + } + + MOShortcut moshortcut{ arguments.size() > 1 ? arguments.at(1) : "" }; + + SingleInstance instance(forcePrimary); + if (!instance.primaryInstance()) { + if (moshortcut || + arguments.size() > 1 && OrganizerCore::isNxmLink(arguments.at(1))) + { + qDebug("not primary instance, sending shortcut/download message"); + instance.sendMessage(arguments.at(1)); + return 0; + } else if (arguments.size() == 1) { + QMessageBox::information( + nullptr, QObject::tr("Mod Organizer"), + QObject::tr("An instance of Mod Organizer is already running")); + return 0; + } + } // we continue for the primary instance OR if MO was called with parameters + + do { + QString dataPath; + + try { + InstanceManager& instanceManager = InstanceManager::instance(); + if (moshortcut && moshortcut.hasInstance()) + instanceManager.overrideInstance(moshortcut.instance()); + dataPath = instanceManager.determineDataPath(); + } catch (const std::exception &e) { + if (strcmp(e.what(),"Canceled")) + QMessageBox::critical(nullptr, QObject::tr("Failed to set up instance"), e.what()); + return 1; + } + application.setProperty("dataPath", dataPath); + + // initialize dump collection only after "dataPath" since the crashes are stored under it + prevUnhandledExceptionFilter = SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); + + LogBuffer::init(1000, QtDebugMsg, qApp->property("dataPath").toString() + "/logs/mo_interface.log"); + + QString splash = dataPath + "/splash.png"; + if (!QFile::exists(dataPath + "/splash.png")) { + splash = ":/MO/gui/splash"; + } + + int result = runApplication(application, instance, splash); + if (result != INT_MAX) { + return result; + } + argc = 1; + moshortcut = MOShortcut(""); + } while (true); +} -- cgit v1.3.1