/*
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
#include
#include
#include
#include
#include
#include
#include "helper.h"
#include
#include
#include
#include
#include
using namespace MOBase;
using namespace MOShared;
namespace
{
struct SpawnParameters
{
std::wstring binary;
std::wstring arguments;
std::wstring currentDirectory;
bool suspended = false;
bool hooked = false;
HANDLE stdOut = INVALID_HANDLE_VALUE;
HANDLE stdErr = INVALID_HANDLE_VALUE;
};
std::wstring pathEnv()
{
std::wstring s(4000, L' ');
DWORD realSize = ::GetEnvironmentVariableW(
L"PATH", s.data(), static_cast(s.size()));
if (realSize > s.size()) {
s.resize(realSize);
::GetEnvironmentVariableW(
TEXT("PATH"), s.data(), static_cast(s.size()));
}
return s;
}
void setPathEnv(const std::wstring& s)
{
::SetEnvironmentVariableW(L"PATH", s.c_str());
}
DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle, HANDLE& threadHandle)
{
BOOL inheritHandles = FALSE;
STARTUPINFO si = {};
si.cb = sizeof(si);
// inherit handles if we plan to use stdout or stderr reroute
if (sp.stdOut != INVALID_HANDLE_VALUE) {
si.hStdOutput = sp.stdOut;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
if (sp.stdErr != INVALID_HANDLE_VALUE) {
si.hStdError = sp.stdErr;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
std::wstring commandLine;
if (sp.arguments[0] != L'\0') {
commandLine = L"\"" + sp.binary + L"\" " + sp.arguments;
} else {
commandLine = L"\"" + sp.binary + L"\"";
}
QString moPath = QCoreApplication::applicationDirPath();
const auto oldPath = pathEnv();
setPathEnv(oldPath + L";" + QDir::toNativeSeparators(moPath).toStdWString());
PROCESS_INFORMATION pi;
BOOL success = FALSE;
if (sp.hooked) {
success = ::CreateProcessHooked(
nullptr, const_cast(commandLine.c_str()), nullptr, nullptr,
inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr,
sp.currentDirectory.c_str(), &si, &pi);
} else {
success = ::CreateProcess(
nullptr, const_cast(commandLine.c_str()), nullptr, nullptr,
inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr,
sp.currentDirectory.c_str(), &si, &pi);
}
const auto e = GetLastError();
setPathEnv(oldPath);
if (!success) {
return e;
}
processHandle = pi.hProcess;
threadHandle = pi.hThread;
return ERROR_SUCCESS;
}
std::wstring makeRightsDetails(const env::FileSecurity& fs)
{
if (fs.rights.normalRights) {
return L"(normal rights)";
}
if (fs.rights.list.isEmpty()) {
return L"(none)";
}
std::wstring s = fs.rights.list.join("|").toStdWString();
if (!fs.rights.hasExecute) {
s += L" (execute is missing)";
}
return s;
}
std::wstring makeDetails(const SpawnParameters& sp, DWORD code)
{
const QFileInfo bin(QString::fromStdWString(sp.binary));
std::wstring owner, rights;
if (bin.isFile()) {
const auto fs = env::getFileSecurity(bin.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.empty() ?
true : QFileInfo(QString::fromStdWString(sp.currentDirectory)).isDir());
const auto appDir = QCoreApplication::applicationDirPath();
const auto sep = QDir::separator();
const std::wstring usvfs_x86_dll =
QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found";
const std::wstring usvfs_x64_dll =
QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found";
const std::wstring usvfs_x86_proxy =
QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" : L"not found";
const std::wstring usvfs_x64_proxy =
QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" : L"not found";
std::wstring elevated;
if (auto b=env::Environment().windowsInfo().isElevated()) {
elevated = (*b ? L"yes" : L"no");
} else {
elevated = L"(not available)";
}
return fmt::format(
L"Error {code} {codename}: {error}\n"
L" . binary: '{bin}'\n"
L" . owner: {owner}\n"
L" . rights: {rights}\n"
L" . arguments: '{args}'\n"
L" . cwd: '{cwd}'{cwdexists}\n"
L" . stdout: {stdout}, stderr: {stderr}, suspended: {susp}, hooked: {hooked}\n"
L" . usvfs x86:{x86_dll} x64:{x64_dll} proxy_x86:{x86_proxy} proxy_x64:{x64_proxy}\n"
L" . MO elevated: {elevated}",
fmt::arg(L"code", code),
fmt::arg(L"codename", errorCodeName(code)),
fmt::arg(L"bin", sp.binary),
fmt::arg(L"owner", owner),
fmt::arg(L"rights", rights),
fmt::arg(L"error", formatSystemMessage(code)),
fmt::arg(L"args", sp.arguments),
fmt::arg(L"cwd", sp.currentDirectory),
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"susp", (sp.suspended ? L"yes" : L"no")),
fmt::arg(L"hooked", (sp.hooked ? L"yes" : L"no")),
fmt::arg(L"x86_dll", usvfs_x86_dll),
fmt::arg(L"x64_dll", usvfs_x64_dll),
fmt::arg(L"x86_proxy", usvfs_x86_proxy),
fmt::arg(L"x64_proxy", usvfs_x64_proxy),
fmt::arg(L"elevated", elevated));
}
void spawnFailed(const SpawnParameters& sp, DWORD code)
{
const auto details = QString::fromStdWString(makeDetails(sp, code));
log::error("{}", details);
const auto binary = QFileInfo(QString::fromStdWString(sp.binary));
const auto title = QObject::tr("Cannot launch program");
const auto mainText = QObject::tr("Cannot start %1")
.arg(binary.fileName());
QString content;
if (code == ERROR_INVALID_PARAMETER) {
content = QObject::tr(
"This error typically happens because an antivirus has deleted critical "
"files from Mod Organizer's installation folder or has made them "
"generally inaccessible. Add an exclusion for Mod Organizer's "
"installation folder in your antivirus, reinstall Mod Organizer and try "
"again.");
} else if (code == ERROR_ACCESS_DENIED) {
content = QObject::tr(
"This error typically happens because an antivirus is preventing Mod "
"Organizer from starting programs. Add an exclusion for Mod Organizer's "
"installation folder in your antivirus and try again.");
} else {
content = QString::fromStdWString(formatSystemMessage(code));
}
QWidget *window = qApp->activeWindow();
if ((window != nullptr) && (!window->isVisible())) {
window = nullptr;
}
MOBase::TaskDialog(window, title)
.main(mainText)
.content(content)
.details(details)
.exec();
}
bool confirmRestartAsAdmin(const SpawnParameters& sp)
{
const auto details = QString::fromStdWString(
makeDetails(sp, ERROR_ELEVATION_REQUIRED));
log::error("{}", details);
const auto binary = QFileInfo(QString::fromStdWString(sp.binary));
const auto title = QObject::tr("Elevation required");
const auto mainText = QObject::tr("Cannot start %1")
.arg(binary.fileName());
const auto content = QObject::tr(
"This program is requesting to run as administrator but Mod Organizer "
"itself is not running as administrator. Running programs as administrator "
"is typically unnecessary as long as the game and Mod Organizer have been "
"installed outside \"Program Files\".\r\n\r\n"
"You can restart Mod Organizer as administrator and try launching the "
"program again.");
QWidget *window = qApp->activeWindow();
if ((window != nullptr) && (!window->isVisible())) {
window = nullptr;
}
log::debug("asking user to restart MO as administrator");
const auto r = MOBase::TaskDialog(window, title)
.main(mainText)
.content(content)
.details(details)
.button({
QObject::tr("Restart Mod Organizer as administrator"),
QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
QMessageBox::Yes})
.button({
QObject::tr("Cancel"),
QMessageBox::Cancel})
.exec();
return (r == QMessageBox::Yes);
}
} // namespace
void startBinaryAdmin(const SpawnParameters& sp)
{
if (!confirmRestartAsAdmin(sp)) {
log::debug("user declined");
return;
}
log::info("restarting MO as administrator");
WCHAR cwd[MAX_PATH] = {};
if (!GetCurrentDirectory(MAX_PATH, cwd)) {
cwd[0] = L'\0';
}
if (Helper::adminLaunch(
qApp->applicationDirPath().toStdWString(),
qApp->applicationFilePath().toStdWString(),
std::wstring(cwd))) {
qApp->exit(0);
}
}
HANDLE startBinary(
const QFileInfo &binary, const QString &arguments, const QDir ¤tDirectory,
bool hooked, HANDLE stdOut, HANDLE stdErr)
{
SpawnParameters sp;
sp.binary = QDir::toNativeSeparators(binary.absoluteFilePath()).toStdWString();
sp.arguments = arguments.toStdWString();
sp.currentDirectory = QDir::toNativeSeparators(currentDirectory.absolutePath()).toStdWString();
sp.suspended = true;
sp.hooked = hooked;
sp.stdOut = stdOut;
sp.stdErr = stdErr;
HANDLE processHandle, threadHandle;
const auto e = spawn(sp, processHandle, threadHandle);
switch (e)
{
case ERROR_SUCCESS:
{
::CloseHandle(threadHandle);
return processHandle;
}
case ERROR_ELEVATION_REQUIRED:
{
startBinaryAdmin(sp);
return INVALID_HANDLE_VALUE;
}
default:
{
spawnFailed(sp, e);
return INVALID_HANDLE_VALUE;
}
}
}