diff options
| author | Mikaƫl Capelle <capelle.mikael@gmail.com> | 2023-07-09 17:36:03 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-07-09 17:36:03 +0200 |
| commit | ef94aee28464039672b277243a0181ae93550d6c (patch) | |
| tree | 0575a68af5f154e9d6d0738edbaefb16a11d6f5c /src/spawn.cpp | |
| parent | 7d6cb8528d20e36a4cee822263865ee2f7f32481 (diff) | |
| parent | 3de050e9c03e553f7ae3f780f6bd080a30ae123e (diff) | |
Merge pull request #1839 from Holt59/ci/initial-gh-action-for-build
Clang-Format + Github Workflow
Diffstat (limited to 'src/spawn.cpp')
| -rw-r--r-- | src/spawn.cpp | 2092 |
1 files changed, 1010 insertions, 1082 deletions
diff --git a/src/spawn.cpp b/src/spawn.cpp index 5b14c52c..05f796a5 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -1,1082 +1,1010 @@ -/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "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 <errorcodes.h>
-#include <report.h>
-#include <log.h>
-#include <usvfs.h>
-#include <Shellapi.h>
-#include "shared/appconfig.h"
-#include "shared/windows_error.h"
-#include <QApplication>
-#include <QMessageBox>
-#include <QtDebug>
-#include <Shellapi.h>
-#include <fmt/format.h>
-#include <fmt/xchar.h>
-
-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<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
- inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
- } else {
- success = ::CreateProcess(
- nullptr, const_cast<wchar_t*>(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<QString> 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<int>(reinterpret_cast<std::uintptr_t>(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 <http://www.gnu.org/licenses/>. +*/ + +#include "spawn.h" + +#include "env.h" +#include "envmodule.h" +#include "envsecurity.h" +#include "envwindows.h" +#include "report.h" +#include "settings.h" +#include "settingsdialogworkarounds.h" +#include "shared/appconfig.h" +#include "shared/windows_error.h" +#include "utility.h" +#include <QApplication> +#include <QMessageBox> +#include <QtDebug> +#include <Shellapi.h> +#include <errorcodes.h> +#include <fmt/format.h> +#include <fmt/xchar.h> +#include <log.h> +#include <report.h> +#include <usvfs.h> + +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} " + L"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 spawn::dialogs + +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<wchar_t*>(wcommandLine.c_str()), + nullptr, nullptr, inheritHandles, flags, nullptr, + wcwd.c_str(), &si, &pi); + } else { + success = ::CreateProcess(nullptr, const_cast<wchar_t*>(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<QString> 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<int>(reinterpret_cast<std::uintptr_t>(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 spawn + +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 helper |
