From 0015a12cf8916d8000c6d14356b4c17c62f4a588 Mon Sep 17 00:00:00 2001
From: isanae <14251494+isanae@users.noreply.github.com>
Date: Wed, 11 Sep 2019 23:36:13 -0400
Subject: rewritten spawn() to use std::wstring instead of manual buffers error
dialogs now use TaskDialog with more user-friendly text
---
src/spawn.cpp | 414 +++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 307 insertions(+), 107 deletions(-)
(limited to 'src/spawn.cpp')
diff --git a/src/spawn.cpp b/src/spawn.cpp
index f77da35f..614bc92f 100644
--- a/src/spawn.cpp
+++ b/src/spawn.cpp
@@ -21,164 +21,364 @@ along with Mod Organizer. If not, see .
#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
+#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;
+};
-static const int BUFSIZE = 4096;
-static bool spawn(LPCWSTR binary, LPCWSTR arguments, LPCWSTR currentDirectory,
- bool suspended, bool hooked,
- HANDLE stdOut, HANDLE stdErr,
- HANDLE& processHandle, HANDLE& threadHandle)
+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;
- ::ZeroMemory(&si, sizeof(si));
- if (stdOut != INVALID_HANDLE_VALUE) {
- si.hStdOutput = stdOut;
+
+ 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 (stdErr != INVALID_HANDLE_VALUE) {
- si.hStdError = stdErr;
+
+ if (sp.stdErr != INVALID_HANDLE_VALUE) {
+ si.hStdError = sp.stdErr;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
- si.cb = sizeof(si);
- size_t length = wcslen(binary) + wcslen(arguments) + 4;
- wchar_t *commandLine = nullptr;
- if (arguments[0] != L'\0') {
- commandLine = new wchar_t[length];
- _snwprintf(commandLine, length, L"\"%ls\" %ls", binary, arguments);
- } else {
- commandLine = new wchar_t[length];
- _snwprintf_s(commandLine, length, _TRUNCATE, L"\"%ls\"", binary);
- }
- QString moPath = QCoreApplication::applicationDirPath();
+ std::wstring commandLine;
- boost::scoped_array oldPath(new TCHAR[BUFSIZE]);
- DWORD offset = ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), BUFSIZE);
- if (offset > BUFSIZE) {
- oldPath.reset(new TCHAR[offset]);
- ::GetEnvironmentVariable(TEXT("PATH"), oldPath.get(), offset);
+ if (sp.arguments[0] != L'\0') {
+ commandLine = L"\"" + sp.binary + L"\" " + sp.arguments;
+ } else {
+ commandLine = L"\"" + sp.binary + L"\"";
}
- {
- boost::scoped_array newPath(new TCHAR[offset + moPath.length() + 2]);
- _tcsncpy(newPath.get(), oldPath.get(), offset);
- newPath.get()[offset] = '\0';
- _tcsncat(newPath.get(), TEXT(";"), 1);
- _tcsncat(newPath.get(), ToWString(QDir::toNativeSeparators(moPath)).c_str(), moPath.length());
+ QString moPath = QCoreApplication::applicationDirPath();
- ::SetEnvironmentVariable(TEXT("PATH"), newPath.get());
- }
+ const auto oldPath = pathEnv();
+ setPathEnv(oldPath + L";" + QDir::toNativeSeparators(moPath).toStdWString());
PROCESS_INFORMATION pi;
BOOL success = FALSE;
- if (hooked) {
- success = ::CreateProcessHooked(nullptr,
- commandLine,
- nullptr, nullptr, // no special process or thread attributes
- inheritHandles, // inherit handles if we plan to use stdout or stderr reroute
- CREATE_BREAKAWAY_FROM_JOB,
- nullptr, // same environment as parent
- currentDirectory, // current directory
- &si, &pi // startup and process information
- );
+
+ 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,
- commandLine,
- nullptr, nullptr, // no special process or thread attributes
- inheritHandles, // inherit handles if we plan to use stdout or stderr reroute
- CREATE_BREAKAWAY_FROM_JOB,
- nullptr, // same environment as parent
- currentDirectory, // current directory
- &si, &pi // startup and process information
- );
+ success = ::CreateProcess(
+ nullptr, const_cast(commandLine.c_str()), nullptr, nullptr,
+ inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr,
+ sp.currentDirectory.c_str(), &si, &pi);
}
- ::SetEnvironmentVariable(TEXT("PATH"), oldPath.get());
-
- delete [] commandLine;
+ const auto e = GetLastError();
+ setPathEnv(oldPath);
if (!success) {
- throw windows_error("failed to start process");
+ return e;
}
processHandle = pi.hProcess;
threadHandle = pi.hThread;
- return true;
+
+ 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;
+}
-HANDLE startBinary(const QFileInfo &binary,
- const QString &arguments,
- const QDir ¤tDirectory,
- bool hooked,
- HANDLE stdOut,
- HANDLE stdErr)
+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;
- std::wstring binaryName = ToWString(QDir::toNativeSeparators(binary.absoluteFilePath()));
- std::wstring currentDirectoryName = ToWString(QDir::toNativeSeparators(currentDirectory.absolutePath()));
+ const auto e = spawn(sp, processHandle, threadHandle);
- try {
- if (!spawn(binaryName.c_str(), ToWString(arguments).c_str(), currentDirectoryName.c_str(),
- true, hooked, stdOut, stdErr, processHandle, threadHandle)) {
- reportError(QObject::tr("failed to spawn \"%1\"").arg(binary.fileName()));
- return INVALID_HANDLE_VALUE;
+ switch (e)
+ {
+ case ERROR_SUCCESS:
+ {
+ ::CloseHandle(threadHandle);
+ return processHandle;
}
- } catch (const windows_error &e) {
- if (e.getErrorCode() == ERROR_ELEVATION_REQUIRED) {
- if (QMessageBox::question(QApplication::activeModalWidget(), QObject::tr("Elevation required"),
- QObject::tr("This process requires elevation to run.\n"
- "This is a potential security risk so I highly advise you to investigate if\n"
- "\"%1\"\n"
- "can be installed to work without elevation.\n\n"
- "Restart Mod Organizer as an elevated process?\n"
- "You will be asked if you want to allow helper.exe to make changes to the system. "
- "You will need to relaunch the process above manually.").arg(
- QDir::toNativeSeparators(binary.absoluteFilePath())),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- WCHAR cwd[MAX_PATH];
- if (!GetCurrentDirectory(MAX_PATH, cwd)) {
- reportError(QObject::tr("failed to spawn \"%1\": %2").arg(binary.fileName()).arg(::GetLastError()));
- cwd[0] = L'\0';
- }
- if (!Helper::adminLaunch(
- qApp->applicationDirPath().toStdWString(),
- qApp->applicationFilePath().toStdWString(),
- std::wstring(cwd))) {
- return INVALID_HANDLE_VALUE;
- }
- qApp->exit(0);
- }
+
+ case ERROR_ELEVATION_REQUIRED:
+ {
+ startBinaryAdmin(sp);
return INVALID_HANDLE_VALUE;
+ }
- } else {
- reportError(QObject::tr("failed to spawn \"%1\": %2").arg(binary.fileName()).arg(e.what()));
+ default:
+ {
+ spawnFailed(sp, e);
return INVALID_HANDLE_VALUE;
}
}
-
- ::CloseHandle(threadHandle);
- return processHandle;
}
--
cgit v1.3.1