/*
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 "env.h"
#include "envmodule.h"
#include "envsecurity.h"
#include "envwindows.h"
#include "settings.h"
#include "settingsdialogworkarounds.h"
#include "shared/appconfig.h"
#include "shared/windows_error.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
class QMessageBox;
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)";
}
auto s = std::format(L"Error {} {}{}: {}\n"
L" . binary: '{}'\n"
L" . owner: {}\n"
L" . rights: {}\n"
L" . arguments: '{}'\n"
L" . cwd: '{}'{}\n"
L" . stdout: {}, stderr: {}, hooked: {}\n"
L" . MO elevated: {}",
code, errorCodeName(code), (more.isEmpty() ? more : ", " + more),
formatSystemMessage(code),
QDir::toNativeSeparators(sp.binary.absoluteFilePath()), owner,
rights, sp.arguments,
QDir::toNativeSeparators(sp.currentDirectory.absolutePath()),
(cwdExists ? L"" : L" (not found)"),
(sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
(sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
(sp.hooked ? L"yes" : L"no"), elevated);
if (sp.hooked) {
s += std::format(L"\n . usvfs x86:{} x64:{} proxy_x86:{} proxy_x64:{}",
usvfs_x86_dll, usvfs_x64_dll, usvfs_x86_proxy, usvfs_x64_proxy);
}
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 = ::usvfsCreateProcessHooked(
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 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 = std::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 = std::format(
L"adminLaunch {} \"{}\" \"{}\"", ::GetCurrentProcessId(), moFile, workingDir);
return helperExec(parent, moPath, commandLine, true);
}
} // namespace helper