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