summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/lockwidget.cpp12
-rw-r--r--src/processrunner.cpp197
-rw-r--r--src/processrunner.h76
-rw-r--r--src/spawn.cpp43
-rw-r--r--src/spawn.h4
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);