From 86bb01ba9eac879d3685c439ac9da0028bc4bc80 Mon Sep 17 00:00:00 2001 From: Mikaƫl Capelle Date: Tue, 17 May 2022 11:37:19 +0200 Subject: Convert everything to CRLF. --- src/spawn.cpp | 2164 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 1082 insertions(+), 1082 deletions(-) (limited to 'src/spawn.cpp') diff --git a/src/spawn.cpp b/src/spawn.cpp index 5b14c52c..858c3061 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -1,1082 +1,1082 @@ -/* -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 "settingsdialogworkarounds.h" -#include -#include -#include -#include -#include -#include "shared/appconfig.h" -#include "shared/windows_error.h" -#include -#include -#include -#include -#include -#include - -using namespace MOBase; -using namespace MOShared; - -namespace spawn::dialogs -{ - -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; -} - -QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more={}) -{ - 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)"; - } - - std::wstring f = - L"Error {code} {codename}{more}: {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" . MO elevated: {elevated}"; - - if (sp.hooked) { - f += L"\n . usvfs x86:{x86_dll} x64:{x64_dll} proxy_x86:{x86_proxy} proxy_x64:{x64_proxy}"; - } - - const std::wstring wmore = (more.isEmpty() ? L"" : (", " + more).toStdWString()); - - const auto s = fmt::format(f, - fmt::arg(L"code", code), - fmt::arg(L"codename", errorCodeName(code)), - fmt::arg(L"more", wmore), - 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)); - - return QString::fromStdWString(s); -} - -QString makeContent(const SpawnParameters& sp, DWORD code) -{ - if (code == ERROR_INVALID_PARAMETER) { - return 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) { - return 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) { - return QObject::tr("The file '%1' does not exist.") - .arg(QDir::toNativeSeparators(sp.binary.absoluteFilePath())); - } else if (code == ERROR_DIRECTORY) { - if (!sp.currentDirectory.exists()) { - return QObject::tr("The working directory '%1' does not exist.") - .arg(QDir::toNativeSeparators(sp.currentDirectory.absolutePath())); - } - } - - return QString::fromStdWString(formatSystemMessage(code)); -} - -QMessageBox::StandardButton badSteamReg( - QWidget* parent, const QString& keyName, const QString& valueName) -{ - const auto details = QString( - "can't start steam, registry value at '%1' is empty or doesn't exist") - .arg(keyName + "\\" + valueName); - - log::error("{}", details); - - return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam")) - .main(QObject::tr("Cannot start Steam")) - .content(QObject::tr( - "The path to the Steam executable cannot be found. You might try " - "reinstalling Steam.")) - .details(details) - .icon(QMessageBox::Critical) - .button({ - QObject::tr("Continue without starting Steam"), - QObject::tr("The program may fail to launch."), - QMessageBox::Yes}) - .button({ - QObject::tr("Cancel"), - QMessageBox::Cancel}) - .exec(); -} - -QMessageBox::StandardButton startSteamFailed( - QWidget* parent, - const QString& keyName, const QString& valueName, const QString& exe, - const SpawnParameters& sp, DWORD e) -{ - auto details = QString( - "a steam install was found in the registry at '%1': '%2'\n\n") - .arg(keyName + "\\" + valueName) - .arg(exe); - - details += makeDetails(sp, e); - - log::error("{}", details); - - return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam")) - .main(QObject::tr("Cannot start Steam")) - .content(makeContent(sp, e)) - .details(details) - .icon(QMessageBox::Critical) - .button({ - QObject::tr("Continue without starting Steam"), - QObject::tr("The program may fail to launch."), - QMessageBox::Yes}) - .button({ - QObject::tr("Cancel"), - QMessageBox::Cancel}) - .exec(); -} - -void spawnFailed(QWidget* parent, const SpawnParameters& sp, DWORD code) -{ - const auto details = 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()); - - MOBase::TaskDialog(parent, title) - .main(mainText) - .content(makeContent(sp, code)) - .details(details) - .icon(QMessageBox::Critical) - .exec(); -} - -void helperFailed( - QWidget* parent, DWORD code, const QString& why, const std::wstring& binary, - const std::wstring& cwd, const std::wstring& args) -{ - SpawnParameters sp; - sp.binary = QFileInfo(QString::fromStdWString(binary)); - sp.currentDirectory.setPath(QString::fromStdWString(cwd)); - sp.arguments = QString::fromStdWString(args); - - const auto details = makeDetails(sp, code, "in " + why); - log::error("{}", details); - - const auto title = QObject::tr("Cannot launch helper"); - - const auto mainText = QObject::tr("Cannot start %1") - .arg(sp.binary.fileName()); - - MOBase::TaskDialog(parent, title) - .main(mainText) - .content(makeContent(sp, code)) - .details(details) - .icon(QMessageBox::Critical) - .exec(); -} - -bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp) -{ - const auto details = 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."); - - log::debug("asking user to restart MO as administrator"); - - const auto r = MOBase::TaskDialog(parent, title) - .main(mainText) - .content(content) - .details(details) - .icon(QMessageBox::Question) - .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); -} - -QMessageBox::StandardButton confirmStartSteam( - QWidget* parent, const SpawnParameters& sp, const QString& details) -{ - const auto title = QObject::tr("Launch Steam"); - const auto mainText = QObject::tr("This program requires Steam"); - const auto content = QObject::tr( - "Mod Organizer has detected that this program likely requires Steam to be " - "running to function properly."); - - return MOBase::TaskDialog(parent, title) - .main(mainText) - .content(content) - .details(details) - .icon(QMessageBox::Question) - .button({ - QObject::tr("Start Steam"), - QMessageBox::Yes}) - .button({ - QObject::tr("Continue without starting Steam"), - QObject::tr("The program might fail to run."), - QMessageBox::No}) - .button({ - QObject::tr("Cancel"), - QMessageBox::Cancel}) - .remember("steamQuery", sp.binary.fileName()) - .exec(); -} - -QMessageBox::StandardButton confirmRestartAsAdminForSteam( - QWidget* parent, const SpawnParameters& sp) -{ - const auto title = QObject::tr("Elevation required"); - const auto mainText = QObject::tr("Steam is running as administrator"); - const auto content = QObject::tr( - "Running Steam as administrator is typically unnecessary and can cause " - "problems when Mod Organizer itself is not running as administrator." - "\r\n\r\n" - "You can restart Mod Organizer as administrator and try launching the " - "program again."); - - return MOBase::TaskDialog(parent, title) - .main(mainText) - .content(content) - .icon(QMessageBox::Question) - .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("Continue"), - QObject::tr("The program might fail to run."), - QMessageBox::No}) - .button({ - QObject::tr("Cancel"), - QMessageBox::Cancel}) - .remember("steamAdminQuery", sp.binary.fileName()) - .exec(); -} - -bool eventLogNotRunning( - QWidget* parent, const env::Service& s, const SpawnParameters& sp) -{ - const auto title = QObject::tr("Event Log not running"); - const auto mainText = QObject::tr("The Event Log service is not running"); - const auto content = QObject::tr( - "The Windows Event Log service is not running. This can prevent USVFS from " - "running properly and your mods may not be recognized by the program being " - "launched."); - - const auto r = MOBase::TaskDialog(parent, title) - .main(mainText) - .content(content) - .details(s.toString()) - .icon(QMessageBox::Question) - .remember("eventLogService", sp.binary.fileName()) - .button({ - QObject::tr("Continue"), - QObject::tr("Your mods might not work."), - QMessageBox::Yes}) - .button({ - QObject::tr("Cancel"), - QMessageBox::Cancel}) - .exec(); - - return (r == QMessageBox::Yes); -} - -QMessageBox::StandardButton confirmBlacklisted( - QWidget* parent, const SpawnParameters& sp, Settings& settings) -{ - const auto title = QObject::tr("Blacklisted program"); - const auto mainText = QObject::tr("The program %1 is blacklisted") - .arg(sp.binary.fileName()); - const auto content = QObject::tr( - "The program you are attempting to launch is blacklisted in the virtual " - "filesystem. This will likely prevent it from seeing any mods, INI files " - "or any other virtualized files."); - - const auto details = - "Executable: " + sp.binary.fileName() + "\n" - "Current blacklist: " + settings.executablesBlacklist(); - - auto r = MOBase::TaskDialog(parent, title) - .main(mainText) - .content(content) - .details(details) - .icon(QMessageBox::Question) - .remember("blacklistedExecutable", sp.binary.fileName()) - .button({ - QObject::tr("Continue"), - QObject::tr("Your mods might not work."), - QMessageBox::Yes}) - .button({ - QObject::tr("Change the blacklist"), - QMessageBox::Retry}) - .button({ - QObject::tr("Cancel"), - QMessageBox::Cancel}) - .exec(); - - if (r == QMessageBox::Retry) { - if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) { - r = QMessageBox::Cancel; - } - } - - return r; -} - -} // namespace - - -namespace spawn -{ - -void logSpawning(const SpawnParameters& sp, const QString& realCmd) -{ - log::debug( - "spawning binary:\n" - " . exe: '{}'\n" - " . args: '{}'\n" - " . cwd: '{}'\n" - " . steam id: '{}'\n" - " . hooked: {}\n" - " . stdout: {}\n" - " . stderr: {}\n" - " . real cmd: '{}'", - sp.binary.absoluteFilePath(), sp.arguments, - sp.currentDirectory.absolutePath(), sp.steamAppID, sp.hooked, - (sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"), - (sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"), - realCmd); -} - -DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) -{ - 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()); - const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath()); - - QString commandLine = "\"" + bin + "\""; - if (!sp.arguments.isEmpty()) { - commandLine += " " + sp.arguments; - } - - const QString moPath = QCoreApplication::applicationDirPath(); - const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath)); - - PROCESS_INFORMATION pi = {}; - BOOL success = FALSE; - - logSpawning(sp, commandLine); - - const auto wcommandLine = commandLine.toStdWString(); - const auto wcwd = cwd.toStdWString(); - - const DWORD flags = CREATE_BREAKAWAY_FROM_JOB; - - if (sp.hooked) { - success = ::CreateProcessHooked( - nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, - inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); - } else { - success = ::CreateProcess( - nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, - inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); - } - - const auto e = GetLastError(); - env::setPath(oldPath); - - if (!success) { - return e; - } - - processHandle = pi.hProcess; - ::CloseHandle(pi.hThread); - - return ERROR_SUCCESS; -} - -bool restartAsAdmin(QWidget* parent) -{ - WCHAR cwd[MAX_PATH] = {}; - if (!GetCurrentDirectory(MAX_PATH, cwd)) { - cwd[0] = L'\0'; - } - - if (!helper::adminLaunch( - parent, - qApp->applicationDirPath().toStdWString(), - qApp->applicationFilePath().toStdWString(), - std::wstring(cwd))) - { - log::error("admin launch failed"); - return false; - } - - log::debug("exiting MO"); - ExitModOrganizer(Exit::Force); - - return true; -} - -void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp) -{ - if (!dialogs::confirmRestartAsAdmin(parent, sp)) { - log::debug("user declined"); - return; - } - - log::info("restarting MO as administrator"); - restartAsAdmin(parent); -} - -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; -} - -QString makeSteamArguments(const QString& username, const QString& password) -{ - QString args; - - if (username != "") { - args += "-login " + username; - - if (password != "") { - args += " " + password; - } - } - - return args; -} - -bool startSteam(QWidget* parent) -{ - 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()) { - return (dialogs::badSteamReg(parent, keyName, valueName) == QMessageBox::Yes); - } - - SpawnParameters sp; - sp.binary = QFileInfo(exe); - - // See if username and password supplied. If so, pass them into steam. - QString username, password; - if (Settings::instance().steam().login(username, password)) { - if (username.length() > 0) - MOBase::log::getDefault().addToBlacklist(username.toStdString(), "STEAM_USERNAME"); - if (password.length() > 0) - MOBase::log::getDefault().addToBlacklist(password.toStdString(), "STEAM_PASSWORD"); - sp.arguments = makeSteamArguments(username, password); - } - - log::debug( - "starting steam process:\n" - " . program: '{}'\n" - " . username={}, password={}", - sp.binary.filePath().toStdString(), - (username.isEmpty() ? "no" : "yes"), - (password.isEmpty() ? "no" : "yes")); - - HANDLE ph = INVALID_HANDLE_VALUE; - const auto e = spawn(sp, ph); - ::CloseHandle(ph); - - if (e != ERROR_SUCCESS) { - // make sure username and passwords are not shown - sp.arguments = makeSteamArguments( - (username.isEmpty() ? "" : "USERNAME"), - (password.isEmpty() ? "" : "PASSWORD")); - - const auto r = dialogs::startSteamFailed( - parent, keyName, valueName, exe, sp, e); - - return (r == QMessageBox::Yes); - } - - QMessageBox::information( - parent, QObject::tr("Waiting"), - QObject::tr("Please press OK once you're logged into steam.")); - - return true; -} - -bool checkSteam( - QWidget* parent, const SpawnParameters& sp, - const QDir& gameDirectory, const QString &steamAppID, const Settings& settings) -{ - static const std::vector steamFiles = { - "steam_api.dll", "steam_api64.dll" - }; - - log::debug("checking steam"); - - if (!steamAppID.isEmpty()) { - env::set("SteamAPPId", steamAppID); - } else { - env::set("SteamAPPId", settings.steam().appID()); - } - - - bool steamRequired = false; - QString details; - - for (const auto& file : steamFiles) { - const QFileInfo fi(gameDirectory.absoluteFilePath(file)); - if (fi.exists()) { - details = QString( - "managed game is located at '%1' and file '%2' exists") - .arg(gameDirectory.absolutePath()) - .arg(fi.absoluteFilePath()); - - log::debug("{}", details); - steamRequired = true; - - break; - } - } - - if (!steamRequired) { - log::debug("program 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 = dialogs::confirmStartSteam(parent, sp, details); - - if (c == QDialogButtonBox::Yes) { - log::debug("user wants to start steam"); - - if (!startSteam(parent)) { - // cancel - return false; - } - - // double-check that Steam is started - ss = getSteamStatus(); - if (!ss.running) { - log::error("steam is still not running, 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 = dialogs::confirmRestartAsAdminForSteam(parent, sp); - - if (c == QDialogButtonBox::Yes) { - restartAsAdmin(parent); - 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 checkBlacklist( - QWidget* parent, const SpawnParameters& sp, Settings& settings) -{ - for (;;) { - if (!settings.isExecutableBlacklisted(sp.binary.fileName())) { - return true; - } - - const auto r = dialogs::confirmBlacklisted(parent, sp, settings); - - if (r != QMessageBox::Retry) { - return (r == QMessageBox::Yes); - } - } -} - -HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) -{ - HANDLE handle = INVALID_HANDLE_VALUE; - const auto e = spawn::spawn(sp, handle); - - switch (e) - { - case ERROR_SUCCESS: - { - return handle; - } - - case ERROR_ELEVATION_REQUIRED: - { - startBinaryAdmin(parent, sp); - return INVALID_HANDLE_VALUE; - } - - default: - { - dialogs::spawnFailed(parent, sp, e); - return INVALID_HANDLE_VALUE; - } - } -} - -QString getExecutableForJarFile(const QString& jarFile) -{ - const std::wstring jarFileW = jarFile.toStdWString(); - - WCHAR buffer[MAX_PATH]; - - const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer); - const auto r = static_cast(reinterpret_cast(hinst)); - - // anything <= 32 signals failure - if (r <= 32) { - log::warn( - "failed to find executable associated with file '{}', {}", - jarFile, shell::formatError(r)); - - return {}; - } - - DWORD binaryType = 0; - - if (!::GetBinaryTypeW(buffer, &binaryType)) { - const auto e = ::GetLastError(); - - log::warn( - "failed to determine binary type of '{}', {}", - QString::fromWCharArray(buffer), formatSystemMessage(e)); - - return {}; - } - - if (binaryType != SCS_32BIT_BINARY && binaryType != SCS_64BIT_BINARY) { - log::warn( - "unexpected binary type {} for file '{}'", - binaryType, QString::fromWCharArray(buffer)); - - return {}; - } - - return QString::fromWCharArray(buffer); -} - -QString getJavaHome() -{ - const QString key = "HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment"; - const QString value = "CurrentVersion"; - - QSettings reg(key, QSettings::NativeFormat); - - if (!reg.contains(value)) { - log::warn("key '{}\\{}' doesn't exist", key, value); - return {}; - } - - const QString currentVersion = reg.value("CurrentVersion").toString(); - const QString javaHome = QString("%1/JavaHome").arg(currentVersion); - - if (!reg.contains(javaHome)) { - log::warn( - "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist", - currentVersion, key, value, key, javaHome); - - return {}; - } - - const auto path = reg.value(javaHome).toString(); - return path + "\\bin\\javaw.exe"; -} - -QString findJavaInstallation(const QString& jarFile) -{ - // try to find java automatically based on the given jar file - if (!jarFile.isEmpty()) { - const auto s = getExecutableForJarFile(jarFile); - if (!s.isEmpty()) { - return s; - } - } - - // second attempt: look to the registry - const auto s = getJavaHome(); - if (!s.isEmpty()) { - return s; - } - - // not found - return {}; -} - -bool isBatchFile(const QFileInfo& target) -{ - const auto batchExtensions = {"cmd", "bat"}; - - const QString extension = target.suffix(); - for (auto&& e : batchExtensions) { - if (extension.compare(e, Qt::CaseInsensitive) == 0) { - return true; - } - } - - return false; -} - -bool isExeFile(const QFileInfo& target) -{ - return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0); -} - -bool isJavaFile(const QFileInfo& target) -{ - return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0); -} - -QFileInfo getCmdPath() -{ - const auto p = env::get("COMSPEC"); - if (!p.isEmpty()) { - return QFileInfo(p); - } - - QString systemDirectory; - - const std::size_t buffer_size = 1000; - wchar_t buffer[buffer_size + 1] = {}; - - const auto length = ::GetSystemDirectoryW(buffer, buffer_size); - if (length != 0) { - systemDirectory = QString::fromWCharArray(buffer, length); - - if (!systemDirectory.endsWith("\\")) { - systemDirectory += "\\"; - } - } else { - systemDirectory = "C:\\Windows\\System32\\"; - } - - return QFileInfo(systemDirectory + "cmd.exe"); -} - -FileExecutionTypes getFileExecutionType(const QFileInfo& target) -{ - if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) { - return FileExecutionTypes::Executable; - } - - return FileExecutionTypes::Other; -} - -FileExecutionContext getFileExecutionContext( - QWidget* parent, const QFileInfo& target) -{ - if (isExeFile(target)) { - return { - target, - "", - FileExecutionTypes::Executable - }; - } - - if (isBatchFile(target)) { - return { - getCmdPath(), - QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())), - FileExecutionTypes::Executable - }; - } - - if (isJavaFile(target)) { - auto java = findJavaInstallation(target.absoluteFilePath()); - - if (java.isEmpty()) { - java = QFileDialog::getOpenFileName( - parent, QObject::tr("Select binary"), - QString(), QObject::tr("Binary") + " (*.exe)"); - } - - if (!java.isEmpty()) { - return { - QFileInfo(java), - QString("-jar \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())), - FileExecutionTypes::Executable - }; - } - } - - return {{}, {}, FileExecutionTypes::Other}; -} - -} // namespace - - - -namespace helper -{ - -bool helperExec( - QWidget* parent, - const std::wstring& moDirectory, const std::wstring& commandLine, BOOL async) -{ - const std::wstring fileName = moDirectory + L"\\helper.exe"; - - env::HandlePtr process; - - { - SHELLEXECUTEINFOW execInfo = {}; - - ULONG flags = SEE_MASK_FLAG_NO_UI; - if (!async) - flags |= SEE_MASK_NOCLOSEPROCESS; - - execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); - execInfo.fMask = flags; - execInfo.hwnd = 0; - execInfo.lpVerb = L"runas"; - execInfo.lpFile = fileName.c_str(); - execInfo.lpParameters = commandLine.c_str(); - execInfo.lpDirectory = moDirectory.c_str(); - execInfo.nShow = SW_SHOW; - - if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) { - const auto e = GetLastError(); - - spawn::dialogs::helperFailed( - parent, e, "ShellExecuteExW()", fileName, moDirectory, commandLine); - - return false; - } - - if (async) { - return true; - } - - process.reset(execInfo.hProcess); - } - - const auto r = ::WaitForSingleObject(process.get(), INFINITE); - - if (r != WAIT_OBJECT_0) { - // for WAIT_ABANDONED, the documentation doesn't mention that GetLastError() - // returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so - // use that instead - const auto code = (r == WAIT_ABANDONED ? - ERROR_ABANDONED_WAIT_0 : GetLastError()); - - spawn::dialogs::helperFailed( - parent, code, "WaitForSingleObject()", - fileName, moDirectory, commandLine); - - return false; - } - - DWORD exitCode = 0; - if (!GetExitCodeProcess(process.get(), &exitCode)) { - const auto e = GetLastError(); - - spawn::dialogs::helperFailed( - parent, e, "GetExitCodeProcess()", fileName, moDirectory, commandLine); - - return false; - } - - return (exitCode == 0); -} - -bool backdateBSAs( - QWidget* parent, const std::wstring &moPath, const std::wstring &dataPath) -{ - const std::wstring commandLine = fmt::format( - L"backdateBSA \"{}\"", dataPath); - - return helperExec(parent, moPath, commandLine, FALSE); -} - -bool adminLaunch( - QWidget* parent, const std::wstring &moPath, - const std::wstring &moFile, const std::wstring &workingDir) -{ - const std::wstring commandLine = fmt::format( - L"adminLaunch {} \"{}\" \"{}\"", - ::GetCurrentProcessId(), moFile, workingDir); - - return helperExec(parent, moPath, commandLine, true); -} - -} // namespace +/* +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 "settingsdialogworkarounds.h" +#include +#include +#include +#include +#include +#include "shared/appconfig.h" +#include "shared/windows_error.h" +#include +#include +#include +#include +#include +#include + +using namespace MOBase; +using namespace MOShared; + +namespace spawn::dialogs +{ + +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; +} + +QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more={}) +{ + 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)"; + } + + std::wstring f = + L"Error {code} {codename}{more}: {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" . MO elevated: {elevated}"; + + if (sp.hooked) { + f += L"\n . usvfs x86:{x86_dll} x64:{x64_dll} proxy_x86:{x86_proxy} proxy_x64:{x64_proxy}"; + } + + const std::wstring wmore = (more.isEmpty() ? L"" : (", " + more).toStdWString()); + + const auto s = fmt::format(f, + fmt::arg(L"code", code), + fmt::arg(L"codename", errorCodeName(code)), + fmt::arg(L"more", wmore), + 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)); + + return QString::fromStdWString(s); +} + +QString makeContent(const SpawnParameters& sp, DWORD code) +{ + if (code == ERROR_INVALID_PARAMETER) { + return 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) { + return 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) { + return QObject::tr("The file '%1' does not exist.") + .arg(QDir::toNativeSeparators(sp.binary.absoluteFilePath())); + } else if (code == ERROR_DIRECTORY) { + if (!sp.currentDirectory.exists()) { + return QObject::tr("The working directory '%1' does not exist.") + .arg(QDir::toNativeSeparators(sp.currentDirectory.absolutePath())); + } + } + + return QString::fromStdWString(formatSystemMessage(code)); +} + +QMessageBox::StandardButton badSteamReg( + QWidget* parent, const QString& keyName, const QString& valueName) +{ + const auto details = QString( + "can't start steam, registry value at '%1' is empty or doesn't exist") + .arg(keyName + "\\" + valueName); + + log::error("{}", details); + + return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam")) + .main(QObject::tr("Cannot start Steam")) + .content(QObject::tr( + "The path to the Steam executable cannot be found. You might try " + "reinstalling Steam.")) + .details(details) + .icon(QMessageBox::Critical) + .button({ + QObject::tr("Continue without starting Steam"), + QObject::tr("The program may fail to launch."), + QMessageBox::Yes}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .exec(); +} + +QMessageBox::StandardButton startSteamFailed( + QWidget* parent, + const QString& keyName, const QString& valueName, const QString& exe, + const SpawnParameters& sp, DWORD e) +{ + auto details = QString( + "a steam install was found in the registry at '%1': '%2'\n\n") + .arg(keyName + "\\" + valueName) + .arg(exe); + + details += makeDetails(sp, e); + + log::error("{}", details); + + return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam")) + .main(QObject::tr("Cannot start Steam")) + .content(makeContent(sp, e)) + .details(details) + .icon(QMessageBox::Critical) + .button({ + QObject::tr("Continue without starting Steam"), + QObject::tr("The program may fail to launch."), + QMessageBox::Yes}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .exec(); +} + +void spawnFailed(QWidget* parent, const SpawnParameters& sp, DWORD code) +{ + const auto details = 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()); + + MOBase::TaskDialog(parent, title) + .main(mainText) + .content(makeContent(sp, code)) + .details(details) + .icon(QMessageBox::Critical) + .exec(); +} + +void helperFailed( + QWidget* parent, DWORD code, const QString& why, const std::wstring& binary, + const std::wstring& cwd, const std::wstring& args) +{ + SpawnParameters sp; + sp.binary = QFileInfo(QString::fromStdWString(binary)); + sp.currentDirectory.setPath(QString::fromStdWString(cwd)); + sp.arguments = QString::fromStdWString(args); + + const auto details = makeDetails(sp, code, "in " + why); + log::error("{}", details); + + const auto title = QObject::tr("Cannot launch helper"); + + const auto mainText = QObject::tr("Cannot start %1") + .arg(sp.binary.fileName()); + + MOBase::TaskDialog(parent, title) + .main(mainText) + .content(makeContent(sp, code)) + .details(details) + .icon(QMessageBox::Critical) + .exec(); +} + +bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp) +{ + const auto details = 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."); + + log::debug("asking user to restart MO as administrator"); + + const auto r = MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .details(details) + .icon(QMessageBox::Question) + .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); +} + +QMessageBox::StandardButton confirmStartSteam( + QWidget* parent, const SpawnParameters& sp, const QString& details) +{ + const auto title = QObject::tr("Launch Steam"); + const auto mainText = QObject::tr("This program requires Steam"); + const auto content = QObject::tr( + "Mod Organizer has detected that this program likely requires Steam to be " + "running to function properly."); + + return MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .details(details) + .icon(QMessageBox::Question) + .button({ + QObject::tr("Start Steam"), + QMessageBox::Yes}) + .button({ + QObject::tr("Continue without starting Steam"), + QObject::tr("The program might fail to run."), + QMessageBox::No}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .remember("steamQuery", sp.binary.fileName()) + .exec(); +} + +QMessageBox::StandardButton confirmRestartAsAdminForSteam( + QWidget* parent, const SpawnParameters& sp) +{ + const auto title = QObject::tr("Elevation required"); + const auto mainText = QObject::tr("Steam is running as administrator"); + const auto content = QObject::tr( + "Running Steam as administrator is typically unnecessary and can cause " + "problems when Mod Organizer itself is not running as administrator." + "\r\n\r\n" + "You can restart Mod Organizer as administrator and try launching the " + "program again."); + + return MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .icon(QMessageBox::Question) + .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("Continue"), + QObject::tr("The program might fail to run."), + QMessageBox::No}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .remember("steamAdminQuery", sp.binary.fileName()) + .exec(); +} + +bool eventLogNotRunning( + QWidget* parent, const env::Service& s, const SpawnParameters& sp) +{ + const auto title = QObject::tr("Event Log not running"); + const auto mainText = QObject::tr("The Event Log service is not running"); + const auto content = QObject::tr( + "The Windows Event Log service is not running. This can prevent USVFS from " + "running properly and your mods may not be recognized by the program being " + "launched."); + + const auto r = MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .details(s.toString()) + .icon(QMessageBox::Question) + .remember("eventLogService", sp.binary.fileName()) + .button({ + QObject::tr("Continue"), + QObject::tr("Your mods might not work."), + QMessageBox::Yes}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .exec(); + + return (r == QMessageBox::Yes); +} + +QMessageBox::StandardButton confirmBlacklisted( + QWidget* parent, const SpawnParameters& sp, Settings& settings) +{ + const auto title = QObject::tr("Blacklisted program"); + const auto mainText = QObject::tr("The program %1 is blacklisted") + .arg(sp.binary.fileName()); + const auto content = QObject::tr( + "The program you are attempting to launch is blacklisted in the virtual " + "filesystem. This will likely prevent it from seeing any mods, INI files " + "or any other virtualized files."); + + const auto details = + "Executable: " + sp.binary.fileName() + "\n" + "Current blacklist: " + settings.executablesBlacklist(); + + auto r = MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .details(details) + .icon(QMessageBox::Question) + .remember("blacklistedExecutable", sp.binary.fileName()) + .button({ + QObject::tr("Continue"), + QObject::tr("Your mods might not work."), + QMessageBox::Yes}) + .button({ + QObject::tr("Change the blacklist"), + QMessageBox::Retry}) + .button({ + QObject::tr("Cancel"), + QMessageBox::Cancel}) + .exec(); + + if (r == QMessageBox::Retry) { + if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) { + r = QMessageBox::Cancel; + } + } + + return r; +} + +} // namespace + + +namespace spawn +{ + +void logSpawning(const SpawnParameters& sp, const QString& realCmd) +{ + log::debug( + "spawning binary:\n" + " . exe: '{}'\n" + " . args: '{}'\n" + " . cwd: '{}'\n" + " . steam id: '{}'\n" + " . hooked: {}\n" + " . stdout: {}\n" + " . stderr: {}\n" + " . real cmd: '{}'", + sp.binary.absoluteFilePath(), sp.arguments, + sp.currentDirectory.absolutePath(), sp.steamAppID, sp.hooked, + (sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"), + (sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"), + realCmd); +} + +DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) +{ + 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()); + const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath()); + + QString commandLine = "\"" + bin + "\""; + if (!sp.arguments.isEmpty()) { + commandLine += " " + sp.arguments; + } + + const QString moPath = QCoreApplication::applicationDirPath(); + const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath)); + + PROCESS_INFORMATION pi = {}; + BOOL success = FALSE; + + logSpawning(sp, commandLine); + + const auto wcommandLine = commandLine.toStdWString(); + const auto wcwd = cwd.toStdWString(); + + const DWORD flags = CREATE_BREAKAWAY_FROM_JOB; + + if (sp.hooked) { + success = ::CreateProcessHooked( + nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, + inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); + } else { + success = ::CreateProcess( + nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, + inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); + } + + const auto e = GetLastError(); + env::setPath(oldPath); + + if (!success) { + return e; + } + + processHandle = pi.hProcess; + ::CloseHandle(pi.hThread); + + return ERROR_SUCCESS; +} + +bool restartAsAdmin(QWidget* parent) +{ + WCHAR cwd[MAX_PATH] = {}; + if (!GetCurrentDirectory(MAX_PATH, cwd)) { + cwd[0] = L'\0'; + } + + if (!helper::adminLaunch( + parent, + qApp->applicationDirPath().toStdWString(), + qApp->applicationFilePath().toStdWString(), + std::wstring(cwd))) + { + log::error("admin launch failed"); + return false; + } + + log::debug("exiting MO"); + ExitModOrganizer(Exit::Force); + + return true; +} + +void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp) +{ + if (!dialogs::confirmRestartAsAdmin(parent, sp)) { + log::debug("user declined"); + return; + } + + log::info("restarting MO as administrator"); + restartAsAdmin(parent); +} + +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; +} + +QString makeSteamArguments(const QString& username, const QString& password) +{ + QString args; + + if (username != "") { + args += "-login " + username; + + if (password != "") { + args += " " + password; + } + } + + return args; +} + +bool startSteam(QWidget* parent) +{ + 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()) { + return (dialogs::badSteamReg(parent, keyName, valueName) == QMessageBox::Yes); + } + + SpawnParameters sp; + sp.binary = QFileInfo(exe); + + // See if username and password supplied. If so, pass them into steam. + QString username, password; + if (Settings::instance().steam().login(username, password)) { + if (username.length() > 0) + MOBase::log::getDefault().addToBlacklist(username.toStdString(), "STEAM_USERNAME"); + if (password.length() > 0) + MOBase::log::getDefault().addToBlacklist(password.toStdString(), "STEAM_PASSWORD"); + sp.arguments = makeSteamArguments(username, password); + } + + log::debug( + "starting steam process:\n" + " . program: '{}'\n" + " . username={}, password={}", + sp.binary.filePath().toStdString(), + (username.isEmpty() ? "no" : "yes"), + (password.isEmpty() ? "no" : "yes")); + + HANDLE ph = INVALID_HANDLE_VALUE; + const auto e = spawn(sp, ph); + ::CloseHandle(ph); + + if (e != ERROR_SUCCESS) { + // make sure username and passwords are not shown + sp.arguments = makeSteamArguments( + (username.isEmpty() ? "" : "USERNAME"), + (password.isEmpty() ? "" : "PASSWORD")); + + const auto r = dialogs::startSteamFailed( + parent, keyName, valueName, exe, sp, e); + + return (r == QMessageBox::Yes); + } + + QMessageBox::information( + parent, QObject::tr("Waiting"), + QObject::tr("Please press OK once you're logged into steam.")); + + return true; +} + +bool checkSteam( + QWidget* parent, const SpawnParameters& sp, + const QDir& gameDirectory, const QString &steamAppID, const Settings& settings) +{ + static const std::vector steamFiles = { + "steam_api.dll", "steam_api64.dll" + }; + + log::debug("checking steam"); + + if (!steamAppID.isEmpty()) { + env::set("SteamAPPId", steamAppID); + } else { + env::set("SteamAPPId", settings.steam().appID()); + } + + + bool steamRequired = false; + QString details; + + for (const auto& file : steamFiles) { + const QFileInfo fi(gameDirectory.absoluteFilePath(file)); + if (fi.exists()) { + details = QString( + "managed game is located at '%1' and file '%2' exists") + .arg(gameDirectory.absolutePath()) + .arg(fi.absoluteFilePath()); + + log::debug("{}", details); + steamRequired = true; + + break; + } + } + + if (!steamRequired) { + log::debug("program 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 = dialogs::confirmStartSteam(parent, sp, details); + + if (c == QDialogButtonBox::Yes) { + log::debug("user wants to start steam"); + + if (!startSteam(parent)) { + // cancel + return false; + } + + // double-check that Steam is started + ss = getSteamStatus(); + if (!ss.running) { + log::error("steam is still not running, 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 = dialogs::confirmRestartAsAdminForSteam(parent, sp); + + if (c == QDialogButtonBox::Yes) { + restartAsAdmin(parent); + 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 checkBlacklist( + QWidget* parent, const SpawnParameters& sp, Settings& settings) +{ + for (;;) { + if (!settings.isExecutableBlacklisted(sp.binary.fileName())) { + return true; + } + + const auto r = dialogs::confirmBlacklisted(parent, sp, settings); + + if (r != QMessageBox::Retry) { + return (r == QMessageBox::Yes); + } + } +} + +HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) +{ + HANDLE handle = INVALID_HANDLE_VALUE; + const auto e = spawn::spawn(sp, handle); + + switch (e) + { + case ERROR_SUCCESS: + { + return handle; + } + + case ERROR_ELEVATION_REQUIRED: + { + startBinaryAdmin(parent, sp); + return INVALID_HANDLE_VALUE; + } + + default: + { + dialogs::spawnFailed(parent, sp, e); + return INVALID_HANDLE_VALUE; + } + } +} + +QString getExecutableForJarFile(const QString& jarFile) +{ + const std::wstring jarFileW = jarFile.toStdWString(); + + WCHAR buffer[MAX_PATH]; + + const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer); + const auto r = static_cast(reinterpret_cast(hinst)); + + // anything <= 32 signals failure + if (r <= 32) { + log::warn( + "failed to find executable associated with file '{}', {}", + jarFile, shell::formatError(r)); + + return {}; + } + + DWORD binaryType = 0; + + if (!::GetBinaryTypeW(buffer, &binaryType)) { + const auto e = ::GetLastError(); + + log::warn( + "failed to determine binary type of '{}', {}", + QString::fromWCharArray(buffer), formatSystemMessage(e)); + + return {}; + } + + if (binaryType != SCS_32BIT_BINARY && binaryType != SCS_64BIT_BINARY) { + log::warn( + "unexpected binary type {} for file '{}'", + binaryType, QString::fromWCharArray(buffer)); + + return {}; + } + + return QString::fromWCharArray(buffer); +} + +QString getJavaHome() +{ + const QString key = "HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment"; + const QString value = "CurrentVersion"; + + QSettings reg(key, QSettings::NativeFormat); + + if (!reg.contains(value)) { + log::warn("key '{}\\{}' doesn't exist", key, value); + return {}; + } + + const QString currentVersion = reg.value("CurrentVersion").toString(); + const QString javaHome = QString("%1/JavaHome").arg(currentVersion); + + if (!reg.contains(javaHome)) { + log::warn( + "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist", + currentVersion, key, value, key, javaHome); + + return {}; + } + + const auto path = reg.value(javaHome).toString(); + return path + "\\bin\\javaw.exe"; +} + +QString findJavaInstallation(const QString& jarFile) +{ + // try to find java automatically based on the given jar file + if (!jarFile.isEmpty()) { + const auto s = getExecutableForJarFile(jarFile); + if (!s.isEmpty()) { + return s; + } + } + + // second attempt: look to the registry + const auto s = getJavaHome(); + if (!s.isEmpty()) { + return s; + } + + // not found + return {}; +} + +bool isBatchFile(const QFileInfo& target) +{ + const auto batchExtensions = {"cmd", "bat"}; + + const QString extension = target.suffix(); + for (auto&& e : batchExtensions) { + if (extension.compare(e, Qt::CaseInsensitive) == 0) { + return true; + } + } + + return false; +} + +bool isExeFile(const QFileInfo& target) +{ + return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0); +} + +bool isJavaFile(const QFileInfo& target) +{ + return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0); +} + +QFileInfo getCmdPath() +{ + const auto p = env::get("COMSPEC"); + if (!p.isEmpty()) { + return QFileInfo(p); + } + + QString systemDirectory; + + const std::size_t buffer_size = 1000; + wchar_t buffer[buffer_size + 1] = {}; + + const auto length = ::GetSystemDirectoryW(buffer, buffer_size); + if (length != 0) { + systemDirectory = QString::fromWCharArray(buffer, length); + + if (!systemDirectory.endsWith("\\")) { + systemDirectory += "\\"; + } + } else { + systemDirectory = "C:\\Windows\\System32\\"; + } + + return QFileInfo(systemDirectory + "cmd.exe"); +} + +FileExecutionTypes getFileExecutionType(const QFileInfo& target) +{ + if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) { + return FileExecutionTypes::Executable; + } + + return FileExecutionTypes::Other; +} + +FileExecutionContext getFileExecutionContext( + QWidget* parent, const QFileInfo& target) +{ + if (isExeFile(target)) { + return { + target, + "", + FileExecutionTypes::Executable + }; + } + + if (isBatchFile(target)) { + return { + getCmdPath(), + QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())), + FileExecutionTypes::Executable + }; + } + + if (isJavaFile(target)) { + auto java = findJavaInstallation(target.absoluteFilePath()); + + if (java.isEmpty()) { + java = QFileDialog::getOpenFileName( + parent, QObject::tr("Select binary"), + QString(), QObject::tr("Binary") + " (*.exe)"); + } + + if (!java.isEmpty()) { + return { + QFileInfo(java), + QString("-jar \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())), + FileExecutionTypes::Executable + }; + } + } + + return {{}, {}, FileExecutionTypes::Other}; +} + +} // namespace + + + +namespace helper +{ + +bool helperExec( + QWidget* parent, + const std::wstring& moDirectory, const std::wstring& commandLine, BOOL async) +{ + const std::wstring fileName = moDirectory + L"\\helper.exe"; + + env::HandlePtr process; + + { + SHELLEXECUTEINFOW execInfo = {}; + + ULONG flags = SEE_MASK_FLAG_NO_UI; + if (!async) + flags |= SEE_MASK_NOCLOSEPROCESS; + + execInfo.cbSize = sizeof(SHELLEXECUTEINFOW); + execInfo.fMask = flags; + execInfo.hwnd = 0; + execInfo.lpVerb = L"runas"; + execInfo.lpFile = fileName.c_str(); + execInfo.lpParameters = commandLine.c_str(); + execInfo.lpDirectory = moDirectory.c_str(); + execInfo.nShow = SW_SHOW; + + if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) { + const auto e = GetLastError(); + + spawn::dialogs::helperFailed( + parent, e, "ShellExecuteExW()", fileName, moDirectory, commandLine); + + return false; + } + + if (async) { + return true; + } + + process.reset(execInfo.hProcess); + } + + const auto r = ::WaitForSingleObject(process.get(), INFINITE); + + if (r != WAIT_OBJECT_0) { + // for WAIT_ABANDONED, the documentation doesn't mention that GetLastError() + // returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so + // use that instead + const auto code = (r == WAIT_ABANDONED ? + ERROR_ABANDONED_WAIT_0 : GetLastError()); + + spawn::dialogs::helperFailed( + parent, code, "WaitForSingleObject()", + fileName, moDirectory, commandLine); + + return false; + } + + DWORD exitCode = 0; + if (!GetExitCodeProcess(process.get(), &exitCode)) { + const auto e = GetLastError(); + + spawn::dialogs::helperFailed( + parent, e, "GetExitCodeProcess()", fileName, moDirectory, commandLine); + + return false; + } + + return (exitCode == 0); +} + +bool backdateBSAs( + QWidget* parent, const std::wstring &moPath, const std::wstring &dataPath) +{ + const std::wstring commandLine = fmt::format( + L"backdateBSA \"{}\"", dataPath); + + return helperExec(parent, moPath, commandLine, FALSE); +} + +bool adminLaunch( + QWidget* parent, const std::wstring &moPath, + const std::wstring &moFile, const std::wstring &workingDir) +{ + const std::wstring commandLine = fmt::format( + L"adminLaunch {} \"{}\" \"{}\"", + ::GetCurrentProcessId(), moFile, workingDir); + + return helperExec(parent, moPath, commandLine, true); +} + +} // namespace -- cgit v1.3.1