/* 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 "spawn.h" #include "report.h" #include "utility.h" #include "env.h" #include "envwindows.h" #include "envsecurity.h" #include "envmodule.h" #include "settings.h" #include #include #include #include #include #include #include #include "helper.h" #include #include #include #include #include using namespace MOBase; using namespace MOShared; namespace spawn { // details namespace { std::wstring pathEnv() { std::wstring s(4000, L' '); DWORD realSize = ::GetEnvironmentVariableW( L"PATH", s.data(), static_cast(s.size())); if (realSize > s.size()) { s.resize(realSize); ::GetEnvironmentVariableW( TEXT("PATH"), s.data(), static_cast(s.size())); } return s; } void setPathEnv(const std::wstring& s) { ::SetEnvironmentVariableW(L"PATH", s.c_str()); } DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle, HANDLE& threadHandle) { BOOL inheritHandles = FALSE; STARTUPINFO si = {}; si.cb = sizeof(si); // inherit handles if we plan to use stdout or stderr reroute if (sp.stdOut != INVALID_HANDLE_VALUE) { si.hStdOutput = sp.stdOut; inheritHandles = TRUE; si.dwFlags |= STARTF_USESTDHANDLES; } if (sp.stdErr != INVALID_HANDLE_VALUE) { si.hStdError = sp.stdErr; inheritHandles = TRUE; si.dwFlags |= STARTF_USESTDHANDLES; } const auto bin = QDir::toNativeSeparators(sp.binary.absoluteFilePath()).toStdWString(); std::wstring commandLine = L"\"" + bin + L"\""; if (sp.arguments[0] != L'\0') { commandLine += L" " + sp.arguments.toStdWString(); } QString moPath = QCoreApplication::applicationDirPath(); const auto oldPath = pathEnv(); setPathEnv(oldPath + L";" + QDir::toNativeSeparators(moPath).toStdWString()); PROCESS_INFORMATION pi; BOOL success = FALSE; const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath()).toStdWString(); if (sp.hooked) { success = ::CreateProcessHooked( nullptr, const_cast(commandLine.c_str()), nullptr, nullptr, inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr, cwd.c_str(), &si, &pi); } else { success = ::CreateProcess( nullptr, const_cast(commandLine.c_str()), nullptr, nullptr, inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr, cwd.c_str(), &si, &pi); } const auto e = GetLastError(); setPathEnv(oldPath); if (!success) { return e; } processHandle = pi.hProcess; threadHandle = pi.hThread; return ERROR_SUCCESS; } std::wstring makeRightsDetails(const env::FileSecurity& fs) { if (fs.rights.normalRights) { return L"(normal rights)"; } if (fs.rights.list.isEmpty()) { return L"(none)"; } std::wstring s = fs.rights.list.join("|").toStdWString(); if (!fs.rights.hasExecute) { s += L" (execute is missing)"; } return s; } std::wstring makeDetails(const SpawnParameters& sp, DWORD code) { std::wstring owner, rights; if (sp.binary.isFile()) { const auto fs = env::getFileSecurity(sp.binary.absoluteFilePath()); if (fs.error.isEmpty()) { owner = fs.owner.toStdWString(); rights = makeRightsDetails(fs); } else { owner = fs.error.toStdWString(); rights = fs.error.toStdWString(); } } else { owner = L"(file not found)"; rights = L"(file not found)"; } const bool cwdExists = (sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists()); const auto appDir = QCoreApplication::applicationDirPath(); const auto sep = QDir::separator(); const std::wstring usvfs_x86_dll = QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found"; const std::wstring usvfs_x64_dll = QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found"; const std::wstring usvfs_x86_proxy = QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" : L"not found"; const std::wstring usvfs_x64_proxy = QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" : L"not found"; std::wstring elevated; if (auto b=env::Environment().windowsInfo().isElevated()) { elevated = (*b ? L"yes" : L"no"); } else { elevated = L"(not available)"; } return fmt::format( L"Error {code} {codename}: {error}\n" L" . binary: '{bin}'\n" L" . owner: {owner}\n" L" . rights: {rights}\n" L" . arguments: '{args}'\n" L" . cwd: '{cwd}'{cwdexists}\n" L" . stdout: {stdout}, stderr: {stderr}, hooked: {hooked}\n" L" . usvfs x86:{x86_dll} x64:{x64_dll} proxy_x86:{x86_proxy} proxy_x64:{x64_proxy}\n" L" . MO elevated: {elevated}", fmt::arg(L"code", code), fmt::arg(L"codename", errorCodeName(code)), fmt::arg(L"bin", QDir::toNativeSeparators(sp.binary.absoluteFilePath()).toStdWString()), fmt::arg(L"owner", owner), fmt::arg(L"rights", rights), fmt::arg(L"error", formatSystemMessage(code)), fmt::arg(L"args", sp.arguments.toStdWString()), fmt::arg(L"cwd", QDir::toNativeSeparators(sp.currentDirectory.absolutePath()).toStdWString()), fmt::arg(L"cwdexists", (cwdExists ? L"" : L" (not found)")), fmt::arg(L"stdout", (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes")), fmt::arg(L"stderr", (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes")), fmt::arg(L"hooked", (sp.hooked ? L"yes" : L"no")), fmt::arg(L"x86_dll", usvfs_x86_dll), fmt::arg(L"x64_dll", usvfs_x64_dll), fmt::arg(L"x86_proxy", usvfs_x86_proxy), fmt::arg(L"x64_proxy", usvfs_x64_proxy), fmt::arg(L"elevated", elevated)); } void spawnFailed(const SpawnParameters& sp, DWORD code) { const auto details = QString::fromStdWString(makeDetails(sp, code)); log::error("{}", details); const auto title = QObject::tr("Cannot launch program"); const auto mainText = QObject::tr("Cannot start %1") .arg(sp.binary.fileName()); QString content; if (code == ERROR_INVALID_PARAMETER) { content = QObject::tr( "This error typically happens because an antivirus has deleted critical " "files from Mod Organizer's installation folder or has made them " "generally inaccessible. Add an exclusion for Mod Organizer's " "installation folder in your antivirus, reinstall Mod Organizer and try " "again."); } else if (code == ERROR_ACCESS_DENIED) { content = QObject::tr( "This error typically happens because an antivirus is preventing Mod " "Organizer from starting programs. Add an exclusion for Mod Organizer's " "installation folder in your antivirus and try again."); } else if (code == ERROR_FILE_NOT_FOUND) { content = QObject::tr("The file '%1' does not exist.") .arg(QDir::toNativeSeparators(sp.binary.absoluteFilePath())); } else { content = QString::fromStdWString(formatSystemMessage(code)); } QWidget *window = qApp->activeWindow(); if ((window != nullptr) && (!window->isVisible())) { window = nullptr; } MOBase::TaskDialog(window, title) .main(mainText) .content(content) .details(details) .exec(); } bool confirmRestartAsAdmin(const SpawnParameters& sp) { const auto details = QString::fromStdWString( makeDetails(sp, ERROR_ELEVATION_REQUIRED)); log::error("{}", details); const auto title = QObject::tr("Elevation required"); const auto mainText = QObject::tr("Cannot start %1") .arg(sp.binary.fileName()); const auto content = QObject::tr( "This program is requesting to run as administrator but Mod Organizer " "itself is not running as administrator. Running programs as administrator " "is typically unnecessary as long as the game and Mod Organizer have been " "installed outside \"Program Files\".\r\n\r\n" "You can restart Mod Organizer as administrator and try launching the " "program again."); QWidget *window = qApp->activeWindow(); if ((window != nullptr) && (!window->isVisible())) { window = nullptr; } log::debug("asking user to restart MO as administrator"); const auto r = MOBase::TaskDialog(window, title) .main(mainText) .content(content) .details(details) .button({ QObject::tr("Restart Mod Organizer as administrator"), QObject::tr("You must allow \"helper.exe\" to make changes to the system."), QMessageBox::Yes}) .button({ QObject::tr("Cancel"), QMessageBox::Cancel}) .exec(); return (r == QMessageBox::Yes); } void startBinaryAdmin(const SpawnParameters& sp) { if (!confirmRestartAsAdmin(sp)) { log::debug("user declined"); return; } log::info("restarting MO as administrator"); WCHAR cwd[MAX_PATH] = {}; if (!GetCurrentDirectory(MAX_PATH, cwd)) { cwd[0] = L'\0'; } if (Helper::adminLaunch( qApp->applicationDirPath().toStdWString(), qApp->applicationFilePath().toStdWString(), std::wstring(cwd))) { log::debug("exiting MO"); qApp->exit(0); } } } // namespace bool checkBinary(QWidget* parent, const SpawnParameters& sp) { if (!sp.binary.exists()) { spawnFailed(sp, ERROR_FILE_NOT_FOUND); return false; } return true; } struct SteamStatus { bool running=false; bool accessible=false; }; SteamStatus getSteamStatus() { SteamStatus ss; const auto ps = env::Environment().runningProcesses(); for (const auto& p : ps) { if ((p.name().compare("Steam.exe", Qt::CaseInsensitive) == 0) || (p.name().compare("SteamService.exe", Qt::CaseInsensitive) == 0)) { ss.running = true; ss.accessible = p.canAccess(); log::debug( "'{}' is running, accessible={}", p.name(), (ss.accessible ? "yes" : "no")); break; } } return ss; } bool startSteam(QWidget *widget) { log::debug("starting steam"); const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam"; const QString valueName = "SteamExe"; const QSettings steamSettings(keyName, QSettings::NativeFormat); const QString exe = steamSettings.value(valueName, "").toString(); if (exe.isEmpty()) { log::error( "can't start steam, registry value at '{}' is empty", keyName + "\\" + valueName); return false; } const QString program = QString("\"%1\"").arg(exe); // See if username and password supplied. If so, pass them into steam. QStringList args; QString username, password; if (Settings::instance().steam().login(username, password)) { args.push_back("-login"); args.push_back(username); if (password != "") { args.push_back(password); } } log::debug( "starting steam process:\n" " . program: '{}'\n" " . username={}, password={}", program, (username.isEmpty() ? "no" : "yes"), (password.isEmpty() ? "no" : "yes")); if (!QProcess::startDetached(program, args)) { reportError(QObject::tr("Failed to start \"%1\"").arg(program)); return false; } QMessageBox::information( widget, QObject::tr("Waiting"), QObject::tr("Please press OK once you're logged into steam.")); return true; } bool gameRequiresSteam(const QDir& gameDirectory, const Settings& settings) { static const std::vector files = { "steam_api.dll", "steam_api64.dll" }; for (const auto& file : files) { const QFileInfo fi(gameDirectory.absoluteFilePath(file)); if (fi.exists()) { log::debug("found '{}'", fi.absoluteFilePath()); return true; } } return false; } QuestionBoxMemory::Button confirmStartSteam(QWidget* parent, const SpawnParameters& sp) { return QuestionBoxMemory::query( parent, "steamQuery", sp.binary.fileName(), QObject::tr("Start Steam?"), QObject::tr("Steam is required to be running already to correctly start the game. " "Should MO try to start steam now?"), QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel); } QuestionBoxMemory::Button confirmRestartAsAdminForSteam(QWidget* parent, const SpawnParameters& sp) { return QuestionBoxMemory::query( parent, "steamAdminQuery", sp.binary.fileName(), QObject::tr("Steam: Access Denied"), QObject::tr("MO was denied access to the Steam process. This normally indicates that " "Steam is being run as administrator while MO is not. This can cause issues " "launching the game. It is recommended to not run Steam as administrator unless " "absolutely necessary.\n\n" "Restart MO as administrator?"), QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel); } bool checkSteam( QWidget* parent, const SpawnParameters& sp, const QDir& gameDirectory, const QString &steamAppID, const Settings& settings) { log::debug("checking steam"); if (!steamAppID.isEmpty()) { ::SetEnvironmentVariableW(L"SteamAPPId", steamAppID.toStdWString().c_str()); } else { ::SetEnvironmentVariableW(L"SteamAPPId", settings.steam().appID().toStdWString().c_str()); } if (!gameRequiresSteam(gameDirectory, settings)) { log::debug("games doesn't seem to require steam"); return true; } auto ss = getSteamStatus(); if (!ss.running) { log::debug("steam isn't running, asking to start steam"); const auto c = confirmStartSteam(parent, sp); if (c == QDialogButtonBox::Yes) { log::debug("user wants to start steam"); startSteam(parent); // double-check that Steam is started ss = getSteamStatus(); if (!ss.running) { log::error("could not start steam, continuing and hoping for the best"); return true; } } else if (c == QDialogButtonBox::No) { log::debug("user declined to start steam"); return true; } else { log::debug("user cancelled"); return false; } } if (ss.running && !ss.accessible) { log::debug("steam is running but is not accessible, asking to restart MO"); const auto c = confirmRestartAsAdminForSteam(parent, sp); if (c == QDialogButtonBox::Yes) { WCHAR cwd[MAX_PATH]; if (!GetCurrentDirectory(MAX_PATH, cwd)) { cwd[0] = L'\0'; } if (Helper::adminLaunch( qApp->applicationDirPath().toStdWString(), qApp->applicationFilePath().toStdWString(), std::wstring(cwd))) { log::debug("exiting MO"); qApp->exit(0); return false; } log::error("unable to relaunch MO as admin"); return false; } else if (c == QDialogButtonBox::No) { log::debug("user declined to restart MO, continuing"); return true; } else { log::debug("user cancelled"); return false; } } return true; } bool checkService() { SC_HANDLE serviceManagerHandle = NULL; SC_HANDLE serviceHandle = NULL; LPSERVICE_STATUS_PROCESS serviceStatus = NULL; LPQUERY_SERVICE_CONFIG serviceConfig = NULL; bool serviceRunning = true; DWORD bytesNeeded; try { serviceManagerHandle = OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); if (!serviceManagerHandle) { log::warn("failed to open service manager (query status) (error {})", GetLastError()); throw 1; } serviceHandle = OpenService(serviceManagerHandle, L"EventLog", SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); if (!serviceHandle) { log::warn("failed to open EventLog service (query status) (error {})", GetLastError()); throw 2; } if (QueryServiceConfig(serviceHandle, NULL, 0, &bytesNeeded) || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { log::warn("failed to get size of service config (error {})", GetLastError()); throw 3; } DWORD serviceConfigSize = bytesNeeded; serviceConfig = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, serviceConfigSize); if (!QueryServiceConfig(serviceHandle, serviceConfig, serviceConfigSize, &bytesNeeded)) { log::warn("failed to query service config (error {})", GetLastError()); throw 4; } if (serviceConfig->dwStartType == SERVICE_DISABLED) { log::error("Windows Event Log service is disabled!"); serviceRunning = false; } if (QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded) || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { log::warn("failed to get size of service status (error {})", GetLastError()); throw 5; } DWORD serviceStatusSize = bytesNeeded; serviceStatus = (LPSERVICE_STATUS_PROCESS)LocalAlloc(LMEM_FIXED, serviceStatusSize); if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, (LPBYTE)serviceStatus, serviceStatusSize, &bytesNeeded)) { log::warn("failed to query service status (error {})", GetLastError()); throw 6; } if (serviceStatus->dwCurrentState != SERVICE_RUNNING) { log::error("Windows Event Log service is not running"); serviceRunning = false; } } catch (int) { serviceRunning = false; } if (serviceStatus) { LocalFree(serviceStatus); } if (serviceConfig) { LocalFree(serviceConfig); } if (serviceHandle) { CloseServiceHandle(serviceHandle); } if (serviceManagerHandle) { CloseServiceHandle(serviceManagerHandle); } return serviceRunning; } bool checkEnvironment(QWidget* parent, const SpawnParameters& sp) { // Check if the Windows Event Logging service is running. For some reason, this seems to be // critical to the successful running of usvfs. if (!checkService()) { if (QuestionBoxMemory::query(parent, QString("eventLogService"), sp.binary.fileName(), QObject::tr("Windows Event Log Error"), QObject::tr("The Windows Event Log service is disabled and/or not running. This prevents" " USVFS from running properly. Your mods may not be working in the executable" " that you are launching. Note that you may have to restart MO and/or your PC" " after the service is fixed.\n\nContinue launching %1?").arg(sp.binary.fileName()), QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) { return false; } } return true; } bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, const Settings& settings) { for (auto exec : settings.executablesBlacklist().split(";")) { if (exec.compare(sp.binary.fileName(), Qt::CaseInsensitive) == 0) { if (QuestionBoxMemory::query(parent, QString("blacklistedExecutable"), sp.binary.fileName(), QObject::tr("Blacklisted Executable"), QObject::tr("The executable you are attempted to launch is blacklisted in the virtual file" " system. This will likely prevent the executable, and any executables that are" " launched by this one, from seeing any mods. This could extend to INI files, save" " games and any other virtualized files.\n\nContinue launching %1?").arg(sp.binary.fileName()), QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) { return false; } } } return true; } HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) { HANDLE processHandle, threadHandle; const auto e = spawn(sp, processHandle, threadHandle); switch (e) { case ERROR_SUCCESS: { ::CloseHandle(threadHandle); return processHandle; } case ERROR_ELEVATION_REQUIRED: { startBinaryAdmin(sp); return INVALID_HANDLE_VALUE; } default: { spawnFailed(sp, e); return INVALID_HANDLE_VALUE; } } } } // namespace