From 4d269c2e1a625e6d50b7e6272b4f474a921c6bfa Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 29 Oct 2019 11:34:06 -0400 Subject: split to processrunner added IUserInterface::qtWidget() put back IUserInterface in OrganizerCore now that there's a way to get the widget --- src/processrunner.cpp | 634 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 634 insertions(+) create mode 100644 src/processrunner.cpp (limited to 'src/processrunner.cpp') diff --git a/src/processrunner.cpp b/src/processrunner.cpp new file mode 100644 index 00000000..81c4b99e --- /dev/null +++ b/src/processrunner.cpp @@ -0,0 +1,634 @@ +#include "processrunner.h" +#include "organizercore.h" +#include "instancemanager.h" +#include "lockeddialog.h" +#include "iuserinterface.h" +#include "envmodule.h" +#include + +using namespace MOBase; + +void adjustForVirtualized( + const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) +{ + const QString modsPath = settings.paths().mods(); + + // Check if this a request with either an executable or a working directory + // under our mods folder then will start the process in a virtualized + // "environment" with the appropriate paths fixed: + // (i.e. mods\FNIS\path\exe => game\data\path\exe) + QString cwdPath = sp.currentDirectory.absolutePath(); + bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); + QString binPath = sp.binary.absoluteFilePath(); + bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); + if (virtualizedCwd || virtualizedBin) { + if (virtualizedCwd) { + int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); + QString adjustedCwd = cwdPath.mid(cwdOffset, -1); + cwdPath = game->dataDirectory().absolutePath(); + if (cwdOffset >= 0) + cwdPath += adjustedCwd; + + } + + if (virtualizedBin) { + int binOffset = binPath.indexOf('/', modsPath.length() + 1); + QString adjustedBin = binPath.mid(binOffset, -1); + binPath = game->dataDirectory().absolutePath(); + if (binOffset >= 0) + binPath += adjustedBin; + } + + QString cmdline + = QString("launch \"%1\" \"%2\" %3") + .arg(QDir::toNativeSeparators(cwdPath), + QDir::toNativeSeparators(binPath), sp.arguments); + + sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); + sp.arguments = cmdline; + sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); + } +} + +env::Process* getInterestingProcess(std::vector& processes) +{ + if (processes.empty()) { + return nullptr; + } + + // Certain process names we wish to "hide" for aesthetic reason: + const std::vector hiddenList = { + QFileInfo(QCoreApplication::applicationFilePath()).fileName() + }; + + auto isHidden = [&](auto&& p) { + for (auto h : hiddenList) { + if (p.name().contains(h, Qt::CaseInsensitive)) { + return true; + } + } + + return false; + }; + + + for (auto&& root : processes) { + if (!isHidden(root)) { + return &root; + } + + for (auto&& child : root.children()) { + if (!isHidden(child)) { + return &child; + } + } + } + + + // everything is hidden, just pick the first one + return &processes[0]; +} + + +SpawnedProcess::SpawnedProcess(HANDLE handle, spawn::SpawnParameters sp) + : m_handle(handle), m_parameters(std::move(sp)) +{ +} + +SpawnedProcess::SpawnedProcess(SpawnedProcess&& other) + : m_handle(other.m_handle), m_parameters(std::move(other.m_parameters)) +{ + other.m_handle = INVALID_HANDLE_VALUE; +} + +SpawnedProcess& SpawnedProcess::operator=(SpawnedProcess&& other) +{ + if (this != &other) { + destroy(); + + m_handle = other.m_handle; + other.m_handle = INVALID_HANDLE_VALUE; + + m_parameters = std::move(other.m_parameters); + } + + return *this; +} + +SpawnedProcess::~SpawnedProcess() +{ + destroy(); +} + +HANDLE SpawnedProcess::releaseHandle() +{ + const auto h = m_handle; + m_handle = INVALID_HANDLE_VALUE; + return h; +} + +void SpawnedProcess::destroy() +{ + if (m_handle != INVALID_HANDLE_VALUE) { + ::CloseHandle(m_handle); + m_handle = INVALID_HANDLE_VALUE; + } +} + + +ProcessRunner::ProcessRunner(OrganizerCore& core) + : m_core(core), m_ui(nullptr) +{ +} + +void ProcessRunner::setUserInterface(IUserInterface* ui) +{ + m_ui = ui; +} + +bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) +{ + if (!parent && m_ui) { + parent = m_ui->qtWidget(); + } + + const auto fec = spawn::getFileExecutionContext(parent, targetInfo); + + switch (fec.type) + { + case spawn::FileExecutionTypes::Executable: + { + runExecutableFile(fec.binary, fec.arguments, targetInfo.absoluteDir()); + return true; + } + + case spawn::FileExecutionTypes::Other: // fall-through + default: + { + auto r = shell::Open(targetInfo.absoluteFilePath()); + if (!r.success()) { + return false; + } + + // not all files will return a valid handle even if opening them was + // successful, such as inproc handlers (like the photo viewer) + if (r.processHandle() != INVALID_HANDLE_VALUE) { + // steal because it gets closed after the wait + return waitForProcessCompletionWithLock(r.stealProcessHandle(), nullptr); + } + + return true; + } + } +} + +bool ProcessRunner::runExecutableFile( + const QFileInfo &binary, const QString &arguments, + const QDir ¤tDirectory, const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries, + bool refresh) +{ + DWORD processExitCode = 0; + HANDLE processHandle = spawnAndWait( + binary, arguments, m_core.currentProfile()->name(), + currentDirectory, steamAppID, customOverwrite, forcedLibraries, + &processExitCode); + + if (processHandle == INVALID_HANDLE_VALUE) { + // failed + return false; + } + + if (refresh) { + m_core.afterRun(binary, processExitCode); + } + + return true; +} + +bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) +{ + const auto* profile = m_core.currentProfile(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); + } + + const QString customOverwrite = profile->setting( + "custom_overwrites", exe.title()).toString(); + + QList forcedLibraries; + + if (profile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = profile->determineForcedLibraries(exe.title()); + } + + return runExecutableFile( + exe.binaryInfo(), + exe.arguments(), + exe.workingDirectory().length() != 0 ? exe.workingDirectory() : exe.binaryInfo().absolutePath(), + exe.steamAppID(), + customOverwrite, + forcedLibraries, + refresh); +} + +bool ProcessRunner::runShortcut(const MOShortcut& shortcut) +{ + if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance()) { + throw std::runtime_error( + QString("Refusing to run executable from different instance %1:%2") + .arg(shortcut.instance(),shortcut.executable()) + .toLocal8Bit().constData()); + } + + const Executable& exe = m_core.executablesList()->get(shortcut.executable()); + return runExecutable(exe, false); +} + +HANDLE ProcessRunner::runExecutableOrExecutableFile( + const QString& executable, const QStringList &args, const QString &cwd, + const QString& profileOverride, const QString &forcedCustomOverwrite, + bool ignoreCustomOverwrite) +{ + const auto* profile = m_core.currentProfile(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); + } + + QString profileName = profileOverride; + if (profileName == "") { + profileName = profile->name(); + } + + QFileInfo binary; + QString arguments = args.join(" "); + QString currentDirectory = cwd; + QString steamAppID; + QString customOverwrite; + QList forcedLibraries; + + if (executable.contains('\\') || executable.contains('/')) { + // file path + + binary = QFileInfo(executable); + if (binary.isRelative()) { + // relative path, should be relative to game directory + binary = m_core.managedGame()->gameDirectory().absoluteFilePath(executable); + } + + if (currentDirectory == "") { + currentDirectory = binary.absolutePath(); + } + + try { + const Executable& exe = m_core.executablesList()->getByBinary(binary); + steamAppID = exe.steamAppID(); + customOverwrite = profile->setting("custom_overwrites", exe.title()).toString(); + if (profile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = profile->determineForcedLibraries(exe.title()); + } + } catch (const std::runtime_error &) { + // nop + } + } else { + // only a file name, search executables list + try { + const Executable &exe = m_core.executablesList()->get(executable); + steamAppID = exe.steamAppID(); + customOverwrite = profile->setting("custom_overwrites", exe.title()).toString(); + if (profile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = profile->determineForcedLibraries(exe.title()); + } + if (arguments == "") { + arguments = exe.arguments(); + } + binary = exe.binaryInfo(); + if (currentDirectory == "") { + currentDirectory = exe.workingDirectory(); + } + } catch (const std::runtime_error &) { + log::warn("\"{}\" not set up as executable", executable); + binary = QFileInfo(executable); + } + } + + if (!forcedCustomOverwrite.isEmpty()) + customOverwrite = forcedCustomOverwrite; + + if (ignoreCustomOverwrite) + customOverwrite.clear(); + + return spawnAndWait( + binary, + arguments, + profileName, + currentDirectory, + steamAppID, + customOverwrite, + forcedLibraries); +} + +HANDLE ProcessRunner::spawnAndWait( + const QFileInfo &binary, const QString &arguments, const QString &profileName, + const QDir ¤tDirectory, const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries, + LPDWORD exitCode) +{ + spawn::SpawnParameters sp; + sp.binary = binary; + sp.arguments = arguments; + sp.currentDirectory = currentDirectory; + sp.steamAppID = steamAppID; + sp.hooked = true; + + if (!m_core.beforeRun(binary, profileName, customOverwrite, forcedLibraries)) { + return INVALID_HANDLE_VALUE; + } + + HANDLE handle = spawn(sp).releaseHandle(); + + if (handle == INVALID_HANDLE_VALUE) { + // failed + return INVALID_HANDLE_VALUE; + } + + waitForProcessCompletionWithLock(handle, exitCode); + return handle; +} + +SpawnedProcess ProcessRunner::spawn(spawn::SpawnParameters sp) +{ + QWidget* parent = nullptr; + if (m_ui) { + parent = m_ui->qtWidget(); + } + + if (!checkBinary(parent, sp)) { + return {INVALID_HANDLE_VALUE, sp}; + } + + const auto* game = m_core.managedGame(); + auto& settings = m_core.settings(); + + if (!checkSteam(parent, sp, game->gameDirectory(), sp.steamAppID, settings)) { + return {INVALID_HANDLE_VALUE, sp}; + } + + if (!checkEnvironment(parent, sp)) { + return {INVALID_HANDLE_VALUE, sp}; + } + + if (!checkBlacklist(parent, sp, settings)) { + return {INVALID_HANDLE_VALUE, sp}; + } + + adjustForVirtualized(game, sp, settings); + + return {startBinary(parent, sp), sp}; +} + +void ProcessRunner::withLock(std::function f) +{ + std::unique_ptr dlg; + ILockedWaitingForProcess* uilock = nullptr; + + if (m_ui != nullptr) { + uilock = m_ui->lock(); + } + else { + // i.e. when running command line shortcuts there is no user interface + dlg.reset(new LockedDialog); + dlg->show(); + dlg->setEnabled(true); + uilock = dlg.get(); + } + + Guard g([&]() { + if (m_ui != nullptr) { + m_ui->unlock(); + } + }); + + f(uilock); +} + +bool ProcessRunner::waitForProcessCompletionWithLock( + HANDLE handle, LPDWORD exitCode) +{ + if (!Settings::instance().interface().lockGUI()) { + return true; + } + + bool r = false; + + withLock([&](auto* uilock) { + DWORD ignoreExitCode; + r = waitForProcessCompletion(handle, exitCode ? exitCode : &ignoreExitCode, uilock); + }); + + return r; +} + +bool ProcessRunner::waitForApplication(HANDLE handle, LPDWORD exitCode) +{ + if (!Settings::instance().interface().lockGUI()) + return true; + + bool r = false; + + withLock([&](auto* uilock) { + r = waitForProcessCompletion(handle, exitCode, uilock); + }); + + return r; +} + +bool ProcessRunner::waitForProcessCompletion( + HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) +{ + const auto tree = env::getProcessTree(handle); + std::vector processes = {tree}; + + const auto* interesting = getInterestingProcess(processes); + if (!interesting) { + return true; + } + + if (uilock) { + uilock->setProcessInformation(interesting->pid(), interesting->name()); + } + + auto interestingHandle = interesting->openHandleForWait(); + if (!interestingHandle) { + return true; + } + + auto progress = [&]{ return uilock->unlockForced(); }; + const auto r = waitForProcess( + interestingHandle.get(), exitCode, progress); + + switch (r) + { + case WaitResults::Completed: // fall-through + case WaitResults::Cancelled: + return true; + + case WaitResults::Error: // fall-through + default: + return false; + } +} + +bool ProcessRunner::waitForAllUSVFSProcessesWithLock() +{ + if (!Settings::instance().interface().lockGUI()) + return true; + + bool r = false; + + withLock([&](auto* uilock) { + r = waitForAllUSVFSProcesses(uilock); + }); + + return r; +} + +bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) +{ + for (;;) { + const auto handles = getRunningUSVFSProcesses(); + if (handles.empty()) { + break; + } + + std::vector processes; + for (auto&& h : handles) { + auto p = env::getProcessTree(h); + if (p.isValid()) { + processes.emplace_back(std::move(p)); + } + } + + const auto* interesting = getInterestingProcess(processes); + if (!interesting) { + break; + } + + if (uilock) { + uilock->setProcessInformation(interesting->pid(), interesting->name()); + } + + auto interestingHandle = interesting->openHandleForWait(); + if (!interestingHandle) { + break; + } + + auto progress = [&]{ return uilock->unlockForced(); }; + const auto r = waitForProcess( + interestingHandle.get(), nullptr, progress); + + switch (r) + { + case WaitResults::Completed: + // this process is completed, check for others + break; + + case WaitResults::Cancelled: + // force unlocked + log::debug("waiting for process completion aborted by UI"); + return true; + + case WaitResults::Error: // fall-through + default: + log::debug("waiting for process completion not successful"); + return false; + } + } + + log::debug("waiting for process completion successful"); + return true; +} + + + +WaitResults waitForProcess( + HANDLE handle, DWORD* exitCode, std::function progress) +{ + if (handle == INVALID_HANDLE_VALUE) { + return WaitResults::Error; + } + + log::debug("waiting for completion on pid {}", ::GetProcessId(handle)); + + std::vector handles; + handles.push_back(handle); + + std::vector exitCodes; + + const auto r = waitForProcesses(handles, exitCodes, progress); + + if (r == WaitResults::Completed) { + if (exitCode && !exitCodes.empty()) { + *exitCode = exitCodes[0]; + } + } + + return r; +} + +WaitResults waitForProcesses( + const std::vector& handles, std::vector& exitCodes, + std::function progress) +{ + if (handles.empty()) { + return WaitResults::Completed; + } + + const auto WAIT_OBJECT_N = static_cast(WAIT_OBJECT_0 + handles.size()); + + for (;;) { + // Wait for a an event on the handle, a key press, mouse click or timeout + const auto res = MsgWaitForMultipleObjects( + static_cast(handles.size()), &handles[0], + TRUE, 50, QS_KEY | QS_MOUSEBUTTON); + + if (res == WAIT_FAILED) { + // error + const auto e = ::GetLastError(); + + log::error( + "failed waiting for process completion, {}", formatSystemMessage(e)); + + return WaitResults::Error; + } else if (res >= WAIT_OBJECT_0 && res < WAIT_OBJECT_N) { + // completed + exitCodes.resize(handles.size()); + std::fill(exitCodes.begin(), exitCodes.end(), 0); + + for (std::size_t i=0; i Date: Tue, 29 Oct 2019 12:08:13 -0400 Subject: refactored waiting into waitForProcesses() --- src/processrunner.cpp | 276 +++++++++++++++++++++----------------------------- src/processrunner.h | 15 --- 2 files changed, 116 insertions(+), 175 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 81c4b99e..dbcee3ab 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -8,45 +8,62 @@ using namespace MOBase; -void adjustForVirtualized( - const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) +enum class WaitResults { - const QString modsPath = settings.paths().mods(); + Completed = 1, + Error, + Cancelled +}; - // Check if this a request with either an executable or a working directory - // under our mods folder then will start the process in a virtualized - // "environment" with the appropriate paths fixed: - // (i.e. mods\FNIS\path\exe => game\data\path\exe) - QString cwdPath = sp.currentDirectory.absolutePath(); - bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); - QString binPath = sp.binary.absoluteFilePath(); - bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); - if (virtualizedCwd || virtualizedBin) { - if (virtualizedCwd) { - int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); - QString adjustedCwd = cwdPath.mid(cwdOffset, -1); - cwdPath = game->dataDirectory().absolutePath(); - if (cwdOffset >= 0) - cwdPath += adjustedCwd; - } +WaitResults waitForProcess( + HANDLE handle, DWORD* exitCode, std::function progress) +{ + if (handle == INVALID_HANDLE_VALUE) { + return WaitResults::Error; + } - if (virtualizedBin) { - int binOffset = binPath.indexOf('/', modsPath.length() + 1); - QString adjustedBin = binPath.mid(binOffset, -1); - binPath = game->dataDirectory().absolutePath(); - if (binOffset >= 0) - binPath += adjustedBin; + log::debug("waiting for completion on pid {}", ::GetProcessId(handle)); + + std::vector handles; + handles.push_back(handle); + + std::vector exitCodes; + + for (;;) { + // Wait for a an event on the handle, a key press, mouse click or timeout + const auto res = MsgWaitForMultipleObjects( + 1, &handle, FALSE, 50, QS_KEY | QS_MOUSEBUTTON); + + if (res == WAIT_FAILED) { + // error + const auto e = ::GetLastError(); + + log::error( + "failed waiting for process completion, {}", formatSystemMessage(e)); + + return WaitResults::Error; + } else if (res == WAIT_OBJECT_0) { + // completed + if (exitCode) { + if (!::GetExitCodeProcess(handle, exitCode)) { + const auto e = ::GetLastError(); + log::warn( + "failed to get exit code of process, {}", + formatSystemMessage(e)); + } + } + + return WaitResults::Completed; } - QString cmdline - = QString("launch \"%1\" \"%2\" %3") - .arg(QDir::toNativeSeparators(cwdPath), - QDir::toNativeSeparators(binPath), sp.arguments); + // keep processing events so the app doesn't appear dead + QCoreApplication::sendPostedEvents(); + QCoreApplication::processEvents(); - sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); - sp.arguments = cmdline; - sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); + if (progress && progress()) { + return WaitResults::Cancelled; + } } } @@ -84,11 +101,32 @@ env::Process* getInterestingProcess(std::vector& processes) } } - // everything is hidden, just pick the first one return &processes[0]; } +WaitResults waitForProcesses( + std::vector& processes, + LPDWORD exitCode, ILockedWaitingForProcess* uilock) +{ + const auto* interesting = getInterestingProcess(processes); + if (!interesting) { + return WaitResults::Error; + } + + if (uilock) { + uilock->setProcessInformation(interesting->pid(), interesting->name()); + } + + auto interestingHandle = interesting->openHandleForWait(); + if (!interestingHandle) { + return WaitResults::Error; + } + + auto progress = [&]{ return uilock->unlockForced(); }; + return waitForProcess(interestingHandle.get(), exitCode, progress); +} + SpawnedProcess::SpawnedProcess(HANDLE handle, spawn::SpawnParameters sp) : m_handle(handle), m_parameters(std::move(sp)) @@ -358,6 +396,48 @@ HANDLE ProcessRunner::spawnAndWait( return handle; } +void adjustForVirtualized( + const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) +{ + const QString modsPath = settings.paths().mods(); + + // Check if this a request with either an executable or a working directory + // under our mods folder then will start the process in a virtualized + // "environment" with the appropriate paths fixed: + // (i.e. mods\FNIS\path\exe => game\data\path\exe) + QString cwdPath = sp.currentDirectory.absolutePath(); + bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); + QString binPath = sp.binary.absoluteFilePath(); + bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); + if (virtualizedCwd || virtualizedBin) { + if (virtualizedCwd) { + int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); + QString adjustedCwd = cwdPath.mid(cwdOffset, -1); + cwdPath = game->dataDirectory().absolutePath(); + if (cwdOffset >= 0) + cwdPath += adjustedCwd; + + } + + if (virtualizedBin) { + int binOffset = binPath.indexOf('/', modsPath.length() + 1); + QString adjustedBin = binPath.mid(binOffset, -1); + binPath = game->dataDirectory().absolutePath(); + if (binOffset >= 0) + binPath += adjustedBin; + } + + QString cmdline + = QString("launch \"%1\" \"%2\" %3") + .arg(QDir::toNativeSeparators(cwdPath), + QDir::toNativeSeparators(binPath), sp.arguments); + + sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); + sp.arguments = cmdline; + sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); + } +} + SpawnedProcess ProcessRunner::spawn(spawn::SpawnParameters sp) { QWidget* parent = nullptr; @@ -451,34 +531,8 @@ bool ProcessRunner::waitForProcessCompletion( const auto tree = env::getProcessTree(handle); std::vector processes = {tree}; - const auto* interesting = getInterestingProcess(processes); - if (!interesting) { - return true; - } - - if (uilock) { - uilock->setProcessInformation(interesting->pid(), interesting->name()); - } - - auto interestingHandle = interesting->openHandleForWait(); - if (!interestingHandle) { - return true; - } - - auto progress = [&]{ return uilock->unlockForced(); }; - const auto r = waitForProcess( - interestingHandle.get(), exitCode, progress); - - switch (r) - { - case WaitResults::Completed: // fall-through - case WaitResults::Cancelled: - return true; - - case WaitResults::Error: // fall-through - default: - return false; - } + const auto r = waitForProcesses(processes, exitCode, uilock); + return (r != WaitResults::Error); } bool ProcessRunner::waitForAllUSVFSProcessesWithLock() @@ -511,23 +565,7 @@ bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) } } - const auto* interesting = getInterestingProcess(processes); - if (!interesting) { - break; - } - - if (uilock) { - uilock->setProcessInformation(interesting->pid(), interesting->name()); - } - - auto interestingHandle = interesting->openHandleForWait(); - if (!interestingHandle) { - break; - } - - auto progress = [&]{ return uilock->unlockForced(); }; - const auto r = waitForProcess( - interestingHandle.get(), nullptr, progress); + const auto r = waitForProcesses(processes, nullptr, uilock); switch (r) { @@ -550,85 +588,3 @@ bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) log::debug("waiting for process completion successful"); return true; } - - - -WaitResults waitForProcess( - HANDLE handle, DWORD* exitCode, std::function progress) -{ - if (handle == INVALID_HANDLE_VALUE) { - return WaitResults::Error; - } - - log::debug("waiting for completion on pid {}", ::GetProcessId(handle)); - - std::vector handles; - handles.push_back(handle); - - std::vector exitCodes; - - const auto r = waitForProcesses(handles, exitCodes, progress); - - if (r == WaitResults::Completed) { - if (exitCode && !exitCodes.empty()) { - *exitCode = exitCodes[0]; - } - } - - return r; -} - -WaitResults waitForProcesses( - const std::vector& handles, std::vector& exitCodes, - std::function progress) -{ - if (handles.empty()) { - return WaitResults::Completed; - } - - const auto WAIT_OBJECT_N = static_cast(WAIT_OBJECT_0 + handles.size()); - - for (;;) { - // Wait for a an event on the handle, a key press, mouse click or timeout - const auto res = MsgWaitForMultipleObjects( - static_cast(handles.size()), &handles[0], - TRUE, 50, QS_KEY | QS_MOUSEBUTTON); - - if (res == WAIT_FAILED) { - // error - const auto e = ::GetLastError(); - - log::error( - "failed waiting for process completion, {}", formatSystemMessage(e)); - - return WaitResults::Error; - } else if (res >= WAIT_OBJECT_0 && res < WAIT_OBJECT_N) { - // completed - exitCodes.resize(handles.size()); - std::fill(exitCodes.begin(), exitCodes.end(), 0); - - for (std::size_t i=0; i progress); - -WaitResults waitForProcesses( - const std::vector& handles, std::vector& exitCodes, - std::function progress); - #endif // PROCESSRUNNER_H -- cgit v1.3.1 From b5a5ea1b4d97d79fe6f4ff8a2da2e0120936b179 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 29 Oct 2019 13:17:44 -0400 Subject: recheck the process tree if the current process is not that interesting --- src/processrunner.cpp | 223 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 165 insertions(+), 58 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/processrunner.cpp b/src/processrunner.cpp index dbcee3ab..4e1dda46 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -12,38 +12,27 @@ enum class WaitResults { Completed = 1, Error, - Cancelled + Cancelled, + StillRunning }; -WaitResults waitForProcess( - HANDLE handle, DWORD* exitCode, std::function progress) +WaitResults singleWait(HANDLE handle, DWORD* exitCode) { if (handle == INVALID_HANDLE_VALUE) { return WaitResults::Error; } - log::debug("waiting for completion on pid {}", ::GetProcessId(handle)); + const DWORD WAIT_EVENT = WAIT_OBJECT_0 + 1; - std::vector handles; - handles.push_back(handle); + // Wait for a an event on the handle, a key press, mouse click or timeout + const auto res = MsgWaitForMultipleObjects( + 1, &handle, FALSE, 50, QS_KEY | QS_MOUSEBUTTON); - std::vector exitCodes; - - for (;;) { - // Wait for a an event on the handle, a key press, mouse click or timeout - const auto res = MsgWaitForMultipleObjects( - 1, &handle, FALSE, 50, QS_KEY | QS_MOUSEBUTTON); - - if (res == WAIT_FAILED) { - // error - const auto e = ::GetLastError(); - - log::error( - "failed waiting for process completion, {}", formatSystemMessage(e)); - - return WaitResults::Error; - } else if (res == WAIT_OBJECT_0) { + switch (res) + { + case WAIT_OBJECT_0: + { // completed if (exitCode) { if (!::GetExitCodeProcess(handle, exitCode)) { @@ -57,20 +46,55 @@ WaitResults waitForProcess( return WaitResults::Completed; } - // keep processing events so the app doesn't appear dead - QCoreApplication::sendPostedEvents(); - QCoreApplication::processEvents(); + case WAIT_TIMEOUT: + case WAIT_EVENT: + { + return WaitResults::StillRunning; + } - if (progress && progress()) { - return WaitResults::Cancelled; + case WAIT_FAILED: // fall-through + default: + { + // error + const auto e = ::GetLastError(); + + log::error( + "failed waiting for process completion, {}", formatSystemMessage(e)); + + return WaitResults::Error; } } } -env::Process* getInterestingProcess(std::vector& processes) +enum class Interest +{ + None = 0, + Weak, + Strong +}; + +QString toString(Interest i) +{ + switch (i) + { + case Interest::Weak: + return "weak"; + + case Interest::Strong: + return "strong"; + + case Interest::None: // fall-through + default: + return "no"; + } +} + + +std::pair findInterestingProcessInTrees( + std::vector& processes) { if (processes.empty()) { - return nullptr; + return {{}, Interest::None}; } // Certain process names we wish to "hide" for aesthetic reason: @@ -91,40 +115,132 @@ env::Process* getInterestingProcess(std::vector& processes) for (auto&& root : processes) { if (!isHidden(root)) { - return &root; + return {root, Interest::Strong}; } for (auto&& child : root.children()) { if (!isHidden(child)) { - return &child; + return {child, Interest::Strong}; } } } // everything is hidden, just pick the first one - return &processes[0]; + return {processes[0], Interest::Weak}; } -WaitResults waitForProcesses( - std::vector& processes, - LPDWORD exitCode, ILockedWaitingForProcess* uilock) +std::pair getInterestingProcess( + const std::vector& initialProcesses) { - const auto* interesting = getInterestingProcess(processes); - if (!interesting) { - return WaitResults::Error; + std::vector processes; + + log::debug("getting process tree for {} processes", initialProcesses.size()); + for (auto&& h : initialProcesses) { + auto tree = env::getProcessTree(h); + if (tree.isValid()) { + processes.push_back(tree); + } } - if (uilock) { - uilock->setProcessInformation(interesting->pid(), interesting->name()); + if (processes.empty()) { + log::debug("nothing to wait for"); + return {{}, Interest::None}; } - auto interestingHandle = interesting->openHandleForWait(); - if (!interestingHandle) { - return WaitResults::Error; + const auto interest = findInterestingProcessInTrees(processes); + if (!interest.first.isValid()) { + log::debug("no interesting process to wait for"); + return {{}, Interest::None}; } - auto progress = [&]{ return uilock->unlockForced(); }; - return waitForProcess(interestingHandle.get(), exitCode, progress); + return interest; +} + +const std::chrono::milliseconds Infinite(-1); + +WaitResults timedWait( + HANDLE handle, DWORD* exitCode, ILockedWaitingForProcess* uilock, + std::chrono::milliseconds wait) +{ + using namespace std::chrono; + + high_resolution_clock::time_point start; + if (wait != Infinite) { + start = high_resolution_clock::now(); + } + + for (;;) { + const auto r = singleWait(handle, exitCode); + + if (r != WaitResults::StillRunning) { + return r; + } + + // keep processing events so the app doesn't appear dead + QCoreApplication::sendPostedEvents(); + QCoreApplication::processEvents(); + + if (uilock && uilock->unlockForced()) { + return WaitResults::Cancelled; + } + + if (wait != Infinite) { + const auto now = high_resolution_clock::now(); + if (duration_cast(now - start) >= wait) { + return WaitResults::StillRunning; + } + } + } +} + +WaitResults waitForProcesses( + const std::vector& initialProcesses, + LPDWORD exitCode, ILockedWaitingForProcess* uilock) +{ + using namespace std::chrono; + + if (initialProcesses.empty()) { + return WaitResults::Completed; + } + + DWORD currentPID = 0; + milliseconds wait(50); + + for (;;) { + auto [p, interest] = getInterestingProcess(initialProcesses); + + if (uilock) { + uilock->setProcessInformation(p.pid(), p.name()); + } + + auto interestingHandle = p.openHandleForWait(); + if (!interestingHandle) { + return WaitResults::Error; + } + + if (p.pid() != currentPID) { + currentPID = p.pid(); + + log::debug( + "waiting for completion on {} ({}), {} interest", + p.name(), p.pid(), toString(interest)); + } + + if (interest == Interest::Strong) { + wait = Infinite; + } + + const auto r = timedWait(interestingHandle.get(), exitCode, uilock, wait); + if (r != WaitResults::StillRunning) { + return r; + } + + wait = std::min(wait * 2, milliseconds(2000)); + + log::debug( + "looking for a more interesting process (next check in {}ms)", + wait.count()); + } } @@ -528,10 +644,9 @@ bool ProcessRunner::waitForApplication(HANDLE handle, LPDWORD exitCode) bool ProcessRunner::waitForProcessCompletion( HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) { - const auto tree = env::getProcessTree(handle); - std::vector processes = {tree}; - + std::vector processes = {handle}; const auto r = waitForProcesses(processes, exitCode, uilock); + return (r != WaitResults::Error); } @@ -552,19 +667,11 @@ bool ProcessRunner::waitForAllUSVFSProcessesWithLock() bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) { for (;;) { - const auto handles = getRunningUSVFSProcesses(); - if (handles.empty()) { + const auto processes = getRunningUSVFSProcesses(); + if (processes.empty()) { break; } - std::vector processes; - for (auto&& h : handles) { - auto p = env::getProcessTree(h); - if (p.isValid()) { - processes.emplace_back(std::move(p)); - } - } - const auto r = waitForProcesses(processes, nullptr, uilock); switch (r) -- cgit v1.3.1 From f2cc71779feab647c508084c5dd5c175904c7f0f Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 29 Oct 2019 13:40:45 -0400 Subject: always wait until completion in waitForApplication(), regardless of lock setting --- src/processrunner.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 4e1dda46..014f98ee 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -592,8 +592,7 @@ void ProcessRunner::withLock(std::function f) if (m_ui != nullptr) { uilock = m_ui->lock(); - } - else { + } else { // i.e. when running command line shortcuts there is no user interface dlg.reset(new LockedDialog); dlg->show(); @@ -614,14 +613,14 @@ bool ProcessRunner::waitForProcessCompletionWithLock( HANDLE handle, LPDWORD exitCode) { if (!Settings::instance().interface().lockGUI()) { + log::debug("not waiting for process because user has disabled locking"); return true; } bool r = false; withLock([&](auto* uilock) { - DWORD ignoreExitCode; - r = waitForProcessCompletion(handle, exitCode ? exitCode : &ignoreExitCode, uilock); + r = waitForProcessCompletion(handle, exitCode, uilock); }); return r; @@ -629,8 +628,14 @@ bool ProcessRunner::waitForProcessCompletionWithLock( bool ProcessRunner::waitForApplication(HANDLE handle, LPDWORD exitCode) { - if (!Settings::instance().interface().lockGUI()) - return true; + // don't check for lockGUI() setting; this _always_ locks the ui + // + // this is typically called only from OrganizerProxy, which allows plugins + // to wait on applications until they're finished + // + // the check_fnis plugin for example will start FNIS, wait for it to complete, + // and then check the exit code; this has to work regardless of the locking + // setting bool r = false; @@ -652,8 +657,10 @@ bool ProcessRunner::waitForProcessCompletion( bool ProcessRunner::waitForAllUSVFSProcessesWithLock() { - if (!Settings::instance().interface().lockGUI()) + if (!Settings::instance().interface().lockGUI()) { + log::debug("not waiting for usvfs processes because user has disabled locking"); return true; + } bool r = false; -- cgit v1.3.1 From 2aa70de49e89245467299d94d76825bad31c63a2 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 29 Oct 2019 13:46:33 -0400 Subject: return failure if the user has unlocked in waitForApplication() --- src/processrunner.cpp | 34 ++++++++++++++++++---------------- src/processrunner.h | 3 --- 2 files changed, 18 insertions(+), 19 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 014f98ee..5d4a9fde 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -243,6 +243,14 @@ WaitResults waitForProcesses( } } +WaitResults waitForProcess( + HANDLE initialProcess, LPDWORD exitCode, ILockedWaitingForProcess* uilock) +{ + std::vector processes = {initialProcess}; + return waitForProcesses(processes, exitCode, uilock); +} + + SpawnedProcess::SpawnedProcess(HANDLE handle, spawn::SpawnParameters sp) : m_handle(handle), m_parameters(std::move(sp)) @@ -617,18 +625,20 @@ bool ProcessRunner::waitForProcessCompletionWithLock( return true; } - bool r = false; + auto r = WaitResults::Error; withLock([&](auto* uilock) { - r = waitForProcessCompletion(handle, exitCode, uilock); + r = waitForProcess(handle, exitCode, uilock); }); - return r; + // completed/unlocked is fine + return (r != WaitResults::Error); } bool ProcessRunner::waitForApplication(HANDLE handle, LPDWORD exitCode) { - // don't check for lockGUI() setting; this _always_ locks the ui + // don't check for lockGUI() setting; this _always_ locks the ui and waits + // for completion // // this is typically called only from OrganizerProxy, which allows plugins // to wait on applications until they're finished @@ -637,22 +647,14 @@ bool ProcessRunner::waitForApplication(HANDLE handle, LPDWORD exitCode) // and then check the exit code; this has to work regardless of the locking // setting - bool r = false; + auto r = WaitResults::Error; withLock([&](auto* uilock) { - r = waitForProcessCompletion(handle, exitCode, uilock); + r = waitForProcess(handle, exitCode, uilock); }); - return r; -} - -bool ProcessRunner::waitForProcessCompletion( - HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) -{ - std::vector processes = {handle}; - const auto r = waitForProcesses(processes, exitCode, uilock); - - return (r != WaitResults::Error); + // treat unlocked as an error since this should always wait for completion + return (r == WaitResults::Completed); } bool ProcessRunner::waitForAllUSVFSProcessesWithLock() diff --git a/src/processrunner.h b/src/processrunner.h index d9423893..28f4da75 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -80,9 +80,6 @@ private: bool waitForProcessCompletionWithLock(HANDLE handle, LPDWORD exitCode); - bool waitForProcessCompletion( - HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock); - bool waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock); }; -- cgit v1.3.1 From db0b92776b5c9a34ebb1a5ce5c3f0b105844ee16 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 30 Oct 2019 23:42:59 -0400 Subject: added lockwidget to replace all the other dialogs rewrote ProcessRunner to have a bunch of setters and then a run() fixed bad exit code when waiting on a process that's already completed removed lock()/unlock() from main window, ProcessRunner is in charge of that now --- src/CMakeLists.txt | 3 + src/envmodule.cpp | 1 - src/iuserinterface.h | 6 +- src/lockwidget.cpp | 224 ++++++++++++++++ src/lockwidget.h | 68 +++++ src/mainwindow.cpp | 44 +--- src/mainwindow.h | 7 - src/organizercore.cpp | 8 +- src/organizercore.h | 3 +- src/organizerproxy.cpp | 19 +- src/pch.h | 47 ++-- src/processrunner.cpp | 687 ++++++++++++++++++++++++++++--------------------- src/processrunner.h | 70 ++++- 13 files changed, 806 insertions(+), 381 deletions(-) create mode 100644 src/lockwidget.cpp create mode 100644 src/lockwidget.h (limited to 'src/processrunner.cpp') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5e909760..168b79dc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -144,6 +144,7 @@ SET(organizer_SRCS colortable.cpp sanitychecks.cpp processrunner.cpp + lockwidget.cpp shared/windows_error.cpp shared/error_report.cpp @@ -268,6 +269,7 @@ SET(organizer_HDRS envwindows.h colortable.h processrunner.h + lockwidget.h shared/windows_error.h shared/error_report.h @@ -486,6 +488,7 @@ set(widgets filterwidget icondelegate lcdnumber + lockwidget loglist loghighlighter modflagicondelegate diff --git a/src/envmodule.cpp b/src/envmodule.cpp index 0e2e8ec7..09593e61 100644 --- a/src/envmodule.cpp +++ b/src/envmodule.cpp @@ -545,7 +545,6 @@ Process getProcessTree(HANDLE parent) } if (root.pid() == 0) { - log::error("process {} is not running", parentPID); return {}; } diff --git a/src/iuserinterface.h b/src/iuserinterface.h index 91487aee..e5755f03 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -4,12 +4,13 @@ #include "modinfodialogfwd.h" #include "ilockedwaitingforprocess.h" +#include "lockwidget.h" #include #include #include - #include + class IUserInterface { public: @@ -31,9 +32,6 @@ public: virtual MOBase::DelayedFileWriterBase &archivesWriter() = 0; - virtual ILockedWaitingForProcess* lock() = 0; - virtual void unlock() = 0; - virtual QWidget* qtWidget() = 0; }; diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp new file mode 100644 index 00000000..720cac36 --- /dev/null +++ b/src/lockwidget.cpp @@ -0,0 +1,224 @@ +#include "lockwidget.h" +#include "mainwindow.h" +#include +#include +#include + +QWidget* createTransparentWidget(QWidget* parent=nullptr) +{ + auto* w = new QWidget(parent); + + w->setWindowOpacity(0); + w->setAttribute(Qt::WA_NoSystemBackground); + w->setAttribute(Qt::WA_TranslucentBackground); + + return w; +} + + +LockWidget::LockWidget(QWidget* parent, Reasons reason) : + m_parent(parent), m_overlay(nullptr), m_info(nullptr), m_result(NoResult), + m_filter(nullptr) +{ + if (reason != NoReason) { + lock(reason); + } +} + +LockWidget::~LockWidget() +{ + unlock(); +} + +void LockWidget::lock(Reasons reason) +{ + m_result = StillLocked; + createUi(reason); +} + +void LockWidget::unlock() +{ + m_overlay.reset(); + + if (m_filter && m_parent) { + m_parent->removeEventFilter(m_filter.get()); + } + + enableAll(); +} + +void LockWidget::setInfo(DWORD pid, const QString& name) +{ + m_info->setText(QString("%1 (%2)").arg(name).arg(pid)); +} + +LockWidget::Results LockWidget::result() const +{ + return m_result; +} + +void LockWidget::createUi(Reasons reason) +{ + if (m_parent) { + m_overlay.reset(createTransparentWidget(m_parent)); + m_overlay->setWindowFlags(m_overlay->windowFlags() & Qt::FramelessWindowHint); + m_overlay->setGeometry(m_parent->rect()); + } else { + m_overlay.reset(new QDialog); + } + + auto* center = new QFrame; + + if (m_parent) { + center->setFrameStyle(QFrame::StyledPanel); + center->setLineWidth(1); + center->setAutoFillBackground(true); + + auto* shadow = new QGraphicsDropShadowEffect; + shadow->setBlurRadius(50); + shadow->setOffset(0); + shadow->setColor(QColor(0, 0, 0, 100)); + center->setGraphicsEffect(shadow); + } + + m_info = new QLabel(" "); + m_info->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); + + auto* ly = new QVBoxLayout(center); + + if (!m_parent) { + ly->setContentsMargins(0, 0, 0, 0); + } + + auto* message = new QLabel; + ly->addWidget(message); + ly->addWidget(m_info); + + auto* buttons = new QWidget; + auto* buttonsLayout = new QHBoxLayout(buttons); + ly->addWidget(buttons); + + switch (reason) + { + case LockUI: + { + message->setText(QObject::tr( + "Mod Organizer is locked while the executable is running.")); + + auto* unlockButton = new QPushButton(QObject::tr("Unlock")); + QObject::connect(unlockButton, &QPushButton::clicked, [&]{ onForceUnlock(); }); + buttonsLayout->addWidget(unlockButton); + + break; + } + + case OutputRequired: + { + message->setText(QObject::tr( + "The executable must run to completion because a its output is " + "required.")); + + auto* unlockButton = new QPushButton(QObject::tr("Unlock")); + QObject::connect(unlockButton, &QPushButton::clicked, [&]{ onForceUnlock(); }); + buttonsLayout->addWidget(unlockButton); + + break; + } + + case PreventExit: + { + message->setText(QObject::tr( + "Mod Organizer is waiting on processes to finish before exiting.")); + + auto* exit = new QPushButton(QObject::tr("Exit Now")); + QObject::connect(exit, &QPushButton::clicked, [&]{ onForceUnlock(); }); + buttonsLayout->addWidget(exit); + + auto* cancel = new QPushButton(QObject::tr("Cancel")); + QObject::connect(cancel, &QPushButton::clicked, [&]{ onCancel(); }); + buttonsLayout->addWidget(cancel); + + break; + } + } + + auto* grid = new QGridLayout(m_overlay.get()); + grid->addWidget(createTransparentWidget(), 0, 1); + grid->addWidget(createTransparentWidget(), 2, 1); + grid->addWidget(createTransparentWidget(), 1, 0); + grid->addWidget(createTransparentWidget(), 1, 2); + grid->addWidget(center, 1, 1); + + if (!m_parent) { + grid->setContentsMargins(0, 0, 0, 0); + } + + grid->setRowStretch(0, 1); + grid->setRowStretch(2, 1); + grid->setColumnStretch(0, 1); + grid->setColumnStretch(2, 1); + + disableAll(); + + if (m_parent) { + m_filter.reset(new Filter); + m_filter->resized = [=]{ m_overlay->setGeometry(m_parent->rect()); }; + m_parent->installEventFilter(m_filter.get()); + } + + m_overlay->setFocusPolicy(Qt::TabFocus); + m_overlay->setFocus(); + m_overlay->show(); + m_overlay->setEnabled(true); +} + +void LockWidget::onForceUnlock() +{ + m_result = ForceUnlocked; + unlock(); +} + +void LockWidget::onCancel() +{ + m_result = Cancelled; + unlock(); +} + +void LockWidget::disableAll() +{ + if (!m_parent) { + // nothing to disable without a main window + return; + } + + if (auto* mw=dynamic_cast(m_parent)) { + disable(mw->centralWidget()); + disable(mw->menuBar()); + disable(mw->statusBar()); + } + + for (auto* tb : m_parent->findChildren()) { + disable(tb); + } + + for (auto* d : m_parent->findChildren()) { + disable(d); + } +} + +void LockWidget::enableAll() +{ + for (auto* w : m_disabled) { + w->setEnabled(true); + } + + m_disabled.clear(); +} + +void LockWidget::disable(QWidget* w) +{ + if (w->isEnabled()) { + w->setEnabled(false); + m_disabled.push_back(w); + } +} diff --git a/src/lockwidget.h b/src/lockwidget.h new file mode 100644 index 00000000..bfb0b30f --- /dev/null +++ b/src/lockwidget.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +class LockWidget +{ +public: + enum Reasons + { + NoReason = 0, + LockUI, + OutputRequired, + PreventExit + }; + + enum Results + { + NoResult = 0, + StillLocked, + ForceUnlocked, + Cancelled + }; + + LockWidget(QWidget* parent, Reasons reason=NoReason); + ~LockWidget(); + + void lock(Reasons reason); + void unlock(); + + void setInfo(DWORD pid, const QString& name); + Results result() const; + +private: + class Filter : public QObject + { + public: + std::function resized; + + protected: + bool eventFilter(QObject* o, QEvent* e) override + { + if (e->type() == QEvent::Resize) { + if (resized) { + resized(); + } + } + + return QObject::eventFilter(o, e); + } + }; + + + QWidget* m_parent; + std::unique_ptr m_overlay; + QLabel* m_info; + Results m_result; + std::unique_ptr m_filter; + std::vector m_disabled; + + void createUi(Reasons reason); + + void onForceUnlock(); + void onCancel(); + + void disableAll(); + void enableAll(); + void disable(QWidget* w); +}; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ce5280a6..7d3d20b3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -57,7 +57,6 @@ along with Mod Organizer. If not, see . #include "downloadlistwidget.h" #include "messagedialog.h" #include "installationmanager.h" -#include "lockeddialog.h" #include "waitingonclosedialog.h" #include "downloadlistsortproxy.h" #include "motddialog.h" @@ -1311,9 +1310,10 @@ bool MainWindow::canExit() } } - m_exitAfterWait = true; - m_OrganizerCore.processRunner().waitForAllUSVFSProcessesWithLock(); - if (!m_exitAfterWait) { // if operation cancelled + const auto r = m_OrganizerCore.processRunner() + .waitForAllUSVFSProcessesWithLock(LockWidget::PreventExit); + + if (r == ProcessRunner::Cancelled) { return false; } @@ -2258,42 +2258,6 @@ void MainWindow::storeSettings() s.widgets().saveIndex(ui->executablesListBox); } -ILockedWaitingForProcess* MainWindow::lock() -{ - if (m_LockDialog != nullptr) { - ++m_LockCount; - return m_LockDialog; - } - if (m_exitAfterWait) - m_LockDialog = new WaitingOnCloseDialog(this); - else - m_LockDialog = new LockedDialog(this, true); - m_LockDialog->setModal(true); - m_LockDialog->show(); - setEnabled(false); - m_LockDialog->setEnabled(true); //What's the point otherwise? - ++m_LockCount; - return m_LockDialog; -} - -void MainWindow::unlock() -{ - //If you come through here with a null lock pointer, it's a bug! - if (m_LockDialog == nullptr) { - log::debug("Unlocking main window when already unlocked"); - return; - } - --m_LockCount; - if (m_LockCount == 0) { - if (m_exitAfterWait && m_LockDialog->canceled()) - m_exitAfterWait = false; - m_LockDialog->hide(); - m_LockDialog->deleteLater(); - m_LockDialog = nullptr; - setEnabled(true); - } -} - QWidget* MainWindow::qtWidget() { return this; diff --git a/src/mainwindow.h b/src/mainwindow.h index 19723480..c80287b2 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -38,7 +38,6 @@ along with Mod Organizer. If not, see . //when I get round to cleaning up main.cpp class Executable; class CategoryFactory; -class LockedDialogBase; class OrganizerCore; class PluginListSortProxy; @@ -118,8 +117,6 @@ public: void processUpdates(Settings& settings); - ILockedWaitingForProcess* lock() override; - void unlock() override; QWidget* qtWidget() override; bool addProfile(); @@ -381,11 +378,7 @@ private: bool m_DidUpdateMasterList; - LockedDialogBase *m_LockDialog { nullptr }; - uint64_t m_LockCount { 0 }; - bool m_showArchiveData{ true }; - bool m_exitAfterWait{ false }; MOBase::DelayedFileWriter m_ArchiveListWriter; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 0f767f46..89e8bd9e 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -91,7 +91,6 @@ OrganizerCore::OrganizerCore(Settings &settings) , m_PluginContainer(nullptr) , m_GameName() , m_CurrentProfile(nullptr) - , m_Runner(*this) , m_Settings(settings) , m_Updater(NexusInterface::instance(m_PluginContainer)) , m_AboutToRun() @@ -252,7 +251,6 @@ void OrganizerCore::setUserInterface(IUserInterface* ui) m_InstallationManager.setParentWidget(w); m_Updater.setUserInterface(w); - m_Runner.setUserInterface(ui); checkForUpdates(); } @@ -357,7 +355,7 @@ void OrganizerCore::externalMessage(const QString &message) { if (MOShortcut moshortcut{ message } ) { if(moshortcut.hasExecutable()) - m_Runner.runShortcut(moshortcut); + processRunner().runShortcut(moshortcut); } else if (isNxmLink(message)) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); @@ -1721,9 +1719,9 @@ void OrganizerCore::saveCurrentProfile() storeSettings(); } -ProcessRunner& OrganizerCore::processRunner() +ProcessRunner OrganizerCore::processRunner() { - return m_Runner; + return ProcessRunner(*this, m_UserInterface); } bool OrganizerCore::beforeRun( diff --git a/src/organizercore.h b/src/organizercore.h index 2252c118..d4882b92 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -134,7 +134,7 @@ public: bool saveCurrentLists(); - ProcessRunner& processRunner(); + ProcessRunner processRunner(); bool beforeRun( const QFileInfo& binary, const QString& profileName, @@ -305,7 +305,6 @@ private: Profile *m_CurrentProfile; - ProcessRunner m_Runner; Settings& m_Settings; SelfUpdater m_Updater; diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 75b3ea41..3ee35fe2 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -119,7 +119,24 @@ HANDLE OrganizerProxy::startApplication( bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const { - return m_Proxied->processRunner().waitForApplication(handle, exitCode); + const auto r = m_Proxied->processRunner().waitForApplication( + handle, exitCode, LockWidget::OutputRequired); + + switch (r) + { + case ProcessRunner::Completed: + return true; + + case ProcessRunner::Cancelled: // fall-through + case ProcessRunner::ForceUnlocked: + // this is always an error because the application should have run to + // completion + return false; + + case ProcessRunner::Error: // fall-through + default: + return false; + } } bool OrganizerProxy::onAboutToRun(const std::function &func) diff --git a/src/pch.h b/src/pch.h index 01a97357..b7c5d695 100644 --- a/src/pch.h +++ b/src/pch.h @@ -100,9 +100,9 @@ #include #include #include +#include #include #include -#include #include #include #include @@ -111,13 +111,14 @@ #include #include #include -#include +#include #include +#include #include +#include #include #include #include -#include #include #include #include @@ -126,20 +127,21 @@ #include #include #include -#include #include +#include #include #include #include #include #include #include -#include #include #include +#include #include #include #include +#include #include #include #include @@ -190,53 +192,42 @@ #include #include #include -#include #include #include +#include #include #include -#include #include +#include +#include #include #include #include #include -#include #include #include #include +#include #include -#include -#include #include -#include -#include -#include -#include -#include -#include +#include #include #include #include #include #include #include -#include -#include #include #include #include #include #include #include -#include -#include #include #include #include #include #include -#include #include #include #include @@ -255,4 +246,16 @@ #include #include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 5d4a9fde..62c77efc 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -1,26 +1,58 @@ #include "processrunner.h" #include "organizercore.h" #include "instancemanager.h" -#include "lockeddialog.h" #include "iuserinterface.h" #include "envmodule.h" #include using namespace MOBase; -enum class WaitResults +void adjustForVirtualized( + const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) { - Completed = 1, - Error, - Cancelled, - StillRunning -}; + const QString modsPath = settings.paths().mods(); + + // Check if this a request with either an executable or a working directory + // under our mods folder then will start the process in a virtualized + // "environment" with the appropriate paths fixed: + // (i.e. mods\FNIS\path\exe => game\data\path\exe) + QString cwdPath = sp.currentDirectory.absolutePath(); + bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); + QString binPath = sp.binary.absoluteFilePath(); + bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); + if (virtualizedCwd || virtualizedBin) { + if (virtualizedCwd) { + int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); + QString adjustedCwd = cwdPath.mid(cwdOffset, -1); + cwdPath = game->dataDirectory().absolutePath(); + if (cwdOffset >= 0) + cwdPath += adjustedCwd; + + } + + if (virtualizedBin) { + int binOffset = binPath.indexOf('/', modsPath.length() + 1); + QString adjustedBin = binPath.mid(binOffset, -1); + binPath = game->dataDirectory().absolutePath(); + if (binOffset >= 0) + binPath += adjustedBin; + } + + QString cmdline + = QString("launch \"%1\" \"%2\" %3") + .arg(QDir::toNativeSeparators(cwdPath), + QDir::toNativeSeparators(binPath), sp.arguments); + sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); + sp.arguments = cmdline; + sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); + } +} -WaitResults singleWait(HANDLE handle, DWORD* exitCode) +std::optional singleWait(HANDLE handle, DWORD pid) { if (handle == INVALID_HANDLE_VALUE) { - return WaitResults::Error; + return ProcessRunner::Error; } const DWORD WAIT_EVENT = WAIT_OBJECT_0 + 1; @@ -33,23 +65,15 @@ WaitResults singleWait(HANDLE handle, DWORD* exitCode) { case WAIT_OBJECT_0: { - // completed - if (exitCode) { - if (!::GetExitCodeProcess(handle, exitCode)) { - const auto e = ::GetLastError(); - log::warn( - "failed to get exit code of process, {}", - formatSystemMessage(e)); - } - } - - return WaitResults::Completed; + log::debug("process {} completed", pid); + return ProcessRunner::Completed; } case WAIT_TIMEOUT: case WAIT_EVENT: { - return WaitResults::StillRunning; + // still running + return {}; } case WAIT_FAILED: // fall-through @@ -57,11 +81,8 @@ WaitResults singleWait(HANDLE handle, DWORD* exitCode) { // error const auto e = ::GetLastError(); - - log::error( - "failed waiting for process completion, {}", formatSystemMessage(e)); - - return WaitResults::Error; + log::error("failed waiting for {}, {}", pid, formatSystemMessage(e)); + return ProcessRunner::Error; } } } @@ -132,6 +153,11 @@ std::pair findInterestingProcessInTrees( std::pair getInterestingProcess( const std::vector& initialProcesses) { + if (initialProcesses.empty()) { + log::debug("nothing to wait for"); + return {{}, Interest::None}; + } + std::vector processes; log::debug("getting process tree for {} processes", initialProcesses.size()); @@ -143,7 +169,7 @@ std::pair getInterestingProcess( } if (processes.empty()) { - log::debug("nothing to wait for"); + log::debug("processes are already completed"); return {{}, Interest::None}; } @@ -158,9 +184,8 @@ std::pair getInterestingProcess( const std::chrono::milliseconds Infinite(-1); -WaitResults timedWait( - HANDLE handle, DWORD* exitCode, ILockedWaitingForProcess* uilock, - std::chrono::milliseconds wait) +std::optional timedWait( + HANDLE handle, DWORD pid, LockWidget& lock, std::chrono::milliseconds wait) { using namespace std::chrono; @@ -170,37 +195,66 @@ WaitResults timedWait( } for (;;) { - const auto r = singleWait(handle, exitCode); + const auto r = singleWait(handle, pid); - if (r != WaitResults::StillRunning) { - return r; + if (r) { + return *r; } + // still running + // keep processing events so the app doesn't appear dead QCoreApplication::sendPostedEvents(); QCoreApplication::processEvents(); - if (uilock && uilock->unlockForced()) { - return WaitResults::Cancelled; + switch (lock.result()) + { + case LockWidget::StillLocked: + { + break; + } + + case LockWidget::ForceUnlocked: + { + log::debug("waiting for {} force unlocked by user", pid); + return ProcessRunner::ForceUnlocked; + } + + case LockWidget::Cancelled: + { + log::debug("waiting for {} cancelled by user", pid); + return ProcessRunner::Cancelled; + } + + case LockWidget::NoResult: // fall-through + default: + { + // shouldn't happen + log::debug( + "unexpected result {} while waiting for {}", + static_cast(lock.result()), pid); + + return ProcessRunner::Error; + } } if (wait != Infinite) { const auto now = high_resolution_clock::now(); if (duration_cast(now - start) >= wait) { - return WaitResults::StillRunning; + return {}; } } } } -WaitResults waitForProcesses( - const std::vector& initialProcesses, - LPDWORD exitCode, ILockedWaitingForProcess* uilock) +ProcessRunner::Results waitForProcesses( + const std::vector& initialProcesses, LockWidget& lock) { using namespace std::chrono; if (initialProcesses.empty()) { - return WaitResults::Completed; + // shouldn't happen + return ProcessRunner::Completed; } DWORD currentPID = 0; @@ -208,14 +262,16 @@ WaitResults waitForProcesses( for (;;) { auto [p, interest] = getInterestingProcess(initialProcesses); - - if (uilock) { - uilock->setProcessInformation(p.pid(), p.name()); + if (!p.isValid()) { + // nothing to wait on + return ProcessRunner::Completed; } + lock.setInfo(p.pid(), p.name()); + auto interestingHandle = p.openHandleForWait(); if (!interestingHandle) { - return WaitResults::Error; + return ProcessRunner::Error; } if (p.pid() != currentPID) { @@ -230,9 +286,9 @@ WaitResults waitForProcesses( wait = Infinite; } - const auto r = timedWait(interestingHandle.get(), exitCode, uilock, wait); - if (r != WaitResults::StillRunning) { - return r; + const auto r = timedWait(interestingHandle.get(), p.pid(), lock, wait); + if (r) { + return *r; } wait = std::min(wait * 2, milliseconds(2000)); @@ -243,11 +299,24 @@ WaitResults waitForProcesses( } } -WaitResults waitForProcess( - HANDLE initialProcess, LPDWORD exitCode, ILockedWaitingForProcess* uilock) +ProcessRunner::Results waitForProcess( + HANDLE initialProcess, LPDWORD exitCode, LockWidget& lock) { std::vector processes = {initialProcess}; - return waitForProcesses(processes, exitCode, uilock); + + const auto r = waitForProcesses(processes, lock); + + // as long as it's not running anymore, try to get the exit code + if (exitCode && r != ProcessRunner::Running) { + if (!::GetExitCodeProcess(initialProcess, exitCode)) { + const auto e = ::GetLastError(); + log::warn( + "failed to get exit code of process, {}", + formatSystemMessage(e)); + } + } + + return r; } @@ -298,17 +367,64 @@ void SpawnedProcess::destroy() } -ProcessRunner::ProcessRunner(OrganizerCore& core) - : m_core(core), m_ui(nullptr) +ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) : + m_core(core), m_ui(ui), m_lock(LockWidget::NoReason), m_refresh(false), + m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) { + m_sp.hooked = true; } -void ProcessRunner::setUserInterface(IUserInterface* ui) +ProcessRunner& ProcessRunner::setBinary(const QFileInfo &binary) { - m_ui = ui; + m_sp.binary = binary; + return *this; } -bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) +ProcessRunner& ProcessRunner::setArguments(const QString& arguments) +{ + m_sp.arguments = arguments; + return *this; +} + +ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory) +{ + m_sp.currentDirectory = directory; + return *this; +} + +ProcessRunner& ProcessRunner::setSteamID(const QString& steamID) +{ + m_sp.steamAppID = steamID; + return *this; +} + +ProcessRunner& ProcessRunner::setCustomOverwrite(const QString& customOverwrite) +{ + m_customOverwrite = customOverwrite; + return *this; +} + +ProcessRunner& ProcessRunner::setForcedLibraries(const ForcedLibraries& forcedLibraries) +{ + m_forcedLibraries = forcedLibraries; + return *this; +} + +ProcessRunner& ProcessRunner::setProfileName(const QString& profileName) +{ + m_profileName = profileName; + return *this; +} + +ProcessRunner& ProcessRunner::setWaitForCompletion( + LockWidget::Reasons reason, bool refresh) +{ + m_lock = reason; + m_refresh = refresh; + return *this; +} + +ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo) { if (!parent && m_ui) { parent = m_ui->qtWidget(); @@ -320,56 +436,24 @@ bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) { case spawn::FileExecutionTypes::Executable: { - runExecutableFile(fec.binary, fec.arguments, targetInfo.absoluteDir()); - return true; + setBinary(fec.binary); + setArguments(fec.arguments); + setCurrentDirectory(targetInfo.absoluteDir()); + break; } case spawn::FileExecutionTypes::Other: // fall-through default: { - auto r = shell::Open(targetInfo.absoluteFilePath()); - if (!r.success()) { - return false; - } - - // not all files will return a valid handle even if opening them was - // successful, such as inproc handlers (like the photo viewer) - if (r.processHandle() != INVALID_HANDLE_VALUE) { - // steal because it gets closed after the wait - return waitForProcessCompletionWithLock(r.stealProcessHandle(), nullptr); - } - - return true; + m_shellOpen = targetInfo.absoluteFilePath(); + break; } } -} -bool ProcessRunner::runExecutableFile( - const QFileInfo &binary, const QString &arguments, - const QDir ¤tDirectory, const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries, - bool refresh) -{ - DWORD processExitCode = 0; - HANDLE processHandle = spawnAndWait( - binary, arguments, m_core.currentProfile()->name(), - currentDirectory, steamAppID, customOverwrite, forcedLibraries, - &processExitCode); - - if (processHandle == INVALID_HANDLE_VALUE) { - // failed - return false; - } - - if (refresh) { - m_core.afterRun(binary, processExitCode); - } - - return true; + return *this; } -bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) +ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe) { const auto* profile = m_core.currentProfile(); if (!profile) { @@ -379,25 +463,31 @@ bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) const QString customOverwrite = profile->setting( "custom_overwrites", exe.title()).toString(); - QList forcedLibraries; - + ForcedLibraries forcedLibraries; if (profile->forcedLibrariesEnabled(exe.title())) { forcedLibraries = profile->determineForcedLibraries(exe.title()); } - return runExecutableFile( - exe.binaryInfo(), - exe.arguments(), - exe.workingDirectory().length() != 0 ? exe.workingDirectory() : exe.binaryInfo().absolutePath(), - exe.steamAppID(), - customOverwrite, - forcedLibraries, - refresh); + QDir currentDirectory = exe.workingDirectory(); + if (currentDirectory.isEmpty()) { + currentDirectory.setPath(exe.binaryInfo().absolutePath()); + } + + setBinary(exe.binaryInfo()); + setArguments(exe.arguments()); + setCurrentDirectory(currentDirectory); + setSteamID(exe.steamAppID()); + setCustomOverwrite(customOverwrite); + setForcedLibraries(forcedLibraries); + + return *this; } -bool ProcessRunner::runShortcut(const MOShortcut& shortcut) +ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut) { - if (shortcut.hasInstance() && shortcut.instance() != InstanceManager::instance().currentInstance()) { + const auto currentInstance = InstanceManager::instance().currentInstance(); + + if (shortcut.hasInstance() && shortcut.instance() != currentInstance) { throw std::runtime_error( QString("Refusing to run executable from different instance %1:%2") .arg(shortcut.instance(),shortcut.executable()) @@ -405,12 +495,17 @@ bool ProcessRunner::runShortcut(const MOShortcut& shortcut) } const Executable& exe = m_core.executablesList()->get(shortcut.executable()); - return runExecutable(exe, false); + setFromExecutable(exe); + + return *this; } -HANDLE ProcessRunner::runExecutableOrExecutableFile( - const QString& executable, const QStringList &args, const QString &cwd, - const QString& profileOverride, const QString &forcedCustomOverwrite, +ProcessRunner& ProcessRunner::setFromFileOrExecutable( + const QString &executable, + const QStringList &args, + const QString &cwd, + const QString &profileOverride, + const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite) { const auto* profile = m_core.currentProfile(); @@ -418,37 +513,41 @@ HANDLE ProcessRunner::runExecutableOrExecutableFile( throw MyException(QObject::tr("No profile set")); } - QString profileName = profileOverride; - if (profileName == "") { - profileName = profile->name(); - } + setProfileName(profileOverride); - QFileInfo binary; - QString arguments = args.join(" "); - QString currentDirectory = cwd; - QString steamAppID; - QString customOverwrite; - QList forcedLibraries; + //QFileInfo binary; + //QString arguments = args.join(" "); + //QString currentDirectory = cwd; + //QString steamAppID; + //QString customOverwrite; + //QList forcedLibraries; if (executable.contains('\\') || executable.contains('/')) { // file path - binary = QFileInfo(executable); + auto binary = QFileInfo(executable); + if (binary.isRelative()) { // relative path, should be relative to game directory binary = m_core.managedGame()->gameDirectory().absoluteFilePath(executable); } - if (currentDirectory == "") { - currentDirectory = binary.absolutePath(); + setBinary(binary); + + if (cwd == "") { + setCurrentDirectory(binary.absolutePath()); + } else { + setCurrentDirectory(cwd); } try { const Executable& exe = m_core.executablesList()->getByBinary(binary); - steamAppID = exe.steamAppID(); - customOverwrite = profile->setting("custom_overwrites", exe.title()).toString(); + + setSteamID(exe.steamAppID()); + setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); + if (profile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = profile->determineForcedLibraries(exe.title()); + setForcedLibraries(profile->determineForcedLibraries(exe.title())); } } catch (const std::runtime_error &) { // nop @@ -457,223 +556,244 @@ HANDLE ProcessRunner::runExecutableOrExecutableFile( // only a file name, search executables list try { const Executable &exe = m_core.executablesList()->get(executable); - steamAppID = exe.steamAppID(); - customOverwrite = profile->setting("custom_overwrites", exe.title()).toString(); + + setSteamID(exe.steamAppID()); + setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); + if (profile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = profile->determineForcedLibraries(exe.title()); + setForcedLibraries(profile->determineForcedLibraries(exe.title())); } - if (arguments == "") { - arguments = exe.arguments(); + + if (args.isEmpty()) { + setArguments(exe.arguments()); + } else { + setArguments(args.join(" ")); } - binary = exe.binaryInfo(); - if (currentDirectory == "") { - currentDirectory = exe.workingDirectory(); + + setBinary(exe.binaryInfo()); + + if (cwd == "") { + setCurrentDirectory(exe.workingDirectory()); + } else { + setCurrentDirectory(cwd); } } catch (const std::runtime_error &) { log::warn("\"{}\" not set up as executable", executable); - binary = QFileInfo(executable); + setBinary(QFileInfo(executable)); } } - if (!forcedCustomOverwrite.isEmpty()) - customOverwrite = forcedCustomOverwrite; - - if (ignoreCustomOverwrite) - customOverwrite.clear(); + if (ignoreCustomOverwrite) { + setCustomOverwrite(""); + } else if (!forcedCustomOverwrite.isEmpty()) { + setCustomOverwrite(forcedCustomOverwrite); + } - return spawnAndWait( - binary, - arguments, - profileName, - currentDirectory, - steamAppID, - customOverwrite, - forcedLibraries); + return *this; } -HANDLE ProcessRunner::spawnAndWait( - const QFileInfo &binary, const QString &arguments, const QString &profileName, - const QDir ¤tDirectory, const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries, - LPDWORD exitCode) +ProcessRunner::Results ProcessRunner::run() { - spawn::SpawnParameters sp; - sp.binary = binary; - sp.arguments = arguments; - sp.currentDirectory = currentDirectory; - sp.steamAppID = steamAppID; - sp.hooked = true; + if (!m_shellOpen.isEmpty()) { + auto r = shell::Open(m_shellOpen); + if (!r.success()) { + return Error; + } - if (!m_core.beforeRun(binary, profileName, customOverwrite, forcedLibraries)) { - return INVALID_HANDLE_VALUE; - } + // not all files will return a valid handle even if opening them was + // successful, such as inproc handlers (like the photo viewer) + m_handle = r.stealProcessHandle(); + } else { + if (m_profileName.isEmpty()) { + const auto* profile = m_core.currentProfile(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); + } - HANDLE handle = spawn(sp).releaseHandle(); + m_profileName = profile->name(); + } - if (handle == INVALID_HANDLE_VALUE) { - // failed - return INVALID_HANDLE_VALUE; - } + if (!m_core.beforeRun(m_sp.binary, m_profileName, m_customOverwrite, m_forcedLibraries)) { + return Error; + } - waitForProcessCompletionWithLock(handle, exitCode); - return handle; -} + QWidget* parent = nullptr; + if (m_ui) { + parent = m_ui->qtWidget(); + } -void adjustForVirtualized( - const IPluginGame* game, spawn::SpawnParameters& sp, const Settings& settings) -{ - const QString modsPath = settings.paths().mods(); + if (!checkBinary(parent, m_sp)) { + return Error; + } - // Check if this a request with either an executable or a working directory - // under our mods folder then will start the process in a virtualized - // "environment" with the appropriate paths fixed: - // (i.e. mods\FNIS\path\exe => game\data\path\exe) - QString cwdPath = sp.currentDirectory.absolutePath(); - bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); - QString binPath = sp.binary.absoluteFilePath(); - bool virtualizedBin = binPath.startsWith(modsPath, Qt::CaseInsensitive); - if (virtualizedCwd || virtualizedBin) { - if (virtualizedCwd) { - int cwdOffset = cwdPath.indexOf('/', modsPath.length() + 1); - QString adjustedCwd = cwdPath.mid(cwdOffset, -1); - cwdPath = game->dataDirectory().absolutePath(); - if (cwdOffset >= 0) - cwdPath += adjustedCwd; + const auto* game = m_core.managedGame(); + auto& settings = m_core.settings(); + if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) { + return Error; } - if (virtualizedBin) { - int binOffset = binPath.indexOf('/', modsPath.length() + 1); - QString adjustedBin = binPath.mid(binOffset, -1); - binPath = game->dataDirectory().absolutePath(); - if (binOffset >= 0) - binPath += adjustedBin; + if (!checkEnvironment(parent, m_sp)) { + return Error; } - QString cmdline - = QString("launch \"%1\" \"%2\" %3") - .arg(QDir::toNativeSeparators(cwdPath), - QDir::toNativeSeparators(binPath), sp.arguments); + if (!checkBlacklist(parent, m_sp, settings)) { + return Error; + } - sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); - sp.arguments = cmdline; - sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); + adjustForVirtualized(game, m_sp, settings); + + m_handle = startBinary(parent, m_sp); + if (m_handle == INVALID_HANDLE_VALUE) { + return Error; + } + } + + if (m_handle == INVALID_HANDLE_VALUE || m_lock == LockWidget::NoReason) { + return Running; + } else { + const auto r = waitForProcessCompletionWithLock( + m_handle, &m_exitCode, m_lock); + + if (r == Completed && m_refresh) { + m_core.afterRun(m_sp.binary, m_exitCode); + } + + return r; } } -SpawnedProcess ProcessRunner::spawn(spawn::SpawnParameters sp) +DWORD ProcessRunner::exitCode() { - QWidget* parent = nullptr; - if (m_ui) { - parent = m_ui->qtWidget(); - } + return m_exitCode; +} - if (!checkBinary(parent, sp)) { - return {INVALID_HANDLE_VALUE, sp}; - } - const auto* game = m_core.managedGame(); - auto& settings = m_core.settings(); +bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) +{ + setFromFile(parent, targetInfo); + setWaitForCompletion(LockWidget::LockUI, true); - if (!checkSteam(parent, sp, game->gameDirectory(), sp.steamAppID, settings)) { - return {INVALID_HANDLE_VALUE, sp}; - } + const auto r = run(); + return (r != Error); +} - if (!checkEnvironment(parent, sp)) { - return {INVALID_HANDLE_VALUE, sp}; - } +bool ProcessRunner::runExecutableFile( + const QFileInfo &binary, const QString &arguments, + const QDir ¤tDirectory, const QString &steamAppID, + const QString &customOverwrite, + const QList &forcedLibraries, + bool refresh) +{ + setBinary(binary); + setArguments(arguments); + setCurrentDirectory(currentDirectory); + setSteamID(steamAppID); + setCustomOverwrite(customOverwrite); + setForcedLibraries(forcedLibraries); + setWaitForCompletion(LockWidget::LockUI, refresh); + + const auto r = run(); + return (r != Error); +} - if (!checkBlacklist(parent, sp, settings)) { - return {INVALID_HANDLE_VALUE, sp}; - } +bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) +{ + setFromExecutable(exe); + setWaitForCompletion(LockWidget::LockUI, refresh); + + const auto r = run(); + return (r != Error); +} - adjustForVirtualized(game, sp, settings); +bool ProcessRunner::runShortcut(const MOShortcut& shortcut) +{ + setFromShortcut(shortcut); + setWaitForCompletion(LockWidget::LockUI, false); - return {startBinary(parent, sp), sp}; + const auto r = run(); + return (r != Error); } -void ProcessRunner::withLock(std::function f) +HANDLE ProcessRunner::runExecutableOrExecutableFile( + const QString& executable, const QStringList &args, const QString &cwd, + const QString& profileOverride, const QString &forcedCustomOverwrite, + bool ignoreCustomOverwrite) { - std::unique_ptr dlg; - ILockedWaitingForProcess* uilock = nullptr; + setFromFileOrExecutable( + executable, args, cwd, profileOverride, forcedCustomOverwrite, + ignoreCustomOverwrite); - if (m_ui != nullptr) { - uilock = m_ui->lock(); - } else { - // i.e. when running command line shortcuts there is no user interface - dlg.reset(new LockedDialog); - dlg->show(); - dlg->setEnabled(true); - uilock = dlg.get(); - } + setWaitForCompletion(LockWidget::LockUI, true); - Guard g([&]() { - if (m_ui != nullptr) { - m_ui->unlock(); - } - }); + run(); + return m_handle; +} - f(uilock); +void ProcessRunner::withLock( + LockWidget::Reasons reason, std::function f) +{ + auto lock = std::make_unique( + m_ui ? m_ui->qtWidget() : nullptr, reason); + + f(*lock); } -bool ProcessRunner::waitForProcessCompletionWithLock( - HANDLE handle, LPDWORD exitCode) +ProcessRunner::Results ProcessRunner::waitForProcessCompletionWithLock( + HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason) { if (!Settings::instance().interface().lockGUI()) { log::debug("not waiting for process because user has disabled locking"); - return true; + return ForceUnlocked; } - auto r = WaitResults::Error; - - withLock([&](auto* uilock) { - r = waitForProcess(handle, exitCode, uilock); - }); - - // completed/unlocked is fine - return (r != WaitResults::Error); + return waitForApplication(handle, exitCode, reason); } -bool ProcessRunner::waitForApplication(HANDLE handle, LPDWORD exitCode) +ProcessRunner::Results ProcessRunner::waitForApplication( + HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason) { // don't check for lockGUI() setting; this _always_ locks the ui and waits // for completion // - // this is typically called only from OrganizerProxy, which allows plugins - // to wait on applications until they're finished + // this is typically called only from: + // 1) OrganizerProxy, which allows plugins to wait on applications until + // they're finished + // + // the check_fnis plugin for example will start FNIS, wait for it to + // complete, and then check the exit code; this has to work regardless of + // the locking setting; // - // the check_fnis plugin for example will start FNIS, wait for it to complete, - // and then check the exit code; this has to work regardless of the locking - // setting + // 2) waitForProcessCompletionWithLock() above, which has already checked the + // lock setting - auto r = WaitResults::Error; + auto r = Error; - withLock([&](auto* uilock) { - r = waitForProcess(handle, exitCode, uilock); + withLock(reason, [&](auto& lock) { + r = waitForProcess(handle, exitCode, lock); }); - // treat unlocked as an error since this should always wait for completion - return (r == WaitResults::Completed); + return r; } -bool ProcessRunner::waitForAllUSVFSProcessesWithLock() +ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( + LockWidget::Reasons reason) { if (!Settings::instance().interface().lockGUI()) { log::debug("not waiting for usvfs processes because user has disabled locking"); - return true; + return ForceUnlocked; } - bool r = false; + auto r = Error; - withLock([&](auto* uilock) { - r = waitForAllUSVFSProcesses(uilock); + withLock(reason, [&](auto& lock) { + r = waitForAllUSVFSProcesses(lock); }); return r; } -bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) +ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcesses(LockWidget& lock) { for (;;) { const auto processes = getRunningUSVFSProcesses(); @@ -681,26 +801,15 @@ bool ProcessRunner::waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock) break; } - const auto r = waitForProcesses(processes, nullptr, uilock); + const auto r = waitForProcesses(processes, lock); - switch (r) - { - case WaitResults::Completed: - // this process is completed, check for others - break; - - case WaitResults::Cancelled: - // force unlocked - log::debug("waiting for process completion aborted by UI"); - return true; - - case WaitResults::Error: // fall-through - default: - log::debug("waiting for process completion not successful"); - return false; + if (r != Completed) { + // error, cancelled, or unlocked + return r; } + + // this process is completed, check for others } - log::debug("waiting for process completion successful"); - return true; + return Completed; } diff --git a/src/processrunner.h b/src/processrunner.h index 28f4da75..b7895903 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -2,10 +2,10 @@ #define PROCESSRUNNER_H #include "spawn.h" +#include "lockwidget.h" #include class OrganizerCore; -class ILockedWaitingForProcess; class IUserInterface; class Executable; class MOShortcut; @@ -35,9 +35,43 @@ private: class ProcessRunner { public: - ProcessRunner(OrganizerCore& core); + enum Results + { + Running = 1, + Completed, + Error, + Cancelled, + ForceUnlocked + }; + + using ForcedLibraries = QList; + + ProcessRunner(OrganizerCore& core, IUserInterface* ui); + + ProcessRunner& setBinary(const QFileInfo &binary); + ProcessRunner& setArguments(const QString& arguments); + ProcessRunner& setCurrentDirectory(const QDir& directory); + ProcessRunner& setSteamID(const QString& steamID); + ProcessRunner& setCustomOverwrite(const QString& customOverwrite); + ProcessRunner& setForcedLibraries(const ForcedLibraries& forcedLibraries); + ProcessRunner& setProfileName(const QString& profileName); + ProcessRunner& setWaitForCompletion(LockWidget::Reasons reason, bool refresh); + + ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo); + ProcessRunner& setFromExecutable(const Executable& exe); + ProcessRunner& setFromShortcut(const MOShortcut& shortcut); + + ProcessRunner& setFromFileOrExecutable( + const QString &executable, + const QStringList &args, + const QString &cwd, + const QString &profile, + const QString &forcedCustomOverwrite = "", + bool ignoreCustomOverwrite = false); + + Results run(); + DWORD exitCode(); - void setUserInterface(IUserInterface* ui); bool runFile(QWidget* parent, const QFileInfo& targetInfo); @@ -53,17 +87,31 @@ public: bool runShortcut(const MOShortcut& shortcut); HANDLE runExecutableOrExecutableFile( - const QString &executable, const QStringList &args, const QString &cwd, - const QString &profile, const QString &forcedCustomOverwrite = "", + const QString &executable, + const QStringList &args, + const QString &cwd, + const QString &profile, + const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false); - bool waitForApplication(HANDLE processHandle, LPDWORD exitCode = nullptr); - bool waitForAllUSVFSProcessesWithLock(); + Results waitForApplication( + HANDLE processHandle, LPDWORD exitCode, LockWidget::Reasons reason); + + Results waitForAllUSVFSProcessesWithLock(LockWidget::Reasons reason); private: OrganizerCore& m_core; IUserInterface* m_ui; + spawn::SpawnParameters m_sp; + QString m_customOverwrite; + ForcedLibraries m_forcedLibraries; + QString m_profileName; + LockWidget::Reasons m_lock; + bool m_refresh; + QString m_shellOpen; + HANDLE m_handle; + DWORD m_exitCode; HANDLE spawnAndWait( const QFileInfo &binary, const QString &arguments, @@ -76,11 +124,13 @@ private: SpawnedProcess spawn(spawn::SpawnParameters sp); - void withLock(std::function f); + void withLock( + LockWidget::Reasons reason, std::function f); - bool waitForProcessCompletionWithLock(HANDLE handle, LPDWORD exitCode); + Results waitForProcessCompletionWithLock( + HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason); - bool waitForAllUSVFSProcesses(ILockedWaitingForProcess* uilock); + Results waitForAllUSVFSProcesses(LockWidget& lock); }; #endif // PROCESSRUNNER_H -- cgit v1.3.1 From 0cea4833eb48400feb652e883c70d8a2907701c3 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 01:47:08 -0400 Subject: explicit refresh parameter for setWaitForCompletion(), some parts of the ui will crash if things refresh unexpectedly removed runFile() fixed crash when unlocking if some widgets were destroyed in the meantime lock widget will now pick the active window and disable all top levels --- src/lockwidget.cpp | 73 +++++++++++++++++++++++++++--------------- src/lockwidget.h | 2 +- src/mainwindow.cpp | 6 +++- src/modinfodialogconflicts.cpp | 6 +++- src/processrunner.cpp | 25 +++++---------- src/processrunner.h | 13 +++++--- 6 files changed, 75 insertions(+), 50 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp index 720cac36..ad9aefe3 100644 --- a/src/lockwidget.cpp +++ b/src/lockwidget.cpp @@ -59,17 +59,22 @@ LockWidget::Results LockWidget::result() const void LockWidget::createUi(Reasons reason) { - if (m_parent) { - m_overlay.reset(createTransparentWidget(m_parent)); + QWidget* overlayTarget = m_parent; + if (auto* w = qApp->activeWindow()) { + overlayTarget = w; + } + + if (overlayTarget) { + m_overlay.reset(createTransparentWidget(overlayTarget)); m_overlay->setWindowFlags(m_overlay->windowFlags() & Qt::FramelessWindowHint); - m_overlay->setGeometry(m_parent->rect()); + m_overlay->setGeometry(overlayTarget->rect()); } else { m_overlay.reset(new QDialog); } auto* center = new QFrame; - if (m_parent) { + if (overlayTarget) { center->setFrameStyle(QFrame::StyledPanel); center->setLineWidth(1); center->setAutoFillBackground(true); @@ -86,7 +91,7 @@ void LockWidget::createUi(Reasons reason) auto* ly = new QVBoxLayout(center); - if (!m_parent) { + if (!overlayTarget) { ly->setContentsMargins(0, 0, 0, 0); } @@ -115,7 +120,7 @@ void LockWidget::createUi(Reasons reason) case OutputRequired: { message->setText(QObject::tr( - "The executable must run to completion because a its output is " + "The executable must run to completion because its output is " "required.")); auto* unlockButton = new QPushButton(QObject::tr("Unlock")); @@ -149,7 +154,7 @@ void LockWidget::createUi(Reasons reason) grid->addWidget(createTransparentWidget(), 1, 2); grid->addWidget(center, 1, 1); - if (!m_parent) { + if (!overlayTarget) { grid->setContentsMargins(0, 0, 0, 0); } @@ -160,10 +165,10 @@ void LockWidget::createUi(Reasons reason) disableAll(); - if (m_parent) { + if (overlayTarget) { m_filter.reset(new Filter); - m_filter->resized = [=]{ m_overlay->setGeometry(m_parent->rect()); }; - m_parent->installEventFilter(m_filter.get()); + m_filter->resized = [=]{ m_overlay->setGeometry(overlayTarget->rect()); }; + overlayTarget->installEventFilter(m_filter.get()); } m_overlay->setFocusPolicy(Qt::TabFocus); @@ -184,32 +189,48 @@ void LockWidget::onCancel() unlock(); } +template +QList findChildrenImmediate(QWidget* parent) +{ + return parent->findChildren(QString(), Qt::FindDirectChildrenOnly); +} + void LockWidget::disableAll() { - if (!m_parent) { - // nothing to disable without a main window - return; - } + const auto topLevels = QApplication::topLevelWidgets(); - if (auto* mw=dynamic_cast(m_parent)) { - disable(mw->centralWidget()); - disable(mw->menuBar()); - disable(mw->statusBar()); - } + for (auto* w : topLevels) { + if (auto* mw=dynamic_cast(w)) { + disable(mw->centralWidget()); + disable(mw->menuBar()); + disable(mw->statusBar()); - for (auto* tb : m_parent->findChildren()) { - disable(tb); - } + for (auto* tb : findChildrenImmediate(w)) { + disable(tb); + } + + for (auto* d : findChildrenImmediate(w)) { + disable(d); + } + } - for (auto* d : m_parent->findChildren()) { - disable(d); + if (auto* d=dynamic_cast(w)) { + // no central widget, just disable the children, except for the overlay + for (auto* child : findChildrenImmediate(d)) { + if (child != m_overlay.get()) { + disable(child); + } + } + } } } void LockWidget::enableAll() { - for (auto* w : m_disabled) { - w->setEnabled(true); + for (auto w : m_disabled) { + if (w) { + w->setEnabled(true); + } } m_disabled.clear(); diff --git a/src/lockwidget.h b/src/lockwidget.h index bfb0b30f..9c555ac9 100644 --- a/src/lockwidget.h +++ b/src/lockwidget.h @@ -55,7 +55,7 @@ private: QLabel* m_info; Results m_result; std::unique_ptr m_filter; - std::vector m_disabled; + std::vector> m_disabled; void createUi(Reasons reason); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7d3d20b3..63f6c680 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5422,7 +5422,11 @@ void MainWindow::openDataFile() const QString path = m_ContextItem->data(0, Qt::UserRole).toString(); const QFileInfo targetInfo(path); - m_OrganizerCore.processRunner().runFile(this, targetInfo); + + m_OrganizerCore.processRunner() + .setFromFile(this, targetInfo) + .setWaitForCompletion(ProcessRunner::NoRefresh) + .run(); } void MainWindow::openDataOriginExplorer_clicked() diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index 758112cc..8aefd4c6 100644 --- a/src/modinfodialogconflicts.cpp +++ b/src/modinfodialogconflicts.cpp @@ -527,7 +527,11 @@ void ConflictsTab::openItems(QTreeView* tree) // the menu item is only shown for a single selection, but handle all of them // in case this changes for_each_in_selection(tree, [&](const ConflictItem* item) { - core().processRunner().runFile(parentWidget(), item->fileName()); + core().processRunner() + .setFromFile(parentWidget(), item->fileName()) + .setWaitForCompletion(ProcessRunner::NoRefresh) + .run(); + return true; }); } diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 62c77efc..1d9da96b 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -368,7 +368,7 @@ void SpawnedProcess::destroy() ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) : - m_core(core), m_ui(ui), m_lock(LockWidget::NoReason), m_refresh(false), + m_core(core), m_ui(ui), m_lock(LockWidget::NoReason), m_refresh(NoRefresh), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) { m_sp.hooked = true; @@ -417,10 +417,10 @@ ProcessRunner& ProcessRunner::setProfileName(const QString& profileName) } ProcessRunner& ProcessRunner::setWaitForCompletion( - LockWidget::Reasons reason, bool refresh) + RefreshModes refresh, LockWidget::Reasons reason) { - m_lock = reason; m_refresh = refresh; + m_lock = reason; return *this; } @@ -655,7 +655,7 @@ ProcessRunner::Results ProcessRunner::run() const auto r = waitForProcessCompletionWithLock( m_handle, &m_exitCode, m_lock); - if (r == Completed && m_refresh) { + if (r == Completed && m_refresh == Refresh) { m_core.afterRun(m_sp.binary, m_exitCode); } @@ -669,15 +669,6 @@ DWORD ProcessRunner::exitCode() } -bool ProcessRunner::runFile(QWidget* parent, const QFileInfo& targetInfo) -{ - setFromFile(parent, targetInfo); - setWaitForCompletion(LockWidget::LockUI, true); - - const auto r = run(); - return (r != Error); -} - bool ProcessRunner::runExecutableFile( const QFileInfo &binary, const QString &arguments, const QDir ¤tDirectory, const QString &steamAppID, @@ -691,7 +682,7 @@ bool ProcessRunner::runExecutableFile( setSteamID(steamAppID); setCustomOverwrite(customOverwrite); setForcedLibraries(forcedLibraries); - setWaitForCompletion(LockWidget::LockUI, refresh); + setWaitForCompletion(refresh ? Refresh : NoRefresh); const auto r = run(); return (r != Error); @@ -700,7 +691,7 @@ bool ProcessRunner::runExecutableFile( bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) { setFromExecutable(exe); - setWaitForCompletion(LockWidget::LockUI, refresh); + setWaitForCompletion(refresh ? Refresh : NoRefresh); const auto r = run(); return (r != Error); @@ -709,7 +700,7 @@ bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) bool ProcessRunner::runShortcut(const MOShortcut& shortcut) { setFromShortcut(shortcut); - setWaitForCompletion(LockWidget::LockUI, false); + setWaitForCompletion(NoRefresh); const auto r = run(); return (r != Error); @@ -724,7 +715,7 @@ HANDLE ProcessRunner::runExecutableOrExecutableFile( executable, args, cwd, profileOverride, forcedCustomOverwrite, ignoreCustomOverwrite); - setWaitForCompletion(LockWidget::LockUI, true); + setWaitForCompletion(Refresh); run(); return m_handle; diff --git a/src/processrunner.h b/src/processrunner.h index b7895903..21840bfe 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -44,6 +44,12 @@ public: ForceUnlocked }; + enum RefreshModes + { + NoRefresh = 1, + Refresh + }; + using ForcedLibraries = QList; ProcessRunner(OrganizerCore& core, IUserInterface* ui); @@ -55,7 +61,8 @@ public: ProcessRunner& setCustomOverwrite(const QString& customOverwrite); ProcessRunner& setForcedLibraries(const ForcedLibraries& forcedLibraries); ProcessRunner& setProfileName(const QString& profileName); - ProcessRunner& setWaitForCompletion(LockWidget::Reasons reason, bool refresh); + ProcessRunner& setWaitForCompletion( + RefreshModes refresh, LockWidget::Reasons reason=LockWidget::LockUI); ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo); ProcessRunner& setFromExecutable(const Executable& exe); @@ -73,8 +80,6 @@ public: DWORD exitCode(); - bool runFile(QWidget* parent, const QFileInfo& targetInfo); - bool runExecutableFile( const QFileInfo &binary, const QString &arguments, const QDir ¤tDirectory, const QString &steamAppID={}, @@ -108,7 +113,7 @@ private: ForcedLibraries m_forcedLibraries; QString m_profileName; LockWidget::Reasons m_lock; - bool m_refresh; + RefreshModes m_refresh; QString m_shellOpen; HANDLE m_handle; DWORD m_exitCode; -- cgit v1.3.1 From c8e101e19eed4417d42bef678cd60d3efb414eb7 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 01:54:49 -0400 Subject: removed runExecutableFile() turns out on_startButton_clicked() had redundant code --- src/mainwindow.cpp | 43 ++++++++++--------------------------------- src/processrunner.cpp | 26 -------------------------- src/processrunner.h | 7 ------- 3 files changed, 10 insertions(+), 66 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 63f6c680..c1789b0a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2312,41 +2312,18 @@ void MainWindow::installMod(QString fileName) void MainWindow::on_startButton_clicked() { - try { - const Executable* selectedExecutable = getSelectedExecutable(); - if (!selectedExecutable) { - return; - } - - ui->startButton->setEnabled(false); - - auto* profile = m_OrganizerCore.currentProfile(); - - const QString customOverwrite = profile->setting( - "custom_overwrites", selectedExecutable->title()).toString(); - - auto forcedLibraries = profile->determineForcedLibraries( - selectedExecutable->title()); - - if (!profile->forcedLibrariesEnabled(selectedExecutable->title())) { - forcedLibraries.clear(); - } - - m_OrganizerCore.processRunner().runExecutableFile( - selectedExecutable->binaryInfo(), - selectedExecutable->arguments(), - selectedExecutable->workingDirectory().length() != 0 ? - selectedExecutable->workingDirectory() : - selectedExecutable->binaryInfo().absolutePath(), - selectedExecutable->steamAppID(), - customOverwrite, - forcedLibraries); - } catch (...) { - ui->startButton->setEnabled(true); - throw; + const Executable* selectedExecutable = getSelectedExecutable(); + if (!selectedExecutable) { + return; } - ui->startButton->setEnabled(true); + ui->startButton->setEnabled(false); + Guard g([&]{ ui->startButton->setEnabled(true); }); + + m_OrganizerCore.processRunner() + .setFromExecutable(*selectedExecutable) + .setWaitForCompletion(ProcessRunner::Refresh) + .run(); } bool MainWindow::modifyExecutablesDialog(int selection) diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 1d9da96b..dba29bd2 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -515,13 +515,6 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( setProfileName(profileOverride); - //QFileInfo binary; - //QString arguments = args.join(" "); - //QString currentDirectory = cwd; - //QString steamAppID; - //QString customOverwrite; - //QList forcedLibraries; - if (executable.contains('\\') || executable.contains('/')) { // file path @@ -669,25 +662,6 @@ DWORD ProcessRunner::exitCode() } -bool ProcessRunner::runExecutableFile( - const QFileInfo &binary, const QString &arguments, - const QDir ¤tDirectory, const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries, - bool refresh) -{ - setBinary(binary); - setArguments(arguments); - setCurrentDirectory(currentDirectory); - setSteamID(steamAppID); - setCustomOverwrite(customOverwrite); - setForcedLibraries(forcedLibraries); - setWaitForCompletion(refresh ? Refresh : NoRefresh); - - const auto r = run(); - return (r != Error); -} - bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) { setFromExecutable(exe); diff --git a/src/processrunner.h b/src/processrunner.h index 21840bfe..2e9550e0 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -80,13 +80,6 @@ public: DWORD exitCode(); - bool runExecutableFile( - const QFileInfo &binary, const QString &arguments, - const QDir ¤tDirectory, const QString &steamAppID={}, - const QString &customOverwrite={}, - const QList &forcedLibraries={}, - bool refresh=true); - bool runExecutable(const Executable& exe, bool refresh=true); bool runShortcut(const MOShortcut& shortcut); -- cgit v1.3.1 From 2c3079c1dc2aaeb84cbe7e4a021f6b3f22c785a3 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 02:00:58 -0400 Subject: removed runExecutable() --- src/mainwindow.cpp | 8 ++++++-- src/processrunner.cpp | 9 --------- src/processrunner.h | 2 -- 3 files changed, 6 insertions(+), 13 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c1789b0a..3d32aa16 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1536,8 +1536,12 @@ void MainWindow::startExeAction() } action->setEnabled(false); - m_OrganizerCore.processRunner().runExecutable(*itor); - action->setEnabled(true); + Guard g([&]{ action->setEnabled(true); }); + + m_OrganizerCore.processRunner() + .setFromExecutable(*itor) + .setWaitForCompletion(ProcessRunner::Refresh) + .run(); } void MainWindow::activateSelectedProfile() diff --git a/src/processrunner.cpp b/src/processrunner.cpp index dba29bd2..1c8c923b 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -662,15 +662,6 @@ DWORD ProcessRunner::exitCode() } -bool ProcessRunner::runExecutable(const Executable& exe, bool refresh) -{ - setFromExecutable(exe); - setWaitForCompletion(refresh ? Refresh : NoRefresh); - - const auto r = run(); - return (r != Error); -} - bool ProcessRunner::runShortcut(const MOShortcut& shortcut) { setFromShortcut(shortcut); diff --git a/src/processrunner.h b/src/processrunner.h index 2e9550e0..d3437412 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -80,8 +80,6 @@ public: DWORD exitCode(); - bool runExecutable(const Executable& exe, bool refresh=true); - bool runShortcut(const MOShortcut& shortcut); HANDLE runExecutableOrExecutableFile( -- cgit v1.3.1 From b855754c9708c825e6bc1e995cb0262c065c5d20 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 02:18:21 -0400 Subject: removed runShortcut() changed lock widget text when running without a ui --- src/lockwidget.cpp | 16 ++++++++++++---- src/main.cpp | 6 +++++- src/organizercore.cpp | 8 ++++++-- src/processrunner.cpp | 9 --------- src/processrunner.h | 2 -- 5 files changed, 23 insertions(+), 18 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp index ad9aefe3..cc66112e 100644 --- a/src/lockwidget.cpp +++ b/src/lockwidget.cpp @@ -107,8 +107,16 @@ void LockWidget::createUi(Reasons reason) { case LockUI: { - message->setText(QObject::tr( - "Mod Organizer is locked while the executable is running.")); + QString s; + + if (!m_parent) { + s = QObject::tr("Mod Organizer is currently running an application."); + } else { + s = QObject::tr( + "Mod Organizer is locked while the application is running."); + } + + message->setText(s); auto* unlockButton = new QPushButton(QObject::tr("Unlock")); QObject::connect(unlockButton, &QPushButton::clicked, [&]{ onForceUnlock(); }); @@ -120,7 +128,7 @@ void LockWidget::createUi(Reasons reason) case OutputRequired: { message->setText(QObject::tr( - "The executable must run to completion because its output is " + "The application must run to completion because its output is " "required.")); auto* unlockButton = new QPushButton(QObject::tr("Unlock")); @@ -133,7 +141,7 @@ void LockWidget::createUi(Reasons reason) case PreventExit: { message->setText(QObject::tr( - "Mod Organizer is waiting on processes to finish before exiting.")); + "Mod Organizer is waiting on application to close before exiting.")); auto* exit = new QPushButton(QObject::tr("Exit Now")); QObject::connect(exit, &QPushButton::clicked, [&]{ onForceUnlock(); }); diff --git a/src/main.cpp b/src/main.cpp index 5ed7da5d..9decb94e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -632,7 +632,11 @@ int runApplication(MOApplication &application, SingleInstance &instance, if (MOShortcut shortcut{ arguments.at(1) }) { if (shortcut.hasExecutable()) { try { - organizer.processRunner().runShortcut(shortcut); + organizer.processRunner() + .setFromShortcut(shortcut) + .setWaitForCompletion(ProcessRunner::NoRefresh) + .run(); + return 0; } catch (const std::exception &e) { diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 89e8bd9e..57e24f3b 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -354,8 +354,12 @@ void OrganizerCore::downloadRequestedNXM(const QString &url) void OrganizerCore::externalMessage(const QString &message) { if (MOShortcut moshortcut{ message } ) { - if(moshortcut.hasExecutable()) - processRunner().runShortcut(moshortcut); + if(moshortcut.hasExecutable()) { + processRunner() + .setFromShortcut(moshortcut) + .setWaitForCompletion(ProcessRunner::NoRefresh) + .run(); + } } else if (isNxmLink(message)) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 1c8c923b..6ca05147 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -662,15 +662,6 @@ DWORD ProcessRunner::exitCode() } -bool ProcessRunner::runShortcut(const MOShortcut& shortcut) -{ - setFromShortcut(shortcut); - setWaitForCompletion(NoRefresh); - - const auto r = run(); - return (r != Error); -} - HANDLE ProcessRunner::runExecutableOrExecutableFile( const QString& executable, const QStringList &args, const QString &cwd, const QString& profileOverride, const QString &forcedCustomOverwrite, diff --git a/src/processrunner.h b/src/processrunner.h index d3437412..4860be7c 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -80,8 +80,6 @@ public: DWORD exitCode(); - bool runShortcut(const MOShortcut& shortcut); - HANDLE runExecutableOrExecutableFile( const QString &executable, const QStringList &args, -- cgit v1.3.1 From a88f9dd9a703c23263b5561d3cae126e90e36f3f Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 02:52:14 -0400 Subject: removed runExecutableOrExecutableFile() log the actual spawning and requests to start as well as wait from plugins raise the lock widget when it's a dialog --- src/lockwidget.cpp | 5 +++++ src/main.cpp | 16 +++++++++++----- src/organizerproxy.cpp | 32 ++++++++++++++++++++++++++------ src/processrunner.cpp | 14 ++------------ src/processrunner.h | 19 +++++-------------- src/spawn.cpp | 44 ++++++++++++++++++++++++++++++++++---------- 6 files changed, 83 insertions(+), 47 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp index cc66112e..7b6e8430 100644 --- a/src/lockwidget.cpp +++ b/src/lockwidget.cpp @@ -183,6 +183,11 @@ void LockWidget::createUi(Reasons reason) m_overlay->setFocus(); m_overlay->show(); m_overlay->setEnabled(true); + + if (!overlayTarget) { + m_overlay->raise(); + m_overlay->activateWindow(); + } } void LockWidget::onForceUnlock() diff --git a/src/main.cpp b/src/main.cpp index 9decb94e..2b9a2f4d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -653,15 +653,21 @@ int runApplication(MOApplication &application, SingleInstance &instance, else { QString exeName = arguments.at(1); log::debug("starting {} from command line", exeName); + arguments.removeFirst(); // remove application name (ModOrganizer.exe) arguments.removeFirst(); // remove binary name - // pass the remaining parameters to the binary - try { - organizer.processRunner().runExecutableOrExecutableFile( - exeName, arguments, QString(), QString()); + + try + { + // pass the remaining parameters to the binary + organizer.processRunner() + .setFromFileOrExecutable(exeName, arguments) + .setWaitForCompletion(ProcessRunner::NoRefresh) + .run(); return 0; } - catch (const std::exception &e) { + catch (const std::exception &e) + { reportError( QObject::tr("failed to start application: %1").arg(e.what())); return 1; diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 3ee35fe2..9de5b4ba 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -108,17 +108,37 @@ QString OrganizerProxy::pluginDataPath() const } HANDLE OrganizerProxy::startApplication( - const QString &executable, const QStringList &args, const QString &cwd, - const QString &profile, const QString &forcedCustomOverwrite, - bool ignoreCustomOverwrite) + const QString& exe, const QStringList& args, const QString &cwd, + const QString& profile, const QString &overwrite, bool ignoreOverwrite) { - return m_Proxied->processRunner().runExecutableOrExecutableFile( - executable, args, cwd, profile, - forcedCustomOverwrite, ignoreCustomOverwrite); + log::debug( + "a plugin has requested to start an application:\n" + " . executable: '{}'\n" + " . args: '{}'\n" + " . cwd: '{}'\n" + " . profile: '{}'\n" + " . overwrite: '{}'\n" + " . ignore overwrite: {}", + exe, args.join(" "), cwd, profile, overwrite, ignoreOverwrite); + + auto runner = m_Proxied->processRunner(); + + runner + .setFromFileOrExecutable(exe, args, cwd, profile, overwrite, ignoreOverwrite) + .setWaitForCompletion(ProcessRunner::Refresh) + .run(); + + return runner.processHandle(); } bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const { + const auto pid = ::GetProcessId(handle); + + log::debug( + "a plugin wants to wait for an application to complete, pid {}{}", + pid, (pid == 0 ? "unknown (probably already completed)" : "")); + const auto r = m_Proxied->processRunner().waitForApplication( handle, exitCode, LockWidget::OutputRequired); diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 6ca05147..e6f916c5 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -661,22 +661,12 @@ DWORD ProcessRunner::exitCode() return m_exitCode; } - -HANDLE ProcessRunner::runExecutableOrExecutableFile( - const QString& executable, const QStringList &args, const QString &cwd, - const QString& profileOverride, const QString &forcedCustomOverwrite, - bool ignoreCustomOverwrite) +HANDLE ProcessRunner::processHandle() { - setFromFileOrExecutable( - executable, args, cwd, profileOverride, forcedCustomOverwrite, - ignoreCustomOverwrite); - - setWaitForCompletion(Refresh); - - run(); return m_handle; } + void ProcessRunner::withLock( LockWidget::Reasons reason, std::function f) { diff --git a/src/processrunner.h b/src/processrunner.h index 4860be7c..bdd0b260 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -71,23 +71,14 @@ public: ProcessRunner& setFromFileOrExecutable( const QString &executable, const QStringList &args, - const QString &cwd, - const QString &profile, - const QString &forcedCustomOverwrite = "", - bool ignoreCustomOverwrite = false); + const QString &cwd={}, + const QString &profile={}, + const QString &forcedCustomOverwrite={}, + bool ignoreCustomOverwrite=false); Results run(); DWORD exitCode(); - - - HANDLE runExecutableOrExecutableFile( - const QString &executable, - const QStringList &args, - const QString &cwd, - const QString &profile, - const QString &forcedCustomOverwrite = "", - bool ignoreCustomOverwrite = false); - + HANDLE processHandle(); Results waitForApplication( HANDLE processHandle, LPDWORD exitCode, LockWidget::Reasons reason); diff --git a/src/spawn.cpp b/src/spawn.cpp index c8d7c76a..b331db01 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -442,6 +442,25 @@ QMessageBox::StandardButton confirmBlacklisted( 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; @@ -462,30 +481,35 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) si.dwFlags |= STARTF_USESTDHANDLES; } - const auto bin = QDir::toNativeSeparators(sp.binary.absoluteFilePath()).toStdWString(); - const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath()).toStdWString(); + const auto bin = QDir::toNativeSeparators(sp.binary.absoluteFilePath()); + const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath()); - std::wstring commandLine = L"\"" + bin + L"\""; - if (sp.arguments[0] != L'\0') { - commandLine += L" " + sp.arguments.toStdWString(); + QString commandLine = "\"" + bin + "\""; + if (!sp.arguments.isEmpty()) { + commandLine += " " + sp.arguments; } - QString moPath = QCoreApplication::applicationDirPath(); + const QString moPath = QCoreApplication::applicationDirPath(); const auto oldPath = env::addPath(QDir::toNativeSeparators(moPath)); PROCESS_INFORMATION pi; BOOL success = FALSE; + logSpawning(sp, commandLine); + + const auto wcommandLine = commandLine.toStdWString(); + const auto wcwd = cwd.toStdWString(); + if (sp.hooked) { success = ::CreateProcessHooked( - nullptr, const_cast(commandLine.c_str()), nullptr, nullptr, + nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr, - cwd.c_str(), &si, &pi); + wcwd.c_str(), &si, &pi); } else { success = ::CreateProcess( - nullptr, const_cast(commandLine.c_str()), nullptr, nullptr, + nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr, - cwd.c_str(), &si, &pi); + wcwd.c_str(), &si, &pi); } const auto e = GetLastError(); -- cgit v1.3.1 From d72e94a92f31bcc720d12ed0cb2cc75b590e6770 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 03:54:18 -0400 Subject: added attachToProcess(), made waitForApplication() private changed OrganizerProxy::startApplication() so it _doesn't_ wait for completion, which is its original behaviour --- src/organizerproxy.cpp | 13 ++++++++++--- src/processrunner.cpp | 37 +++++++++++++++++++++++++++---------- src/processrunner.h | 19 ++++++------------- 3 files changed, 43 insertions(+), 26 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 9de5b4ba..0b6f8df1 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -123,9 +123,9 @@ HANDLE OrganizerProxy::startApplication( auto runner = m_Proxied->processRunner(); + // don't wait for completion runner .setFromFileOrExecutable(exe, args, cwd, profile, overwrite, ignoreOverwrite) - .setWaitForCompletion(ProcessRunner::Refresh) .run(); return runner.processHandle(); @@ -139,8 +139,15 @@ bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const "a plugin wants to wait for an application to complete, pid {}{}", pid, (pid == 0 ? "unknown (probably already completed)" : "")); - const auto r = m_Proxied->processRunner().waitForApplication( - handle, exitCode, LockWidget::OutputRequired); + auto runner = m_Proxied->processRunner(); + + const auto r = runner + .setWaitForCompletion(ProcessRunner::NoRefresh, LockWidget::OutputRequired) + .attachToProcess(handle); + + if (exitCode) { + *exitCode = runner.exitCode(); + } switch (r) { diff --git a/src/processrunner.cpp b/src/processrunner.cpp index e6f916c5..b732204c 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -593,9 +593,15 @@ ProcessRunner::Results ProcessRunner::run() return Error; } - // not all files will return a valid handle even if opening them was - // successful, such as inproc handlers (like the photo viewer) m_handle = r.stealProcessHandle(); + + // not all files will return a valid handle even if opening them was + // successful, such as inproc handlers (like the photo viewer); in this + // case it's impossible to determine the status, so just say it's still + // running + if (m_handle == INVALID_HANDLE_VALUE) { + return Running; + } } else { if (m_profileName.isEmpty()) { const auto* profile = m_core.currentProfile(); @@ -642,18 +648,29 @@ ProcessRunner::Results ProcessRunner::run() } } - if (m_handle == INVALID_HANDLE_VALUE || m_lock == LockWidget::NoReason) { + return postRun(); +} + +ProcessRunner::Results ProcessRunner::postRun() +{ + if (m_lock == LockWidget::NoReason) { return Running; - } else { - const auto r = waitForProcessCompletionWithLock( - m_handle, &m_exitCode, m_lock); + } - if (r == Completed && m_refresh == Refresh) { - m_core.afterRun(m_sp.binary, m_exitCode); - } + const auto r = waitForProcessCompletionWithLock( + m_handle, &m_exitCode, m_lock); - return r; + if (r == Completed && m_refresh == Refresh) { + m_core.afterRun(m_sp.binary, m_exitCode); } + + return r; +} + +ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h) +{ + m_handle = h; + return postRun(); } DWORD ProcessRunner::exitCode() diff --git a/src/processrunner.h b/src/processrunner.h index bdd0b260..62ec7a9e 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -77,12 +77,11 @@ public: bool ignoreCustomOverwrite=false); Results run(); + Results attachToProcess(HANDLE h); + DWORD exitCode(); HANDLE processHandle(); - Results waitForApplication( - HANDLE processHandle, LPDWORD exitCode, LockWidget::Reasons reason); - Results waitForAllUSVFSProcessesWithLock(LockWidget::Reasons reason); private: @@ -98,16 +97,7 @@ private: HANDLE m_handle; DWORD m_exitCode; - HANDLE spawnAndWait( - const QFileInfo &binary, const QString &arguments, - const QString &profileName, - const QDir ¤tDirectory, - const QString &steamAppID, - const QString &customOverwrite, - const QList &forcedLibraries={}, - LPDWORD exitCode = nullptr); - - SpawnedProcess spawn(spawn::SpawnParameters sp); + Results postRun(); void withLock( LockWidget::Reasons reason, std::function f); @@ -115,6 +105,9 @@ private: Results waitForProcessCompletionWithLock( HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason); + Results waitForApplication( + HANDLE processHandle, LPDWORD exitCode, LockWidget::Reasons reason); + Results waitForAllUSVFSProcesses(LockWidget& lock); }; -- cgit v1.3.1 From 7ebd4debeef2cfdf268679e7b680021d3dc20687 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 05:19:14 -0400 Subject: added a ForceWait flag to bypass disabled locking merged a bunch of unnecessary functions in ProcessRunner --- src/main.cpp | 4 +- src/mainwindow.cpp | 2 +- src/modinfodialogconflicts.cpp | 2 +- src/organizercore.cpp | 2 +- src/organizerproxy.cpp | 6 +- src/processrunner.cpp | 155 ++++++++++++++++++++--------------------- src/processrunner.h | 43 ++++++------ 7 files changed, 107 insertions(+), 107 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/main.cpp b/src/main.cpp index 2b9a2f4d..02347ee3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -634,7 +634,7 @@ int runApplication(MOApplication &application, SingleInstance &instance, try { organizer.processRunner() .setFromShortcut(shortcut) - .setWaitForCompletion(ProcessRunner::NoRefresh) + .setWaitForCompletion() .run(); return 0; @@ -662,7 +662,7 @@ int runApplication(MOApplication &application, SingleInstance &instance, // pass the remaining parameters to the binary organizer.processRunner() .setFromFileOrExecutable(exeName, arguments) - .setWaitForCompletion(ProcessRunner::NoRefresh) + .setWaitForCompletion() .run(); return 0; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d304f8b7..c2d91bcc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5404,7 +5404,7 @@ void MainWindow::openDataFile() m_OrganizerCore.processRunner() .setFromFile(this, targetInfo) - .setWaitForCompletion(ProcessRunner::NoRefresh) + .setWaitForCompletion() .run(); } diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index 8aefd4c6..d37f068c 100644 --- a/src/modinfodialogconflicts.cpp +++ b/src/modinfodialogconflicts.cpp @@ -529,7 +529,7 @@ void ConflictsTab::openItems(QTreeView* tree) for_each_in_selection(tree, [&](const ConflictItem* item) { core().processRunner() .setFromFile(parentWidget(), item->fileName()) - .setWaitForCompletion(ProcessRunner::NoRefresh) + .setWaitForCompletion() .run(); return true; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 943750ed..96ae84a8 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -357,7 +357,7 @@ void OrganizerCore::externalMessage(const QString &message) if(moshortcut.hasExecutable()) { processRunner() .setFromShortcut(moshortcut) - .setWaitForCompletion(ProcessRunner::NoRefresh) + .setWaitForCompletion() .run(); } } diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 0b6f8df1..420e2d82 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -128,7 +128,9 @@ HANDLE OrganizerProxy::startApplication( .setFromFileOrExecutable(exe, args, cwd, profile, overwrite, ignoreOverwrite) .run(); - return runner.processHandle(); + // the plugin is in charge of closing the handle, unless waitForApplication() + // is called on it + return runner.stealProcessHandle().release(); } bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const @@ -142,7 +144,7 @@ bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const auto runner = m_Proxied->processRunner(); const auto r = runner - .setWaitForCompletion(ProcessRunner::NoRefresh, LockWidget::OutputRequired) + .setWaitForCompletion(ProcessRunner::ForceWait, LockWidget::OutputRequired) .attachToProcess(handle); if (exitCode) { diff --git a/src/processrunner.cpp b/src/processrunner.cpp index b732204c..9cc2ce52 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -368,8 +368,8 @@ void SpawnedProcess::destroy() ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) : - m_core(core), m_ui(ui), m_lock(LockWidget::NoReason), m_refresh(NoRefresh), - m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) + m_core(core), m_ui(ui), m_lockReason(LockWidget::NoReason), + m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) { m_sp.hooked = true; } @@ -417,10 +417,10 @@ ProcessRunner& ProcessRunner::setProfileName(const QString& profileName) } ProcessRunner& ProcessRunner::setWaitForCompletion( - RefreshModes refresh, LockWidget::Reasons reason) + WaitFlags flags, LockWidget::Reasons reason) { - m_refresh = refresh; - m_lock = reason; + m_waitFlags = flags; + m_lockReason = reason; return *this; } @@ -593,13 +593,13 @@ ProcessRunner::Results ProcessRunner::run() return Error; } - m_handle = r.stealProcessHandle(); + m_handle.reset(r.stealProcessHandle()); // not all files will return a valid handle even if opening them was // successful, such as inproc handlers (like the photo viewer); in this // case it's impossible to determine the status, so just say it's still // running - if (m_handle == INVALID_HANDLE_VALUE) { + if (m_handle.get() == INVALID_HANDLE_VALUE) { return Running; } } else { @@ -642,8 +642,8 @@ ProcessRunner::Results ProcessRunner::run() adjustForVirtualized(game, m_sp, settings); - m_handle = startBinary(parent, m_sp); - if (m_handle == INVALID_HANDLE_VALUE) { + m_handle.reset(startBinary(parent, m_sp)); + if (m_handle.get() == INVALID_HANDLE_VALUE) { return Error; } } @@ -653,14 +653,46 @@ ProcessRunner::Results ProcessRunner::run() ProcessRunner::Results ProcessRunner::postRun() { - if (m_lock == LockWidget::NoReason) { - return Running; + const bool mustWait = (m_waitFlags & ForceWait); + + if (mustWait && m_lockReason == LockWidget::NoReason) { + // never lock the ui without an escape hatch for the user + log::debug( + "the ForceWait flag is set but the lock reason wasn't, " + "defaulting to LockUI"); + + m_lockReason = LockWidget::LockUI; + } + + if (mustWait) { + if (!Settings::instance().interface().lockGUI()) { + // at least tell the user what's going on + log::debug( + "locking is disabled, but the output of the application is required; " + "overriding this setting and locking the ui"); + } + } else { + // no force wait + + if (m_lockReason == LockWidget::NoReason) { + // no locking requested + return Running; + } + + if (!Settings::instance().interface().lockGUI()) { + // disabling locking is like clicking on unlock immediately + log::debug("not waiting for process because locking is disabled"); + return ForceUnlocked; + } } - const auto r = waitForProcessCompletionWithLock( - m_handle, &m_exitCode, m_lock); + auto r = Error; + + withLock([&](auto& lock) { + r = waitForProcess(m_handle.get(), &m_exitCode, lock); + }); - if (r == Completed && m_refresh == Refresh) { + if (r == Completed && (m_waitFlags & Refresh)) { m_core.afterRun(m_sp.binary, m_exitCode); } @@ -669,101 +701,64 @@ ProcessRunner::Results ProcessRunner::postRun() ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h) { - m_handle = h; + m_handle.reset(h); return postRun(); } -DWORD ProcessRunner::exitCode() +DWORD ProcessRunner::exitCode() const { return m_exitCode; } -HANDLE ProcessRunner::processHandle() +HANDLE ProcessRunner::getProcessHandle() const { - return m_handle; + return m_handle.get(); } - -void ProcessRunner::withLock( - LockWidget::Reasons reason, std::function f) +env::HandlePtr ProcessRunner::stealProcessHandle() { - auto lock = std::make_unique( - m_ui ? m_ui->qtWidget() : nullptr, reason); - - f(*lock); + return std::move(m_handle); } -ProcessRunner::Results ProcessRunner::waitForProcessCompletionWithLock( - HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason) +ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( + LockWidget::Reasons reason) { + m_lockReason = reason; + if (!Settings::instance().interface().lockGUI()) { - log::debug("not waiting for process because user has disabled locking"); + // disabling locking is like clicking on unlock immediately return ForceUnlocked; } - return waitForApplication(handle, exitCode, reason); -} - -ProcessRunner::Results ProcessRunner::waitForApplication( - HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason) -{ - // don't check for lockGUI() setting; this _always_ locks the ui and waits - // for completion - // - // this is typically called only from: - // 1) OrganizerProxy, which allows plugins to wait on applications until - // they're finished - // - // the check_fnis plugin for example will start FNIS, wait for it to - // complete, and then check the exit code; this has to work regardless of - // the locking setting; - // - // 2) waitForProcessCompletionWithLock() above, which has already checked the - // lock setting - auto r = Error; - withLock(reason, [&](auto& lock) { - r = waitForProcess(handle, exitCode, lock); - }); + withLock([&](auto& lock) { + for (;;) { + const auto processes = getRunningUSVFSProcesses(); + if (processes.empty()) { + break; + } - return r; -} + r = waitForProcesses(processes, lock); -ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( - LockWidget::Reasons reason) -{ - if (!Settings::instance().interface().lockGUI()) { - log::debug("not waiting for usvfs processes because user has disabled locking"); - return ForceUnlocked; - } + if (r != Completed) { + // error, cancelled, or unlocked + return; + } - auto r = Error; + // this process is completed, check for others + } - withLock(reason, [&](auto& lock) { - r = waitForAllUSVFSProcesses(lock); + r = Completed; }); return r; } -ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcesses(LockWidget& lock) +void ProcessRunner::withLock(std::function f) { - for (;;) { - const auto processes = getRunningUSVFSProcesses(); - if (processes.empty()) { - break; - } - - const auto r = waitForProcesses(processes, lock); - - if (r != Completed) { - // error, cancelled, or unlocked - return r; - } - - // this process is completed, check for others - } + auto lk = std::make_unique( + m_ui ? m_ui->qtWidget() : nullptr, m_lockReason); - return Completed; + f(*lk); } diff --git a/src/processrunner.h b/src/processrunner.h index 62ec7a9e..41c0f12c 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -3,6 +3,7 @@ #include "spawn.h" #include "lockwidget.h" +#include "envmodule.h" #include class OrganizerCore; @@ -44,16 +45,25 @@ public: ForceUnlocked }; - enum RefreshModes + enum WaitFlag { - NoRefresh = 1, - Refresh + NoFlags = 0x00, + Refresh = 0x01, + ForceWait = 0x02 }; + using WaitFlags = QFlags; + using ForcedLibraries = QList; ProcessRunner(OrganizerCore& core, IUserInterface* ui); + // move only + ProcessRunner(ProcessRunner&&) = default; + ProcessRunner& operator=(const ProcessRunner&) = delete; + ProcessRunner(const ProcessRunner&) = delete; + ProcessRunner& operator=(ProcessRunner&&) = delete; + ProcessRunner& setBinary(const QFileInfo &binary); ProcessRunner& setArguments(const QString& arguments); ProcessRunner& setCurrentDirectory(const QDir& directory); @@ -62,7 +72,7 @@ public: ProcessRunner& setForcedLibraries(const ForcedLibraries& forcedLibraries); ProcessRunner& setProfileName(const QString& profileName); ProcessRunner& setWaitForCompletion( - RefreshModes refresh, LockWidget::Reasons reason=LockWidget::LockUI); + WaitFlags flags=NoFlags, LockWidget::Reasons reason=LockWidget::LockUI); ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo); ProcessRunner& setFromExecutable(const Executable& exe); @@ -79,8 +89,9 @@ public: Results run(); Results attachToProcess(HANDLE h); - DWORD exitCode(); - HANDLE processHandle(); + DWORD exitCode() const; + HANDLE getProcessHandle() const; + env::HandlePtr stealProcessHandle(); Results waitForAllUSVFSProcessesWithLock(LockWidget::Reasons reason); @@ -91,24 +102,16 @@ private: QString m_customOverwrite; ForcedLibraries m_forcedLibraries; QString m_profileName; - LockWidget::Reasons m_lock; - RefreshModes m_refresh; + LockWidget::Reasons m_lockReason; + WaitFlags m_waitFlags; QString m_shellOpen; - HANDLE m_handle; + env::HandlePtr m_handle; DWORD m_exitCode; Results postRun(); - - void withLock( - LockWidget::Reasons reason, std::function f); - - Results waitForProcessCompletionWithLock( - HANDLE handle, LPDWORD exitCode, LockWidget::Reasons reason); - - Results waitForApplication( - HANDLE processHandle, LPDWORD exitCode, LockWidget::Reasons reason); - - Results waitForAllUSVFSProcesses(LockWidget& lock); + void withLock(std::function f); }; +Q_DECLARE_OPERATORS_FOR_FLAGS(ProcessRunner::WaitFlags); + #endif // PROCESSRUNNER_H -- cgit v1.3.1 From 7db5f938841933fb4846441448dafefc414ed5e7 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 05:33:53 -0400 Subject: removed unused SpawnedProcess lock widget now interprets closing the dialog as a forced unlock --- src/lockwidget.cpp | 1 + src/lockwidget.h | 5 +++++ src/processrunner.cpp | 47 ----------------------------------------------- src/processrunner.h | 25 +++---------------------- 4 files changed, 9 insertions(+), 69 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp index 7b6e8430..bbffd390 100644 --- a/src/lockwidget.cpp +++ b/src/lockwidget.cpp @@ -176,6 +176,7 @@ void LockWidget::createUi(Reasons reason) if (overlayTarget) { m_filter.reset(new Filter); m_filter->resized = [=]{ m_overlay->setGeometry(overlayTarget->rect()); }; + m_filter->closed = [=]{ onForceUnlock(); }; overlayTarget->installEventFilter(m_filter.get()); } diff --git a/src/lockwidget.h b/src/lockwidget.h index 9c555ac9..18ff76dd 100644 --- a/src/lockwidget.h +++ b/src/lockwidget.h @@ -35,6 +35,7 @@ private: { public: std::function resized; + std::function closed; protected: bool eventFilter(QObject* o, QEvent* e) override @@ -43,6 +44,10 @@ private: if (resized) { resized(); } + } else if (e->type() == QEvent::Close) { + if (closed) { + closed(); + } } return QObject::eventFilter(o, e); diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 9cc2ce52..58b2a771 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -320,53 +320,6 @@ ProcessRunner::Results waitForProcess( } - -SpawnedProcess::SpawnedProcess(HANDLE handle, spawn::SpawnParameters sp) - : m_handle(handle), m_parameters(std::move(sp)) -{ -} - -SpawnedProcess::SpawnedProcess(SpawnedProcess&& other) - : m_handle(other.m_handle), m_parameters(std::move(other.m_parameters)) -{ - other.m_handle = INVALID_HANDLE_VALUE; -} - -SpawnedProcess& SpawnedProcess::operator=(SpawnedProcess&& other) -{ - if (this != &other) { - destroy(); - - m_handle = other.m_handle; - other.m_handle = INVALID_HANDLE_VALUE; - - m_parameters = std::move(other.m_parameters); - } - - return *this; -} - -SpawnedProcess::~SpawnedProcess() -{ - destroy(); -} - -HANDLE SpawnedProcess::releaseHandle() -{ - const auto h = m_handle; - m_handle = INVALID_HANDLE_VALUE; - return h; -} - -void SpawnedProcess::destroy() -{ - if (m_handle != INVALID_HANDLE_VALUE) { - ::CloseHandle(m_handle); - m_handle = INVALID_HANDLE_VALUE; - } -} - - ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) : m_core(core), m_ui(ui), m_lockReason(LockWidget::NoReason), m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) diff --git a/src/processrunner.h b/src/processrunner.h index 41c0f12c..0fb3f59a 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -11,28 +11,9 @@ class IUserInterface; class Executable; class MOShortcut; -class SpawnedProcess -{ -public: - SpawnedProcess(HANDLE handle, spawn::SpawnParameters sp); - - SpawnedProcess(const SpawnedProcess&) = delete; - SpawnedProcess& operator=(const SpawnedProcess&) = delete; - SpawnedProcess(SpawnedProcess&& other); - SpawnedProcess& operator=(SpawnedProcess&& other); - ~SpawnedProcess(); - - HANDLE releaseHandle(); - void wait(); - -private: - HANDLE m_handle; - spawn::SpawnParameters m_parameters; - - void destroy(); -}; - - +// handles spawning a process and waiting for it, including setting up the lock +// widget if required +// class ProcessRunner { public: -- cgit v1.3.1 From ada2ac0cb5d0ef2039ce55794928c4d05255ed65 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Thu, 31 Oct 2019 07:37:24 -0400 Subject: removed redundant checkBinary(), which used to check for non existing binaries, that's handled when spawning removed useless checkEnvironment(), which checked for EventLog removed CREATE_BREAKAWAY_FROM_JOB from CreateProcess() calls, didn't do anything fixed setFromFileOrExecutable() when running just a filename that's not an executable name split run() fixed lock widget being disabled when running without a ui --- src/lockwidget.cpp | 12 ++- src/processrunner.cpp | 197 ++++++++++++++++++++++++++++++++------------------ src/processrunner.h | 76 ++++++++++++++++++- src/spawn.cpp | 43 +---------- src/spawn.h | 4 - 5 files changed, 212 insertions(+), 120 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp index bbffd390..35d51dab 100644 --- a/src/lockwidget.cpp +++ b/src/lockwidget.cpp @@ -229,10 +229,14 @@ void LockWidget::disableAll() } if (auto* d=dynamic_cast(w)) { - // no central widget, just disable the children, except for the overlay - for (auto* child : findChildrenImmediate(d)) { - if (child != m_overlay.get()) { - disable(child); + // don't disable stuff if this dialog is the overlay, which happens when + // there's no ui + if (d != m_overlay.get()) { + // no central widget, just disable the children, except for the overlay + for (auto* child : findChildrenImmediate(d)) { + if (child != m_overlay.get()) { + disable(child); + } } } } diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 58b2a771..220ffe38 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -111,6 +111,9 @@ QString toString(Interest i) } +// returns a process that's in the hidden list, or the top-level process if +// they're all hidden; returns an invalid process if the list is empty +// std::pair findInterestingProcessInTrees( std::vector& processes) { @@ -150,6 +153,8 @@ std::pair findInterestingProcessInTrees( return {processes[0], Interest::Weak}; } +// gets the most interesting process in the list +// std::pair getInterestingProcess( const std::vector& initialProcesses) { @@ -160,7 +165,7 @@ std::pair getInterestingProcess( std::vector processes; - log::debug("getting process tree for {} processes", initialProcesses.size()); + // getting process trees for all processes for (auto&& h : initialProcesses) { auto tree = env::getProcessTree(h); if (tree.isValid()) { @@ -169,12 +174,15 @@ std::pair getInterestingProcess( } if (processes.empty()) { + // if the initial list wasn't empty but this one is, it means all the + // processes were already completed log::debug("processes are already completed"); return {{}, Interest::None}; } const auto interest = findInterestingProcessInTrees(processes); if (!interest.first.isValid()) { + // this shouldn't happen log::debug("no interesting process to wait for"); return {{}, Interest::None}; } @@ -184,6 +192,8 @@ std::pair getInterestingProcess( const std::chrono::milliseconds Infinite(-1); +// waits for completion, times out after `wait` if not Infinite +// std::optional timedWait( HANDLE handle, DWORD pid, LockWidget& lock, std::chrono::milliseconds wait) { @@ -195,18 +205,21 @@ std::optional timedWait( } for (;;) { + // wait for a very short while, allows for processing events below const auto r = singleWait(handle, pid); if (r) { + // the process has either completed or an error was returned return *r; } - // still running + // the process is still running // keep processing events so the app doesn't appear dead QCoreApplication::sendPostedEvents(); QCoreApplication::processEvents(); + // check the lock widget switch (lock.result()) { case LockWidget::StillLocked: @@ -239,8 +252,10 @@ std::optional timedWait( } if (wait != Infinite) { + // check if enough time has elapsed const auto now = high_resolution_clock::now(); if (duration_cast(now - start) >= wait) { + // if so, return an empty result return {}; } } @@ -258,6 +273,10 @@ ProcessRunner::Results waitForProcesses( } DWORD currentPID = 0; + + // if the interesting process that was found is weak (such as ModOrganizer.exe + // when starting a program from within the Data directory), start with a short + // wait and check for more interesting children milliseconds wait(50); for (;;) { @@ -267,14 +286,17 @@ ProcessRunner::Results waitForProcesses( return ProcessRunner::Completed; } + // update the lock widget lock.setInfo(p.pid(), p.name()); + // open the process auto interestingHandle = p.openHandleForWait(); if (!interestingHandle) { return ProcessRunner::Error; } if (p.pid() != currentPID) { + // log any change in the process being waited for currentPID = p.pid(); log::debug( @@ -283,19 +305,19 @@ ProcessRunner::Results waitForProcesses( } if (interest == Interest::Strong) { + // don't bother with short wait, this is a good process to wait for wait = Infinite; } const auto r = timedWait(interestingHandle.get(), p.pid(), lock, wait); if (r) { + // the process has completed or returned an error return *r; } + // exponentially increase the wait time between checks for interesting + // processes wait = std::min(wait * 2, milliseconds(2000)); - - log::debug( - "looking for a more interesting process (next check in {}ms)", - wait.count()); } } @@ -324,6 +346,7 @@ ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) : m_core(core), m_ui(ui), m_lockReason(LockWidget::NoReason), m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) { + // all processes started in ProcessRunner are hooked m_sp.hooked = true; } @@ -383,6 +406,9 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ parent = m_ui->qtWidget(); } + // if the file is a .exe, start it directory; if it's anything else, ask the + // shell to start it + const auto fec = spawn::getFileExecutionContext(parent, targetInfo); switch (fec.type) @@ -466,28 +492,25 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( throw MyException(QObject::tr("No profile set")); } + setBinary(executable); + setArguments(args.join(" ")); + setCurrentDirectory(cwd); setProfileName(profileOverride); if (executable.contains('\\') || executable.contains('/')) { // file path - auto binary = QFileInfo(executable); - - if (binary.isRelative()) { + if (m_sp.binary.isRelative()) { // relative path, should be relative to game directory - binary = m_core.managedGame()->gameDirectory().absoluteFilePath(executable); + setBinary(m_core.managedGame()->gameDirectory().absoluteFilePath(executable)); } - setBinary(binary); - if (cwd == "") { - setCurrentDirectory(binary.absolutePath()); - } else { - setCurrentDirectory(cwd); + setCurrentDirectory(m_sp.binary.absolutePath()); } try { - const Executable& exe = m_core.executablesList()->getByBinary(binary); + const Executable& exe = m_core.executablesList()->getByBinary(m_sp.binary); setSteamID(exe.steamAppID()); setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); @@ -512,20 +535,15 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( if (args.isEmpty()) { setArguments(exe.arguments()); - } else { - setArguments(args.join(" ")); } setBinary(exe.binaryInfo()); if (cwd == "") { setCurrentDirectory(exe.workingDirectory()); - } else { - setCurrentDirectory(cwd); } } catch (const std::runtime_error &) { log::warn("\"{}\" not set up as executable", executable); - setBinary(QFileInfo(executable)); } } @@ -540,68 +558,91 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( ProcessRunner::Results ProcessRunner::run() { + std::optional r; + if (!m_shellOpen.isEmpty()) { - auto r = shell::Open(m_shellOpen); - if (!r.success()) { - return Error; - } + r = runShell(); + } else { + r = runBinary(); + } - m_handle.reset(r.stealProcessHandle()); + if (r) { + // early result: something went wrong and the process cannot be waited for + return *r; + } - // not all files will return a valid handle even if opening them was - // successful, such as inproc handlers (like the photo viewer); in this - // case it's impossible to determine the status, so just say it's still - // running - if (m_handle.get() == INVALID_HANDLE_VALUE) { - return Running; - } - } else { - if (m_profileName.isEmpty()) { - const auto* profile = m_core.currentProfile(); - if (!profile) { - throw MyException(QObject::tr("No profile set")); - } + return postRun(); +} - m_profileName = profile->name(); - } +std::optional ProcessRunner::runShell() +{ + log::debug("executing from shell: '{}'", m_shellOpen); - if (!m_core.beforeRun(m_sp.binary, m_profileName, m_customOverwrite, m_forcedLibraries)) { - return Error; - } + auto r = shell::Open(m_shellOpen); + if (!r.success()) { + return Error; + } - QWidget* parent = nullptr; - if (m_ui) { - parent = m_ui->qtWidget(); - } + m_handle.reset(r.stealProcessHandle()); - if (!checkBinary(parent, m_sp)) { - return Error; - } + // not all files will return a valid handle even if opening them was + // successful, such as inproc handlers (like the photo viewer); in this + // case it's impossible to determine the status, so just say it's still + // running + if (m_handle.get() == INVALID_HANDLE_VALUE) { + log::debug("shell didn't report an error, but no handle is available"); + return Running; + } - const auto* game = m_core.managedGame(); - auto& settings = m_core.settings(); + return {}; +} - if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) { - return Error; +std::optional ProcessRunner::runBinary() +{ + if (m_profileName.isEmpty()) { + // get the current profile name if it wasn't overridden + const auto* profile = m_core.currentProfile(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); } - if (!checkEnvironment(parent, m_sp)) { - return Error; - } + m_profileName = profile->name(); + } - if (!checkBlacklist(parent, m_sp, settings)) { - return Error; - } + // saves profile, sets up usvfs, notifies plugins, etc.; can return false if + // a plugin doesn't want the program to run (such as when checkFNIS fails to + // run FNIS and the user clicks cancel) + if (!m_core.beforeRun(m_sp.binary, m_profileName, m_customOverwrite, m_forcedLibraries)) { + return Error; + } - adjustForVirtualized(game, m_sp, settings); + // parent widget used for any dialog popped up while checking for things + QWidget* parent = (m_ui ? m_ui->qtWidget() : nullptr); - m_handle.reset(startBinary(parent, m_sp)); - if (m_handle.get() == INVALID_HANDLE_VALUE) { - return Error; - } + const auto* game = m_core.managedGame(); + auto& settings = m_core.settings(); + + // start steam if needed + if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) { + return Error; } - return postRun(); + // warn if the executable is on the blacklist + if (!checkBlacklist(parent, m_sp, settings)) { + return Error; + } + + // if the executable is inside the mods folder another instance of + // ModOrganizer.exe is spawned instead to launch it + adjustForVirtualized(game, m_sp, settings); + + // run the binary + m_handle.reset(startBinary(parent, m_sp)); + if (m_handle.get() == INVALID_HANDLE_VALUE) { + return Error; + } + + return {}; } ProcessRunner::Results ProcessRunner::postRun() @@ -618,7 +659,7 @@ ProcessRunner::Results ProcessRunner::postRun() } if (mustWait) { - if (!Settings::instance().interface().lockGUI()) { + if (!m_core.settings().interface().lockGUI()) { // at least tell the user what's going on log::debug( "locking is disabled, but the output of the application is required; " @@ -632,7 +673,7 @@ ProcessRunner::Results ProcessRunner::postRun() return Running; } - if (!Settings::instance().interface().lockGUI()) { + if (!m_core.settings().interface().lockGUI()) { // disabling locking is like clicking on unlock immediately log::debug("not waiting for process because locking is disabled"); return ForceUnlocked; @@ -646,6 +687,18 @@ ProcessRunner::Results ProcessRunner::postRun() }); if (r == Completed && (m_waitFlags & Refresh)) { + // afterRun() is only called with the Refresh flag; it refreshes the + // directory structure and notifies plugins + // + // refreshing is not always required and can actually cause problems: + // + // 1) running shortcuts doesn't need refreshing because MO closes right + // after + // + // 2) the mod info dialog is not set up to deal with refreshes, so that + // it will crash because the old DirectoryEntry's are still being used + // in the list + // m_core.afterRun(m_sp.binary, m_exitCode); } @@ -670,7 +723,9 @@ HANDLE ProcessRunner::getProcessHandle() const env::HandlePtr ProcessRunner::stealProcessHandle() { - return std::move(m_handle); + auto h = m_handle.release(); + m_handle.reset(INVALID_HANDLE_VALUE); + return env::HandlePtr(h); } ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( @@ -678,7 +733,7 @@ ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( { m_lockReason = reason; - if (!Settings::instance().interface().lockGUI()) { + if (!m_core.settings().interface().lockGUI()) { // disabling locking is like clicking on unlock immediately return ForceUnlocked; } diff --git a/src/processrunner.h b/src/processrunner.h index 0fb3f59a..fd451e38 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -19,22 +19,34 @@ class ProcessRunner public: enum Results { + // the process is still running Running = 1, + + // the process has run to completion Completed, + + // the process couldn't be started or waited for Error, + + // the user has clicked the cancel button in the lock widget Cancelled, + + // the user has clicked the unlock button in the lock widget ForceUnlocked }; enum WaitFlag { NoFlags = 0x00, + + // the ui will be refreshed once the process has completed Refresh = 0x01, + + // the process will be waited for even if locking is disabled ForceWait = 0x02 }; using WaitFlags = QFlags; - using ForcedLibraries = QList; ProcessRunner(OrganizerCore& core, IUserInterface* ui); @@ -55,10 +67,29 @@ public: ProcessRunner& setWaitForCompletion( WaitFlags flags=NoFlags, LockWidget::Reasons reason=LockWidget::LockUI); + // if the target is an executable file, runs that; for anything else, calls + // ShellExecute() on it + // ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo); + ProcessRunner& setFromExecutable(const Executable& exe); ProcessRunner& setFromShortcut(const MOShortcut& shortcut); + // this is a messy one that's used for running an arbitrary file from the + // command line, or by plugins (see OrganizerProxy::startApplication()) + // + // 1) if `executable` contains a path separator, it's treated as a binary on + // disk and will be launched with given settings; it's also looked up in + // the list of configured executables, which sets the steam ID and forced + // libraries + // + // 2) if `executable` has no path separators, it's treated purely as an + // executable, but its arguments, current directory and custom overwrite + // can also be overridden + // + // if the executable is not found in the list, the binary is run solely + // based on the parameters given + // ProcessRunner& setFromFileOrExecutable( const QString &executable, const QStringList &args, @@ -67,13 +98,42 @@ public: const QString &forcedCustomOverwrite={}, bool ignoreCustomOverwrite=false); + // spawns the process and waits for it if required + // Results run(); + + // takes ownership of the given handle and waits for it if required + // Results attachToProcess(HANDLE h); + // exit code of the process, will return -1 if the process wasn't waited for + // DWORD exitCode() const; + + // this may be INVALID_HANDLE_VALUE if: + // + // 1) no process was started, or + // 2) the process was started successfully, but the system didn't return a + // handle for it; this can happen for inproc handlers, for example, such + // the photo viewer + // + // note that the handle is still owned by this ProcessRunner and will be + // closed when destroyed; see stealProcessHandle() + // HANDLE getProcessHandle() const; + + // releases ownership of the process handle; if this is called after the + // process is completed, exitCode() will still return the correct value + // env::HandlePtr stealProcessHandle(); + // waits for all usvfs processes spawned by this instance of MO; returns + // immediately with ForceUnlocked if locking is disabled + // + // strictly speaking, this shouldn't be here, as it has nothing to do with + // running a process, but it uses the same internal stuff as when running a + // process + // Results waitForAllUSVFSProcessesWithLock(LockWidget::Reasons reason); private: @@ -89,7 +149,21 @@ private: env::HandlePtr m_handle; DWORD m_exitCode; + + // runs the command in m_shellOpen; returns empty if it can be waited for + // + std::optional runShell(); + + // runs the binary; returns empty if it can be waited for + // + std::optional runBinary(); + + // waits for process completion if required + // Results postRun(); + + // creates the lock widget and calls f() + // void withLock(std::function f); }; diff --git a/src/spawn.cpp b/src/spawn.cpp index 62745542..f95846c8 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -490,7 +490,7 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) const QString moPath = QCoreApplication::applicationDirPath(); const auto oldPath = env::addPath(QDir::toNativeSeparators(moPath)); - PROCESS_INFORMATION pi; + PROCESS_INFORMATION pi = {}; BOOL success = FALSE; logSpawning(sp, commandLine); @@ -501,13 +501,11 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) if (sp.hooked) { success = ::CreateProcessHooked( nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, - inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr, - wcwd.c_str(), &si, &pi); + inheritHandles, 0, nullptr, wcwd.c_str(), &si, &pi); } else { success = ::CreateProcess( nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, - inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr, - wcwd.c_str(), &si, &pi); + inheritHandles, 0, nullptr, wcwd.c_str(), &si, &pi); } const auto e = GetLastError(); @@ -557,16 +555,6 @@ void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp) restartAsAdmin(parent); } -bool checkBinary(QWidget* parent, const SpawnParameters& sp) -{ - if (!sp.binary.exists()) { - dialogs::spawnFailed(parent, sp, ERROR_FILE_NOT_FOUND); - return false; - } - - return true; -} - struct SteamStatus { bool running=false; @@ -754,31 +742,6 @@ bool checkSteam( return true; } -bool checkEnvironment(QWidget* parent, const SpawnParameters& sp) -{ - // check if the Windows Event Logging service is running; for some reason, - // this seems to be critical to the successful running of usvfs. - const auto serviceName = "EventLog"; - - const auto s = env::getService(serviceName); - - if (!s.isValid()) { - log::error( - "cannot determine the status of the {} service, continuing", - serviceName); - - return true; - } - - if (s.status() == env::Service::Status::Running) { - log::debug("{}", s.toString()); - return true; - } - - log::error("{}", s.toString()); - return dialogs::eventLogNotRunning(parent, s, sp); -} - bool checkBlacklist( QWidget* parent, const SpawnParameters& sp, Settings& settings) { diff --git a/src/spawn.h b/src/spawn.h index 9fb346b0..a615b5ff 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -53,14 +53,10 @@ struct SpawnParameters }; -bool checkBinary(QWidget* parent, const SpawnParameters& sp); - bool checkSteam( QWidget* parent, const SpawnParameters& sp, const QDir& gameDirectory, const QString &steamAppID, const Settings& settings); -bool checkEnvironment(QWidget* parent, const SpawnParameters& sp); - bool checkBlacklist( QWidget* parent, const SpawnParameters& sp, Settings& settings); -- cgit v1.3.1 From 642ddacab802df75d21a1ccf7508268d3efa141a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 6 Nov 2019 05:23:22 -0500 Subject: moved LockWidget back to OrganizerCore to support having multiple locks active moved the ui out of LockWidget into LockInterface added LockWidget::Session to manage multi locks --- src/lockwidget.cpp | 490 ++++++++++++++++++++++++++++++++++++-------------- src/lockwidget.h | 65 +++---- src/organizercore.cpp | 1 + src/organizercore.h | 3 + src/processrunner.cpp | 33 ++-- src/processrunner.h | 2 +- 6 files changed, 414 insertions(+), 180 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp index 35d51dab..68eb9c2d 100644 --- a/src/lockwidget.cpp +++ b/src/lockwidget.cpp @@ -4,203 +4,431 @@ #include #include -QWidget* createTransparentWidget(QWidget* parent=nullptr) +class LockInterface { - auto* w = new QWidget(parent); +public: + LockInterface() : + m_hasMainUI(false), m_message(nullptr), m_info(nullptr), m_buttons(nullptr) + { + } - w->setWindowOpacity(0); - w->setAttribute(Qt::WA_NoSystemBackground); - w->setAttribute(Qt::WA_TranslucentBackground); + ~LockInterface() + { + } - return w; -} + void set(QWidget* target) + { + QFrame* center = nullptr; + if (target) { + center = createOverlay(target); + } else { + center = createDialog(); + } -LockWidget::LockWidget(QWidget* parent, Reasons reason) : - m_parent(parent), m_overlay(nullptr), m_info(nullptr), m_result(NoResult), - m_filter(nullptr) -{ - if (reason != NoReason) { - lock(reason); + createMessageLabel(); + createInfoLabel(); + createButtonsPanel(); + + center->layout()->addWidget(m_message); + center->layout()->addWidget(m_info); + center->layout()->addWidget(m_buttons); + + m_topLevel->setFocusPolicy(Qt::TabFocus); + m_topLevel->setFocus(); + m_topLevel->show(); + m_topLevel->setEnabled(true); + + m_topLevel->raise(); + m_topLevel->activateWindow(); } -} -LockWidget::~LockWidget() -{ - unlock(); -} + void update(LockWidget::Reasons reason) + { + updateMessage(reason); + updateButtons(reason); + } -void LockWidget::lock(Reasons reason) -{ - m_result = StillLocked; - createUi(reason); -} + void setInfo(const QString& s) + { + m_info->setText(s); + } -void LockWidget::unlock() -{ - m_overlay.reset(); + QWidget* topLevel() + { + return m_topLevel.get(); + } + +private: + class Filter : public QObject + { + public: + std::function resized; + std::function closed; + + protected: + bool eventFilter(QObject* o, QEvent* e) override + { + if (e->type() == QEvent::Resize) { + if (resized) { + resized(); + } + } else if (e->type() == QEvent::Close) { + if (closed) { + closed(); + } + } + + return QObject::eventFilter(o, e); + } + }; + + + bool m_hasMainUI; + std::unique_ptr m_topLevel; + QLabel* m_message; + QLabel* m_info; + QWidget* m_buttons; + std::unique_ptr m_filter; + + QWidget* createTransparentWidget(QWidget* parent=nullptr) + { + auto* w = new QWidget(parent); + + w->setWindowOpacity(0); + w->setAttribute(Qt::WA_NoSystemBackground); + w->setAttribute(Qt::WA_TranslucentBackground); - if (m_filter && m_parent) { - m_parent->removeEventFilter(m_filter.get()); + return w; } - enableAll(); -} + QFrame* createOverlay(QWidget* mainUI) + { + m_hasMainUI = true; -void LockWidget::setInfo(DWORD pid, const QString& name) -{ - m_info->setText(QString("%1 (%2)").arg(name).arg(pid)); -} + m_topLevel.reset(createTransparentWidget(mainUI)); + m_topLevel->setWindowFlags(m_topLevel->windowFlags() & Qt::FramelessWindowHint); + m_topLevel->setGeometry(mainUI->rect()); -LockWidget::Results LockWidget::result() const -{ - return m_result; -} + m_filter.reset(new Filter); + m_filter->resized = [=]{ m_topLevel->setGeometry(mainUI->rect()); }; + m_filter->closed = [=]{ LockWidget::instance().onForceUnlock(); }; -void LockWidget::createUi(Reasons reason) -{ - QWidget* overlayTarget = m_parent; - if (auto* w = qApp->activeWindow()) { - overlayTarget = w; + mainUI->installEventFilter(m_filter.get()); + + return createFrame(); } - if (overlayTarget) { - m_overlay.reset(createTransparentWidget(overlayTarget)); - m_overlay->setWindowFlags(m_overlay->windowFlags() & Qt::FramelessWindowHint); - m_overlay->setGeometry(overlayTarget->rect()); - } else { - m_overlay.reset(new QDialog); + QFrame* createDialog() + { + m_hasMainUI = false; + m_topLevel.reset(new QDialog); + + return createFrame(); } - auto* center = new QFrame; + QFrame* createFrame() + { + auto* frame = new QFrame; + auto* ly = new QVBoxLayout(frame); + + if (m_hasMainUI) { + frame->setFrameStyle(QFrame::StyledPanel); + frame->setLineWidth(1); + frame->setAutoFillBackground(true); + + auto* shadow = new QGraphicsDropShadowEffect; + shadow->setBlurRadius(50); + shadow->setOffset(0); + shadow->setColor(QColor(0, 0, 0, 100)); + frame->setGraphicsEffect(shadow); + } else { + ly->setContentsMargins(0, 0, 0, 0); + } - if (overlayTarget) { - center->setFrameStyle(QFrame::StyledPanel); - center->setLineWidth(1); - center->setAutoFillBackground(true); + auto* grid = new QGridLayout(m_topLevel.get()); + grid->addWidget(createTransparentWidget(), 0, 1); + grid->addWidget(createTransparentWidget(), 2, 1); + grid->addWidget(createTransparentWidget(), 1, 0); + grid->addWidget(createTransparentWidget(), 1, 2); + grid->addWidget(frame, 1, 1); - auto* shadow = new QGraphicsDropShadowEffect; - shadow->setBlurRadius(50); - shadow->setOffset(0); - shadow->setColor(QColor(0, 0, 0, 100)); - center->setGraphicsEffect(shadow); - } + if (!m_hasMainUI) { + grid->setContentsMargins(0, 0, 0, 0); + } - m_info = new QLabel(" "); - m_info->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); + grid->setRowStretch(0, 1); + grid->setRowStretch(2, 1); + grid->setColumnStretch(0, 1); + grid->setColumnStretch(2, 1); - auto* ly = new QVBoxLayout(center); + return frame; + } - if (!overlayTarget) { - ly->setContentsMargins(0, 0, 0, 0); + void createMessageLabel() + { + m_message = new QLabel; + } + + void createInfoLabel() + { + m_info = new QLabel(" "); + m_info->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); } - auto* message = new QLabel; - ly->addWidget(message); - ly->addWidget(m_info); + void createButtonsPanel() + { + m_buttons = new QWidget; + m_buttons->setLayout(new QHBoxLayout); + } - auto* buttons = new QWidget; - auto* buttonsLayout = new QHBoxLayout(buttons); - ly->addWidget(buttons); - switch (reason) + void updateMessage(LockWidget::Reasons reason) { - case LockUI: + switch (reason) { - QString s; + case LockWidget::LockUI: + { + QString s; + + if (m_hasMainUI) { + s = QObject::tr( + "Mod Organizer is locked while the application is running."); + } else { + s = QObject::tr("Mod Organizer is currently running an application."); + } - if (!m_parent) { - s = QObject::tr("Mod Organizer is currently running an application."); - } else { - s = QObject::tr( - "Mod Organizer is locked while the application is running."); + m_message->setText(s); + + break; } - message->setText(s); + case LockWidget::OutputRequired: + { + m_message->setText(QObject::tr( + "The application must run to completion because its output is " + "required.")); - auto* unlockButton = new QPushButton(QObject::tr("Unlock")); - QObject::connect(unlockButton, &QPushButton::clicked, [&]{ onForceUnlock(); }); - buttonsLayout->addWidget(unlockButton); + break; + } - break; + case LockWidget::PreventExit: + { + m_message->setText(QObject::tr( + "Mod Organizer is waiting on application to close before exiting.")); + + break; + } } + } - case OutputRequired: + void updateButtons(LockWidget::Reasons reason) + { + MOBase::deleteChildWidgets(m_buttons); + auto* ly = m_buttons->layout(); + + switch (reason) { - message->setText(QObject::tr( - "The application must run to completion because its output is " - "required.")); + case LockWidget::LockUI: // fall-through + case LockWidget::OutputRequired: + { + auto* unlock = new QPushButton(QObject::tr("Unlock")); - auto* unlockButton = new QPushButton(QObject::tr("Unlock")); - QObject::connect(unlockButton, &QPushButton::clicked, [&]{ onForceUnlock(); }); - buttonsLayout->addWidget(unlockButton); + QObject::connect(unlock, &QPushButton::clicked, [&]{ + LockWidget::instance().onForceUnlock(); + }); - break; + ly->addWidget(unlock); + + break; + } + + case LockWidget::PreventExit: + { + auto* exit = new QPushButton(QObject::tr("Exit Now")); + QObject::connect(exit, &QPushButton::clicked, [&]{ + LockWidget::instance().onForceUnlock(); + }); + + ly->addWidget(exit); + + auto* cancel = new QPushButton(QObject::tr("Cancel")); + QObject::connect(cancel, &QPushButton::clicked, [&]{ + LockWidget::instance().onCancel(); + }); + + ly->addWidget(cancel); + + break; + } } + } +}; - case PreventExit: - { - message->setText(QObject::tr( - "Mod Organizer is waiting on application to close before exiting.")); +LockWidget::Session::~Session() +{ + LockWidget::instance().unlock(this); +} + +void LockWidget::Session::setInfo(DWORD pid, const QString& name) +{ + m_pid = pid; + m_name = name; + + LockWidget::instance().updateLabel(); +} + +DWORD LockWidget::Session::pid() const +{ + return m_pid; +} + +const QString& LockWidget::Session::name() const +{ + return m_name; +} - auto* exit = new QPushButton(QObject::tr("Exit Now")); - QObject::connect(exit, &QPushButton::clicked, [&]{ onForceUnlock(); }); - buttonsLayout->addWidget(exit); +LockWidget::Results LockWidget::Session::result() const +{ + return LockWidget::instance().result(); +} + + +static LockWidget* g_instance = nullptr; + + +LockWidget::LockWidget() + : m_parent(nullptr), m_result(NoResult) +{ + Q_ASSERT(!g_instance); + g_instance = this; +} + +LockWidget::~LockWidget() +{ + const auto v = m_sessions; + + for (auto& wp : v) { + if (auto s=wp.lock()) { + unlock(s.get()); + } + } +} - auto* cancel = new QPushButton(QObject::tr("Cancel")); - QObject::connect(cancel, &QPushButton::clicked, [&]{ onCancel(); }); - buttonsLayout->addWidget(cancel); +LockWidget& LockWidget::instance() +{ + Q_ASSERT(g_instance); + return *g_instance; +} + +void LockWidget::setUserInterface(QWidget* parent) +{ + m_parent = parent; +} +std::shared_ptr LockWidget::lock(Reasons reason) +{ + m_result = StillLocked; + createUi(reason); + + auto ls = std::make_shared(); + m_sessions.push_back(ls); + + updateLabel(); + + return ls; +} + +void LockWidget::unlock(Session* s) +{ + auto itor = m_sessions.begin(); + for (;;) { + if (itor == m_sessions.end()) { break; } + + if (auto ss=itor->lock()) { + if (ss.get() == s) { + itor = m_sessions.erase(itor); + continue; + } + } else { + itor = m_sessions.erase(itor); + continue; + } + + ++itor; } - auto* grid = new QGridLayout(m_overlay.get()); - grid->addWidget(createTransparentWidget(), 0, 1); - grid->addWidget(createTransparentWidget(), 2, 1); - grid->addWidget(createTransparentWidget(), 1, 0); - grid->addWidget(createTransparentWidget(), 1, 2); - grid->addWidget(center, 1, 1); + if (m_sessions.empty()) { + m_ui.reset(); + enableAll(); + } else { + updateLabel(); + } +} - if (!overlayTarget) { - grid->setContentsMargins(0, 0, 0, 0); +void LockWidget::unlockCurrent() +{ + if (m_sessions.empty()) { + return; } - grid->setRowStretch(0, 1); - grid->setRowStretch(2, 1); - grid->setColumnStretch(0, 1); - grid->setColumnStretch(2, 1); + auto s = m_sessions.back().lock(); + if (!s) { + m_sessions.pop_back(); + return; + } - disableAll(); + unlock(s.get()); +} - if (overlayTarget) { - m_filter.reset(new Filter); - m_filter->resized = [=]{ m_overlay->setGeometry(overlayTarget->rect()); }; - m_filter->closed = [=]{ onForceUnlock(); }; - overlayTarget->installEventFilter(m_filter.get()); +void LockWidget::updateLabel() +{ + QString label; + + for (auto itor=m_sessions.rbegin(); itor!=m_sessions.rend(); ++itor) { + if (auto ss=itor->lock()) { + label += QString("%1 (%2)").arg(ss->name()).arg(ss->pid()); + break; + } } - m_overlay->setFocusPolicy(Qt::TabFocus); - m_overlay->setFocus(); - m_overlay->show(); - m_overlay->setEnabled(true); + m_ui->setInfo(label); +} - if (!overlayTarget) { - m_overlay->raise(); - m_overlay->activateWindow(); +LockWidget::Results LockWidget::result() const +{ + return m_result; +} + +void LockWidget::createUi(Reasons reason) +{ + QWidget* target = m_parent; + if (auto* w = qApp->activeWindow()) { + target = w; } + + if (!m_ui) { + m_ui.reset(new LockInterface); + } + + m_ui->set(target); + m_ui->update(reason); + + disableAll(); } void LockWidget::onForceUnlock() { m_result = ForceUnlocked; - unlock(); + unlockCurrent(); } void LockWidget::onCancel() { m_result = Cancelled; - unlock(); + unlockCurrent(); } template @@ -231,10 +459,10 @@ void LockWidget::disableAll() if (auto* d=dynamic_cast(w)) { // don't disable stuff if this dialog is the overlay, which happens when // there's no ui - if (d != m_overlay.get()) { + if (d != m_ui->topLevel()) { // no central widget, just disable the children, except for the overlay for (auto* child : findChildrenImmediate(d)) { - if (child != m_overlay.get()) { + if (child != m_ui->topLevel()) { disable(child); } } diff --git a/src/lockwidget.h b/src/lockwidget.h index d3ff2f7d..8062c478 100644 --- a/src/lockwidget.h +++ b/src/lockwidget.h @@ -2,8 +2,12 @@ #include +class LockInterface; + class LockWidget { + friend class LockInterface; + public: // reason to show the widget // @@ -37,51 +41,50 @@ public: Cancelled }; + + class Session + { + public: + ~Session(); + + void setInfo(DWORD pid, const QString& name); + Results result() const; + + DWORD pid() const; + const QString& name() const; + + private: + DWORD m_pid; + QString m_name; + }; + + // if `reason` is not NoReason, lock() is called with it // - LockWidget(QWidget* parent, Reasons reason=NoReason); + LockWidget(); ~LockWidget(); - void lock(Reasons reason); - void unlock(); + static LockWidget& instance(); - void setInfo(DWORD pid, const QString& name); - Results result() const; + void setUserInterface(QWidget* parent); -private: - class Filter : public QObject - { - public: - std::function resized; - std::function closed; - - protected: - bool eventFilter(QObject* o, QEvent* e) override - { - if (e->type() == QEvent::Resize) { - if (resized) { - resized(); - } - } else if (e->type() == QEvent::Close) { - if (closed) { - closed(); - } - } - - return QObject::eventFilter(o, e); - } - }; + std::shared_ptr lock(Reasons reason); + Results result() const; +private: QWidget* m_parent; - std::unique_ptr m_overlay; - QLabel* m_info; + std::unique_ptr m_ui; + std::vector> m_sessions; Results m_result; - std::unique_ptr m_filter; std::vector> m_disabled; void createUi(Reasons reason); + void unlockCurrent(); + void unlock(Session* s); + void updateLabel(); + void onForceUnlock(); void onCancel(); diff --git a/src/organizercore.cpp b/src/organizercore.cpp index dda60f76..3d4d8e4b 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -250,6 +250,7 @@ void OrganizerCore::setUserInterface(IUserInterface* ui) m_InstallationManager.setParentWidget(w); m_Updater.setUserInterface(w); + m_LockWidget.setUserInterface(w); checkForUpdates(); } diff --git a/src/organizercore.h b/src/organizercore.h index 0ba52284..7e0e4b7f 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -14,6 +14,7 @@ #include "usvfsconnector.h" #include "moshortcut.h" #include "processrunner.h" +#include "lockwidget.h" #include #include #include @@ -343,6 +344,8 @@ private: MOBase::DelayedFileWriter m_PluginListsWriter; UsvfsConnector m_USVFS; + LockWidget m_LockWidget; + static CrashDumpsType m_globalCrashDumpsType; }; diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 220ffe38..d97c00ef 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -195,7 +195,8 @@ const std::chrono::milliseconds Infinite(-1); // waits for completion, times out after `wait` if not Infinite // std::optional timedWait( - HANDLE handle, DWORD pid, LockWidget& lock, std::chrono::milliseconds wait) + HANDLE handle, DWORD pid, LockWidget::Session& ls, + std::chrono::milliseconds wait) { using namespace std::chrono; @@ -220,7 +221,7 @@ std::optional timedWait( QCoreApplication::processEvents(); // check the lock widget - switch (lock.result()) + switch (ls.result()) { case LockWidget::StillLocked: { @@ -245,7 +246,7 @@ std::optional timedWait( // shouldn't happen log::debug( "unexpected result {} while waiting for {}", - static_cast(lock.result()), pid); + static_cast(ls.result()), pid); return ProcessRunner::Error; } @@ -263,7 +264,7 @@ std::optional timedWait( } ProcessRunner::Results waitForProcesses( - const std::vector& initialProcesses, LockWidget& lock) + const std::vector& initialProcesses, LockWidget::Session& ls) { using namespace std::chrono; @@ -287,7 +288,7 @@ ProcessRunner::Results waitForProcesses( } // update the lock widget - lock.setInfo(p.pid(), p.name()); + ls.setInfo(p.pid(), p.name()); // open the process auto interestingHandle = p.openHandleForWait(); @@ -309,7 +310,7 @@ ProcessRunner::Results waitForProcesses( wait = Infinite; } - const auto r = timedWait(interestingHandle.get(), p.pid(), lock, wait); + const auto r = timedWait(interestingHandle.get(), p.pid(), ls, wait); if (r) { // the process has completed or returned an error return *r; @@ -322,11 +323,11 @@ ProcessRunner::Results waitForProcesses( } ProcessRunner::Results waitForProcess( - HANDLE initialProcess, LPDWORD exitCode, LockWidget& lock) + HANDLE initialProcess, LPDWORD exitCode, LockWidget::Session& ls) { std::vector processes = {initialProcess}; - const auto r = waitForProcesses(processes, lock); + const auto r = waitForProcesses(processes, ls); // as long as it's not running anymore, try to get the exit code if (exitCode && r != ProcessRunner::Running) { @@ -682,8 +683,8 @@ ProcessRunner::Results ProcessRunner::postRun() auto r = Error; - withLock([&](auto& lock) { - r = waitForProcess(m_handle.get(), &m_exitCode, lock); + withLock([&](auto& ls) { + r = waitForProcess(m_handle.get(), &m_exitCode, ls); }); if (r == Completed && (m_waitFlags & Refresh)) { @@ -740,14 +741,14 @@ ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( auto r = Error; - withLock([&](auto& lock) { + withLock([&](auto& ls) { for (;;) { const auto processes = getRunningUSVFSProcesses(); if (processes.empty()) { break; } - r = waitForProcesses(processes, lock); + r = waitForProcesses(processes, ls); if (r != Completed) { // error, cancelled, or unlocked @@ -763,10 +764,8 @@ ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( return r; } -void ProcessRunner::withLock(std::function f) +void ProcessRunner::withLock(std::function f) { - auto lk = std::make_unique( - m_ui ? m_ui->qtWidget() : nullptr, m_lockReason); - - f(*lk); + auto ls = LockWidget::instance().lock(m_lockReason); + f(*ls); } diff --git a/src/processrunner.h b/src/processrunner.h index fd451e38..276c46b1 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -164,7 +164,7 @@ private: // creates the lock widget and calls f() // - void withLock(std::function f); + void withLock(std::function f); }; Q_DECLARE_OPERATORS_FOR_FLAGS(ProcessRunner::WaitFlags); -- cgit v1.3.1 From 8f40ec0bdcb99cc1a0a2bde3074842391844f5d6 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 6 Nov 2019 06:50:05 -0500 Subject: threaded wait for process --- src/lockwidget.cpp | 22 ++++++++++++++++++---- src/lockwidget.h | 5 ++++- src/processrunner.cpp | 42 +++++++++++++++++++++++++++++++----------- 3 files changed, 53 insertions(+), 16 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp index aae0316b..8b654674 100644 --- a/src/lockwidget.cpp +++ b/src/lockwidget.cpp @@ -305,24 +305,38 @@ private: LockWidget::Session::~Session() { - LockWidget::instance().unlock(this); + unlock(); +} + +void LockWidget::Session::unlock() +{ + QMetaObject::invokeMethod(qApp, [this]{ + LockWidget::instance().unlock(this); + }); } void LockWidget::Session::setInfo(DWORD pid, const QString& name) { - m_pid = pid; - m_name = name; + { + std::scoped_lock lock(m_mutex); + m_pid = pid; + m_name = name; + } - LockWidget::instance().updateLabel(); + QMetaObject::invokeMethod(qApp, [this]{ + LockWidget::instance().updateLabel(); + }); } DWORD LockWidget::Session::pid() const { + std::scoped_lock lock(m_mutex); return m_pid; } const QString& LockWidget::Session::name() const { + std::scoped_lock lock(m_mutex); return m_name; } diff --git a/src/lockwidget.h b/src/lockwidget.h index 8062c478..640d0076 100644 --- a/src/lockwidget.h +++ b/src/lockwidget.h @@ -1,6 +1,7 @@ #pragma once #include +#include class LockInterface; @@ -47,6 +48,7 @@ public: public: ~Session(); + void unlock(); void setInfo(DWORD pid, const QString& name); Results result() const; @@ -54,6 +56,7 @@ public: const QString& name() const; private: + mutable std::mutex m_mutex; DWORD m_pid; QString m_name; }; @@ -76,7 +79,7 @@ private: QWidget* m_parent; std::unique_ptr m_ui; std::vector> m_sessions; - Results m_result; + std::atomic m_result; std::vector> m_disabled; void createUi(Reasons reason); diff --git a/src/processrunner.cpp b/src/processrunner.cpp index d97c00ef..68acca92 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -55,11 +55,7 @@ std::optional singleWait(HANDLE handle, DWORD pid) return ProcessRunner::Error; } - const DWORD WAIT_EVENT = WAIT_OBJECT_0 + 1; - - // Wait for a an event on the handle, a key press, mouse click or timeout - const auto res = MsgWaitForMultipleObjects( - 1, &handle, FALSE, 50, QS_KEY | QS_MOUSEBUTTON); + const auto res = WaitForSingleObject(handle, 50); switch (res) { @@ -70,7 +66,6 @@ std::optional singleWait(HANDLE handle, DWORD pid) } case WAIT_TIMEOUT: - case WAIT_EVENT: { // still running return {}; @@ -216,10 +211,6 @@ std::optional timedWait( // the process is still running - // keep processing events so the app doesn't appear dead - QCoreApplication::sendPostedEvents(); - QCoreApplication::processEvents(); - // check the lock widget switch (ls.result()) { @@ -263,7 +254,7 @@ std::optional timedWait( } } -ProcessRunner::Results waitForProcesses( +ProcessRunner::Results waitForProcessesThreadImpl( const std::vector& initialProcesses, LockWidget::Session& ls) { using namespace std::chrono; @@ -322,6 +313,35 @@ ProcessRunner::Results waitForProcesses( } } +void waitForProcessesThread( + ProcessRunner::Results& result, + const std::vector& initialProcesses, LockWidget::Session& ls) +{ + result = waitForProcessesThreadImpl(initialProcesses, ls); + ls.unlock(); +} + +ProcessRunner::Results waitForProcesses( + const std::vector& initialProcesses, LockWidget::Session& ls) +{ + auto results = ProcessRunner::Running; + + auto* t = QThread::create( + waitForProcessesThread, std::ref(results), initialProcesses, std::ref(ls)); + + QEventLoop events; + QObject::connect(t, &QThread::finished, [&]{ + events.quit(); + }); + + t->start(); + events.exec(); + + delete t; + + return results; +} + ProcessRunner::Results waitForProcess( HANDLE initialProcess, LPDWORD exitCode, LockWidget::Session& ls) { -- cgit v1.3.1 From 4f84565085e19b6f6939783c64ebf95599f879be Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 6 Nov 2019 07:20:56 -0500 Subject: renamed LockWidget to UILocker lock interface up to two processes --- src/lockwidget.cpp | 113 +++++++++++++++++++++++++++---------------------- src/lockwidget.h | 16 +++---- src/organizercore.cpp | 4 +- src/organizercore.h | 4 +- src/organizerproxy.cpp | 2 +- src/processrunner.cpp | 34 +++++++-------- src/processrunner.h | 8 ++-- 7 files changed, 95 insertions(+), 86 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/lockwidget.cpp b/src/lockwidget.cpp index 8b654674..150d2847 100644 --- a/src/lockwidget.cpp +++ b/src/lockwidget.cpp @@ -4,12 +4,12 @@ #include #include -class LockInterface +class UILockerInterface { public: - LockInterface(QWidget* mainUI) : + UILockerInterface(QWidget* mainUI) : m_mainUI(mainUI), m_target(nullptr), m_message(nullptr), m_info(nullptr), - m_buttons(nullptr), m_reason(LockWidget::NoReason) + m_buttons(nullptr), m_reason(UILocker::NoReason) { m_timer.reset(new QTimer); QObject::connect(m_timer.get(), &QTimer::timeout, [&]{ checkTarget(); }); @@ -18,7 +18,7 @@ public: set(); } - ~LockInterface() + ~UILockerInterface() { } @@ -71,17 +71,28 @@ public: return true; } - void update(LockWidget::Reasons reason) + void update(UILocker::Reasons reason) { m_reason = reason; updateMessage(reason); updateButtons(reason); - setInfo(m_infoText); + setInfo(m_labels); } - void setInfo(const QString& s) + void setInfo(const QStringList& labels) { - m_infoText = s; + const int MaxLabels = 2; + + m_labels = labels; + + QString s; + + if (labels.size() > MaxLabels) { + s = labels.mid(0, MaxLabels).join(", ") + "..."; + } else { + s = labels.join(", "); + } + m_info->setText(s); } @@ -121,10 +132,10 @@ private: std::unique_ptr m_topLevel; QLabel* m_message; QLabel* m_info; - QString m_infoText; + QStringList m_labels; QWidget* m_buttons; std::unique_ptr m_filter; - LockWidget::Reasons m_reason; + UILocker::Reasons m_reason; bool hasMainUI() const @@ -206,6 +217,7 @@ private: void createMessageLabel() { m_message = new QLabel; + m_message->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); } void createInfoLabel() @@ -221,11 +233,11 @@ private: } - void updateMessage(LockWidget::Reasons reason) + void updateMessage(UILocker::Reasons reason) { switch (reason) { - case LockWidget::LockUI: + case UILocker::LockUI: { QString s; @@ -241,7 +253,7 @@ private: break; } - case LockWidget::OutputRequired: + case UILocker::OutputRequired: { m_message->setText(QObject::tr( "The application must run to completion because its output is " @@ -250,7 +262,7 @@ private: break; } - case LockWidget::PreventExit: + case UILocker::PreventExit: { m_message->setText(QObject::tr( "Mod Organizer is waiting on application to close before exiting.")); @@ -260,20 +272,20 @@ private: } } - void updateButtons(LockWidget::Reasons reason) + void updateButtons(UILocker::Reasons reason) { MOBase::deleteChildWidgets(m_buttons); auto* ly = m_buttons->layout(); switch (reason) { - case LockWidget::LockUI: // fall-through - case LockWidget::OutputRequired: + case UILocker::LockUI: // fall-through + case UILocker::OutputRequired: { auto* unlock = new QPushButton(QObject::tr("Unlock")); QObject::connect(unlock, &QPushButton::clicked, [&]{ - LockWidget::instance().onForceUnlock(); + UILocker::instance().onForceUnlock(); }); ly->addWidget(unlock); @@ -281,18 +293,18 @@ private: break; } - case LockWidget::PreventExit: + case UILocker::PreventExit: { auto* exit = new QPushButton(QObject::tr("Exit Now")); QObject::connect(exit, &QPushButton::clicked, [&]{ - LockWidget::instance().onForceUnlock(); + UILocker::instance().onForceUnlock(); }); ly->addWidget(exit); auto* cancel = new QPushButton(QObject::tr("Cancel")); QObject::connect(cancel, &QPushButton::clicked, [&]{ - LockWidget::instance().onCancel(); + UILocker::instance().onCancel(); }); ly->addWidget(cancel); @@ -303,19 +315,19 @@ private: } }; -LockWidget::Session::~Session() +UILocker::Session::~Session() { unlock(); } -void LockWidget::Session::unlock() +void UILocker::Session::unlock() { QMetaObject::invokeMethod(qApp, [this]{ - LockWidget::instance().unlock(this); + UILocker::instance().unlock(this); }); } -void LockWidget::Session::setInfo(DWORD pid, const QString& name) +void UILocker::Session::setInfo(DWORD pid, const QString& name) { { std::scoped_lock lock(m_mutex); @@ -324,39 +336,39 @@ void LockWidget::Session::setInfo(DWORD pid, const QString& name) } QMetaObject::invokeMethod(qApp, [this]{ - LockWidget::instance().updateLabel(); + UILocker::instance().updateLabel(); }); } -DWORD LockWidget::Session::pid() const +DWORD UILocker::Session::pid() const { std::scoped_lock lock(m_mutex); return m_pid; } -const QString& LockWidget::Session::name() const +const QString& UILocker::Session::name() const { std::scoped_lock lock(m_mutex); return m_name; } -LockWidget::Results LockWidget::Session::result() const +UILocker::Results UILocker::Session::result() const { - return LockWidget::instance().result(); + return UILocker::instance().result(); } -static LockWidget* g_instance = nullptr; +static UILocker* g_instance = nullptr; -LockWidget::LockWidget() +UILocker::UILocker() : m_parent(nullptr), m_result(NoResult) { Q_ASSERT(!g_instance); g_instance = this; } -LockWidget::~LockWidget() +UILocker::~UILocker() { const auto v = m_sessions; @@ -367,18 +379,18 @@ LockWidget::~LockWidget() } } -LockWidget& LockWidget::instance() +UILocker& UILocker::instance() { Q_ASSERT(g_instance); return *g_instance; } -void LockWidget::setUserInterface(QWidget* parent) +void UILocker::setUserInterface(QWidget* parent) { m_parent = parent; } -std::shared_ptr LockWidget::lock(Reasons reason) +std::shared_ptr UILocker::lock(Reasons reason) { m_result = StillLocked; createUi(reason); @@ -391,7 +403,7 @@ std::shared_ptr LockWidget::lock(Reasons reason) return ls; } -void LockWidget::unlock(Session* s) +void UILocker::unlock(Session* s) { auto itor = m_sessions.begin(); for (;;) { @@ -420,7 +432,7 @@ void LockWidget::unlock(Session* s) } } -void LockWidget::unlockCurrent() +void UILocker::unlockCurrent() { if (m_sessions.empty()) { return; @@ -435,29 +447,28 @@ void LockWidget::unlockCurrent() unlock(s.get()); } -void LockWidget::updateLabel() +void UILocker::updateLabel() { - QString label; + QStringList labels; for (auto itor=m_sessions.rbegin(); itor!=m_sessions.rend(); ++itor) { if (auto ss=itor->lock()) { - label += QString("%1 (%2)").arg(ss->name()).arg(ss->pid()); - break; + labels.push_back(QString("%1 (%2)").arg(ss->name()).arg(ss->pid())); } } - m_ui->setInfo(label); + m_ui->setInfo(labels); } -LockWidget::Results LockWidget::result() const +UILocker::Results UILocker::result() const { return m_result; } -void LockWidget::createUi(Reasons reason) +void UILocker::createUi(Reasons reason) { if (!m_ui) { - m_ui.reset(new LockInterface(m_parent)); + m_ui.reset(new UILockerInterface(m_parent)); } m_ui->update(reason); @@ -465,13 +476,13 @@ void LockWidget::createUi(Reasons reason) disableAll(); } -void LockWidget::onForceUnlock() +void UILocker::onForceUnlock() { m_result = ForceUnlocked; unlockCurrent(); } -void LockWidget::onCancel() +void UILocker::onCancel() { m_result = Cancelled; unlockCurrent(); @@ -483,7 +494,7 @@ QList findChildrenImmediate(QWidget* parent) return parent->findChildren(QString(), Qt::FindDirectChildrenOnly); } -void LockWidget::disableAll() +void UILocker::disableAll() { const auto topLevels = QApplication::topLevelWidgets(); @@ -517,7 +528,7 @@ void LockWidget::disableAll() } } -void LockWidget::enableAll() +void UILocker::enableAll() { for (auto w : m_disabled) { if (w) { @@ -528,7 +539,7 @@ void LockWidget::enableAll() m_disabled.clear(); } -void LockWidget::disable(QWidget* w) +void UILocker::disable(QWidget* w) { if (w->isEnabled()) { w->setEnabled(false); diff --git a/src/lockwidget.h b/src/lockwidget.h index 640d0076..d8c22999 100644 --- a/src/lockwidget.h +++ b/src/lockwidget.h @@ -3,11 +3,11 @@ #include #include -class LockInterface; +class UILockerInterface; -class LockWidget +class UILocker { - friend class LockInterface; + friend class UILockerInterface; public: // reason to show the widget @@ -62,12 +62,10 @@ public: }; - // if `reason` is not NoReason, lock() is called with it - // - LockWidget(); - ~LockWidget(); + UILocker(); + ~UILocker(); - static LockWidget& instance(); + static UILocker& instance(); void setUserInterface(QWidget* parent); @@ -77,7 +75,7 @@ public: private: QWidget* m_parent; - std::unique_ptr m_ui; + std::unique_ptr m_ui; std::vector> m_sessions; std::atomic m_result; std::vector> m_disabled; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 3d4d8e4b..9ceb149e 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -250,7 +250,7 @@ void OrganizerCore::setUserInterface(IUserInterface* ui) m_InstallationManager.setParentWidget(w); m_Updater.setUserInterface(w); - m_LockWidget.setUserInterface(w); + m_UILocker.setUserInterface(w); checkForUpdates(); } @@ -1797,7 +1797,7 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) } ProcessRunner::Results OrganizerCore::waitForAllUSVFSProcesses( - LockWidget::Reasons reason) + UILocker::Reasons reason) { return processRunner().waitForAllUSVFSProcessesWithLock(reason); } diff --git a/src/organizercore.h b/src/organizercore.h index 7e0e4b7f..47630dc2 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -145,7 +145,7 @@ public: void afterRun(const QFileInfo& binary, DWORD exitCode); ProcessRunner::Results waitForAllUSVFSProcesses( - LockWidget::Reasons reason=LockWidget::PreventExit); + UILocker::Reasons reason=UILocker::PreventExit); void refreshESPList(bool force = false); void refreshBSAList(); @@ -344,7 +344,7 @@ private: MOBase::DelayedFileWriter m_PluginListsWriter; UsvfsConnector m_USVFS; - LockWidget m_LockWidget; + UILocker m_UILocker; static CrashDumpsType m_globalCrashDumpsType; }; diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 420e2d82..613de742 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -144,7 +144,7 @@ bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const auto runner = m_Proxied->processRunner(); const auto r = runner - .setWaitForCompletion(ProcessRunner::ForceWait, LockWidget::OutputRequired) + .setWaitForCompletion(ProcessRunner::ForceWait, UILocker::OutputRequired) .attachToProcess(handle); if (exitCode) { diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 68acca92..cfddbb63 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -190,7 +190,7 @@ const std::chrono::milliseconds Infinite(-1); // waits for completion, times out after `wait` if not Infinite // std::optional timedWait( - HANDLE handle, DWORD pid, LockWidget::Session& ls, + HANDLE handle, DWORD pid, UILocker::Session& ls, std::chrono::milliseconds wait) { using namespace std::chrono; @@ -214,24 +214,24 @@ std::optional timedWait( // check the lock widget switch (ls.result()) { - case LockWidget::StillLocked: + case UILocker::StillLocked: { break; } - case LockWidget::ForceUnlocked: + case UILocker::ForceUnlocked: { log::debug("waiting for {} force unlocked by user", pid); return ProcessRunner::ForceUnlocked; } - case LockWidget::Cancelled: + case UILocker::Cancelled: { log::debug("waiting for {} cancelled by user", pid); return ProcessRunner::Cancelled; } - case LockWidget::NoResult: // fall-through + case UILocker::NoResult: // fall-through default: { // shouldn't happen @@ -255,7 +255,7 @@ std::optional timedWait( } ProcessRunner::Results waitForProcessesThreadImpl( - const std::vector& initialProcesses, LockWidget::Session& ls) + const std::vector& initialProcesses, UILocker::Session& ls) { using namespace std::chrono; @@ -315,14 +315,14 @@ ProcessRunner::Results waitForProcessesThreadImpl( void waitForProcessesThread( ProcessRunner::Results& result, - const std::vector& initialProcesses, LockWidget::Session& ls) + const std::vector& initialProcesses, UILocker::Session& ls) { result = waitForProcessesThreadImpl(initialProcesses, ls); ls.unlock(); } ProcessRunner::Results waitForProcesses( - const std::vector& initialProcesses, LockWidget::Session& ls) + const std::vector& initialProcesses, UILocker::Session& ls) { auto results = ProcessRunner::Running; @@ -343,7 +343,7 @@ ProcessRunner::Results waitForProcesses( } ProcessRunner::Results waitForProcess( - HANDLE initialProcess, LPDWORD exitCode, LockWidget::Session& ls) + HANDLE initialProcess, LPDWORD exitCode, UILocker::Session& ls) { std::vector processes = {initialProcess}; @@ -364,7 +364,7 @@ ProcessRunner::Results waitForProcess( ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) : - m_core(core), m_ui(ui), m_lockReason(LockWidget::NoReason), + m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason), m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) { // all processes started in ProcessRunner are hooked @@ -414,7 +414,7 @@ ProcessRunner& ProcessRunner::setProfileName(const QString& profileName) } ProcessRunner& ProcessRunner::setWaitForCompletion( - WaitFlags flags, LockWidget::Reasons reason) + WaitFlags flags, UILocker::Reasons reason) { m_waitFlags = flags; m_lockReason = reason; @@ -670,13 +670,13 @@ ProcessRunner::Results ProcessRunner::postRun() { const bool mustWait = (m_waitFlags & ForceWait); - if (mustWait && m_lockReason == LockWidget::NoReason) { + if (mustWait && m_lockReason == UILocker::NoReason) { // never lock the ui without an escape hatch for the user log::debug( "the ForceWait flag is set but the lock reason wasn't, " "defaulting to LockUI"); - m_lockReason = LockWidget::LockUI; + m_lockReason = UILocker::LockUI; } if (mustWait) { @@ -689,7 +689,7 @@ ProcessRunner::Results ProcessRunner::postRun() } else { // no force wait - if (m_lockReason == LockWidget::NoReason) { + if (m_lockReason == UILocker::NoReason) { // no locking requested return Running; } @@ -750,7 +750,7 @@ env::HandlePtr ProcessRunner::stealProcessHandle() } ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( - LockWidget::Reasons reason) + UILocker::Reasons reason) { m_lockReason = reason; @@ -784,8 +784,8 @@ ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( return r; } -void ProcessRunner::withLock(std::function f) +void ProcessRunner::withLock(std::function f) { - auto ls = LockWidget::instance().lock(m_lockReason); + auto ls = UILocker::instance().lock(m_lockReason); f(*ls); } diff --git a/src/processrunner.h b/src/processrunner.h index 276c46b1..af5cd416 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -65,7 +65,7 @@ public: ProcessRunner& setForcedLibraries(const ForcedLibraries& forcedLibraries); ProcessRunner& setProfileName(const QString& profileName); ProcessRunner& setWaitForCompletion( - WaitFlags flags=NoFlags, LockWidget::Reasons reason=LockWidget::LockUI); + WaitFlags flags=NoFlags, UILocker::Reasons reason=UILocker::LockUI); // if the target is an executable file, runs that; for anything else, calls // ShellExecute() on it @@ -134,7 +134,7 @@ public: // running a process, but it uses the same internal stuff as when running a // process // - Results waitForAllUSVFSProcessesWithLock(LockWidget::Reasons reason); + Results waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason); private: OrganizerCore& m_core; @@ -143,7 +143,7 @@ private: QString m_customOverwrite; ForcedLibraries m_forcedLibraries; QString m_profileName; - LockWidget::Reasons m_lockReason; + UILocker::Reasons m_lockReason; WaitFlags m_waitFlags; QString m_shellOpen; env::HandlePtr m_handle; @@ -164,7 +164,7 @@ private: // creates the lock widget and calls f() // - void withLock(std::function f); + void withLock(std::function f); }; Q_DECLARE_OPERATORS_FOR_FLAGS(ProcessRunner::WaitFlags); -- cgit v1.3.1 From 2527d6aa87f36894c291512a14f12940a5100484 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 26 Nov 2019 03:30:44 -0500 Subject: switched to using a job object to monitor for processes so child processes can also be captured --- src/envmodule.cpp | 180 ++++++++++++++++++++++++++++++++++++++++++++--- src/envmodule.h | 5 +- src/processrunner.cpp | 186 +++++++++++++++++++++++++++++++------------------ src/usvfsconnector.cpp | 8 ++- 4 files changed, 298 insertions(+), 81 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/envmodule.cpp b/src/envmodule.cpp index 09593e61..13831631 100644 --- a/src/envmodule.cpp +++ b/src/envmodule.cpp @@ -365,16 +365,13 @@ const QString& Process::name() const HandlePtr Process::openHandleForWait() const { - HandlePtr h(OpenProcess( - PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, m_pid)); - - if (!h) { - const auto e = GetLastError(); - log::error("can't get name of process {}, {}", m_pid, formatSystemMessage(e)); - return {}; - } + const auto rights = + PROCESS_QUERY_LIMITED_INFORMATION | // exit code, image name, etc. + SYNCHRONIZE | // wait functions + PROCESS_SET_QUOTA | PROCESS_TERMINATE; // add to job - return h; + // don't log errors, failure can happen if the process doesn't exist + return HandlePtr(OpenProcess(rights, FALSE, m_pid)); } // whether this process can be accessed; fails if the current process doesn't @@ -405,6 +402,11 @@ std::vector& Process::children() return m_children; } +const std::vector& Process::children() const +{ + return m_children; +} + std::vector getLoadedModules() { @@ -531,9 +533,10 @@ void findChildren(Process& parent, const std::vector& processes) } } -Process getProcessTree(HANDLE parent) + +Process getProcessTreeFromProcess(HANDLE h) { - const auto parentPID = ::GetProcessId(parent); + const auto parentPID = ::GetProcessId(h); const auto v = getRunningProcesses(); Process root; @@ -553,6 +556,161 @@ Process getProcessTree(HANDLE parent) return root; } + +std::vector processesInJob(HANDLE h) +{ + for (int tries=0; tries<5; ++tries) { + DWORD maxIds = 100; + + const DWORD idsSize = sizeof(ULONG_PTR) * maxIds; + const DWORD bufferSize = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + idsSize; + + MallocPtr buffer(std::malloc(bufferSize)); + auto* ids = static_cast(buffer.get()); + + const auto r = QueryInformationJobObject( + h, JobObjectBasicProcessIdList, ids, bufferSize, nullptr); + + if (!r) { + const auto e = GetLastError(); + log::error("failed to get process ids in job, {}", formatSystemMessage(e)); + return {}; + } + + if (ids->NumberOfProcessIdsInList >= ids->NumberOfAssignedProcesses) { + std::vector v; + for (DWORD i=0; iNumberOfProcessIdsInList; ++i) { + v.push_back(ids->ProcessIdList[i]); + } + + return v; + } + + // try again with a larger buffer + maxIds *= 2; + } + + log::error("failed to get processes in job, can't get a buffer large enough"); + return {}; +} + + +void findChildProcesses(Process& parent, std::vector& processes) +{ + // find all processes that are direct children of `parent` + auto itor = processes.begin(); + + while (itor != processes.end()) { + if (itor->ppid() == parent.pid()) { + parent.addChild(*itor); + itor = processes.erase(itor); + } else { + ++itor; + } + } + + // find all processes that are direct children of `parent`'s children + for (auto&& c : parent.children()) { + findChildProcesses(c, processes); + } +} + +Process getProcessTreeFromJob(HANDLE h) +{ + const auto ids = processesInJob(h); + if (ids.empty()) { + return {}; + } + + std::vector ps; + + forEachRunningProcess([&](auto&& entry) { + for (auto&& id : ids) { + if (entry.th32ProcessID == id) { + ps.push_back(Process( + entry.th32ProcessID, + entry.th32ParentProcessID, + QString::fromStdWString(entry.szExeFile))); + + break; + } + } + + return true; + }); + + Process root; + + { + // getting processes whose parent is not in the list + for (auto&& possibleRoot : ps) { + const auto ppid = possibleRoot.ppid(); + bool found = false; + + for (auto&& p : ps) { + if (p.pid() == ppid) { + found = true; + break; + } + } + + if (!found) { + // this is a root process + root.addChild(possibleRoot); + } + } + + // removing root processes from the list + auto newEnd = std::remove_if(ps.begin(), ps.end(), [&](auto&& p) { + for (auto&& rp : root.children()) { + if (rp.pid() == p.pid()) { + return true; + } + } + + return false; + }); + + ps.erase(newEnd, ps.end()); + } + + // at this point, `processes` should only contain processes that are direct + // or indirect children of the ones in `root` + + if (ps.empty()) { + // and that's all there is + return root; + } + + { + // recursively find children + for (auto&& r : root.children()) { + findChildProcesses(r, ps); + } + } + + return root; +} + +bool isJobHandle(HANDLE h) +{ + JOBOBJECT_BASIC_ACCOUNTING_INFORMATION info = {}; + + const auto r = ::QueryInformationJobObject( + h, JobObjectBasicAccountingInformation, &info, sizeof(info), nullptr); + + return r; +} + +Process getProcessTree(HANDLE h) +{ + if (isJobHandle(h)) { + return getProcessTreeFromJob(h); + } else { + return getProcessTreeFromProcess(h); + } +} + QString getProcessName(DWORD pid) { HandlePtr h(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid)); diff --git a/src/envmodule.h b/src/envmodule.h index d152b840..c2829b32 100644 --- a/src/envmodule.h +++ b/src/envmodule.h @@ -136,6 +136,7 @@ public: void addChild(Process p); std::vector& children(); + const std::vector& children() const; private: DWORD m_pid; @@ -148,7 +149,9 @@ private: std::vector getRunningProcesses(); std::vector getLoadedModules(); -Process getProcessTree(HANDLE parent); +// works for both jobs and processes +// +Process getProcessTree(HANDLE h); QString getProcessName(DWORD pid); QString getProcessName(HANDLE process); diff --git a/src/processrunner.cpp b/src/processrunner.cpp index cfddbb63..ce1ac681 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -106,21 +106,46 @@ QString toString(Interest i) } +struct InterestingProcess +{ + env::Process p; + Interest interest = Interest::None; + env::HandlePtr handle; +}; + + +InterestingProcess findRandomProcess(const env::Process& root) +{ + for (auto&& c : root.children()) { + env::HandlePtr h = c.openHandleForWait(); + if (h) { + return {c, Interest::Weak, std::move(h)}; + } + + auto r = findRandomProcess(c); + if (r.handle) { + return r; + } + } + + return {}; +} + // returns a process that's in the hidden list, or the top-level process if // they're all hidden; returns an invalid process if the list is empty // -std::pair findInterestingProcessInTrees( - std::vector& processes) +InterestingProcess findInterestingProcessInTrees(const env::Process& root) { - if (processes.empty()) { - return {{}, Interest::None}; - } - // Certain process names we wish to "hide" for aesthetic reason: - const std::vector hiddenList = { - QFileInfo(QCoreApplication::applicationFilePath()).fileName() + static const std::vector hiddenList = { + QFileInfo(QCoreApplication::applicationFilePath()).fileName(), + "conhost.exe" }; + if (root.children().empty()) { + return {}; + } + auto isHidden = [&](auto&& p) { for (auto h : hiddenList) { if (p.name().contains(h, Qt::CaseInsensitive)) { @@ -132,54 +157,61 @@ std::pair findInterestingProcessInTrees( }; - for (auto&& root : processes) { - if (!isHidden(root)) { - return {root, Interest::Strong}; + for (auto&& p : root.children()) { + if (!isHidden(p)) { + env::HandlePtr h = p.openHandleForWait(); + if (h) { + return {p, Interest::Strong, std::move(h)}; + } } - for (auto&& child : root.children()) { - if (!isHidden(child)) { - return {child, Interest::Strong}; - } + auto r = findInterestingProcessInTrees(p); + if (r.interest == Interest::Strong) { + return r; } } - // everything is hidden, just pick the first one - return {processes[0], Interest::Weak}; + // everything is hidden, just pick the first one that can be used + return findRandomProcess(root); } -// gets the most interesting process in the list -// -std::pair getInterestingProcess( - const std::vector& initialProcesses) +void dump(const env::Process& p, int indent) { - if (initialProcesses.empty()) { - log::debug("nothing to wait for"); - return {{}, Interest::None}; + log::debug( + "{}{}, pid={}, ppid={}", + std::string(indent * 4, ' '), p.name(), p.pid(), p.ppid()); + + for (auto&& c : p.children()) { + dump(c, indent + 1); } +} - std::vector processes; +void dump(const env::Process& root) +{ + log::debug("process tree:"); - // getting process trees for all processes - for (auto&& h : initialProcesses) { - auto tree = env::getProcessTree(h); - if (tree.isValid()) { - processes.push_back(tree); - } + for (auto&& p : root.children()) { + dump(p, 1); } +} - if (processes.empty()) { - // if the initial list wasn't empty but this one is, it means all the - // processes were already completed - log::debug("processes are already completed"); - return {{}, Interest::None}; +// gets the most interesting process in the list +// +InterestingProcess getInterestingProcess(HANDLE job) +{ + env::Process root = env::getProcessTree(job); + if (root.children().empty()) { + log::debug("nothing to wait for"); + return {}; } - const auto interest = findInterestingProcessInTrees(processes); - if (!interest.first.isValid()) { - // this shouldn't happen + dump(root); + + auto interest = findInterestingProcessInTrees(root); + if (!interest.handle) { + // this can happen if none of the processes can be opened log::debug("no interesting process to wait for"); - return {{}, Interest::None}; + return {}; } return interest; @@ -255,56 +287,52 @@ std::optional timedWait( } ProcessRunner::Results waitForProcessesThreadImpl( - const std::vector& initialProcesses, UILocker::Session& ls) + HANDLE job, UILocker::Session& ls) { using namespace std::chrono; - if (initialProcesses.empty()) { - // shouldn't happen - return ProcessRunner::Completed; - } - DWORD currentPID = 0; // if the interesting process that was found is weak (such as ModOrganizer.exe // when starting a program from within the Data directory), start with a short // wait and check for more interesting children - milliseconds wait(50); + const milliseconds defaultWait(50); + auto wait = defaultWait; for (;;) { - auto [p, interest] = getInterestingProcess(initialProcesses); - if (!p.isValid()) { + auto ip = getInterestingProcess(job); + if (!ip.handle) { // nothing to wait on return ProcessRunner::Completed; } // update the lock widget - ls.setInfo(p.pid(), p.name()); + ls.setInfo(ip.p.pid(), ip.p.name()); - // open the process - auto interestingHandle = p.openHandleForWait(); - if (!interestingHandle) { - return ProcessRunner::Error; - } - - if (p.pid() != currentPID) { + if (ip.p.pid() != currentPID) { // log any change in the process being waited for - currentPID = p.pid(); + currentPID = ip.p.pid(); log::debug( "waiting for completion on {} ({}), {} interest", - p.name(), p.pid(), toString(interest)); + ip.p.name(), ip.p.pid(), toString(ip.interest)); } - if (interest == Interest::Strong) { + if (ip.interest == Interest::Strong) { // don't bother with short wait, this is a good process to wait for wait = Infinite; } - const auto r = timedWait(interestingHandle.get(), p.pid(), ls, wait); + const auto r = timedWait(ip.handle.get(), ip.p.pid(), ls, wait); if (r) { - // the process has completed or returned an error - return *r; + if (*r == ProcessRunner::Results::Completed) { + // process completed, check another one, reset the wait time to find + // interesting processes + wait = defaultWait; + } else if (*r != ProcessRunner::Results::Running) { + // something's wrong, or the user unlocked the ui + return *r; + } } // exponentially increase the wait time between checks for interesting @@ -314,20 +342,44 @@ ProcessRunner::Results waitForProcessesThreadImpl( } void waitForProcessesThread( - ProcessRunner::Results& result, - const std::vector& initialProcesses, UILocker::Session& ls) + ProcessRunner::Results& result, HANDLE job, UILocker::Session& ls) { - result = waitForProcessesThreadImpl(initialProcesses, ls); + result = waitForProcessesThreadImpl(job, ls); ls.unlock(); } ProcessRunner::Results waitForProcesses( const std::vector& initialProcesses, UILocker::Session& ls) { + // using a job so any child process started by any of those processes can also + // be captured and monitored + env::HandlePtr job(CreateJobObjectW(nullptr, nullptr)); + if (!job) { + const auto e = GetLastError(); + + log::error( + "failed to create job to wait for processes, {}", + formatSystemMessage(e)); + + return ProcessRunner::Error; + } + + for (auto&& h : initialProcesses) { + if (!::AssignProcessToJobObject(job.get(), h)) { + const auto e = GetLastError(); + + log::error( + "can't assign process to job to wait for processes, {}", + formatSystemMessage(e)); + + // keep going + } + } + auto results = ProcessRunner::Running; auto* t = QThread::create( - waitForProcessesThread, std::ref(results), initialProcesses, std::ref(ls)); + waitForProcessesThread, std::ref(results), job.get(), std::ref(ls)); QEventLoop events; QObject::connect(t, &QThread::finished, [&]{ diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp index 3dba3efc..3c8c355b 100644 --- a/src/usvfsconnector.cpp +++ b/src/usvfsconnector.cpp @@ -280,13 +280,17 @@ std::vector getRunningUSVFSProcesses() const auto thisPid = GetCurrentProcessId(); std::vector v; + const auto rights = + PROCESS_QUERY_LIMITED_INFORMATION | // exit code, image name, etc. + SYNCHRONIZE | // wait functions + PROCESS_SET_QUOTA | PROCESS_TERMINATE; // add to job + for (auto&& pid : pids) { if (pid == thisPid) { continue; // obviously don't wait for MO process } - HANDLE handle = ::OpenProcess( - PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pid); + HANDLE handle = ::OpenProcess(rights, FALSE, pid); if (handle == INVALID_HANDLE_VALUE) { const auto e = GetLastError(); -- cgit v1.3.1 From a87874497a3fbe7b3ac646479d4ba51883f5acc6 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Tue, 26 Nov 2019 16:04:21 -0500 Subject: cwd was MO instead of binary if non was specified in the exe settings --- src/processrunner.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/processrunner.cpp b/src/processrunner.cpp index ce1ac681..89777072 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -520,9 +520,9 @@ ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe) forcedLibraries = profile->determineForcedLibraries(exe.title()); } - QDir currentDirectory = exe.workingDirectory(); + QString currentDirectory = exe.workingDirectory(); if (currentDirectory.isEmpty()) { - currentDirectory.setPath(exe.binaryInfo().absolutePath()); + currentDirectory = exe.binaryInfo().absolutePath(); } setBinary(exe.binaryInfo()); -- cgit v1.3.1 From 2b4d7929769a91d9de30e1e10319d1c9cf0450af Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 2 Dec 2019 11:06:10 -0500 Subject: don't show the lock overlay for processes that are not hooked starting exe from filetree is now hooked usvfs progress dialog now on top of dialogs --- src/mainwindow.ui | 3 --- src/modinfodialogfiletree.cpp | 5 ++++- src/processrunner.cpp | 9 +++++++++ src/processrunner.h | 5 +++-- src/usvfsconnector.cpp | 3 ++- 5 files changed, 18 insertions(+), 7 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 85be22b3..0520db84 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -1073,9 +1073,6 @@ p, li { white-space: pre-wrap; } true - - true - 400 diff --git a/src/modinfodialogfiletree.cpp b/src/modinfodialogfiletree.cpp index 79ed4cba..00471a72 100644 --- a/src/modinfodialogfiletree.cpp +++ b/src/modinfodialogfiletree.cpp @@ -140,7 +140,10 @@ void FileTreeTab::onOpen() return; } - shell::Open(m_fs->filePath(selection)); + core().processRunner() + .setFromFile(parentWidget(), m_fs->filePath(selection)) + .setWaitForCompletion() + .run(); } void FileTreeTab::onPreview() diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 89777072..b6167706 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -498,6 +498,10 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ default: { m_shellOpen = targetInfo.absoluteFilePath(); + + // picked up by postRun() + m_sp.hooked = false; + break; } } @@ -722,6 +726,11 @@ ProcessRunner::Results ProcessRunner::postRun() { const bool mustWait = (m_waitFlags & ForceWait); + if (!m_sp.hooked && !mustWait) { + // the process wasn't hooked and there's no force wait, don't lock + return Running; + } + if (mustWait && m_lockReason == UILocker::NoReason) { // never lock the ui without an escape hatch for the user log::debug( diff --git a/src/processrunner.h b/src/processrunner.h index c61d6b70..a5099136 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -42,8 +42,9 @@ public: // the ui will be refreshed once the process has completed Refresh = 0x01, - // the process will be waited for even if locking is disabled - ForceWait = 0x02 + // the process will be waited for even if locking is disabled or the + // process is not hooked + ForceWait = 0x02, }; using WaitFlags = QFlags; diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp index 3c8c355b..59880754 100644 --- a/src/usvfsconnector.cpp +++ b/src/usvfsconnector.cpp @@ -181,10 +181,11 @@ UsvfsConnector::~UsvfsConnector() void UsvfsConnector::updateMapping(const MappingType &mapping) { - QProgressDialog progress; + QProgressDialog progress(qApp->activeWindow()); progress.setLabelText(tr("Preparing vfs")); progress.setMaximum(static_cast(mapping.size())); progress.show(); + int value = 0; int files = 0; int dirs = 0; -- cgit v1.3.1 From d4172dc5f8c642dbbe235a86a28992af310e703a Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Mon, 2 Dec 2019 13:51:46 -0500 Subject: added "open with vfs" option to conflicts tab --- src/env.cpp | 157 +++++++++++++++++++++++++++++++++++++++++ src/env.h | 20 ++++++ src/modinfodialog.cpp | 23 +++++- src/modinfodialogconflicts.cpp | 39 +++++++++- src/modinfodialogconflicts.h | 2 + src/modinfodialogfwd.h | 1 + src/processrunner.cpp | 20 +++++- src/processrunner.h | 12 ++-- 8 files changed, 263 insertions(+), 11 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/env.cpp b/src/env.cpp index f9507dc1..0098456e 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -675,6 +675,163 @@ Service getService(const QString& name) } +std::optional getAssocString(const QFileInfo& file, ASSOCSTR astr) +{ + const auto ext = L"." + file.suffix().toStdWString(); + + // getting buffer size + DWORD bufferSize = 0; + auto r = AssocQueryStringW( + ASSOCF_INIT_IGNOREUNKNOWN, astr, ext.c_str(), L"open", nullptr, &bufferSize); + + // returns S_FALSE when giving back the buffer size, so that's actually the + // expected return value + + if (r != S_FALSE || bufferSize == 0) { + if (r == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { + log::error("file '{}' has no associated executable", file.absoluteFilePath()); + } else { + log::error( + "can't get buffer size for AssocQueryStringW(), {}", + formatSystemMessage(r)); + } + return {}; + } + + // getting string + auto buffer = std::make_unique(bufferSize + 1); + std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0); + + r = AssocQueryStringW( + ASSOCF_INIT_IGNOREUNKNOWN, astr, ext.c_str(), L"open", buffer.get(), &bufferSize); + + if (FAILED(r)) { + log::error( + "failed to get exe associated with '{}', {}", + file.suffix(), formatSystemMessage(r)); + + return {}; + } + + // buffer size includes the null terminator + return QString::fromWCharArray(buffer.get(), bufferSize - 1); +} + +QString formatCommandLine(const QFileInfo& targetInfo, const QString& cmd) +{ + // yeah, FormatMessage() expects at least as many arguments as there are + // placeholders and while the command for associations should typically only + // have %1, the user can actually enter anything in the registry + // + // since the maximum number of arguments is 99, this creates an array of 99 + // wchar_* where the first one (%1) points to the filename and the remaining + // 98 to "" + // + // FormatMessage() actually takes a va_list* for the arguments, but by passing + // FORMAT_MESSAGE_ARGUMENT_ARRAY, an array of DWORD_PTR can be given instead + + // 99 arguments + std::array args; + + // first one is the filename + const auto wpath = targetInfo.absoluteFilePath().toStdWString(); + args[0] = reinterpret_cast(wpath.c_str()); + + // remaining are "" + std::fill(args.begin() + 1, args.end(), reinterpret_cast(L"")); + + // must be freed with LocalFree() + wchar_t* buffer = nullptr; + + const auto wcmd = cmd.toStdWString(); + + const auto n = ::FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_ARGUMENT_ARRAY | + FORMAT_MESSAGE_FROM_STRING, + wcmd.c_str(), 0, 0, + reinterpret_cast(&buffer), + 0, reinterpret_cast(&args[0])); + + if (n == 0 || !buffer){ + const auto e = GetLastError(); + + log::error( + "failed to format command line '{}' with path '{}', {}", + cmd, targetInfo.absoluteFilePath(), formatSystemMessage(e)); + + return {}; + } + + auto s = QString::fromWCharArray(buffer, n); + ::LocalFree(buffer); + + return s.trimmed(); +} + +std::pair splitExeAndArguments(const QString& cmd) +{ + int exeBegin = 0; + int exeEnd = -1; + + if (cmd[0] == '"'){ + // surrounded by double-quotes, so find the next one + exeBegin = 1; + exeEnd = cmd.indexOf('"', exeBegin); + + if (exeEnd == -1) { + log::error("missing terminating double-quote in command line '{}'", cmd); + return {}; + } + } else { + // no double-quotes, find the first whitespace + exeEnd = cmd.indexOf(QRegExp("\\s")); + if (exeEnd == -1) { + exeEnd = cmd.size(); + } + } + + QString exe = cmd.mid(exeBegin, exeEnd - exeBegin).trimmed(); + QString args = cmd.mid(exeEnd + 1).trimmed(); + + return {std::move(exe), std::move(args)}; +} + +Association getAssociation(const QFileInfo& targetInfo) +{ + log::debug( + "getting association for '{}', extension is '.{}'", + targetInfo.absoluteFilePath(), targetInfo.suffix()); + + const auto cmd = getAssocString(targetInfo, ASSOCSTR_COMMAND); + if (!cmd) { + return {}; + } + + log::debug("raw cmd is '{}'", *cmd); + + QString formattedCmd = formatCommandLine(targetInfo, *cmd); + if (formattedCmd.isEmpty()) { + log::error( + "command line associated with '{}' is empty", + targetInfo.absoluteFilePath()); + + return {}; + } + + log::debug("formatted cmd is '{}'", formattedCmd); + + const auto p = splitExeAndArguments(formattedCmd); + if (p.first.isEmpty()) { + return {}; + } + + log::debug("split into exe='{}' and cmd='{}'", p.first, p.second); + + return {p.first, *cmd, p.second}; +} + + // returns the filename of the given process or the current one // std::wstring processFilename(HANDLE process=INVALID_HANDLE_VALUE) diff --git a/src/env.h b/src/env.h index dc0fd864..16f8039e 100644 --- a/src/env.h +++ b/src/env.h @@ -246,6 +246,26 @@ Service getService(const QString& name); QString toString(Service::StartType st); QString toString(Service::Status st); + +struct Association +{ + // path to the executable associated with the file + QFileInfo executable; + + // full command line associated with the file, no replacements + QString commandLine; + + // command line _without_ the executable and with placeholders such as %1 + // replaced by the given file + QString formattedCommandLine; +}; + +// returns the associated executable and command line, executable is empty on +// error +// +Association getAssociation(const QFileInfo& file); + + enum class CoreDumpTypes { Mini = 1, diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index c7e071ad..f5ca1de7 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -61,10 +61,27 @@ bool canPreviewFile( return pluginContainer.previewGenerator().previewSupported(ext); } -bool canOpenFile(bool isArchive, const QString&) +bool isExecutableFilename(const QString& filename) { - // can open anything as long as it's not in an archive - return !isArchive; + static const std::set exeExtensions = { + "exe", "cmd", "bat" + }; + + const auto ext = QFileInfo(filename).suffix().toLower(); + + return exeExtensions.contains(ext); +} + +bool canRunFile(bool isArchive, const QString& filename) +{ + // can run executables that are not archives + return !isArchive && isExecutableFilename(filename); +} + +bool canOpenFile(bool isArchive, const QString& filename) +{ + // can open non-executables that are not archives + return !isArchive && !isExecutableFilename(filename); } bool canExploreFile(bool isArchive, const QString&) diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index d37f068c..58e935f2 100644 --- a/src/modinfodialogconflicts.cpp +++ b/src/modinfodialogconflicts.cpp @@ -81,6 +81,11 @@ public: return canUnhideFile(isArchive(), fileName()); } + bool canRun() const + { + return canRunFile(isArchive(), fileName()); + } + bool canOpen() const { return canOpenFile(isArchive(), fileName()); @@ -536,6 +541,20 @@ void ConflictsTab::openItems(QTreeView* tree) }); } +void ConflictsTab::runItemsHooked(QTreeView* tree) +{ + // the menu item is only shown for a single selection, but handle all of them + // in case this changes + for_each_in_selection(tree, [&](const ConflictItem* item) { + core().processRunner() + .setFromFile(parentWidget(), item->fileName(), true) + .setWaitForCompletion() + .run(); + + return true; + }); +} + void ConflictsTab::previewItems(QTreeView* tree) { // the menu item is only shown for a single selection, but handle all of them @@ -571,6 +590,15 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree) menu.addAction(actions.open); } + // run hooked + if (actions.runHooked) { + connect(actions.runHooked, &QAction::triggered, [&]{ + runItemsHooked(tree); + }); + + menu.addAction(actions.runHooked); + } + // preview if (actions.preview) { connect(actions.preview, &QAction::triggered, [&]{ @@ -633,6 +661,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) bool enableHide = true; bool enableUnhide = true; + bool enableRun = true; bool enableOpen = true; bool enablePreview = true; bool enableExplore = true; @@ -657,6 +686,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) enableHide = item->canHide(); enableUnhide = item->canUnhide(); + enableRun = item->canRun(); enableOpen = item->canOpen(); enablePreview = item->canPreview(plugin()); enableExplore = item->canExplore(); @@ -665,6 +695,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) else { // this is a multiple selection, don't show open/preview so users don't open // a thousand files + enableRun = false; enableOpen = false; enablePreview = false; @@ -709,8 +740,12 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) actions.unhide = new QAction(tr("&Unhide"), parentWidget()); actions.unhide->setEnabled(enableUnhide); - actions.open = new QAction(tr("&Open/Execute"), parentWidget()); - actions.open->setEnabled(enableOpen); + if (enableRun) { + actions.open = new QAction(tr("&Execute"), parentWidget()); + } else if (enableOpen) { + actions.open = new QAction(tr("&Open"), parentWidget()); + actions.runHooked = new QAction(tr("Open with &VFS"), parentWidget()); + } actions.preview = new QAction(tr("&Preview"), parentWidget()); actions.preview->setEnabled(enablePreview); diff --git a/src/modinfodialogconflicts.h b/src/modinfodialogconflicts.h index ad305dfc..ebf82033 100644 --- a/src/modinfodialogconflicts.h +++ b/src/modinfodialogconflicts.h @@ -112,6 +112,7 @@ public: bool canHandleUnmanaged() const override; void openItems(QTreeView* tree); + void runItemsHooked(QTreeView* tree); void previewItems(QTreeView* tree); void exploreItems(QTreeView* tree); @@ -125,6 +126,7 @@ private: QAction* hide = nullptr; QAction* unhide = nullptr; QAction* open = nullptr; + QAction* runHooked = nullptr; QAction* preview = nullptr; QAction* explore = nullptr; QMenu* gotoMenu = nullptr; diff --git a/src/modinfodialogfwd.h b/src/modinfodialogfwd.h index 9ede766f..2147fc04 100644 --- a/src/modinfodialogfwd.h +++ b/src/modinfodialogfwd.h @@ -23,6 +23,7 @@ enum class ModInfoTabIDs class PluginContainer; bool canPreviewFile(PluginContainer& pluginContainer, bool isArchive, const QString& filename); +bool canRunFile(bool isArchive, const QString& filename); bool canOpenFile(bool isArchive, const QString& filename); bool canExploreFile(bool isArchive, const QString& filename); bool canHideFile(bool isArchive, const QString& filename); diff --git a/src/processrunner.cpp b/src/processrunner.cpp index b6167706..46065d69 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -3,6 +3,8 @@ #include "instancemanager.h" #include "iuserinterface.h" #include "envmodule.h" +#include "env.h" +#include #include using namespace MOBase; @@ -473,13 +475,14 @@ ProcessRunner& ProcessRunner::setWaitForCompletion( return *this; } -ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo) +ProcessRunner& ProcessRunner::setFromFile( + QWidget* parent, const QFileInfo& targetInfo, bool forceHook) { if (!parent && m_ui) { parent = m_ui->qtWidget(); } - // if the file is a .exe, start it directory; if it's anything else, ask the + // if the file is a .exe, start it directly; if it's anything else, ask the // shell to start it const auto fec = spawn::getFileExecutionContext(parent, targetInfo); @@ -497,6 +500,19 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ case spawn::FileExecutionTypes::Other: // fall-through default: { + if (forceHook) { + auto assoc = env::getAssociation(targetInfo); + if (!assoc.executable.filePath().isEmpty()) { + setBinary(assoc.executable); + setArguments(assoc.formattedCommandLine); + setCurrentDirectory(assoc.executable.absoluteDir()); + + return *this; + } + + // if it fails, just use the regular shell open + } + m_shellOpen = targetInfo.absoluteFilePath(); // picked up by postRun() diff --git a/src/processrunner.h b/src/processrunner.h index a5099136..d576216a 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -68,10 +68,14 @@ public: ProcessRunner& setWaitForCompletion( WaitFlags flags=NoFlags, UILocker::Reasons reason=UILocker::LockUI); - // if the target is an executable file, runs that; for anything else, calls - // ShellExecute() on it - // - ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo); + // - if the target is an executable file, runs it hooked + // - if the target is a file: + // - if forceHook is false, calls ShellExecute() on it + // - if forceHook is true, gets the executable associated with the file + // and runs that hooked by passing the file as an argument + // + ProcessRunner& setFromFile( + QWidget* parent, const QFileInfo& targetInfo, bool forceHook = false); ProcessRunner& setFromExecutable(const Executable& exe); ProcessRunner& setFromShortcut(const MOShortcut& shortcut); -- cgit v1.3.1 From c9049bb07274aaefa79c9751ec40910bf8daf0ca Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 8 Dec 2019 11:33:03 -0500 Subject: conflicts tab: run exes unhooked by default --- src/mainwindow.cpp | 3 +- src/modinfodialogconflicts.cpp | 72 +++++++++++++++++++++++------------------- src/modinfodialogconflicts.h | 5 ++- src/modinfodialogfiletree.cpp | 3 +- src/processrunner.cpp | 70 ++++++++++++++++++++++++++-------------- src/processrunner.h | 8 +++-- 6 files changed, 97 insertions(+), 64 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 43d5b820..532d8502 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -5425,7 +5425,8 @@ void MainWindow::runDataFileHooked() const QFileInfo targetInfo(path); m_OrganizerCore.processRunner() - .setFromFile(this, targetInfo, true) + .setFromFile(this, targetInfo) + .setHooked(true) .setWaitForCompletion(ProcessRunner::Refresh) .run(); } diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index 9c7ccc8c..81e8c7a3 100644 --- a/src/modinfodialogconflicts.cpp +++ b/src/modinfodialogconflicts.cpp @@ -542,45 +542,32 @@ void ConflictsTab::activateItems(QTreeView* tree) if (tryPreview && canPreviewFile(plugin(), item->isArchive(), path)) { previewItem(item); } else { - openItem(item); + openItem(item, false); } return true; }); } -void ConflictsTab::openItems(QTreeView* tree) +void ConflictsTab::openItems(QTreeView* tree, bool hooked) { // the menu item is only shown for a single selection, but handle all of them // in case this changes for_each_in_selection(tree, [&](const ConflictItem* item) { - openItem(item); + openItem(item, hooked); return true; }); } -void ConflictsTab::openItem(const ConflictItem* item) +void ConflictsTab::openItem(const ConflictItem* item, bool hooked) { core().processRunner() .setFromFile(parentWidget(), item->fileName()) + .setHooked(hooked) .setWaitForCompletion() .run(); } -void ConflictsTab::runItemsHooked(QTreeView* tree) -{ - // the menu item is only shown for a single selection, but handle all of them - // in case this changes - for_each_in_selection(tree, [&](const ConflictItem* item) { - core().processRunner() - .setFromFile(parentWidget(), item->fileName(), true) - .setWaitForCompletion() - .run(); - - return true; - }); -} - void ConflictsTab::previewItems(QTreeView* tree) { // the menu item is only shown for a single selection, but handle all of them @@ -615,30 +602,44 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree) // open if (actions.open) { connect(actions.open, &QAction::triggered, [&]{ - openItems(tree); + openItems(tree, false); }); + } - menu.addAction(actions.open); + // preview + if (actions.preview) { + connect(actions.preview, &QAction::triggered, [&]{ + previewItems(tree); + }); + } + + if ((actions.open && actions.open->isEnabled()) && (actions.preview && actions.preview->isEnabled())) { + if (Settings::instance().interface().doubleClicksOpenPreviews()) { + menu.addAction(actions.preview); + menu.addAction(actions.open); + } else { + menu.addAction(actions.open); + menu.addAction(actions.preview); + } + } else { + if (actions.open) { + menu.addAction(actions.open); + } + + if (actions.preview) { + menu.addAction(actions.preview); + } } // run hooked if (actions.runHooked) { connect(actions.runHooked, &QAction::triggered, [&]{ - runItemsHooked(tree); + openItems(tree, true); }); menu.addAction(actions.runHooked); } - // preview - if (actions.preview) { - connect(actions.preview, &QAction::triggered, [&]{ - previewItems(tree); - }); - - menu.addAction(actions.preview); - } - // goto if (actions.gotoMenu) { menu.addMenu(actions.gotoMenu); @@ -681,9 +682,15 @@ void ConflictsTab::showContextMenu(const QPoint &pos, QTreeView* tree) menu.addAction(actions.unhide); } - setDefaultActivationActionForFile(actions.open, actions.preview); - if (!menu.isEmpty()) { + if (actions.open || actions.preview || actions.runHooked) { + // bold the first option + auto* top = menu.actions()[0]; + auto f = top->font(); + f.setBold(true); + top->setFont(f); + } + menu.exec(tree->viewport()->mapToGlobal(pos)); } } @@ -769,6 +776,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) if (enableRun) { actions.open = new QAction(tr("&Execute"), parentWidget()); + actions.runHooked = new QAction(tr("Execute with &VFS"), parentWidget()); } else if (enableOpen) { actions.open = new QAction(tr("&Open"), parentWidget()); actions.runHooked = new QAction(tr("Open with &VFS"), parentWidget()); diff --git a/src/modinfodialogconflicts.h b/src/modinfodialogconflicts.h index 3ac8de23..1297e536 100644 --- a/src/modinfodialogconflicts.h +++ b/src/modinfodialogconflicts.h @@ -108,12 +108,11 @@ public: bool canHandleUnmanaged() const override; void activateItems(QTreeView* tree); - void openItems(QTreeView* tree); - void runItemsHooked(QTreeView* tree); + void openItems(QTreeView* tree, bool hooked); void previewItems(QTreeView* tree); void exploreItems(QTreeView* tree); - void openItem(const ConflictItem* item); + void openItem(const ConflictItem* item, bool hooked); void previewItem(const ConflictItem* item); void changeItemsVisibility(QTreeView* tree, bool visible); diff --git a/src/modinfodialogfiletree.cpp b/src/modinfodialogfiletree.cpp index d1ae3823..0b9e8da4 100644 --- a/src/modinfodialogfiletree.cpp +++ b/src/modinfodialogfiletree.cpp @@ -183,7 +183,8 @@ void FileTreeTab::onRunHooked() } core().processRunner() - .setFromFile(parentWidget(), m_fs->filePath(selection), true) + .setFromFile(parentWidget(), m_fs->filePath(selection)) + .setHooked(true) .setWaitForCompletion() .run(); } diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 46065d69..19aae632 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -421,8 +421,8 @@ ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui) : m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason), m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) { - // all processes started in ProcessRunner are hooked - m_sp.hooked = true; + // all processes started in ProcessRunner are hooked by default + setHooked(true); } ProcessRunner& ProcessRunner::setBinary(const QFileInfo &binary) @@ -475,8 +475,14 @@ ProcessRunner& ProcessRunner::setWaitForCompletion( return *this; } +ProcessRunner& ProcessRunner::setHooked(bool b) +{ + m_sp.hooked = b; + return *this; +} + ProcessRunner& ProcessRunner::setFromFile( - QWidget* parent, const QFileInfo& targetInfo, bool forceHook) + QWidget* parent, const QFileInfo& targetInfo) { if (!parent && m_ui) { parent = m_ui->qtWidget(); @@ -500,24 +506,8 @@ ProcessRunner& ProcessRunner::setFromFile( case spawn::FileExecutionTypes::Other: // fall-through default: { - if (forceHook) { - auto assoc = env::getAssociation(targetInfo); - if (!assoc.executable.filePath().isEmpty()) { - setBinary(assoc.executable); - setArguments(assoc.formattedCommandLine); - setCurrentDirectory(assoc.executable.absoluteDir()); - - return *this; - } - - // if it fails, just use the regular shell open - } - - m_shellOpen = targetInfo.absoluteFilePath(); - - // picked up by postRun() - m_sp.hooked = false; - + m_shellOpen = targetInfo; + setHooked(false); break; } } @@ -649,11 +639,41 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( return *this; } +bool ProcessRunner::shouldRunShell() const +{ + return !m_shellOpen.filePath().isEmpty(); +} + ProcessRunner::Results ProcessRunner::run() { + // check if setHooked() was called after setFromFile(); this needs to + // modify the settings to run the associated executable instead of using + // shell::Open() + + if (shouldRunShell() && m_sp.hooked) { + // this is a non-executable file, but it should be hooked; the associated + // executable needs to be retrieved and run instead + auto assoc = env::getAssociation(m_shellOpen); + if (!assoc.executable.filePath().isEmpty()) { + setBinary(assoc.executable); + setArguments(assoc.formattedCommandLine); + setCurrentDirectory(assoc.executable.absoluteDir()); + m_shellOpen = {}; + } else { + // if it fails, just use the regular shell open + log::error("failed to get the associated executable, running unhooked"); + m_sp.hooked = false; + } + } else if (!shouldRunShell() && !m_sp.hooked) { + // this is an executable that should not be hooked; just run it through + // the shell + m_shellOpen = m_sp.binary; + } + + std::optional r; - if (!m_shellOpen.isEmpty()) { + if (shouldRunShell()) { r = runShell(); } else { r = runBinary(); @@ -669,9 +689,11 @@ ProcessRunner::Results ProcessRunner::run() std::optional ProcessRunner::runShell() { - log::debug("executing from shell: '{}'", m_shellOpen); + const auto file = m_shellOpen.absoluteFilePath(); + + log::debug("executing from shell: '{}'", file); - auto r = shell::Open(m_shellOpen); + auto r = shell::Open(file); if (!r.success()) { return Error; } diff --git a/src/processrunner.h b/src/processrunner.h index d576216a..1bfdc465 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -67,6 +67,7 @@ public: ProcessRunner& setProfileName(const QString& profileName); ProcessRunner& setWaitForCompletion( WaitFlags flags=NoFlags, UILocker::Reasons reason=UILocker::LockUI); + ProcessRunner& setHooked(bool b); // - if the target is an executable file, runs it hooked // - if the target is a file: @@ -74,8 +75,7 @@ public: // - if forceHook is true, gets the executable associated with the file // and runs that hooked by passing the file as an argument // - ProcessRunner& setFromFile( - QWidget* parent, const QFileInfo& targetInfo, bool forceHook = false); + ProcessRunner& setFromFile(QWidget* parent, const QFileInfo& targetInfo); ProcessRunner& setFromExecutable(const Executable& exe); ProcessRunner& setFromShortcut(const MOShortcut& shortcut); @@ -150,11 +150,13 @@ private: QString m_profileName; UILocker::Reasons m_lockReason; WaitFlags m_waitFlags; - QString m_shellOpen; + QFileInfo m_shellOpen; env::HandlePtr m_handle; DWORD m_exitCode; + bool shouldRunShell() const; + // runs the command in m_shellOpen; returns empty if it can be waited for // std::optional runShell(); -- cgit v1.3.1 From f3c5cebb6e9262625105d4339a54a810d7816811 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 15 Dec 2019 21:56:50 -0500 Subject: fixed exiting before QThread joins when pressing the X twice --- src/mainwindow.cpp | 21 ++++++++++++++------- src/processrunner.cpp | 31 +++++++++++++++++++++---------- src/shared/util.cpp | 11 ++++++++++- src/shared/util.h | 1 + 4 files changed, 46 insertions(+), 18 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a29ea8ab..d1578d85 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1317,18 +1317,25 @@ void MainWindow::closeEvent(QCloseEvent* event) // // for 2), the settings have been saved and the window can just close - if (ModOrganizerExiting()) { + if (ModOrganizerCanCloseNow()) { // the user has confirmed if necessary and all settings have been saved, // just close it QMainWindow::closeEvent(event); - } else { - // never close the window because settings might need to be changed - event->ignore(); + return; + } - // start the process of exiting, which may require confirmation by calling - // canExit(), among other things - ExitModOrganizer(); + if (ModOrganizerExiting()) { + // ignore repeated attempts + event->ignore(); + return; } + + // never close the window because settings might need to be changed + event->ignore(); + + // start the process of exiting, which may require confirmation by calling + // canExit(), among other things + ExitModOrganizer(); } bool MainWindow::canExit() diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 19aae632..aead42d1 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -225,7 +225,7 @@ const std::chrono::milliseconds Infinite(-1); // std::optional timedWait( HANDLE handle, DWORD pid, UILocker::Session& ls, - std::chrono::milliseconds wait) + std::chrono::milliseconds wait, std::atomic& interrupt) { using namespace std::chrono; @@ -234,7 +234,7 @@ std::optional timedWait( start = high_resolution_clock::now(); } - for (;;) { + while (!interrupt) { // wait for a very short while, allows for processing events below const auto r = singleWait(handle, pid); @@ -286,10 +286,13 @@ std::optional timedWait( } } } + + log::debug("waiting for {} interrupted", pid); + return ProcessRunner::ForceUnlocked; } ProcessRunner::Results waitForProcessesThreadImpl( - HANDLE job, UILocker::Session& ls) + HANDLE job, UILocker::Session& ls, std::atomic& interrupt) { using namespace std::chrono; @@ -301,7 +304,7 @@ ProcessRunner::Results waitForProcessesThreadImpl( const milliseconds defaultWait(50); auto wait = defaultWait; - for (;;) { + while (!interrupt) { auto ip = getInterestingProcess(job); if (!ip.handle) { // nothing to wait on @@ -325,7 +328,7 @@ ProcessRunner::Results waitForProcessesThreadImpl( wait = Infinite; } - const auto r = timedWait(ip.handle.get(), ip.p.pid(), ls, wait); + const auto r = timedWait(ip.handle.get(), ip.p.pid(), ls, wait, interrupt); if (r) { if (*r == ProcessRunner::Results::Completed) { // process completed, check another one, reset the wait time to find @@ -344,9 +347,10 @@ ProcessRunner::Results waitForProcessesThreadImpl( } void waitForProcessesThread( - ProcessRunner::Results& result, HANDLE job, UILocker::Session& ls) + ProcessRunner::Results& result, HANDLE job, UILocker::Session& ls, + std::atomic& interrupt) { - result = waitForProcessesThreadImpl(job, ls); + result = waitForProcessesThreadImpl(job, ls, interrupt); ls.unlock(); } @@ -379,9 +383,11 @@ ProcessRunner::Results waitForProcesses( } auto results = ProcessRunner::Running; + std::atomic interrupt(false); auto* t = QThread::create( - waitForProcessesThread, std::ref(results), job.get(), std::ref(ls)); + waitForProcessesThread, + std::ref(results), job.get(), std::ref(ls), std::ref(interrupt)); QEventLoop events; QObject::connect(t, &QThread::finished, [&]{ @@ -391,6 +397,11 @@ ProcessRunner::Results waitForProcesses( t->start(); events.exec(); + if (t->isRunning()) { + interrupt = true; + t->wait(); + } + delete t; return results; @@ -861,7 +872,7 @@ ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( auto r = Error; withLock([&](auto& ls) { - for (;;) { + for (;;) { const auto processes = getRunningUSVFSProcesses(); if (processes.empty()) { break; @@ -878,7 +889,7 @@ ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( } r = Completed; - }); + }); return r; } diff --git a/src/shared/util.cpp b/src/shared/util.cpp index 32eb825c..baceddeb 100644 --- a/src/shared/util.cpp +++ b/src/shared/util.cpp @@ -294,6 +294,7 @@ QString getUsvfsVersionString() static bool g_exiting = false; +static bool g_canClose = false; MainWindow* findMainWindow() { @@ -312,6 +313,9 @@ bool ExitModOrganizer(ExitFlags e) return true; } + g_exiting = true; + MOBase::Guard g([&]{ g_exiting = false; }); + if (!e.testFlag(Exit::Force)) { if (auto* mw=findMainWindow()) { if (!mw->canExit()) { @@ -320,7 +324,7 @@ bool ExitModOrganizer(ExitFlags e) } } - g_exiting = true; + g_canClose = true; const int code = (e.testFlag(Exit::Restart) ? RestartExitCode : 0); qApp->exit(code); @@ -328,6 +332,11 @@ bool ExitModOrganizer(ExitFlags e) return true; } +bool ModOrganizerCanCloseNow() +{ + return g_canClose; +} + bool ModOrganizerExiting() { return g_exiting; diff --git a/src/shared/util.h b/src/shared/util.h index e87244b6..e8a58549 100644 --- a/src/shared/util.h +++ b/src/shared/util.h @@ -65,6 +65,7 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(ExitFlags); bool ExitModOrganizer(ExitFlags e=Exit::Normal); bool ModOrganizerExiting(); +bool ModOrganizerCanCloseNow(); void ResetExitFlag(); #endif // UTIL_H -- cgit v1.3.1 From 05231eab45f86e3d0d342c429e35c8f7c813ea42 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 15 Dec 2019 22:35:02 -0500 Subject: temporary fix to keep MO locked for all processes when closing save main window settings in closeEvent() --- src/main.cpp | 1 - src/mainwindow.cpp | 22 ++++++++++++++++++++++ src/processrunner.cpp | 26 ++++++++++++++++++-------- src/spawn.cpp | 6 ++++-- src/uilocker.cpp | 5 +++++ src/uilocker.h | 1 + 6 files changed, 50 insertions(+), 11 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/main.cpp b/src/main.cpp index eeabd497..29d2d02c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -745,7 +745,6 @@ int runApplication(MOApplication &application, SingleInstance &instance, splash.finish(&mainWindow); res = application.exec(); - mainWindow.onBeforeClose(); mainWindow.close(); NexusInterface::instance(&pluginContainer) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d1578d85..284c33e3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1306,6 +1306,21 @@ void MainWindow::onBeforeClose() void MainWindow::closeEvent(QCloseEvent* event) { + if (isVisible()) { + // this is messy + // + // the main problem this is solving is when closing MO, then getting the + // lock overlay because processes are still running, then pressing the X + // again + // + // in this case, closeEvent() is _not_ called for the second event and the + // window is immediately hidden + // + // this always saves the settings here; in the event where a lock overlay + // is then shown, it might save settings multiple times, but it's harmless + onBeforeClose(); + } + // this happens for two reasons: // 1) the user requested to close the window, such as clicking the X // 2) close() is called in runApplication() after application.exec() @@ -1324,6 +1339,13 @@ void MainWindow::closeEvent(QCloseEvent* event) return; } + if (UILocker::instance().locked()) { + // don't bother asking the user to confirm if the ui is already locked + event->ignore(); + ExitModOrganizer(Exit::Force); + return; + } + if (ModOrganizerExiting()) { // ignore repeated attempts event->ignore(); diff --git a/src/processrunner.cpp b/src/processrunner.cpp index aead42d1..945d61c3 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -344,6 +344,9 @@ ProcessRunner::Results waitForProcessesThreadImpl( // processes wait = std::min(wait * 2, milliseconds(2000)); } + + log::debug("waiting for processes interrupted"); + return ProcessRunner::ForceUnlocked; } void waitForProcessesThread( @@ -374,9 +377,12 @@ ProcessRunner::Results waitForProcesses( if (!::AssignProcessToJobObject(job.get(), h)) { const auto e = GetLastError(); - log::error( - "can't assign process to job to wait for processes, {}", - formatSystemMessage(e)); + // this happens when closing MO while multiple processes are running, + // so the logging is disabled until it gets fixed + + //log::error( + // "can't assign process to job to wait for processes, {}", + // formatSystemMessage(e)); // keep going } @@ -871,11 +877,12 @@ ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( auto r = Error; - withLock([&](auto& ls) { for (;;) { + withLock([&](auto& ls) { const auto processes = getRunningUSVFSProcesses(); if (processes.empty()) { - break; + r = Completed; + return; } r = waitForProcesses(processes, ls); @@ -886,11 +893,14 @@ ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( } // this process is completed, check for others - } - - r = Completed; + r = Running; }); + if (r != Running) { + break; + } + } + return r; } diff --git a/src/spawn.cpp b/src/spawn.cpp index f95846c8..33bdbc05 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -498,14 +498,16 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) 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(wcommandLine.c_str()), nullptr, nullptr, - inheritHandles, 0, nullptr, wcwd.c_str(), &si, &pi); + inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); } else { success = ::CreateProcess( nullptr, const_cast(wcommandLine.c_str()), nullptr, nullptr, - inheritHandles, 0, nullptr, wcwd.c_str(), &si, &pi); + inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); } const auto e = GetLastError(); diff --git a/src/uilocker.cpp b/src/uilocker.cpp index af9b9b96..07fe7f1b 100644 --- a/src/uilocker.cpp +++ b/src/uilocker.cpp @@ -461,6 +461,11 @@ std::shared_ptr UILocker::lock(Reasons reason) return ls; } +bool UILocker::locked() const +{ + return !m_sessions.empty(); +} + void UILocker::unlock(Session* s) { auto itor = m_sessions.begin(); diff --git a/src/uilocker.h b/src/uilocker.h index d8c22999..cc467184 100644 --- a/src/uilocker.h +++ b/src/uilocker.h @@ -70,6 +70,7 @@ public: void setUserInterface(QWidget* parent); std::shared_ptr lock(Reasons reason); + bool locked() const; Results result() const; -- cgit v1.3.1 From 31cd5531d030838a30d55bcd63cadfff4ecd50ca Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Wed, 18 Dec 2019 17:21:52 -0500 Subject: windows 7 doesn't play well with job objects, so just wait on individual handles fixed getProcessTreeFromProcess() not behaving like getProcessTreeFromJob() --- src/envmodule.cpp | 13 +++++-------- src/processrunner.cpp | 22 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 10 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/envmodule.cpp b/src/envmodule.cpp index 8d348b5e..5be52de6 100644 --- a/src/envmodule.cpp +++ b/src/envmodule.cpp @@ -545,23 +545,20 @@ void findChildren(Process& parent, const std::vector& processes) Process getProcessTreeFromProcess(HANDLE h) { + Process root; + const auto parentPID = ::GetProcessId(h); const auto v = getRunningProcesses(); - Process root; for (auto&& p : v) { if (p.pid() == parentPID) { - root = p; + Process child = p; + findChildren(child, v); + root.addChild(child); break; } } - if (root.pid() == 0) { - return {}; - } - - findChildren(root, v); - return root; } diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 945d61c3..3f1e3a3b 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -360,6 +360,11 @@ void waitForProcessesThread( ProcessRunner::Results waitForProcesses( const std::vector& initialProcesses, UILocker::Session& ls) { + if (initialProcesses.empty()) { + // nothing to wait for + return ProcessRunner::Completed; + } + // using a job so any child process started by any of those processes can also // be captured and monitored env::HandlePtr job(CreateJobObjectW(nullptr, nullptr)); @@ -373,8 +378,12 @@ ProcessRunner::Results waitForProcesses( return ProcessRunner::Error; } + bool oneWorked = false; + for (auto&& h : initialProcesses) { - if (!::AssignProcessToJobObject(job.get(), h)) { + if (::AssignProcessToJobObject(job.get(), h)) { + oneWorked = true; + } else { const auto e = GetLastError(); // this happens when closing MO while multiple processes are running, @@ -388,12 +397,21 @@ ProcessRunner::Results waitForProcesses( } } + HANDLE monitor = INVALID_HANDLE_VALUE; + + if (oneWorked) { + monitor = job.get(); + } else { + // none of the handles could be added to the job, just monitor the first one + monitor = initialProcesses[0]; + } + auto results = ProcessRunner::Running; std::atomic interrupt(false); auto* t = QThread::create( waitForProcessesThread, - std::ref(results), job.get(), std::ref(ls), std::ref(interrupt)); + std::ref(results), monitor, std::ref(ls), std::ref(interrupt)); QEventLoop events; QObject::connect(t, &QThread::finished, [&]{ -- cgit v1.3.1 From 52cc3a41c73851d88983223361c03b800db8c2a2 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Sun, 29 Dec 2019 07:16:09 -0500 Subject: refresh after manually unlocking the ui fixed nexus connect button staying as "cancel" in case of errors fixed logging of duplicate dll loading bumped to rc6 --- src/env.cpp | 1 + src/nxmaccessmanager.cpp | 2 +- src/organizer_en.ts | 6 +++--- src/processrunner.cpp | 56 +++++++++++++++++++++++++++++++++++++----------- src/processrunner.h | 1 + src/version.rc | 2 +- 6 files changed, 50 insertions(+), 18 deletions(-) (limited to 'src/processrunner.cpp') diff --git a/src/env.cpp b/src/env.cpp index 0098456e..4c0aeb86 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -101,6 +101,7 @@ void ModuleNotification::fire(QString path, std::size_t fileSize) { if (m_loaded.contains(path)) { // don't notify if it's been loaded before + return; } m_loaded.insert(path); diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index e5f1fffe..20540593 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -333,8 +333,8 @@ void NexusSSOLogin::onDisconnected() void NexusSSOLogin::onError(QAbstractSocket::SocketError e) { if (m_active) { - setState(Error, m_socket.errorString()); close(); + setState(Error, m_socket.errorString()); } } diff --git a/src/organizer_en.ts b/src/organizer_en.ts index d05ada70..fe4dfd32 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -6317,9 +6317,9 @@ If the folder was still in use, restart MO and try again. - - - + + + No profile set diff --git a/src/processrunner.cpp b/src/processrunner.cpp index 3f1e3a3b..91443750 100644 --- a/src/processrunner.cpp +++ b/src/processrunner.cpp @@ -795,6 +795,48 @@ std::optional ProcessRunner::runBinary() return {}; } +bool ProcessRunner::shouldRefresh(Results r) const +{ + // afterRun() is only called with the Refresh flag; it refreshes the + // directory structure and notifies plugins + // + // refreshing is not always required and can actually cause problems: + // + // 1) running shortcuts doesn't need refreshing because MO closes right + // after + // + // 2) the mod info dialog is not set up to deal with refreshes, so that + // it will crash because the old DirectoryEntry's are still being used + // in the list + if (!m_waitFlags.testFlag(Refresh)) { + log::debug("not refreshing because the flag isn't set"); + return false; + } + + switch (r) + { + case Completed: + { + log::debug("refreshing because the process completed"); + return true; + } + + case ForceUnlocked: + { + log::debug("refreshing because the ui was force unlocked"); + return true; + } + + case Error: // fall-through + case Cancelled: + case Running: + default: + { + return false; + } + } +} + ProcessRunner::Results ProcessRunner::postRun() { const bool mustWait = (m_waitFlags & ForceWait); @@ -841,19 +883,7 @@ ProcessRunner::Results ProcessRunner::postRun() r = waitForProcess(m_handle.get(), &m_exitCode, ls); }); - if (r == Completed && (m_waitFlags & Refresh)) { - // afterRun() is only called with the Refresh flag; it refreshes the - // directory structure and notifies plugins - // - // refreshing is not always required and can actually cause problems: - // - // 1) running shortcuts doesn't need refreshing because MO closes right - // after - // - // 2) the mod info dialog is not set up to deal with refreshes, so that - // it will crash because the old DirectoryEntry's are still being used - // in the list - // + if (shouldRefresh(r)) { m_core.afterRun(m_sp.binary, m_exitCode); } diff --git a/src/processrunner.h b/src/processrunner.h index 1bfdc465..d8ff0227 100644 --- a/src/processrunner.h +++ b/src/processrunner.h @@ -156,6 +156,7 @@ private: bool shouldRunShell() const; + bool shouldRefresh(Results r) const; // runs the command in m_shellOpen; returns empty if it can be waited for // diff --git a/src/version.rc b/src/version.rc index 0cf44243..f7b5f0d5 100644 --- a/src/version.rc +++ b/src/version.rc @@ -4,7 +4,7 @@ // Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser // Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha #define VER_FILEVERSION 2,2,2 -#define VER_FILEVERSION_STR "2.2.2rc5\0" +#define VER_FILEVERSION_STR "2.2.2rc6\0" VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION -- cgit v1.3.1