diff options
| author | isanae <14251494+isanae@users.noreply.github.com> | 2019-11-11 14:14:01 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-11-11 14:14:01 -0500 |
| commit | d6c3bad01a85c1a5b1a3bdd86efc85efdd4c5f29 (patch) | |
| tree | fddae0025e65bd1d5064bf8ad6fa73a23aa750e9 /src | |
| parent | 7b3c5dcbb3b5d520d166eb5b51f669968f57302e (diff) | |
| parent | decd5c1828f495be4e230c9fc6fb79dd9bfdfb81 (diff) | |
Merge pull request #887 from isanae/spawning-and-waiting
Spawning, waiting and locking
Diffstat (limited to 'src')
35 files changed, 2440 insertions, 1595 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 180422ef..d935981d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -82,9 +82,6 @@ SET(organizer_SRCS main.cpp loghighlighter.cpp loglist.cpp - lockeddialogbase.cpp - lockeddialog.cpp - waitingonclosedialog.cpp loadmechanism.cpp installationmanager.cpp filedialogmemory.cpp @@ -143,6 +140,8 @@ SET(organizer_SRCS envwindows.cpp colortable.cpp sanitychecks.cpp + processrunner.cpp + uilocker.cpp shared/windows_error.cpp shared/error_report.cpp @@ -204,9 +203,6 @@ SET(organizer_HDRS mainwindow.h loghighlighter.h loglist.h - lockeddialogbase.h - lockeddialog.h - waitingonclosedialog.h loadmechanism.h installationmanager.h filedialogmemory.h @@ -244,7 +240,6 @@ SET(organizer_HDRS viewmarkingscrollbar.h plugincontainer.h organizercore.h - ilockedwaitingforprocess.h iuserinterface.h instancemanager.h usvfsconnector.h @@ -266,6 +261,8 @@ SET(organizer_HDRS envshortcut.h envwindows.h colortable.h + processrunner.h + uilocker.h shared/windows_error.h shared/error_report.h @@ -289,8 +286,6 @@ SET(organizer_UIS modinfodialog.ui messagedialog.ui mainwindow.ui - lockeddialog.ui - waitingonclosedialog.ui editexecutablesdialog.ui credentialsdialog.ui categoriesdialog.ui @@ -348,6 +343,8 @@ set(core organizercore organizerproxy apiuseraccount + processrunner + uilocker ) set(dialogs @@ -392,12 +389,6 @@ set(executables editexecutablesdialog ) -set(locking - ilockedwaitingforprocess - lockeddialog - lockeddialogbase -) - set(modinfo modinfo modinfobackup @@ -494,7 +485,7 @@ set(widgets ) set(src_filters - application core browser dialogs downloads env executables locking modinfo + application core browser dialogs downloads env executables modinfo modinfo\\dialog modlist plugins previews profiles settings settingsdialog utilities widgets ) diff --git a/src/editexecutablesdialog.cpp b/src/editexecutablesdialog.cpp index 8535b7a7..32b31357 100644 --- a/src/editexecutablesdialog.cpp +++ b/src/editexecutablesdialog.cpp @@ -24,6 +24,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "modlist.h" #include "forcedloaddialog.h" #include "organizercore.h" +#include "spawn.h" #include <QMessageBox> #include <Shellapi.h> @@ -800,7 +801,7 @@ QFileInfo EditExecutablesDialog::browseBinary(const QString& initial) void EditExecutablesDialog::setJarBinary(const QFileInfo& binary) { - auto java = OrganizerCore::findJavaInstallation(binary.absoluteFilePath()); + auto java = spawn::findJavaInstallation(binary.absoluteFilePath()); if (java.isEmpty()) { QMessageBox::information( diff --git a/src/env.cpp b/src/env.cpp index 78b5dc96..507607d1 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -190,19 +190,36 @@ QString setPath(const QString& s) QString get(const QString& name) { - std::wstring s(4000, L' '); + std::size_t bufferSize = 4000; + auto buffer = std::make_unique<wchar_t[]>(bufferSize); DWORD realSize = ::GetEnvironmentVariableW( - name.toStdWString().c_str(), s.data(), static_cast<DWORD>(s.size())); + name.toStdWString().c_str(), + buffer.get(), static_cast<DWORD>(bufferSize)); - if (realSize > s.size()) { - s.resize(realSize); + if (realSize > bufferSize) { + bufferSize = realSize; + buffer = std::make_unique<wchar_t[]>(bufferSize); - ::GetEnvironmentVariableW( - name.toStdWString().c_str(), s.data(), static_cast<DWORD>(s.size())); + realSize = ::GetEnvironmentVariableW( + name.toStdWString().c_str(), + buffer.get(), static_cast<DWORD>(bufferSize)); } - return QString::fromStdWString(s); + if (realSize == 0) { + const auto e = ::GetLastError(); + + // don't log if not found + if (e != ERROR_ENVVAR_NOT_FOUND) { + log::error( + "failed to get environment variable '{}', {}", + name, formatSystemMessage(e)); + } + + return {}; + } + + return QString::fromWCharArray(buffer.get(), realSize); } QString set(const QString& n, const QString& v) @@ -13,23 +13,6 @@ class WindowsInfo; class Metrics; -// used by HandlePtr, calls CloseHandle() as the deleter -// -struct HandleCloser -{ - using pointer = HANDLE; - - void operator()(HANDLE h) - { - if (h != INVALID_HANDLE_VALUE) { - ::CloseHandle(h); - } - } -}; - -using HandlePtr = std::unique_ptr<HANDLE, HandleCloser>; - - // used by DesktopDCPtr, calls ReleaseDC(0, dc) as the deleter // struct DesktopDCReleaser diff --git a/src/envmodule.cpp b/src/envmodule.cpp index 3f1f8912..09593e61 100644 --- a/src/envmodule.cpp +++ b/src/envmodule.cpp @@ -320,19 +320,61 @@ QString Module::getMD5() const } -Process::Process(DWORD pid, QString name) - : m_pid(pid), m_name(std::move(name)) +Process::Process() + : Process(0, 0, {}) { } +Process::Process(HANDLE h) + : Process(::GetProcessId(h), 0, {}) +{ +} + +Process::Process(DWORD pid, DWORD ppid, QString name) + : m_pid(pid), m_ppid(ppid), m_name(std::move(name)) +{ +} + +bool Process::isValid() const +{ + return (m_pid != 0); +} + DWORD Process::pid() const { return m_pid; } +DWORD Process::ppid() const +{ + if (!m_ppid) { + m_ppid = getProcessParentID(m_pid); + } + + return *m_ppid; +} + const QString& Process::name() const { - return m_name; + if (!m_name) { + m_name = getProcessName(m_pid); + } + + return *m_name; +} + +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 {}; + } + + return h; } // whether this process can be accessed; fails if the current process doesn't @@ -353,6 +395,16 @@ bool Process::canAccess() const return true; } +void Process::addChild(Process p) +{ + m_children.push_back(p); +} + +std::vector<Process>& Process::children() +{ + return m_children; +} + std::vector<Module> getLoadedModules() { @@ -408,7 +460,8 @@ std::vector<Module> getLoadedModules() } -std::vector<Process> getRunningProcesses() +template <class F> +void forEachRunningProcess(F&& f) { HandlePtr snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)); @@ -416,7 +469,7 @@ std::vector<Process> getRunningProcesses() { const auto e = GetLastError(); log::error("CreateToolhelp32Snapshot() failed, {}", formatSystemMessage(e)); - return {}; + return; } PROCESSENTRY32 entry = {}; @@ -427,16 +480,14 @@ std::vector<Process> getRunningProcesses() if (!Process32First(snapshot.get(), &entry)) { const auto e = GetLastError(); log::error("Process32First() failed, {}", formatSystemMessage(e)); - return {}; + return; } - std::vector<Process> v; - for (;;) { - v.push_back(Process( - entry.th32ProcessID, - QString::fromStdWString(entry.szExeFile))); + if (!f(entry)) { + break; + } // next process if (!Process32Next(snapshot.get(), &entry)) @@ -450,8 +501,120 @@ std::vector<Process> getRunningProcesses() break; } } +} + +std::vector<Process> getRunningProcesses() +{ + std::vector<Process> v; + + forEachRunningProcess([&](auto&& entry) { + v.push_back(Process( + entry.th32ProcessID, + entry.th32ParentProcessID, + QString::fromStdWString(entry.szExeFile))); + + return true; + }); return v; } +void findChildren(Process& parent, const std::vector<Process>& processes) +{ + for (auto&& p : processes) { + if (p.ppid() == parent.pid()) { + Process child = p; + findChildren(child, processes); + + parent.addChild(child); + } + } +} + +Process getProcessTree(HANDLE parent) +{ + const auto parentPID = ::GetProcessId(parent); + const auto v = getRunningProcesses(); + + Process root; + for (auto&& p : v) { + if (p.pid() == parentPID) { + root = p; + break; + } + } + + if (root.pid() == 0) { + return {}; + } + + findChildren(root, v); + + return root; +} + +QString getProcessName(DWORD pid) +{ + HandlePtr h(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid)); + + if (!h) { + const auto e = GetLastError(); + log::error("can't get name of process {}, {}", pid, formatSystemMessage(e)); + return {}; + } + + return getProcessName(h.get()); +} + +QString getProcessName(HANDLE process) +{ + const QString badName = "unknown"; + + if (process == 0 || process == INVALID_HANDLE_VALUE) { + return badName; + } + + const DWORD bufferSize = MAX_PATH; + wchar_t buffer[bufferSize + 1] = {}; + + const auto realSize = ::GetProcessImageFileNameW(process, buffer, bufferSize); + + if (realSize == 0) { + const auto e = ::GetLastError(); + log::error("GetProcessImageFileNameW() failed, {}", formatSystemMessage(e)); + return badName; + } + + auto s = QString::fromWCharArray(buffer, realSize); + + const auto lastSlash = s.lastIndexOf("\\"); + if (lastSlash != -1) { + s = s.mid(lastSlash + 1); + } + + return s; +} + +DWORD getProcessParentID(DWORD pid) +{ + DWORD ppid = 0; + + forEachRunningProcess([&](auto&& entry) { + if (entry.th32ProcessID == pid) { + ppid = entry.th32ParentProcessID; + return false; + } + + return true; + }); + + return ppid; +} + + +DWORD getProcessParentID(HANDLE handle) +{ + return getProcessParentID(GetProcessId(handle)); +} + } // namespace diff --git a/src/envmodule.h b/src/envmodule.h index deb7520f..d152b840 100644 --- a/src/envmodule.h +++ b/src/envmodule.h @@ -7,6 +7,23 @@ namespace env { +// used by HandlePtr, calls CloseHandle() as the deleter +// +struct HandleCloser +{ + using pointer = HANDLE; + + void operator()(HANDLE h) + { + if (h != INVALID_HANDLE_VALUE) { + ::CloseHandle(h); + } + } +}; + +using HandlePtr = std::unique_ptr<HANDLE, HandleCloser>; + + // represents one module // class Module @@ -101,25 +118,44 @@ private: class Process { public: - Process(DWORD pid, QString name); + Process(); + explicit Process(HANDLE h); + Process(DWORD pid, DWORD ppid, QString name); + bool isValid() const; DWORD pid() const; + DWORD ppid() const; const QString& name() const; + HandlePtr openHandleForWait() const; + // whether this process can be accessed; fails if the current process doesn't // have the proper permissions // bool canAccess() const; + void addChild(Process p); + std::vector<Process>& children(); + private: DWORD m_pid; - QString m_name; + mutable std::optional<DWORD> m_ppid; + mutable std::optional<QString> m_name; + std::vector<Process> m_children; }; std::vector<Process> getRunningProcesses(); std::vector<Module> getLoadedModules(); +Process getProcessTree(HANDLE parent); + +QString getProcessName(DWORD pid); +QString getProcessName(HANDLE process); + +DWORD getProcessParentID(DWORD pid); +DWORD getProcessParentID(HANDLE handle); + } // namespace env #endif // ENV_MODULE_H diff --git a/src/envsecurity.cpp b/src/envsecurity.cpp index 786291c6..6d62728b 100644 --- a/src/envsecurity.cpp +++ b/src/envsecurity.cpp @@ -1,5 +1,6 @@ #include "envsecurity.h" #include "env.h" +#include "envmodule.h" #include <utility.h> #include <log.h> diff --git a/src/envwindows.cpp b/src/envwindows.cpp index 3932a9b5..98e78a3e 100644 --- a/src/envwindows.cpp +++ b/src/envwindows.cpp @@ -1,5 +1,6 @@ #include "envwindows.h" #include "env.h" +#include "envmodule.h" #include <utility.h> #include <log.h> diff --git a/src/ilockedwaitingforprocess.h b/src/ilockedwaitingforprocess.h deleted file mode 100644 index 9475ddb9..00000000 --- a/src/ilockedwaitingforprocess.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef ILOCKEDWAITINGFORPROCESS_H -#define ILOCKEDWAITINGFORPROCESS_H - -class QString; - -class ILockedWaitingForProcess -{ -public: - virtual bool unlockForced() const = 0; - virtual void setProcessName(QString const &) = 0; -}; - -#endif // ILOCKEDWAITINGFORPROCESS_H diff --git a/src/iuserinterface.h b/src/iuserinterface.h index a309ed9b..aa48194f 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -3,13 +3,12 @@ #include "modinfodialogfwd.h"
-#include "ilockedwaitingforprocess.h"
#include <iplugintool.h>
#include <ipluginmodpage.h>
#include <delayedfilewriter.h>
-
#include <QMenu>
+
class IUserInterface
{
public:
@@ -31,8 +30,7 @@ public: virtual MOBase::DelayedFileWriterBase &archivesWriter() = 0;
- virtual ILockedWaitingForProcess* lock() = 0;
- virtual void unlock() = 0;
+ virtual QWidget* qtWidget() = 0;
};
#endif // IUSERINTERFACE_H
diff --git a/src/lockeddialog.cpp b/src/lockeddialog.cpp deleted file mode 100644 index 143d5838..00000000 --- a/src/lockeddialog.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#include "lockeddialog.h"
-#include "ui_lockeddialog.h"
-
-#include <QPoint>
-#include <QResizeEvent>
-#include <QWidget>
-#include <Qt> // for Qt::FramelessWindowHint, etc
-
-LockedDialog::LockedDialog(QWidget *parent, bool unlockByButton)
- : LockedDialogBase(parent, !unlockByButton)
- , ui(new Ui::LockedDialog)
-{
- ui->setupUi(this);
-
- // Supposedly the Qt::CustomizeWindowHint should use a customized window
- // allowing us to select if there is a close button. In practice this doesn't
- // seem to work. We will ignore pressing the close button if unlockByButton == true
- Qt::WindowFlags flags =
- this->windowFlags() | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint;
- if (m_allowClose)
- flags |= Qt::WindowCloseButtonHint;
- this->setWindowFlags(flags);
-
- if (!unlockByButton)
- {
- ui->unlockButton->hide();
- ui->verticalLayout->addItem(
- new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
- }
-}
-
-LockedDialog::~LockedDialog()
-{
- delete ui;
-}
-
-
-void LockedDialog::setProcessName(const QString &name)
-{
- ui->processLabel->setText(name);
-}
-
-void LockedDialog::on_unlockButton_clicked()
-{
- unlock();
-}
-
-void LockedDialog::unlock() {
- LockedDialogBase::unlock();
- ui->label->setText("unlocking may take a few seconds");
- ui->unlockButton->setEnabled(false);
-}
diff --git a/src/lockeddialog.h b/src/lockeddialog.h deleted file mode 100644 index 36c16429..00000000 --- a/src/lockeddialog.h +++ /dev/null @@ -1,57 +0,0 @@ -/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-Mod Organizer is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#pragma once
-
-#include "lockeddialogbase.h"
-
-namespace Ui {
- class LockedDialog;
-}
-
-/**
- * a small borderless dialog displayed while the Mod Organizer UI is locked
- * The dialog contains only a label and a button to force the UI to be unlocked
- *
- * The UI gets locked while running external applications since they may modify the
- * data on which Mod Organizer works. After the UI is unlocked (manually or after the
- * external application closed) MO will refresh all of its data sources
- **/
-class LockedDialog : public LockedDialogBase
-{
- Q_OBJECT
-
-public:
- explicit LockedDialog(QWidget *parent = 0, bool unlockByButton = false);
- ~LockedDialog();
-
- void setProcessName(const QString &name) override;
-
-protected:
-
- void unlock() override;
-
-private slots:
-
- void on_unlockButton_clicked();
-
-private:
-
- Ui::LockedDialog *ui;
-};
diff --git a/src/lockeddialog.ui b/src/lockeddialog.ui deleted file mode 100644 index 0ec2e467..00000000 --- a/src/lockeddialog.ui +++ /dev/null @@ -1,98 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>LockedDialog</class>
- <widget class="QDialog" name="LockedDialog">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>317</width>
- <height>151</height>
- </rect>
- </property>
- <property name="windowTitle">
- <string>Running virtualized processes</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0">
- <item>
- <widget class="QLabel" name="label">
- <property name="toolTip">
- <string>This dialog should disappear automatically if the application/game is done. Click unlock if it didn't.</string>
- </property>
- <property name="text">
- <string>MO is locked while the executable is running.</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer1">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="processLabel">
- <property name="font">
- <font>
- <italic>true</italic>
- </font>
- </property>
- <property name="styleSheet">
- <string notr="true">color: grey;</string>
- </property>
- <property name="text">
- <string/>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer2">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="unlockButton">
- <property name="text">
- <string>Unlock</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer3">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <height>10</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/lockeddialogbase.cpp b/src/lockeddialogbase.cpp deleted file mode 100644 index b18f7429..00000000 --- a/src/lockeddialogbase.cpp +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. -*/ - -#include "lockeddialogbase.h" - -#include <QPoint> -#include <QResizeEvent> -#include <QWidget> -#include <Qt> // for Qt::FramelessWindowHint, etc - -LockedDialogBase::LockedDialogBase(QWidget *parent, bool allowClose) - : QDialog(parent) - , m_Unlocked(false) - , m_Canceled(false) - , m_allowClose(allowClose) -{ - if (parent != nullptr) { - QPoint position = parent->mapToGlobal(QPoint(parent->width() / 2, parent->height() / 2)); - position.rx() -= this->width() / 2; - position.ry() -= this->height() / 2; - move(position); - } -} - -void LockedDialogBase::resizeEvent(QResizeEvent *event) -{ - QWidget *par = parentWidget(); - if (par != nullptr) { - QPoint position = par->mapToGlobal(QPoint(par->width() / 2, par->height() / 2)); - position.rx() -= event->size().width() / 2; - position.ry() -= event->size().height() / 2; - move(position); - } -} - -void LockedDialogBase::reject() -{ - if (m_allowClose) - unlock(); -} - -bool LockedDialogBase::unlockForced() const { - return m_Unlocked; -} - -bool LockedDialogBase::canceled() const { - return m_Canceled; -} - -void LockedDialogBase::unlock() { - m_Unlocked = true; -} - -void LockedDialogBase::cancel() { - m_Canceled = true; -} - diff --git a/src/lockeddialogbase.h b/src/lockeddialogbase.h deleted file mode 100644 index 3c974a38..00000000 --- a/src/lockeddialogbase.h +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. -*/ - -#pragma once - -#include "ilockedwaitingforprocess.h" -#include <QDialog> // for QDialog -#include <QObject> // for Q_OBJECT, slots -#include <QString> // for QString - -class QResizeEvent; -class QWidget; - -/** - * a small borderless dialog displayed while the Mod Organizer UI is locked - * The dialog contains only a label and a button to force the UI to be unlocked - * - * The UI gets locked while running external applications since they may modify the - * data on which Mod Organizer works. After the UI is unlocked (manually or after the - * external application closed) MO will refresh all of its data sources - **/ -class LockedDialogBase : public QDialog, public ILockedWaitingForProcess -{ - Q_OBJECT - -public: - explicit LockedDialogBase(QWidget *parent, bool allowClose); - - bool unlockForced() const override; - - virtual bool canceled() const; - -protected: - - virtual void resizeEvent(QResizeEvent *event); - - virtual void reject(); - - virtual void unlock(); - - virtual void cancel(); - - bool m_Unlocked; - bool m_Canceled; - bool m_allowClose; -}; diff --git a/src/main.cpp b/src/main.cpp index 776c3775..02347ee3 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.runShortcut(shortcut); + organizer.processRunner() + .setFromShortcut(shortcut) + .setWaitForCompletion() + .run(); + return 0; } catch (const std::exception &e) { @@ -649,14 +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.startApplication(exeName, arguments, QString(), QString()); + + try + { + // pass the remaining parameters to the binary + organizer.processRunner() + .setFromFileOrExecutable(exeName, arguments) + .setWaitForCompletion() + .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/mainwindow.cpp b/src/mainwindow.cpp index 12ed40b3..9453f07c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -57,8 +57,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "downloadlistwidget.h" #include "messagedialog.h" #include "installationmanager.h" -#include "lockeddialog.h" -#include "waitingonclosedialog.h" #include "downloadlistsortproxy.h" #include "motddialog.h" #include "filedialogmemory.h" @@ -411,7 +409,7 @@ MainWindow::MainWindow(Settings &settings m_Tutorial.expose("modList", m_OrganizerCore.modList()); m_Tutorial.expose("espList", m_OrganizerCore.pluginList()); - m_OrganizerCore.setUserInterface(this, this); + m_OrganizerCore.setUserInterface(this); for (const QString &fileName : m_PluginContainer.pluginFileNames()) { installTranslator(QFileInfo(fileName).baseName()); } @@ -595,7 +593,7 @@ MainWindow::~MainWindow() cleanup(); m_PluginContainer.setUserInterface(nullptr, nullptr); - m_OrganizerCore.setUserInterface(nullptr, nullptr); + m_OrganizerCore.setUserInterface(nullptr); m_IntegratedBrowser.close(); delete ui; } catch (std::exception &e) { @@ -1311,16 +1309,9 @@ bool MainWindow::canExit() } } - std::vector<QString> hiddenList; - hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); - HANDLE injected_process_still_running = m_OrganizerCore.findAndOpenAUSVFSProcess(hiddenList, GetCurrentProcessId()); - if (injected_process_still_running != INVALID_HANDLE_VALUE) - { - m_exitAfterWait = true; - m_OrganizerCore.waitForApplication(injected_process_still_running); - if (!m_exitAfterWait) { // if operation cancelled - return false; - } + const auto r = m_OrganizerCore.waitForAllUSVFSProcesses(); + if (r == ProcessRunner::Cancelled) { + return false; } setCursor(Qt::WaitCursor); @@ -1542,26 +1533,12 @@ void MainWindow::startExeAction() } action->setEnabled(false); - const Executable& exe = *itor; - auto& profile = *m_OrganizerCore.currentProfile(); - - QString customOverwrite = profile.setting("custom_overwrites", exe.title()).toString(); - auto forcedLibraries = profile.determineForcedLibraries(exe.title()); - - if (!profile.forcedLibrariesEnabled(exe.title())) { - forcedLibraries.clear(); - } - - m_OrganizerCore.spawnBinary( - exe.binaryInfo(), exe.arguments(), - exe.workingDirectory().length() != 0 - ? exe.workingDirectory() - : exe.binaryInfo().absolutePath(), - exe.steamAppID(), - customOverwrite, - forcedLibraries); - action->setEnabled(true); + Guard g([&]{ action->setEnabled(true); }); + m_OrganizerCore.processRunner() + .setFromExecutable(*itor) + .setWaitForCompletion(ProcessRunner::Refresh) + .run(); } void MainWindow::activateSelectedProfile() @@ -2282,40 +2259,9 @@ 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() +QWidget* MainWindow::qtWidget() { - //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); - } + return this; } void MainWindow::on_btnRefreshData_clicked() @@ -2367,41 +2313,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.spawnBinary( - 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) @@ -5313,43 +5236,42 @@ void MainWindow::addAsExecutable() return; } - using FileExecutionTypes = OrganizerCore::FileExecutionTypes; - - QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - FileExecutionTypes type; + const QFileInfo target(m_ContextItem->data(0, Qt::UserRole).toString()); + const auto fec = spawn::getFileExecutionContext(this, target); - if (!OrganizerCore::getFileExecutionContext(this, targetInfo, binaryInfo, arguments, type)) { - return; - } - - switch (type) + switch (fec.type) { - case FileExecutionTypes::Executable: { - QString name = QInputDialog::getText(this, tr("Enter Name"), - tr("Please enter a name for the executable"), QLineEdit::Normal, - targetInfo.completeBaseName()); - - if (!name.isEmpty()) { - //Note: If this already exists, you'll lose custom settings - m_OrganizerCore.executablesList()->setExecutable(Executable() - .title(name) - .binaryInfo(binaryInfo) - .arguments(arguments) - .workingDirectory(targetInfo.absolutePath())); + case spawn::FileExecutionTypes::Executable: + { + const QString name = QInputDialog::getText( + this, tr("Enter Name"), + tr("Enter a name for the executable"), + QLineEdit::Normal, + target.completeBaseName()); - refreshExecutablesList(); - } + if (!name.isEmpty()) { + //Note: If this already exists, you'll lose custom settings + m_OrganizerCore.executablesList()->setExecutable(Executable() + .title(name) + .binaryInfo(fec.binary) + .arguments(fec.arguments) + .workingDirectory(target.absolutePath())); - break; + refreshExecutablesList(); } - case FileExecutionTypes::Other: // fall-through - default: { - QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable.")); - break; - } + break; + } + + case spawn::FileExecutionTypes::Other: // fall-through + default: + { + QMessageBox::information( + this, tr("Not an executable"), + tr("This is not a recognized executable.")); + + break; + } } } @@ -5476,8 +5398,13 @@ void MainWindow::openDataFile() return; } - QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); - m_OrganizerCore.executeFileVirtualized(this, targetInfo); + const QString path = m_ContextItem->data(0, Qt::UserRole).toString(); + const QFileInfo targetInfo(path); + + m_OrganizerCore.processRunner() + .setFromFile(this, targetInfo) + .setWaitForCompletion(ProcessRunner::Refresh) + .run(); } void MainWindow::openDataOriginExplorer_clicked() diff --git a/src/mainwindow.h b/src/mainwindow.h index dbbd0bd9..c80287b2 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -38,7 +38,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. //when I get round to cleaning up main.cpp class Executable; class CategoryFactory; -class LockedDialogBase; class OrganizerCore; class PluginListSortProxy; @@ -118,8 +117,7 @@ public: void processUpdates(Settings& settings); - virtual ILockedWaitingForProcess* lock() override; - virtual void unlock() override; + QWidget* qtWidget() override; bool addProfile(); void updateBSAList(const QStringList &defaultArchives, const QStringList &activeArchives); @@ -380,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/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index 36559a75..d37f068c 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().executeFileVirtualized(parentWidget(), item->fileName()); + core().processRunner() + .setFromFile(parentWidget(), item->fileName()) + .setWaitForCompletion() + .run(); + return true; }); } diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 5613e8ce..9ceb149e 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -1,5 +1,4 @@ #include "organizercore.h" - #include "delayedfilewriter.h" #include "guessedvalue.h" #include "imodinterface.h" @@ -30,10 +29,11 @@ #include "appconfig.h" #include <report.h> #include <questionboxmemory.h> -#include "lockeddialog.h" #include "instancemanager.h" #include <scriptextender.h> #include "previewdialog.h" +#include "env.h" +#include "envmodule.h" #include <QApplication> #include <QCoreApplication> @@ -74,47 +74,6 @@ using namespace MOBase; //static CrashDumpsType OrganizerCore::m_globalCrashDumpsType = CrashDumpsType::None; -static std::wstring getProcessName(HANDLE process) -{ - wchar_t buffer[MAX_PATH]; - const wchar_t *fileName = L"unknown"; - - if (process == nullptr) return fileName; - - if (::GetProcessImageFileNameW(process, buffer, MAX_PATH) != 0) { - fileName = wcsrchr(buffer, L'\\'); - if (fileName == nullptr) { - fileName = buffer; - } - else { - fileName += 1; - } - } - - return fileName; -} - -// Get parent PID for the given process, return 0 on failure -static DWORD getProcessParentID(DWORD pid) -{ - HANDLE th = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - PROCESSENTRY32 pe = { 0 }; - pe.dwSize = sizeof(PROCESSENTRY32); - - DWORD res = 0; - if (Process32First(th, &pe)) - do { - if (pe.th32ProcessID == pid) { - res = pe.th32ParentProcessID; - break; - } - } while (Process32Next(th, &pe)); - - CloseHandle(th); - - return res; -} - template <typename InputIterator> QStringList toStringList(InputIterator current, InputIterator end) { @@ -190,7 +149,7 @@ OrganizerCore::~OrganizerCore() m_RefresherThread.exit(); m_RefresherThread.wait(); - prepareStart(); + saveCurrentProfile(); // profile has to be cleaned up before the modinfo-buffer is cleared delete m_CurrentProfile; @@ -249,44 +208,49 @@ void OrganizerCore::updateExecutablesList() m_PluginContainer, m_Settings.interface().displayForeign(), managedGame()); } -void OrganizerCore::setUserInterface(IUserInterface *userInterface, - QWidget *widget) +void OrganizerCore::setUserInterface(IUserInterface* ui) { storeSettings(); - m_UserInterface = userInterface; + m_UserInterface = ui; + + QWidget* w = nullptr; + if (m_UserInterface) { + w = m_UserInterface->qtWidget(); + } - if (widget != nullptr) { - connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), widget, + if (w) { + connect(&m_ModList, SIGNAL(modlistChanged(QModelIndex, int)), w, SLOT(modlistChanged(QModelIndex, int))); - connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), widget, + connect(&m_ModList, SIGNAL(modlistChanged(QModelIndexList, int)), w, SLOT(modlistChanged(QModelIndexList, int))); - connect(&m_ModList, SIGNAL(showMessage(QString)), widget, + connect(&m_ModList, SIGNAL(showMessage(QString)), w, SLOT(showMessage(QString))); - connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), widget, + connect(&m_ModList, SIGNAL(modRenamed(QString, QString)), w, SLOT(modRenamed(QString, QString))); - connect(&m_ModList, SIGNAL(modUninstalled(QString)), widget, + connect(&m_ModList, SIGNAL(modUninstalled(QString)), w, SLOT(modRemoved(QString))); - connect(&m_ModList, SIGNAL(removeSelectedMods()), widget, + connect(&m_ModList, SIGNAL(removeSelectedMods()), w, SLOT(removeMod_clicked())); - connect(&m_ModList, SIGNAL(clearOverwrite()), widget, + connect(&m_ModList, SIGNAL(clearOverwrite()), w, SLOT(clearOverwrite())); - connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), widget, + connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), w, SLOT(displayColumnSelection(QPoint))); - connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), widget, + connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), w, SLOT(fileMoved(QString, QString, QString))); - connect(&m_ModList, SIGNAL(modorder_changed()), widget, + connect(&m_ModList, SIGNAL(modorder_changed()), w, SLOT(modorder_changed())); - connect(&m_PluginList, SIGNAL(writePluginsList()), widget, + connect(&m_PluginList, SIGNAL(writePluginsList()), w, SLOT(esplist_changed())); - connect(&m_PluginList, SIGNAL(esplist_changed()), widget, + connect(&m_PluginList, SIGNAL(esplist_changed()), w, SLOT(esplist_changed())); - connect(&m_DownloadManager, SIGNAL(showMessage(QString)), widget, + connect(&m_DownloadManager, SIGNAL(showMessage(QString)), w, SLOT(showMessage(QString))); } - m_InstallationManager.setParentWidget(widget); - m_Updater.setUserInterface(widget); + m_InstallationManager.setParentWidget(w); + m_Updater.setUserInterface(w); + m_UILocker.setUserInterface(w); checkForUpdates(); } @@ -390,8 +354,12 @@ void OrganizerCore::downloadRequestedNXM(const QString &url) void OrganizerCore::externalMessage(const QString &message) { if (MOShortcut moshortcut{ message } ) { - if(moshortcut.hasExecutable()) - runShortcut(moshortcut); + if(moshortcut.hasExecutable()) { + processRunner() + .setFromShortcut(moshortcut) + .setWaitForCompletion(ProcessRunner::Refresh) + .run(); + } } else if (isNxmLink(message)) { MessageDialog::showMessage(tr("Download started"), qApp->activeWindow()); @@ -977,110 +945,6 @@ QStringList OrganizerCore::modsSortedByProfilePriority() const return res; } -QString OrganizerCore::findJavaInstallation(const QString& jarFile) -{ - if (!jarFile.isEmpty()) { - // try to find java automatically based on the given jar file - std::wstring jarFileW = jarFile.toStdWString(); - - WCHAR buffer[MAX_PATH]; - if (::FindExecutableW(jarFileW.c_str(), nullptr, buffer) > (HINSTANCE)32) { - DWORD binaryType = 0UL; - if (!::GetBinaryTypeW(buffer, &binaryType)) { - log::debug( - "failed to determine binary type of \"{}\": {}", - QString::fromWCharArray(buffer), ::GetLastError()); - } else if (binaryType == SCS_32BIT_BINARY || binaryType == SCS_64BIT_BINARY) { - return QString::fromWCharArray(buffer); - } - } - } - - // second attempt: look to the registry - QSettings reg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); - if (reg.contains("CurrentVersion")) { - QString currentVersion = reg.value("CurrentVersion").toString(); - return reg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); - } - - // not found - return {}; -} - -bool OrganizerCore::getFileExecutionContext( - QWidget* parent, const QFileInfo &targetInfo, - QFileInfo &binaryInfo, QString &arguments, FileExecutionTypes& type) -{ - QString extension = targetInfo.suffix(); - if ((extension.compare("cmd", Qt::CaseInsensitive) == 0) || - (extension.compare("com", Qt::CaseInsensitive) == 0) || - (extension.compare("bat", Qt::CaseInsensitive) == 0)) { - binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); - arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - type = FileExecutionTypes::Executable; - return true; - } else if (extension.compare("exe", Qt::CaseInsensitive) == 0) { - binaryInfo = targetInfo; - type = FileExecutionTypes::Executable; - return true; - } else if (extension.compare("jar", Qt::CaseInsensitive) == 0) { - auto java = findJavaInstallation(targetInfo.absoluteFilePath()); - - if (java.isEmpty()) { - java = QFileDialog::getOpenFileName( - parent, QObject::tr("Select binary"), - QString(), QObject::tr("Binary") + " (*.exe)"); - } - - if (java.isEmpty()) { - return false; - } - - binaryInfo = QFileInfo(java); - arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - type = FileExecutionTypes::Executable; - - return true; - } else { - type = FileExecutionTypes::Other; - return true; - } -} - -bool OrganizerCore::executeFileVirtualized( - QWidget* parent, const QFileInfo& targetInfo) -{ - QFileInfo binaryInfo; - QString arguments; - FileExecutionTypes type; - - if (!getFileExecutionContext(parent, targetInfo, binaryInfo, arguments, type)) { - return false; - } - - switch (type) - { - case FileExecutionTypes::Executable: { - spawnBinaryDirect( - binaryInfo, arguments, currentProfile()->name(), - targetInfo.absolutePath(), "", ""); - - return true; - } - - case FileExecutionTypes::Other: { - ::ShellExecuteW(nullptr, L"open", - ToWString(targetInfo.absoluteFilePath()).c_str(), - nullptr, nullptr, SW_SHOWNORMAL); - - return true; - } - } - - // nop - return false; -} - bool OrganizerCore::previewFileWithAlternatives( QWidget* parent, QString fileName, int selectedOrigin) { @@ -1209,475 +1073,6 @@ bool OrganizerCore::previewFile( return true; } -void OrganizerCore::spawnBinary(const QFileInfo &binary, - const QString &arguments, - const QDir ¤tDirectory, - const QString &steamAppID, - const QString &customOverwrite, - const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries) -{ - DWORD processExitCode = 0; - HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->name(), currentDirectory, steamAppID, customOverwrite, forcedLibraries, &processExitCode); - if (processHandle != INVALID_HANDLE_VALUE) { - refreshDirectoryStructure(); - // need to remove our stored load order because it may be outdated if a foreign tool changed the - // file time. After removing that file, refreshESPList will use the file time as the order - if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) { - log::debug("removing loadorder.txt"); - QFile::remove(m_CurrentProfile->getLoadOrderFileName()); - } - refreshDirectoryStructure(); - - refreshESPList(true); - savePluginList(); - - //These callbacks should not fiddle with directoy structure and ESPs. - m_FinishedRun(binary.absoluteFilePath(), processExitCode); - } -} - -HANDLE OrganizerCore::spawnBinaryDirect(const QFileInfo &binary, - const QString &arguments, - const QString &profileName, - const QDir ¤tDirectory, - const QString &steamAppID, - const QString &customOverwrite, - const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries, - LPDWORD exitCode) -{ - HANDLE processHandle = spawnBinaryProcess(binary, arguments, profileName, currentDirectory, steamAppID, customOverwrite, forcedLibraries); - if (Settings::instance().interface().lockGUI() && processHandle != INVALID_HANDLE_VALUE) { - std::unique_ptr<LockedDialog> dlg; - ILockedWaitingForProcess* uilock = nullptr; - - if (m_UserInterface != nullptr) { - uilock = m_UserInterface->lock(); - } - else { - // i.e. when running command line shortcuts there is no m_UserInterface - dlg.reset(new LockedDialog); - dlg->show(); - dlg->setEnabled(true); - uilock = dlg.get(); - } - - ON_BLOCK_EXIT([&]() { - if (m_UserInterface != nullptr) { - m_UserInterface->unlock(); - } }); - - DWORD ignoreExitCode; - waitForProcessCompletion(processHandle, exitCode ? exitCode : &ignoreExitCode, uilock); - cycleDiagnostics(); - } - - return processHandle; -} - - -HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary, - const QString &arguments, - const QString &profileName, - const QDir ¤tDirectory, - const QString &steamAppID, - const QString &customOverwrite, - const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries) -{ - spawn::SpawnParameters sp; - sp.binary = binary; - sp.arguments = arguments; - sp.currentDirectory = currentDirectory; - sp.hooked = true; - - prepareStart(); - - QWidget *window = qApp->activeWindow(); - if ((window != nullptr) && (!window->isVisible())) { - window = nullptr; - } - - if (!spawn::checkBinary(window, sp)) { - return INVALID_HANDLE_VALUE; - } - - if (!spawn::checkSteam(window, sp, managedGame()->gameDirectory(), steamAppID, m_Settings)) { - return INVALID_HANDLE_VALUE; - } - - while (m_DirectoryUpdate) { - ::Sleep(100); - QCoreApplication::processEvents(); - } - - // need to make sure all data is saved before we start the application - if (m_CurrentProfile != nullptr) { - m_CurrentProfile->writeModlistNow(true); - } - - // TODO: should also pass arguments - if (m_AboutToRun(binary.absoluteFilePath())) { - try { - m_USVFS.updateMapping(fileMapping(profileName, customOverwrite)); - m_USVFS.updateForcedLibraries(forcedLibraries); - - } catch (const UsvfsConnectorException &e) { - log::debug(e.what()); - return INVALID_HANDLE_VALUE; - } catch (const std::exception &e) { - QMessageBox::warning(window, tr("Error"), e.what()); - return INVALID_HANDLE_VALUE; - } - - if (!spawn::checkEnvironment(window, sp)) { - return INVALID_HANDLE_VALUE; - } - - if (!spawn::checkBlacklist(window, sp, m_Settings)) { - return INVALID_HANDLE_VALUE; - } - - 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 = currentDirectory.absolutePath(); - bool virtualizedCwd = cwdPath.startsWith(modsPath, Qt::CaseInsensitive); - QString binPath = 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 = m_GamePlugin->dataDirectory().absolutePath(); - if (cwdOffset >= 0) - cwdPath += adjustedCwd; - - } - - if (virtualizedBin) { - int binOffset = binPath.indexOf('/', modsPath.length() + 1); - QString adjustedBin = binPath.mid(binOffset, -1); - binPath = m_GamePlugin->dataDirectory().absolutePath(); - if (binOffset >= 0) - binPath += adjustedBin; - } - - QString cmdline - = QString("launch \"%1\" \"%2\" %3") - .arg(QDir::toNativeSeparators(cwdPath), - QDir::toNativeSeparators(binPath), arguments); - - sp.binary = QFileInfo(QCoreApplication::applicationFilePath()); - sp.arguments = cmdline; - sp.currentDirectory.setPath(QCoreApplication::applicationDirPath()); - - return spawn::startBinary(window, sp); - } else { - log::debug("Spawning direct process <{}, {}, {}>", binPath, arguments, cwdPath); - return spawn::startBinary(window, sp); - } - } else { - log::debug("start of \"{}\" canceled by plugin", binary.absoluteFilePath()); - return INVALID_HANDLE_VALUE; - } -} - -HANDLE OrganizerCore::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_ExecutablesList.get(shortcut.executable()); - - auto forcedLibaries = m_CurrentProfile->determineForcedLibraries(shortcut.executable()); - if (!m_CurrentProfile->forcedLibrariesEnabled(shortcut.executable())) { - forcedLibaries.clear(); - } - - return spawnBinaryDirect( - exe.binaryInfo(), exe.arguments(), - m_CurrentProfile->name(), - exe.workingDirectory().length() != 0 - ? exe.workingDirectory() - : exe.binaryInfo().absolutePath(), - exe.steamAppID(), - "", - forcedLibaries); -} - -HANDLE OrganizerCore::startApplication(const QString &executable, - const QStringList &args, - const QString &cwd, - const QString &profile, - const QString &forcedCustomOverwrite, - bool ignoreCustomOverwrite) -{ - QFileInfo binary; - QString arguments = args.join(" "); - QString currentDirectory = cwd; - QString profileName = profile; - if (profile.length() == 0) { - if (m_CurrentProfile != nullptr) { - profileName = m_CurrentProfile->name(); - } else { - throw MyException(tr("No profile set")); - } - } - QString steamAppID; - QString customOverwrite; - QList<ExecutableForcedLoadSetting> forcedLibraries; - if (executable.contains('\\') || executable.contains('/')) { - // file path - - binary = QFileInfo(executable); - if (binary.isRelative()) { - // relative path, should be relative to game directory - binary = QFileInfo( - managedGame()->gameDirectory().absoluteFilePath(executable)); - } - if (cwd.length() == 0) { - currentDirectory = binary.absolutePath(); - } - try { - const Executable &exe = m_ExecutablesList.getByBinary(binary); - steamAppID = exe.steamAppID(); - customOverwrite - = m_CurrentProfile->setting("custom_overwrites", exe.title()) - .toString(); - if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title()); - } - } catch (const std::runtime_error &) { - // nop - } - } else { - // only a file name, search executables list - try { - const Executable &exe = m_ExecutablesList.get(executable); - steamAppID = exe.steamAppID(); - customOverwrite - = m_CurrentProfile->setting("custom_overwrites", exe.title()) - .toString(); - if (m_CurrentProfile->forcedLibrariesEnabled(exe.title())) { - forcedLibraries = m_CurrentProfile->determineForcedLibraries(exe.title()); - } - if (arguments == "") { - arguments = exe.arguments(); - } - binary = exe.binaryInfo(); - if (cwd.length() == 0) { - 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 spawnBinaryDirect(binary, - arguments, - profileName, - currentDirectory, - steamAppID, - customOverwrite, - forcedLibraries); -} - -bool OrganizerCore::waitForApplication(HANDLE handle, LPDWORD exitCode) -{ - if (!Settings::instance().interface().lockGUI()) - return true; - - ILockedWaitingForProcess* uilock = nullptr; - if (m_UserInterface != nullptr) { - uilock = m_UserInterface->lock(); - } - - ON_BLOCK_EXIT([&] () { - if (m_UserInterface != nullptr) { - m_UserInterface->unlock(); - } }); - return waitForProcessCompletion(handle, exitCode, uilock); -} - -bool OrganizerCore::waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock) -{ - bool originalHandle = true; - bool newHandle = true; - bool uiunlocked = false; - - DWORD currentPID = 0; - QString processName; - auto waitForChildUntil = GetTickCount64(); - if (handle != INVALID_HANDLE_VALUE) { - currentPID = GetProcessId(handle); - processName = QString::fromStdWString(getProcessName(handle)); - } - - // Certain process names we wish to "hide" for aesthetic reason: - bool waitingOnHidden = false; - std::vector<QString> hiddenList; - hiddenList.push_back(QFileInfo(QCoreApplication::applicationFilePath()).fileName()); - for (QString hide : hiddenList) - if (processName.contains(hide, Qt::CaseInsensitive)) - waitingOnHidden = true; - // The main reason for adding the hidden list is to hide the MO proxy we use to spawn virtualized processes. - // On the one hand we want to display the real executable without it feeling laggy, on the other we don't want - // to requery processes all the time if for some reason we are waiting on hidden processes and find no "unhidden" - // process. For this reason we use exponential backoff and also start with a delibrately low value to improve - // the responsiveness of the initial update - DWORD64 nextHiddenCheck = GetTickCount64(); - DWORD64 nextHiddenCheckDelay = 50; - - constexpr DWORD INPUT_EVENT = WAIT_OBJECT_0 + 1; - DWORD res = WAIT_TIMEOUT; - while (handle != INVALID_HANDLE_VALUE && (newHandle || res == WAIT_TIMEOUT || res == INPUT_EVENT)) - { - if (newHandle) { - processName += QString(" (%1)").arg(currentPID); - if (uilock) - uilock->setProcessName(processName); - - log::debug( - "Waiting for {} process completion: {}", - (originalHandle ? "spawned" : "usvfs"), processName); - - newHandle = false; - } - - // Wait for a an event on the handle, a key press, mouse click or timeout - res = MsgWaitForMultipleObjects(1, &handle, FALSE, 200, QS_KEY | QS_MOUSEBUTTON); - if (res == WAIT_FAILED) { - log::warn("Failed waiting for process completion : MsgWaitForMultipleObjects WAIT_FAILED {}", GetLastError()); - break; - } - - // keep processing events so the app doesn't appear dead - QCoreApplication::sendPostedEvents(); - QCoreApplication::processEvents(); - - if (uilock && uilock->unlockForced()) { - uiunlocked = true; - break; - } - - if (res == WAIT_OBJECT_0) { - // process we were waiting on has completed - if (originalHandle && exitCode && !::GetExitCodeProcess(handle, exitCode)) - log::warn("Failed getting exit code of complete process: {}", GetLastError()); - CloseHandle(handle); - handle = INVALID_HANDLE_VALUE; - originalHandle = false; - // if the previous process spawned a child process and immediately exits we may miss it if we check immediately - waitForChildUntil = GetTickCount64() + 800; - } - - // search for another process to wait on if either: - // 1. we just completed waiting for a process and need to find/wait for an inject child - // 2. we are currently waiting on a hidden process so periodically check if there is a non-hidden process to wait on - bool firstIteration = true; - while ((handle == INVALID_HANDLE_VALUE && GetTickCount64() <= waitForChildUntil) - || (waitingOnHidden && GetTickCount64() >= nextHiddenCheck)) - { - if (firstIteration) - firstIteration = false; - else { - QThread::msleep(200); - QCoreApplication::sendPostedEvents(); - QCoreApplication::processEvents(); - } - - // search if there is another usvfs process active - handle = findAndOpenAUSVFSProcess(hiddenList, currentPID); - waitingOnHidden = false; - newHandle = handle != INVALID_HANDLE_VALUE; - if (newHandle) { - currentPID = GetProcessId(handle); - processName = QString::fromStdWString(getProcessName(handle)); - for (QString hide : hiddenList) - if (processName.contains(hide, Qt::CaseInsensitive)) - waitingOnHidden = true; - } - if (waitingOnHidden) { - nextHiddenCheck = GetTickCount64() + nextHiddenCheckDelay; - nextHiddenCheckDelay = std::min(nextHiddenCheckDelay * 2, (DWORD64) 2000); - } - else { - nextHiddenCheck = GetTickCount64(); - nextHiddenCheckDelay = 200; - } - } - } - - if (res == WAIT_OBJECT_0) - log::debug("Waiting for process completion successfull"); - else if (uiunlocked) - log::debug("Waiting for process completion aborted by UI"); - else - log::debug("Waiting for process completion not successfull: {}", res); - - if (handle != INVALID_HANDLE_VALUE) - ::CloseHandle(handle); - - return res == WAIT_OBJECT_0; -} - -HANDLE OrganizerCore::findAndOpenAUSVFSProcess(const std::vector<QString>& hiddenList, DWORD preferedParentPid) { - // for practical reasons a querySize of 1 is probably enough, we use a larger query as a heuristics - // to find a more "aesthetic injected processes (attempting to comply to hiddenList and preferedParentPid) - constexpr size_t querySize = 100; - DWORD pids[querySize]; - size_t found = querySize; - if (!::GetVFSProcessList(&found, pids)) { - log::warn("Failed seeking USVFS processes : GetVFSProcessList failed?!"); - return INVALID_HANDLE_VALUE; - } - - HANDLE best_match = INVALID_HANDLE_VALUE; - bool best_match_hidden = true; - for (size_t i = 0; i < found; ++i) { - if (pids[i] == GetCurrentProcessId()) - continue; // obviously don't wait for MO process - - HANDLE handle = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pids[i]); - if (handle == INVALID_HANDLE_VALUE) { - log::warn("Failed opening USVFS process {}: OpenProcess failed {}", pids[i], GetLastError()); - continue; - } - - QString pname = QString::fromStdWString(getProcessName(handle)); - bool phidden = false; - for (auto hide : hiddenList) - if (pname.contains(hide, Qt::CaseInsensitive)) - phidden = true; - - bool pprefered = preferedParentPid && getProcessParentID(pids[i]) == preferedParentPid; - - if (best_match == INVALID_HANDLE_VALUE || best_match_hidden || (!phidden && pprefered)) { - if (best_match != INVALID_HANDLE_VALUE) - CloseHandle(best_match); - best_match = handle; - best_match_hidden = phidden; - } - else - CloseHandle(handle); - - if (!phidden && pprefered) - return best_match; - } - - return best_match; -} - bool OrganizerCore::onAboutToRun( const std::function<bool(const QString &)> &func) { @@ -2315,11 +1710,12 @@ void OrganizerCore::savePluginList() m_PluginList.saveLoadOrder(*m_DirectoryStructure); } -void OrganizerCore::prepareStart() +void OrganizerCore::saveCurrentProfile() { if (m_CurrentProfile == nullptr) { return; } + m_CurrentProfile->writeModlist(); m_CurrentProfile->createTweakedIniFile(); saveCurrentLists(); @@ -2327,6 +1723,85 @@ void OrganizerCore::prepareStart() storeSettings(); } +ProcessRunner OrganizerCore::processRunner() +{ + return ProcessRunner(*this, m_UserInterface); +} + +bool OrganizerCore::beforeRun( + const QFileInfo& binary, const QString& profileName, + const QString& customOverwrite, + const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries) +{ + saveCurrentProfile(); + + while (m_DirectoryUpdate) { + ::Sleep(100); + QCoreApplication::processEvents(); + } + + // need to make sure all data is saved before we start the application + if (m_CurrentProfile != nullptr) { + m_CurrentProfile->writeModlistNow(true); + } + + // TODO: should also pass arguments + if (!m_AboutToRun(binary.absoluteFilePath())) { + log::debug("start of \"{}\" cancelled by plugin", binary.absoluteFilePath()); + return false; + } + + try + { + m_USVFS.updateMapping(fileMapping(profileName, customOverwrite)); + m_USVFS.updateForcedLibraries(forcedLibraries); + } + catch (const UsvfsConnectorException &e) + { + log::debug(e.what()); + return false; + } + catch (const std::exception &e) + { + QWidget* w = nullptr; + if (m_UserInterface) { + w = m_UserInterface->qtWidget(); + } + QMessageBox::warning(w, tr("Error"), e.what()); + return false; + } + + return true; +} + +void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) +{ + refreshDirectoryStructure(); + + // need to remove our stored load order because it may be outdated if a + // foreign tool changed the file time. After removing that file, + // refreshESPList will use the file time as the order + if (managedGame()->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::FileTime) { + log::debug("removing loadorder.txt"); + QFile::remove(m_CurrentProfile->getLoadOrderFileName()); + } + + refreshDirectoryStructure(); + + refreshESPList(true); + savePluginList(); + cycleDiagnostics(); + + //These callbacks should not fiddle with directoy structure and ESPs. + m_FinishedRun(binary.absoluteFilePath(), exitCode); +} + +ProcessRunner::Results OrganizerCore::waitForAllUSVFSProcesses( + UILocker::Reasons reason) +{ + return processRunner().waitForAllUSVFSProcessesWithLock(reason); +} + std::vector<Mapping> OrganizerCore::fileMapping(const QString &profileName, const QString &customOverwrite) { diff --git a/src/organizercore.h b/src/organizercore.h index 0d0a092c..6c9edb9f 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -3,7 +3,6 @@ #include "selfupdater.h"
-#include "iuserinterface.h" //should be class IUserInterface;
#include "settings.h"
#include "modlist.h"
#include "modinfo.h"
@@ -14,6 +13,8 @@ #include "executableslist.h"
#include "usvfsconnector.h"
#include "moshortcut.h"
+#include "processrunner.h"
+#include "uilocker.h"
#include <directoryentry.h>
#include <imoinfo.h>
#include <iplugindiagnose.h>
@@ -26,6 +27,8 @@ class ModListSortProxy;
class PluginListSortProxy;
class Profile;
+class IUserInterface;
+
namespace MOBase {
template <typename T> class GuessedValue;
class IModInterface;
@@ -89,19 +92,13 @@ private: typedef boost::signals2::signal<void (const QString&)> SignalModInstalled;
public:
- enum class FileExecutionTypes
- {
- Executable = 1,
- Other = 2
- };
-
static bool isNxmLink(const QString &link) { return link.startsWith("nxm://", Qt::CaseInsensitive); }
OrganizerCore(Settings &settings);
~OrganizerCore();
- void setUserInterface(IUserInterface *userInterface, QWidget *widget);
+ void setUserInterface(IUserInterface* ui);
void connectPlugins(PluginContainer *container);
void disconnectPlugins();
@@ -138,7 +135,17 @@ public: bool saveCurrentLists();
- void prepareStart();
+ ProcessRunner processRunner();
+
+ bool beforeRun(
+ const QFileInfo& binary, const QString& profileName,
+ const QString& customOverwrite,
+ const QList<MOBase::ExecutableForcedLoadSetting>& forcedLibraries);
+
+ void afterRun(const QFileInfo& binary, DWORD exitCode);
+
+ ProcessRunner::Results waitForAllUSVFSProcesses(
+ UILocker::Reasons reason=UILocker::PreventExit);
void refreshESPList(bool force = false);
void refreshBSAList();
@@ -150,37 +157,9 @@ public: void doAfterLogin(const std::function<void()> &function) { m_PostLoginTasks.append(function); }
void loggedInAction(QWidget* parent, std::function<void ()> f);
- static QString findJavaInstallation(const QString& jarFile={});
-
- static bool getFileExecutionContext(
- QWidget* parent, const QFileInfo &targetInfo,
- QFileInfo &binaryInfo, QString &arguments, FileExecutionTypes& type);
-
- bool executeFileVirtualized(QWidget* parent, const QFileInfo& targetInfo);
bool previewFileWithAlternatives(QWidget* parent, QString filename, int selectedOrigin=-1);
bool previewFile(QWidget* parent, const QString& originName, const QString& path);
- void spawnBinary(const QFileInfo &binary, const QString &arguments = "",
- const QDir ¤tDirectory = QDir(),
- const QString &steamAppID = "",
- const QString &customOverwrite = "",
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>());
-
- HANDLE spawnBinaryDirect(const QFileInfo &binary, const QString &arguments,
- const QString &profileName,
- const QDir ¤tDirectory,
- const QString &steamAppID,
- const QString &customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>(),
- LPDWORD exitCode = nullptr);
-
- HANDLE spawnBinaryProcess(const QFileInfo &binary, const QString &arguments,
- const QString &profileName,
- const QDir ¤tDirectory,
- const QString &steamAppID,
- const QString &customOverwrite,
- const QList<MOBase::ExecutableForcedLoadSetting> &forcedLibraries = QList<MOBase::ExecutableForcedLoadSetting>());
-
void loginSuccessfulUpdate(bool necessary);
void loginFailedUpdate(const QString &message);
@@ -234,10 +213,6 @@ public: DownloadManager *downloadManager();
PluginList *pluginList();
ModList *modList();
- HANDLE runShortcut(const MOShortcut& shortcut);
- HANDLE startApplication(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);
- HANDLE findAndOpenAUSVFSProcess(const std::vector<QString>& hiddenList, DWORD preferedParentPid);
bool onModInstalled(const std::function<void (const QString &)> &func);
bool onAboutToRun(const std::function<bool (const QString &)> &func);
bool onFinishedRun(const std::function<void (const QString &, unsigned int)> &func);
@@ -288,6 +263,7 @@ signals: private:
+ void saveCurrentProfile();
void storeSettings();
bool queryApi(QString &apiKey);
@@ -313,8 +289,6 @@ private: const MOShared::DirectoryEntry *directoryEntry,
int createDestination);
- bool waitForProcessCompletion(HANDLE handle, LPDWORD exitCode, ILockedWaitingForProcess* uilock);
-
private slots:
void directory_refreshed();
@@ -328,8 +302,7 @@ private: static const unsigned int PROBLEM_MO1SCRIPTEXTENDERWORKAROUND = 1;
private:
-
- IUserInterface *m_UserInterface;
+ IUserInterface* m_UserInterface;
PluginContainer *m_PluginContainer;
QString m_GameName;
MOBase::IPluginGame *m_GamePlugin;
@@ -371,6 +344,8 @@ private: MOBase::DelayedFileWriter m_PluginListsWriter;
UsvfsConnector m_USVFS;
+ UILocker m_UILocker;
+
static CrashDumpsType m_globalCrashDumpsType;
};
diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index d6996b3a..613de742 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -107,15 +107,65 @@ QString OrganizerProxy::pluginDataPath() const return m_Proxied->pluginDataPath();
}
-HANDLE OrganizerProxy::startApplication(const QString &executable, const QStringList &args, const QString &cwd,
- const QString &profile, const QString &forcedCustomOverwrite, bool ignoreCustomOverwrite)
+HANDLE OrganizerProxy::startApplication(
+ const QString& exe, const QStringList& args, const QString &cwd,
+ const QString& profile, const QString &overwrite, bool ignoreOverwrite)
{
- return m_Proxied->startApplication(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();
+
+ // don't wait for completion
+ runner
+ .setFromFileOrExecutable(exe, args, cwd, profile, overwrite, ignoreOverwrite)
+ .run();
+
+ // 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
{
- return m_Proxied->waitForApplication(handle, exitCode);
+ 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)" : ""));
+
+ auto runner = m_Proxied->processRunner();
+
+ const auto r = runner
+ .setWaitForCompletion(ProcessRunner::ForceWait, UILocker::OutputRequired)
+ .attachToProcess(handle);
+
+ if (exitCode) {
+ *exitCode = runner.exitCode();
+ }
+
+ 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<bool (const QString &)> &func)
@@ -100,9 +100,9 @@ #include <QDropEvent> #include <QElapsedTimer> #include <QEvent> +#include <QFIleIconProvider> #include <QFile> #include <QFileDialog> -#include <QFIleIconProvider> #include <QFileInfo> #include <QFileSystemModel> #include <QFileSystemWatcher> @@ -111,13 +111,14 @@ #include <QFontDatabase> #include <QFuture> #include <QFutureWatcher> -#include <QHash> +#include <QGraphicsDropShadowEffect> #include <QHBoxLayout> +#include <QHash> #include <QHeaderView> +#include <QIODevice> #include <QIcon> #include <QImageReader> #include <QInputDialog> -#include <QIODevice> #include <QItemDelegate> #include <QItemSelection> #include <QItemSelectionModel> @@ -126,20 +127,21 @@ #include <QJsonObject> #include <QJsonValueRef> #include <QKeyEvent> -#include <QLabel> #include <QLCDNumber> +#include <QLabel> #include <QLibrary> #include <QLibraryInfo> #include <QLineEdit> #include <QList> #include <QListWidget> #include <QListWidgetItem> -#include <QLocale> #include <QLocalServer> #include <QLocalSocket> +#include <QLocale> #include <QMainWindow> #include <QMap> #include <QMenu> +#include <QMenuBar> #include <QMessageBox> #include <QMetaType> #include <QMimeData> @@ -190,53 +192,42 @@ #include <QSize> #include <QSizePolicy> #include <QSortFilterProxyModel> -#include <QSplitter> #include <QSpinBox> #include <QSplashScreen> +#include <QSplitter> #include <QSslSocket> #include <QStandardItemModel> -#include <qstandardpaths.h> #include <QStandardPaths> +#include <QStatusBar> +#include <QStorageInfo> #include <QString> #include <QStringList> #include <QStringListModel> #include <QStyle> -#include <QStyledItemDelegate> #include <QStyleFactory> #include <QStyleOption> #include <QStyleOptionSlider> +#include <QStyledItemDelegate> #include <QSyntaxHighlighter> -#include <Qt> -#include <QTableWidget> #include <QTabWidget> -#include <QtAlgorithms> -#include <QtConcurrent/QtConcurrentRun> -#include <QtCore/QAbstractItemModel> -#include <QtCore/QObject> -#include <QtCore/QStack> -#include <QtDebug> +#include <QTableWidget> #include <QTemporaryFile> #include <QTextBrowser> #include <QTextCodec> #include <QTextDocument> #include <QTextEdit> #include <QTextStream> -#include <QtGlobal> -#include <QtGui/QtGui> #include <QThread> #include <QTime> #include <QTimer> #include <QToolBar> #include <QToolButton> #include <QToolTip> -#include <QtPlatformHeaders/QWindowsWindowFunctions> -#include <QtPlugin> #include <QTranslator> #include <QTreeView> #include <QTreeWidget> #include <QTreeWidgetItem> #include <QTreeWidgetItemIterator> -#include <QtTest/QtTest> #include <QUrl> #include <QUrlQuery> #include <QVariant> @@ -255,4 +246,16 @@ #include <QWhatsThisClickedEvent> #include <QWidget> #include <QWidgetAction> -#include <QStorageInfo> +#include <Qt> +#include <QtAlgorithms> +#include <QtConcurrent/QtConcurrentRun> +#include <QtCore/QAbstractItemModel> +#include <QtCore/QObject> +#include <QtCore/QStack> +#include <QtDebug> +#include <QtGlobal> +#include <QtGui/QtGui> +#include <QtPlatformHeaders/QWindowsWindowFunctions> +#include <QtPlugin> +#include <QtTest/QtTest> +#include <QStandardPaths> diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index c0706ba8..767d3eb8 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -3,6 +3,7 @@ #include "organizerproxy.h"
#include "report.h"
#include <ipluginproxy.h>
+#include <iuserinterface.h>
#include <idownloadmanager.h>
#include <appconfig.h>
#include <QAction>
diff --git a/src/processrunner.cpp b/src/processrunner.cpp new file mode 100644 index 00000000..cfddbb63 --- /dev/null +++ b/src/processrunner.cpp @@ -0,0 +1,791 @@ +#include "processrunner.h" +#include "organizercore.h" +#include "instancemanager.h" +#include "iuserinterface.h" +#include "envmodule.h" +#include <log.h> + +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()); + } +} + +std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) +{ + if (handle == INVALID_HANDLE_VALUE) { + return ProcessRunner::Error; + } + + const auto res = WaitForSingleObject(handle, 50); + + switch (res) + { + case WAIT_OBJECT_0: + { + log::debug("process {} completed", pid); + return ProcessRunner::Completed; + } + + case WAIT_TIMEOUT: + { + // still running + return {}; + } + + case WAIT_FAILED: // fall-through + default: + { + // error + const auto e = ::GetLastError(); + log::error("failed waiting for {}, {}", pid, formatSystemMessage(e)); + return ProcessRunner::Error; + } + } +} + +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"; + } +} + + +// 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) +{ + if (processes.empty()) { + return {{}, Interest::None}; + } + + // Certain process names we wish to "hide" for aesthetic reason: + const std::vector<QString> 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, Interest::Strong}; + } + + for (auto&& child : root.children()) { + if (!isHidden(child)) { + return {child, Interest::Strong}; + } + } + } + + // everything is hidden, just pick the first one + return {processes[0], Interest::Weak}; +} + +// gets the most interesting process in the list +// +std::pair<env::Process, Interest> getInterestingProcess( + const std::vector<HANDLE>& initialProcesses) +{ + if (initialProcesses.empty()) { + log::debug("nothing to wait for"); + return {{}, Interest::None}; + } + + std::vector<env::Process> processes; + + // getting process trees for all processes + for (auto&& h : initialProcesses) { + auto tree = env::getProcessTree(h); + if (tree.isValid()) { + processes.push_back(tree); + } + } + + 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}; + } + + return interest; +} + +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, UILocker::Session& ls, + std::chrono::milliseconds wait) +{ + using namespace std::chrono; + + high_resolution_clock::time_point start; + if (wait != Infinite) { + start = high_resolution_clock::now(); + } + + 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; + } + + // the process is still running + + // check the lock widget + switch (ls.result()) + { + case UILocker::StillLocked: + { + break; + } + + case UILocker::ForceUnlocked: + { + log::debug("waiting for {} force unlocked by user", pid); + return ProcessRunner::ForceUnlocked; + } + + case UILocker::Cancelled: + { + log::debug("waiting for {} cancelled by user", pid); + return ProcessRunner::Cancelled; + } + + case UILocker::NoResult: // fall-through + default: + { + // shouldn't happen + log::debug( + "unexpected result {} while waiting for {}", + static_cast<int>(ls.result()), pid); + + return ProcessRunner::Error; + } + } + + 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 {}; + } + } + } +} + +ProcessRunner::Results waitForProcessesThreadImpl( + const std::vector<HANDLE>& initialProcesses, 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); + + for (;;) { + auto [p, interest] = getInterestingProcess(initialProcesses); + if (!p.isValid()) { + // nothing to wait on + return ProcessRunner::Completed; + } + + // update the lock widget + ls.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( + "waiting for completion on {} ({}), {} interest", + p.name(), p.pid(), toString(interest)); + } + + 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(), ls, 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)); + } +} + +void waitForProcessesThread( + ProcessRunner::Results& result, + const std::vector<HANDLE>& initialProcesses, UILocker::Session& ls) +{ + result = waitForProcessesThreadImpl(initialProcesses, ls); + ls.unlock(); +} + +ProcessRunner::Results waitForProcesses( + const std::vector<HANDLE>& initialProcesses, UILocker::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, UILocker::Session& ls) +{ + std::vector<HANDLE> processes = {initialProcess}; + + 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) { + if (!::GetExitCodeProcess(initialProcess, exitCode)) { + const auto e = ::GetLastError(); + log::warn( + "failed to get exit code of process, {}", + formatSystemMessage(e)); + } + } + + return r; +} + + +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; +} + +ProcessRunner& ProcessRunner::setBinary(const QFileInfo &binary) +{ + m_sp.binary = binary; + return *this; +} + +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( + WaitFlags flags, UILocker::Reasons reason) +{ + m_waitFlags = flags; + m_lockReason = reason; + return *this; +} + +ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo) +{ + if (!parent && m_ui) { + 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) + { + case spawn::FileExecutionTypes::Executable: + { + setBinary(fec.binary); + setArguments(fec.arguments); + setCurrentDirectory(targetInfo.absoluteDir()); + break; + } + + case spawn::FileExecutionTypes::Other: // fall-through + default: + { + m_shellOpen = targetInfo.absoluteFilePath(); + break; + } + } + + return *this; +} + +ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe) +{ + 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(); + + ForcedLibraries forcedLibraries; + if (profile->forcedLibrariesEnabled(exe.title())) { + forcedLibraries = profile->determineForcedLibraries(exe.title()); + } + + 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; +} + +ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut) +{ + 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()) + .toLocal8Bit().constData()); + } + + const Executable& exe = m_core.executablesList()->get(shortcut.executable()); + setFromExecutable(exe); + + return *this; +} + +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(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); + } + + setBinary(executable); + setArguments(args.join(" ")); + setCurrentDirectory(cwd); + setProfileName(profileOverride); + + if (executable.contains('\\') || executable.contains('/')) { + // file path + + if (m_sp.binary.isRelative()) { + // relative path, should be relative to game directory + setBinary(m_core.managedGame()->gameDirectory().absoluteFilePath(executable)); + } + + if (cwd == "") { + setCurrentDirectory(m_sp.binary.absolutePath()); + } + + try { + const Executable& exe = m_core.executablesList()->getByBinary(m_sp.binary); + + setSteamID(exe.steamAppID()); + setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); + + if (profile->forcedLibrariesEnabled(exe.title())) { + setForcedLibraries(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); + + setSteamID(exe.steamAppID()); + setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString()); + + if (profile->forcedLibrariesEnabled(exe.title())) { + setForcedLibraries(profile->determineForcedLibraries(exe.title())); + } + + if (args.isEmpty()) { + setArguments(exe.arguments()); + } + + setBinary(exe.binaryInfo()); + + if (cwd == "") { + setCurrentDirectory(exe.workingDirectory()); + } + } catch (const std::runtime_error &) { + log::warn("\"{}\" not set up as executable", executable); + } + } + + if (ignoreCustomOverwrite) { + setCustomOverwrite(""); + } else if (!forcedCustomOverwrite.isEmpty()) { + setCustomOverwrite(forcedCustomOverwrite); + } + + return *this; +} + +ProcessRunner::Results ProcessRunner::run() +{ + std::optional<Results> r; + + if (!m_shellOpen.isEmpty()) { + r = runShell(); + } else { + r = runBinary(); + } + + if (r) { + // early result: something went wrong and the process cannot be waited for + return *r; + } + + return postRun(); +} + +std::optional<ProcessRunner::Results> ProcessRunner::runShell() +{ + log::debug("executing from shell: '{}'", m_shellOpen); + + auto r = shell::Open(m_shellOpen); + if (!r.success()) { + return Error; + } + + 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.get() == INVALID_HANDLE_VALUE) { + log::debug("shell didn't report an error, but no handle is available"); + return Running; + } + + return {}; +} + +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")); + } + + m_profileName = profile->name(); + } + + // 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; + } + + // parent widget used for any dialog popped up while checking for things + QWidget* parent = (m_ui ? m_ui->qtWidget() : nullptr); + + 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; + } + + // 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() +{ + const bool mustWait = (m_waitFlags & ForceWait); + + 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 = UILocker::LockUI; + } + + if (mustWait) { + 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; " + "overriding this setting and locking the ui"); + } + } else { + // no force wait + + if (m_lockReason == UILocker::NoReason) { + // no locking requested + return Running; + } + + 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; + } + } + + auto r = Error; + + withLock([&](auto& ls) { + 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 + // + m_core.afterRun(m_sp.binary, m_exitCode); + } + + return r; +} + +ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h) +{ + m_handle.reset(h); + return postRun(); +} + +DWORD ProcessRunner::exitCode() const +{ + return m_exitCode; +} + +HANDLE ProcessRunner::getProcessHandle() const +{ + return m_handle.get(); +} + +env::HandlePtr ProcessRunner::stealProcessHandle() +{ + auto h = m_handle.release(); + m_handle.reset(INVALID_HANDLE_VALUE); + return env::HandlePtr(h); +} + +ProcessRunner::Results ProcessRunner::waitForAllUSVFSProcessesWithLock( + UILocker::Reasons reason) +{ + m_lockReason = reason; + + if (!m_core.settings().interface().lockGUI()) { + // disabling locking is like clicking on unlock immediately + return ForceUnlocked; + } + + auto r = Error; + + withLock([&](auto& ls) { + for (;;) { + const auto processes = getRunningUSVFSProcesses(); + if (processes.empty()) { + break; + } + + r = waitForProcesses(processes, ls); + + if (r != Completed) { + // error, cancelled, or unlocked + return; + } + + // this process is completed, check for others + } + + r = Completed; + }); + + return r; +} + +void ProcessRunner::withLock(std::function<void (UILocker::Session&)> f) +{ + auto ls = UILocker::instance().lock(m_lockReason); + f(*ls); +} diff --git a/src/processrunner.h b/src/processrunner.h new file mode 100644 index 00000000..c61d6b70 --- /dev/null +++ b/src/processrunner.h @@ -0,0 +1,172 @@ +#ifndef PROCESSRUNNER_H +#define PROCESSRUNNER_H + +#include "spawn.h" +#include "uilocker.h" +#include "envmodule.h" +#include <executableinfo.h> + +class OrganizerCore; +class IUserInterface; +class Executable; +class MOShortcut; + +// handles spawning a process and waiting for it, including setting up the lock +// widget if required +// +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); + + // 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); + ProcessRunner& setSteamID(const QString& steamID); + ProcessRunner& setCustomOverwrite(const QString& customOverwrite); + ProcessRunner& setForcedLibraries(const ForcedLibraries& forcedLibraries); + ProcessRunner& setProfileName(const QString& profileName); + 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); + + 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, + const QString &cwd={}, + const QString &profile={}, + 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(UILocker::Reasons reason); + +private: + OrganizerCore& m_core; + IUserInterface* m_ui; + spawn::SpawnParameters m_sp; + QString m_customOverwrite; + ForcedLibraries m_forcedLibraries; + QString m_profileName; + UILocker::Reasons m_lockReason; + WaitFlags m_waitFlags; + QString m_shellOpen; + 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 (UILocker::Session&)> f); +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(ProcessRunner::WaitFlags); + +#endif // PROCESSRUNNER_H diff --git a/src/spawn.cpp b/src/spawn.cpp index a34230b2..f95846c8 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -440,7 +440,26 @@ QMessageBox::StandardButton confirmBlacklisted( namespace spawn
{
-DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle, HANDLE& threadHandle)
+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;
@@ -460,30 +479,33 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle, HANDLE& threadHand 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;
+ 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<wchar_t*>(commandLine.c_str()), nullptr, nullptr,
- inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr,
- cwd.c_str(), &si, &pi);
+ nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ inheritHandles, 0, nullptr, wcwd.c_str(), &si, &pi);
} else {
success = ::CreateProcess(
- nullptr, const_cast<wchar_t*>(commandLine.c_str()), nullptr, nullptr,
- inheritHandles, CREATE_BREAKAWAY_FROM_JOB, nullptr,
- cwd.c_str(), &si, &pi);
+ nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ inheritHandles, 0, nullptr, wcwd.c_str(), &si, &pi);
}
const auto e = GetLastError();
@@ -494,7 +516,7 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle, HANDLE& threadHand }
processHandle = pi.hProcess;
- threadHandle = pi.hThread;
+ ::CloseHandle(pi.hThread);
return ERROR_SUCCESS;
}
@@ -533,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;
@@ -618,8 +630,8 @@ bool startSteam(QWidget* parent) (password.isEmpty() ? "no" : "yes"));
HANDLE ph = INVALID_HANDLE_VALUE;
- HANDLE th = INVALID_HANDLE_VALUE;
- const auto e = spawn(sp, ph, th);
+ const auto e = spawn(sp, ph);
+ ::CloseHandle(ph);
if (e != ERROR_SUCCESS) {
// make sure username and passwords are not shown
@@ -730,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)
{
@@ -771,18 +758,16 @@ bool checkBlacklist( }
}
-
HANDLE startBinary(QWidget* parent, const SpawnParameters& sp)
{
- HANDLE processHandle, threadHandle;
- const auto e = spawn(sp, processHandle, threadHandle);
+ HANDLE handle = INVALID_HANDLE_VALUE;
+ const auto e = spawn::spawn(sp, handle);
switch (e)
{
case ERROR_SUCCESS:
{
- ::CloseHandle(threadHandle);
- return processHandle;
+ return handle;
}
case ERROR_ELEVATION_REQUIRED:
@@ -799,6 +784,184 @@ HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) }
}
+QString getExecutableForJarFile(const QString& jarFile)
+{
+ const std::wstring jarFileW = jarFile.toStdWString();
+
+ WCHAR buffer[MAX_PATH];
+
+ const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer);
+ const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(hinst));
+
+ // anything <= 32 signals failure
+ if (r <= 32) {
+ log::warn(
+ "failed to find executable associated with file '{}', {}",
+ jarFile, shell::formatError(r));
+
+ return {};
+ }
+
+ DWORD binaryType = 0;
+
+ if (!::GetBinaryTypeW(buffer, &binaryType)) {
+ const auto e = ::GetLastError();
+
+ log::warn(
+ "failed to determine binary type of '{}', {}",
+ QString::fromWCharArray(buffer), formatSystemMessage(e));
+
+ return {};
+ }
+
+ if (binaryType != SCS_32BIT_BINARY && binaryType != SCS_64BIT_BINARY) {
+ log::warn(
+ "unexpected binary type {} for file '{}'",
+ binaryType, QString::fromWCharArray(buffer));
+
+ return {};
+ }
+
+ return QString::fromWCharArray(buffer);
+}
+
+QString getJavaHome()
+{
+ const QString key = "HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment";
+ const QString value = "CurrentVersion";
+
+ QSettings reg(key, QSettings::NativeFormat);
+
+ if (!reg.contains(value)) {
+ log::warn("key '{}\\{}' doesn't exist", key, value);
+ return {};
+ }
+
+ const QString currentVersion = reg.value("CurrentVersion").toString();
+ const QString javaHome = QString("%1/JavaHome").arg(currentVersion);
+
+ if (!reg.contains(javaHome)) {
+ log::warn(
+ "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist",
+ currentVersion, key, value, key, javaHome);
+
+ return {};
+ }
+
+ const auto path = reg.value(javaHome).toString();
+ return path + "\\bin\\javaw.exe";
+}
+
+QString findJavaInstallation(const QString& jarFile)
+{
+ // try to find java automatically based on the given jar file
+ if (!jarFile.isEmpty()) {
+ const auto s = getExecutableForJarFile(jarFile);
+ if (!s.isEmpty()) {
+ return s;
+ }
+ }
+
+ // second attempt: look to the registry
+ const auto s = getJavaHome();
+ if (!s.isEmpty()) {
+ return s;
+ }
+
+ // not found
+ return {};
+}
+
+bool isBatchFile(const QFileInfo& target)
+{
+ const auto batchExtensions = {"cmd", "bat"};
+
+ const QString extension = target.suffix();
+ for (auto&& e : batchExtensions) {
+ if (extension.compare(e, Qt::CaseInsensitive) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool isExeFile(const QFileInfo& target)
+{
+ return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0);
+}
+
+bool isJavaFile(const QFileInfo& target)
+{
+ return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0);
+}
+
+QFileInfo getCmdPath()
+{
+ const auto p = env::get("COMSPEC");
+ if (!p.isEmpty()) {
+ return p;
+ }
+
+ QString systemDirectory;
+
+ const std::size_t buffer_size = 1000;
+ wchar_t buffer[buffer_size + 1] = {};
+
+ const auto length = ::GetSystemDirectoryW(buffer, buffer_size);
+ if (length != 0) {
+ systemDirectory = QString::fromWCharArray(buffer, length);
+
+ if (!systemDirectory.endsWith("\\")) {
+ systemDirectory += "\\";
+ }
+ } else {
+ systemDirectory = "C:\\Windows\\System32\\";
+ }
+
+ return systemDirectory + "cmd.exe";
+}
+
+FileExecutionContext getFileExecutionContext(
+ QWidget* parent, const QFileInfo& target)
+{
+ if (isExeFile(target)) {
+ return {
+ target,
+ "",
+ FileExecutionTypes::Executable
+ };
+ }
+
+ if (isBatchFile(target)) {
+ return {
+ getCmdPath(),
+ QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
+ FileExecutionTypes::Executable
+ };
+ }
+
+ if (isJavaFile(target)) {
+ auto java = findJavaInstallation(target.absoluteFilePath());
+
+ if (java.isEmpty()) {
+ java = QFileDialog::getOpenFileName(
+ parent, QObject::tr("Select binary"),
+ QString(), QObject::tr("Binary") + " (*.exe)");
+ }
+
+ if (!java.isEmpty()) {
+ return {
+ QFileInfo(java),
+ QString("-jar \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
+ FileExecutionTypes::Executable
+ };
+ }
+ }
+
+ return {{}, {}, FileExecutionTypes::Other};
+}
+
} // namespace
@@ -817,7 +980,7 @@ bool helperExec( {
SHELLEXECUTEINFOW execInfo = {};
- ULONG flags = SEE_MASK_FLAG_NO_UI ;
+ ULONG flags = SEE_MASK_FLAG_NO_UI;
if (!async)
flags |= SEE_MASK_NOCLOSEPROCESS;
diff --git a/src/spawn.h b/src/spawn.h index 31b44739..a615b5ff 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -46,20 +46,17 @@ struct SpawnParameters QFileInfo binary;
QString arguments;
QDir currentDirectory;
+ QString steamAppID;
bool hooked = false;
HANDLE stdOut = INVALID_HANDLE_VALUE;
HANDLE stdErr = INVALID_HANDLE_VALUE;
};
-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);
@@ -69,6 +66,25 @@ bool checkBlacklist( **/
HANDLE startBinary(QWidget* parent, const SpawnParameters& sp);
+
+enum class FileExecutionTypes
+{
+ Executable = 1,
+ Other
+};
+
+struct FileExecutionContext
+{
+ QFileInfo binary;
+ QString arguments;
+ FileExecutionTypes type;
+};
+
+QString findJavaInstallation(const QString& jarFile);
+
+FileExecutionContext getFileExecutionContext(
+ QWidget* parent, const QFileInfo& target);
+
} // namespace
diff --git a/src/uilocker.cpp b/src/uilocker.cpp new file mode 100644 index 00000000..01794d6b --- /dev/null +++ b/src/uilocker.cpp @@ -0,0 +1,548 @@ +#include "uilocker.h" +#include "mainwindow.h" +#include <QGraphicsDropShadowEffect> +#include <QMenuBar> +#include <QStatusBar> + +class UILockerInterface +{ +public: + UILockerInterface(QWidget* mainUI) : + m_mainUI(mainUI), m_target(nullptr), m_message(nullptr), m_info(nullptr), + m_buttons(nullptr), m_reason(UILocker::NoReason) + { + m_timer.reset(new QTimer); + QObject::connect(m_timer.get(), &QTimer::timeout, [&]{ checkTarget(); }); + m_timer->start(200); + + set(); + } + + ~UILockerInterface() + { + } + + void checkTarget() + { + if (set()) { + update(m_reason); + } + } + + bool set() + { + QWidget* newTarget = nullptr; + + newTarget = m_mainUI; + if (auto* w = QApplication::activeModalWidget()) { + newTarget = w; + } + + if (newTarget == m_target) { + return false; + } + + m_target = newTarget; + + QFrame* center = nullptr; + + if (m_target) { + center = createOverlay(m_target); + } else { + center = createDialog(); + } + + 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(); + + return true; + } + + void update(UILocker::Reasons reason) + { + m_reason = reason; + updateMessage(reason); + updateButtons(reason); + setInfo(m_labels); + } + + void setInfo(const QStringList& labels) + { + 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); + } + + QWidget* topLevel() + { + return m_topLevel.get(); + } + +private: + class Filter : public QObject + { + public: + std::function<void ()> resized; + std::function<void ()> 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::unique_ptr<QTimer> m_timer; + QWidget* m_mainUI; + QWidget* m_target; + std::unique_ptr<QWidget> m_topLevel; + QLabel* m_message; + QLabel* m_info; + QStringList m_labels; + QWidget* m_buttons; + std::unique_ptr<Filter> m_filter; + UILocker::Reasons m_reason; + + + bool hasMainUI() const + { + return (m_target != nullptr); + } + + 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; + } + + QFrame* createOverlay(QWidget* mainUI) + { + m_topLevel.reset(createTransparentWidget(mainUI)); + m_topLevel->setWindowFlags(m_topLevel->windowFlags() & Qt::FramelessWindowHint); + m_topLevel->setGeometry(mainUI->rect()); + + m_filter.reset(new Filter); + m_filter->resized = [=]{ m_topLevel->setGeometry(mainUI->rect()); }; + m_filter->closed = [=]{ checkTarget(); }; + + mainUI->installEventFilter(m_filter.get()); + + return createFrame(); + } + + QFrame* createDialog() + { + m_topLevel.reset(new QDialog); + + return createFrame(); + } + + QFrame* createFrame() + { + auto* frame = new QFrame; + auto* ly = new QVBoxLayout(frame); + + if (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); + } + + 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); + + if (!hasMainUI()) { + grid->setContentsMargins(0, 0, 0, 0); + } + + grid->setRowStretch(0, 1); + grid->setRowStretch(2, 1); + grid->setColumnStretch(0, 1); + grid->setColumnStretch(2, 1); + + return frame; + } + + void createMessageLabel() + { + m_message = new QLabel; + m_message->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); + } + + void createInfoLabel() + { + m_info = new QLabel(" "); + m_info->setAlignment(Qt::AlignCenter | Qt::AlignHCenter); + } + + void createButtonsPanel() + { + m_buttons = new QWidget; + m_buttons->setLayout(new QHBoxLayout); + } + + + void updateMessage(UILocker::Reasons reason) + { + switch (reason) + { + case UILocker::LockUI: + { + QString s; + + if (hasMainUI()) { + s = QObject::tr( + "Mod Organizer is locked while the application is running."); + } else { + s = QObject::tr("Mod Organizer is currently running an application."); + } + + m_message->setText(s); + + break; + } + + case UILocker::OutputRequired: + { + m_message->setText(QObject::tr( + "The application must run to completion because its output is " + "required.")); + + break; + } + + case UILocker::PreventExit: + { + m_message->setText(QObject::tr( + "Mod Organizer is waiting on application to close before exiting.")); + + break; + } + } + } + + void updateButtons(UILocker::Reasons reason) + { + MOBase::deleteChildWidgets(m_buttons); + auto* ly = m_buttons->layout(); + + switch (reason) + { + case UILocker::LockUI: // fall-through + case UILocker::OutputRequired: + { + auto* unlock = new QPushButton(QObject::tr("Unlock")); + + QObject::connect(unlock, &QPushButton::clicked, [&]{ + UILocker::instance().onForceUnlock(); + }); + + ly->addWidget(unlock); + + break; + } + + case UILocker::PreventExit: + { + auto* exit = new QPushButton(QObject::tr("Exit Now")); + QObject::connect(exit, &QPushButton::clicked, [&]{ + UILocker::instance().onForceUnlock(); + }); + + ly->addWidget(exit); + + auto* cancel = new QPushButton(QObject::tr("Cancel")); + QObject::connect(cancel, &QPushButton::clicked, [&]{ + UILocker::instance().onCancel(); + }); + + ly->addWidget(cancel); + + break; + } + } + } +}; + +UILocker::Session::~Session() +{ + unlock(); +} + +void UILocker::Session::unlock() +{ + QMetaObject::invokeMethod(qApp, [this]{ + UILocker::instance().unlock(this); + }); +} + +void UILocker::Session::setInfo(DWORD pid, const QString& name) +{ + { + std::scoped_lock lock(m_mutex); + m_pid = pid; + m_name = name; + } + + QMetaObject::invokeMethod(qApp, [this]{ + UILocker::instance().updateLabel(); + }); +} + +DWORD UILocker::Session::pid() const +{ + std::scoped_lock lock(m_mutex); + return m_pid; +} + +const QString& UILocker::Session::name() const +{ + std::scoped_lock lock(m_mutex); + return m_name; +} + +UILocker::Results UILocker::Session::result() const +{ + return UILocker::instance().result(); +} + + +static UILocker* g_instance = nullptr; + + +UILocker::UILocker() + : m_parent(nullptr), m_result(NoResult) +{ + Q_ASSERT(!g_instance); + g_instance = this; +} + +UILocker::~UILocker() +{ + const auto v = m_sessions; + + for (auto& wp : v) { + if (auto s=wp.lock()) { + unlock(s.get()); + } + } +} + +UILocker& UILocker::instance() +{ + Q_ASSERT(g_instance); + return *g_instance; +} + +void UILocker::setUserInterface(QWidget* parent) +{ + m_parent = parent; +} + +std::shared_ptr<UILocker::Session> UILocker::lock(Reasons reason) +{ + m_result = StillLocked; + createUi(reason); + + auto ls = std::make_shared<Session>(); + m_sessions.push_back(ls); + + updateLabel(); + + return ls; +} + +void UILocker::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; + } + + if (m_sessions.empty()) { + m_ui.reset(); + enableAll(); + } else { + updateLabel(); + } +} + +void UILocker::unlockCurrent() +{ + if (m_sessions.empty()) { + return; + } + + auto s = m_sessions.back().lock(); + if (!s) { + m_sessions.pop_back(); + return; + } + + unlock(s.get()); +} + +void UILocker::updateLabel() +{ + QStringList labels; + + for (auto itor=m_sessions.rbegin(); itor!=m_sessions.rend(); ++itor) { + if (auto ss=itor->lock()) { + labels.push_back(QString("%1 (%2)").arg(ss->name()).arg(ss->pid())); + } + } + + m_ui->setInfo(labels); +} + +UILocker::Results UILocker::result() const +{ + return m_result; +} + +void UILocker::createUi(Reasons reason) +{ + if (!m_ui) { + m_ui.reset(new UILockerInterface(m_parent)); + } + + m_ui->update(reason); + + disableAll(); +} + +void UILocker::onForceUnlock() +{ + m_result = ForceUnlocked; + unlockCurrent(); +} + +void UILocker::onCancel() +{ + m_result = Cancelled; + unlockCurrent(); +} + +template <class T> +QList<T> findChildrenImmediate(QWidget* parent) +{ + return parent->findChildren<T>(QString(), Qt::FindDirectChildrenOnly); +} + +void UILocker::disableAll() +{ + const auto topLevels = QApplication::topLevelWidgets(); + + for (auto* w : topLevels) { + if (auto* mw=dynamic_cast<QMainWindow*>(w)) { + disable(mw->centralWidget()); + disable(mw->menuBar()); + disable(mw->statusBar()); + + for (auto* tb : findChildrenImmediate<QToolBar*>(w)) { + disable(tb); + } + + for (auto* d : findChildrenImmediate<QDockWidget*>(w)) { + disable(d); + } + } + + if (auto* d=dynamic_cast<QDialog*>(w)) { + // don't disable stuff if this dialog is the overlay, which happens when + // there's no ui + if (d != m_ui->topLevel()) { + // no central widget, just disable the children, except for the overlay + for (auto* child : findChildrenImmediate<QWidget*>(d)) { + if (child != m_ui->topLevel()) { + disable(child); + } + } + } + } + } +} + +void UILocker::enableAll() +{ + for (auto w : m_disabled) { + if (w) { + w->setEnabled(true); + } + } + + m_disabled.clear(); +} + +void UILocker::disable(QWidget* w) +{ + if (w->isEnabled()) { + w->setEnabled(false); + m_disabled.push_back(w); + } +} diff --git a/src/uilocker.h b/src/uilocker.h new file mode 100644 index 00000000..d8c22999 --- /dev/null +++ b/src/uilocker.h @@ -0,0 +1,95 @@ +#pragma once + +#include <QMainWindow> +#include <mutex> + +class UILockerInterface; + +class UILocker +{ + friend class UILockerInterface; + +public: + // reason to show the widget + // + enum Reasons + { + NoReason = 0, + + // lock the ui + LockUI, + + // because the output is required + OutputRequired, + + // to prevent exiting until all processes are completed + PreventExit + }; + + // returned by result() + // + enum Results + { + NoResult = 0, + + // the widget is still up + StillLocked, + + // force unlock was clicked + ForceUnlocked, + + // cancel was clicked + Cancelled + }; + + + class Session + { + public: + ~Session(); + + void unlock(); + void setInfo(DWORD pid, const QString& name); + Results result() const; + + DWORD pid() const; + const QString& name() const; + + private: + mutable std::mutex m_mutex; + DWORD m_pid; + QString m_name; + }; + + + UILocker(); + ~UILocker(); + + static UILocker& instance(); + + void setUserInterface(QWidget* parent); + + std::shared_ptr<Session> lock(Reasons reason); + + Results result() const; + +private: + QWidget* m_parent; + std::unique_ptr<UILockerInterface> m_ui; + std::vector<std::weak_ptr<Session>> m_sessions; + std::atomic<Results> m_result; + std::vector<QPointer<QWidget>> m_disabled; + + void createUi(Reasons reason); + + void unlockCurrent(); + void unlock(Session* s); + void updateLabel(); + + void onForceUnlock(); + void onCancel(); + + void disableAll(); + void enableAll(); + void disable(QWidget* w); +}; diff --git a/src/usvfsconnector.cpp b/src/usvfsconnector.cpp index 311c6dd3..3dba3efc 100644 --- a/src/usvfsconnector.cpp +++ b/src/usvfsconnector.cpp @@ -20,6 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "usvfsconnector.h" #include "settings.h" #include "organizercore.h" +#include "envmodule.h" #include "shared/util.h" #include <memory> #include <sstream> @@ -256,3 +257,49 @@ void UsvfsConnector::updateForcedLibraries(const QList<MOBase::ExecutableForcedL } } } + + +std::vector<HANDLE> getRunningUSVFSProcesses() +{ + std::vector<DWORD> pids; + + { + size_t count = 0; + DWORD* buffer = nullptr; + if (!::GetVFSProcessList2(&count, &buffer)) { + log::error("failed to get usvfs process list"); + return {}; + } + + if (buffer) { + pids.assign(buffer, buffer + count); + std::free(buffer); + } + } + + const auto thisPid = GetCurrentProcessId(); + std::vector<HANDLE> v; + + 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); + + if (handle == INVALID_HANDLE_VALUE) { + const auto e = GetLastError(); + + log::warn( + "failed to open usvfs process {}: {}", + pid, formatSystemMessage(e)); + + continue; + } + + v.push_back(handle); + } + + return v; +} diff --git a/src/usvfsconnector.h b/src/usvfsconnector.h index d0071678..5982778b 100644 --- a/src/usvfsconnector.h +++ b/src/usvfsconnector.h @@ -103,4 +103,6 @@ private: CrashDumpsType crashDumpsType(int type); +std::vector<HANDLE> getRunningUSVFSProcesses(); + #endif // USVFSCONNECTOR_H diff --git a/src/waitingonclosedialog.cpp b/src/waitingonclosedialog.cpp deleted file mode 100644 index 565d0a36..00000000 --- a/src/waitingonclosedialog.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. -*/ - -#include "waitingonclosedialog.h" -#include "ui_waitingonclosedialog.h" - -#include <QPoint> -#include <QResizeEvent> -#include <QWidget> -#include <Qt> // for Qt::FramelessWindowHint, etc - -WaitingOnCloseDialog::WaitingOnCloseDialog(QWidget *parent) - : LockedDialogBase(parent,true) - , ui(new Ui::WaitingOnCloseDialog) -{ - ui->setupUi(this); - - // Supposedly the Qt::CustomizeWindowHint should use a customized window - // allowing us to select if there is a close button. In practice this doesn't - // seem to work. We will ignore pressing the close button if unlockByButton == true - Qt::WindowFlags flags = - this->windowFlags() | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint; - if (m_allowClose) - flags |= Qt::WindowCloseButtonHint; - this->setWindowFlags(flags); -} - -WaitingOnCloseDialog::~WaitingOnCloseDialog() -{ - delete ui; -} - - -void WaitingOnCloseDialog::setProcessName(const QString &name) -{ - ui->processLabel->setText(name); -} - -void WaitingOnCloseDialog::on_closeButton_clicked() -{ - unlock(); -} - -void WaitingOnCloseDialog::on_cancelButton_clicked() -{ - cancel(); - unlock(); -} - -void WaitingOnCloseDialog::unlock() { - LockedDialogBase::unlock(); - ui->label->setText("unlocking may take a few seconds"); - ui->closeButton->setEnabled(false); - ui->cancelButton->setEnabled(false); -} diff --git a/src/waitingonclosedialog.h b/src/waitingonclosedialog.h deleted file mode 100644 index 6650c390..00000000 --- a/src/waitingonclosedialog.h +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Mod Organizer is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. -*/ - -#pragma once - -#include "lockeddialogbase.h" - -namespace Ui { - class WaitingOnCloseDialog; -} - -/** - * Similar to the LockedDialog but used for waiting on running process during - * a process close request which requries a slightly different dialog. - **/ -class WaitingOnCloseDialog : public LockedDialogBase -{ - Q_OBJECT - -public: - explicit WaitingOnCloseDialog(QWidget *parent = 0); - ~WaitingOnCloseDialog(); - - bool canceled() const { return m_Canceled; } - - void setProcessName(const QString &name) override; - -protected: - - void unlock() override; - -private slots: - - void on_closeButton_clicked(); - void on_cancelButton_clicked(); - -private: - - Ui::WaitingOnCloseDialog *ui; -}; diff --git a/src/waitingonclosedialog.ui b/src/waitingonclosedialog.ui deleted file mode 100644 index 9c7818e0..00000000 --- a/src/waitingonclosedialog.ui +++ /dev/null @@ -1,119 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>WaitingOnCloseDialog</class> - <widget class="QDialog" name="WaitingOnCloseDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>317</width> - <height>151</height> - </rect> - </property> - <property name="windowTitle"> - <string>Waiting for virtualized processes</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0"> - <item> - <widget class="QLabel" name="label"> - <property name="toolTip"> - <string>This dialog should disappear automatically if the application/game is done.</string> - </property> - <property name="text"> - <string>Virtualized processes are still running, it is prefered to keep MO running until they are finished.</string> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <spacer name="verticalSpacer1"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <height>10</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="processLabel"> - <property name="font"> - <font> - <italic>true</italic> - </font> - </property> - <property name="styleSheet"> - <string notr="true">color: grey;</string> - </property> - <property name="text"> - <string/> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item> - <spacer name="verticalSpacer2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <height>10</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,0"> - <item> - <widget class="QPushButton" name="closeButton"> - <property name="text"> - <string>Close Now</string> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_6"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="cancelButton"> - <property name="text"> - <string>Cancel</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <spacer name="verticalSpacer3"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <height>10</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - <resources/> - <connections/> -</ui> |
