diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lockwidget.cpp | 12 | ||||
| -rw-r--r-- | src/processrunner.cpp | 197 | ||||
| -rw-r--r-- | src/processrunner.h | 76 | ||||
| -rw-r--r-- | src/spawn.cpp | 43 | ||||
| -rw-r--r-- | src/spawn.h | 4 |
5 files changed, 212 insertions, 120 deletions
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<QDialog*>(w)) { - // no central widget, just disable the children, except for the overlay - for (auto* child : findChildrenImmediate<QWidget*>(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<QWidget*>(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<env::Process, Interest> findInterestingProcessInTrees( std::vector<env::Process>& processes) { @@ -150,6 +153,8 @@ std::pair<env::Process, Interest> findInterestingProcessInTrees( return {processes[0], Interest::Weak}; } +// gets the most interesting process in the list +// std::pair<env::Process, Interest> getInterestingProcess( const std::vector<HANDLE>& initialProcesses) { @@ -160,7 +165,7 @@ std::pair<env::Process, Interest> getInterestingProcess( std::vector<env::Process> 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<env::Process, Interest> 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<env::Process, Interest> getInterestingProcess( const std::chrono::milliseconds Infinite(-1); +// waits for completion, times out after `wait` if not Infinite +// std::optional<ProcessRunner::Results> timedWait( HANDLE handle, DWORD pid, LockWidget& lock, std::chrono::milliseconds wait) { @@ -195,18 +205,21 @@ std::optional<ProcessRunner::Results> 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<ProcessRunner::Results> timedWait( } if (wait != Infinite) { + // check if enough time has elapsed const auto now = high_resolution_clock::now(); if (duration_cast<milliseconds>(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<Results> 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::Results> 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::Results> 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<WaitFlag>; - using ForcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>; 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<Results> runShell(); + + // runs the binary; returns empty if it can be waited for + // + std::optional<Results> runBinary(); + + // waits for process completion if required + // Results postRun(); + + // creates the lock widget and calls f() + // void withLock(std::function<void (LockWidget&)> 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<wchar_t*>(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<wchar_t*>(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);
|
