diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-28 21:22:45 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-28 21:22:45 -0500 |
| commit | 5d42085dd36f14aa3385547947ccd6e6c5f03d50 (patch) | |
| tree | ad09bcb7acf58caba0cc53411d5bab4df0d17899 | |
| parent | 2edfae46cc600079cb705e78f885df1fac269ce2 (diff) | |
Strip remaining Windows-only conditionals across src/src/
Removes every #ifdef _WIN32 / #ifndef _WIN32 / Q_OS_WIN block from the
organizer source tree. The Linux build is the only target this fork
produces, so the dead Windows branches were just noise. Stubs for
Win32-shaped APIs that the upstream code references (FILETIME, HANDLE,
DWORD, etc.) live in shared/windows_compat.h so the surrounding source
keeps compiling without rewriting every signature.
Also:
- Reimplemented env::DirectoryWalker / env::forEachEntry / env::Module
/ env::Process / env::WindowsInfo as Linux-only (uname, /etc/os-release,
/proc, std::filesystem) since the Win32 branches that previously held
those implementations are gone.
- Reduced spawn.cpp / processrunner.cpp / env.cpp to their Linux paths;
dropped the helper:: namespace, Win-only waitForAllUSVFSProcesses-
WithLock(), Steam Win-registry probe, and Windows mini-dump path.
- Linux launcher uses BUILD_JOBS (default 4) so docker rebuilds during
the audit don't pin the host.
Build verified after every batch with ./build.sh tarball.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 files changed, 4206 insertions, 10126 deletions
@@ -72,11 +72,16 @@ if [ "${BUILD_MODE}" = "shell" ]; then fi echo "=== Starting build (mode: ${BUILD_MODE}) ===" +# BUILD_JOBS controls parallelism (override with `BUILD_JOBS=N ./build.sh`). +# Defaults to all available cores; set to 4 by default for the in-progress +# code-review pass to keep the host responsive. +BUILD_JOBS="${BUILD_JOBS:-4}" ${DOCKER} run --rm \ -v "${SCRIPT_DIR}:/src:rw" \ -v "${CCACHE_DIR}:/ccache:rw" \ -e CCACHE_DIR=/ccache \ -e BUILD_MODE="${BUILD_MODE}" \ + -e BUILD_JOBS="${BUILD_JOBS}" \ -w /src \ --device /dev/fuse \ --cap-add SYS_ADMIN \ diff --git a/docker/build-inner.sh b/docker/build-inner.sh index 3934889..15b2efd 100755 --- a/docker/build-inner.sh +++ b/docker/build-inner.sh @@ -39,7 +39,7 @@ cmake -S . -B build -G Ninja \ -DFLUORINE_BUILD_COMMIT="${FLUORINE_BUILD_COMMIT}" \ "${CMAKE_EXTRA_ARGS[@]}" -cmake --build build --parallel +cmake --build build --parallel "${BUILD_JOBS:-}" MODORG_BIN="build/src/src/ModOrganizer" if [ ! -f "${MODORG_BIN}" ]; then diff --git a/src/src/browserview.cpp b/src/src/browserview.cpp index ee9523f..7b085d8 100644 --- a/src/src/browserview.cpp +++ b/src/src/browserview.cpp @@ -26,9 +26,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QNetworkDiskCache>
#include <QWebEngineContextMenuRequest>
#include <QWebEngineSettings>
-#ifdef _WIN32
-#include <Shlwapi.h>
-#endif
BrowserView::BrowserView(QWidget* parent) : QWebEngineView(parent)
{
diff --git a/src/src/commandline.cpp b/src/src/commandline.cpp index bb08b4a..0d8b016 100644 --- a/src/src/commandline.cpp +++ b/src/src/commandline.cpp @@ -63,16 +63,13 @@ std::optional<int> CommandLine::process(const std::wstring& line) {
m_originalLine = line;
try {
-#ifdef _WIN32
- auto args = po::split_winmain(line);
-#else
- // Convert wstring args to vector<wstring> for compatibility with wcommand_line_parser
+ // Convert wstring args to vector<wstring> for compatibility with
+ // wcommand_line_parser.
auto narrow_args = po::split_unix(QString::fromStdWString(line).toStdString());
std::vector<std::wstring> args;
for (const auto& a : narrow_args) {
args.push_back(QString::fromStdString(a).toStdWString());
}
-#endif
if (!args.empty()) {
// remove program name
args.erase(args.begin());
@@ -212,11 +209,7 @@ bool CommandLine::forwardToPrimary(MOMultiProcess& multiProcess) } else if (m_nxmLink) {
multiProcess.sendMessage(*m_nxmLink);
} else if (m_command && m_command->canForwardToPrimary()) {
-#ifdef _WIN32
- multiProcess.sendMessage(QString::fromWCharArray(GetCommandLineW()));
-#else
multiProcess.sendMessage(QString::fromStdWString(m_originalLine));
-#endif
} else {
return false;
}
@@ -636,78 +629,11 @@ bool LaunchCommand::legacy() const std::optional<int> LaunchCommand::runEarly()
{
-#ifdef _WIN32
- // needs at least the working directory and process name
- if (untouched().size() < 2) {
- return 1;
- }
-
- std::vector<std::wstring> arg;
- auto args = UntouchedCommandLineArguments(2, arg);
-
- return SpawnWaitProcess(arg[1].c_str(), args);
-#else
- // The launch command is a Windows-specific internal command for spawning
- // processes from within the virtualized directory; not applicable on Linux
+ // The launch command is a Windows-specific internal command used to spawn
+ // processes from within USVFS; not applicable on Linux.
log::error("The 'launch' command is not supported on Linux");
return 1;
-#endif
-}
-
-#ifdef _WIN32
-int LaunchCommand::SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine)
-{
- PROCESS_INFORMATION pi{0};
- STARTUPINFO si{0};
- si.cb = sizeof(si);
- std::wstring commandLineCopy = commandLine;
-
- if (!CreateProcessW(NULL, &commandLineCopy[0], NULL, NULL, FALSE, 0, NULL,
- workingDirectory, &si, &pi)) {
- // A bit of a problem where to log the error message here, at least this way you can
- // get the message using a either DebugView or a live debugger:
- std::wostringstream ost;
- ost << L"CreateProcess failed: " << commandLine << ", " << GetLastError();
- OutputDebugStringW(ost.str().c_str());
- return -1;
- }
-
- WaitForSingleObject(pi.hProcess, INFINITE);
-
- DWORD exitCode = (DWORD)-1;
- ::GetExitCodeProcess(pi.hProcess, &exitCode);
- CloseHandle(pi.hThread);
- CloseHandle(pi.hProcess);
- return static_cast<int>(exitCode);
-}
-
-// Parses the first parseArgCount arguments of the current process command line and
-// returns them in parsedArgs, the rest of the command line is returned untouched.
-LPCWSTR
-LaunchCommand::UntouchedCommandLineArguments(int parseArgCount,
- std::vector<std::wstring>& parsedArgs)
-{
- LPCWSTR cmd = GetCommandLineW();
- LPCWSTR arg = nullptr; // to skip executable name
- for (; parseArgCount >= 0 && *cmd; ++cmd) {
- if (*cmd == '"') {
- int escaped = 0;
- for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd)
- escaped = *cmd == '\\' ? escaped + 1 : 0;
- }
- if (*cmd == ' ') {
- if (arg)
- if (cmd - 1 > arg && *arg == '"' && *(cmd - 1) == '"')
- parsedArgs.push_back(std::wstring(arg + 1, cmd - 1));
- else
- parsedArgs.push_back(std::wstring(arg, cmd));
- arg = cmd + 1;
- --parseArgCount;
- }
- }
- return cmd;
}
-#endif
Command::Meta RunCommand::meta() const
{
diff --git a/src/src/commandline.h b/src/src/commandline.h index b466a51..77016f0 100644 --- a/src/src/commandline.h +++ b/src/src/commandline.h @@ -169,13 +169,6 @@ public: protected:
Meta meta() const override;
std::optional<int> runEarly() override;
-
-#ifdef _WIN32
- int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine);
-
- LPCWSTR UntouchedCommandLineArguments(int parseArgCount,
- std::vector<std::wstring>& parsedArgs);
-#endif
};
// runs a program or an executable
diff --git a/src/src/datatab.cpp b/src/src/datatab.cpp index 19df5bb..4ef982f 100644 --- a/src/src/datatab.cpp +++ b/src/src/datatab.cpp @@ -48,10 +48,6 @@ DataTab::DataTab(OrganizerCore& core, PluginContainer& pc, QWidget* parent, ensureFullyLoaded();
});
-#ifdef _WIN32
- ui.browseVFS->setVisible(false);
- ui.browseRootBuilder->setVisible(false);
-#else
connect(ui.browseVFS, &QPushButton::clicked, [&] {
onBrowseVFS();
});
@@ -68,7 +64,6 @@ DataTab::DataTab(OrganizerCore& core, PluginContainer& pc, QWidget* parent, }
ui.browseRootBuilder->setVisible(rbEnabled);
}
-#endif
connect(ui.refresh, &QPushButton::clicked, [&] {
onRefresh();
@@ -153,7 +148,6 @@ void DataTab::onRefresh() void DataTab::onBrowseVFS()
{
-#ifndef _WIN32
QString dataPath = m_core.managedGame()->dataDirectory().absolutePath();
// Mount the FUSE VFS so the file manager sees the merged mod files.
@@ -178,12 +172,10 @@ void DataTab::onBrowseVFS() log::info("Unmounting VFS after Browse...");
m_core.unmountVFS();
-#endif
}
void DataTab::onBrowseRootBuilder()
{
-#ifndef _WIN32
QString gameRoot = m_core.managedGame()->gameDirectory().absolutePath();
// Mount the FUSE VFS which also triggers Root Builder deployment to the
@@ -208,7 +200,6 @@ void DataTab::onBrowseRootBuilder() log::info("Unmounting VFS after Root Builder browse...");
m_core.unmountVFS();
-#endif
}
void DataTab::updateTree()
diff --git a/src/src/editexecutablesdialog.cpp b/src/src/editexecutablesdialog.cpp index 4c6819d..c39eb0f 100644 --- a/src/src/editexecutablesdialog.cpp +++ b/src/src/editexecutablesdialog.cpp @@ -26,9 +26,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "ui_editexecutablesdialog.h"
#include <QMessageBox>
-#ifdef _WIN32
-#include <Shellapi.h>
-#endif
#include <algorithm>
#include <utility.h>
diff --git a/src/src/env.cpp b/src/src/env.cpp index 8674f39..b78d43f 100644 --- a/src/src/env.cpp +++ b/src/src/env.cpp @@ -1,1557 +1,473 @@ -#include "env.h"
-#include "envdump.h"
-#include "envmetrics.h"
-#include "envmodule.h"
-#include "envsecurity.h"
-#include "envshortcut.h"
-#include "envwindows.h"
-#include "settings.h"
-#include "shared/util.h"
-#include <log.h>
-#include <utility.h>
-
-#ifndef _WIN32
-#include <QTimeZone>
-#endif
-
-namespace env
-{
-
-using namespace MOBase;
-
-#ifdef _WIN32
-
-Console::Console() : m_hasConsole(false), m_in(nullptr), m_out(nullptr), m_err(nullptr)
-{
- // try to attach to parent
- if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
- if (GetLastError() != ERROR_ACCESS_DENIED) {
- // parent has no console, create one
- if (!AllocConsole()) {
- // failed, ignore
- return;
- }
- }
- }
-
- m_hasConsole = true;
-
- // redirect stdin, stdout and stderr to it
- freopen_s(&m_in, "CONIN$", "r", stdin);
- freopen_s(&m_out, "CONOUT$", "w", stdout);
- freopen_s(&m_err, "CONOUT$", "w", stderr);
-}
-
-Console::~Console()
-{
- // close redirected handles and redirect standard stream to NUL in case
- // they're used after this
-
- if (m_err) {
- std::fclose(m_err);
- freopen_s(&m_err, "NUL", "w", stderr);
- }
-
- if (m_out) {
- std::fclose(m_out);
- freopen_s(&m_out, "NUL", "w", stdout);
- }
-
- if (m_in) {
- std::fclose(m_in);
- freopen_s(&m_in, "NUL", "r", stdin);
- }
-
- // close console
- if (m_hasConsole) {
- FreeConsole();
- }
-}
-
-#else // !_WIN32
-
-Console::Console() : m_hasConsole(false), m_in(nullptr), m_out(nullptr), m_err(nullptr)
-{
- // on Linux, stdout/stderr are already available
-}
-
-Console::~Console()
-{
- // no-op on Linux
-}
-
-#endif // _WIN32
-
-ModuleNotification::ModuleNotification(QObject* o, std::function<void(Module)> f)
- : m_cookie(nullptr), m_object(o), m_f(std::move(f))
-{}
-
-#ifdef _WIN32
-
-ModuleNotification::~ModuleNotification()
-{
- if (!m_cookie) {
- return;
- }
-
- typedef NTSTATUS NTAPI LdrUnregisterDllNotificationType(PVOID Cookie);
-
- LibraryPtr ntdll(LoadLibraryW(L"ntdll.dll"));
-
- if (!ntdll) {
- log::error("failed to load ntdll.dll while unregistering for module notifications");
- return;
- }
-
- auto* LdrUnregisterDllNotification =
- reinterpret_cast<LdrUnregisterDllNotificationType*>(
- GetProcAddress(ntdll.get(), "LdrUnregisterDllNotification"));
-
- if (!LdrUnregisterDllNotification) {
- log::error("LdrUnregisterDllNotification not found in ntdll.dll");
- return;
- }
-
- const auto r = LdrUnregisterDllNotification(m_cookie);
- if (r != 0) {
- log::error("failed to unregister for module notifications, error {}", r);
- }
-}
-
-#else // !_WIN32
-
-ModuleNotification::~ModuleNotification()
-{
- // no-op on Linux (no ntdll DLL notification system)
-}
-
-#endif // _WIN32
-
-void ModuleNotification::setCookie(void* c)
-{
- m_cookie = c;
-}
-
-void ModuleNotification::fire(QString path, std::size_t fileSize)
-{
- if (m_loaded.contains(path)) {
- // don't notify if it's been loaded before
- return;
- }
-
- m_loaded.insert(path);
-
- // constructing a Module will query the version info of the file, which seems
- // to generate an access violation for at least plugin_python.dll on Windows 7
- //
- // it's not clear what the problem is, but making sure this is deferred until
- // _after_ the dll is loaded seems to fix it
- //
- // so this queues the callback in the main thread
-
- if (m_f) {
- QMetaObject::invokeMethod(
- m_object,
- [path, fileSize, f = m_f] {
- f(Module(path, fileSize));
- },
- Qt::QueuedConnection);
- }
-}
-
-Environment::Environment() {}
-
-// anchor
-Environment::~Environment() = default;
-
-const std::vector<Module>& Environment::loadedModules() const
-{
- if (m_modules.empty()) {
- m_modules = getLoadedModules();
- }
-
- return m_modules;
-}
-
-std::vector<Process> Environment::runningProcesses() const
-{
- return getRunningProcesses();
-}
-
-const WindowsInfo& Environment::windowsInfo() const
-{
- if (!m_windows) {
- m_windows.reset(new WindowsInfo);
- }
-
- return *m_windows;
-}
-
-const std::vector<SecurityProduct>& Environment::securityProducts() const
-{
- if (m_security.empty()) {
- m_security = getSecurityProducts();
- }
-
- return m_security;
-}
-
-const Metrics& Environment::metrics() const
-{
- if (!m_metrics) {
- m_metrics.reset(new Metrics);
- }
-
- return *m_metrics;
-}
-
-#ifdef _WIN32
-
-QString Environment::timezone() const
-{
- TIME_ZONE_INFORMATION tz = {};
-
- const auto r = GetTimeZoneInformation(&tz);
- if (r == TIME_ZONE_ID_INVALID) {
- const auto e = GetLastError();
- log::error("failed to get timezone, {}", formatSystemMessage(e));
- return "unknown";
- }
-
- auto offsetString = [](int o) {
- return QString("%1%2:%3")
- .arg(o < 0 ? "" : "+")
- .arg(QString::number(o / 60), 2, QChar::fromLatin1('0'))
- .arg(QString::number(o % 60), 2, QChar::fromLatin1('0'));
- };
-
- const auto stdName = QString::fromWCharArray(tz.StandardName);
- const auto stdOffset = -(tz.Bias + tz.StandardBias);
- const auto std = QString("%1, %2").arg(stdName).arg(offsetString(stdOffset));
-
- const auto dstName = QString::fromWCharArray(tz.DaylightName);
- const auto dstOffset = -(tz.Bias + tz.DaylightBias);
- const auto dst = QString("%1, %2").arg(dstName).arg(offsetString(dstOffset));
-
- QString s;
-
- if (r == TIME_ZONE_ID_DAYLIGHT) {
- s = dst + " (dst is active, std is " + std + ")";
- } else {
- s = std + " (std is active, dst is " + dst + ")";
- }
-
- return s;
-}
-
-#else // !_WIN32
-
-QString Environment::timezone() const
-{
- QTimeZone tz = QTimeZone::systemTimeZone();
- if (!tz.isValid()) {
- log::error("failed to get system timezone");
- return "unknown";
- }
-
- return QString::fromUtf8(tz.id());
-}
-
-#endif // _WIN32
-
-#ifdef _WIN32
-
-std::unique_ptr<ModuleNotification>
-Environment::onModuleLoaded(QObject* o, std::function<void(Module)> f)
-{
- typedef struct _UNICODE_STRING
- {
- USHORT Length;
- USHORT MaximumLength;
- PWSTR Buffer;
- } UNICODE_STRING, *PUNICODE_STRING;
-
- typedef const PUNICODE_STRING PCUNICODE_STRING;
-
- typedef struct _LDR_DLL_LOADED_NOTIFICATION_DATA
- {
- ULONG Flags; // Reserved.
- PCUNICODE_STRING FullDllName; // The full path name of the DLL module.
- PCUNICODE_STRING BaseDllName; // The base file name of the DLL module.
- PVOID DllBase; // A pointer to the base address for the DLL in memory.
- ULONG SizeOfImage; // The size of the DLL image, in bytes.
- } LDR_DLL_LOADED_NOTIFICATION_DATA, *PLDR_DLL_LOADED_NOTIFICATION_DATA;
-
- typedef struct _LDR_DLL_UNLOADED_NOTIFICATION_DATA
- {
- ULONG Flags; // Reserved.
- PCUNICODE_STRING FullDllName; // The full path name of the DLL module.
- PCUNICODE_STRING BaseDllName; // The base file name of the DLL module.
- PVOID DllBase; // A pointer to the base address for the DLL in memory.
- ULONG SizeOfImage; // The size of the DLL image, in bytes.
- } LDR_DLL_UNLOADED_NOTIFICATION_DATA, *PLDR_DLL_UNLOADED_NOTIFICATION_DATA;
-
- typedef union _LDR_DLL_NOTIFICATION_DATA
- {
- LDR_DLL_LOADED_NOTIFICATION_DATA Loaded;
- LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded;
- } LDR_DLL_NOTIFICATION_DATA, *PLDR_DLL_NOTIFICATION_DATA;
-
- typedef VOID CALLBACK LDR_DLL_NOTIFICATION_FUNCTION(
- ULONG NotificationReason, const PLDR_DLL_NOTIFICATION_DATA NotificationData,
- PVOID Context);
-
- typedef LDR_DLL_NOTIFICATION_FUNCTION* PLDR_DLL_NOTIFICATION_FUNCTION;
-
- typedef NTSTATUS NTAPI LdrRegisterDllNotificationType(
- ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context,
- PVOID * Cookie);
-
- const ULONG LDR_DLL_NOTIFICATION_REASON_LOADED = 1;
- const ULONG LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2;
-
- // loading ntdll.dll, the function will be found with GetProcAddress()
- LibraryPtr ntdll(LoadLibraryW(L"ntdll.dll"));
-
- if (!ntdll) {
- log::error("failed to load ntdll.dll while registering for module notifications");
- return {};
- }
-
- auto* LdrRegisterDllNotification = reinterpret_cast<LdrRegisterDllNotificationType*>(
- GetProcAddress(ntdll.get(), "LdrRegisterDllNotification"));
-
- if (!LdrRegisterDllNotification) {
- log::error("LdrRegisterDllNotification not found in ntdll.dll");
- return {};
- }
-
- auto context = std::make_unique<ModuleNotification>(o, f);
- void* cookie = nullptr;
-
- auto OnDllLoaded = [](ULONG reason, const PLDR_DLL_NOTIFICATION_DATA data,
- void* context) {
- if (reason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
- if (data && data->Loaded.FullDllName) {
- if (context) {
- static_cast<ModuleNotification*>(context)->fire(
- QString::fromWCharArray(data->Loaded.FullDllName->Buffer,
- data->Loaded.FullDllName->Length /
- sizeof(wchar_t)),
- data->Loaded.SizeOfImage);
- }
- }
- }
- };
-
- const auto r = LdrRegisterDllNotification(0, OnDllLoaded, context.get(), &cookie);
-
- if (r != 0) {
- log::error("failed to register for module notifications, error {}", r);
- return {};
- }
-
- context->setCookie(cookie);
-
- return context;
-}
-
-#else // !_WIN32
-
-std::unique_ptr<ModuleNotification>
-Environment::onModuleLoaded(QObject*, std::function<void(Module)>)
-{
- // no DLL notification system on Linux
- return {};
-}
-
-#endif // _WIN32
-
-void Environment::dump(const Settings& s) const
-{
- log::debug("windows: {}", windowsInfo().toString());
-
- log::debug("time zone: {}", timezone());
-
- if (windowsInfo().compatibilityMode()) {
- log::warn("MO seems to be running in compatibility mode");
- }
-
- log::debug("security products:");
-
- {
- // ignore products with identical names, some AVs register themselves with
- // the same names and provider, but different guids
- std::set<QString> productNames;
- for (const auto& sp : securityProducts()) {
- productNames.insert(sp.toString());
- }
-
- for (auto&& name : productNames) {
- log::debug(" . {}", name);
- }
- }
-
- log::debug("modules loaded in process:");
- for (const auto& m : loadedModules()) {
- if (m.interesting()) {
- log::debug(" . {}", m.toString());
- }
- }
-
- log::debug("displays:");
- for (const auto& d : metrics().displays()) {
- log::debug(" . {}", d.toString());
- }
-
- const auto r = metrics().desktopGeometry();
- log::debug("desktop geometry: ({},{})-({},{})", r.left(), r.top(), r.right(),
- r.bottom());
-
- dumpDisks(s);
-}
-
-void Environment::dumpDisks(const Settings& s) const
-{
- std::set<QString> rootPaths;
-
- auto dump = [&](auto&& path) {
- const QFileInfo fi(path);
- const QStorageInfo si(fi.absoluteFilePath());
-
- if (rootPaths.contains(si.rootPath())) {
- // already seen
- return;
- }
-
- // remember
- rootPaths.insert(si.rootPath());
-
- log::debug(" . {} free={} MB{}", si.rootPath(), (si.bytesFree() / 1000 / 1000),
- (si.isReadOnly() ? " (readonly)" : ""));
- };
-
- log::debug("drives:");
-
- dump(QStorageInfo::root().rootPath());
- dump(s.paths().base());
- dump(s.paths().downloads());
- dump(s.paths().mods());
- dump(s.paths().cache());
- dump(s.paths().profiles());
- dump(s.paths().overwrite());
- dump(QCoreApplication::applicationDirPath());
-}
-
-QString path()
-{
- return get("PATH");
-}
-
-#ifdef _WIN32
-
-QString appendToPath(const QString& s)
-{
- auto old = path();
- set("PATH", old + ";" + s);
- return old;
-}
-
-QString prependToPath(const QString& s)
-{
- auto old = path();
- set("PATH", s + ";" + old);
- return old;
-}
-
-#else // !_WIN32
-
-QString appendToPath(const QString& s)
-{
- auto old = path();
- set("PATH", old + ":" + s);
- return old;
-}
-
-QString prependToPath(const QString& s)
-{
- auto old = path();
- set("PATH", s + ":" + old);
- return old;
-}
-
-#endif // _WIN32
-
-void setPath(const QString& s)
-{
- set("PATH", s);
-}
-
-#ifdef _WIN32
-
-QString get(const QString& name)
-{
- std::size_t bufferSize = 4000;
- auto buffer = std::make_unique<wchar_t[]>(bufferSize);
-
- DWORD realSize = ::GetEnvironmentVariableW(name.toStdWString().c_str(), buffer.get(),
- static_cast<DWORD>(bufferSize));
-
- if (realSize > bufferSize) {
- bufferSize = realSize;
- buffer = std::make_unique<wchar_t[]>(bufferSize);
-
- realSize = ::GetEnvironmentVariableW(name.toStdWString().c_str(), buffer.get(),
- static_cast<DWORD>(bufferSize));
- }
-
- 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);
-}
-
-void set(const QString& n, const QString& v)
-{
- ::SetEnvironmentVariableW(n.toStdWString().c_str(), v.toStdWString().c_str());
-}
-
-#else // !_WIN32
-
-QString get(const QString& name)
-{
- return qEnvironmentVariable(name.toUtf8());
-}
-
-void set(const QString& n, const QString& v)
-{
- qputenv(n.toUtf8(), v.toUtf8());
-}
-
-#endif // _WIN32
-
-Service::Service(QString name) : Service(std::move(name), StartType::None, Status::None)
-{}
-
-Service::Service(QString name, StartType st, Status s)
- : m_name(std::move(name)), m_startType(st), m_status(s)
-{}
-
-const QString& Service::name() const
-{
- return m_name;
-}
-
-bool Service::isValid() const
-{
- return (m_startType != StartType::None) && (m_status != Status::None);
-}
-
-Service::StartType Service::startType() const
-{
- return m_startType;
-}
-
-Service::Status Service::status() const
-{
- return m_status;
-}
-
-QString Service::toString() const
-{
- return QString("service '%1', start=%2, status=%3")
- .arg(m_name)
- .arg(env::toString(m_startType))
- .arg(env::toString(m_status));
-}
-
-QString toString(Service::StartType st)
-{
- using ST = Service::StartType;
-
- switch (st) {
- case ST::None:
- return "none";
-
- case ST::Disabled:
- return "disabled";
-
- case ST::Enabled:
- return "enabled";
-
- default:
- return QString("unknown %1").arg(static_cast<int>(st));
- }
-}
-
-QString toString(Service::Status st)
-{
- using S = Service::Status;
-
- switch (st) {
- case S::None:
- return "none";
-
- case S::Stopped:
- return "stopped";
-
- case S::Running:
- return "running";
-
- default:
- return QString("unknown %1").arg(static_cast<int>(st));
- }
-}
-
-#ifdef _WIN32
-
-Service::StartType getServiceStartType(SC_HANDLE s, const QString& name)
-{
- DWORD needed = 0;
-
- if (!QueryServiceConfig(s, NULL, 0, &needed)) {
- const auto e = GetLastError();
-
- if (e != ERROR_INSUFFICIENT_BUFFER) {
- log::error("QueryServiceConfig() for size for '{}' failed, {}", name,
- GetLastError());
-
- return Service::StartType::None;
- }
- }
-
- const auto size = needed;
- MallocPtr<QUERY_SERVICE_CONFIG> config(
- static_cast<QUERY_SERVICE_CONFIG*>(std::malloc(size)));
-
- if (!QueryServiceConfig(s, config.get(), size, &needed)) {
- const auto e = GetLastError();
-
- log::error("QueryServiceConfig() for '{}' failed", name, formatSystemMessage(e));
-
- return Service::StartType::None;
- }
-
- switch (config->dwStartType) {
- case SERVICE_AUTO_START: // fall-through
- case SERVICE_BOOT_START:
- case SERVICE_DEMAND_START:
- case SERVICE_SYSTEM_START: {
- return Service::StartType::Enabled;
- }
-
- case SERVICE_DISABLED: {
- return Service::StartType::Disabled;
- }
-
- default: {
- log::error("unknown service start type {} for '{}'", config->dwStartType, name);
-
- return Service::StartType::None;
- }
- }
-}
-
-Service::Status getServiceStatus(SC_HANDLE s, const QString& name)
-{
- DWORD needed = 0;
-
- if (!QueryServiceStatusEx(s, SC_STATUS_PROCESS_INFO, NULL, 0, &needed)) {
- const auto e = GetLastError();
-
- if (e != ERROR_INSUFFICIENT_BUFFER) {
- log::error("QueryServiceStatusEx() for size for '{}' failed, {}", name,
- GetLastError());
-
- return Service::Status::None;
- }
- }
-
- const auto size = needed;
- MallocPtr<SERVICE_STATUS_PROCESS> status(
- static_cast<SERVICE_STATUS_PROCESS*>(std::malloc(size)));
-
- const auto r = QueryServiceStatusEx(
- s, SC_STATUS_PROCESS_INFO, reinterpret_cast<BYTE*>(status.get()), size, &needed);
-
- if (!r) {
- const auto e = GetLastError();
-
- log::error("QueryServiceStatusEx() failed for '{}', {}", name,
- formatSystemMessage(e));
-
- return Service::Status::None;
- }
-
- switch (status->dwCurrentState) {
- case SERVICE_START_PENDING: // fall-through
- case SERVICE_CONTINUE_PENDING:
- case SERVICE_RUNNING: {
- return Service::Status::Running;
- }
-
- case SERVICE_STOPPED: // fall-through
- case SERVICE_STOP_PENDING:
- case SERVICE_PAUSE_PENDING:
- case SERVICE_PAUSED: {
- return Service::Status::Stopped;
- }
-
- default: {
- log::error("unknown service status {} for '{}'", status->dwCurrentState, name);
-
- return Service::Status::None;
- }
- }
-}
-
-Service getService(const QString& name)
-{
- // service manager
- const LocalPtr<SC_HANDLE> scm(
- OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG));
-
- if (!scm) {
- const auto e = GetLastError();
- log::error("OpenSCManager() failed, {}", formatSystemMessage(e));
- return Service(name);
- }
-
- // service
- const LocalPtr<SC_HANDLE> s(OpenService(scm.get(), name.toStdWString().c_str(),
- SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG));
-
- if (!s) {
- const auto e = GetLastError();
- log::error("OpenService() failed for '{}', {}", name, formatSystemMessage(e));
- return Service(name);
- }
-
- const auto startType = getServiceStartType(s.get(), name);
- const auto status = getServiceStatus(s.get(), name);
-
- return {name, startType, status};
-}
-
-#else // !_WIN32
-
-Service getService(const QString& name)
-{
- // no Windows SCM on Linux, return invalid service
- return Service(name);
-}
-
-#endif // _WIN32
-
-#ifdef _WIN32
-
-std::optional<QString> getAssocString(const QFileInfo& file, ASSOCSTR astr)
-{
- const auto ext = L"." + file.suffix().toStdWString();
-
- // getting buffer size
- DWORD bufferSize = 0;
- auto r = AssocQueryStringW(ASSOCF_INIT_IGNOREUNKNOWN, astr, ext.c_str(), L"open",
- nullptr, &bufferSize);
-
- // returns S_FALSE when giving back the buffer size, so that's actually the
- // expected return value
-
- if (r != S_FALSE || bufferSize == 0) {
- if (r == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) {
- log::error("file '{}' has no associated executable", file.absoluteFilePath());
- } else {
- log::error("can't get buffer size for AssocQueryStringW(), {}",
- formatSystemMessage(r));
- }
- return {};
- }
-
- // getting string
- auto buffer = std::make_unique<wchar_t[]>(bufferSize + 1);
- std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0);
-
- r = AssocQueryStringW(ASSOCF_INIT_IGNOREUNKNOWN, astr, ext.c_str(), L"open",
- buffer.get(), &bufferSize);
-
- if (FAILED(r)) {
- log::error("failed to get exe associated with '{}', {}", file.suffix(),
- formatSystemMessage(r));
-
- return {};
- }
-
- // buffer size includes the null terminator
- return QString::fromWCharArray(buffer.get(), bufferSize - 1);
-}
-
-QString formatCommandLine(const QFileInfo& targetInfo, const QString& cmd)
-{
- // yeah, FormatMessage() expects at least as many arguments as there are
- // placeholders and while the command for associations should typically only
- // have %1, the user can actually enter anything in the registry
- //
- // since the maximum number of arguments is 99, this creates an array of 99
- // wchar_* where the first one (%1) points to the filename and the remaining
- // 98 to ""
- //
- // FormatMessage() actually takes a va_list* for the arguments, but by passing
- // FORMAT_MESSAGE_ARGUMENT_ARRAY, an array of DWORD_PTR can be given instead
-
- // 99 arguments
- std::array<DWORD_PTR, 99> args;
-
- // first one is the filename
- const auto wpath = targetInfo.absoluteFilePath().toStdWString();
- args[0] = reinterpret_cast<DWORD_PTR>(wpath.c_str());
-
- // remaining are ""
- std::fill(args.begin() + 1, args.end(), reinterpret_cast<DWORD_PTR>(L""));
-
- // must be freed with LocalFree()
- wchar_t* buffer = nullptr;
-
- const auto wcmd = cmd.toStdWString();
-
- const auto n =
- ::FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_ARGUMENT_ARRAY |
- FORMAT_MESSAGE_FROM_STRING,
- wcmd.c_str(), 0, 0, reinterpret_cast<LPWSTR>(&buffer), 0,
- reinterpret_cast<va_list*>(&args[0]));
-
- if (n == 0 || !buffer) {
- const auto e = GetLastError();
-
- log::error("failed to format command line '{}' with path '{}', {}", cmd,
- targetInfo.absoluteFilePath(), formatSystemMessage(e));
-
- return {};
- }
-
- auto s = QString::fromWCharArray(buffer, n);
- ::LocalFree(buffer);
-
- return s.trimmed();
-}
-
-std::pair<QString, QString> splitExeAndArguments(const QString& cmd)
-{
- int exeBegin = 0;
- int exeEnd = -1;
-
- if (cmd[0] == '"') {
- // surrounded by double-quotes, so find the next one
- exeBegin = 1;
- exeEnd = cmd.indexOf('"', exeBegin);
-
- if (exeEnd == -1) {
- log::error("missing terminating double-quote in command line '{}'", cmd);
- return {};
- }
- } else {
- // no double-quotes, find the first whitespace
- exeEnd = cmd.indexOf(QRegularExpression("\\s"));
- if (exeEnd == -1) {
- exeEnd = cmd.size();
- }
- }
-
- QString exe = cmd.mid(exeBegin, exeEnd - exeBegin).trimmed();
- QString args = cmd.mid(exeEnd + 1).trimmed();
-
- return {std::move(exe), std::move(args)};
-}
-
-Association getAssociation(const QFileInfo& targetInfo)
-{
- log::debug("getting association for '{}', extension is '.{}'",
- targetInfo.absoluteFilePath(), targetInfo.suffix());
-
- const auto cmd = getAssocString(targetInfo, ASSOCSTR_COMMAND);
- if (!cmd) {
- return {};
- }
-
- log::debug("raw cmd is '{}'", *cmd);
-
- QString formattedCmd = formatCommandLine(targetInfo, *cmd);
- if (formattedCmd.isEmpty()) {
- log::error("command line associated with '{}' is empty",
- targetInfo.absoluteFilePath());
-
- return {};
- }
-
- log::debug("formatted cmd is '{}'", formattedCmd);
-
- const auto p = splitExeAndArguments(formattedCmd);
- if (p.first.isEmpty()) {
- return {};
- }
-
- log::debug("split into exe='{}' and cmd='{}'", p.first, p.second);
-
- return {QFileInfo(p.first), *cmd, p.second};
-}
-
-#else // !_WIN32
-
-Association getAssociation(const QFileInfo& targetInfo)
-{
- log::debug("getting association for '{}', extension is '.{}'",
- targetInfo.absoluteFilePath(), targetInfo.suffix());
-
- // try xdg-mime to find the default application
- const QString mimeType = "application/x-" + targetInfo.suffix();
-
- QProcess xdgMime;
- xdgMime.start("xdg-mime", QStringList() << "query" << "default" << mimeType);
-
- if (!xdgMime.waitForFinished(3000)) {
- log::debug("xdg-mime query timed out for '{}'", targetInfo.suffix());
- return {};
- }
-
- const QString desktopFile = QString::fromUtf8(xdgMime.readAllStandardOutput()).trimmed();
- if (desktopFile.isEmpty()) {
- log::debug("no association found for '{}'", targetInfo.suffix());
- return {};
- }
-
- log::debug("associated desktop file: '{}'", desktopFile);
-
- return {};
-}
-
-#endif // _WIN32
-
-#ifdef _WIN32
-
-struct RegistryKeyCloser
-{
- using pointer = HKEY;
-
- void operator()(HKEY key)
- {
- if (key != 0) {
- ::RegCloseKey(key);
- }
- }
-};
-
-using RegistryKeyPtr = std::unique_ptr<HKEY, RegistryKeyCloser>;
-
-RegistryKeyPtr openRegistryKey(HKEY parent, const wchar_t* name)
-{
- HKEY subkey = 0;
-
- auto r = ::RegOpenKeyExW(parent, name, 0,
- KEY_SET_VALUE | KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE,
- &subkey);
-
- if (r != ERROR_SUCCESS) {
- return {};
- }
-
- return RegistryKeyPtr(subkey);
-}
-
-bool keyHasValues(HKEY key)
-{
- auto name = std::make_unique<wchar_t[]>(1000 + 1);
- DWORD nameSize = 1000;
-
- // note that RegEnumValueW() also enumerates the default value if it exists
- auto r = ::RegEnumValueW(key, 0, name.get(), &nameSize, nullptr, nullptr, nullptr,
- nullptr);
-
- if (r != ERROR_NO_MORE_ITEMS) {
- return true;
- }
-
- // no values, no default, it's empty
- return false;
-}
-
-bool forEachSubKey(HKEY key, std::function<bool(const wchar_t* name)> f)
-{
- auto name = std::make_unique<wchar_t[]>(1000 + 1);
- DWORD nameSize = 1000;
-
- DWORD i = 0;
-
- // something would be really wrong if it had more than 100 keys
- while (i < 100) {
- auto r = ::RegEnumKeyExW(key, i, name.get(), &nameSize, nullptr, nullptr, nullptr,
- nullptr);
-
- if (r == ERROR_NO_MORE_ITEMS) {
- // no more subkeys
- break;
- }
-
- if (r == ERROR_SUCCESS) {
- // a subkey exists
- auto subkey = openRegistryKey(key, name.get());
-
- if (!subkey) {
- // can't open it, stop
- return false;
- }
-
- // fire callback
- if (!f(name.get())) {
- return false;
- }
- } else {
- // something went wrong, stop
- return false;
- }
-
- ++i;
- }
-
- return true;
-}
-
-bool isKeyEmpty(HKEY key)
-{
- // check for any values
- if (keyHasValues(key)) {
- return false;
- }
-
- auto r = forEachSubKey(key, [&](const wchar_t* name) {
- // a subkey exists, recursively check if it's empty
- auto subkey = openRegistryKey(key, name);
-
- if (!subkey) {
- // can't open, stop
- return false;
- }
-
- if (!isKeyEmpty(subkey.get())) {
- // not empty, stop
- return false;
- }
-
- // empty, go on
- return true;
- });
-
- if (!r) {
- // something went wrong or some subkey has values
- return false;
- }
-
- // key has no values and has either no subkeys or all subkeys are empty
- return true;
-}
-
-void deleteRegistryKeyIfEmpty(const QString& name)
-{
- if (name.isEmpty()) {
- return;
- }
-
- auto key = openRegistryKey(HKEY_CURRENT_USER, name.toStdWString().c_str());
- if (!key) {
- return;
- }
-
- if (!isKeyEmpty(key.get())) {
- return;
- }
-
- ::RegDeleteTreeW(HKEY_CURRENT_USER, name.toStdWString().c_str());
-}
-
-bool registryValueExists(const QString& keyName, const QString& valueName)
-{
- auto key = openRegistryKey(HKEY_CURRENT_USER, keyName.toStdWString().c_str());
-
- if (!key) {
- return false;
- }
-
- DWORD type = 0;
-
- auto r = ::RegQueryValueExW(key.get(), valueName.toStdWString().c_str(), nullptr,
- &type, nullptr, nullptr);
-
- return (r == ERROR_SUCCESS);
-}
-
-#else // !_WIN32
-
-void deleteRegistryKeyIfEmpty(const QString&)
-{
- // no registry on Linux
-}
-
-bool registryValueExists(const QString&, const QString&)
-{
- // no registry on Linux
- return false;
-}
-
-#endif // _WIN32
-
-#ifdef _WIN32
-
-// returns the filename of the given process or the current one
-//
-std::filesystem::path processPath(HANDLE process = INVALID_HANDLE_VALUE)
-{
- // double the buffer size 10 times
- const int MaxTries = 10;
-
- DWORD bufferSize = MAX_PATH;
-
- for (int tries = 0; tries < MaxTries; ++tries) {
- auto buffer = std::make_unique<wchar_t[]>(bufferSize + 1);
- std::fill(buffer.get(), buffer.get() + bufferSize + 1, 0);
-
- DWORD writtenSize = 0;
-
- if (process == INVALID_HANDLE_VALUE) {
- // query this process
- writtenSize = GetModuleFileNameW(0, buffer.get(), bufferSize);
- } else {
- // query another process
- writtenSize = GetModuleBaseNameW(process, 0, buffer.get(), bufferSize);
- }
-
- if (writtenSize == 0) {
- // hard failure
- const auto e = GetLastError();
- std::wcerr << formatSystemMessage(e) << L"\n";
- break;
- } else if (writtenSize >= bufferSize) {
- // buffer is too small, try again
- bufferSize *= 2;
- } else {
- // if GetModuleFileName() works, `writtenSize` does not include the null
- // terminator
- const std::wstring s(buffer.get(), writtenSize);
- const std::filesystem::path path(s);
-
- return path;
- }
- }
-
- // something failed or the path is way too long to make sense
-
- std::wstring what;
- if (process == INVALID_HANDLE_VALUE) {
- what = L"the current process";
- } else {
- what = L"pid " + std::to_wstring(reinterpret_cast<std::uintptr_t>(process));
- }
-
- std::wcerr << L"failed to get filename for " << what << L"\n";
- return {};
-}
-
-std::wstring processFilename(HANDLE process = INVALID_HANDLE_VALUE)
-{
- const auto p = processPath(process);
- if (p.empty()) {
- return {};
- }
-
- return p.filename().native();
-}
-
-std::filesystem::path thisProcessPath()
-{
- return processPath();
-}
-
-#else // !_WIN32
-
-std::filesystem::path thisProcessPath()
-{
- char buf[PATH_MAX] = {};
- ssize_t len = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1);
- if (len <= 0) {
- std::cerr << "failed to readlink /proc/self/exe: " << strerror(errno) << "\n";
- return {};
- }
-
- buf[len] = '\0';
- return std::filesystem::path(buf);
-}
-
-#endif // _WIN32
-
-#ifdef _WIN32
-
-DWORD findOtherPid()
-{
- const std::wstring defaultName = L"ModOrganizer.exe";
-
- std::wclog << L"looking for the other process...\n";
-
- // used to skip the current process below
- const auto thisPid = GetCurrentProcessId();
- std::wclog << L"this process id is " << thisPid << L"\n";
-
- // getting the filename for this process, assumes the other process has the
- // same one
- auto filename = processFilename();
- if (filename.empty()) {
- std::wcerr << L"can't get current process filename, defaulting to " << defaultName
- << L"\n";
-
- filename = defaultName;
- } else {
- std::wclog << L"this process filename is " << filename << L"\n";
- }
-
- // getting all running processes
- const auto processes = getRunningProcesses();
- std::wclog << L"there are " << processes.size() << L" processes running\n";
-
- // going through processes, trying to find one with the same name and a
- // different pid than this process has
- for (const auto& p : processes) {
- if (p.name().toStdWString() == filename) {
- if (p.pid() != thisPid) {
- return p.pid();
- }
- }
- }
-
- std::wclog << L"no process with this filename\n"
- << L"MO may not be running, or it may be running as administrator\n"
- << L"you can try running this again as administrator\n";
-
- return 0;
-}
-
-#else // !_WIN32
-
-pid_t findOtherPid()
-{
- std::clog << "looking for the other process...\n";
-
- const pid_t thisPid = ::getpid();
- std::clog << "this process id is " << thisPid << "\n";
-
- // scan /proc/*/comm for "ModOrganizer"
- const std::string targetName = "ModOrganizer";
-
- for (const auto& entry : std::filesystem::directory_iterator("/proc")) {
- if (!entry.is_directory()) {
- continue;
- }
-
- const auto pidStr = entry.path().filename().string();
-
- // check if the directory name is a number (a PID)
- bool isNumber = true;
- for (char c : pidStr) {
- if (!std::isdigit(static_cast<unsigned char>(c))) {
- isNumber = false;
- break;
- }
- }
- if (!isNumber) {
- continue;
- }
-
- pid_t pid = std::stoi(pidStr);
- if (pid == thisPid) {
- continue;
- }
-
- std::ifstream commFile(entry.path() / "comm");
- if (!commFile.is_open()) {
- continue;
- }
-
- std::string comm;
- std::getline(commFile, comm);
-
- if (comm == targetName) {
- std::clog << "found other process with pid " << pid << "\n";
- return pid;
- }
- }
-
- std::clog << "no process with this filename\n";
- return 0;
-}
-
-#endif // _WIN32
-
-#ifdef _WIN32
-
-std::wstring tempDir()
-{
- const DWORD bufferSize = MAX_PATH + 1;
- wchar_t buffer[bufferSize + 1] = {};
-
- const auto written = GetTempPathW(bufferSize, buffer);
- if (written == 0) {
- const auto e = GetLastError();
-
- std::wcerr << L"failed to get temp path, " << formatSystemMessage(e) << L"\n";
-
- return {};
- }
-
- // `written` does not include the null terminator
- return std::wstring(buffer, buffer + written);
-}
-
-std::wstring safeVersion()
-{
- try {
- // this can throw
- return MOShared::createVersionInfo().string().toStdWString() + L"-";
- } catch (...) {
- return {};
- }
-}
-
-HandlePtr tempFile(const std::wstring dir)
-{
- // maximum tries of incrementing the counter
- const int MaxTries = 100;
-
- // UTC time and date will be in the filename
- const auto now = std::time(0);
- const auto tm = std::gmtime(&now);
-
- // "ModOrganizer-YYYYMMDDThhmmss.dmp", with a possible "-i" appended, where
- // i can go until MaxTries
- std::wostringstream oss;
- oss << L"ModOrganizer-" << safeVersion() << std::setw(4) << (1900 + tm->tm_year)
- << std::setw(2) << std::setfill(L'0') << (tm->tm_mon + 1) << std::setw(2)
- << std::setfill(L'0') << tm->tm_mday << "T" << std::setw(2) << std::setfill(L'0')
- << tm->tm_hour << std::setw(2) << std::setfill(L'0') << tm->tm_min << std::setw(2)
- << std::setfill(L'0') << tm->tm_sec;
-
- const std::wstring prefix = oss.str();
- const std::wstring ext = L".dmp";
-
- // first path to try, without counter in it
- std::wstring path = dir + L"\\" + prefix + ext;
-
- for (int i = 0; i < MaxTries; ++i) {
- std::wclog << L"trying file '" << path << L"'\n";
-
- HandlePtr h(CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
- FILE_ATTRIBUTE_NORMAL, nullptr));
-
- if (h.get() != INVALID_HANDLE_VALUE) {
- // worked
- return h;
- }
-
- const auto e = GetLastError();
-
- if (e != ERROR_FILE_EXISTS) {
- // probably no write access
- std::wcerr << L"failed to create dump file, " << formatSystemMessage(e) << L"\n";
-
- return {};
- }
-
- // try again with "-i"
- path = dir + L"\\" + prefix + L"-" + std::to_wstring(i + 1) + ext;
- }
-
- std::wcerr << L"can't create dump file, ran out of filenames\n";
- return {};
-}
-
-HandlePtr dumpFile(const wchar_t* dir)
-{
- // try the given directory, if any
- if (dir) {
- HandlePtr h = tempFile(dir);
- if (h.get() != INVALID_HANDLE_VALUE) {
- return h;
- }
- }
-
- // try the current directory
- HandlePtr h = tempFile(L".");
- if (h.get() != INVALID_HANDLE_VALUE) {
- return h;
- }
-
- std::wclog << L"cannot write dump file in current directory\n";
-
- // try the temp directory
- const auto temp = tempDir();
-
- if (!temp.empty()) {
- h = tempFile(temp.c_str());
- if (h.get() != INVALID_HANDLE_VALUE) {
- return h;
- }
- }
-
- return {};
-}
-
-#endif // _WIN32
-
-CoreDumpTypes coreDumpTypeFromString(const std::string& s)
-{
- if (s == "data")
- return env::CoreDumpTypes::Data;
- else if (s == "full")
- return env::CoreDumpTypes::Full;
- else
- return env::CoreDumpTypes::Mini;
-}
-
-std::string toString(CoreDumpTypes type)
-{
- switch (type) {
- case CoreDumpTypes::Mini:
- return "mini";
-
- case CoreDumpTypes::Data:
- return "data";
-
- case CoreDumpTypes::Full:
- return "full";
-
- default:
- return "?";
- }
-}
-
-#ifdef _WIN32
-
-bool createMiniDump(const wchar_t* dir, HANDLE process, CoreDumpTypes type)
-{
- const DWORD pid = GetProcessId(process);
-
- const HandlePtr file = dumpFile(dir);
- if (!file) {
- std::wcerr << L"nowhere to write the dump file\n";
- return false;
- }
-
- auto flags =
- _MINIDUMP_TYPE(MiniDumpNormal | MiniDumpWithHandleData |
- MiniDumpWithUnloadedModules | MiniDumpWithProcessThreadData);
-
- if (type == CoreDumpTypes::Data) {
- std::wclog << L"writing minidump with data\n";
- flags = _MINIDUMP_TYPE(flags | MiniDumpWithDataSegs);
- } else if (type == CoreDumpTypes::Full) {
- std::wclog << L"writing full minidump\n";
- flags = _MINIDUMP_TYPE(flags | MiniDumpWithFullMemory);
- } else {
- std::wclog << L"writing mini minidump\n";
- }
-
- const auto ret =
- MiniDumpWriteDump(process, pid, file.get(), flags, nullptr, nullptr, nullptr);
-
- if (!ret) {
- const auto e = GetLastError();
-
- std::wcerr << L"failed to write mini dump, " << formatSystemMessage(e) << L"\n";
-
- return false;
- }
-
- std::wclog << L"minidump written correctly\n";
- return true;
-}
-
-bool coredump(const wchar_t* dir, CoreDumpTypes type)
-{
- std::wclog << L"creating minidump for the current process\n";
- return createMiniDump(dir, GetCurrentProcess(), type);
-}
-
-bool coredumpOther(CoreDumpTypes type)
-{
- std::wclog << L"creating minidump for a running process\n";
-
- const auto pid = findOtherPid();
- if (pid == 0) {
- std::wcerr << L"no other process found\n";
- return false;
- }
-
- std::wclog << L"found other process with pid " << pid << L"\n";
-
- HandlePtr handle(
- OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid));
-
- if (!handle) {
- const auto e = GetLastError();
-
- std::wcerr << L"failed to open process " << pid << L", " << formatSystemMessage(e)
- << L"\n";
-
- return false;
- }
-
- return createMiniDump(nullptr, handle.get(), type);
-}
-
-#else // !_WIN32
-
-bool coredump(const char* dir, CoreDumpTypes type)
-{
- std::clog << "coredump requested (type: " << toString(type) << ")\n";
-
- if (dir) {
- std::clog << "dump directory: " << dir << "\n";
- }
-
- // on Linux, call abort() to generate a core dump (if ulimit allows)
- std::clog << "calling abort() to generate core dump\n";
- std::abort();
-
- // unreachable
- return false;
-}
-
-bool coredumpOther(CoreDumpTypes type)
-{
- std::clog << "creating core dump for a running process\n";
-
- const auto pid = findOtherPid();
- if (pid == 0) {
- std::cerr << "no other process found\n";
- return false;
- }
-
- std::clog << "found other process with pid " << pid << "\n";
-
- // send SIGABRT to the other process to trigger a core dump
- if (::kill(pid, SIGABRT) != 0) {
- std::cerr << "failed to send SIGABRT to process " << pid << ": "
- << strerror(errno) << "\n";
- return false;
- }
-
- std::clog << "sent SIGABRT to process " << pid << "\n";
- return true;
-}
-
-#endif // _WIN32
-
-} // namespace env
+#include "env.h" +#include "envdump.h" +#include "envmetrics.h" +#include "envmodule.h" +#include "envsecurity.h" +#include "envshortcut.h" +#include "envwindows.h" +#include "settings.h" +#include "shared/util.h" +#include <log.h> +#include <utility.h> + +#include <QTimeZone> + +#include <climits> +#include <csignal> +#include <cstdlib> +#include <cstring> +#include <cerrno> +#include <filesystem> +#include <fstream> +#include <iostream> +#include <unistd.h> + +namespace env +{ + +using namespace MOBase; + +Console::Console() : m_hasConsole(false), m_in(nullptr), m_out(nullptr), m_err(nullptr) +{ + // stdout/stderr are already attached on Linux. +} + +Console::~Console() = default; + +ModuleNotification::ModuleNotification(QObject* o, std::function<void(Module)> f) + : m_cookie(nullptr), m_object(o), m_f(std::move(f)) +{} + +ModuleNotification::~ModuleNotification() = default; + +void ModuleNotification::setCookie(void* c) +{ + m_cookie = c; +} + +void ModuleNotification::fire(QString path, std::size_t fileSize) +{ + if (m_loaded.contains(path)) { + return; + } + + m_loaded.insert(path); + + if (m_f) { + QMetaObject::invokeMethod( + m_object, + [path, fileSize, f = m_f] { + f(Module(path, fileSize)); + }, + Qt::QueuedConnection); + } +} + +Environment::Environment() {} + +// anchor +Environment::~Environment() = default; + +const std::vector<Module>& Environment::loadedModules() const +{ + if (m_modules.empty()) { + m_modules = getLoadedModules(); + } + + return m_modules; +} + +std::vector<Process> Environment::runningProcesses() const +{ + return getRunningProcesses(); +} + +const WindowsInfo& Environment::windowsInfo() const +{ + if (!m_windows) { + m_windows.reset(new WindowsInfo); + } + + return *m_windows; +} + +const std::vector<SecurityProduct>& Environment::securityProducts() const +{ + if (m_security.empty()) { + m_security = getSecurityProducts(); + } + + return m_security; +} + +const Metrics& Environment::metrics() const +{ + if (!m_metrics) { + m_metrics.reset(new Metrics); + } + + return *m_metrics; +} + +QString Environment::timezone() const +{ + QTimeZone tz = QTimeZone::systemTimeZone(); + if (!tz.isValid()) { + log::error("failed to get system timezone"); + return "unknown"; + } + + return QString::fromUtf8(tz.id()); +} + +std::unique_ptr<ModuleNotification> +Environment::onModuleLoaded(QObject*, std::function<void(Module)>) +{ + // Linux's dynamic loader has no equivalent of LdrRegisterDllNotification. + return {}; +} + +void Environment::dump(const Settings& s) const +{ + log::debug("os: {}", windowsInfo().toString()); + log::debug("time zone: {}", timezone()); + + log::debug("security products:"); + + { + std::set<QString> productNames; + for (const auto& sp : securityProducts()) { + productNames.insert(sp.toString()); + } + + for (auto&& name : productNames) { + log::debug(" . {}", name); + } + } + + log::debug("modules loaded in process:"); + for (const auto& m : loadedModules()) { + if (m.interesting()) { + log::debug(" . {}", m.toString()); + } + } + + log::debug("displays:"); + for (const auto& d : metrics().displays()) { + log::debug(" . {}", d.toString()); + } + + const auto r = metrics().desktopGeometry(); + log::debug("desktop geometry: ({},{})-({},{})", r.left(), r.top(), r.right(), + r.bottom()); + + dumpDisks(s); +} + +void Environment::dumpDisks(const Settings& s) const +{ + std::set<QString> rootPaths; + + auto dump = [&](auto&& path) { + const QFileInfo fi(path); + const QStorageInfo si(fi.absoluteFilePath()); + + if (rootPaths.contains(si.rootPath())) { + return; + } + + rootPaths.insert(si.rootPath()); + + log::debug(" . {} free={} MB{}", si.rootPath(), (si.bytesFree() / 1000 / 1000), + (si.isReadOnly() ? " (readonly)" : "")); + }; + + log::debug("drives:"); + + dump(QStorageInfo::root().rootPath()); + dump(s.paths().base()); + dump(s.paths().downloads()); + dump(s.paths().mods()); + dump(s.paths().cache()); + dump(s.paths().profiles()); + dump(s.paths().overwrite()); + dump(QCoreApplication::applicationDirPath()); +} + +QString path() +{ + return get("PATH"); +} + +QString appendToPath(const QString& s) +{ + auto old = path(); + set("PATH", old + ":" + s); + return old; +} + +QString prependToPath(const QString& s) +{ + auto old = path(); + set("PATH", s + ":" + old); + return old; +} + +void setPath(const QString& s) +{ + set("PATH", s); +} + +QString get(const QString& name) +{ + return qEnvironmentVariable(name.toUtf8()); +} + +void set(const QString& n, const QString& v) +{ + qputenv(n.toUtf8(), v.toUtf8()); +} + +Service::Service(QString name) : Service(std::move(name), StartType::None, Status::None) +{} + +Service::Service(QString name, StartType st, Status s) + : m_name(std::move(name)), m_startType(st), m_status(s) +{} + +const QString& Service::name() const +{ + return m_name; +} + +bool Service::isValid() const +{ + return (m_startType != StartType::None) && (m_status != Status::None); +} + +Service::StartType Service::startType() const +{ + return m_startType; +} + +Service::Status Service::status() const +{ + return m_status; +} + +QString Service::toString() const +{ + return QString("service '%1', start=%2, status=%3") + .arg(m_name) + .arg(env::toString(m_startType)) + .arg(env::toString(m_status)); +} + +QString toString(Service::StartType st) +{ + using ST = Service::StartType; + + switch (st) { + case ST::None: + return "none"; + + case ST::Disabled: + return "disabled"; + + case ST::Enabled: + return "enabled"; + + default: + return QString("unknown %1").arg(static_cast<int>(st)); + } +} + +QString toString(Service::Status st) +{ + using S = Service::Status; + + switch (st) { + case S::None: + return "none"; + + case S::Stopped: + return "stopped"; + + case S::Running: + return "running"; + + default: + return QString("unknown %1").arg(static_cast<int>(st)); + } +} + +Service getService(const QString& name) +{ + // Linux has no Windows SCM equivalent — return an invalid service. + return Service(name); +} + +Association getAssociation(const QFileInfo& targetInfo) +{ + log::debug("getting association for '{}', extension is '.{}'", + targetInfo.absoluteFilePath(), targetInfo.suffix()); + + const QString mimeType = "application/x-" + targetInfo.suffix(); + + QProcess xdgMime; + xdgMime.start("xdg-mime", QStringList() << "query" << "default" << mimeType); + + if (!xdgMime.waitForFinished(3000)) { + log::debug("xdg-mime query timed out for '{}'", targetInfo.suffix()); + return {}; + } + + const QString desktopFile = + QString::fromUtf8(xdgMime.readAllStandardOutput()).trimmed(); + if (desktopFile.isEmpty()) { + log::debug("no association found for '{}'", targetInfo.suffix()); + return {}; + } + + log::debug("associated desktop file: '{}'", desktopFile); + + return {}; +} + +void deleteRegistryKeyIfEmpty(const QString&) +{ + // No registry on Linux. +} + +bool registryValueExists(const QString&, const QString&) +{ + // No registry on Linux. + return false; +} + +std::filesystem::path thisProcessPath() +{ + char buf[PATH_MAX] = {}; + ssize_t len = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (len <= 0) { + std::cerr << "failed to readlink /proc/self/exe: " << strerror(errno) << "\n"; + return {}; + } + + buf[len] = '\0'; + return std::filesystem::path(buf); +} + +pid_t findOtherPid() +{ + std::clog << "looking for the other process...\n"; + + const pid_t thisPid = ::getpid(); + std::clog << "this process id is " << thisPid << "\n"; + + const std::string targetName = "ModOrganizer"; + + for (const auto& entry : std::filesystem::directory_iterator("/proc")) { + if (!entry.is_directory()) { + continue; + } + + const auto pidStr = entry.path().filename().string(); + + bool isNumber = true; + for (char c : pidStr) { + if (!std::isdigit(static_cast<unsigned char>(c))) { + isNumber = false; + break; + } + } + if (!isNumber) { + continue; + } + + pid_t pid = std::stoi(pidStr); + if (pid == thisPid) { + continue; + } + + std::ifstream commFile(entry.path() / "comm"); + if (!commFile.is_open()) { + continue; + } + + std::string comm; + std::getline(commFile, comm); + + if (comm == targetName) { + std::clog << "found other process with pid " << pid << "\n"; + return pid; + } + } + + std::clog << "no process with this filename\n"; + return 0; +} + +CoreDumpTypes coreDumpTypeFromString(const std::string& s) +{ + if (s == "data") + return env::CoreDumpTypes::Data; + else if (s == "full") + return env::CoreDumpTypes::Full; + else + return env::CoreDumpTypes::Mini; +} + +std::string toString(CoreDumpTypes type) +{ + switch (type) { + case CoreDumpTypes::Mini: + return "mini"; + + case CoreDumpTypes::Data: + return "data"; + + case CoreDumpTypes::Full: + return "full"; + + default: + return "?"; + } +} + +bool coredump(const char* dir, CoreDumpTypes type) +{ + std::clog << "coredump requested (type: " << toString(type) << ")\n"; + + if (dir) { + std::clog << "dump directory: " << dir << "\n"; + } + + // On Linux, abort() generates a core dump if ulimit -c allows it. + std::clog << "calling abort() to generate core dump\n"; + std::abort(); +} + +bool coredumpOther(CoreDumpTypes type) +{ + std::clog << "creating core dump for a running process\n"; + + const auto pid = findOtherPid(); + if (pid == 0) { + std::cerr << "no other process found\n"; + return false; + } + + std::clog << "found other process with pid " << pid << "\n"; + + if (::kill(pid, SIGABRT) != 0) { + std::cerr << "failed to send SIGABRT to process " << pid << ": " + << strerror(errno) << "\n"; + return false; + } + + std::clog << "sent SIGABRT to process " << pid << "\n"; + return true; +} + +} // namespace env diff --git a/src/src/env.h b/src/src/env.h index c9d9804..e7d1484 100644 --- a/src/src/env.h +++ b/src/src/env.h @@ -12,101 +12,7 @@ class SecurityProduct; class WindowsInfo;
class Metrics;
-#ifdef _WIN32
-
-// used by DesktopDCPtr, calls ReleaseDC(0, dc) as the deleter
-//
-struct DesktopDCReleaser
-{
- using pointer = HDC;
-
- void operator()(HDC dc)
- {
- if (dc != 0) {
- ::ReleaseDC(0, dc);
- }
- }
-};
-
-using DesktopDCPtr = std::unique_ptr<HDC, DesktopDCReleaser>;
-
-// used by HMenuPtr, calls DestroyMenu() as the deleter
-//
-struct HMenuFreer
-{
- using pointer = HMENU;
-
- void operator()(HMENU h)
- {
- if (h != 0) {
- ::DestroyMenu(h);
- }
- }
-};
-
-using HMenuPtr = std::unique_ptr<HMENU, HMenuFreer>;
-
-// used by LibraryPtr, calls FreeLibrary as the deleter
-//
-struct LibraryFreer
-{
- using pointer = HINSTANCE;
-
- void operator()(HINSTANCE h)
- {
- if (h != 0) {
- ::FreeLibrary(h);
- }
- }
-};
-
-using LibraryPtr = std::unique_ptr<HINSTANCE, LibraryFreer>;
-
-// used by COMPtr, calls Release() as the deleter
-//
-struct COMReleaser
-{
- void operator()(IUnknown* p)
- {
- if (p) {
- p->Release();
- }
- }
-};
-
-template <class T>
-using COMPtr = std::unique_ptr<T, COMReleaser>;
-
-// used by LocalPtr, calls LocalFree() as the deleter
-//
-template <class T>
-struct LocalFreer
-{
- using pointer = T;
-
- void operator()(T p) { ::LocalFree(p); }
-};
-
-template <class T>
-using LocalPtr = std::unique_ptr<T, LocalFreer<T>>;
-
-// used by the CoTaskMemPtr, calls CoTaskMemFree() as the deleter
-//
-template <class T>
-struct CoTaskMemFreer
-{
- using pointer = T;
-
- void operator()(T p) { ::CoTaskMemFree(p); }
-};
-
-template <class T>
-using CoTaskMemPtr = std::unique_ptr<T, CoTaskMemFreer<T>>;
-
-#else // !_WIN32
-
-// used by LibraryPtr on Linux, calls dlclose() as the deleter
-//
+// used by LibraryPtr, calls dlclose() as the deleter
struct LibraryFreer
{
void operator()(void* h)
@@ -119,8 +25,6 @@ struct LibraryFreer using LibraryPtr = std::unique_ptr<void, LibraryFreer>;
-#endif // _WIN32
-
// used by MallocPtr, calls std::free() as the deleter
//
struct MallocFreer
diff --git a/src/src/envdump.h b/src/src/envdump.h index 3795e9d..eb789a4 100644 --- a/src/src/envdump.h +++ b/src/src/envdump.h @@ -15,13 +15,8 @@ enum class CoreDumpTypes CoreDumpTypes coreDumpTypeFromString(const std::string& s);
std::string toString(CoreDumpTypes type);
-// creates a minidump file for this process
-//
-#ifdef _WIN32
-bool coredump(const wchar_t* dir, CoreDumpTypes type);
-#else
+// creates a core dump file for this process (calls abort() on Linux)
bool coredump(const char* dir, CoreDumpTypes type);
-#endif
// finds another process with the same name as this one and creates a minidump
// file for it
diff --git a/src/src/envfs.cpp b/src/src/envfs.cpp index 9c7cbc6..5493f0a 100644 --- a/src/src/envfs.cpp +++ b/src/src/envfs.cpp @@ -1,722 +1,102 @@ -#include "envfs.h"
-#include "env.h"
-#include "shared/util.h"
-#include <log.h>
-#include <utility.h>
-
-using namespace MOBase;
-
-#ifdef _WIN32
-
-typedef struct _UNICODE_STRING
-{
- USHORT Length;
- USHORT MaximumLength;
- PWSTR Buffer;
-} UNICODE_STRING, *PUNICODE_STRING;
-typedef const UNICODE_STRING* PCUNICODE_STRING;
-
-typedef struct _OBJECT_ATTRIBUTES
-{
- ULONG Length;
- HANDLE RootDirectory;
- PUNICODE_STRING ObjectName;
- ULONG Attributes;
- PVOID SecurityDescriptor;
- PVOID SecurityQualityOfService;
-} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
-
-typedef struct _FILE_DIRECTORY_INFORMATION
-{
- ULONG NextEntryOffset;
- ULONG FileIndex;
- LARGE_INTEGER CreationTime;
- LARGE_INTEGER LastAccessTime;
- LARGE_INTEGER LastWriteTime;
- LARGE_INTEGER ChangeTime;
- LARGE_INTEGER EndOfFile;
- LARGE_INTEGER AllocationSize;
- ULONG FileAttributes;
- ULONG FileNameLength;
- WCHAR FileName[1];
-} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
-
-#define FILE_SHARE_VALID_FLAGS 0x00000007
-
-// copied from ntstatus.h
-#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
-#define STATUS_BUFFER_OVERFLOW ((NTSTATUS)0x80000005L)
-#define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L)
-#define STATUS_NO_SUCH_FILE ((NTSTATUS)0xC000000FL)
-
-typedef struct _IO_STATUS_BLOCK IO_STATUS_BLOCK;
-
-typedef struct _IO_STATUS_BLOCK* PIO_STATUS_BLOCK;
-typedef VOID(NTAPI* PIO_APC_ROUTINE)(PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock,
- ULONG Reserved);
-
-typedef enum _FILE_INFORMATION_CLASS
-{
- FileDirectoryInformation = 1
-} FILE_INFORMATION_CLASS;
-
-typedef NTSTATUS(WINAPI* NtQueryDirectoryFile_type)(HANDLE, HANDLE, PIO_APC_ROUTINE,
- PVOID, PIO_STATUS_BLOCK, PVOID,
- ULONG, FILE_INFORMATION_CLASS,
- BOOLEAN, PUNICODE_STRING, BOOLEAN);
-
-typedef NTSTATUS(WINAPI* NtOpenFile_type)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES,
- PIO_STATUS_BLOCK, ULONG, ULONG);
-
-typedef NTSTATUS(WINAPI* NtClose_type)(HANDLE);
-
-NtOpenFile_type NtOpenFile = nullptr;
-NtQueryDirectoryFile_type NtQueryDirectoryFile = nullptr;
-extern NtClose_type NtClose = nullptr;
-
-#define FILE_DIRECTORY_FILE 0x00000001
-#define FILE_WRITE_THROUGH 0x00000002
-#define FILE_SEQUENTIAL_ONLY 0x00000004
-#define FILE_NO_INTERMEDIATE_BUFFERING 0x00000008
-
-#define FILE_SYNCHRONOUS_IO_ALERT 0x00000010
-#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020
-#define FILE_NON_DIRECTORY_FILE 0x00000040
-#define FILE_CREATE_TREE_CONNECTION 0x00000080
-
-#define FILE_COMPLETE_IF_OPLOCKED 0x00000100
-#define FILE_NO_EA_KNOWLEDGE 0x00000200
-#define FILE_OPEN_REMOTE_INSTANCE 0x00000400
-#define FILE_RANDOM_ACCESS 0x00000800
-
-#define FILE_DELETE_ON_CLOSE 0x00001000
-#define FILE_OPEN_BY_FILE_ID 0x00002000
-#define FILE_OPEN_FOR_BACKUP_INTENT 0x00004000
-#define FILE_NO_COMPRESSION 0x00008000
-
-#if (_WIN32_WINNT >= _WIN32_WINNT_WIN7)
-#define FILE_OPEN_REQUIRING_OPLOCK 0x00010000
-#endif
-
-#define FILE_RESERVE_OPFILTER 0x00100000
-#define FILE_OPEN_REPARSE_POINT 0x00200000
-#define FILE_OPEN_NO_RECALL 0x00400000
-#define FILE_OPEN_FOR_FREE_SPACE_QUERY 0x00800000
-
-#define FILE_VALID_OPTION_FLAGS 0x00ffffff
-#define FILE_VALID_PIPE_OPTION_FLAGS 0x00000032
-#define FILE_VALID_MAILSLOT_OPTION_FLAGS 0x00000032
-#define FILE_VALID_SET_FLAGS 0x00000036
-
-typedef struct _IO_STATUS_BLOCK
-{
-#pragma warning(push)
-#pragma warning(disable : 4201) // we'll always use the Microsoft compiler
- union
- {
- NTSTATUS Status;
- PVOID Pointer;
- } DUMMYUNIONNAME;
-#pragma warning(pop)
-
- ULONG_PTR Information;
-} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
-
-namespace env
-{
-
-std::wstring_view toStringView(const UNICODE_STRING* s)
-{
- if (s && s->Buffer) {
- return {s->Buffer, (s->Length / sizeof(wchar_t))};
- } else {
- return {};
- }
-}
-
-std::wstring_view toStringView(POBJECT_ATTRIBUTES poa)
-{
- if (poa->ObjectName) {
- return toStringView(poa->ObjectName);
- }
-
- return {};
-}
-
-QString toString(POBJECT_ATTRIBUTES poa)
-{
- const auto sv = toStringView(poa);
- return QString::fromWCharArray(sv.data(), static_cast<int>(sv.size()));
-}
-
-class HandleCloserThread
-{
-public:
- HandleCloserThread() : m_ready(false) { m_handles.reserve(50000); }
-
- void shrink() { m_handles.shrink_to_fit(); }
-
- void add(HANDLE h) { m_handles.push_back(h); }
-
- void wakeup()
- {
- {
- std::unique_lock lock(m_mutex);
- m_ready = true;
- }
-
- m_cv.notify_one();
- }
-
- void run()
- {
- MOShared::SetThisThreadName("HandleCloserThread");
-
- std::unique_lock lock(m_mutex);
- m_cv.wait(lock, [&] {
- return m_ready;
- });
-
- closeHandles();
- }
-
-private:
- std::vector<HANDLE> m_handles;
- std::condition_variable m_cv;
- std::mutex m_mutex;
- bool m_ready;
-
- void closeHandles()
- {
- for (auto& h : m_handles) {
- NtClose(h);
- }
-
- m_handles.clear();
- m_ready = false;
- }
-};
-
-constexpr std::size_t AllocSize = 1024 * 1024;
-static ThreadPool<HandleCloserThread> g_handleClosers;
-
-void setHandleCloserThreadCount(std::size_t n)
-{
- g_handleClosers.setMax(n);
-}
-
-void forEachEntryImpl(void* cx, HandleCloserThread& hc,
- std::vector<std::unique_ptr<unsigned char[]>>& buffers,
- POBJECT_ATTRIBUTES poa, std::size_t depth, DirStartF* dirStartF,
- DirEndF* dirEndF, FileF* fileF)
-{
- IO_STATUS_BLOCK iosb;
- UNICODE_STRING ObjectName;
- OBJECT_ATTRIBUTES oa = {sizeof(oa), 0, &ObjectName};
- NTSTATUS status;
-
- status = NtOpenFile(&oa.RootDirectory, FILE_GENERIC_READ, poa, &iosb,
- FILE_SHARE_VALID_FLAGS,
- FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_FOR_BACKUP_INTENT);
-
- if (status < 0) {
- log::error("failed to open directory '{}': {}", toString(poa),
- formatNtMessage(status));
-
- return;
- }
-
- hc.add(oa.RootDirectory);
- unsigned char* buffer;
-
- if (depth >= buffers.size()) {
- buffers.emplace_back(std::make_unique<unsigned char[]>(AllocSize));
- buffer = buffers.back().get();
- } else {
- buffer = buffers[depth].get();
- }
-
- union
- {
- PVOID pv;
- PBYTE pb;
- PFILE_DIRECTORY_INFORMATION DirInfo;
- };
-
- for (;;) {
- status =
- NtQueryDirectoryFile(oa.RootDirectory, NULL, NULL, NULL, &iosb, buffer,
- AllocSize, FileDirectoryInformation, FALSE, NULL, FALSE);
-
- if (status == STATUS_NO_MORE_FILES) {
- break;
- } else if (status < 0) {
- log::error("failed to read directory '{}': {}", toString(poa),
- formatNtMessage(status));
-
- break;
- }
-
- ULONG NextEntryOffset = 0;
-
- pv = buffer;
-
- auto isDotDir = [](auto* o) {
- if (o->Length == 2 && o->Buffer[0] == '.') {
- return true;
- }
-
- if (o->Length == 4 && o->Buffer[0] == '.' && o->Buffer[1] == '.') {
- return true;
- }
-
- return false;
- };
-
- std::size_t count = 0;
-
- for (;;) {
- ++count;
- pb += NextEntryOffset;
-
- ObjectName.Buffer = DirInfo->FileName;
- ObjectName.Length = (USHORT)DirInfo->FileNameLength;
-
- if (!isDotDir(&ObjectName)) {
- ObjectName.MaximumLength = ObjectName.Length;
-
- if (DirInfo->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
- if (dirStartF && dirEndF) {
- dirStartF(cx, toStringView(&oa));
- forEachEntryImpl(cx, hc, buffers, &oa, depth + 1, dirStartF, dirEndF,
- fileF);
- dirEndF(cx, toStringView(&oa));
- }
- } else {
- FILETIME ft;
- ft.dwLowDateTime = DirInfo->LastWriteTime.LowPart;
- ft.dwHighDateTime = DirInfo->LastWriteTime.HighPart;
-
- fileF(cx, toStringView(&oa), ft, DirInfo->AllocationSize.QuadPart);
- }
- }
-
- NextEntryOffset = DirInfo->NextEntryOffset;
-
- if (NextEntryOffset == 0) {
- break;
- }
- }
- }
-}
-
-std::wstring makeNtPath(const std::wstring& path)
-{
- constexpr const wchar_t* nt_prefix = L"\\??\\";
- constexpr const wchar_t* nt_unc_prefix = L"\\??\\UNC\\";
- constexpr const wchar_t* share_prefix = L"\\\\";
-
- if (path.starts_with(nt_prefix)) {
- // already an nt path
- return path;
- } else if (path.starts_with(share_prefix)) {
- // network shared need \??\UNC\ as a prefix
- return nt_unc_prefix + path.substr(2);
- } else {
- // prepend the \??\ prefix
- return nt_prefix + path;
- }
-}
-
-void DirectoryWalker::forEachEntry(const std::wstring& path, void* cx,
- DirStartF* dirStartF, DirEndF* dirEndF, FileF* fileF)
-{
- auto& hc = g_handleClosers.request();
-
- if (!NtOpenFile) {
- LibraryPtr m(::LoadLibraryW(L"ntdll.dll"));
- NtOpenFile = (NtOpenFile_type)::GetProcAddress(m.get(), "NtOpenFile");
- NtQueryDirectoryFile =
- (NtQueryDirectoryFile_type)::GetProcAddress(m.get(), "NtQueryDirectoryFile");
- NtClose = (NtClose_type)::GetProcAddress(m.get(), "NtClose");
- }
-
- const std::wstring ntpath = makeNtPath(path);
-
- UNICODE_STRING ObjectName = {};
- ObjectName.Buffer = const_cast<wchar_t*>(ntpath.c_str());
- ObjectName.Length = (USHORT)ntpath.size() * sizeof(wchar_t);
- ObjectName.MaximumLength = ObjectName.Length;
-
- OBJECT_ATTRIBUTES oa = {};
- oa.Length = sizeof(oa);
- oa.ObjectName = &ObjectName;
-
- forEachEntryImpl(cx, hc, m_buffers, &oa, 0, dirStartF, dirEndF, fileF);
- hc.wakeup();
-}
-
-void forEachEntry(const std::wstring& path, void* cx, DirStartF* dirStartF,
- DirEndF* dirEndF, FileF* fileF)
-{
- DirectoryWalker().forEachEntry(path, cx, dirStartF, dirEndF, fileF);
-}
-
-Directory getFilesAndDirs(const std::wstring& path)
-{
- struct Context
- {
- std::stack<Directory*> current;
- };
-
- Directory root;
-
- Context cx;
- cx.current.push(&root);
-
- env::forEachEntry(
- path, &cx,
- [](void* pcx, std::wstring_view path) {
- Context* cx = (Context*)pcx;
-
- cx->current.top()->dirs.push_back(Directory(path));
- cx->current.push(&cx->current.top()->dirs.back());
- },
-
- [](void* pcx, std::wstring_view path) {
- Context* cx = (Context*)pcx;
- cx->current.pop();
- },
-
- [](void* pcx, std::wstring_view path, FILETIME ft, uint64_t s) {
- Context* cx = (Context*)pcx;
-
- cx->current.top()->files.push_back(File(path, ft, s));
- });
-
- return root;
-}
-
-void getFilesAndDirsWithFindImpl(const std::wstring& path, Directory& d)
-{
- const std::wstring searchString = path + L"\\*";
-
- WIN32_FIND_DATAW findData;
-
- HANDLE searchHandle =
- ::FindFirstFileExW(searchString.c_str(), FindExInfoBasic, &findData,
- FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH);
-
- if (searchHandle != INVALID_HANDLE_VALUE) {
- BOOL result = true;
-
- while (result) {
- if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
- if ((wcscmp(findData.cFileName, L".") != 0) &&
- (wcscmp(findData.cFileName, L"..") != 0)) {
- const std::wstring newPath = path + L"\\" + findData.cFileName;
- d.dirs.push_back(Directory(findData.cFileName));
- getFilesAndDirsWithFindImpl(newPath, d.dirs.back());
- }
- } else {
- const auto size =
- (findData.nFileSizeHigh * (MAXDWORD + 1)) + findData.nFileSizeLow;
-
- d.files.push_back(File(findData.cFileName, findData.ftLastWriteTime, size));
- }
-
- result = ::FindNextFileW(searchHandle, &findData);
- }
- }
-
- ::FindClose(searchHandle);
-}
-
-Directory getFilesAndDirsWithFind(const std::wstring& path)
-{
- Directory d;
- getFilesAndDirsWithFindImpl(path, d);
- return d;
-}
-
-} // namespace env
-
-#else // Linux +#include "envfs.h" +#include "env.h" +#include "shared/util.h" -#include <dirent.h> -#include <QDir> -#include <QProcess> -#include <sys/stat.h> -#include <cstring> -#include <cctype> -#include <stack> +#include <log.h> +#include <utility.h> + +#include <chrono> #include <filesystem> -#include <fstream> -#include <sstream> -
-namespace env
-{
-
-// Convert timespec to FILETIME (100-nanosecond intervals since Jan 1, 1601)
-static FILETIME timespecToFiletime(const struct timespec& ts)
-{
- // Unix epoch to Windows epoch offset: 11644473600 seconds
- constexpr uint64_t EPOCH_DIFF = 116444736000000000ULL;
- uint64_t winTime = static_cast<uint64_t>(ts.tv_sec) * 10000000ULL +
- static_cast<uint64_t>(ts.tv_nsec) / 100ULL + EPOCH_DIFF;
- FILETIME ft;
- ft.dwLowDateTime = static_cast<DWORD>(winTime & 0xFFFFFFFF);
- ft.dwHighDateTime = static_cast<DWORD>(winTime >> 32);
- return ft;
-}
-
-// Convert std::wstring path to narrow string for POSIX APIs
-static std::string toNarrow(const std::wstring& ws)
-{
- return QString::fromStdWString(ws).toStdString();
-}
-
-// Convert narrow string to std::wstring
-static std::wstring toWide(const std::string& s) -{ - return QString::fromStdString(s).toStdWString(); -} -static std::string decodeProcMountField(const std::string& in) +namespace env { - std::string out; - out.reserve(in.size()); - for (size_t i = 0; i < in.size();) { - if (in[i] == '\\' && i + 3 < in.size() && std::isdigit(in[i + 1]) && - std::isdigit(in[i + 2]) && std::isdigit(in[i + 3])) { - const std::string oct = in.substr(i + 1, 3); - const int value = std::stoi(oct, nullptr, 8); - out.push_back(static_cast<char>(value)); - i += 4; - continue; - } +namespace fs = std::filesystem; - out.push_back(in[i]); - ++i; - } +File::File(std::wstring_view n, FILETIME ft, uint64_t s) + : name(n.begin(), n.end()), lcname(MOShared::ToLowerCopy(name)), lastModified(ft), + size(s) +{} + +Directory::Directory() {} + +Directory::Directory(std::wstring_view n) + : name(n.begin(), n.end()), lcname(MOShared::ToLowerCopy(name)) +{} - return out; +void setHandleCloserThreadCount(std::size_t /*n*/) +{ + // No-op on Linux: there are no win32 handles to close on background + // threads. Kept as a stub so callers from upstream don't need #ifdef. } -static bool isMountPoint(const std::string& path) +namespace { - const std::string cleanPath = QDir::cleanPath(QString::fromStdString(path)).toStdString(); - std::ifstream mounts("/proc/mounts"); - if (!mounts.is_open()) { - return false; + // Convert a filesystem clock time to a Win32 FILETIME (100ns ticks since + // 1601-01-01 UTC). Used so the rest of the codebase can carry timestamps in + // a single representation regardless of host OS. + FILETIME toFileTime(fs::file_time_type t) + { + using namespace std::chrono; + const auto sysTime = time_point_cast<system_clock::duration>( + t - decltype(t)::clock::now() + system_clock::now()); + const auto epoch = sysTime.time_since_epoch(); + const auto ticks = duration_cast<duration<int64_t, std::ratio<1, 10000000>>>(epoch) + .count(); + // Win32 epoch is 1601-01-01; Unix epoch is 1970-01-01. Offset = 11644473600s. + constexpr int64_t epochDiff = 116444736000000000LL; + const int64_t winTicks = ticks + epochDiff; + + FILETIME ft; + ft.dwLowDateTime = static_cast<uint32_t>(winTicks & 0xFFFFFFFF); + ft.dwHighDateTime = static_cast<uint32_t>((winTicks >> 32) & 0xFFFFFFFF); + return ft; } - std::string line; - while (std::getline(mounts, line)) { - std::istringstream iss(line); - std::string device; - std::string mountPoint; - if (!(iss >> device >> mountPoint)) { - continue; + // Walk `path` recursively, calling the C-style callbacks. dirStartF and + // dirEndF wrap a directory's contents so callers can build a tree. + void walkDirectory(const fs::path& path, void* cx, DirStartF* dirStartF, + DirEndF* dirEndF, FileF* fileF) + { + std::error_code ec; + fs::directory_iterator it(path, ec); + if (ec) { + return; } - const std::string decoded = - QDir::cleanPath(QString::fromStdString(decodeProcMountField(mountPoint))) - .toStdString(); - if (decoded == cleanPath) { - return true; + for (const auto& entry : it) { + const auto name = entry.path().filename().wstring(); + + if (entry.is_directory(ec)) { + if (dirStartF) { + dirStartF(cx, name); + } + walkDirectory(entry.path(), cx, dirStartF, dirEndF, fileF); + if (dirEndF) { + dirEndF(cx, name); + } + } else if (entry.is_regular_file(ec)) { + if (fileF) { + const auto size = static_cast<uint64_t>(entry.file_size(ec)); + const auto ft = toFileTime(entry.last_write_time(ec)); + fileF(cx, name, ft, size); + } + } } } +} // namespace - return false; -} - -static bool runUnmountCommand(const QString& program, const QStringList& args) +void DirectoryWalker::forEachEntry(const std::wstring& path, void* cx, + DirStartF* dirStartF, DirEndF* dirEndF, + FileF* fileF) { - QProcess p; - p.start(program, args); - if (!p.waitForFinished(3000)) { - p.kill(); - return false; - } - - return p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0; + walkDirectory(fs::path(path), cx, dirStartF, dirEndF, fileF); } -static bool tryRecoverStaleMount(const std::string& dirPath) +void forEachEntry(const std::wstring& path, void* cx, DirStartF* dirStartF, + DirEndF* dirEndF, FileF* fileF) { - if (!isMountPoint(dirPath)) { - return false; - } - - const QString clean = QDir::cleanPath(QString::fromStdString(dirPath)); - log::warn("stale mount detected at '{}', attempting recovery", clean); - - runUnmountCommand("fusermount3", {"-u", clean}); - runUnmountCommand("fusermount", {"-u", clean}); - runUnmountCommand("umount", {clean}); - runUnmountCommand("umount", {"-l", clean}); - runUnmountCommand("fusermount3", {"-uz", clean}); - runUnmountCommand("fusermount", {"-uz", clean}); - - const bool recovered = !isMountPoint(dirPath); - if (recovered) { - log::info("recovered stale mount at '{}'", clean); - } else { - log::warn("failed to recover stale mount at '{}'", clean); - } - - return recovered; + walkDirectory(fs::path(path), cx, dirStartF, dirEndF, fileF); } -
-// HandleCloserThread stub for Linux (no NT handles to close)
-class HandleCloserThread
-{
-public:
- HandleCloserThread() : m_ready(false) {}
- void shrink() {}
- void add(void*) {}
- void wakeup()
- {
- std::unique_lock lock(m_mutex);
- m_ready = true;
- m_cv.notify_one();
- }
- void run()
- {
- std::unique_lock lock(m_mutex);
- m_cv.wait(lock, [&] { return m_ready; });
- m_ready = false;
- }
-
-private:
- std::condition_variable m_cv;
- std::mutex m_mutex;
- bool m_ready;
-};
-
-static ThreadPool<HandleCloserThread> g_handleClosers;
-
-void setHandleCloserThreadCount(std::size_t n)
-{
- g_handleClosers.setMax(n);
-}
-
-// Recursive directory walker using POSIX opendir/readdir
-static void forEachEntryImpl(const std::string& dirPath, void* cx, - DirStartF* dirStartF, DirEndF* dirEndF, FileF* fileF) -{ - DIR* dir = opendir(dirPath.c_str()); - if (!dir && errno == ENOTCONN) { - if (tryRecoverStaleMount(dirPath)) { - dir = opendir(dirPath.c_str()); - } - } - if (!dir) { - log::error("failed to open directory '{}': {}", - QString::fromStdString(dirPath), strerror(errno)); - return; - } -
- struct dirent* entry;
- while ((entry = readdir(dir)) != nullptr) {
- if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
- continue;
- }
-
- std::string fullPath = dirPath + "/" + entry->d_name;
- std::wstring wname = toWide(entry->d_name);
-
- struct stat st;
- if (lstat(fullPath.c_str(), &st) != 0) {
- continue;
- }
-
- if (S_ISDIR(st.st_mode)) {
- if (dirStartF && dirEndF) {
- std::wstring_view nameView(wname);
- dirStartF(cx, nameView);
- forEachEntryImpl(fullPath, cx, dirStartF, dirEndF, fileF);
- dirEndF(cx, nameView);
- }
- } else {
- FILETIME ft = timespecToFiletime(st.st_mtim);
- fileF(cx, std::wstring_view(wname), ft, static_cast<uint64_t>(st.st_size));
- }
- }
-
- closedir(dir);
-}
-
-void DirectoryWalker::forEachEntry(const std::wstring& path, void* cx,
- DirStartF* dirStartF, DirEndF* dirEndF, FileF* fileF)
-{
- forEachEntryImpl(toNarrow(path), cx, dirStartF, dirEndF, fileF);
-}
-
-void forEachEntry(const std::wstring& path, void* cx, DirStartF* dirStartF,
- DirEndF* dirEndF, FileF* fileF)
-{
- DirectoryWalker().forEachEntry(path, cx, dirStartF, dirEndF, fileF);
-}
-
-Directory getFilesAndDirs(const std::wstring& path)
-{
- struct Context
- {
- std::stack<Directory*> current;
- };
-
- Directory root;
-
- Context cx;
- cx.current.push(&root);
-
- env::forEachEntry(
- path, &cx,
- [](void* pcx, std::wstring_view path) {
- Context* cx = (Context*)pcx;
- cx->current.top()->dirs.push_back(Directory(path));
- cx->current.push(&cx->current.top()->dirs.back());
- },
-
- [](void* pcx, std::wstring_view) {
- Context* cx = (Context*)pcx;
- cx->current.pop();
- },
-
- [](void* pcx, std::wstring_view path, FILETIME ft, uint64_t s) {
- Context* cx = (Context*)pcx;
- cx->current.top()->files.push_back(File(path, ft, s));
- });
-
- return root;
-}
-
-// Linux equivalent of getFilesAndDirsWithFind - same as getFilesAndDirs
-Directory getFilesAndDirsWithFind(const std::wstring& path)
-{
- return getFilesAndDirs(path);
-}
-
-} // namespace env
-
-#endif // _WIN32
-
-// Cross-platform constructors
-namespace env
-{
-
-File::File(std::wstring_view n, FILETIME ft, uint64_t s)
- : name(n.begin(), n.end()), lcname(MOShared::ToLowerCopy(name)), lastModified(ft),
- size(s)
-{}
-
-Directory::Directory() {}
-
-Directory::Directory(std::wstring_view n)
- : name(n.begin(), n.end()), lcname(MOShared::ToLowerCopy(name))
-{}
-
-} // namespace env
+ +} // namespace env diff --git a/src/src/envmodule.cpp b/src/src/envmodule.cpp index fd5651a..6149333 100644 --- a/src/src/envmodule.cpp +++ b/src/src/envmodule.cpp @@ -1,1108 +1,407 @@ -#include "envmodule.h"
-#include "env.h"
-#include <log.h>
-#include <utility.h>
-
-#ifndef _WIN32
-#include <QDir>
-#include <QFile>
-#include <QFileInfo>
-#include <QCryptographicHash>
-#include <dirent.h>
-#include <fstream>
-#include <sstream>
-#include <unistd.h>
-#include <signal.h>
-#endif
-
-namespace env
-{
-
-using namespace MOBase;
-
-// the rationale for logging md5 was to make sure the various files were the
-// same as in the released version; this turned out to be of dubious interest,
-// while adding to the startup time
-constexpr bool UseMD5 = false;
-
-#ifdef _WIN32
-
-Module::Module(QString path, std::size_t fileSize)
- : m_path(std::move(path)), m_fileSize(fileSize)
-{
- const auto fi = getFileInfo();
-
- m_version = getVersion(fi.ffi);
- m_timestamp = getTimestamp(fi.ffi);
- m_versionString = fi.fileDescription;
-
- if (UseMD5) {
- m_md5 = getMD5();
- }
-}
-
-#else // Linux
-
-Module::Module(QString path, std::size_t fileSize)
- : m_path(std::move(path)), m_fileSize(fileSize)
-{
- const auto fi = getFileInfo();
-
- m_timestamp = getTimestamp();
- m_versionString = fi.fileDescription;
-
- if (UseMD5) {
- m_md5 = getMD5();
- }
-}
-
-#endif
-
-const QString& Module::path() const
-{
- return m_path;
-}
-
-QString Module::displayPath() const
-{
- return QDir::fromNativeSeparators(m_path.toLower());
-}
-
-std::size_t Module::fileSize() const
-{
- return m_fileSize;
-}
-
-const QString& Module::version() const
-{
- return m_version;
-}
-
-const QString& Module::versionString() const
-{
- return m_versionString;
-}
-
-const QDateTime& Module::timestamp() const
-{
- return m_timestamp;
-}
-
-const QString& Module::md5() const
-{
- return m_md5;
-}
-
-QString Module::timestampString() const
-{
- if (!m_timestamp.isValid()) {
- return "(no timestamp)";
- }
-
- return m_timestamp.toString(Qt::DateFormat::ISODate);
-}
-
-QString Module::toString() const
-{
- QStringList sl;
-
- // file size
- sl.push_back(displayPath());
- sl.push_back(QString("%1 B").arg(m_fileSize));
-
- // version
- if (m_version.isEmpty() && m_versionString.isEmpty()) {
- sl.push_back("(no version)");
- } else {
- if (!m_version.isEmpty()) {
- sl.push_back(m_version);
- }
-
- if (!m_versionString.isEmpty() && m_versionString != m_version) {
- sl.push_back(versionString());
- }
- }
-
- // timestamp
- if (m_timestamp.isValid()) {
- sl.push_back(m_timestamp.toString(Qt::DateFormat::ISODate));
- } else {
- sl.push_back("(no timestamp)");
- }
-
- // md5
- if (!m_md5.isEmpty()) {
- sl.push_back(m_md5);
- }
-
- return sl.join(", ");
-}
-
-#ifdef _WIN32
-
-Module::FileInfo Module::getFileInfo() const
-{
- const auto wspath = m_path.toStdWString();
-
- // getting version info size
- DWORD dummy = 0;
- const DWORD size = GetFileVersionInfoSizeW(wspath.c_str(), &dummy);
-
- if (size == 0) {
- const auto e = GetLastError();
-
- if (e == ERROR_RESOURCE_TYPE_NOT_FOUND) {
- // not an error, no version information built into that module
- return {};
- }
-
- if (e == ERROR_RESOURCE_DATA_NOT_FOUND) {
- // not an error, no version information built into that module;
- // happens often in wine
- return {};
- }
-
- log::debug("GetFileVersionInfoSizeW() failed on '{}', {}", m_path,
- formatSystemMessage(e));
-
- return {};
- }
-
- // getting version info
- auto buffer = std::make_unique<std::byte[]>(size);
-
- if (!GetFileVersionInfoW(wspath.c_str(), 0, size, buffer.get())) {
- const auto e = GetLastError();
-
- log::error("GetFileVersionInfoW() failed on '{}', {}", m_path,
- formatSystemMessage(e));
-
- return {};
- }
-
- // the version info has two major parts: a fixed version and a localizable
- // set of strings
-
- FileInfo fi;
- fi.ffi = getFixedFileInfo(buffer.get());
- fi.fileDescription = getFileDescription(buffer.get());
-
- return fi;
-}
-
-VS_FIXEDFILEINFO Module::getFixedFileInfo(std::byte* buffer) const
-{
- void* valuePointer = nullptr;
- unsigned int valueSize = 0;
-
- // the fixed version info is in the root
- const auto ret = VerQueryValueW(buffer, L"\\", &valuePointer, &valueSize);
-
- if (!ret || !valuePointer || valueSize == 0) {
- // not an error, no fixed file info
- return {};
- }
-
- const auto* fi = static_cast<VS_FIXEDFILEINFO*>(valuePointer);
-
- // signature is always 0xfeef04bd
- if (fi->dwSignature != 0xfeef04bd) {
- log::error("bad file info signature {:#x} for '{}'", fi->dwSignature, m_path);
-
- return {};
- }
-
- return *fi;
-}
-
-QString Module::getFileDescription(std::byte* buffer) const
-{
- struct LANGANDCODEPAGE
- {
- WORD wLanguage;
- WORD wCodePage;
- };
-
- void* valuePointer = nullptr;
- unsigned int valueSize = 0;
-
- // getting list of available languages
- auto ret =
- VerQueryValueW(buffer, L"\\VarFileInfo\\Translation", &valuePointer, &valueSize);
-
- if (!ret || !valuePointer || valueSize == 0) {
- log::error("VerQueryValueW() for translations failed on '{}'", m_path);
- return {};
- }
-
- // number of languages
- const auto count = valueSize / sizeof(LANGANDCODEPAGE);
- if (count == 0) {
- return {};
- }
-
- // using the first language in the list to get FileVersion
- const auto* lcp = static_cast<LANGANDCODEPAGE*>(valuePointer);
-
- const auto subBlock = QString("\\StringFileInfo\\%1%2\\FileVersion")
- .arg(lcp->wLanguage, 4, 16, QChar('0'))
- .arg(lcp->wCodePage, 4, 16, QChar('0'));
-
- ret = VerQueryValueW(buffer, subBlock.toStdWString().c_str(), &valuePointer,
- &valueSize);
-
- if (!ret || !valuePointer || valueSize == 0) {
- // not an error, no file version
- return {};
- }
-
- // valueSize includes the null terminator
- return QString::fromWCharArray(static_cast<wchar_t*>(valuePointer), valueSize - 1);
-}
-
-QString Module::getVersion(const VS_FIXEDFILEINFO& fi) const
-{
- if (fi.dwSignature == 0) {
- return {};
- }
-
- const DWORD major = (fi.dwFileVersionMS >> 16) & 0xffff;
- const DWORD minor = (fi.dwFileVersionMS >> 0) & 0xffff;
- const DWORD maintenance = (fi.dwFileVersionLS >> 16) & 0xffff;
- const DWORD build = (fi.dwFileVersionLS >> 0) & 0xffff;
-
- if (major == 0 && minor == 0 && maintenance == 0 && build == 0) {
- return {};
- }
-
- return QString("%1.%2.%3.%4").arg(major).arg(minor).arg(maintenance).arg(build);
-}
-
-QDateTime Module::getTimestamp(const VS_FIXEDFILEINFO& fi) const
-{
- FILETIME ft = {};
-
- if (fi.dwSignature == 0 || (fi.dwFileDateMS == 0 && fi.dwFileDateLS == 0)) {
- // if the file info is invalid or doesn't have a date, use the creation
- // time on the file
-
- // opening the file
- HandlePtr h(CreateFileW(m_path.toStdWString().c_str(), GENERIC_READ,
- FILE_SHARE_READ, nullptr, OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL, 0));
-
- if (h.get() == INVALID_HANDLE_VALUE) {
- const auto e = GetLastError();
-
- log::debug("can't open file '{}' for timestamp, {}", m_path,
- formatSystemMessage(e));
-
- return {};
- }
-
- // getting the file time
- if (!GetFileTime(h.get(), &ft, nullptr, nullptr)) {
- const auto e = GetLastError();
-
- log::error("can't get file time for '{}', {}", m_path, formatSystemMessage(e));
-
- return {};
- }
- } else {
- // use the time from the file info
- ft.dwHighDateTime = fi.dwFileDateMS;
- ft.dwLowDateTime = fi.dwFileDateLS;
- }
-
- // converting to SYSTEMTIME
- SYSTEMTIME utc = {};
-
- if (!FileTimeToSystemTime(&ft, &utc)) {
- log::error(
- "FileTimeToSystemTime() failed on timestamp high={:#x} low={:#x} for '{}'",
- ft.dwHighDateTime, ft.dwLowDateTime, m_path);
-
- return {};
- }
-
- return QDateTime(QDate(utc.wYear, utc.wMonth, utc.wDay),
- QTime(utc.wHour, utc.wMinute, utc.wSecond, utc.wMilliseconds));
-}
-
-bool Module::interesting() const
-{
- static const auto windir = []() -> QString {
- try {
- return QDir::toNativeSeparators(MOBase::getKnownFolder(FOLDERID_Windows).path()) +
- "\\";
- } catch (...) {
- return "c:\\windows\\";
- }
- }();
-
- if (m_path.startsWith(windir, Qt::CaseInsensitive)) {
- return false;
- }
-
- return true;
-}
-
-QString Module::getMD5() const
-{
- static const std::set<QString> ignore = {
- "\\windows\\", "\\program files\\", "\\program files (x86)\\", "\\programdata\\"};
-
- // don't calculate md5 for system files, it's not really relevant and
- // it takes a while
- for (auto&& i : ignore) {
- if (m_path.contains(i, Qt::CaseInsensitive)) {
- return {};
- }
- }
-
- // opening the file
- QFile f(m_path);
-
- if (!f.open(QFile::ReadOnly)) {
- log::error("failed to open file '{}' for md5", m_path);
- return {};
- }
-
- // hashing
- QCryptographicHash hash(QCryptographicHash::Md5);
- if (!hash.addData(&f)) {
- log::error("failed to calculate md5 for '{}'", m_path);
- return {};
- }
-
- return hash.result().toHex();
-}
-
-#else // Linux
-
-Module::FileInfo Module::getFileInfo() const
-{
- // On Linux, there are no PE version resources. Return empty info.
- return {};
-}
-
-QDateTime Module::getTimestamp() const
-{
- QFileInfo fi(m_path);
- if (!fi.exists()) {
- return {};
- }
- return fi.lastModified();
-}
-
-bool Module::interesting() const
-{
- // Filter out system libraries
- if (m_path.startsWith("/usr/lib/", Qt::CaseInsensitive) ||
- m_path.startsWith("/usr/lib64/", Qt::CaseInsensitive) ||
- m_path.startsWith("/lib/", Qt::CaseInsensitive) ||
- m_path.startsWith("/lib64/", Qt::CaseInsensitive)) {
- return false;
- }
-
- return true;
-}
-
-QString Module::getMD5() const
-{
- static const std::set<QString> ignore = {
- "/usr/lib/", "/usr/lib64/", "/usr/share/", "/lib/", "/lib64/"};
-
- // don't calculate md5 for system files
- for (auto&& i : ignore) {
- if (m_path.startsWith(i, Qt::CaseInsensitive)) {
- return {};
- }
- }
-
- QFile f(m_path);
- if (!f.open(QFile::ReadOnly)) {
- log::error("failed to open file '{}' for md5", m_path);
- return {};
- }
-
- QCryptographicHash hash(QCryptographicHash::Md5);
- if (!hash.addData(&f)) {
- log::error("failed to calculate md5 for '{}'", m_path);
- return {};
- }
-
- return hash.result().toHex();
-}
-
-#endif // _WIN32
-
-// -- Process implementation --
-
-Process::Process()
- : m_pid(0)
-{}
-
-#ifdef _WIN32
-
-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))
-{}
-
-#else // Linux
-
-Process::Process(pid_t pid, pid_t ppid, QString name)
- : m_pid(pid), m_ppid(ppid), m_name(std::move(name))
-{}
-
-#endif
-
-bool Process::isValid() const
-{
- return (m_pid != 0);
-}
-
-#ifdef _WIN32
-DWORD Process::pid() const
-#else
-pid_t Process::pid() const
-#endif
-{
- return m_pid;
-}
-
-#ifdef _WIN32
-DWORD Process::ppid() const
-#else
-pid_t Process::ppid() const
-#endif
-{
- if (!m_ppid) {
- m_ppid = getProcessParentID(m_pid);
- }
-
- return *m_ppid;
-}
-
-const QString& Process::name() const
-{
- if (!m_name) {
- m_name = getProcessName(m_pid);
- }
-
- return *m_name;
-}
-
-#ifdef _WIN32
-
-HandlePtr Process::openHandleForWait() const
-{
- const auto rights =
- PROCESS_QUERY_LIMITED_INFORMATION | // exit code, image name, etc.
- SYNCHRONIZE | // wait functions
- PROCESS_SET_QUOTA | PROCESS_TERMINATE; // add to job
-
- // don't log errors, failure can happen if the process doesn't exist
- return HandlePtr(OpenProcess(rights, FALSE, m_pid));
-}
-
-bool Process::canAccess() const
-{
- HandlePtr h(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, m_pid));
-
- if (!h) {
- const auto e = GetLastError();
- if (e == ERROR_ACCESS_DENIED) {
- return false;
- }
- }
-
- return true;
-}
-
-#else // Linux
-
-HandlePtr Process::openHandleForWait() const
-{
- // On Linux, just wrap the pid. The caller can use waitpid() or
- // kill(pid, 0) to check on the process.
- if (kill(m_pid, 0) == 0) {
- return HandlePtr(HandleCloser::pointer(static_cast<uintptr_t>(m_pid)));
- }
- return {};
-}
-
-bool Process::canAccess() const
-{
- // kill with signal 0 checks if we can send signals to the process
- return (kill(m_pid, 0) == 0);
-}
-
-#endif
-
-void Process::addChild(Process p)
-{
- m_children.push_back(p);
-}
-
-std::vector<Process>& Process::children()
-{
- return m_children;
-}
-
-const std::vector<Process>& Process::children() const
-{
- return m_children;
-}
-
-// -- Free functions --
-
-#ifdef _WIN32
-
-std::vector<Module> getLoadedModules()
-{
- HandlePtr snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPMODULE32 | TH32CS_SNAPMODULE,
- GetCurrentProcessId()));
-
- if (snapshot.get() == INVALID_HANDLE_VALUE) {
- const auto e = GetLastError();
- log::error("CreateToolhelp32Snapshot() failed, {}", formatSystemMessage(e));
- return {};
- }
-
- MODULEENTRY32 me = {};
- me.dwSize = sizeof(me);
-
- // first module, this shouldn't fail because there's at least the executable
- if (!Module32First(snapshot.get(), &me)) {
- const auto e = GetLastError();
- log::error("Module32First() failed, {}", formatSystemMessage(e));
- return {};
- }
-
- std::vector<Module> v;
-
- for (;;) {
- const auto path = QString::fromWCharArray(me.szExePath);
- if (!path.isEmpty()) {
- v.push_back(Module(path, me.modBaseSize));
- }
-
- // next module
- if (!Module32Next(snapshot.get(), &me)) {
- const auto e = GetLastError();
-
- // no more modules is not an error
- if (e != ERROR_NO_MORE_FILES) {
- log::error("Module32Next() failed, {}", formatSystemMessage(e));
- }
-
- break;
- }
- }
-
- // sorting by display name
- std::sort(v.begin(), v.end(), [](auto&& a, auto&& b) {
- return (a.displayPath().compare(b.displayPath(), Qt::CaseInsensitive) < 0);
- });
-
- return v;
-}
-
-template <class F>
-void forEachRunningProcess(F&& f)
-{
- HandlePtr snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
-
- if (snapshot.get() == INVALID_HANDLE_VALUE) {
- const auto e = GetLastError();
- log::error("CreateToolhelp32Snapshot() failed, {}", formatSystemMessage(e));
- return;
- }
-
- PROCESSENTRY32 entry = {};
- entry.dwSize = sizeof(entry);
-
- // first process, this shouldn't fail because there's at least one process
- // running
- if (!Process32First(snapshot.get(), &entry)) {
- const auto e = GetLastError();
- log::error("Process32First() failed, {}", formatSystemMessage(e));
- return;
- }
-
- for (;;) {
- if (!f(entry)) {
- break;
- }
-
- // next process
- if (!Process32Next(snapshot.get(), &entry)) {
- const auto e = GetLastError();
-
- // no more processes is not an error
- if (e != ERROR_NO_MORE_FILES)
- log::error("Process32Next() failed, {}", formatSystemMessage(e));
-
- 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 getProcessTreeFromProcess(HANDLE h)
-{
- Process root;
-
- const auto parentPID = ::GetProcessId(h);
- const auto v = getRunningProcesses();
-
- for (auto&& p : v) {
- if (p.pid() == parentPID) {
- Process child = p;
- findChildren(child, v);
- root.addChild(child);
- break;
- }
- }
-
- return root;
-}
-
-std::vector<DWORD> processesInJob(HANDLE h)
-{
- const int MaxTries = 5;
-
- // doubled MaxTries times on failure
- DWORD maxIds = 100;
-
- // for logging
- DWORD lastCount = 0, lastAssigned = 0;
-
- for (int tries = 0; tries < MaxTries; ++tries) {
- const DWORD idsSize = sizeof(ULONG_PTR) * maxIds;
- const DWORD bufferSize = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + idsSize;
-
- MallocPtr<void> buffer(std::malloc(bufferSize));
- auto* ids = static_cast<JOBOBJECT_BASIC_PROCESS_ID_LIST*>(buffer.get());
-
- const auto r = QueryInformationJobObject(h, JobObjectBasicProcessIdList, ids,
- bufferSize, nullptr);
-
- if (!r) {
- const auto e = GetLastError();
- if (e != ERROR_MORE_DATA) {
- log::error("failed to get process ids in job, {}", formatSystemMessage(e));
- return {};
- }
- }
-
- if (ids->NumberOfProcessIdsInList >= ids->NumberOfAssignedProcesses) {
- std::vector<DWORD> v;
- for (DWORD i = 0; i < ids->NumberOfProcessIdsInList; ++i) {
- v.push_back(ids->ProcessIdList[i]);
- }
-
- return v;
- }
-
- // try again with a larger buffer
- maxIds *= 2;
-
- // for logging
- lastCount = ids->NumberOfProcessIdsInList;
- lastAssigned = ids->NumberOfAssignedProcesses;
- }
-
- log::error("failed to get processes in job, can't get a buffer large enough, "
- "{}/{} ids",
- lastCount, lastAssigned);
-
- return {};
-}
-
-void findChildProcesses(Process& parent, std::vector<Process>& processes)
-{
- // find all processes that are direct children of `parent`
- auto itor = processes.begin();
-
- while (itor != processes.end()) {
- if (itor->ppid() == parent.pid()) {
- parent.addChild(*itor);
- itor = processes.erase(itor);
- } else {
- ++itor;
- }
- }
-
- // find all processes that are direct children of `parent`'s children
- for (auto&& c : parent.children()) {
- findChildProcesses(c, processes);
- }
-}
-
-Process getProcessTreeFromJob(HANDLE h)
-{
- const auto ids = processesInJob(h);
- if (ids.empty()) {
- return {};
- }
-
- std::vector<Process> ps;
-
- forEachRunningProcess([&](auto&& entry) {
- for (auto&& id : ids) {
- if (entry.th32ProcessID == id) {
- ps.push_back(Process(entry.th32ProcessID, entry.th32ParentProcessID,
- QString::fromStdWString(entry.szExeFile)));
-
- break;
- }
- }
-
- return true;
- });
-
- Process root;
-
- {
- // getting processes whose parent is not in the list
- for (auto&& possibleRoot : ps) {
- const auto ppid = possibleRoot.ppid();
- bool found = false;
-
- for (auto&& p : ps) {
- if (p.pid() == ppid) {
- found = true;
- break;
- }
- }
-
- if (!found) {
- // this is a root process
- root.addChild(possibleRoot);
- }
- }
-
- // removing root processes from the list
- auto newEnd = std::remove_if(ps.begin(), ps.end(), [&](auto&& p) {
- for (auto&& rp : root.children()) {
- if (rp.pid() == p.pid()) {
- return true;
- }
- }
-
- return false;
- });
-
- ps.erase(newEnd, ps.end());
- }
-
- // at this point, `processes` should only contain processes that are direct
- // or indirect children of the ones in `root`
-
- if (ps.empty()) {
- // and that's all there is
- return root;
- }
-
- {
- // recursively find children
- for (auto&& r : root.children()) {
- findChildProcesses(r, ps);
- }
- }
-
- return root;
-}
-
-bool isJobHandle(HANDLE h)
-{
- JOBOBJECT_BASIC_ACCOUNTING_INFORMATION info = {};
-
- const auto r = ::QueryInformationJobObject(h, JobObjectBasicAccountingInformation,
- &info, sizeof(info), nullptr);
-
- return r;
-}
-
-Process getProcessTree(HANDLE h)
-{
- if (isJobHandle(h)) {
- return getProcessTreeFromJob(h);
- } else {
- return getProcessTreeFromProcess(h);
- }
-}
-
-QString getProcessName(DWORD pid)
-{
- HandlePtr h(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid));
-
- 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));
-}
-
-#else // Linux
-
-std::vector<Module> getLoadedModules()
-{
- std::vector<Module> v;
-
- // Parse /proc/self/maps to find loaded shared libraries
- std::ifstream maps("/proc/self/maps");
- if (!maps.is_open()) {
- log::error("failed to open /proc/self/maps");
- return {};
- }
-
- std::set<QString> seen;
- std::string line;
-
- while (std::getline(maps, line)) {
- // Each line has format: address perms offset dev inode pathname
- // We want the pathname (last field) if it's a .so file
- auto spacePos = line.rfind(' ');
- if (spacePos == std::string::npos) {
- continue;
- }
-
- std::string path = line.substr(spacePos + 1);
- if (path.empty() || path[0] != '/') {
- continue;
- }
-
- QString qpath = QString::fromStdString(path);
-
- // Skip duplicates (same library mapped multiple times)
- if (seen.count(qpath)) {
- continue;
- }
- seen.insert(qpath);
-
- QFileInfo fi(qpath);
- if (fi.exists()) {
- v.push_back(Module(qpath, fi.size()));
- }
- }
-
- // sorting by display name
- std::sort(v.begin(), v.end(), [](auto&& a, auto&& b) {
- return (a.displayPath().compare(b.displayPath(), Qt::CaseInsensitive) < 0);
- });
-
- return v;
-}
-
-std::vector<Process> getRunningProcesses()
-{
- std::vector<Process> v;
-
- DIR* procDir = opendir("/proc");
- if (!procDir) {
- log::error("failed to open /proc");
- return {};
- }
-
- struct dirent* entry;
- while ((entry = readdir(procDir)) != nullptr) {
- // Only process numeric directories (PIDs)
- bool isNumeric = true;
- for (const char* p = entry->d_name; *p; ++p) {
- if (*p < '0' || *p > '9') {
- isNumeric = false;
- break;
- }
- }
-
- if (!isNumeric) {
- continue;
- }
-
- pid_t pid = static_cast<pid_t>(std::strtol(entry->d_name, nullptr, 10));
-
- // Read process name from /proc/[pid]/comm
- QString name;
- {
- std::string commPath = "/proc/" + std::string(entry->d_name) + "/comm";
- std::ifstream commFile(commPath);
- if (commFile.is_open()) {
- std::string commName;
- std::getline(commFile, commName);
- name = QString::fromStdString(commName);
- }
- }
-
- // Read parent PID from /proc/[pid]/status
- pid_t ppid = 0;
- {
- std::string statusPath = "/proc/" + std::string(entry->d_name) + "/status";
- std::ifstream statusFile(statusPath);
- if (statusFile.is_open()) {
- std::string statusLine;
- while (std::getline(statusFile, statusLine)) {
- if (statusLine.compare(0, 5, "PPid:") == 0) {
- ppid = static_cast<pid_t>(
- std::strtol(statusLine.c_str() + 5, nullptr, 10));
- break;
- }
- }
- }
- }
-
- v.push_back(Process(pid, ppid, name));
- }
-
- closedir(procDir);
- 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(pid_t pid)
-{
- Process root;
-
- const auto v = getRunningProcesses();
-
- for (auto&& p : v) {
- if (p.pid() == pid) {
- Process child = p;
- findChildren(child, v);
- root.addChild(child);
- break;
- }
- }
-
- return root;
-}
-
-QString getProcessName(pid_t pid)
-{
- std::string commPath = "/proc/" + std::to_string(pid) + "/comm";
- std::ifstream commFile(commPath);
-
- if (!commFile.is_open()) {
- log::error("can't get name of process {}", pid);
- return "unknown";
- }
-
- std::string name;
- std::getline(commFile, name);
- return QString::fromStdString(name);
-}
-
-pid_t getProcessParentID(pid_t pid)
-{
- std::string statusPath = "/proc/" + std::to_string(pid) + "/status";
- std::ifstream statusFile(statusPath);
-
- if (!statusFile.is_open()) {
- return 0;
- }
-
- std::string line;
- while (std::getline(statusFile, line)) {
- if (line.compare(0, 5, "PPid:") == 0) {
- return static_cast<pid_t>(std::strtol(line.c_str() + 5, nullptr, 10));
- }
- }
-
- return 0;
-}
-
-#endif // _WIN32
-
-} // namespace env
+#include "envmodule.h" +#include "env.h" +#include <log.h> +#include <utility.h> + +#include <QCryptographicHash> +#include <QDir> +#include <QFile> +#include <QFileInfo> +#include <dirent.h> +#include <fstream> +#include <sstream> +#include <unistd.h> +#include <signal.h> + +namespace env +{ + +using namespace MOBase; + +// the rationale for logging md5 was to make sure the various files were the +// same as in the released version; this turned out to be of dubious interest, +// while adding to the startup time +constexpr bool UseMD5 = false; + +Module::Module(QString path, std::size_t fileSize) + : m_path(std::move(path)), m_fileSize(fileSize) +{ + const auto fi = getFileInfo(); + + m_timestamp = getTimestamp(); + m_versionString = fi.fileDescription; + + if (UseMD5) { + m_md5 = getMD5(); + } +} + +const QString& Module::path() const +{ + return m_path; +} + +QString Module::displayPath() const +{ + return QDir::fromNativeSeparators(m_path.toLower()); +} + +std::size_t Module::fileSize() const +{ + return m_fileSize; +} + +const QString& Module::version() const +{ + return m_version; +} + +const QString& Module::versionString() const +{ + return m_versionString; +} + +const QDateTime& Module::timestamp() const +{ + return m_timestamp; +} + +const QString& Module::md5() const +{ + return m_md5; +} + +QString Module::timestampString() const +{ + if (!m_timestamp.isValid()) { + return "(no timestamp)"; + } + + return m_timestamp.toString(Qt::DateFormat::ISODate); +} + +QString Module::toString() const +{ + QStringList sl; + + sl.push_back(displayPath()); + sl.push_back(QString("%1 B").arg(m_fileSize)); + + if (m_version.isEmpty() && m_versionString.isEmpty()) { + sl.push_back("(no version)"); + } else { + if (!m_version.isEmpty()) { + sl.push_back(m_version); + } + + if (!m_versionString.isEmpty() && m_versionString != m_version) { + sl.push_back(versionString()); + } + } + + if (m_timestamp.isValid()) { + sl.push_back(m_timestamp.toString(Qt::DateFormat::ISODate)); + } else { + sl.push_back("(no timestamp)"); + } + + if (!m_md5.isEmpty()) { + sl.push_back(m_md5); + } + + return sl.join(", "); +} + +Module::FileInfo Module::getFileInfo() const +{ + // ELF .so files don't carry the equivalent of a Win32 PE version resource. + return {}; +} + +QDateTime Module::getTimestamp() const +{ + QFileInfo fi(m_path); + if (!fi.exists()) { + return {}; + } + return fi.lastModified(); +} + +bool Module::interesting() const +{ + if (m_path.startsWith("/usr/lib/", Qt::CaseInsensitive) || + m_path.startsWith("/usr/lib64/", Qt::CaseInsensitive) || + m_path.startsWith("/lib/", Qt::CaseInsensitive) || + m_path.startsWith("/lib64/", Qt::CaseInsensitive)) { + return false; + } + + return true; +} + +QString Module::getMD5() const +{ + static const std::set<QString> ignore = { + "/usr/lib/", "/usr/lib64/", "/usr/share/", "/lib/", "/lib64/"}; + + for (auto&& i : ignore) { + if (m_path.startsWith(i, Qt::CaseInsensitive)) { + return {}; + } + } + + QFile f(m_path); + if (!f.open(QFile::ReadOnly)) { + log::error("failed to open file '{}' for md5", m_path); + return {}; + } + + QCryptographicHash hash(QCryptographicHash::Md5); + if (!hash.addData(&f)) { + log::error("failed to calculate md5 for '{}'", m_path); + return {}; + } + + return hash.result().toHex(); +} + +// -- Process implementation -- + +Process::Process() : m_pid(0) {} + +Process::Process(pid_t pid, pid_t ppid, QString name) + : m_pid(pid), m_ppid(ppid), m_name(std::move(name)) +{} + +bool Process::isValid() const +{ + return (m_pid != 0); +} + +pid_t Process::pid() const +{ + return m_pid; +} + +pid_t Process::ppid() const +{ + if (!m_ppid) { + m_ppid = getProcessParentID(m_pid); + } + + return *m_ppid; +} + +const QString& Process::name() const +{ + if (!m_name) { + m_name = getProcessName(m_pid); + } + + return *m_name; +} + +HandlePtr Process::openHandleForWait() const +{ + // On Linux, the caller can use waitpid() or kill(pid, 0) to check on the + // process. We just wrap the pid in the HANDLE-shaped type so callers + // designed for the Win32 API still compile. + if (kill(m_pid, 0) == 0) { + return HandlePtr(HandleCloser::pointer(static_cast<uintptr_t>(m_pid))); + } + return {}; +} + +bool Process::canAccess() const +{ + return (kill(m_pid, 0) == 0); +} + +void Process::addChild(Process p) +{ + m_children.push_back(p); +} + +std::vector<Process>& Process::children() +{ + return m_children; +} + +const std::vector<Process>& Process::children() const +{ + return m_children; +} + +// -- Free functions -- + +std::vector<Module> getLoadedModules() +{ + std::vector<Module> v; + + std::ifstream maps("/proc/self/maps"); + if (!maps.is_open()) { + log::error("failed to open /proc/self/maps"); + return {}; + } + + std::set<QString> seen; + std::string line; + + while (std::getline(maps, line)) { + // Each /proc/self/maps line: address perms offset dev inode pathname. + // The pathname (last field) is what we want when it's an absolute path. + auto spacePos = line.rfind(' '); + if (spacePos == std::string::npos) { + continue; + } + + std::string path = line.substr(spacePos + 1); + if (path.empty() || path[0] != '/') { + continue; + } + + QString qpath = QString::fromStdString(path); + + if (seen.count(qpath)) { + continue; + } + seen.insert(qpath); + + QFileInfo fi(qpath); + if (fi.exists()) { + v.push_back(Module(qpath, fi.size())); + } + } + + std::sort(v.begin(), v.end(), [](auto&& a, auto&& b) { + return (a.displayPath().compare(b.displayPath(), Qt::CaseInsensitive) < 0); + }); + + return v; +} + +std::vector<Process> getRunningProcesses() +{ + std::vector<Process> v; + + DIR* procDir = opendir("/proc"); + if (!procDir) { + log::error("failed to open /proc"); + return {}; + } + + struct dirent* entry; + while ((entry = readdir(procDir)) != nullptr) { + bool isNumeric = true; + for (const char* p = entry->d_name; *p; ++p) { + if (*p < '0' || *p > '9') { + isNumeric = false; + break; + } + } + + if (!isNumeric) { + continue; + } + + pid_t pid = static_cast<pid_t>(std::strtol(entry->d_name, nullptr, 10)); + + QString name; + { + std::string commPath = "/proc/" + std::string(entry->d_name) + "/comm"; + std::ifstream commFile(commPath); + if (commFile.is_open()) { + std::string commName; + std::getline(commFile, commName); + name = QString::fromStdString(commName); + } + } + + pid_t ppid = 0; + { + std::string statusPath = "/proc/" + std::string(entry->d_name) + "/status"; + std::ifstream statusFile(statusPath); + if (statusFile.is_open()) { + std::string statusLine; + while (std::getline(statusFile, statusLine)) { + if (statusLine.compare(0, 5, "PPid:") == 0) { + ppid = static_cast<pid_t>( + std::strtol(statusLine.c_str() + 5, nullptr, 10)); + break; + } + } + } + } + + v.push_back(Process(pid, ppid, name)); + } + + closedir(procDir); + 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(pid_t pid) +{ + Process root; + + const auto v = getRunningProcesses(); + + for (auto&& p : v) { + if (p.pid() == pid) { + Process child = p; + findChildren(child, v); + root.addChild(child); + break; + } + } + + return root; +} + +QString getProcessName(pid_t pid) +{ + std::string commPath = "/proc/" + std::to_string(pid) + "/comm"; + std::ifstream commFile(commPath); + + if (!commFile.is_open()) { + log::error("can't get name of process {}", pid); + return "unknown"; + } + + std::string name; + std::getline(commFile, name); + return QString::fromStdString(name); +} + +pid_t getProcessParentID(pid_t pid) +{ + std::string statusPath = "/proc/" + std::to_string(pid) + "/status"; + std::ifstream statusFile(statusPath); + + if (!statusFile.is_open()) { + return 0; + } + + std::string line; + while (std::getline(statusFile, line)) { + if (line.compare(0, 5, "PPid:") == 0) { + return static_cast<pid_t>(std::strtol(line.c_str() + 5, nullptr, 10)); + } + } + + return 0; +} + +} // namespace env diff --git a/src/src/envmodule.h b/src/src/envmodule.h index 1dfc927..6b41aef 100644 --- a/src/src/envmodule.h +++ b/src/src/envmodule.h @@ -1,228 +1,138 @@ -#ifndef ENV_MODULE_H
-#define ENV_MODULE_H
-
-#include <QDateTime>
-#include <QString>
-
-#ifdef _WIN32
-#include <windows.h>
-#else
-#include <sys/types.h>
-#include <signal.h>
-#endif
-
-namespace env
-{
-
-#ifdef _WIN32
-// 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>;
-#else
-// On Linux, HandlePtr uses HANDLE (void*) from windows_compat.h for
-// compatibility with the rest of the codebase. Handles don't need
-// closing on Linux since they are just compatibility shims.
-struct HandleCloser
-{
- using pointer = HANDLE;
-
- void operator()(HANDLE) { /* no-op on Linux */ }
-};
-
-using HandlePtr = std::unique_ptr<HANDLE, HandleCloser>;
-#endif
-
-// represents one module
-//
-class Module
-{
-public:
- Module(QString path, std::size_t fileSize);
-
- // returns the module's path
- //
- const QString& path() const;
-
- // returns the module's path in lowercase and using forward slashes
- //
- QString displayPath() const;
-
- // returns the size in bytes, may be 0
- //
- std::size_t fileSize() const;
-
- // returns the x.x.x.x version embedded from the version info, may be empty
- //
- const QString& version() const;
-
- // returns the FileVersion entry from the resource file, returns
- // "(no version)" if not available
- //
- const QString& versionString() const;
-
- // returns the build date from the version info, or the creation time of the
- // file on the filesystem, may be empty
- //
- const QDateTime& timestamp() const;
-
- // returns the md5 of the file, may be empty for system files
- //
- const QString& md5() const;
-
- // converts timestamp() to a string for display, returns "(no timestamp)" if
- // not available
- //
- QString timestampString() const;
-
- // returns false for modules in system directories
- //
- bool interesting() const;
-
- // returns a string with all the above information on one line
- //
- QString toString() const;
-
-private:
-#ifdef _WIN32
- // contains the information from the version resource
- //
- struct FileInfo
- {
- VS_FIXEDFILEINFO ffi;
- QString fileDescription;
- };
-#else
- struct FileInfo
- {
- QString fileDescription;
- };
-#endif
-
- QString m_path;
- std::size_t m_fileSize;
- QString m_version;
- QDateTime m_timestamp;
- QString m_versionString;
- QString m_md5;
-
-#ifdef _WIN32
- // returns information from the version resource
- //
- FileInfo getFileInfo() const;
-
- // uses VS_FIXEDFILEINFO to build the version string
- //
- QString getVersion(const VS_FIXEDFILEINFO& fi) const;
-
- // uses the file date from VS_FIXEDFILEINFO if available, or gets the
- // creation date on the file
- //
- QDateTime getTimestamp(const VS_FIXEDFILEINFO& fi) const;
-
- // gets VS_FIXEDFILEINFO from the file version info buffer
- //
- VS_FIXEDFILEINFO getFixedFileInfo(std::byte* buffer) const;
-
- // gets FileVersion from the file version info buffer
- //
- QString getFileDescription(std::byte* buffer) const;
-#else
- // returns information about the file (Linux: uses QFileInfo)
- //
- FileInfo getFileInfo() const;
-
- // gets the file timestamp from the filesystem
- //
- QDateTime getTimestamp() const;
-#endif
-
- // returns the md5 hash unless the path is in a system directory
- //
- QString getMD5() const;
-};
-
-// represents one process
-//
-class Process
-{
-public:
- Process();
-#ifdef _WIN32
- explicit Process(HANDLE h);
- Process(DWORD pid, DWORD ppid, QString name);
-#else
- Process(pid_t pid, pid_t ppid, QString name);
-#endif
-
- bool isValid() const;
-
-#ifdef _WIN32
- DWORD pid() const;
- DWORD ppid() const;
-#else
- pid_t pid() const;
- pid_t ppid() const;
-#endif
-
- 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();
- const std::vector<Process>& children() const;
-
-private:
-#ifdef _WIN32
- DWORD m_pid;
- mutable std::optional<DWORD> m_ppid;
-#else
- pid_t m_pid;
- mutable std::optional<pid_t> m_ppid;
-#endif
- mutable std::optional<QString> m_name;
- std::vector<Process> m_children;
-};
-
-std::vector<Process> getRunningProcesses();
-std::vector<Module> getLoadedModules();
-
-#ifdef _WIN32
-// works for both jobs and processes
-//
-Process getProcessTree(HANDLE h);
-
-QString getProcessName(DWORD pid);
-QString getProcessName(HANDLE process);
-
-DWORD getProcessParentID(DWORD pid);
-DWORD getProcessParentID(HANDLE handle);
-#else
-// builds a process tree from a given pid
-//
-Process getProcessTree(pid_t pid);
-
-QString getProcessName(pid_t pid);
-pid_t getProcessParentID(pid_t pid);
-#endif
-
-} // namespace env
-
-#endif // ENV_MODULE_H
+#ifndef ENV_MODULE_H +#define ENV_MODULE_H + +#include <QDateTime> +#include <QString> + +#include <sys/types.h> +#include <signal.h> + +namespace env +{ + +// HandlePtr exists for compatibility with the rest of the codebase that +// expects a Win32 HANDLE-shaped type. On Linux there's nothing to close, +// so the deleter is a no-op. +struct HandleCloser +{ + using pointer = HANDLE; + + void operator()(HANDLE) {} +}; + +using HandlePtr = std::unique_ptr<HANDLE, HandleCloser>; + +// represents one module +// +class Module +{ +public: + Module(QString path, std::size_t fileSize); + + // returns the module's path + // + const QString& path() const; + + // returns the module's path in lowercase and using forward slashes + // + QString displayPath() const; + + // returns the size in bytes, may be 0 + // + std::size_t fileSize() const; + + // returns the x.x.x.x version embedded from the version info, may be empty + // + const QString& version() const; + + // returns the FileVersion entry from the resource file, returns + // "(no version)" if not available + // + const QString& versionString() const; + + // returns the build date from the version info, or the creation time of the + // file on the filesystem, may be empty + // + const QDateTime& timestamp() const; + + // returns the md5 of the file, may be empty for system files + // + const QString& md5() const; + + // converts timestamp() to a string for display, returns "(no timestamp)" if + // not available + // + QString timestampString() const; + + // returns false for modules in system directories + // + bool interesting() const; + + // returns a string with all the above information on one line + // + QString toString() const; + +private: + struct FileInfo + { + QString fileDescription; + }; + + QString m_path; + std::size_t m_fileSize; + QString m_version; + QDateTime m_timestamp; + QString m_versionString; + QString m_md5; + + FileInfo getFileInfo() const; + QDateTime getTimestamp() const; + QString getMD5() const; +}; + +// represents one process +// +class Process +{ +public: + Process(); + Process(pid_t pid, pid_t ppid, QString name); + + bool isValid() const; + + pid_t pid() const; + pid_t 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(); + const std::vector<Process>& children() const; + +private: + pid_t m_pid; + mutable std::optional<pid_t> m_ppid; + mutable std::optional<QString> m_name; + std::vector<Process> m_children; +}; + +std::vector<Process> getRunningProcesses(); +std::vector<Module> getLoadedModules(); + +// builds a process tree from a given pid +// +Process getProcessTree(pid_t pid); + +QString getProcessName(pid_t pid); +pid_t getProcessParentID(pid_t pid); + +} // namespace env + +#endif // ENV_MODULE_H diff --git a/src/src/envsecurity.cpp b/src/src/envsecurity.cpp index 8b6c6de..33066a0 100644 --- a/src/src/envsecurity.cpp +++ b/src/src/envsecurity.cpp @@ -1,764 +1,119 @@ -#include "envsecurity.h"
-#include "env.h"
-#include "envmodule.h"
-#include <log.h>
-#include <utility.h>
-
-#ifdef _WIN32
-#include <Wbemidl.h>
-#include <comdef.h>
-#include <netfw.h>
-#include <wscapi.h>
-#pragma comment(lib, "Wbemuuid.lib")
-
-#include <accctrl.h>
-#include <aclapi.h>
-#include <sddl.h>
-#pragma comment(lib, "advapi32.lib")
-#else
-#include <sys/stat.h>
-#include <pwd.h>
-#include <unistd.h>
-#endif
-
-namespace env
-{
-
-using namespace MOBase;
-
-#ifdef _WIN32
-
-class WMI
-{
-public:
- class failed
- {};
-
- WMI(const std::string& ns)
- {
- try {
- createLocator();
- createService(ns);
- setSecurity();
- } catch (failed&) {
- }
- }
-
- template <class F>
- void query(const std::string& q, F&& f)
- {
- if (!m_locator || !m_service) {
- return;
- }
-
- auto enumerator = getEnumerator(q);
- if (!enumerator) {
- return;
- }
-
- for (;;) {
- COMPtr<IWbemClassObject> object;
-
- {
- IWbemClassObject* rawObject = nullptr;
- ULONG count = 0;
- auto ret = enumerator->Next(WBEM_INFINITE, 1, &rawObject, &count);
-
- if (count == 0 || !rawObject) {
- break;
- }
-
- if (FAILED(ret)) {
- log::error("enum->next() failed, {}", formatSystemMessage(ret));
- break;
- }
-
- object.reset(rawObject);
- }
-
- f(object.get());
- }
- }
-
-private:
- COMPtr<IWbemLocator> m_locator;
- COMPtr<IWbemServices> m_service;
-
- void createLocator()
- {
- void* rawLocator = nullptr;
-
- const auto ret = CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,
- IID_IWbemLocator, &rawLocator);
-
- if (FAILED(ret) || !rawLocator) {
- log::error("CoCreateInstance for WbemLocator failed, {}",
- formatSystemMessage(ret));
-
- throw failed();
- }
-
- m_locator.reset(static_cast<IWbemLocator*>(rawLocator));
- }
-
- void createService(const std::string& ns)
- {
- IWbemServices* rawService = nullptr;
-
- const auto res =
- m_locator->ConnectServer(_bstr_t(ns.c_str()), nullptr, nullptr, nullptr, 0,
- nullptr, nullptr, &rawService);
-
- if (FAILED(res) || !rawService) {
- log::debug("locator->ConnectServer() failed for namespace '{}', {}", ns,
- formatSystemMessage(res));
-
- throw failed();
- }
-
- m_service.reset(rawService);
- }
-
- void setSecurity()
- {
- auto ret = CoSetProxyBlanket(m_service.get(), RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE,
- nullptr, RPC_C_AUTHN_LEVEL_CALL,
- RPC_C_IMP_LEVEL_IMPERSONATE, 0, EOAC_NONE);
-
- if (FAILED(ret)) {
- log::error("CoSetProxyBlanket() failed, {}", formatSystemMessage(ret));
- throw failed();
- }
- }
-
- COMPtr<IEnumWbemClassObject> getEnumerator(const std::string& query)
- {
- IEnumWbemClassObject* rawEnumerator = NULL;
-
- auto ret = m_service->ExecQuery(
- bstr_t("WQL"), bstr_t(query.c_str()),
- WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &rawEnumerator);
-
- if (FAILED(ret) || !rawEnumerator) {
- log::error("query '{}' failed, {}", query, formatSystemMessage(ret));
- return {};
- }
-
- return COMPtr<IEnumWbemClassObject>(rawEnumerator);
- }
-};
-
-#endif // _WIN32
-
-SecurityProduct::SecurityProduct(QUuid guid, QString name, int provider, bool active,
- bool upToDate)
- : m_guid(std::move(guid)), m_name(std::move(name)), m_provider(provider),
- m_active(active), m_upToDate(upToDate)
-{}
-
-const QUuid& SecurityProduct::guid() const
-{
- return m_guid;
-}
-
-const QString& SecurityProduct::name() const
-{
- return m_name;
-}
-
-int SecurityProduct::provider() const
-{
- return m_provider;
-}
-
-bool SecurityProduct::active() const
-{
- return m_active;
-}
-
-bool SecurityProduct::upToDate() const
-{
- return m_upToDate;
-}
-
-QString SecurityProduct::toString() const
-{
- QString s;
-
- if (m_name.isEmpty()) {
- s += "(no name)";
- } else {
- s += m_name;
- }
-
- s += " (" + providerToString() + ")";
-
- if (!m_active) {
- s += ", inactive";
- }
-
- if (!m_upToDate) {
- s += ", definitions outdated";
- }
-
- return s;
-}
-
-QString SecurityProduct::providerToString() const
-{
-#ifdef _WIN32
- QStringList ps;
-
- if (m_provider & WSC_SECURITY_PROVIDER_FIREWALL) {
- ps.push_back("firewall");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_AUTOUPDATE_SETTINGS) {
- ps.push_back("autoupdate");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_ANTIVIRUS) {
- ps.push_back("antivirus");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_ANTISPYWARE) {
- ps.push_back("antispyware");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_INTERNET_SETTINGS) {
- ps.push_back("settings");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_USER_ACCOUNT_CONTROL) {
- ps.push_back("uac");
- }
-
- if (m_provider & WSC_SECURITY_PROVIDER_SERVICE) {
- ps.push_back("service");
- }
-
- if (ps.empty()) {
- return "doesn't provide anything";
- }
-
- return ps.join("|");
-#else
- return "n/a";
-#endif
-}
-
-#ifdef _WIN32
-
-std::optional<SecurityProduct> handleProduct(IWbemClassObject* o)
-{
- VARIANT prop;
-
- // guid
- auto ret = o->Get(L"instanceGuid", 0, &prop, 0, 0);
- if (FAILED(ret)) {
- log::error("failed to get instanceGuid, {}", formatSystemMessage(ret));
- return {};
- }
-
- if (prop.vt != VT_BSTR) {
- log::error("instanceGuid is a {}, not a bstr", prop.vt);
- return {};
- }
-
- const QUuid guid(QString::fromWCharArray(prop.bstrVal));
- VariantClear(&prop);
-
- // display name
- QString displayName;
- ret = o->Get(L"displayName", 0, &prop, 0, 0);
-
- if (FAILED(ret)) {
- log::error("failed to get displayName, {}", formatSystemMessage(ret));
- } else if (prop.vt != VT_BSTR) {
- log::error("displayName is a {}, not a bstr", prop.vt);
- } else {
- displayName = QString::fromWCharArray(prop.bstrVal);
- }
-
- VariantClear(&prop);
-
- // product state
- DWORD state = 0;
- ret = o->Get(L"productState", 0, &prop, 0, 0);
-
- if (FAILED(ret)) {
- log::error("failed to get productState, {}", formatSystemMessage(ret));
- } else {
- if (prop.vt == VT_I4) {
- state = prop.lVal;
- } else if (prop.vt == VT_UI4) {
- state = prop.ulVal;
- } else if (prop.vt == VT_UI1) {
- state = prop.bVal;
- } else if (prop.vt == VT_NULL) {
- log::warn("productState is null");
- } else {
- log::error("productState is a {}, not a VT_I4 or a VT_UI4", prop.vt);
- }
- }
-
- VariantClear(&prop);
-
- const auto provider = static_cast<int>((state >> 16) & 0xff);
- const auto scanner = (state >> 8) & 0xff;
- const auto definitions = state & 0xff;
-
- const bool active = ((scanner & 0x10) != 0);
- const bool upToDate = (definitions == 0);
-
- return SecurityProduct(guid, displayName, provider, active, upToDate);
-}
-
-std::vector<SecurityProduct> getSecurityProductsFromWMI()
-{
- std::map<QUuid, SecurityProduct> map;
-
- auto f = [&](auto* o) {
- if (auto p = handleProduct(o)) {
- map.emplace(p->guid(), std::move(*p));
- }
- };
-
- {
- WMI wmi("root\\SecurityCenter2");
- wmi.query("select * from AntivirusProduct", f);
- wmi.query("select * from FirewallProduct", f);
- wmi.query("select * from AntiSpywareProduct", f);
- }
-
- {
- WMI wmi("root\\SecurityCenter");
- wmi.query("select * from AntivirusProduct", f);
- wmi.query("select * from FirewallProduct", f);
- wmi.query("select * from AntiSpywareProduct", f);
- }
-
- std::vector<SecurityProduct> v;
-
- for (auto&& p : map) {
- v.push_back(p.second);
- }
-
- return v;
-}
-
-std::optional<SecurityProduct> getWindowsFirewall()
-{
- HRESULT hr = 0;
-
- COMPtr<INetFwPolicy2> policy;
-
- {
- void* rawPolicy = nullptr;
-
- hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr, CLSCTX_INPROC_SERVER,
- __uuidof(INetFwPolicy2), &rawPolicy);
-
- if (FAILED(hr) || !rawPolicy) {
- log::error("CoCreateInstance for NetFwPolicy2 failed, {}",
- formatSystemMessage(hr));
-
- return {};
- }
-
- policy.reset(static_cast<INetFwPolicy2*>(rawPolicy));
- }
-
- VARIANT_BOOL enabledVariant;
-
- if (policy) {
- hr = policy->get_FirewallEnabled(NET_FW_PROFILE2_PUBLIC, &enabledVariant);
- if (FAILED(hr)) {
- if (hr != EPT_S_NOT_REGISTERED && hr != 0x800706d9) {
- log::debug("get_FirewallEnabled failed, {}", formatSystemMessage(hr));
- }
-
- return {};
- }
- }
-
- const auto enabled = (enabledVariant != VARIANT_FALSE);
- if (!enabled) {
- return {};
- }
-
- return SecurityProduct({}, "Windows Firewall", WSC_SECURITY_PROVIDER_FIREWALL, true,
- true);
-}
-
-std::vector<SecurityProduct> getSecurityProducts()
-{
- std::vector<SecurityProduct> v;
-
- {
- auto fromWMI = getSecurityProductsFromWMI();
- v.insert(v.end(), std::make_move_iterator(fromWMI.begin()),
- std::make_move_iterator(fromWMI.end()));
- }
-
- if (auto p = getWindowsFirewall()) {
- v.push_back(std::move(*p));
- }
-
- return v;
-}
-
-class failed
-{
-public:
- failed(DWORD e, QString what)
- : m_what(what + ", " + QString::fromStdWString(formatSystemMessage(e)))
- {}
-
- QString what() const { return m_what; }
-
-private:
- QString m_what;
-};
-
-MallocPtr<SECURITY_DESCRIPTOR> getSecurityDescriptor(const QString& path)
-{
- const auto wpath = path.toStdWString();
- BOOL ret = FALSE;
-
- DWORD length = 0;
- ret = ::GetFileSecurityW(wpath.c_str(),
- DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
- nullptr, 0, &length);
-
- if (!ret || length == 0) {
- const auto e = GetLastError();
-
- if (e != ERROR_INSUFFICIENT_BUFFER) {
- if (e == ERROR_ACCESS_DENIED) {
- throw failed(e, "cannot get security descriptor");
- } else {
- throw failed(e, "GetFileSecurity() for length failed");
- }
- }
- }
-
- MallocPtr<SECURITY_DESCRIPTOR> sd(
- static_cast<SECURITY_DESCRIPTOR*>(std::malloc(length)));
-
- std::memset(sd.get(), 0, length);
-
- ret = ::GetFileSecurityW(wpath.c_str(),
- DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION,
- sd.get(), length, &length);
-
- if (!ret) {
- const auto e = GetLastError();
- throw failed(e, "GetFileSecurity()");
- }
-
- return sd;
-}
-
-PACL getDacl(SECURITY_DESCRIPTOR* sd)
-{
- BOOL present = FALSE;
- BOOL daclDefaulted = FALSE;
- PACL acl = nullptr;
-
- BOOL ret = ::GetSecurityDescriptorDacl(sd, &present, &acl, &daclDefaulted);
-
- if (!ret) {
- const auto e = GetLastError();
- throw failed(e, "GetSecurityDescriptorDacl()");
- }
-
- if (!present) {
- return nullptr;
- }
-
- return acl;
-}
-
-PSID getFileOwner(SECURITY_DESCRIPTOR* sd)
-{
- BOOL ownerDefaulted = FALSE;
- PSID owner;
-
- BOOL ret = ::GetSecurityDescriptorOwner(sd, &owner, &ownerDefaulted);
-
- if (!ret) {
- const auto e = GetLastError();
- throw failed(e, "GetSecurityDescriptionOwner()");
- }
-
- return owner;
-}
-
-MallocPtr<void> getCurrentUser()
-{
- HANDLE hnd = ::GetCurrentProcess();
- HANDLE rawToken = 0;
-
- BOOL ret = ::OpenProcessToken(hnd, TOKEN_QUERY, &rawToken);
- if (!ret) {
- const auto e = GetLastError();
- throw(e, "OpenProcessToken()");
- }
-
- HandlePtr token(rawToken);
-
- DWORD retsize = 0;
- ret = ::GetTokenInformation(token.get(), TokenUser, 0, 0, &retsize);
-
- if (!ret) {
- const auto e = GetLastError();
- if (e != ERROR_INSUFFICIENT_BUFFER) {
- throw failed(e, "GetTokenInformation() for length");
- }
- }
-
- MallocPtr<void> tokenBuffer(std::malloc(retsize));
- ret = ::GetTokenInformation(token.get(), TokenUser, tokenBuffer.get(), retsize,
- &retsize);
-
- if (!ret) {
- const auto e = GetLastError();
- throw failed(e, "GetTokenInformation()");
- }
-
- PSID tokenSid = ((PTOKEN_USER)(tokenBuffer.get()))->User.Sid;
- DWORD sidLen = ::GetLengthSid(tokenSid);
- MallocPtr<void> currentUserSID((SID*)(malloc(sidLen)));
-
- ret = ::CopySid(sidLen, currentUserSID.get(), tokenSid);
-
- if (!ret) {
- const auto e = GetLastError();
- throw failed(e, "CopySid()");
- }
-
- return currentUserSID;
-}
-
-ACCESS_MASK getEffectiveRights(ACL* dacl, PSID sid)
-{
- TRUSTEEW trustee = {};
- BuildTrusteeWithSid(&trustee, sid);
-
- ACCESS_MASK access = 0;
- DWORD ret = ::GetEffectiveRightsFromAclW(dacl, &trustee, &access);
-
- if (ret != ERROR_SUCCESS) {
- throw failed(ret, "GetEffectiveRightsFromAclW()");
- }
-
- return access;
-}
-
-QString getUsername(PSID owner)
-{
- DWORD nameSize = 0, domainSize = 0;
- auto use = SidTypeUnknown;
-
- BOOL ret =
- LookupAccountSidW(nullptr, owner, nullptr, &nameSize, nullptr, &domainSize, &use);
-
- if (!ret) {
- const auto e = GetLastError();
-
- if (e != ERROR_INSUFFICIENT_BUFFER) {
- throw failed(e, "LookupAccountSid() for sizes");
- }
- }
-
- auto wsName = std::make_unique<wchar_t[]>(nameSize);
- auto wsDomain = std::make_unique<wchar_t[]>(domainSize);
-
- ret = LookupAccountSidW(nullptr, owner, wsName.get(), &nameSize, wsDomain.get(),
- &domainSize, &use);
-
- if (!ret) {
- const auto e = GetLastError();
- throw failed(e, "LookupAccountSid()");
- }
-
- const QString name = QString::fromWCharArray(wsName.get(), nameSize);
- const QString domain = QString::fromWCharArray(wsDomain.get(), domainSize);
-
- if (!name.isEmpty() && !domain.isEmpty()) {
- return domain + "\\" + name;
- } else {
- // either or both are empty
- return name + domain;
- }
-}
-
-FileRights makeFileRights(ACCESS_MASK m)
-{
- FileRights fr;
-
- if (m & FILE_GENERIC_READ) {
- fr.list.push_back("file_generic_read");
- } else {
- if (m & READ_CONTROL) {
- fr.list.push_back("read_ctrl");
- }
-
- if (m & FILE_READ_DATA) {
- fr.list.push_back("read_data");
- }
-
- if (m & FILE_READ_ATTRIBUTES) {
- fr.list.push_back("read_atts");
- }
-
- if (m & FILE_READ_EA) {
- fr.list.push_back("read_ex_atts");
- }
-
- if (m & SYNCHRONIZE) {
- fr.list.push_back("sync");
- }
- }
-
- if (m & FILE_GENERIC_WRITE) {
- fr.list.push_back("file_generic_write");
- } else {
- if (m & FILE_WRITE_DATA) {
- fr.list.push_back("write_data");
- }
-
- if (m & FILE_WRITE_ATTRIBUTES) {
- fr.list.push_back("write_atts");
- }
-
- if (m & FILE_WRITE_EA) {
- fr.list.push_back("write_ex_atts");
- }
-
- if (m & FILE_APPEND_DATA) {
- fr.list.push_back("append_data");
- }
- }
-
- if (m & FILE_GENERIC_EXECUTE) {
- fr.list.push_back("file_generic_execute");
- fr.hasExecute = true;
- } else {
- if (m & FILE_EXECUTE) {
- fr.list.push_back("execute");
- fr.hasExecute = true;
- }
- }
-
- if (m & DELETE) {
- fr.list.push_back("delete");
- }
-
- if (m & WRITE_DAC) {
- fr.list.push_back("write_dac");
- }
-
- if (m & WRITE_OWNER) {
- fr.list.push_back("write_owner");
- }
-
- if (m & GENERIC_ALL) {
- fr.list.push_back("generic_all");
- }
-
- if (m & GENERIC_WRITE) {
- fr.list.push_back("generic_write");
- }
-
- if (m & GENERIC_READ) {
- fr.list.push_back("generic_read");
- }
-
- const auto normalRights = STANDARD_RIGHTS_ALL | FILE_GENERIC_READ |
- FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE |
- FILE_DELETE_CHILD;
-
- if (m == normalRights) {
- fr.normalRights = true;
- }
-
- return fr;
-}
-
-FileSecurity getFileSecurity(const QString& path)
-{
- FileSecurity fs;
-
- try {
- auto sd = getSecurityDescriptor(path);
- auto dacl = getDacl(sd.get());
- auto currentUser = getCurrentUser();
- auto owner = getFileOwner(sd.get());
- auto access = getEffectiveRights(dacl, currentUser.get());
-
- fs.rights = makeFileRights(access);
-
- if (EqualSid(owner, currentUser.get())) {
- fs.owner = "(this user)";
- } else {
- fs.owner = getUsername(owner);
- }
- } catch (failed& f) {
- fs.error = f.what();
- }
-
- return fs;
-}
-
-#else // Linux
-
-std::vector<SecurityProduct> getSecurityProducts()
-{
- // No equivalent on Linux
- return {};
-}
-
-FileSecurity getFileSecurity(const QString& path)
-{
- FileSecurity fs;
-
- struct stat st;
- if (stat(path.toStdString().c_str(), &st) != 0) {
- fs.error = QString("stat() failed for '%1': %2").arg(path).arg(strerror(errno));
- return fs;
- }
-
- // Owner
- struct passwd* pw = getpwuid(st.st_uid);
- if (pw) {
- uid_t currentUid = getuid();
- if (st.st_uid == currentUid) {
- fs.owner = "(this user)";
- } else {
- fs.owner = QString::fromUtf8(pw->pw_name);
- }
- } else {
- fs.owner = QString::number(st.st_uid);
- }
-
- // Rights
- if (st.st_mode & S_IRUSR) fs.rights.list.push_back("read");
- if (st.st_mode & S_IWUSR) fs.rights.list.push_back("write");
- if (st.st_mode & S_IXUSR) {
- fs.rights.list.push_back("execute");
- fs.rights.hasExecute = true;
- }
-
- // Check if current user has access
- if (access(path.toStdString().c_str(), R_OK | W_OK | X_OK) == 0) {
- fs.rights.normalRights = true;
- }
-
- return fs;
-}
-
-#endif // _WIN32
-
-} // namespace env
+#include "envsecurity.h" +#include "env.h" +#include "envmodule.h" +#include <log.h> +#include <utility.h> + +#include <sys/stat.h> +#include <pwd.h> +#include <unistd.h> + +namespace env +{ + +using namespace MOBase; + +SecurityProduct::SecurityProduct(QUuid guid, QString name, int provider, bool active, + bool upToDate) + : m_guid(std::move(guid)), m_name(std::move(name)), m_provider(provider), + m_active(active), m_upToDate(upToDate) +{} + +const QUuid& SecurityProduct::guid() const +{ + return m_guid; +} + +const QString& SecurityProduct::name() const +{ + return m_name; +} + +int SecurityProduct::provider() const +{ + return m_provider; +} + +bool SecurityProduct::active() const +{ + return m_active; +} + +bool SecurityProduct::upToDate() const +{ + return m_upToDate; +} + +QString SecurityProduct::toString() const +{ + QString s; + + if (m_name.isEmpty()) { + s += "(no name)"; + } else { + s += m_name; + } + + s += " (" + providerToString() + ")"; + + if (!m_active) { + s += ", inactive"; + } + + if (!m_upToDate) { + s += ", definitions outdated"; + } + + return s; +} + +QString SecurityProduct::providerToString() const +{ + return "n/a"; +} + +std::vector<SecurityProduct> getSecurityProducts() +{ + // Linux has no equivalent of Windows Security Center / WSC API. + return {}; +} + +FileSecurity getFileSecurity(const QString& path) +{ + FileSecurity fs; + + struct stat st; + if (stat(path.toStdString().c_str(), &st) != 0) { + fs.error = QString("stat() failed for '%1': %2").arg(path).arg(strerror(errno)); + return fs; + } + + struct passwd* pw = getpwuid(st.st_uid); + if (pw) { + uid_t currentUid = getuid(); + if (st.st_uid == currentUid) { + fs.owner = "(this user)"; + } else { + fs.owner = QString::fromUtf8(pw->pw_name); + } + } else { + fs.owner = QString::number(st.st_uid); + } + + if (st.st_mode & S_IRUSR) + fs.rights.list.push_back("read"); + if (st.st_mode & S_IWUSR) + fs.rights.list.push_back("write"); + if (st.st_mode & S_IXUSR) { + fs.rights.list.push_back("execute"); + fs.rights.hasExecute = true; + } + + if (access(path.toStdString().c_str(), R_OK | W_OK | X_OK) == 0) { + fs.rights.normalRights = true; + } + + return fs; +} + +} // namespace env diff --git a/src/src/filetreeitem.cpp b/src/src/filetreeitem.cpp index bca020c..e2e8a30 100644 --- a/src/src/filetreeitem.cpp +++ b/src/src/filetreeitem.cpp @@ -6,10 +6,8 @@ #include <log.h>
#include <utility.h>
-#ifndef _WIN32
#include <QMimeDatabase>
#include <QMimeType>
-#endif
using namespace MOBase;
using namespace MOShared;
@@ -19,61 +17,17 @@ constexpr bool AlwaysSortDirectoriesFirst = true; const QString& directoryFileType()
{
- static const QString name = [] {
-#ifdef _WIN32
- const DWORD flags = SHGFI_TYPENAME;
- SHFILEINFOW sfi = {};
-
- // "." for the current directory, which should always exist
- const auto r = SHGetFileInfoW(L".", 0, &sfi, sizeof(sfi), flags);
-
- if (!r) {
- const auto e = GetLastError();
-
- log::error("SHGetFileInfoW failed for folder file type, {}",
- formatSystemMessage(e));
-
- return QString("File folder");
- } else {
- return QString::fromWCharArray(sfi.szTypeName);
- }
-#else
- return QString("File folder");
-#endif
- }();
-
+ static const QString name = QStringLiteral("File folder");
return name;
}
const QString& cachedFileTypeNoExtension()
{
- static const QString name = [] {
-#ifdef _WIN32
- const DWORD flags = SHGFI_TYPENAME | SHGFI_USEFILEATTRIBUTES;
- SHFILEINFOW sfi = {};
-
- // dummy filename with no extension
- const auto r = SHGetFileInfoW(L"file", 0, &sfi, sizeof(sfi), flags);
-
- if (!r) {
- const auto e = GetLastError();
-
- log::error("SHGetFileInfoW failed for file without extension, {}",
- formatSystemMessage(e));
-
- return QString("File");
- } else {
- return QString::fromWCharArray(sfi.szTypeName);
- }
-#else
- return QString("File");
-#endif
- }();
-
+ static const QString name = QStringLiteral("File");
return name;
}
-const QString& cachedFileType(const std::wstring& file, bool isOnFilesystem)
+const QString& cachedFileType(const std::wstring& file, bool /*isOnFilesystem*/)
{
static std::map<std::wstring, QString, std::less<>> map;
static std::mutex mutex;
@@ -91,39 +45,13 @@ const QString& cachedFileType(const std::wstring& file, bool isOnFilesystem) return itor->second;
}
-#ifdef _WIN32
- DWORD flags = SHGFI_TYPENAME;
-
- if (!isOnFilesystem) {
- // files from archives are not on the filesystem; this flag forces
- // SHGetFileInfoW() to only work with the filename
- flags |= SHGFI_USEFILEATTRIBUTES;
- }
-
- SHFILEINFOW sfi = {};
- const auto r = SHGetFileInfoW(file.c_str(), 0, &sfi, sizeof(sfi), flags);
-
- QString s;
-
- if (!r) {
- const auto e = GetLastError();
-
- log::error("SHGetFileInfoW failed for '{}', {}", file, formatSystemMessage(e));
-
- s = cachedFileTypeNoExtension();
- } else {
- s = QString::fromWCharArray(sfi.szTypeName);
- }
-#else
- // On Linux, use QMimeDatabase to get the file type description
static QMimeDatabase mimeDb;
- QString filename = QString::fromStdWString(file);
+ QString filename = QString::fromStdWString(file);
QMimeType mimeType = mimeDb.mimeTypeForFile(filename, QMimeDatabase::MatchExtension);
- QString s = mimeType.comment();
+ QString s = mimeType.comment();
if (s.isEmpty()) {
s = cachedFileTypeNoExtension();
}
-#endif
return map.emplace(sv, s).first->second;
}
diff --git a/src/src/fluorinepaths.cpp b/src/src/fluorinepaths.cpp index b6e6633..141a1bc 100644 --- a/src/src/fluorinepaths.cpp +++ b/src/src/fluorinepaths.cpp @@ -19,10 +19,6 @@ QString fluorineDataDir() void fluorineMigrateDataDir() { -#ifdef _WIN32 - return; -#endif - const QString oldRoot = OldFlatpakRoot; const QString newRoot = fluorineDataDir(); diff --git a/src/src/iconfetcher.cpp b/src/src/iconfetcher.cpp index 5dce4ae..e73100f 100644 --- a/src/src/iconfetcher.cpp +++ b/src/src/iconfetcher.cpp @@ -21,11 +21,7 @@ void IconFetcher::Waiter::wakeUp() m_wakeUp.notify_one();
}
-#ifdef _WIN32
-IconFetcher::IconFetcher() : m_iconSize(GetSystemMetrics(SM_CXSMICON)), m_stop(false)
-#else
IconFetcher::IconFetcher() : m_iconSize(16), m_stop(false)
-#endif
{
m_quickCache.file = getPixmapIcon(QFileIconProvider::File);
m_quickCache.directory = getPixmapIcon(QFileIconProvider::Folder);
diff --git a/src/src/installationmanager.cpp b/src/src/installationmanager.cpp index 9bd0cd8..a59458d 100644 --- a/src/src/installationmanager.cpp +++ b/src/src/installationmanager.cpp @@ -50,10 +50,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QTextDocument>
#include <QtConcurrent/QtConcurrentRun>
-#ifdef _WIN32
-#include <Shellapi.h>
-#endif
-
#include <boost/assign.hpp>
#include <boost/scoped_ptr.hpp>
@@ -452,13 +448,10 @@ InstallationResult InstallationManager::testOverwrite(GuessedValue<QString>& mod // remove the directory with all content, then recreate it empty
shellDelete(QStringList(targetDirectory));
if (!QDir().mkdir(targetDirectory)) {
- // windows may keep the directory around for a moment, preventing its
- // re-creation. Not sure if this still happens with shellDelete
-#ifdef _WIN32
- Sleep(100);
-#else
+ // The retry exists because Windows can keep a directory around for a
+ // moment after delete. Linux doesn't have that problem; the sleep is
+ // kept for safety in case the underlying filesystem is slow.
QThread::msleep(100);
-#endif
QDir().mkdir(targetDirectory);
}
// restore the saved settings
diff --git a/src/src/installationmanager.h b/src/src/installationmanager.h index 462baa6..26a9bca 100644 --- a/src/src/installationmanager.h +++ b/src/src/installationmanager.h @@ -29,10 +29,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QObject>
#include <QProgressDialog>
-#ifdef _WIN32
-#define WIN32_LEAN_AND_MEAN
-#include <Windows.h>
-#endif
+
#include <map>
#include <set>
diff --git a/src/src/instancemanager.cpp b/src/src/instancemanager.cpp index 7821761..4b6d4f2 100644 --- a/src/src/instancemanager.cpp +++ b/src/src/instancemanager.cpp @@ -50,31 +50,27 @@ Instance::Instance(QString dir, bool portable, QString profileName) : m_dir(std::move(dir)), m_portable(portable), m_plugin(nullptr),
m_profile(std::move(profileName))
{
-#ifndef _WIN32
// Ensure portable instances have a ModOrganizer.sh launcher script.
if (m_portable && !m_dir.isEmpty()) {
const QString launchPath = QDir(m_dir).filePath("ModOrganizer.sh");
if (!QFileInfo::exists(launchPath)) {
- QFile launchFile(launchPath);
+ QFile launchFile(launchPath);
if (launchFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&launchFile);
out << "#!/bin/bash\n";
out << "# Auto-generated by fluorine-manager\n";
out << "INSTANCE_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n";
- out << "MO2_BIN=\"$(which ModOrganizer 2>/dev/null || echo "
- "ModOrganizer)\"\n";
+ out << "MO2_BIN=\"$(which ModOrganizer 2>/dev/null || echo ModOrganizer)\"\n";
out << "exec \"${MO2_BIN}\" --instance \"${INSTANCE_DIR}\" \"$@\"\n";
launchFile.close();
- QFile::setPermissions(
- launchPath,
- QFileDevice::ReadOwner | QFileDevice::WriteOwner |
- QFileDevice::ExeOwner | QFileDevice::ReadGroup |
- QFileDevice::ExeGroup | QFileDevice::ReadOther |
- QFileDevice::ExeOther);
+ QFile::setPermissions(launchPath,
+ QFileDevice::ReadOwner | QFileDevice::WriteOwner |
+ QFileDevice::ExeOwner | QFileDevice::ReadGroup |
+ QFileDevice::ExeGroup | QFileDevice::ReadOther |
+ QFileDevice::ExeOther);
}
}
}
-#endif
}
QString Instance::displayName() const
@@ -623,14 +619,9 @@ QString InstanceManager::instancePath(const QString& instanceName) const QString InstanceManager::globalInstancesRootPath() const
{
-#ifndef _WIN32
// Use the shared Fluorine data dir so the path is the same in native and
// Flatpak builds (QStandardPaths is remapped inside a Flatpak sandbox).
return QDir::fromNativeSeparators(fluorineDataDir());
-#else
- return QDir::fromNativeSeparators(
- QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
-#endif
}
QString InstanceManager::iniPath(const QString& instanceDir) const
diff --git a/src/src/loglist.cpp b/src/src/loglist.cpp index cf4b6a7..eb6d757 100644 --- a/src/src/loglist.cpp +++ b/src/src/loglist.cpp @@ -239,12 +239,7 @@ void LogList::clear() void LogList::openLogsFolder()
{
-#ifndef _WIN32
const QString logsPath = qApp->property("dataPath").toString() + "/logs";
-#else
- const QString logsPath = qApp->property("dataPath").toString() + "/" +
- QString::fromStdWString(AppConfig::logPath());
-#endif
shell::Explore(logsPath);
}
@@ -404,7 +399,6 @@ bool createAndMakeWritable(const std::wstring& subPath) bool setLogDirectory(const QString& dir)
{
-#ifndef _WIN32
// Place logs in the instance directory so each instance has its own logs.
const QString logDir = dir + "/logs";
QDir().mkpath(logDir);
@@ -419,19 +413,11 @@ bool setLogDirectory(const QString& dir) {
QDir d(logDir);
QStringList existing = d.entryList({"mo_interface_*.log"}, QDir::Files, QDir::Name);
- const int keep = std::max(0, AppConfig::numLogFiles() - 1);
+ const int keep = std::max(0, AppConfig::numLogFiles() - 1);
while (existing.size() > keep) {
d.remove(existing.takeFirst());
}
}
-#else
- const auto logFile = dir + "/" + QString::fromStdWString(AppConfig::logPath()) + "/" +
- QString::fromStdWString(AppConfig::logFileName());
-
- if (!createAndMakeWritable(AppConfig::logPath())) {
- return false;
- }
-#endif
log::getDefault().setFile(MOBase::log::File::single(logFile.toStdWString()));
diff --git a/src/src/main.cpp b/src/src/main.cpp index f418246..29f6ca9 100644 --- a/src/src/main.cpp +++ b/src/src/main.cpp @@ -1,401 +1,323 @@ -#include "commandline.h"
-#include "env.h"
-#include "fluorinepaths.h"
-#include "instancemanager.h"
-#include "loglist.h"
-#include "moapplication.h"
-#include "multiprocess.h"
-#include "nxmhandler_linux.h"
-#include "organizercore.h"
-#include "shared/util.h"
-#include "thread_utils.h"
-#include <log.h>
-#include <report.h>
-#include <QString>
-
-#ifdef _WIN32
-#include <windows.h>
-#else
-#include <atomic>
-#include <csignal>
-#include <cstdlib>
-#include <cstring>
-#include <exception>
-#include <execinfo.h>
-#include <sys/mount.h>
-#include <sys/wait.h>
-#include <unistd.h>
-#endif
-
-using namespace MOBase;
-
-#ifdef _WIN32
-thread_local LPTOP_LEVEL_EXCEPTION_FILTER g_prevExceptionFilter = nullptr;
-thread_local std::terminate_handler g_prevTerminateHandler = nullptr;
-#endif
-
-int run(int argc, char* argv[]);
-
-int main(int argc, char* argv[])
-{
- const int r = run(argc, argv);
- std::cout << "mod organizer done\n";
- return r;
-}
-
-int run(int argc, char* argv[])
-{
-#ifndef _WIN32
- if (argc >= 3 && QString(argv[1]) == "nxm-handle") {
- QString nxmUrl = QString::fromLocal8Bit(argv[2]);
- if (nxmUrl == "nxm-handle" && argc >= 4) {
- nxmUrl = QString::fromLocal8Bit(argv[3]);
- }
- return NxmHandlerLinux::sendToSocket(nxmUrl) ? 0 : 1;
- }
-#endif
-
- MOShared::SetThisThreadName("main");
- setExceptionHandlers();
-
- cl::CommandLine cl;
-#ifdef _WIN32
- if (auto r = cl.process(GetCommandLineW())) {
- return *r;
- }
-#else
- // Build a wstring from argv for the CommandLine parser.
- // Each argument must be quoted so that po::split_unix() round-trips
- // correctly when paths contain spaces.
- std::wstring cmdLine;
- for (int i = 0; i < argc; ++i) {
- if (i > 0) cmdLine += L' ';
- std::string arg(argv[i]);
- std::wstring warg(arg.begin(), arg.end());
- if (warg.find(L' ') != std::wstring::npos) {
- cmdLine += L'"';
- cmdLine += warg;
- cmdLine += L'"';
- } else {
- cmdLine += warg;
- }
- }
- if (auto r = cl.process(cmdLine)) {
- return *r;
- }
-#endif
-
- fluorineMigrateDataDir();
-
- initLogging();
-
- // must be after logging
- TimeThis tt("main() multiprocess");
-
- MOApplication app(argc, argv);
-
- // check if the command line wants to run something right now
- if (auto r = cl.runPostApplication(app)) {
- return *r;
- }
-
- // check if there's another process running
- MOMultiProcess multiProcess(cl.multiple());
-
- if (multiProcess.ephemeral()) {
- // this is not the primary process
-
- if (cl.forwardToPrimary(multiProcess)) {
- // but there's something on the command line that could be forwarded to
- // it, so just exit
- return 0;
- }
-
- QMessageBox::information(
- nullptr, QObject::tr("Mod Organizer"),
- QObject::tr("An instance of Mod Organizer is already running"));
-
- return 1;
- }
-
- // check if the command line wants to run something right now
- if (auto r = cl.runPostMultiProcess(multiProcess)) {
- return *r;
- }
-
- tt.stop();
-
- // stuff that's done only once, even if MO restarts in the loop below
- app.firstTimeSetup(multiProcess);
-
- // force the "Select instance" dialog on startup, only for first loop or when
- // the current instance cannot be used
- bool pick = cl.pick();
-
- // MO runs in a loop because it can be restarted in several ways, such as
- // when switching instances or changing some settings
- for (;;) {
- try {
- auto& m = InstanceManager::singleton();
-
- if (cl.instance()) {
- m.overrideInstance(*cl.instance());
- }
-
- if (cl.profile()) {
- m.overrideProfile(*cl.profile());
- }
-
- // set up plugins, OrganizerCore, etc.
- {
- const auto r = app.setup(multiProcess, pick);
- pick = false;
-
- if (r == RestartExitCode || r == ReselectExitCode) {
- // resets things when MO is "restarted"
- app.resetForRestart();
-
- // don't reprocess command line
- cl.clear();
-
- if (r == ReselectExitCode) {
- pick = true;
- }
-
- continue;
- } else if (r != 0) {
- // something failed, quit
- return r;
- }
- }
-
- // check if the command line wants to run something right now
- if (auto r = cl.runPostOrganizer(app.core())) {
- return *r;
- }
-
-#ifndef _WIN32
- NxmHandlerLinux nxmHandler;
- if (!nxmHandler.startListener()) {
- log::warn("nxm listener could not be started");
- } else {
- QObject::connect(&nxmHandler, &NxmHandlerLinux::nxmReceived, &app.core(),
- [&](const NxmLink& link) {
- app.core().downloadRequestedNXM(
- QString("nxm://%1/mods/%2/files/%3?key=%4&expires=%5&user_id=%6")
- .arg(link.game_domain)
- .arg(link.mod_id)
- .arg(link.file_id)
- .arg(link.key)
- .arg(link.expires)
- .arg(link.user_id));
- });
-
- QObject::connect(&nxmHandler, &NxmHandlerLinux::directDownloadReceived,
- &app.core(), [&](const QString& url, const QString&) {
- QMetaObject::invokeMethod(&app.core(), [&app, url] {
- app.core().downloadManager()->startDownloadURLs(QStringList{url});
- }, Qt::QueuedConnection);
- });
- }
-#endif
-
- // run the main window
- const auto r = app.run(multiProcess);
-
- if (r == RestartExitCode) {
- // resets things when MO is "restarted"
- app.resetForRestart();
-
- // don't reprocess command line
- cl.clear();
-
- continue;
- }
-
- return r;
- } catch (const std::exception& e) {
- reportError(e.what());
- return 1;
- }
- }
-}
-
-#ifdef _WIN32
-LONG WINAPI onUnhandledException(_EXCEPTION_POINTERS* ptrs)
-{
- const auto path = OrganizerCore::getGlobalCoreDumpPath();
- const auto type = OrganizerCore::getGlobalCoreDumpType();
-
- const auto r = env::coredump(path.empty() ? nullptr : path.c_str(), type);
-
- if (r) {
- log::error("ModOrganizer has crashed, core dump created.");
- } else {
- log::error("ModOrganizer has crashed, core dump failed");
- }
-
- // g_prevExceptionFilter somehow sometimes point to this function, making this
- // recurse and create hundreds of core dump, not sure why
- if (g_prevExceptionFilter && ptrs && g_prevExceptionFilter != onUnhandledException)
- return g_prevExceptionFilter(ptrs);
- else
- return EXCEPTION_CONTINUE_SEARCH;
-}
-
-void onTerminate() noexcept
-{
- __try {
- // force an exception to get a valid stack trace for this thread
- *(int*)0 = 42;
- } __except (onUnhandledException(GetExceptionInformation()),
- EXCEPTION_EXECUTE_HANDLER) {
- }
-
- if (g_prevTerminateHandler) {
- g_prevTerminateHandler();
- } else {
- std::abort();
- }
-}
-
-void setExceptionHandlers()
-{
- if (g_prevExceptionFilter) {
- // already called
- return;
- }
-
- g_prevExceptionFilter = SetUnhandledExceptionFilter(onUnhandledException);
- g_prevTerminateHandler = std::set_terminate(onTerminate);
-}
-
-#else // Linux
-
-// Defined in fuseconnector.cpp — returns the current FUSE mount point
-// (or nullptr if nothing is mounted). The backing buffer is a plain
-// char[] so reading it in a signal handler is async-signal-safe.
-extern const char* getFuseMountPointForCrashCleanup();
-
-// Async-signal-safe-ish: try direct umount2(MNT_DETACH) first (single syscall,
-// no fork/malloc), then fall back to fusermount3 -uz. Lazy detach is what
-// actually keeps us out of D-state — a normal unmount blocks indefinitely if
-// the mount is busy, which is exactly when we crash.
-static void emergencyFuseUnmount() noexcept
-{
- const char* mp = getFuseMountPointForCrashCleanup();
- if (mp == nullptr || mp[0] == '\0') {
- return;
- }
-
- // Step 1: direct lazy unmount. Works without fusermount3 setuid helper if
- // we own the mount (we do — it's our session). Single syscall, no heap.
- if (umount2(mp, MNT_DETACH) == 0) {
- return;
- }
-
- // Step 2: fall back to fusermount3 -uz. Don't waitpid() — a hung child
- // would wedge the crash handler. Detach with double-fork so PID 1 reaps it.
- const pid_t child = fork();
- if (child == 0) {
- // Intermediate child: fork again then exit so the grandchild is reparented.
- const pid_t grand = fork();
- if (grand == 0) {
- execlp("fusermount3", "fusermount3", "-uz", mp, nullptr);
- execlp("fusermount", "fusermount", "-uz", mp, nullptr);
- _exit(1);
- }
- _exit(0);
- } else if (child > 0) {
- // Reap the intermediate child only — it exits immediately.
- int status = 0;
- waitpid(child, &status, 0);
- }
-}
-
-static void linuxCrashHandler(int sig)
-{
- // Reset to default immediately to avoid recursion
- signal(sig, SIG_DFL);
-
- // Best-effort FUSE cleanup before we die
- emergencyFuseUnmount();
-
- const char* sigName = (sig == SIGSEGV) ? "SIGSEGV"
- : (sig == SIGABRT) ? "SIGABRT"
- : (sig == SIGFPE) ? "SIGFPE"
- : "UNKNOWN";
-
- fprintf(stderr, "\n=== MO2 CRASH: signal %s (%d) ===\n", sigName, sig);
-
- // Print backtrace
- void* frames[64];
- int count = backtrace(frames, 64);
- fprintf(stderr, "Backtrace (%d frames):\n", count);
- backtrace_symbols_fd(frames, count, STDERR_FILENO);
- fprintf(stderr, "=== END BACKTRACE ===\n");
-
- // Force-terminate the whole process group. raise(sig) only delivers to the
- // current thread and depends on the signal not being masked process-wide;
- // libfuse blocks signals on its workers, so raise() can hang. _exit() always
- // terminates immediately and reaps every thread.
- _exit(128 + sig);
-}
-
-static void linuxTermHandler(int sig)
-{
- // Graceful shutdown: unmount FUSE and exit cleanly.
- signal(sig, SIG_DFL);
- emergencyFuseUnmount();
- _exit(128 + sig);
-}
-
-// std::terminate fires for uncaught C++ exceptions (including bad_alloc thrown
-// out of a noexcept boundary). The default handler aborts via SIGABRT, but if
-// SIGABRT is masked on the throwing thread the process can hang. Run our FUSE
-// cleanup first, then call abort() ourselves.
-static void linuxTerminateHandler() noexcept
-{
- static std::atomic<bool> entered{false};
- if (entered.exchange(true)) {
- // Recursion (terminate during our own cleanup) — die immediately.
- _exit(134);
- }
-
- emergencyFuseUnmount();
-
- fprintf(stderr, "\n=== MO2 std::terminate ===\n");
- void* frames[64];
- int count = backtrace(frames, 64);
- fprintf(stderr, "Backtrace (%d frames):\n", count);
- backtrace_symbols_fd(frames, count, STDERR_FILENO);
- fprintf(stderr, "=== END BACKTRACE ===\n");
-
- std::abort();
-}
-
-void setExceptionHandlers()
-{
- // sigaction with SA_RESETHAND + no SA_RESTART; preferred over signal() since
- // signal() semantics vary. Don't block other fatal signals while handling
- // one — we want a second crash to terminate immediately rather than recurse.
- struct sigaction sa{};
- sa.sa_handler = linuxCrashHandler;
- sa.sa_flags = SA_RESETHAND;
- sigemptyset(&sa.sa_mask);
- sigaction(SIGSEGV, &sa, nullptr);
- sigaction(SIGABRT, &sa, nullptr);
- sigaction(SIGFPE, &sa, nullptr);
- sigaction(SIGBUS, &sa, nullptr);
-
- struct sigaction term{};
- term.sa_handler = linuxTermHandler;
- term.sa_flags = SA_RESETHAND;
- sigemptyset(&term.sa_mask);
- sigaction(SIGTERM, &term, nullptr);
- sigaction(SIGINT, &term, nullptr);
-
- std::set_terminate(linuxTerminateHandler);
-}
-
-#endif
+#include "commandline.h" +#include "env.h" +#include "fluorinepaths.h" +#include "instancemanager.h" +#include "loglist.h" +#include "moapplication.h" +#include "multiprocess.h" +#include "nxmhandler_linux.h" +#include "organizercore.h" +#include "shared/util.h" +#include "thread_utils.h" + +#include <log.h> +#include <report.h> + +#include <QString> + +#include <atomic> +#include <csignal> +#include <cstdlib> +#include <cstring> +#include <exception> +#include <execinfo.h> +#include <sys/mount.h> +#include <sys/wait.h> +#include <unistd.h> + +using namespace MOBase; + +int run(int argc, char* argv[]); + +int main(int argc, char* argv[]) +{ + const int r = run(argc, argv); + std::cout << "mod organizer done\n"; + return r; +} + +int run(int argc, char* argv[]) +{ + if (argc >= 3 && QString(argv[1]) == "nxm-handle") { + QString nxmUrl = QString::fromLocal8Bit(argv[2]); + if (nxmUrl == "nxm-handle" && argc >= 4) { + nxmUrl = QString::fromLocal8Bit(argv[3]); + } + return NxmHandlerLinux::sendToSocket(nxmUrl) ? 0 : 1; + } + + MOShared::SetThisThreadName("main"); + setExceptionHandlers(); + + cl::CommandLine cl; + + // Build a wstring from argv for the CommandLine parser. Each argument must + // be quoted so that po::split_unix() round-trips correctly when paths + // contain spaces. + std::wstring cmdLine; + for (int i = 0; i < argc; ++i) { + if (i > 0) + cmdLine += L' '; + std::string arg(argv[i]); + std::wstring warg(arg.begin(), arg.end()); + if (warg.find(L' ') != std::wstring::npos) { + cmdLine += L'"'; + cmdLine += warg; + cmdLine += L'"'; + } else { + cmdLine += warg; + } + } + if (auto r = cl.process(cmdLine)) { + return *r; + } + + fluorineMigrateDataDir(); + + initLogging(); + + // must be after logging + TimeThis tt("main() multiprocess"); + + MOApplication app(argc, argv); + + if (auto r = cl.runPostApplication(app)) { + return *r; + } + + MOMultiProcess multiProcess(cl.multiple()); + + if (multiProcess.ephemeral()) { + // not the primary process + + if (cl.forwardToPrimary(multiProcess)) { + // there's something on the command line that could be forwarded — exit + return 0; + } + + QMessageBox::information( + nullptr, QObject::tr("Mod Organizer"), + QObject::tr("An instance of Mod Organizer is already running")); + + return 1; + } + + if (auto r = cl.runPostMultiProcess(multiProcess)) { + return *r; + } + + tt.stop(); + + // stuff that's done only once, even if MO restarts in the loop below + app.firstTimeSetup(multiProcess); + + // force the "Select instance" dialog on startup, only for first loop or when + // the current instance cannot be used + bool pick = cl.pick(); + + // MO runs in a loop because it can be restarted in several ways, such as + // when switching instances or changing some settings + for (;;) { + try { + auto& m = InstanceManager::singleton(); + + if (cl.instance()) { + m.overrideInstance(*cl.instance()); + } + + if (cl.profile()) { + m.overrideProfile(*cl.profile()); + } + + // set up plugins, OrganizerCore, etc. + { + const auto r = app.setup(multiProcess, pick); + pick = false; + + if (r == RestartExitCode || r == ReselectExitCode) { + app.resetForRestart(); + cl.clear(); + + if (r == ReselectExitCode) { + pick = true; + } + + continue; + } else if (r != 0) { + return r; + } + } + + if (auto r = cl.runPostOrganizer(app.core())) { + return *r; + } + + NxmHandlerLinux nxmHandler; + if (!nxmHandler.startListener()) { + log::warn("nxm listener could not be started"); + } else { + QObject::connect( + &nxmHandler, &NxmHandlerLinux::nxmReceived, &app.core(), + [&](const NxmLink& link) { + app.core().downloadRequestedNXM( + QString("nxm://%1/mods/%2/files/%3?key=%4&expires=%5&user_id=%6") + .arg(link.game_domain) + .arg(link.mod_id) + .arg(link.file_id) + .arg(link.key) + .arg(link.expires) + .arg(link.user_id)); + }); + + QObject::connect(&nxmHandler, &NxmHandlerLinux::directDownloadReceived, + &app.core(), [&](const QString& url, const QString&) { + QMetaObject::invokeMethod( + &app.core(), + [&app, url] { + app.core().downloadManager()->startDownloadURLs( + QStringList{url}); + }, + Qt::QueuedConnection); + }); + } + + const auto r = app.run(multiProcess); + + if (r == RestartExitCode) { + app.resetForRestart(); + cl.clear(); + continue; + } + + return r; + } catch (const std::exception& e) { + reportError(e.what()); + return 1; + } + } +} + +// Defined in fuseconnector.cpp — returns the current FUSE mount point (or +// nullptr if nothing is mounted). The backing buffer is a plain char[] so +// reading it in a signal handler is async-signal-safe. +extern const char* getFuseMountPointForCrashCleanup(); + +// Async-signal-safe-ish: try direct umount2(MNT_DETACH) first (single +// syscall, no fork/malloc), then fall back to fusermount3 -uz. Lazy detach +// is what actually keeps us out of D-state — a normal unmount blocks +// indefinitely if the mount is busy, which is exactly when we crash. +static void emergencyFuseUnmount() noexcept +{ + const char* mp = getFuseMountPointForCrashCleanup(); + if (mp == nullptr || mp[0] == '\0') { + return; + } + + // Step 1: direct lazy unmount. Works without the fusermount3 setuid helper + // if we own the mount (we do — it's our session). Single syscall, no heap. + if (umount2(mp, MNT_DETACH) == 0) { + return; + } + + // Step 2: fall back to fusermount3 -uz. Don't waitpid() on the grandchild — + // a hung child would wedge the crash handler. Detach with double-fork so + // PID 1 reaps it. + const pid_t child = fork(); + if (child == 0) { + const pid_t grand = fork(); + if (grand == 0) { + execlp("fusermount3", "fusermount3", "-uz", mp, nullptr); + execlp("fusermount", "fusermount", "-uz", mp, nullptr); + _exit(1); + } + _exit(0); + } else if (child > 0) { + int status = 0; + waitpid(child, &status, 0); + } +} + +static void linuxCrashHandler(int sig) +{ + // Reset to default immediately to avoid recursion + signal(sig, SIG_DFL); + + // Best-effort FUSE cleanup before we die + emergencyFuseUnmount(); + + const char* sigName = (sig == SIGSEGV) ? "SIGSEGV" + : (sig == SIGABRT) ? "SIGABRT" + : (sig == SIGFPE) ? "SIGFPE" + : "UNKNOWN"; + + fprintf(stderr, "\n=== MO2 CRASH: signal %s (%d) ===\n", sigName, sig); + + void* frames[64]; + int count = backtrace(frames, 64); + fprintf(stderr, "Backtrace (%d frames):\n", count); + backtrace_symbols_fd(frames, count, STDERR_FILENO); + fprintf(stderr, "=== END BACKTRACE ===\n"); + + // Force-terminate the whole process group. raise(sig) only delivers to the + // current thread and depends on the signal not being masked process-wide; + // libfuse blocks signals on its workers, so raise() can hang. _exit() + // always terminates immediately and reaps every thread. + _exit(128 + sig); +} + +static void linuxTermHandler(int sig) +{ + // Graceful shutdown: unmount FUSE and exit cleanly. + signal(sig, SIG_DFL); + emergencyFuseUnmount(); + _exit(128 + sig); +} + +// std::terminate fires for uncaught C++ exceptions (including bad_alloc +// thrown out of a noexcept boundary). The default handler aborts via +// SIGABRT, but if SIGABRT is masked on the throwing thread the process can +// hang. Run our FUSE cleanup first, then call abort() ourselves. +static void linuxTerminateHandler() noexcept +{ + static std::atomic<bool> entered{false}; + if (entered.exchange(true)) { + // Recursion (terminate during our own cleanup) — die immediately. + _exit(134); + } + + emergencyFuseUnmount(); + + fprintf(stderr, "\n=== MO2 std::terminate ===\n"); + void* frames[64]; + int count = backtrace(frames, 64); + fprintf(stderr, "Backtrace (%d frames):\n", count); + backtrace_symbols_fd(frames, count, STDERR_FILENO); + fprintf(stderr, "=== END BACKTRACE ===\n"); + + std::abort(); +} + +void setExceptionHandlers() +{ + // sigaction with SA_RESETHAND + no SA_RESTART; preferred over signal() + // since signal() semantics vary. Don't block other fatal signals while + // handling one — we want a second crash to terminate immediately rather + // than recurse. + struct sigaction sa{}; + sa.sa_handler = linuxCrashHandler; + sa.sa_flags = SA_RESETHAND; + sigemptyset(&sa.sa_mask); + sigaction(SIGSEGV, &sa, nullptr); + sigaction(SIGABRT, &sa, nullptr); + sigaction(SIGFPE, &sa, nullptr); + sigaction(SIGBUS, &sa, nullptr); + + struct sigaction term{}; + term.sa_handler = linuxTermHandler; + term.sa_flags = SA_RESETHAND; + sigemptyset(&term.sa_mask); + sigaction(SIGTERM, &term, nullptr); + sigaction(SIGINT, &term, nullptr); + + std::set_terminate(linuxTerminateHandler); +} diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index 39a5c09..95540a7 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -173,10 +173,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <boost/thread.hpp>
#endif
-#ifdef _WIN32
-#include <shlobj.h>
-#endif
-
#include <exception>
#include <functional>
#include <limits.h>
@@ -370,14 +366,9 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, this, SLOT(linkToolbar()));
m_LinkDesktop = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Desktop"), this,
SLOT(linkDesktop()));
-#ifdef _WIN32
- m_LinkStartMenu = linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this,
- SLOT(linkMenu()));
-#else
m_LinkStartMenu = linkMenu->addAction(QIcon(":/MO/gui/link"),
tr("Application Launcher"), this,
SLOT(linkMenu()));
-#endif
ui->linkButton->setMenu(linkMenu);
ui->listOptionsBtn->setMenu(
@@ -2676,25 +2667,17 @@ void MainWindow::fileMoved(const QString& filePath, const QString& oldOriginName ToWString(newOriginName));
QString fullNewPath = ToQString(newOrigin.getPath()) + "/" + filePath;
-#ifdef _WIN32
- WIN32_FIND_DATAW findData;
- HANDLE hFind;
- hFind = ::FindFirstFileW(ToWString(fullNewPath).c_str(), &findData);
- filePtr->addOrigin(newOrigin.getID(), findData.ftCreationTime, L"", -1);
- FindClose(hFind);
-#else
- // On Linux, use a default FILETIME for the origin
FILETIME ft = {};
QFileInfo fi(fullNewPath);
if (fi.exists()) {
- // Convert QDateTime to Windows FILETIME (100-ns intervals since 1601)
- qint64 msecs = fi.birthTime().toMSecsSinceEpoch();
- uint64_t ticks = (static_cast<uint64_t>(msecs) * 10000ULL) + 116444736000000000ULL;
- ft.dwLowDateTime = static_cast<DWORD>(ticks & 0xFFFFFFFF);
+ // Win32 FILETIME = 100-ns intervals since 1601-01-01.
+ qint64 msecs = fi.birthTime().toMSecsSinceEpoch();
+ uint64_t ticks = (static_cast<uint64_t>(msecs) * 10000ULL) +
+ 116444736000000000ULL;
+ ft.dwLowDateTime = static_cast<DWORD>(ticks & 0xFFFFFFFF);
ft.dwHighDateTime = static_cast<DWORD>(ticks >> 32);
}
filePtr->addOrigin(newOrigin.getID(), ft, L"", -1);
-#endif
}
if (m_OrganizerCore.directoryStructure()->originExists(
ToWString(oldOriginName))) {
@@ -2710,8 +2693,7 @@ void MainWindow::fileMoved(const QString& filePath, const QString& oldOriginName .arg(e.what()));
}
-#ifndef _WIN32
- // Track files moved from Overwrite to a mod for in-place write-back
+ // Track files moved from Overwrite to a mod for in-place write-back.
ModInfo::Ptr owInfo = ModInfo::getOverwrite();
if (owInfo && oldOriginName == owInfo->name()) {
if (m_OrganizerCore.directoryStructure()->originExists(
@@ -2719,11 +2701,9 @@ void MainWindow::fileMoved(const QString& filePath, const QString& oldOriginName FilesOrigin& newOrigin =
m_OrganizerCore.directoryStructure()->getOriginByName(
ToWString(newOriginName));
- m_OrganizerCore.trackOverwriteMove(
- filePath, ToQString(newOrigin.getPath()));
+ m_OrganizerCore.trackOverwriteMove(filePath, ToQString(newOrigin.getPath()));
}
}
-#endif
} else {
// this is probably not an error, the specified path is likely a directory
}
@@ -2799,21 +2779,16 @@ void MainWindow::openPluginsFolder() void MainWindow::openStylesheetsFolder()
{
-#ifndef _WIN32
- // On Linux, open the instance's stylesheets directory (where custom themes
- // from modlists live), or the user data dir as fallback.
+ // Open the instance's stylesheets directory (where custom themes from
+ // modlists live), or the user data dir as fallback.
QString ssPath;
if (auto ci = InstanceManager::singleton().currentInstance()) {
- ssPath = ci->directory() + "/" +
- QString::fromStdWString(AppConfig::stylesheetsPath());
+ ssPath =
+ ci->directory() + "/" + QString::fromStdWString(AppConfig::stylesheetsPath());
} else {
ssPath = fluorineDataDir() + "/stylesheets";
}
QDir().mkpath(ssPath);
-#else
- QString ssPath = AppConfig::basePath() + "/" +
- ToQString(AppConfig::stylesheetsPath());
-#endif
shell::Explore(ssPath);
}
@@ -2904,11 +2879,7 @@ void MainWindow::linkDesktop() void MainWindow::linkMenu()
{
if (auto* exe = getSelectedExecutable()) {
-#ifdef _WIN32
- env::Shortcut(*exe).toggle(env::Shortcut::StartMenu);
-#else
env::Shortcut(*exe).toggle(env::Shortcut::ApplicationMenu);
-#endif
}
}
@@ -2930,14 +2901,8 @@ void MainWindow::on_linkButton_pressed() : addIcon);
if (m_LinkStartMenu) {
-#ifdef _WIN32
- m_LinkStartMenu->setIcon(shortcut.exists(env::Shortcut::StartMenu) ? removeIcon
- : addIcon);
-#else
- m_LinkStartMenu->setIcon(shortcut.exists(env::Shortcut::ApplicationMenu)
- ? removeIcon
- : addIcon);
-#endif
+ m_LinkStartMenu->setIcon(
+ shortcut.exists(env::Shortcut::ApplicationMenu) ? removeIcon : addIcon);
}
}
diff --git a/src/src/mainwindow.h b/src/src/mainwindow.h index b6c515a..161ef5c 100644 --- a/src/src/mainwindow.h +++ b/src/src/mainwindow.h @@ -105,12 +105,6 @@ class QWidget; #include <boost/signals2.hpp>
#endif
-#ifdef _WIN32
-// Sigh - just for HANDLE
-#define WIN32_LEAN_AND_MEAN
-#include <Windows.h>
-#endif
-
#include <functional>
#include <set>
#include <string>
diff --git a/src/src/messagedialog.cpp b/src/src/messagedialog.cpp index 1ff4f9d..d5b82c6 100644 --- a/src/src/messagedialog.cpp +++ b/src/src/messagedialog.cpp @@ -21,9 +21,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "ui_messagedialog.h"
#include <QResizeEvent>
#include <QTimer>
-#ifdef _WIN32
-#include <Windows.h>
-#endif
#include <log.h>
using namespace MOBase;
diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp index 08c5703..7230536 100644 --- a/src/src/moapplication.cpp +++ b/src/src/moapplication.cpp @@ -29,13 +29,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "organizercore.h"
#include "sanitychecks.h"
#include "settings.h"
-#ifndef _WIN32
#include "fluorineconfig.h"
#include "fluorinepaths.h"
#include "fuseconnector.h"
#include "wineprefix.h"
+
#include <cerrno>
-#endif
#include <filesystem>
#include "shared/appconfig.h"
#include "shared/util.h"
@@ -56,15 +55,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <utility.h>
#include <cstdio>
-#ifdef _WIN32
-// see addDllsToPath() below
-#pragma comment(linker, "/manifestDependency:\"" \
- "name='dlls' " \
- "processorArchitecture='x86' " \
- "version='1.0.0.0' " \
- "type='win32' \"")
-#endif
-
using namespace MOBase;
using namespace MOShared;
@@ -239,15 +229,15 @@ MOApplication::MOApplication(int& argc, char** argv) : QApplication(argc, argv) updateStyle(file);
});
- m_defaultStyle = "windowsvista";
-#ifndef _WIN32
+ // Pick a Qt style available on this system. "Fusion" is bundled with Qt and
+ // looks identical across distros, so prefer it; fall back to whatever the
+ // QStyleFactory advertises first.
const auto availableStyles = QStyleFactory::keys();
if (availableStyles.contains("Fusion")) {
m_defaultStyle = "Fusion";
} else if (!availableStyles.isEmpty()) {
m_defaultStyle = availableStyles.first();
}
-#endif
// Start with "None" style setting and only apply custom styles from settings
// later during setup().
setStyleFile("");
@@ -310,11 +300,7 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) std::fprintf(stderr, "[setup-diag] setLogDirectory() ok\n");
std::fflush(stderr);
-#ifdef _WIN32
- log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW()));
-#else
log::debug("command line: '{}'", QCoreApplication::arguments().join(' '));
-#endif
#ifndef GITID
#define GITID "unknown"
@@ -420,7 +406,6 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) // setting up organizer core
m_core->setManagedGame(m_instance->gamePlugin());
-#ifndef _WIN32
// Clean up stale FUSE mounts from a previous crash BEFORE any game
// directory access (profile init, BSA invalidation, etc.).
{
@@ -429,14 +414,14 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) FuseConnector::tryCleanupStaleMount(dataDir);
}
- // Restore any stale INI/save backups left by a previous crash.
- // This ensures the documents directory is clean before we do anything else.
+ // Restore any stale INI/save backups left by a previous crash. This ensures
+ // the documents directory is clean before we do anything else.
{
auto prefixPath = FluorineConfig::prefixPath();
if (!prefixPath || prefixPath->isEmpty()) {
QSettings instanceSettings(m_settings->filename(), QSettings::IniFormat);
for (const auto& key : {"Settings/proton_prefix_path", "Settings/prefix_path",
- "Proton/prefix_path", "fluorine/prefix_path"}) {
+ "Proton/prefix_path", "fluorine/prefix_path"}) {
const QString value = instanceSettings.value(key).toString().trimmed();
if (!value.isEmpty()) {
prefixPath = value;
@@ -452,7 +437,6 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) }
}
}
-#endif
m_core->createDefaultProfile();
m_core->createOverwriteDirectories();
@@ -712,7 +696,6 @@ bool MOApplication::setStyleFile(const QString& styleName) const QString ssSubdir = MOBase::ToQString(AppConfig::stylesheetsPath());
QStringList searchDirs;
searchDirs << applicationDirPath() + "/" + ssSubdir;
-#ifndef _WIN32
if (m_instance) {
// Prefer baseDirectory() (populated after readFromIni), fall back to
// directory() which is always set by the constructor.
@@ -726,7 +709,6 @@ bool MOApplication::setStyleFile(const QString& styleName) const QString userDir = fluorineDataDir() + "/stylesheets";
if (!searchDirs.contains(userDir))
searchDirs << userDir;
-#endif
QString resolved;
for (const auto& dir : searchDirs) {
@@ -759,8 +741,7 @@ bool MOApplication::notify(QObject* receiver, QEvent* event) log::error("uncaught filesystem exception in handler (object {}, eventtype {}): {}",
receiver->objectName(), event->type(), fe.what());
-#ifndef _WIN32
- // ENOTCONN = stale FUSE mount. Attempt recovery so MO2 can continue.
+ // ENOTCONN = stale FUSE mount. Attempt recovery so MO2 can continue.
if (fe.code().value() == ENOTCONN) {
const auto& p1 = fe.path1();
if (!p1.empty()) {
@@ -773,7 +754,6 @@ bool MOApplication::notify(QObject* receiver, QEvent* event) FuseConnector::tryCleanupStaleMount(QString::fromStdString(p2.string()));
}
}
-#endif
reportError(tr("an error occurred: %1").arg(fe.what()));
return false;
@@ -868,13 +848,12 @@ QString extractBaseStyleFromStyleSheet(QFile& stylesheet, const QString& default } // namespace
-#ifndef _WIN32
// Walk a directory and create lowercase symlinks for any file whose name
// contains uppercase letters, if the lowercase name doesn't already exist.
// QSS files authored on Windows often reference asset files with lowercase
// names but the actual files on disk use mixed case — on a case-sensitive
-// filesystem Qt's url() resolver fails. Shimming lowercase symlinks lets
-// the existing paths resolve without touching the original files.
+// filesystem Qt's url() resolver fails. Shimming lowercase symlinks lets the
+// existing paths resolve without touching the original files.
static void createLowercaseStylesheetShims(const QString& dirPath)
{
namespace fs = std::filesystem;
@@ -908,7 +887,6 @@ static void createLowercaseStylesheetShims(const QString& dirPath) }
}
}
-#endif
void MOApplication::updateStyle(const QString& fileName)
{
@@ -918,11 +896,9 @@ void MOApplication::updateStyle(const QString& fileName) } else {
QFile stylesheet(fileName);
if (stylesheet.exists()) {
-#ifndef _WIN32
// Pre-create lowercase shims so url(foo.svg) in the QSS resolves even
// when the on-disk file is Foo.svg.
createLowercaseStylesheetShims(QFileInfo(fileName).absolutePath());
-#endif
setStyle(new ProxyStyle(QStyleFactory::create(
extractBaseStyleFromStyleSheet(stylesheet, m_defaultStyle))));
setStyleSheet(QString("file:///%1").arg(fileName));
diff --git a/src/src/modinfodialog.cpp b/src/src/modinfodialog.cpp index 7ae3492..eac6a0f 100644 --- a/src/src/modinfodialog.cpp +++ b/src/src/modinfodialog.cpp @@ -588,11 +588,7 @@ void ModInfoDialog::feedFiles(std::vector<TabInfo*>& interestedTabs) continue;
}
-#ifdef _WIN32
- const auto filePath = QString::fromStdWString(entry.path().native());
-#else
const auto filePath = QString::fromStdString(entry.path().native());
-#endif
// for each tab
for (auto* tabInfo : interestedTabs) {
diff --git a/src/src/modinfowithconflictinfo.cpp b/src/src/modinfowithconflictinfo.cpp index da2cb13..9ac5fbc 100644 --- a/src/src/modinfowithconflictinfo.cpp +++ b/src/src/modinfowithconflictinfo.cpp @@ -325,9 +325,9 @@ bool ModInfoWithConflictInfo::doIsValid() const }
}
-#ifndef _WIN32
- // If VFS Root Builder is enabled, a mod with only a Root/ folder is still valid
- // (e.g. SKSE, ENB — files get deployed to the game directory at launch).
+ // If VFS Root Builder is enabled, a mod with only a Root/ folder is still
+ // valid (e.g. SKSE, ENB — files get deployed to the game directory at
+ // launch).
if (const auto* s = Settings::maybeInstance()) {
const QSettings instanceIni(s->filename(), QSettings::IniFormat);
if (instanceIni.value("fluorine/vfs_root_builder", true).toBool()) {
@@ -345,7 +345,6 @@ bool ModInfoWithConflictInfo::doIsValid() const }
}
}
-#endif
return mdc == nullptr;
}
diff --git a/src/src/motddialog.cpp b/src/src/motddialog.cpp index b985c65..f4d3835 100644 --- a/src/src/motddialog.cpp +++ b/src/src/motddialog.cpp @@ -22,9 +22,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "organizercore.h"
#include "ui_motddialog.h"
#include "utility.h"
-#ifdef _WIN32
-#include <Shlwapi.h>
-#endif
#include <utility.h>
using namespace MOBase;
diff --git a/src/src/multiprocess.cpp b/src/src/multiprocess.cpp index 67270b8..73dbaab 100644 --- a/src/src/multiprocess.cpp +++ b/src/src/multiprocess.cpp @@ -1,9 +1,8 @@ #include "multiprocess.h"
#include "utility.h"
+
#include <QLocalSocket>
-#ifndef _WIN32
#include <QThread>
-#endif
#include <log.h>
#include <report.h>
@@ -12,51 +11,51 @@ static const int s_Timeout = 5000; using MOBase::reportError;
-MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject* parent) - : QObject(parent), m_Ephemeral(false), m_OwnsSM(false) -{ - m_SharedMem.setKey(s_Key); - - if (m_SharedMem.create(1)) { - m_OwnsSM = true; - } else { - auto error = m_SharedMem.error(); - - if (error == QSharedMemory::AlreadyExists && !allowMultiple) { - // Primary instance likely exists. Try to attach as ephemeral process. - if (m_SharedMem.attach()) { - m_Ephemeral = true; - } else { - // Handle races with stale shared memory state: - // between create() and attach(), the owner may have disappeared. - auto attachError = m_SharedMem.error(); - - if (attachError == QSharedMemory::NotFound || - attachError == QSharedMemory::KeyError || - attachError == QSharedMemory::UnknownError) { - MOBase::log::debug( - "shared memory attach race: {} ({}), retrying as owner", - m_SharedMem.errorString(), static_cast<int>(attachError)); - if (m_SharedMem.create(1)) { - m_OwnsSM = true; - error = QSharedMemory::NoError; - } else { - error = m_SharedMem.error(); - } - } else { - MOBase::log::debug("shared memory attach failed: {} ({})", - m_SharedMem.errorString(), - static_cast<int>(attachError)); - error = attachError; - } - } - } - - if (!m_OwnsSM && !m_Ephemeral && error != QSharedMemory::NoError && - error != QSharedMemory::AlreadyExists) { - throw MOBase::MyException(tr("SHM error: %1").arg(m_SharedMem.errorString())); - } - } +MOMultiProcess::MOMultiProcess(bool allowMultiple, QObject* parent)
+ : QObject(parent), m_Ephemeral(false), m_OwnsSM(false)
+{
+ m_SharedMem.setKey(s_Key);
+
+ if (m_SharedMem.create(1)) {
+ m_OwnsSM = true;
+ } else {
+ auto error = m_SharedMem.error();
+
+ if (error == QSharedMemory::AlreadyExists && !allowMultiple) {
+ // Primary instance likely exists. Try to attach as ephemeral process.
+ if (m_SharedMem.attach()) {
+ m_Ephemeral = true;
+ } else {
+ // Handle races with stale shared memory state:
+ // between create() and attach(), the owner may have disappeared.
+ auto attachError = m_SharedMem.error();
+
+ if (attachError == QSharedMemory::NotFound ||
+ attachError == QSharedMemory::KeyError ||
+ attachError == QSharedMemory::UnknownError) {
+ MOBase::log::debug(
+ "shared memory attach race: {} ({}), retrying as owner",
+ m_SharedMem.errorString(), static_cast<int>(attachError));
+ if (m_SharedMem.create(1)) {
+ m_OwnsSM = true;
+ error = QSharedMemory::NoError;
+ } else {
+ error = m_SharedMem.error();
+ }
+ } else {
+ MOBase::log::debug("shared memory attach failed: {} ({})",
+ m_SharedMem.errorString(),
+ static_cast<int>(attachError));
+ error = attachError;
+ }
+ }
+ }
+
+ if (!m_OwnsSM && !m_Ephemeral && error != QSharedMemory::NoError &&
+ error != QSharedMemory::AlreadyExists) {
+ throw MOBase::MyException(tr("SHM error: %1").arg(m_SharedMem.errorString()));
+ }
+ }
if (m_OwnsSM) {
connect(&m_Server, SIGNAL(newConnection()), this, SLOT(receiveMessage()),
@@ -78,11 +77,7 @@ void MOMultiProcess::sendMessage(const QString& message) bool connected = false;
for (int i = 0; i < 2 && !connected; ++i) {
if (i > 0) {
-#ifdef _WIN32
- Sleep(250);
-#else
QThread::msleep(250);
-#endif
}
// other process may be just starting up
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 96b3264..e4fa162 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -65,13 +65,6 @@ #include <QtDebug>
#include <QtGlobal> // for qUtf8Printable, etc
-#ifdef _WIN32
-#include <Psapi.h>
-#include <Shlobj.h>
-#include <tchar.h> // for _tcsicmp
-#include <tlhelp32.h>
-#endif
-
#include <limits.h>
#include <stddef.h>
#include <string.h> // for memset, wcsrchr
@@ -105,7 +98,6 @@ QStringList toStringList(InputIterator current, InputIterator end) return result;
}
-#ifndef _WIN32
QString resolveWinePrefixPath(const Settings& settings,
const IPluginGame* managedGame)
{
@@ -241,7 +233,6 @@ QString resolveAbsoluteSaveDir(const WinePrefix& prefix, log::debug("resolveAbsoluteSaveDir: fallback -> '{}'", fallback);
return fallback;
}
-#endif
OrganizerCore::OrganizerCore(Settings& settings)
: m_UserInterface(nullptr), m_PluginContainer(nullptr), m_GamePlugin(nullptr),
@@ -669,9 +660,8 @@ void OrganizerCore::createOverwriteDirectories() void OrganizerCore::prepareVFS()
{
-#ifndef _WIN32
// Read the load order and pass it to the FUSE VFS so plugin files get
- // incrementing timestamps matching their position. This prevents LOOT
+ // incrementing timestamps matching their position. This prevents LOOT
// from reporting "ambiguous load order".
{
std::vector<std::string> loadOrder;
@@ -718,7 +708,6 @@ void OrganizerCore::prepareVFS() m_USVFS.setRootBuilderEnabled(vfsRootBuilder, storageDir.toStdString());
}
-#endif
m_USVFS.updateMapping(fileMapping(m_CurrentProfile->name(), QString()));
}
@@ -730,19 +719,15 @@ void OrganizerCore::unmountVFS() void OrganizerCore::trackOverwriteMove(const QString& relativePath,
const QString& modFolderPath)
{
-#ifndef _WIN32
auto tw = m_USVFS.trackedWrites();
if (tw) {
tw->track(relativePath.toStdString(), modFolderPath.toStdString());
}
-#endif
}
void OrganizerCore::discardVFSStagingOnUnmount()
{
-#ifndef _WIN32
m_USVFS.discardStagingOnUnmount();
-#endif
}
void OrganizerCore::updateVFSParams(log::Levels logLevel,
@@ -1019,14 +1004,10 @@ void OrganizerCore::setPersistent(const QString& pluginName, const QString& key, QString OrganizerCore::pluginDataPath()
{
-#ifndef _WIN32
- // On Linux, the plugins/ directory may contain symlinks into a read-only
- // bundled directory (e.g. /app/ in Flatpak). Place plugin data in a
- // separate writable directory so mkdir() never hits a read-only FS.
+ // The plugins/ directory may contain symlinks into a read-only bundled
+ // directory (e.g. /app/ in Flatpak). Place plugin data in a separate
+ // writable directory so mkdir() never hits a read-only filesystem.
return AppConfig::basePath() + "/plugin_data";
-#else
- return AppConfig::pluginsPath() + "/data";
-#endif
}
MOBase::IModInterface* OrganizerCore::installMod(const QString& archivePath,
@@ -2186,8 +2167,7 @@ void OrganizerCore::syncOverwrite() {
ModInfo::Ptr modInfo = ModInfo::getOverwrite();
-#ifndef _WIN32
- // Snapshot overwrite before sync so we can detect what was moved
+ // Snapshot overwrite before sync so we can detect what was moved.
QStringList beforeFiles;
{
QDirIterator it(modInfo->absolutePath(), QDir::Files | QDir::NoDotAndDotDot,
@@ -2197,18 +2177,16 @@ void OrganizerCore::syncOverwrite() beforeFiles << QDir(modInfo->absolutePath()).relativeFilePath(it.filePath());
}
}
-#endif
SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure,
qApp->activeWindow());
if (syncDialog.exec() == QDialog::Accepted) {
syncDialog.apply(QDir::fromNativeSeparators(m_Settings.paths().mods()));
-#ifndef _WIN32
- // Track files that were moved out of overwrite to mods.
- // Files that existed before but are gone now were synced to a mod.
- // Use profile priority order (highest-priority mod wins) so writes
- // go to the correct mod when multiple mods contain the same path.
+ // Track files that were moved out of overwrite to mods. Files that existed
+ // before but are gone now were synced to a mod. Use profile priority order
+ // (highest-priority mod wins) so writes go to the correct mod when
+ // multiple mods contain the same path.
const QString modsDir = QDir::fromNativeSeparators(m_Settings.paths().mods());
// Mod names in ascending priority order (last = highest priority).
@@ -2218,7 +2196,6 @@ void OrganizerCore::syncOverwrite() for (const auto& relPath : beforeFiles) {
const QString owFile = modInfo->absolutePath() + "/" + relPath;
if (!QFile::exists(owFile)) {
- // Find highest-priority mod that has this file.
QString bestModPath;
for (const auto& modName : modsByPriority) {
const QString modPath = modsDir + "/" + modName;
@@ -2232,7 +2209,6 @@ void OrganizerCore::syncOverwrite() }
}
}
-#endif
modInfo->diskContentModified();
refreshDirectoryStructure();
@@ -2353,7 +2329,6 @@ ProcessRunner OrganizerCore::processRunner() return ProcessRunner(*this, m_UserInterface);
}
-#ifndef _WIN32
bool OrganizerCore::checkGameRegistryKey()
{
// Map of game short names to their registry key info.
@@ -2459,7 +2434,6 @@ bool OrganizerCore::checkGameRegistryKey() return true;
}
-#endif
bool OrganizerCore::beforeRun(
const QFileInfo& binary, const QDir& cwd, const QString& arguments,
@@ -2489,14 +2463,11 @@ bool OrganizerCore::beforeRun( return false;
}
-#ifndef _WIN32
// Check the game's registry key in the Wine prefix and fix if needed.
if (!checkGameRegistryKey()) {
return false; // user cancelled
}
-#endif
-#ifndef _WIN32
// VFS Root Builder: read per-instance setting and configure.
{
bool vfsRootBuilder = false;
@@ -2508,20 +2479,13 @@ bool OrganizerCore::beforeRun( QDir(QDir::fromNativeSeparators(basePath())).filePath("rootbuilder");
m_USVFS.setRootBuilderEnabled(vfsRootBuilder, storageDir.toStdString());
}
-#endif
try {
m_USVFS.updateMapping(fileMapping(profileName, customOverwrite));
m_USVFS.updateForcedLibraries(forcedLibraries);
-#ifdef _WIN32
- } catch (const UsvfsConnectorException& e) {
- log::debug("{}", e.what());
- return false;
-#else
} catch (const FuseConnectorException& e) {
log::error("VFS mount failed: {}", e.what());
return false;
-#endif
} catch (const std::exception& e) {
QWidget* w = nullptr;
if (m_UserInterface) {
@@ -2531,15 +2495,14 @@ bool OrganizerCore::beforeRun( return false;
}
-#ifndef _WIN32
- // Deploy plugins.txt and loadorder.txt to Wine prefix before launch
+ // Deploy plugins.txt and loadorder.txt to the Wine prefix before launch.
if (m_CurrentProfile != nullptr) {
const QString prefixPathStr = resolveWinePrefixPath(m_Settings, managedGame());
if (!prefixPathStr.isEmpty()) {
WinePrefix prefix(prefixPathStr);
if (prefix.isValid()) {
- const QString dataDirName = resolveWineDataDirName(managedGame());
+ const QString dataDirName = resolveWineDataDirName(managedGame());
const QString appDataLocal = prefix.appdataLocal();
const QString pluginsTargetDir =
QDir(appDataLocal).filePath(dataDirName);
@@ -2744,7 +2707,6 @@ bool OrganizerCore::beforeRun( log::debug("No Wine prefix configured, skipping plugin deployment");
}
}
-#endif
return true;
}
@@ -2755,10 +2717,9 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) managedGame()->loadOrderMechanism() ==
IPluginGame::LoadOrderMechanism::FileTime;
-#ifndef _WIN32
- // Unmount the FUSE VFS now that the application has exited. unmount()
+ // Unmount the FUSE VFS now that the application has exited. unmount()
// flushes the staging directory (moves new/changed files to overwrite)
- // and tears down the FUSE session. This mirrors Windows behaviour where
+ // and tears down the FUSE session. This mirrors Windows behaviour where
// USVFS is only active while a hooked process is running.
m_USVFS.unmount();
@@ -2877,7 +2838,6 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) }
}
}
-#endif
// FileTime-based games (Skyrim LE, FO3, FNV) derive the load order from
// plugin file mtimes rather than loadorder.txt. Drop the profile copy so
@@ -2903,12 +2863,12 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) ProcessRunner::Results OrganizerCore::waitForAllUSVFSProcesses(UILocker::Reasons reason)
{
-#ifdef _WIN32
- return processRunner().waitForAllUSVFSProcessesWithLock(reason);
-#else
+ // The Win32 path called waitForAllUSVFSProcessesWithLock(), which iterated
+ // every USVFS-hooked process via the job-handle bookkeeping. The Linux
+ // FUSE-based VFS doesn't have a process job so there's nothing to wait
+ // for here — ProcessRunner::run() already handles the per-launch wait.
Q_UNUSED(reason);
return ProcessRunner::Completed;
-#endif
}
std::vector<Mapping> OrganizerCore::fileMapping(const QString& profileName,
diff --git a/src/src/organizercore.h b/src/src/organizercore.h index 2ff7484..e2c9127 100644 --- a/src/src/organizercore.h +++ b/src/src/organizercore.h @@ -315,9 +315,7 @@ public: QString* saveBindMountSource = nullptr,
QString* saveBindMountTarget = nullptr);
-#ifndef _WIN32
bool checkGameRegistryKey();
-#endif
void afterRun(const QFileInfo& binary, DWORD exitCode);
diff --git a/src/src/organizerproxy.cpp b/src/src/organizerproxy.cpp index 02e1e20..b1fac87 100644 --- a/src/src/organizerproxy.cpp +++ b/src/src/organizerproxy.cpp @@ -255,14 +255,11 @@ HANDLE OrganizerProxy::startApplication(const QString& exe, const QStringList& a bool OrganizerProxy::waitForApplication(HANDLE handle, bool refresh,
LPDWORD exitCode) const
{
-#ifdef _WIN32
- const auto pid = ::GetProcessId(handle);
-#else
- const DWORD pid = 0; // No GetProcessId on Linux
-#endif
+ // The plugin API hands us an opaque HANDLE — on Linux the underlying value
+ // is a pid_t we packed via reinterpret_cast<intptr_t>(pid).
+ const pid_t pid = static_cast<pid_t>(reinterpret_cast<intptr_t>(handle));
- log::debug("a plugin wants to wait for an application to complete, pid {}{}", pid,
- (pid == 0 ? "unknown (probably already completed)" : ""));
+ log::debug("a plugin wants to wait for an application to complete, pid {}", pid);
auto runner = m_Proxied->processRunner();
@@ -272,13 +269,8 @@ bool OrganizerProxy::waitForApplication(HANDLE handle, bool refresh, waitFlags |= ProcessRunner::TriggerRefresh | ProcessRunner::WaitForRefresh;
}
-#ifdef _WIN32
const auto r = runner.setWaitForCompletion(waitFlags, UILocker::OutputRequired)
- .attachToProcess(handle);
-#else
- const auto r = runner.setWaitForCompletion(waitFlags, UILocker::OutputRequired)
- .attachToProcess(static_cast<pid_t>(reinterpret_cast<intptr_t>(handle)));
-#endif
+ .attachToProcess(pid);
if (exitCode) {
*exitCode = runner.exitCode();
diff --git a/src/src/overwriteinfodialog.cpp b/src/src/overwriteinfodialog.cpp index 0c86ccd..36735ac 100644 --- a/src/src/overwriteinfodialog.cpp +++ b/src/src/overwriteinfodialog.cpp @@ -24,9 +24,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QMenu>
#include <QMessageBox>
#include <QShortcut>
-#ifdef _WIN32
-#include <Shlwapi.h>
-#endif
using namespace MOBase;
diff --git a/src/src/pch.h b/src/src/pch.h index f0a1776..f4849b7 100644 --- a/src/src/pch.h +++ b/src/src/pch.h @@ -31,24 +31,6 @@ #include <vector>
#include <wchar.h>
-#ifdef _WIN32
-// keep this header separated to avoid ordering issue since this must be
-// included before DbgHelp.h
-#include <Windows.h>
-
-// windows
-#include <DbgHelp.h>
-#include <LMCons.h>
-#include <Psapi.h>
-#include <Shellapi.h>
-#include <Shlwapi.h>
-#include <eh.h>
-#include <shlobj.h>
-#include <tchar.h>
-#include <wincred.h>
-#include <windowsx.h>
-#include <tlhelp32.h>
-#else
// linux
#include <unistd.h>
#include <sys/stat.h>
@@ -59,7 +41,6 @@ #include <signal.h>
#include <pwd.h>
#include "shared/windows_compat.h"
-#endif
// boost
#include <boost/algorithm/string.hpp>
diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp index d53d8bf..a5189cc 100644 --- a/src/src/plugincontainer.cpp +++ b/src/src/plugincontainer.cpp @@ -1224,7 +1224,6 @@ void PluginContainer::loadPlugins() m_BundledPluginPath = AppConfig::pluginsPath();
-#ifndef _WIN32
if (m_Organizer) {
QString instancePluginPath =
QDir(QDir::fromNativeSeparators(m_Organizer->basePath())).filePath("plugins");
@@ -1234,18 +1233,17 @@ void PluginContainer::loadPlugins() log::debug("instance plugin directory: {}",
QDir::toNativeSeparators(m_PluginPath));
- // Migration: remove stale symlinks left by the old ensureBundledPluginsLinked()
- // approach. Only symlinks are removed; real user files are left untouched.
- {
- QDirIterator cleanIter(instancePluginPath,
- QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
- while (cleanIter.hasNext()) {
- cleanIter.next();
- if (QFileInfo(cleanIter.filePath()).isSymLink()) {
- log::debug("removing stale plugin symlink '{}'",
- QDir::toNativeSeparators(cleanIter.filePath()));
- QFile::remove(cleanIter.filePath());
- }
+ // Migration: remove stale symlinks left by the old
+ // ensureBundledPluginsLinked() approach. Only symlinks are removed; real
+ // user files are left untouched.
+ QDirIterator cleanIter(instancePluginPath,
+ QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
+ while (cleanIter.hasNext()) {
+ cleanIter.next();
+ if (QFileInfo(cleanIter.filePath()).isSymLink()) {
+ log::debug("removing stale plugin symlink '{}'",
+ QDir::toNativeSeparators(cleanIter.filePath()));
+ QFile::remove(cleanIter.filePath());
}
}
} else {
@@ -1254,9 +1252,6 @@ void PluginContainer::loadPlugins() } else {
m_PluginPath = m_BundledPluginPath;
}
-#else
- m_PluginPath = m_BundledPluginPath;
-#endif
log::debug("bundled plugins: {}", QDir::toNativeSeparators(m_BundledPluginPath));
log::debug("looking for plugins in {}", QDir::toNativeSeparators(m_PluginPath));
diff --git a/src/src/pluginlist.cpp b/src/src/pluginlist.cpp index 72abbc4..49fa323 100644 --- a/src/src/pluginlist.cpp +++ b/src/src/pluginlist.cpp @@ -55,10 +55,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "shared/windows_error.h"
#include "viewmarkingscrollbar.h"
-#ifndef _WIN32
#include <sys/stat.h>
#include <fcntl.h>
-#endif
using namespace MOBase;
using namespace MOShared;
@@ -780,66 +778,31 @@ bool PluginList::saveLoadOrder(DirectoryEntry& directoryStructure) directoryStructure.getOriginByID(originid).getPath()))
.filePath(esp.name);
-#ifdef _WIN32
- HANDLE file =
- ::CreateFile(ToWString(fileName).c_str(), GENERIC_READ | GENERIC_WRITE, 0,
- nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
- if (file == INVALID_HANDLE_VALUE) {
- if (::GetLastError() == ERROR_SHARING_VIOLATION) {
- // file is locked, probably the game is running
- return false;
- } else {
- throw windows_error(
- QObject::tr("failed to access %1").arg(fileName).toUtf8().constData());
- }
- }
-
- ULONGLONG temp = 0;
- temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
+ // Use utimensat to set the file modification time. The values track the
+ // Windows-epoch FILETIME so they round-trip with the in-memory state.
+ ULONGLONG temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
FILETIME newWriteTime;
-
newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF);
newWriteTime.dwHighDateTime = (DWORD)(temp >> 32);
esp.time = newWriteTime;
fileEntry->setFileTime(newWriteTime);
- if (!::SetFileTime(file, nullptr, nullptr, &newWriteTime)) {
- throw windows_error(QObject::tr("failed to set file time %1")
- .arg(fileName)
- .toUtf8()
- .constData());
- }
-
- CloseHandle(file);
-#else
- // On Linux, use utimensat to set file modification time
- ULONGLONG temp = 0;
- temp = (145731ULL + esp.priority) * 24 * 60 * 60 * 10000000ULL;
- FILETIME newWriteTime;
- newWriteTime.dwLowDateTime = (DWORD)(temp & 0xFFFFFFFF);
- newWriteTime.dwHighDateTime = (DWORD)(temp >> 32);
- esp.time = newWriteTime;
- fileEntry->setFileTime(newWriteTime);
-
- // Convert FILETIME to timespec and set the file modification time
- {
- uint64_t ticks = (static_cast<uint64_t>(newWriteTime.dwHighDateTime) << 32) |
- newWriteTime.dwLowDateTime;
- // Convert from Windows epoch (1601) to Unix epoch (1970)
- ticks -= 116444736000000000ULL;
- time_t secs = static_cast<time_t>(ticks / 10000000ULL);
- struct timespec times[2];
- times[0].tv_sec = 0;
- times[0].tv_nsec = UTIME_OMIT;
- times[1].tv_sec = secs;
- times[1].tv_nsec = 0;
- std::string path = fileName.toStdString();
- if (utimensat(AT_FDCWD, path.c_str(), times, 0) != 0) {
- log::warn("failed to set file time for {}", fileName);
- }
+ uint64_t ticks =
+ (static_cast<uint64_t>(newWriteTime.dwHighDateTime) << 32) |
+ newWriteTime.dwLowDateTime;
+ // Convert from Windows epoch (1601) to Unix epoch (1970).
+ ticks -= 116444736000000000ULL;
+ time_t secs = static_cast<time_t>(ticks / 10000000ULL);
+ struct timespec times[2];
+ times[0].tv_sec = 0;
+ times[0].tv_nsec = UTIME_OMIT;
+ times[1].tv_sec = secs;
+ times[1].tv_nsec = 0;
+ std::string path = fileName.toStdString();
+ if (utimensat(AT_FDCWD, path.c_str(), times, 0) != 0) {
+ log::warn("failed to set file time for {}", fileName);
}
-#endif
}
}
return true;
@@ -1950,7 +1913,9 @@ PluginList::ESPInfo::ESPInfo(const QString& name, bool forceLoaded, bool forceEn isMasterOfSelectedPlugin(false)
{
QString parsePath = fullPath;
-#ifndef _WIN32
+ // Linux is case-sensitive while Windows-authored paths sometimes mismatch
+ // the actual filename casing. Resolve to the on-disk casing so the plugin
+ // parser doesn't fail when only the case differs.
if (!QFileInfo::exists(parsePath)) {
const QFileInfo fi(parsePath);
const QDir dir(fi.path());
@@ -1963,13 +1928,13 @@ PluginList::ESPInfo::ESPInfo(const QString& name, bool forceLoaded, bool forceEn Qt::CaseInsensitive) == 0;
});
if (it != candidates.end()) {
- parsePath = dir.filePath(*it);
+ parsePath = dir.filePath(*it);
this->fullPath = parsePath;
- log::warn("plugin path case mismatch, resolved '{}' -> '{}'", fullPath, parsePath);
+ log::warn("plugin path case mismatch, resolved '{}' -> '{}'", fullPath,
+ parsePath);
}
}
}
-#endif
try {
// Linux filesystem is UTF-8. ToWString → wstring → naive wchar_t->char
diff --git a/src/src/problemsdialog.cpp b/src/src/problemsdialog.cpp index 8a1791e..b4ce7fa 100644 --- a/src/src/problemsdialog.cpp +++ b/src/src/problemsdialog.cpp @@ -2,9 +2,6 @@ #include "organizercore.h"
#include "ui_problemsdialog.h"
#include <QPushButton>
-#ifdef _WIN32
-#include <Shellapi.h>
-#endif
#include <iplugin.h>
#include <iplugindiagnose.h>
#include <utility.h>
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index 5a47d95..99e1742 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -1,1795 +1,1333 @@ -#include "processrunner.h"
-#include "env.h"
-#include "envmodule.h"
-#include "instancemanager.h"
-#include "iuserinterface.h"
-#include "organizercore.h"
-#include <iplugingame.h>
-#include <log.h>
-#include <report.h>
-#include <uibase/utility.h>
-#ifndef _WIN32
-#include <QCoreApplication>
-#include <QEventLoop>
-#include <QFile>
-#include <QFileInfo>
-#include <QMetaObject>
-#include <QPointer>
-#include <QProcess>
-#include <QThread>
-#include <cerrno>
-#include <deque>
-#include <dirent.h>
-#include <fstream>
-#include <signal.h>
-#include <sys/stat.h>
-#include <sys/wait.h>
-#include <unordered_map>
-#include <unordered_set>
-
-#endif
-
-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();
- QString trailedModsPath = modsPath;
- if (!trailedModsPath.endsWith('/')) {
- trailedModsPath = trailedModsPath + '/';
- }
- bool virtualizedCwd =
- cwdPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
- QString binPath = sp.binary.absoluteFilePath();
- bool virtualizedBin =
- binPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
- if (virtualizedCwd || virtualizedBin) {
- if (virtualizedCwd) {
- int cwdOffset = cwdPath.indexOf('/', trailedModsPath.length());
- QString adjustedCwd = cwdPath.mid(cwdOffset, -1);
- cwdPath = game->dataDirectory().absolutePath();
- if (cwdOffset >= 0)
- cwdPath += adjustedCwd;
- }
-
- if (virtualizedBin) {
- int binOffset = binPath.indexOf('/', trailedModsPath.length());
- QString adjustedBin = binPath.mid(binOffset, -1);
- binPath = game->dataDirectory().absolutePath();
- if (binOffset >= 0)
- binPath += adjustedBin;
- }
-
-#ifdef _WIN32
- // On Windows, launch through MO2 helper to set up USVFS.
- 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());
-#else
- // On Linux, FUSE is already mounted — resolve paths directly without
- // launching through MO2-core (which would fail in the Proton prefix).
- //
- // Root Builder deploys Root/ contents to the game directory root,
- // stripping the "Root/" prefix. Fix paths that were remapped to
- // <dataDir>/Root/... so they point to <gameDir>/... instead.
- // Also handle direct mods/.../Root/ paths (not just dataDir/Root/).
- const QString gameDir = game->gameDirectory().absolutePath();
- const QString dataDir = game->dataDirectory().absolutePath();
-
- // Normalize any path that was remapped to dataDir/Root/... → gameDir/...
- // Handles both dataDir/Root/file and dataDir/Root (exact, no trailing content).
- auto normalizeRootPath = [&](QString& path) {
- const QString rootWithSlash = dataDir + QStringLiteral("/Root/");
- const QString rootExact = dataDir + QStringLiteral("/Root");
- if (path.startsWith(rootWithSlash, Qt::CaseInsensitive)) {
- const QString after = path.mid(rootWithSlash.length());
- path = after.isEmpty() ? gameDir
- : gameDir + QStringLiteral("/") + after;
- return true;
- }
- if (path.compare(rootExact, Qt::CaseInsensitive) == 0) {
- path = gameDir;
- return true;
- }
- return false;
- };
-
- bool binNormalized = normalizeRootPath(binPath);
- bool cwdNormalized = normalizeRootPath(cwdPath);
-
- if (binNormalized) {
- log::info("Root Builder: rewrote binary -> '{}'", binPath);
- }
- if (cwdNormalized) {
- log::info("Root Builder: rewrote start-in -> '{}'", cwdPath);
- }
-
- // If neither was caught by the dataDir/Root/ check, the path might
- // still be the original mods/.../Root/ path (not yet remapped).
- // This happens when the first remapping (lines 61-66) produced
- // something that didn't match the dataDir/Root pattern.
- if (!binNormalized && binPath.startsWith(trailedModsPath, Qt::CaseInsensitive)) {
- int rootIdx = binPath.indexOf("/Root/", trailedModsPath.length(),
- Qt::CaseInsensitive);
- if (rootIdx < 0)
- rootIdx = binPath.indexOf("/Root", trailedModsPath.length(),
- Qt::CaseInsensitive);
- if (rootIdx >= 0) {
- int afterRootStart = rootIdx + 5; // skip "/Root"
- if (afterRootStart < binPath.length() && binPath[afterRootStart] == '/')
- ++afterRootStart; // skip trailing slash
- const QString afterRoot = binPath.mid(afterRootStart);
- const QString modRoot = binPath.left(rootIdx + 5); // up to "/Root"
-
- binPath = afterRoot.isEmpty() ? gameDir
- : gameDir + QStringLiteral("/") + afterRoot;
- log::info("Root Builder: rewrote binary (mod path) -> '{}'", binPath);
-
- // Normalize start-in if it's under the same mod's Root/
- if (!cwdNormalized &&
- cwdPath.startsWith(modRoot, Qt::CaseInsensitive)) {
- int cwdAfterStart = modRoot.length();
- if (cwdAfterStart < cwdPath.length() && cwdPath[cwdAfterStart] == '/')
- ++cwdAfterStart;
- const QString cwdAfter = cwdPath.mid(cwdAfterStart);
- cwdPath = cwdAfter.isEmpty() ? gameDir
- : gameDir + QStringLiteral("/") + cwdAfter;
- log::info("Root Builder: rewrote start-in (mod path) -> '{}'", cwdPath);
- }
- }
- }
-
- sp.binary = QFileInfo(binPath);
- sp.currentDirectory.setPath(cwdPath);
-#endif
- }
-}
-
-#ifdef _WIN32
-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;
- }
- }
-}
-#else
-std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) {
- Q_UNUSED(handle);
- Q_UNUSED(pid);
- return ProcessRunner::Completed;
-}
-#endif
-
-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";
- }
-}
-
-struct InterestingProcess {
- env::Process p;
- Interest interest = Interest::None;
- env::HandlePtr handle;
-};
-
-InterestingProcess findRandomProcess(const env::Process &root) {
- for (auto &&c : root.children()) {
- env::HandlePtr h = c.openHandleForWait();
- if (h) {
- return {c, Interest::Weak, std::move(h)};
- }
-
- auto r = findRandomProcess(c);
- if (r.handle) {
- return r;
- }
- }
-
- return {};
-}
-
-// returns a process that's in the hidden list, or the top-level process if
-// they're all hidden; returns an invalid process if the list is empty
-//
-InterestingProcess findInterestingProcessInTrees(const env::Process &root) {
- // Certain process names we wish to "hide" for aesthetic reason:
- static const std::vector<QString> hiddenList = {
- QFileInfo(QCoreApplication::applicationFilePath()).fileName(),
- "conhost.exe"};
-
- if (root.children().empty()) {
- return {};
- }
-
- auto isHidden = [&](auto &&p) {
- for (auto &h : hiddenList) {
- if (p.name().contains(h, Qt::CaseInsensitive)) {
- return true;
- }
- }
-
- return false;
- };
-
- for (auto &&p : root.children()) {
- if (!isHidden(p)) {
- env::HandlePtr h = p.openHandleForWait();
- if (h) {
- return {p, Interest::Strong, std::move(h)};
- }
- }
-
- auto r = findInterestingProcessInTrees(p);
- if (r.interest == Interest::Strong) {
- return r;
- }
- }
-
- // everything is hidden, just pick the first one that can be used
- return findRandomProcess(root);
-}
-
-void dump(const env::Process &p, int indent) {
- log::debug("{}{}, pid={}, ppid={}", std::string(indent * 4, ' '), p.name(),
- p.pid(), p.ppid());
-
- for (auto &&c : p.children()) {
- dump(c, indent + 1);
- }
-}
-
-void dump(const env::Process &root) {
- log::debug("process tree:");
-
- for (auto &&p : root.children()) {
- dump(p, 1);
- }
-}
-
-#ifdef _WIN32
-// gets the most interesting process in the list
-//
-InterestingProcess getInterestingProcess(HANDLE job) {
- env::Process root = env::getProcessTree(job);
- if (root.children().empty()) {
- log::debug("nothing to wait for");
- return {};
- }
-
- dump(root);
-
- auto interest = findInterestingProcessInTrees(root);
- if (!interest.handle) {
- // this can happen if none of the processes can be opened
- log::debug("no interesting process to wait for");
- return {};
- }
-
- return interest;
-}
-#endif
-
-#ifdef _WIN32
-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,
- std::atomic<bool> &interrupt) {
- using namespace std::chrono;
-
- high_resolution_clock::time_point start;
- if (wait != Infinite) {
- start = high_resolution_clock::now();
- }
-
- while (!interrupt) {
- // 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; the session can be null when running shortcuts
- // with locking disabled, in which case the user cannot force unlock
- if (ls) {
- 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 {};
- }
- }
- }
-
- log::debug("waiting for {} interrupted", pid);
- return ProcessRunner::ForceUnlocked;
-}
-
-ProcessRunner::Results
-waitForProcessesThreadImpl(HANDLE job, UILocker::Session *ls,
- std::atomic<bool> &interrupt) {
- using namespace std::chrono;
-
- 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
- const milliseconds defaultWait(50);
- auto wait = defaultWait;
-
- while (!interrupt) {
- auto ip = getInterestingProcess(job);
- if (!ip.handle) {
- // nothing to wait on
- return ProcessRunner::Completed;
- }
-
- // update the lock widget; the session can be null when running shortcuts
- // with locking disabled
- if (ls) {
- ls->setInfo(ip.p.pid(), ip.p.name());
- }
-
- if (ip.p.pid() != currentPID) {
- // log any change in the process being waited for
- currentPID = ip.p.pid();
-
- log::debug("waiting for completion on {} ({}), {} interest", ip.p.name(),
- ip.p.pid(), toString(ip.interest));
- }
-
- if (ip.interest == Interest::Strong) {
- // don't bother with short wait, this is a good process to wait for
- wait = Infinite;
- }
-
- const auto r = timedWait(ip.handle.get(), ip.p.pid(), ls, wait, interrupt);
- if (r) {
- if (*r == ProcessRunner::Results::Completed) {
- // process completed, check another one, reset the wait time to find
- // interesting processes
- wait = defaultWait;
- } else if (*r != ProcessRunner::Results::Running) {
- // something's wrong, or the user unlocked the ui
- return *r;
- }
- }
-
- // exponentially increase the wait time between checks for interesting
- // processes
- wait = std::min(wait * 2, milliseconds(2000));
- }
-
- log::debug("waiting for processes interrupted");
- return ProcessRunner::ForceUnlocked;
-}
-
-void waitForProcessesThread(ProcessRunner::Results &result, HANDLE job,
- UILocker::Session *ls,
- std::atomic<bool> &interrupt) {
- result = waitForProcessesThreadImpl(job, ls, interrupt);
-
- // the session can be null when running shortcuts with locking disabled
- if (ls) {
- ls->unlock();
- }
-}
-
-ProcessRunner::Results
-waitForProcesses(const std::vector<HANDLE> &initialProcesses,
- UILocker::Session *ls) {
- if (initialProcesses.empty()) {
- // nothing to wait for
- return ProcessRunner::Completed;
- }
-
- // using a job so any child process started by any of those processes can also
- // be captured and monitored
- env::HandlePtr job(CreateJobObjectW(nullptr, nullptr));
- if (!job) {
- const auto e = GetLastError();
-
- log::error("failed to create job to wait for processes, {}",
- formatSystemMessage(e));
-
- return ProcessRunner::Error;
- }
-
- bool oneWorked = false;
-
- for (auto &&h : initialProcesses) {
- if (::AssignProcessToJobObject(job.get(), h)) {
- oneWorked = true;
- } else {
- const auto e = GetLastError();
-
- // this happens when closing MO while multiple processes are running,
- // so the logging is disabled until it gets fixed
-
- // log::error(
- // "can't assign process to job to wait for processes, {}",
- // formatSystemMessage(e));
-
- // keep going
- }
- }
-
- HANDLE monitor = INVALID_HANDLE_VALUE;
-
- if (oneWorked) {
- monitor = job.get();
- } else {
- // none of the handles could be added to the job, just monitor the first one
- monitor = initialProcesses[0];
- }
-
- auto results = ProcessRunner::Running;
- std::atomic<bool> interrupt(false);
-
- auto *t = QThread::create(waitForProcessesThread, std::ref(results), monitor,
- ls, std::ref(interrupt));
-
- QEventLoop events;
- QObject::connect(t, &QThread::finished, [&] { events.quit(); });
-
- t->start();
- events.exec();
-
- if (t->isRunning()) {
- interrupt = true;
- t->wait();
- }
-
- 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;
-}
-
-#else // !_WIN32
-
-pid_t handleToPid(HANDLE h) {
- return static_cast<pid_t>(reinterpret_cast<intptr_t>(h));
-}
-
-QString readProcComm(pid_t pid) {
- QFile f(QString("/proc/%1/comm").arg(pid));
- if (!f.open(QIODevice::ReadOnly)) {
- return {};
- }
-
- return QString::fromUtf8(f.readAll()).trimmed();
-}
-
-// Read /proc/<pid>/cmdline (NUL-separated) and return all argv entries.
-QStringList readProcCmdline(pid_t pid) {
- QFile f(QString("/proc/%1/cmdline").arg(pid));
- if (!f.open(QIODevice::ReadOnly)) {
- return {};
- }
-
- const QByteArray data = f.readAll();
- QStringList parts;
- for (const QByteArray &part : data.split('\0')) {
- if (!part.isEmpty()) {
- parts.push_back(QString::fromUtf8(part));
- }
- }
- return parts;
-}
-
-// Read a specific environment variable from /proc/<pid>/environ.
-// The environ file is NUL-separated KEY=VALUE pairs.
-QString readProcEnvVar(pid_t pid, const char *varName) {
- QFile f(QString("/proc/%1/environ").arg(pid));
- if (!f.open(QIODevice::ReadOnly)) {
- return {};
- }
-
- const QByteArray data = f.readAll();
- const QByteArray prefix = QByteArray(varName) + '=';
- for (const QByteArray &entry : data.split('\0')) {
- if (entry.startsWith(prefix)) {
- return QString::fromUtf8(entry.mid(prefix.size()));
- }
- }
- return {};
-}
-
-// Find a wineserver process owned by the current user that belongs to the
-// given WINEPREFIX. When expectedPrefix is empty, returns the first
-// wineserver owned by us (legacy behaviour).
-//
-// Wineserver stays alive as long as any Wine process in the prefix is
-// running, making it the most reliable way to detect when a game has truly
-// exited — even when launcher .exe's (nvse_loader, skse_loader, etc.) exit
-// before the actual game.
-pid_t findWineserver(const QString &expectedPrefix = {}) {
- const uid_t myUid = ::getuid();
- DIR *proc = opendir("/proc");
- if (!proc) return 0;
-
- pid_t result = 0;
- struct dirent *entry = nullptr;
- while ((entry = readdir(proc)) != nullptr) {
- if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) continue;
- const char *name = entry->d_name;
- if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name))) continue;
-
- const pid_t pid = static_cast<pid_t>(std::strtol(name, nullptr, 10));
- const QString comm = readProcComm(pid);
- if (comm != "wineserver") continue;
-
- // Verify it's owned by us.
- struct stat st;
- if (::stat(QString("/proc/%1").arg(pid).toStdString().c_str(), &st) != 0 ||
- st.st_uid != myUid) {
- continue;
- }
-
- // If a prefix filter was given, verify this wineserver belongs to it.
- // A wineserver without WINEPREFIX in its environ is using the default
- // ~/.wine prefix, which is never the game prefix — skip it too.
- if (!expectedPrefix.isEmpty()) {
- const QString wsPrefix = readProcEnvVar(pid, "WINEPREFIX");
- if (wsPrefix.isEmpty() ||
- QDir(wsPrefix).canonicalPath() != QDir(expectedPrefix).canonicalPath()) {
- log::debug("skipping wineserver {} (prefix '{}' != expected '{}')",
- pid, wsPrefix.toStdString(), expectedPrefix.toStdString());
- continue;
- }
- }
-
- result = pid;
- break;
- }
- closedir(proc);
- return result;
-}
-
-// Check whether any of the expected executable names appear in a process's
-// comm or cmdline. Wine processes often show "wine64-preload" or "start.exe"
-// in /proc/comm while the actual game executable only appears in cmdline.
-// Also handles the 15-char TASK_COMM_LEN truncation in /proc/comm.
-bool processMatchesExpected(pid_t pid, const QStringList &expected,
- QString *matchedNameOut) {
- // 1. Check /proc/comm (fast path).
- const QString comm = readProcComm(pid);
- if (!comm.isEmpty()) {
- const QString lower = comm.toLower();
- for (const QString &exp : expected) {
- if (lower == exp) {
- if (matchedNameOut) *matchedNameOut = comm;
- return true;
- }
- // Handle TASK_COMM_LEN truncation (15 chars): if the expected name
- // is longer than 15 chars, check if comm matches its first 15 chars.
- if (exp.size() > 15 && lower == exp.left(15)) {
- if (matchedNameOut) *matchedNameOut = exp;
- return true;
- }
- }
- }
-
- // 2. Check /proc/cmdline — Wine/Proton processes carry the .exe name here
- // even when comm shows wine64-preloader or start.exe.
- const QStringList cmdline = readProcCmdline(pid);
- for (const QString &arg : cmdline) {
- // Extract just the filename from paths like
- // "c:\windows\system32\start.exe" or "/home/.../FalloutNV.exe"
- const QString normalized = QString(arg).replace('\\', '/');
- const QString base = QFileInfo(normalized).fileName().toLower();
- if (expected.contains(base)) {
- if (matchedNameOut) *matchedNameOut = QFileInfo(normalized).fileName();
- return true;
- }
- }
-
- return false;
-}
-
-std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap() {
- std::unordered_map<pid_t, std::vector<pid_t>> children;
- DIR *proc = opendir("/proc");
- if (!proc) {
- return children;
- }
-
- struct dirent *entry = nullptr;
- while ((entry = readdir(proc)) != nullptr) {
- if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) {
- continue;
- }
-
- const char *name = entry->d_name;
- if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name))) {
- continue;
- }
-
- const pid_t pid = static_cast<pid_t>(std::strtol(name, nullptr, 10));
- std::ifstream status(QString("/proc/%1/status").arg(pid).toStdString());
- if (!status.is_open()) {
- continue;
- }
-
- std::string line;
- pid_t ppid = 0;
- while (std::getline(status, line)) {
- if (line.rfind("PPid:", 0) == 0) {
- ppid = static_cast<pid_t>(std::strtol(line.c_str() + 5, nullptr, 10));
- break;
- }
- }
-
- if (ppid > 0) {
- children[ppid].push_back(pid);
- }
- }
-
- closedir(proc);
- return children;
-}
-
-std::unordered_set<pid_t> collectDescendants(
- pid_t root, const std::unordered_map<pid_t, std::vector<pid_t>> &children) {
- std::unordered_set<pid_t> out;
- std::deque<pid_t> q;
- q.push_back(root);
-
- while (!q.empty()) {
- const pid_t cur = q.front();
- q.pop_front();
-
- const auto it = children.find(cur);
- if (it == children.end()) {
- continue;
- }
-
- for (pid_t child : it->second) {
- if (out.insert(child).second) {
- q.push_back(child);
- }
- }
- }
-
- return out;
-}
-
-QStringList buildExpectedExecutables(const QFileInfo &binary,
- const QString &arguments) {
- QStringList expected;
- auto addName = [&](QString name) {
- name = name.trimmed().toLower();
- if (!name.isEmpty() && !expected.contains(name)) {
- expected.push_back(name);
- }
- };
-
- addName(binary.fileName());
-
- const auto args = QProcess::splitCommand(arguments);
- for (const QString &arg : args) {
- const QFileInfo fi(arg);
- const QString base = fi.fileName();
- if (base.endsWith(".exe", Qt::CaseInsensitive)) {
- addName(base);
- }
- }
-
- log::debug("buildExpectedExecutables: returning [{}]",
- expected.join(", ").toStdString());
- return expected;
-}
-
-pid_t findTrackedProcess(pid_t rootPid, const QStringList &expected,
- QString *trackedNameOut) {
- if (expected.isEmpty()) {
- return 0;
- }
-
- const auto children = buildProcChildrenMap();
- const auto descendants = collectDescendants(rootPid, children);
- if (descendants.empty()) {
- return 0;
- }
-
- pid_t best = 0;
- QString bestName;
- for (pid_t pid : descendants) {
- QString matched;
- if (processMatchesExpected(pid, expected, &matched)) {
- best = pid;
- bestName = matched;
- break;
- }
- }
-
- if (best > 0 && trackedNameOut) {
- *trackedNameOut = bestName;
- }
- // Logging moved to caller to avoid spamming every 50ms poll cycle.
- return best;
-}
-
-// Scan every process owned by the current user for one whose comm or
-// cmdline matches an expected game executable (e.g. FalloutNV.exe,
-// SkyrimSE.exe). Used as a fallback after the immediate descendant tree
-// loses the game — Proton's session manager can reparent game processes
-// outside of our root PID's subtree, so a plain-descendant walk misses
-// them. Only processes inside the given WINEPREFIX are considered so we
-// don't latch onto an unrelated Wine/Proton session.
-pid_t findGameProcessInPrefix(const QStringList &expected,
- const QString &winePrefix,
- QString *matchedNameOut) {
- if (expected.isEmpty()) {
- return 0;
- }
-
- const uid_t myUid = ::getuid();
- DIR *proc = opendir("/proc");
- if (!proc) {
- return 0;
- }
-
- QString expectedPrefixCanon;
- if (!winePrefix.isEmpty()) {
- expectedPrefixCanon = QDir(winePrefix).canonicalPath();
- }
-
- pid_t best = 0;
- struct dirent *entry = nullptr;
- while ((entry = readdir(proc)) != nullptr) {
- if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) continue;
- const char *name = entry->d_name;
- if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name)))
- continue;
-
- const pid_t pid = static_cast<pid_t>(std::strtol(name, nullptr, 10));
-
- struct stat st;
- if (::stat(QString("/proc/%1").arg(pid).toStdString().c_str(), &st) != 0 ||
- st.st_uid != myUid) {
- continue;
- }
-
- QString matched;
- if (!processMatchesExpected(pid, expected, &matched)) {
- continue;
- }
-
- // Constrain to the same WINEPREFIX so we don't latch onto an unrelated
- // Wine process (another instance, winetricks, etc.).
- if (!expectedPrefixCanon.isEmpty()) {
- const QString pidPrefix = readProcEnvVar(pid, "WINEPREFIX");
- if (pidPrefix.isEmpty()) continue;
- if (QDir(pidPrefix).canonicalPath() != expectedPrefixCanon) continue;
- }
-
- best = pid;
- if (matchedNameOut) *matchedNameOut = matched;
- break;
- }
- closedir(proc);
- return best;
-}
-
-// Best-effort "hard kill" of the wineserver (and every Wine process it
-// owns) for the given prefix. Used when the user clicks Unlock in the
-// lock dialog — Proton's session manager can keep wineserver alive for
-// tens of seconds after the game exits, and that's exactly what the
-// user is asking us to short-circuit. We call `wineserver -k` if the
-// Proton distribution exposes one, then fall back to SIGKILL on the
-// wineserver pid so the prefix is freed regardless.
-void killWineserverForPrefix(const QString &winePrefix) {
- if (winePrefix.isEmpty()) {
- return;
- }
-
- const pid_t ws = findWineserver(winePrefix);
- if (ws > 0) {
- log::info("sending SIGTERM to wineserver {} for prefix '{}'", ws,
- winePrefix.toStdString());
- if (::kill(ws, SIGTERM) != 0 && errno != ESRCH) {
- log::warn("SIGTERM on wineserver {} failed, errno={}", ws, errno);
- }
- // Give wineserver a short window to tear down cleanly, then SIGKILL
- // if it's still hanging around — the whole point of the Unlock
- // button is to not keep the user waiting.
- for (int i = 0; i < 10; ++i) {
- if (::kill(ws, 0) != 0 && errno == ESRCH) {
- break;
- }
- QThread::msleep(100);
- }
- if (::kill(ws, 0) == 0) {
- log::warn("wineserver {} did not exit on SIGTERM, sending SIGKILL",
- ws);
- ::kill(ws, SIGKILL);
- }
- } else {
- log::debug("killWineserverForPrefix: no wineserver found for '{}'",
- winePrefix.toStdString());
- }
-}
-
-DWORD exitCodeFromWaitStatus(int status) {
- if (WIFEXITED(status)) {
- return static_cast<DWORD>(WEXITSTATUS(status));
- }
-
- if (WIFSIGNALED(status)) {
- return static_cast<DWORD>(128 + WTERMSIG(status));
- }
-
- return 0;
-}
-
-ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode,
- UILocker::Session *ls,
- const QStringList &expected) {
- if (pid <= 0) {
- return ProcessRunner::Error;
- }
-
- // Capture the WINEPREFIX from the launched process so we can filter
- // wineserver lookups to the correct prefix. Without this, Fluorine
- // would track ANY wineserver owned by the user (e.g. one running
- // winecfg under ~/.wine while the game uses a different prefix).
- const QString winePrefix = readProcEnvVar(pid, "WINEPREFIX");
- if (!winePrefix.isEmpty()) {
- log::debug("process {} has WINEPREFIX='{}'", pid, winePrefix.toStdString());
- }
-
- // startDetached() creates a non-child process, so waitpid() will fail with
- // ECHILD. Detect this on the first call and switch to kill(pid, 0) polling
- // which works for any process owned by the same user.
- bool useKillPoll = false;
- {
- int status = 0;
- const pid_t probe = ::waitpid(pid, &status, WNOHANG);
- if (probe == pid) {
- if (exitCode != nullptr) {
- *exitCode = exitCodeFromWaitStatus(status);
- }
- log::debug("process {} completed immediately", pid);
- return ProcessRunner::Completed;
- }
- if (probe < 0 && errno == ECHILD) {
- // Not a child process (detached via startDetached), use kill(0) polling
- useKillPoll = true;
- log::debug("process {} is detached, using kill(0) polling", pid);
- }
- }
-
- bool seenTrackedProcess = false;
- pid_t lastTrackedPid = 0;
-
- while (true) {
- QString trackedName;
- pid_t displayPid = pid;
- QString displayName = readProcComm(pid);
- const pid_t tracked = findTrackedProcess(pid, expected, &trackedName);
- if (tracked > 0) {
- if (!seenTrackedProcess || tracked != lastTrackedPid) {
- log::info("tracking game process {}: {}", tracked, trackedName.toStdString());
- }
- seenTrackedProcess = true;
- lastTrackedPid = tracked;
- displayPid = tracked;
- displayName = trackedName;
- } else if (seenTrackedProcess) {
- // The tracked process is no longer a descendant of the root PID.
- // This can happen when:
- // a) The root (proton) exits and wine/game processes get reparented
- // b) A launcher .exe (nvse_loader, skse_loader, f4se_loader) exits
- // after spawning the actual game (FalloutNV.exe, SkyrimSE.exe,
- // Fallout4.exe)
- //
- // If the last tracked PID is still alive, keep polling it directly.
- if (lastTrackedPid > 0 && ::kill(lastTrackedPid, 0) == 0) {
- displayPid = lastTrackedPid;
- displayName = readProcComm(lastTrackedPid);
- } else {
- // The previously tracked process is gone. Rescan the user's
- // processes for any of the expected game executables in the same
- // WINEPREFIX — Proton's session manager can reparent the game
- // out of our descendant tree. If we find one, track that.
- QString rescanName;
- const pid_t rescanned =
- findGameProcessInPrefix(expected, winePrefix, &rescanName);
- if (rescanned > 0 && rescanned != lastTrackedPid) {
- log::info("tracked process exited, resumed tracking game {}: {}",
- rescanned, rescanName.toStdString());
- lastTrackedPid = rescanned;
- useKillPoll = true;
- displayPid = rescanned;
- displayName = rescanName;
- continue;
- }
-
- // No matching game process remains. Do NOT fall back to waiting
- // for wineserver — Proton's session manager keeps wineserver
- // alive for the prefix idle timeout (several seconds on modern
- // Proton, much longer under load), and blocking MO2's lock on
- // that is indistinguishable from a hang from the user's POV
- // (issue: "Fluorine stuck tracking wineserver"). The game has
- // truly exited once no expected executable is running.
- if (exitCode != nullptr) {
- *exitCode = 0;
- }
- log::debug("game processes for root {} exited; releasing lock "
- "(wineserver may linger in background)", pid);
- return ProcessRunner::Completed;
- }
- }
-
- if (ls != nullptr) {
- ls->setInfo(static_cast<DWORD>(std::max<pid_t>(0, displayPid)),
- displayName);
- }
-
- if (useKillPoll) {
- // Poll for process existence via kill(pid, 0).
- // When we have a tracked game PID, monitor that instead of the root
- // (proton) PID which may have already exited.
- const pid_t pollPid = (seenTrackedProcess && lastTrackedPid > 0)
- ? lastTrackedPid
- : pid;
- if (::kill(pollPid, 0) != 0) {
- if (errno == ESRCH) {
- // The polled process exited. Rescan the prefix for any other
- // matching game executable (launcher .exe's like f4se_loader
- // exit after spawning the real game binary, and Proton can
- // reparent that binary out of our root's subtree).
- if (seenTrackedProcess) {
- QString rescanName;
- const pid_t rescanned =
- findGameProcessInPrefix(expected, winePrefix, &rescanName);
- if (rescanned > 0 && rescanned != pollPid) {
- log::info("polled process {} exited, resumed tracking game {}: {}",
- pollPid, rescanned, rescanName.toStdString());
- lastTrackedPid = rescanned;
- continue;
- }
- }
- // No game process remains — do NOT block on wineserver. See
- // the equivalent note above for why.
- if (exitCode != nullptr) {
- *exitCode = 0;
- }
- log::debug("process {} completed", pollPid);
- return ProcessRunner::Completed;
- }
- // EPERM means the process exists but we can't signal it; keep waiting
- else if (errno != EPERM) {
- log::error("failed checking process {}, errno={}", pollPid, errno);
- return ProcessRunner::Error;
- }
- }
- } else {
- int status = 0;
- const pid_t waitResult = ::waitpid(pid, &status, WNOHANG);
-
- if (waitResult == pid) {
- // Root process (proton) exited. If we have a tracked game process
- // that is still alive, switch to polling the game PID directly
- // rather than declaring the game finished.
- if (seenTrackedProcess && lastTrackedPid > 0 &&
- ::kill(lastTrackedPid, 0) == 0) {
- log::debug("root process {} exited but tracked game {} still alive, "
- "switching to kill-poll", pid, lastTrackedPid);
- useKillPoll = true;
- continue;
- }
- if (exitCode != nullptr) {
- *exitCode = exitCodeFromWaitStatus(status);
- }
- log::debug("process {} completed", pid);
- return ProcessRunner::Completed;
- }
-
- if (waitResult < 0) {
- if (errno == EINTR) {
- continue;
- }
- if (errno == ECHILD) {
- // Process was reparented, switch to kill polling
- useKillPoll = true;
- continue;
- }
- log::error("failed waiting for {}, errno={}", pid, errno);
- return ProcessRunner::Error;
- }
- }
-
- if (ls != nullptr) {
- 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, terminating", displayPid);
- if (::kill(displayPid, SIGTERM) != 0 && errno != ESRCH) {
- log::warn("failed to terminate {}, errno={}", displayPid, errno);
- }
- // User clicked Unlock — they're asking us to stop waiting on the
- // prefix, so also hard-kill the wineserver. Otherwise Proton's
- // session manager keeps it alive for its idle timeout and the
- // next launch inherits a dirty prefix. See the "stuck tracking
- // wineserver" user report.
- killWineserverForPrefix(winePrefix);
- return ProcessRunner::Cancelled;
-
- case UILocker::NoResult:
- default:
- log::debug("unexpected lock result while waiting for {}", pid);
- return ProcessRunner::Error;
- }
- }
-
- QCoreApplication::processEvents(QEventLoop::AllEvents, 20);
- QThread::msleep(50);
- }
-}
-
-ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
- UILocker::Session *ls,
- const QStringList &expected) {
- return waitForPid(handleToPid(initialProcess), exitCode, ls, expected);
-}
-
-ProcessRunner::Results
-waitForProcesses(const std::vector<HANDLE> &initialProcesses,
- UILocker::Session *ls, const QStringList &expected) {
- if (initialProcesses.empty()) {
- return ProcessRunner::Completed;
- }
-
- for (HANDLE h : initialProcesses) {
- DWORD ignored = 0;
- const auto r = waitForPid(handleToPid(h), &ignored, ls, expected);
- if (r != ProcessRunner::Completed) {
- return r;
- }
- }
-
- return ProcessRunner::Completed;
-}
-
-#endif // _WIN32
-
-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 by default
- setHooked(true);
-}
-
-ProcessRunner &ProcessRunner::setBinary(const QFileInfo &binary) {
-#ifndef _WIN32
- m_sp.binary = QFileInfo(MOBase::normalizePathForHost(binary.filePath()));
-#else
- m_sp.binary = binary;
-#endif
- return *this;
-}
-
-ProcessRunner &ProcessRunner::setArguments(const QString &arguments) {
- m_sp.arguments = arguments;
- return *this;
-}
-
-ProcessRunner &ProcessRunner::setCurrentDirectory(const QDir &directory) {
-#ifndef _WIN32
- m_sp.currentDirectory.setPath(MOBase::normalizePathForHost(directory.path()));
-#else
- m_sp.currentDirectory = directory;
-#endif
- 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;
-
- if (m_waitFlags.testFlag(WaitForRefresh) &&
- !m_waitFlags.testFlag(TriggerRefresh)) {
- log::warn("process runner: WaitForRefresh without TriggerRefresh "
- "makes no sense, will be ignored");
- }
-
- return *this;
-}
-
-ProcessRunner &ProcessRunner::setHooked(bool b) {
- m_sp.hooked = b;
- return *this;
-}
-
-ProcessRunner &ProcessRunner::setFromFile(QWidget *parent,
- const QFileInfo &targetInfo) {
- if (!parent && m_ui) {
- parent = m_ui->mainWindow();
- }
-
- // if the file is a .exe, start it directly; if it's anything else, ask the
- // shell to start it
-
- const auto fec = spawn::getFileExecutionContext(parent, targetInfo);
-
- 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;
- setHooked(false);
- 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());
- }
-
- QString currentDirectory = exe.workingDirectory();
- if (currentDirectory.isEmpty()) {
- currentDirectory = exe.binaryInfo().absolutePath();
- }
-
- setBinary(exe.binaryInfo());
- setArguments(exe.arguments());
- setCurrentDirectory(currentDirectory);
- setSteamID(exe.steamAppID());
- setCustomOverwrite(customOverwrite);
- setForcedLibraries(forcedLibraries);
-
- m_sp.useProton = exe.useProton();
- m_sp.useTerminal = exe.useTerminal();
-
- return *this;
-}
-
-ProcessRunner &ProcessRunner::setFromShortcut(const MOShortcut &shortcut) {
- const auto currentInstance = InstanceManager::singleton().currentInstance();
-
- if (currentInstance) {
- if (shortcut.hasInstance() && !shortcut.isForInstance(*currentInstance)) {
- MOBase::reportError(QObject::tr("This shortcut is for instance '%1' but "
- "Mod Organizer is currently "
- "running for '%2'. Exit Mod Organizer "
- "before running the shortcut or "
- "change the active instance.")
- .arg(shortcut.instanceDisplayName())
- .arg(currentInstance->displayName()));
-
- throw std::exception();
- }
- }
-
- const auto *exes = m_core.executablesList();
- const auto exe = exes->find(shortcut.executableName());
-
- if (exe != exes->end()) {
- setFromExecutable(*exe);
- } else {
- MOBase::reportError(
- QObject::tr("Executable '%1' does not exist in instance '%2'.")
- .arg(shortcut.executableName())
- .arg(currentInstance->displayName()));
-
- throw std::exception();
- }
-
- 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(QFileInfo(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(QFileInfo(
- 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;
-}
-
-bool ProcessRunner::shouldRunShell() const {
- return !m_shellOpen.filePath().isEmpty();
-}
-
-ProcessRunner::Results ProcessRunner::run() {
- // check if setHooked() was called after setFromFile(); this needs to
- // modify the settings to run the associated executable instead of using
- // shell::Open()
-
- if (shouldRunShell() && m_sp.hooked) {
- // this is a non-executable file, but it should be hooked; the associated
- // executable needs to be retrieved and run instead
- auto assoc = env::getAssociation(m_shellOpen);
- if (!assoc.executable.filePath().isEmpty()) {
- setBinary(assoc.executable);
- setArguments(assoc.formattedCommandLine);
- setCurrentDirectory(assoc.executable.absoluteDir());
- m_shellOpen = {};
- } else {
- // if it fails, just use the regular shell open
- log::error("failed to get the associated executable, running unhooked");
- m_sp.hooked = false;
- }
- } else if (!shouldRunShell() && !m_sp.hooked) {
- // this is an executable that should not be hooked; just run it through
- // the shell
- m_shellOpen = m_sp.binary;
- }
-
- std::optional<Results> r;
-
- if (shouldRunShell()) {
- 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() {
- const auto file =
- MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath());
-
- log::debug("executing from shell: '{}'", file);
-
- auto r = shell::Open(file);
- 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)
-#ifdef _WIN32
- if (!m_core.beforeRun(m_sp.binary, m_sp.currentDirectory, m_sp.arguments,
- m_profileName, m_customOverwrite, m_forcedLibraries)) {
- return Error;
- }
-#else
- if (!m_core.beforeRun(m_sp.binary, m_sp.currentDirectory, m_sp.arguments,
- m_profileName, m_customOverwrite, m_forcedLibraries,
- &m_sp.saveBindMountSource,
- &m_sp.saveBindMountTarget)) {
- return Error;
- }
-#endif
-
- // parent widget used for any dialog popped up while checking for things
- QWidget *parent = (m_ui ? m_ui->mainWindow() : nullptr);
-
- const auto *game = m_core.managedGame();
- auto &settings = m_core.settings();
-
- if (m_sp.steamAppID.trimmed().isEmpty()) {
- const QString gameSteamId = game->steamAPPId().trimmed();
- if (!gameSteamId.isEmpty()) {
- m_sp.steamAppID = gameSteamId;
- log::debug("process runner: using game steam app id '{}' for launch",
- m_sp.steamAppID);
- }
- }
-
- // 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
-#ifdef _WIN32
- m_handle.reset(startBinary(parent, m_sp));
-#else
- m_handle.reset(reinterpret_cast<HANDLE>(
- static_cast<intptr_t>(startBinary(parent, m_sp))));
-#endif
- if (m_handle.get() == INVALID_HANDLE_VALUE) {
- return Error;
- }
-
- return {};
-}
-
-bool ProcessRunner::shouldRefresh(Results r) const {
- // afterRun() is only called with the Refresh flag; it refreshes the
- // directory structure and notifies plugins
- //
- // refreshing is not always required and can actually cause problems:
- //
- // 1) running shortcuts doesn't need refreshing because MO closes right
- // after
- //
- // 2) the mod info dialog is not set up to deal with refreshes, so that
- // it will crash because the old DirectoryEntry's are still being used
- // in the list
- if (!m_waitFlags.testFlag(TriggerRefresh)) {
- log::debug("process runner: not refreshing because the flag isn't set");
- return false;
- }
-
- switch (r) {
- case Completed: {
- log::debug("process runner: refreshing because the process completed");
- return true;
- }
-
- case ForceUnlocked: {
- // The process may still be running when the user force-unlocks.
- // Refreshing in that state can race with file updates.
- log::debug(
- "process runner: not refreshing because the ui was force unlocked");
- return false;
- }
-
- case Error: // fall-through
- case Cancelled:
- case Running:
- default: {
- return false;
- }
- }
-}
-
-ProcessRunner::Results ProcessRunner::postRun() {
- const bool mustWait = (m_waitFlags & ForceWait);
-
- if (!m_sp.hooked && !mustWait) {
- // the process wasn't hooked and there's no force wait, don't lock
- return Running;
- }
-
- if (mustWait && m_lockReason == UILocker::NoReason) {
- // never lock the ui without an escape hatch for the user
- log::debug("the ForceWait flag is set but the lock reason wasn't, "
- "defaulting to LockUI");
-
- m_lockReason = UILocker::LockUI;
- }
-
- const bool lockEnabled = m_core.settings().interface().lockGUI();
- const QStringList expectedExecutables =
- buildExpectedExecutables(m_sp.binary, m_sp.arguments);
-
- if (mustWait) {
- if (!lockEnabled) {
- // 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
-#ifndef _WIN32
- // Main window launches typically use TriggerRefresh without
- // waiting/locking. In that mode we still need post-run refresh/sync once
- // the process exits.
- if (m_waitFlags.testFlag(TriggerRefresh)) {
- const pid_t pid =
- static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
- const QFileInfo binary = m_sp.binary;
- QPointer<OrganizerCore> core = &m_core;
-
- std::thread([core, binary, pid]() {
- // For detached processes, waitpid will fail with ECHILD.
- // Use kill(0) polling instead.
- int status = 0;
- pid_t waited = -1;
- do {
- waited = ::waitpid(pid, &status, 0);
- } while (waited == -1 && errno == EINTR);
-
- DWORD exitCode = 0;
- if (waited == pid) {
- if (WIFEXITED(status)) {
- exitCode = static_cast<DWORD>(WEXITSTATUS(status));
- } else if (WIFSIGNALED(status)) {
- exitCode = static_cast<DWORD>(128 + WTERMSIG(status));
- }
- } else if (errno == ECHILD) {
- // Detached process — poll with kill(0) until gone.
- while (::kill(pid, 0) == 0 || errno == EPERM) {
- usleep(200000); // 200ms
- }
- } else {
- MOBase::log::warn("process runner: waitpid failed for pid {}: {}",
- pid, errno);
- }
-
- if (!core) {
- return;
- }
-
- QMetaObject::invokeMethod(
- core,
- [core, binary, exitCode]() {
- if (core) {
- core->afterRun(binary, exitCode);
- }
- },
- Qt::QueuedConnection);
- }).detach();
-
- log::debug(
- "process runner: scheduled async post-run refresh for pid {}", pid);
- }
-#endif
- return Running;
- }
-
- if (!lockEnabled) {
- // disabling locking is like clicking on unlock immediately
- log::debug("process runner: not waiting for process because "
- "locking is disabled");
-
- return ForceUnlocked;
- }
- }
-
- auto r = Error;
-
- if (mustWait && m_lockReason == UILocker::PreventExit && !lockEnabled) {
- // this happens when running shortcuts and locking is disabled
- //
- // MO must stay alive until all processes are dead or child processes
- // may not get hooked properly, but the user has disabled locking the ui
- //
- // this is a bit of an edge case, but that means the user wants to run
- // shortcuts without seeing the lock dialog, so allow them to do that
- //
- // MO will be running in the background with no visual feedback, but that's
- // how it is
- r = waitForProcess(m_handle.get(), &m_exitCode, nullptr,
- expectedExecutables);
- } else {
- withLock([&](auto &ls) {
- r = waitForProcess(m_handle.get(), &m_exitCode, &ls, expectedExecutables);
- });
- }
-
- if (shouldRefresh(r)) {
- QEventLoop loop;
- const bool wait = m_waitFlags.testFlag(WaitForRefresh);
-
- if (wait) {
- QObject::connect(&m_core, &OrganizerCore::directoryStructureReady, &loop,
- &QEventLoop::quit, Qt::ConnectionType::QueuedConnection);
- }
-
- m_core.afterRun(m_sp.binary, m_exitCode);
-
- if (wait) {
- log::debug("process runner: waiting until refresh finishes");
- loop.exec();
- log::debug("process runner: refresh is done");
- }
- }
-
- return r;
-}
-
-#ifdef _WIN32
-ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h) {
- m_handle.reset(h);
- return postRun();
-}
-#else
-ProcessRunner::Results ProcessRunner::attachToProcess(pid_t pid) {
- m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(pid)));
- return postRun();
-}
-#endif
-
-DWORD ProcessRunner::exitCode() const { return m_exitCode; }
-
-#ifdef _WIN32
-HANDLE ProcessRunner::getProcessHandle() const { return m_handle.get(); }
-#else
-pid_t ProcessRunner::getProcessHandle() const {
- return static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
-}
-#endif
-
-env::HandlePtr ProcessRunner::stealProcessHandle() {
- auto h = m_handle.release();
- m_handle.reset(INVALID_HANDLE_VALUE);
- return env::HandlePtr(h);
-}
-
-#ifdef _WIN32
-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;
-
- for (;;) {
- withLock([&](auto &ls) {
- const auto processes = getRunningUSVFSProcesses();
- if (processes.empty()) {
- r = Completed;
- return;
- }
-
- r = waitForProcesses(processes, &ls);
-
- if (r != Completed) {
- // error, cancelled, or unlocked
- return;
- }
-
- // this process is completed, check for others
- r = Running;
- });
-
- if (r != Running) {
- break;
- }
- }
-
- return r;
-}
-#endif
-
-void ProcessRunner::withLock(std::function<void(UILocker::Session &)> f) {
- auto ls = UILocker::instance().lock(m_lockReason);
- f(*ls);
-}
+#include "processrunner.h" +#include "env.h" +#include "envmodule.h" +#include "instancemanager.h" +#include "iuserinterface.h" +#include "organizercore.h" + +#include <iplugingame.h> +#include <log.h> +#include <report.h> +#include <uibase/utility.h> + +#include <QCoreApplication> +#include <QEventLoop> +#include <QFile> +#include <QFileInfo> +#include <QMetaObject> +#include <QPointer> +#include <QProcess> +#include <QThread> + +#include <cerrno> +#include <deque> +#include <dirent.h> +#include <fstream> +#include <signal.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <unordered_map> +#include <unordered_set> + +using namespace MOBase; + +void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp, + const Settings& settings) +{ + const QString modsPath = settings.paths().mods(); + + // Check if this is a request with either an executable or a working + // directory under our mods folder; if so, 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(); + QString trailedModsPath = modsPath; + if (!trailedModsPath.endsWith('/')) { + trailedModsPath = trailedModsPath + '/'; + } + bool virtualizedCwd = cwdPath.startsWith(trailedModsPath, Qt::CaseInsensitive); + QString binPath = sp.binary.absoluteFilePath(); + bool virtualizedBin = binPath.startsWith(trailedModsPath, Qt::CaseInsensitive); + if (!virtualizedCwd && !virtualizedBin) { + return; + } + + if (virtualizedCwd) { + int cwdOffset = cwdPath.indexOf('/', trailedModsPath.length()); + QString adjustedCwd = cwdPath.mid(cwdOffset, -1); + cwdPath = game->dataDirectory().absolutePath(); + if (cwdOffset >= 0) + cwdPath += adjustedCwd; + } + + if (virtualizedBin) { + int binOffset = binPath.indexOf('/', trailedModsPath.length()); + QString adjustedBin = binPath.mid(binOffset, -1); + binPath = game->dataDirectory().absolutePath(); + if (binOffset >= 0) + binPath += adjustedBin; + } + + // FUSE is already mounted on Linux — resolve paths directly without + // launching through MO2-core (which would fail in the Proton prefix). + // + // Root Builder deploys Root/ contents to the game directory root, + // stripping the "Root/" prefix. Fix paths that were remapped to + // <dataDir>/Root/... so they point to <gameDir>/... instead. + // Also handle direct mods/.../Root/ paths (not just dataDir/Root/). + const QString gameDir = game->gameDirectory().absolutePath(); + const QString dataDir = game->dataDirectory().absolutePath(); + + auto normalizeRootPath = [&](QString& path) { + const QString rootWithSlash = dataDir + QStringLiteral("/Root/"); + const QString rootExact = dataDir + QStringLiteral("/Root"); + if (path.startsWith(rootWithSlash, Qt::CaseInsensitive)) { + const QString after = path.mid(rootWithSlash.length()); + path = after.isEmpty() ? gameDir : gameDir + QStringLiteral("/") + after; + return true; + } + if (path.compare(rootExact, Qt::CaseInsensitive) == 0) { + path = gameDir; + return true; + } + return false; + }; + + bool binNormalized = normalizeRootPath(binPath); + bool cwdNormalized = normalizeRootPath(cwdPath); + + if (binNormalized) { + log::info("Root Builder: rewrote binary -> '{}'", binPath); + } + if (cwdNormalized) { + log::info("Root Builder: rewrote start-in -> '{}'", cwdPath); + } + + // If neither was caught by the dataDir/Root/ check, the path might still be + // the original mods/.../Root/ path (not yet remapped). This happens when + // the first remapping above produced something that didn't match the + // dataDir/Root pattern. + if (!binNormalized && binPath.startsWith(trailedModsPath, Qt::CaseInsensitive)) { + int rootIdx = + binPath.indexOf("/Root/", trailedModsPath.length(), Qt::CaseInsensitive); + if (rootIdx < 0) + rootIdx = + binPath.indexOf("/Root", trailedModsPath.length(), Qt::CaseInsensitive); + if (rootIdx >= 0) { + int afterRootStart = rootIdx + 5; // skip "/Root" + if (afterRootStart < binPath.length() && binPath[afterRootStart] == '/') + ++afterRootStart; + const QString afterRoot = binPath.mid(afterRootStart); + const QString modRoot = binPath.left(rootIdx + 5); + + binPath = afterRoot.isEmpty() ? gameDir + : gameDir + QStringLiteral("/") + afterRoot; + log::info("Root Builder: rewrote binary (mod path) -> '{}'", binPath); + + if (!cwdNormalized && cwdPath.startsWith(modRoot, Qt::CaseInsensitive)) { + int cwdAfterStart = modRoot.length(); + if (cwdAfterStart < cwdPath.length() && cwdPath[cwdAfterStart] == '/') + ++cwdAfterStart; + const QString cwdAfter = cwdPath.mid(cwdAfterStart); + cwdPath = cwdAfter.isEmpty() ? gameDir + : gameDir + QStringLiteral("/") + cwdAfter; + log::info("Root Builder: rewrote start-in (mod path) -> '{}'", cwdPath); + } + } + } + + sp.binary = QFileInfo(binPath); + sp.currentDirectory.setPath(cwdPath); +} + +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: + default: + return "no"; + } +} + +pid_t handleToPid(HANDLE h) +{ + return static_cast<pid_t>(reinterpret_cast<intptr_t>(h)); +} + +QString readProcComm(pid_t pid) +{ + QFile f(QString("/proc/%1/comm").arg(pid)); + if (!f.open(QIODevice::ReadOnly)) { + return {}; + } + + return QString::fromUtf8(f.readAll()).trimmed(); +} + +// Read /proc/<pid>/cmdline (NUL-separated) and return all argv entries. +QStringList readProcCmdline(pid_t pid) +{ + QFile f(QString("/proc/%1/cmdline").arg(pid)); + if (!f.open(QIODevice::ReadOnly)) { + return {}; + } + + const QByteArray data = f.readAll(); + QStringList parts; + for (const QByteArray& part : data.split('\0')) { + if (!part.isEmpty()) { + parts.push_back(QString::fromUtf8(part)); + } + } + return parts; +} + +// Read a specific environment variable from /proc/<pid>/environ. +// The environ file is NUL-separated KEY=VALUE pairs. +QString readProcEnvVar(pid_t pid, const char* varName) +{ + QFile f(QString("/proc/%1/environ").arg(pid)); + if (!f.open(QIODevice::ReadOnly)) { + return {}; + } + + const QByteArray data = f.readAll(); + const QByteArray prefix = QByteArray(varName) + '='; + for (const QByteArray& entry : data.split('\0')) { + if (entry.startsWith(prefix)) { + return QString::fromUtf8(entry.mid(prefix.size())); + } + } + return {}; +} + +// Find a wineserver process owned by the current user that belongs to the +// given WINEPREFIX. When expectedPrefix is empty, returns the first +// wineserver owned by us (legacy behaviour). +// +// Wineserver stays alive as long as any Wine process in the prefix is +// running, making it the most reliable way to detect when a game has truly +// exited — even when launcher .exe's (nvse_loader, skse_loader, etc.) exit +// before the actual game. +pid_t findWineserver(const QString& expectedPrefix = {}) +{ + const uid_t myUid = ::getuid(); + DIR* proc = opendir("/proc"); + if (!proc) + return 0; + + pid_t result = 0; + struct dirent* entry = nullptr; + while ((entry = readdir(proc)) != nullptr) { + if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) + continue; + const char* name = entry->d_name; + if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name))) + continue; + + const pid_t pid = static_cast<pid_t>(std::strtol(name, nullptr, 10)); + const QString comm = readProcComm(pid); + if (comm != "wineserver") + continue; + + struct stat st; + if (::stat(QString("/proc/%1").arg(pid).toStdString().c_str(), &st) != 0 || + st.st_uid != myUid) { + continue; + } + + // If a prefix filter was given, verify this wineserver belongs to it. + // A wineserver without WINEPREFIX in its environ is using the default + // ~/.wine prefix, which is never the game prefix — skip it too. + if (!expectedPrefix.isEmpty()) { + const QString wsPrefix = readProcEnvVar(pid, "WINEPREFIX"); + if (wsPrefix.isEmpty() || + QDir(wsPrefix).canonicalPath() != QDir(expectedPrefix).canonicalPath()) { + log::debug("skipping wineserver {} (prefix '{}' != expected '{}')", pid, + wsPrefix.toStdString(), expectedPrefix.toStdString()); + continue; + } + } + + result = pid; + break; + } + closedir(proc); + return result; +} + +// Check whether any of the expected executable names appear in a process's +// comm or cmdline. Wine processes often show "wine64-preload" or "start.exe" +// in /proc/comm while the actual game executable only appears in cmdline. +// Also handles the 15-char TASK_COMM_LEN truncation in /proc/comm. +bool processMatchesExpected(pid_t pid, const QStringList& expected, + QString* matchedNameOut) +{ + // 1. Check /proc/comm (fast path). + const QString comm = readProcComm(pid); + if (!comm.isEmpty()) { + const QString lower = comm.toLower(); + for (const QString& exp : expected) { + if (lower == exp) { + if (matchedNameOut) + *matchedNameOut = comm; + return true; + } + // Handle TASK_COMM_LEN truncation (15 chars): if the expected name is + // longer than 15 chars, check if comm matches its first 15 chars. + if (exp.size() > 15 && lower == exp.left(15)) { + if (matchedNameOut) + *matchedNameOut = exp; + return true; + } + } + } + + // 2. Check /proc/cmdline — Wine/Proton processes carry the .exe name here + // even when comm shows wine64-preloader or start.exe. + const QStringList cmdline = readProcCmdline(pid); + for (const QString& arg : cmdline) { + const QString normalized = QString(arg).replace('\\', '/'); + const QString base = QFileInfo(normalized).fileName().toLower(); + if (expected.contains(base)) { + if (matchedNameOut) + *matchedNameOut = QFileInfo(normalized).fileName(); + return true; + } + } + + return false; +} + +std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap() +{ + std::unordered_map<pid_t, std::vector<pid_t>> children; + DIR* proc = opendir("/proc"); + if (!proc) { + return children; + } + + struct dirent* entry = nullptr; + while ((entry = readdir(proc)) != nullptr) { + if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) { + continue; + } + + const char* name = entry->d_name; + if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name))) { + continue; + } + + const pid_t pid = static_cast<pid_t>(std::strtol(name, nullptr, 10)); + std::ifstream status(QString("/proc/%1/status").arg(pid).toStdString()); + if (!status.is_open()) { + continue; + } + + std::string line; + pid_t ppid = 0; + while (std::getline(status, line)) { + if (line.rfind("PPid:", 0) == 0) { + ppid = static_cast<pid_t>(std::strtol(line.c_str() + 5, nullptr, 10)); + break; + } + } + + if (ppid > 0) { + children[ppid].push_back(pid); + } + } + + closedir(proc); + return children; +} + +std::unordered_set<pid_t> collectDescendants( + pid_t root, const std::unordered_map<pid_t, std::vector<pid_t>>& children) +{ + std::unordered_set<pid_t> out; + std::deque<pid_t> q; + q.push_back(root); + + while (!q.empty()) { + const pid_t cur = q.front(); + q.pop_front(); + + const auto it = children.find(cur); + if (it == children.end()) { + continue; + } + + for (pid_t child : it->second) { + if (out.insert(child).second) { + q.push_back(child); + } + } + } + + return out; +} + +QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arguments) +{ + QStringList expected; + auto addName = [&](QString name) { + name = name.trimmed().toLower(); + if (!name.isEmpty() && !expected.contains(name)) { + expected.push_back(name); + } + }; + + addName(binary.fileName()); + + const auto args = QProcess::splitCommand(arguments); + for (const QString& arg : args) { + const QFileInfo fi(arg); + const QString base = fi.fileName(); + if (base.endsWith(".exe", Qt::CaseInsensitive)) { + addName(base); + } + } + + log::debug("buildExpectedExecutables: returning [{}]", + expected.join(", ").toStdString()); + return expected; +} + +pid_t findTrackedProcess(pid_t rootPid, const QStringList& expected, + QString* trackedNameOut) +{ + if (expected.isEmpty()) { + return 0; + } + + const auto children = buildProcChildrenMap(); + const auto descendants = collectDescendants(rootPid, children); + if (descendants.empty()) { + return 0; + } + + pid_t best = 0; + QString bestName; + for (pid_t pid : descendants) { + QString matched; + if (processMatchesExpected(pid, expected, &matched)) { + best = pid; + bestName = matched; + break; + } + } + + if (best > 0 && trackedNameOut) { + *trackedNameOut = bestName; + } + return best; +} + +// Scan every process owned by the current user for one whose comm or cmdline +// matches an expected game executable (e.g. FalloutNV.exe, SkyrimSE.exe). +// Used as a fallback after the immediate descendant tree loses the game — +// Proton's session manager can reparent game processes outside of our root +// PID's subtree, so a plain-descendant walk misses them. Only processes +// inside the given WINEPREFIX are considered so we don't latch onto an +// unrelated Wine/Proton session. +pid_t findGameProcessInPrefix(const QStringList& expected, const QString& winePrefix, + QString* matchedNameOut) +{ + if (expected.isEmpty()) { + return 0; + } + + const uid_t myUid = ::getuid(); + DIR* proc = opendir("/proc"); + if (!proc) { + return 0; + } + + QString expectedPrefixCanon; + if (!winePrefix.isEmpty()) { + expectedPrefixCanon = QDir(winePrefix).canonicalPath(); + } + + pid_t best = 0; + struct dirent* entry = nullptr; + while ((entry = readdir(proc)) != nullptr) { + if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) + continue; + const char* name = entry->d_name; + if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name))) + continue; + + const pid_t pid = static_cast<pid_t>(std::strtol(name, nullptr, 10)); + + struct stat st; + if (::stat(QString("/proc/%1").arg(pid).toStdString().c_str(), &st) != 0 || + st.st_uid != myUid) { + continue; + } + + QString matched; + if (!processMatchesExpected(pid, expected, &matched)) { + continue; + } + + // Constrain to the same WINEPREFIX so we don't latch onto an unrelated + // Wine process (another instance, winetricks, etc.). + if (!expectedPrefixCanon.isEmpty()) { + const QString pidPrefix = readProcEnvVar(pid, "WINEPREFIX"); + if (pidPrefix.isEmpty()) + continue; + if (QDir(pidPrefix).canonicalPath() != expectedPrefixCanon) + continue; + } + + best = pid; + if (matchedNameOut) + *matchedNameOut = matched; + break; + } + closedir(proc); + return best; +} + +// Best-effort "hard kill" of the wineserver (and every Wine process it owns) +// for the given prefix. Used when the user clicks Unlock in the lock dialog — +// Proton's session manager can keep wineserver alive for tens of seconds +// after the game exits, and that's exactly what the user is asking us to +// short-circuit. +void killWineserverForPrefix(const QString& winePrefix) +{ + if (winePrefix.isEmpty()) { + return; + } + + const pid_t ws = findWineserver(winePrefix); + if (ws > 0) { + log::info("sending SIGTERM to wineserver {} for prefix '{}'", ws, + winePrefix.toStdString()); + if (::kill(ws, SIGTERM) != 0 && errno != ESRCH) { + log::warn("SIGTERM on wineserver {} failed, errno={}", ws, errno); + } + // Give wineserver a short window to tear down cleanly, then SIGKILL if + // it's still hanging around. + for (int i = 0; i < 10; ++i) { + if (::kill(ws, 0) != 0 && errno == ESRCH) { + break; + } + QThread::msleep(100); + } + if (::kill(ws, 0) == 0) { + log::warn("wineserver {} did not exit on SIGTERM, sending SIGKILL", ws); + ::kill(ws, SIGKILL); + } + } else { + log::debug("killWineserverForPrefix: no wineserver found for '{}'", + winePrefix.toStdString()); + } +} + +DWORD exitCodeFromWaitStatus(int status) +{ + if (WIFEXITED(status)) { + return static_cast<DWORD>(WEXITSTATUS(status)); + } + + if (WIFSIGNALED(status)) { + return static_cast<DWORD>(128 + WTERMSIG(status)); + } + + return 0; +} + +ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, + UILocker::Session* ls, const QStringList& expected) +{ + if (pid <= 0) { + return ProcessRunner::Error; + } + + // Capture the WINEPREFIX from the launched process so we can filter + // wineserver lookups to the correct prefix. Without this, Fluorine would + // track ANY wineserver owned by the user (e.g. one running winecfg under + // ~/.wine while the game uses a different prefix). + const QString winePrefix = readProcEnvVar(pid, "WINEPREFIX"); + if (!winePrefix.isEmpty()) { + log::debug("process {} has WINEPREFIX='{}'", pid, winePrefix.toStdString()); + } + + // startDetached() creates a non-child process, so waitpid() will fail with + // ECHILD. Detect this on the first call and switch to kill(pid, 0) polling + // which works for any process owned by the same user. + bool useKillPoll = false; + { + int status = 0; + const pid_t probe = ::waitpid(pid, &status, WNOHANG); + if (probe == pid) { + if (exitCode != nullptr) { + *exitCode = exitCodeFromWaitStatus(status); + } + log::debug("process {} completed immediately", pid); + return ProcessRunner::Completed; + } + if (probe < 0 && errno == ECHILD) { + useKillPoll = true; + log::debug("process {} is detached, using kill(0) polling", pid); + } + } + + bool seenTrackedProcess = false; + pid_t lastTrackedPid = 0; + + while (true) { + QString trackedName; + pid_t displayPid = pid; + QString displayName = readProcComm(pid); + const pid_t tracked = findTrackedProcess(pid, expected, &trackedName); + if (tracked > 0) { + if (!seenTrackedProcess || tracked != lastTrackedPid) { + log::info("tracking game process {}: {}", tracked, trackedName.toStdString()); + } + seenTrackedProcess = true; + lastTrackedPid = tracked; + displayPid = tracked; + displayName = trackedName; + } else if (seenTrackedProcess) { + // The tracked process is no longer a descendant of the root PID. This + // can happen when: + // a) The root (proton) exits and wine/game processes get reparented + // b) A launcher .exe (nvse_loader, skse_loader, f4se_loader) exits + // after spawning the actual game (FalloutNV.exe, SkyrimSE.exe, + // Fallout4.exe) + // + // If the last tracked PID is still alive, keep polling it directly. + if (lastTrackedPid > 0 && ::kill(lastTrackedPid, 0) == 0) { + displayPid = lastTrackedPid; + displayName = readProcComm(lastTrackedPid); + } else { + // The previously tracked process is gone. Rescan the user's processes + // for any of the expected game executables in the same WINEPREFIX — + // Proton's session manager can reparent the game out of our + // descendant tree. If we find one, track that. + QString rescanName; + const pid_t rescanned = + findGameProcessInPrefix(expected, winePrefix, &rescanName); + if (rescanned > 0 && rescanned != lastTrackedPid) { + log::info("tracked process exited, resumed tracking game {}: {}", + rescanned, rescanName.toStdString()); + lastTrackedPid = rescanned; + useKillPoll = true; + displayPid = rescanned; + displayName = rescanName; + continue; + } + + // No matching game process remains. Do NOT fall back to waiting for + // wineserver — Proton's session manager keeps wineserver alive for + // the prefix idle timeout (several seconds on modern Proton, much + // longer under load), and blocking MO2's lock on that is + // indistinguishable from a hang from the user's POV. + if (exitCode != nullptr) { + *exitCode = 0; + } + log::debug("game processes for root {} exited; releasing lock " + "(wineserver may linger in background)", + pid); + return ProcessRunner::Completed; + } + } + + if (ls != nullptr) { + ls->setInfo(static_cast<DWORD>(std::max<pid_t>(0, displayPid)), displayName); + } + + if (useKillPoll) { + // Poll for process existence via kill(pid, 0). When we have a tracked + // game PID, monitor that instead of the root (proton) PID which may + // have already exited. + const pid_t pollPid = + (seenTrackedProcess && lastTrackedPid > 0) ? lastTrackedPid : pid; + if (::kill(pollPid, 0) != 0) { + if (errno == ESRCH) { + // The polled process exited. Rescan the prefix for any other + // matching game executable (launcher .exe's like f4se_loader exit + // after spawning the real game binary, and Proton can reparent + // that binary out of our root's subtree). + if (seenTrackedProcess) { + QString rescanName; + const pid_t rescanned = + findGameProcessInPrefix(expected, winePrefix, &rescanName); + if (rescanned > 0 && rescanned != pollPid) { + log::info("polled process {} exited, resumed tracking game {}: {}", + pollPid, rescanned, rescanName.toStdString()); + lastTrackedPid = rescanned; + continue; + } + } + // No game process remains — do NOT block on wineserver. + if (exitCode != nullptr) { + *exitCode = 0; + } + log::debug("process {} completed", pollPid); + return ProcessRunner::Completed; + } + // EPERM means the process exists but we can't signal it; keep + // waiting. + else if (errno != EPERM) { + log::error("failed checking process {}, errno={}", pollPid, errno); + return ProcessRunner::Error; + } + } + } else { + int status = 0; + const pid_t waitResult = ::waitpid(pid, &status, WNOHANG); + + if (waitResult == pid) { + // Root process (proton) exited. If we have a tracked game process + // that is still alive, switch to polling the game PID directly + // rather than declaring the game finished. + if (seenTrackedProcess && lastTrackedPid > 0 && + ::kill(lastTrackedPid, 0) == 0) { + log::debug("root process {} exited but tracked game {} still alive, " + "switching to kill-poll", + pid, lastTrackedPid); + useKillPoll = true; + continue; + } + if (exitCode != nullptr) { + *exitCode = exitCodeFromWaitStatus(status); + } + log::debug("process {} completed", pid); + return ProcessRunner::Completed; + } + + if (waitResult < 0) { + if (errno == EINTR) { + continue; + } + if (errno == ECHILD) { + // Process was reparented, switch to kill polling. + useKillPoll = true; + continue; + } + log::error("failed waiting for {}, errno={}", pid, errno); + return ProcessRunner::Error; + } + } + + if (ls != nullptr) { + 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, terminating", displayPid); + if (::kill(displayPid, SIGTERM) != 0 && errno != ESRCH) { + log::warn("failed to terminate {}, errno={}", displayPid, errno); + } + // User clicked Unlock — they're asking us to stop waiting on the + // prefix, so also hard-kill the wineserver. Otherwise Proton's + // session manager keeps it alive for its idle timeout and the next + // launch inherits a dirty prefix. + killWineserverForPrefix(winePrefix); + return ProcessRunner::Cancelled; + + case UILocker::NoResult: + default: + log::debug("unexpected lock result while waiting for {}", pid); + return ProcessRunner::Error; + } + } + + QCoreApplication::processEvents(QEventLoop::AllEvents, 20); + QThread::msleep(50); + } +} + +ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode, + UILocker::Session* ls, + const QStringList& expected) +{ + return waitForPid(handleToPid(initialProcess), exitCode, ls, expected); +} + +ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProcesses, + UILocker::Session* ls, + const QStringList& expected) +{ + if (initialProcesses.empty()) { + return ProcessRunner::Completed; + } + + for (HANDLE h : initialProcesses) { + DWORD ignored = 0; + const auto r = waitForPid(handleToPid(h), &ignored, ls, expected); + if (r != ProcessRunner::Completed) { + return r; + } + } + + return ProcessRunner::Completed; +} + +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 by default + setHooked(true); +} + +ProcessRunner& ProcessRunner::setBinary(const QFileInfo& binary) +{ + m_sp.binary = QFileInfo(MOBase::normalizePathForHost(binary.filePath())); + return *this; +} + +ProcessRunner& ProcessRunner::setArguments(const QString& arguments) +{ + m_sp.arguments = arguments; + return *this; +} + +ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory) +{ + m_sp.currentDirectory.setPath(MOBase::normalizePathForHost(directory.path())); + 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; + + if (m_waitFlags.testFlag(WaitForRefresh) && !m_waitFlags.testFlag(TriggerRefresh)) { + log::warn("process runner: WaitForRefresh without TriggerRefresh " + "makes no sense, will be ignored"); + } + + return *this; +} + +ProcessRunner& ProcessRunner::setHooked(bool b) +{ + m_sp.hooked = b; + return *this; +} + +ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo) +{ + if (!parent && m_ui) { + parent = m_ui->mainWindow(); + } + + // if the file is a .exe, start it directly; if it's anything else, ask the + // shell to start it + const auto fec = spawn::getFileExecutionContext(parent, targetInfo); + + switch (fec.type) { + case spawn::FileExecutionTypes::Executable: { + setBinary(fec.binary); + setArguments(fec.arguments); + setCurrentDirectory(targetInfo.absoluteDir()); + break; + } + + case spawn::FileExecutionTypes::Other: + default: { + m_shellOpen = targetInfo; + setHooked(false); + 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()); + } + + QString currentDirectory = exe.workingDirectory(); + if (currentDirectory.isEmpty()) { + currentDirectory = exe.binaryInfo().absolutePath(); + } + + setBinary(exe.binaryInfo()); + setArguments(exe.arguments()); + setCurrentDirectory(currentDirectory); + setSteamID(exe.steamAppID()); + setCustomOverwrite(customOverwrite); + setForcedLibraries(forcedLibraries); + + m_sp.useProton = exe.useProton(); + m_sp.useTerminal = exe.useTerminal(); + + return *this; +} + +ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut) +{ + const auto currentInstance = InstanceManager::singleton().currentInstance(); + + if (currentInstance) { + if (shortcut.hasInstance() && !shortcut.isForInstance(*currentInstance)) { + MOBase::reportError(QObject::tr("This shortcut is for instance '%1' but " + "Mod Organizer is currently " + "running for '%2'. Exit Mod Organizer " + "before running the shortcut or " + "change the active instance.") + .arg(shortcut.instanceDisplayName()) + .arg(currentInstance->displayName())); + + throw std::exception(); + } + } + + const auto* exes = m_core.executablesList(); + const auto exe = exes->find(shortcut.executableName()); + + if (exe != exes->end()) { + setFromExecutable(*exe); + } else { + MOBase::reportError(QObject::tr("Executable '%1' does not exist in instance '%2'.") + .arg(shortcut.executableName()) + .arg(currentInstance->displayName())); + + throw std::exception(); + } + + 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(QFileInfo(executable)); + setArguments(args.join(" ")); + setCurrentDirectory(cwd); + setProfileName(profileOverride); + + if (executable.contains('\\') || executable.contains('/')) { + if (m_sp.binary.isRelative()) { + setBinary(QFileInfo( + 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 { + 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; +} + +bool ProcessRunner::shouldRunShell() const +{ + return !m_shellOpen.filePath().isEmpty(); +} + +ProcessRunner::Results ProcessRunner::run() +{ + // check if setHooked() was called after setFromFile(); this needs to modify + // the settings to run the associated executable instead of using + // shell::Open() + if (shouldRunShell() && m_sp.hooked) { + auto assoc = env::getAssociation(m_shellOpen); + if (!assoc.executable.filePath().isEmpty()) { + setBinary(assoc.executable); + setArguments(assoc.formattedCommandLine); + setCurrentDirectory(assoc.executable.absoluteDir()); + m_shellOpen = {}; + } else { + log::error("failed to get the associated executable, running unhooked"); + m_sp.hooked = false; + } + } else if (!shouldRunShell() && !m_sp.hooked) { + m_shellOpen = m_sp.binary; + } + + std::optional<Results> r; + + if (shouldRunShell()) { + r = runShell(); + } else { + r = runBinary(); + } + + if (r) { + return *r; + } + + return postRun(); +} + +std::optional<ProcessRunner::Results> ProcessRunner::runShell() +{ + const auto file = MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath()); + + log::debug("executing from shell: '{}'", file); + + auto r = shell::Open(file); + 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 that 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()) { + const auto profile = m_core.currentProfile(); + if (!profile) { + throw MyException(QObject::tr("No profile set")); + } + + m_profileName = profile->name(); + } + + // saves profile, sets up the VFS, notifies plugins, etc.; can return false + // if a plugin doesn't want the program to run. + if (!m_core.beforeRun(m_sp.binary, m_sp.currentDirectory, m_sp.arguments, + m_profileName, m_customOverwrite, m_forcedLibraries, + &m_sp.saveBindMountSource, &m_sp.saveBindMountTarget)) { + return Error; + } + + QWidget* parent = (m_ui ? m_ui->mainWindow() : nullptr); + + const auto* game = m_core.managedGame(); + auto& settings = m_core.settings(); + + if (m_sp.steamAppID.trimmed().isEmpty()) { + const QString gameSteamId = game->steamAPPId().trimmed(); + if (!gameSteamId.isEmpty()) { + m_sp.steamAppID = gameSteamId; + log::debug("process runner: using game steam app id '{}' for launch", + m_sp.steamAppID); + } + } + + if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) { + return Error; + } + + if (!checkBlacklist(parent, m_sp, settings)) { + return Error; + } + + // if the executable is inside the mods folder another instance of + // ModOrganizer is spawned instead to launch it + adjustForVirtualized(game, m_sp, settings); + + m_handle.reset(reinterpret_cast<HANDLE>( + static_cast<intptr_t>(startBinary(parent, m_sp)))); + + if (m_handle.get() == INVALID_HANDLE_VALUE) { + return Error; + } + + return {}; +} + +bool ProcessRunner::shouldRefresh(Results r) const +{ + // afterRun() is only called with the Refresh flag; it refreshes the + // directory structure and notifies plugins. + // + // Refreshing is not always required and can actually cause problems: + // + // 1) running shortcuts doesn't need refreshing because MO closes right + // after + // + // 2) the mod info dialog is not set up to deal with refreshes, so that it + // will crash because the old DirectoryEntry's are still being used in + // the list + if (!m_waitFlags.testFlag(TriggerRefresh)) { + log::debug("process runner: not refreshing because the flag isn't set"); + return false; + } + + switch (r) { + case Completed: { + log::debug("process runner: refreshing because the process completed"); + return true; + } + + case ForceUnlocked: { + // The process may still be running when the user force-unlocks. Refreshing + // in that state can race with file updates. + log::debug("process runner: not refreshing because the ui was force unlocked"); + return false; + } + + case Error: + case Cancelled: + case Running: + default: { + return false; + } + } +} + +ProcessRunner::Results ProcessRunner::postRun() +{ + const bool mustWait = (m_waitFlags & ForceWait); + + if (!m_sp.hooked && !mustWait) { + return Running; + } + + if (mustWait && m_lockReason == UILocker::NoReason) { + log::debug("the ForceWait flag is set but the lock reason wasn't, " + "defaulting to LockUI"); + + m_lockReason = UILocker::LockUI; + } + + const bool lockEnabled = m_core.settings().interface().lockGUI(); + const QStringList expectedExecutables = + buildExpectedExecutables(m_sp.binary, m_sp.arguments); + + if (mustWait) { + if (!lockEnabled) { + log::debug("locking is disabled, but the output of the application is required; " + "overriding this setting and locking the ui"); + } + } else { + if (m_lockReason == UILocker::NoReason) { + // Main window launches typically use TriggerRefresh without + // waiting/locking. In that mode we still need post-run refresh/sync once + // the process exits. + if (m_waitFlags.testFlag(TriggerRefresh)) { + const pid_t pid = + static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get())); + const QFileInfo binary = m_sp.binary; + QPointer<OrganizerCore> core = &m_core; + + std::thread([core, binary, pid]() { + int status = 0; + pid_t waited = -1; + do { + waited = ::waitpid(pid, &status, 0); + } while (waited == -1 && errno == EINTR); + + DWORD exitCode = 0; + if (waited == pid) { + if (WIFEXITED(status)) { + exitCode = static_cast<DWORD>(WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + exitCode = static_cast<DWORD>(128 + WTERMSIG(status)); + } + } else if (errno == ECHILD) { + // Detached process — poll with kill(0) until gone. + while (::kill(pid, 0) == 0 || errno == EPERM) { + usleep(200000); + } + } else { + MOBase::log::warn("process runner: waitpid failed for pid {}: {}", pid, + errno); + } + + if (!core) { + return; + } + + QMetaObject::invokeMethod( + core, + [core, binary, exitCode]() { + if (core) { + core->afterRun(binary, exitCode); + } + }, + Qt::QueuedConnection); + }).detach(); + + log::debug("process runner: scheduled async post-run refresh for pid {}", pid); + } + return Running; + } + + if (!lockEnabled) { + log::debug("process runner: not waiting for process because " + "locking is disabled"); + + return ForceUnlocked; + } + } + + auto r = Error; + + if (mustWait && m_lockReason == UILocker::PreventExit && !lockEnabled) { + // This happens when running shortcuts and locking is disabled. MO must + // stay alive until all processes are dead or child processes may not get + // hooked properly, but the user has disabled locking the ui — so allow + // them to do that. MO runs in the background with no visual feedback. + r = waitForProcess(m_handle.get(), &m_exitCode, nullptr, expectedExecutables); + } else { + withLock([&](auto& ls) { + r = waitForProcess(m_handle.get(), &m_exitCode, &ls, expectedExecutables); + }); + } + + if (shouldRefresh(r)) { + QEventLoop loop; + const bool wait = m_waitFlags.testFlag(WaitForRefresh); + + if (wait) { + QObject::connect(&m_core, &OrganizerCore::directoryStructureReady, &loop, + &QEventLoop::quit, Qt::ConnectionType::QueuedConnection); + } + + m_core.afterRun(m_sp.binary, m_exitCode); + + if (wait) { + log::debug("process runner: waiting until refresh finishes"); + loop.exec(); + log::debug("process runner: refresh is done"); + } + } + + return r; +} + +ProcessRunner::Results ProcessRunner::attachToProcess(pid_t pid) +{ + m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(pid))); + return postRun(); +} + +DWORD ProcessRunner::exitCode() const +{ + return m_exitCode; +} + +pid_t ProcessRunner::getProcessHandle() const +{ + return static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get())); +} + +env::HandlePtr ProcessRunner::stealProcessHandle() +{ + auto h = m_handle.release(); + m_handle.reset(INVALID_HANDLE_VALUE); + return env::HandlePtr(h); +} + +void ProcessRunner::withLock(std::function<void(UILocker::Session&)> f) +{ + auto ls = UILocker::instance().lock(m_lockReason); + f(*ls); +} diff --git a/src/src/processrunner.h b/src/src/processrunner.h index 2736c86..76ed364 100644 --- a/src/src/processrunner.h +++ b/src/src/processrunner.h @@ -6,10 +6,8 @@ #include "uilocker.h"
#include <executableinfo.h>
-#ifndef _WIN32
#include <sys/types.h>
#include <sys/wait.h>
-#endif
class OrganizerCore;
class IUserInterface;
@@ -129,13 +127,9 @@ public: //
Results run();
- // takes ownership of the given handle and waits for it if required
+ // takes ownership of the given pid and waits for it if required
//
-#ifdef _WIN32
- Results attachToProcess(HANDLE h);
-#else
Results attachToProcess(pid_t pid);
-#endif
// exit code of the process, will return -1 if the process wasn't waited for
//
@@ -151,28 +145,13 @@ public: // note that the handle is still owned by this ProcessRunner and will be
// closed when destroyed; see stealProcessHandle()
//
-#ifdef _WIN32
- HANDLE getProcessHandle() const;
-#else
pid_t getProcessHandle() const;
-#endif
// 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();
-#ifdef _WIN32
- // 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);
-#endif
-
private:
OrganizerCore& m_core;
IUserInterface* m_ui;
diff --git a/src/src/profile.cpp b/src/src/profile.cpp index 5f96be7..cf74cad 100644 --- a/src/src/profile.cpp +++ b/src/src/profile.cpp @@ -45,10 +45,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QStringList> // for QStringList
#include <QtGlobal> // for qUtf8Printable
-#ifdef _WIN32
-#include <Windows.h>
-#endif
-
#include <assert.h> // for assert
#include <limits.h> // for UINT_MAX, INT_MAX, etc
#include <stddef.h> // for size_t
@@ -305,18 +301,11 @@ void Profile::createTweakedIniFile() mergeTweak(getProfileTweaks(), tweakedIni);
bool error = false;
-#ifdef _WIN32
- if (!MOBase::WriteRegistryValue(L"Archive", L"bInvalidateOlderFiles", L"1",
- ToWString(tweakedIni).c_str())) {
- error = true;
- }
-#else
if (!MOBase::WriteRegistryValue(
QStringLiteral("Archive"), QStringLiteral("bInvalidateOlderFiles"),
QStringLiteral("1"), tweakedIni)) {
error = true;
}
-#endif
if (error) {
const auto e = ::GetLastError();
@@ -779,64 +768,16 @@ std::vector<std::wstring> Profile::splitDZString(const wchar_t* buffer) const void Profile::mergeTweak(const QString& tweakName, const QString& tweakedIni) const
{
-#ifdef _WIN32
- static const int bufferSize = 32768;
-
- std::wstring tweakNameW = ToWString(tweakName);
- std::wstring tweakedIniW = ToWString(tweakedIni);
- QScopedArrayPointer<wchar_t> buffer(new wchar_t[bufferSize]);
-
- // retrieve a list of sections
- DWORD size =
- ::GetPrivateProfileSectionNamesW(buffer.data(), bufferSize, tweakNameW.c_str());
-
- if (size == bufferSize - 2) {
- // unfortunately there is no good way to find the required size
- // of the buffer
- throw MyException(QString("Buffer too small. Please report this as a bug. "
- "For now you might want to split up %1")
- .arg(tweakName));
- }
-
- std::vector<std::wstring> sections = splitDZString(buffer.data());
-
- // now iterate over all sections and retrieve a list of keys in each
- for (std::vector<std::wstring>::iterator iter = sections.begin();
- iter != sections.end(); ++iter) {
- // retrieve the names of all keys
- size = ::GetPrivateProfileStringW(iter->c_str(), nullptr, nullptr, buffer.data(),
- bufferSize, tweakNameW.c_str());
- if (size == bufferSize - 2) {
- throw MyException(QString("Buffer too small. Please report this as a bug. "
- "For now you might want to split up %1")
- .arg(tweakName));
- }
-
- std::vector<std::wstring> keys = splitDZString(buffer.data());
-
- for (std::vector<std::wstring>::iterator keyIter = keys.begin();
- keyIter != keys.end(); ++keyIter) {
- // TODO this treats everything as strings but how could I differentiate the type?
- ::GetPrivateProfileStringW(iter->c_str(), keyIter->c_str(), nullptr,
- buffer.data(), bufferSize,
- ToWString(tweakName).c_str());
- MOBase::WriteRegistryValue(iter->c_str(), keyIter->c_str(), buffer.data(),
- tweakedIniW.c_str());
- }
- }
-#else
- // On Linux, parse the tweak INI file line-by-line and merge each
- // key=value into the destination using WriteRegistryValue (which uses
- // the safe line-by-line writer that does NOT interpret backslashes as
- // line continuations or URL-encode spaces in keys).
+ // Parse the tweak INI file line-by-line and merge each key=value into the
+ // destination using WriteRegistryValue (which uses the safe line-by-line
+ // writer that does NOT interpret backslashes as line continuations or
+ // URL-encode spaces in keys).
QFile sourceFile(tweakName);
if (!sourceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
if (QFileInfo::exists(tweakName)) {
- log::warn("mergeTweak: tweak file '{}' exists but could not be opened",
- tweakName);
+ log::warn("mergeTweak: tweak file '{}' exists but could not be opened", tweakName);
} else {
- log::debug("mergeTweak: tweak file '{}' does not exist, skipping",
- tweakName);
+ log::debug("mergeTweak: tweak file '{}' does not exist, skipping", tweakName);
}
return;
}
@@ -859,7 +800,6 @@ void Profile::mergeTweak(const QString& tweakName, const QString& tweakedIni) co MOBase::WriteRegistryValue(currentSection, key, value, tweakedIni);
}
}
-#endif
}
void Profile::mergeTweaks(ModInfo::Ptr modInfo, const QString& tweakedIni) const
diff --git a/src/src/profilesdialog.cpp b/src/src/profilesdialog.cpp index 827eccd..4f9bc29 100644 --- a/src/src/profilesdialog.cpp +++ b/src/src/profilesdialog.cpp @@ -41,10 +41,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QMessageBox>
#include <QWhatsThis>
-#ifdef _WIN32
-#include <Windows.h>
-#endif
-
#include <exception>
using namespace MOBase;
diff --git a/src/src/sanitychecks.cpp b/src/src/sanitychecks.cpp index 77ab759..b18ea43 100644 --- a/src/src/sanitychecks.cpp +++ b/src/src/sanitychecks.cpp @@ -1,487 +1,101 @@ -#include "sanitychecks.h"
-#include "env.h"
-#include "envmodule.h"
-#include "settings.h"
-#include <iplugingame.h>
-#include <log.h>
-#include <utility.h>
-
-namespace sanity
-{
-
-using namespace MOBase;
-
-#ifdef _WIN32
-
-enum class SecurityZone
-{
- NoZone = -1,
- MyComputer = 0,
- Intranet = 1,
- Trusted = 2,
- Internet = 3,
- Untrusted = 4,
-};
-
-QString toCodeName(SecurityZone z)
-{
- switch (z) {
- case SecurityZone::NoZone:
- return "NoZone";
- case SecurityZone::MyComputer:
- return "MyComputer";
- case SecurityZone::Intranet:
- return "Intranet";
- case SecurityZone::Trusted:
- return "Trusted";
- case SecurityZone::Internet:
- return "Internet";
- case SecurityZone::Untrusted:
- return "Untrusted";
- default:
- return "Unknown zone";
- }
-}
-
-QString toString(SecurityZone z)
-{
- return QString("%1 (%2)").arg(toCodeName(z)).arg(static_cast<int>(z));
-}
-
-// whether the given zone is considered blocked
-//
-bool isZoneBlocked(SecurityZone z)
-{
- switch (z) {
- case SecurityZone::Internet:
- case SecurityZone::Untrusted:
- return true;
-
- case SecurityZone::NoZone:
- case SecurityZone::MyComputer:
- case SecurityZone::Intranet:
- case SecurityZone::Trusted:
- default:
- return false;
- }
-}
-
-// whether the given file is blocked
-//
-bool isFileBlocked(const QFileInfo& fi)
-{
- // name of the alternate data stream containing the zone identifier ini
- const QString ads = "Zone.Identifier";
-
- // key in the ini
- const auto key = "ZoneTransfer/ZoneId";
-
- // the path to the ADS is always `filename:Zone.Identifier`
- const auto path = fi.absoluteFilePath();
- const auto adsPath = path + ":" + ads;
-
- QFile f(adsPath);
- if (!f.exists()) {
- // no ADS for this file
- return false;
- }
-
- log::debug("'{}' has an ADS for {}", path, adsPath);
-
- const QSettings qs(adsPath, QSettings::IniFormat);
-
- // looking for key
- if (!qs.contains(key)) {
- log::debug("'{}': key '{}' not found", adsPath, key);
- return false;
- }
-
- // getting value
- const auto v = qs.value(key);
- if (v.isNull()) {
- log::debug("'{}': key '{}' is null", adsPath, key);
- return false;
- }
-
- // should be an int
- bool ok = false;
- const auto z = static_cast<SecurityZone>(v.toInt(&ok));
-
- if (!ok) {
- log::debug("'{}': key '{}' is not an int (value is '{}')", adsPath, key, v);
- return false;
- }
-
- if (!isZoneBlocked(z)) {
- // that zone is not a blocked zone
- log::debug("'{}': zone id is {}, which is fine", adsPath, toString(z));
- return false;
- }
-
- // file is blocked
- log::warn("{}", QObject::tr("'%1': file is blocked (%2)").arg(path).arg(toString(z)));
-
- return true;
-}
-
-int checkBlockedFiles(const QDir& dir)
-{
- // executables file types
- const QStringList FileTypes = {"*.dll", "*.exe"};
-
- if (!dir.exists()) {
- // shouldn't happen
- log::error("while checking for blocked files, directory '{}' not found",
- dir.absolutePath());
-
- return 1;
- }
-
- const auto files = dir.entryInfoList(FileTypes, QDir::Files);
- if (files.empty()) {
- // shouldn't happen
- log::error("while checking for blocked files, directory '{}' is empty",
- dir.absolutePath());
-
- return 1;
- }
-
- int n = 0;
-
- // checking each file in this directory
- for (auto&& fi : files) {
- if (isFileBlocked(fi)) {
- ++n;
- }
- }
-
- return n;
-}
-
-int checkBlocked()
-{
- // directories that contain executables; these need to be explicit because
- // portable instances might add billions of files in MO's directory
- const QString dirs[] = {".", "/dlls", "/loot", "/NCC", "/platforms", "/plugins"};
-
- log::debug(" . blocked files");
- const QString appDir = QCoreApplication::applicationDirPath();
-
- int n = 0;
-
- for (const auto& d : dirs) {
- const auto path = QDir(appDir + "/" + d).canonicalPath();
- n += checkBlockedFiles(path);
- }
-
- return n;
-}
-
-int checkMissingFiles()
-{
- // files that are likely to be eaten
-#ifdef _WIN32
- static const QStringList files(
- {"helper.exe", "nxmhandler.exe", "usvfs_proxy_x64.exe", "usvfs_proxy_x86.exe",
- "usvfs_x64.dll", "usvfs_x86.dll", "loot/loot.dll", "loot/lootcli.exe"});
-#else
- static const QStringList files({});
-#endif
-
- log::debug(" . missing files");
- const auto dir = QCoreApplication::applicationDirPath();
-
- int n = 0;
-
- for (const auto& name : files) {
- const QFileInfo file(dir + "/" + name);
-
- if (!file.exists()) {
- log::warn("{}", QObject::tr(
- "'%1' seems to be missing, an antivirus may have deleted it")
- .arg(file.absoluteFilePath()));
-
- ++n;
- }
- }
-
- return n;
-}
-
-int checkBadOSDs(const env::Module& m)
-{
- // these dlls seem to interfere mostly with dialogs, like the mod info
- // dialog: they render some dialogs fully white and make it impossible to
- // interact with them
- //
- // the dlls is usually loaded on startup, but there has been some reports
- // where they got loaded later, so this is also called every time a new module
- // is loaded into this process
-
- const std::string nahimic = "Nahimic (also known as SonicSuite, SonicRadar, "
- "SteelSeries, A-Volute, etc.)";
-
- auto p = [](std::string re, std::string s) {
- return std::make_pair(std::regex(re, std::regex::icase), s);
- };
-
- static const std::vector<std::pair<std::regex, std::string>> list = {
- p("nahimic(.*)osd\\.dll", nahimic),
- p("cassini(.*)osd\\.dll", nahimic),
- p(".+devprops.*.dll", nahimic),
- p("ss2osd\\.dll", nahimic),
- p("RTSSHooks64\\.dll", "RivaTuner Statistics Server"),
- p("SSAudioOSD\\.dll", "SteelSeries Audio"),
- p("specialk64\\.dll", "SpecialK"),
- p("corsairosdhook\\.x64\\.dll", "Corsair Utility Engine"),
- p("gtii-osd64-vk\\.dll", "ASUS GPU Tweak 2"),
- p("easyhook64\\.dll", "Razer Cortex"),
- p("k_fps64\\.dll", "Razer Cortex"),
- p("fw1fontwrapper\\.dll", "Gigabyte 3D OSD"),
- p("gfxhook64\\.dll", "Gigabyte 3D OSD")};
-
- const QFileInfo file(m.path());
- int n = 0;
-
- for (auto&& p : list) {
- std::smatch m;
- const auto filename = file.fileName().toStdString();
-
- if (std::regex_match(filename, m, p.first)) {
- log::warn("{}", QObject::tr(
- "%1 is loaded.\nThis program is known to cause issues with "
- "Mod Organizer, such as freezing or blank windows. Consider "
- "uninstalling it.")
- .arg(QString::fromStdString(p.second)));
-
- log::warn("{}", file.absoluteFilePath());
- ++n;
- }
- }
-
- return n;
-}
-
-int checkUsvfsIncompatibilites(const env::Module& m)
-{
- // these dlls seems to interfere with usvfs
-
- static const std::map<QString, QString> names = {
- {"mactype64.dll", "Mactype"}, {"epclient64.dll", "Citrix ICA Client"}};
-
- const QFileInfo file(m.path());
- int n = 0;
-
- for (auto&& p : names) {
- if (file.fileName().compare(p.first, Qt::CaseInsensitive) == 0) {
- log::warn(
- "{}",
- QObject::tr("%1 is loaded. This program is known to cause issues with "
- "Mod Organizer and its virtual filesystem, such script extenders "
- "or others programs refusing to run. Consider uninstalling it.")
- .arg(p.second));
-
- log::warn("{}", file.absoluteFilePath());
-
- ++n;
- }
- }
-
- return n;
-}
-
-int checkIncompatibleModule(const env::Module& m)
-{
- int n = 0;
-
- n += checkBadOSDs(m);
- n += checkUsvfsIncompatibilites(m);
-
- return n;
-}
-
-int checkIncompatibilities(const env::Environment& e)
-{
- log::debug(" . incompatibilities");
-
- int n = 0;
-
- for (auto&& m : e.loadedModules()) {
- n += checkIncompatibleModule(m);
- }
-
- return n;
-}
-
-std::vector<std::pair<QString, QString>> getSystemDirectories()
-{
- // folder ids and display names for logging
- const std::vector<std::pair<GUID, QString>> systemFolderIDs = {
- {FOLDERID_ProgramFiles, "in Program Files"},
- {FOLDERID_ProgramFilesX86, "in Program Files"},
- {FOLDERID_Desktop, "on the desktop"},
- {FOLDERID_OneDrive, "in OneDrive"},
- {FOLDERID_Documents, "in Documents"},
- {FOLDERID_Downloads, "in Downloads"}};
-
- std::vector<std::pair<QString, QString>> systemDirs;
-
- for (auto&& p : systemFolderIDs) {
- const auto dir = MOBase::getOptionalKnownFolder(p.first);
-
- if (!dir.isEmpty()) {
- auto path = QDir::toNativeSeparators(dir).toLower();
- if (!path.endsWith("\\")) {
- path += "\\";
- }
-
- systemDirs.push_back({path, p.second});
- }
- }
-
- return systemDirs;
-}
-
-int checkProtected(const QDir& d, const QString& what)
-{
- static const auto systemDirs = getSystemDirectories();
-
- const auto path = QDir::toNativeSeparators(d.absolutePath()).toLower();
-
- log::debug(" . {}: {}", what, path);
-
- for (auto&& sd : systemDirs) {
- if (path.startsWith(sd.first)) {
- log::warn("{} is {}; this may cause issues because it's a special "
- "system folder",
- what, sd.second);
-
- log::debug("path '{}' starts with '{}'", path, sd.first);
-
- return 1;
- }
- }
-
- return 0;
-}
-
-int checkMicrosoftStore(const QDir& gameDir)
-{
- const QStringList pathsToCheck = {
- "/ModifiableWindowsApps/",
- "/WindowsApps/",
- };
- for (auto badPath : pathsToCheck) {
- if (gameDir.path().contains(badPath)) {
- log::error("This game is not supported by Mod Organizer.");
- log::error("Games installed through the Microsoft Store will not work properly.");
- return 1;
- }
- }
-
- return 0;
-}
-
-#else // Linux
-
-int checkMissingFiles()
-{
- log::debug(" . checking Linux dependencies");
- int n = 0;
-
- // Check for FUSE (check both unversioned .so and versioned .so.3, since
- // on Debian/Ubuntu the unversioned symlink is only in libfuse3-dev)
- {
- static const char* fusePaths[] = {
- "/usr/lib/libfuse3.so",
- "/usr/lib/libfuse3.so.3",
- "/usr/lib64/libfuse3.so",
- "/usr/lib64/libfuse3.so.3",
- "/usr/lib/x86_64-linux-gnu/libfuse3.so",
- "/usr/lib/x86_64-linux-gnu/libfuse3.so.3",
- nullptr
- };
- bool fuseFound = false;
- for (int i = 0; fusePaths[i]; ++i) {
- if (QFileInfo::exists(fusePaths[i])) {
- fuseFound = true;
- break;
- }
- }
- if (!fuseFound) {
- log::warn("libfuse3 not found - FUSE VFS will not work");
- ++n;
- }
- }
-
- return n;
-}
-
-int checkIncompatibleModule(const env::Module& /*m*/)
-{
- // No known incompatible modules on Linux
- return 0;
-}
-
-int checkProtected(const QDir& d, const QString& what)
-{
- const auto path = d.absolutePath();
- log::debug(" . {}: {}", what, path);
-
- // Check if running from system directories
- if (path.startsWith("/root") || path.startsWith("/usr") || path.startsWith("/bin")) {
- log::warn("{} is in a system directory; this may cause permission issues", what);
- return 1;
- }
-
- return 0;
-}
-
-#endif // _WIN32
-
-int checkPaths(IPluginGame& game, const Settings& s)
-{
- log::debug("checking paths");
-
- int n = 0;
-
- n += checkProtected(game.gameDirectory(), "the game");
-#ifdef _WIN32
- n += checkMicrosoftStore(game.gameDirectory());
-#endif
- n += checkProtected(QApplication::applicationDirPath(), "Mod Organizer");
-
- if (checkProtected(s.paths().base(), "the instance base directory")) {
- ++n;
- } else {
- n += checkProtected(s.paths().downloads(), "the downloads directory");
- n += checkProtected(s.paths().mods(), "the mods directory");
- n += checkProtected(s.paths().cache(), "the cache directory");
- n += checkProtected(s.paths().profiles(), "the profiles directory");
- n += checkProtected(s.paths().overwrite(), "the overwrite directory");
- }
-
- return n;
-}
-
-void checkEnvironment(const env::Environment& e)
-{
- log::debug("running sanity checks...");
-
- int n = 0;
-
-#ifdef _WIN32
- n += checkBlocked();
-#endif
- n += checkMissingFiles();
-#ifdef _WIN32
- n += checkIncompatibilities(e);
-#else
- Q_UNUSED(e);
-#endif
-
- log::debug("sanity checks done, {}",
- (n > 0 ? "problems were found" : "everything looks okay"));
-}
-
-} // namespace sanity
+#include "sanitychecks.h" +#include "env.h" +#include "envmodule.h" +#include "settings.h" + +#include <iplugingame.h> +#include <log.h> +#include <utility.h> + +namespace sanity +{ + +using namespace MOBase; + +int checkMissingFiles() +{ + log::debug(" . checking Linux dependencies"); + int n = 0; + + // Check for FUSE (check both unversioned .so and versioned .so.3 — on + // Debian/Ubuntu the unversioned symlink is only in libfuse3-dev). + static const char* fusePaths[] = { + "/usr/lib/libfuse3.so", + "/usr/lib/libfuse3.so.3", + "/usr/lib64/libfuse3.so", + "/usr/lib64/libfuse3.so.3", + "/usr/lib/x86_64-linux-gnu/libfuse3.so", + "/usr/lib/x86_64-linux-gnu/libfuse3.so.3", + nullptr}; + + bool fuseFound = false; + for (int i = 0; fusePaths[i]; ++i) { + if (QFileInfo::exists(fusePaths[i])) { + fuseFound = true; + break; + } + } + if (!fuseFound) { + log::warn("libfuse3 not found - FUSE VFS will not work"); + ++n; + } + + return n; +} + +int checkIncompatibleModule(const env::Module& /*m*/) +{ + // No Wine/Linux equivalent of the Win32 OSD/usvfs incompatibility list. + return 0; +} + +int checkProtected(const QDir& d, const QString& what) +{ + const auto path = d.absolutePath(); + log::debug(" . {}: {}", what, path); + + // Warn if running from a system-owned directory. + if (path.startsWith("/root") || path.startsWith("/usr") || path.startsWith("/bin")) { + log::warn("{} is in a system directory; this may cause permission issues", what); + return 1; + } + + return 0; +} + +int checkPaths(IPluginGame& game, const Settings& s) +{ + log::debug("checking paths"); + + int n = 0; + + n += checkProtected(game.gameDirectory(), "the game"); + n += checkProtected(QApplication::applicationDirPath(), "Mod Organizer"); + + if (checkProtected(s.paths().base(), "the instance base directory")) { + ++n; + } else { + n += checkProtected(s.paths().downloads(), "the downloads directory"); + n += checkProtected(s.paths().mods(), "the mods directory"); + n += checkProtected(s.paths().cache(), "the cache directory"); + n += checkProtected(s.paths().profiles(), "the profiles directory"); + n += checkProtected(s.paths().overwrite(), "the overwrite directory"); + } + + return n; +} + +void checkEnvironment(const env::Environment& e) +{ + log::debug("running sanity checks..."); + + int n = 0; + + n += checkMissingFiles(); + Q_UNUSED(e); + + log::debug("sanity checks done, {}", + (n > 0 ? "problems were found" : "everything looks okay")); +} + +} // namespace sanity diff --git a/src/src/savestab.cpp b/src/src/savestab.cpp index 0ea76c1..734f88d 100644 --- a/src/src/savestab.cpp +++ b/src/src/savestab.cpp @@ -246,16 +246,6 @@ QDir SavesTab::currentSavesDir() const QString iniPath = m_core.currentProfile()->absoluteIniFilePath(iniFiles[0]); -#ifdef _WIN32 - wchar_t path[MAX_PATH]; - if (::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"", path, MAX_PATH, - iniPath.toStdWString().c_str())) { - savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath( - QString::fromWCharArray(path))); - } else { - savesDir = m_core.managedGame()->savesDirectory(); - } -#else // Read directly without QSettings — QSettings::IniFormat interprets // trailing backslashes as line continuations, corrupting values like // "sLocalSavePath=__MO_Saves\". @@ -265,12 +255,13 @@ QDir SavesTab::currentSavesDir() const while (savePath.endsWith('\\') || savePath.endsWith('/')) { savePath.chop(1); } - if (!savePath.isEmpty() && savePath.compare("__MO_Saves", Qt::CaseInsensitive) != 0) { - savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(savePath)); + if (!savePath.isEmpty() && + savePath.compare("__MO_Saves", Qt::CaseInsensitive) != 0) { + savesDir.setPath( + m_core.managedGame()->documentsDirectory().absoluteFilePath(savePath)); } else { savesDir = m_core.managedGame()->savesDirectory(); } -#endif } return savesDir; diff --git a/src/src/selfupdater.cpp b/src/src/selfupdater.cpp index 556e40e..d33bc00 100644 --- a/src/src/selfupdater.cpp +++ b/src/src/selfupdater.cpp @@ -58,10 +58,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <boost/bind/bind.hpp>
-#ifdef _WIN32
-#include <Windows.h> //for VS_FIXEDFILEINFO, GetLastError
-#endif
-
#include <exception>
#include <map>
#include <stddef.h> //for size_t
diff --git a/src/src/settings.cpp b/src/src/settings.cpp index dfe7591..a98f609 100644 --- a/src/src/settings.cpp +++ b/src/src/settings.cpp @@ -2045,31 +2045,9 @@ void NexusSettings::setCategoryMappings(bool b) const void NexusSettings::registerAsNXMHandler(bool force)
{
-#ifndef _WIN32
Q_UNUSED(force);
NxmHandlerLinux handler;
handler.registerHandler();
-#else
- const auto nxmPath = QCoreApplication::applicationDirPath() + "/" +
- QString::fromStdWString(AppConfig::nxmHandlerExe());
-
- const auto executable = QCoreApplication::applicationFilePath();
-
- QString mode = force ? "forcereg" : "reg";
- QString parameters = mode + " " + m_Parent.game().plugin()->gameShortName();
- for (const QString& altGame : m_Parent.game().plugin()->validShortNames()) {
- parameters += "," + altGame;
- }
- parameters += " \"" + executable + "\"";
-
- const auto r = shell::Execute(nxmPath, parameters);
-
- if (!r.success()) {
- QMessageBox::critical(
- nullptr, QObject::tr("Failed"),
- QObject::tr("Failed to start the helper application: %1").arg(r.toString()));
- }
-#endif
}
std::vector<std::chrono::seconds> NexusSettings::validationTimeouts() const
diff --git a/src/src/settingsdialogtheme.cpp b/src/src/settingsdialogtheme.cpp index 8d41d35..a88396b 100644 --- a/src/src/settingsdialogtheme.cpp +++ b/src/src/settingsdialogtheme.cpp @@ -5,11 +5,10 @@ #include "modlist.h"
#include "shared/appconfig.h"
#include "ui_settingsdialog.h"
+#include "fluorinepaths.h"
+
#include <questionboxmemory.h>
#include <utility.h>
-#ifndef _WIN32
-#include "fluorinepaths.h"
-#endif
using namespace MOBase;
@@ -61,7 +60,6 @@ void ThemeSettingsTab::addStyles() const QString ssSubdir = QString::fromStdWString(AppConfig::stylesheetsPath());
QStringList searchDirs;
searchDirs << QCoreApplication::applicationDirPath() + "/" + ssSubdir;
-#ifndef _WIN32
if (auto ci = InstanceManager::singleton().currentInstance()) {
// currentInstance() returns a bare Instance (readFromIni() not called),
// so baseDirectory() is empty. Use directory() which is always set.
@@ -72,7 +70,6 @@ void ThemeSettingsTab::addStyles() const QString userDir = fluorineDataDir() + "/stylesheets";
if (!searchDirs.contains(userDir))
searchDirs << userDir;
-#endif
QSet<QString> seen;
for (const auto& dir : searchDirs) {
@@ -100,21 +97,15 @@ void ThemeSettingsTab::selectStyle() void ThemeSettingsTab::onExploreStyles()
{
- // On Linux, open the instance's stylesheets directory (where custom themes
- // from modlists live). Falls back to the user data dir if no instance, or
- // to the app dir on Windows.
-#ifndef _WIN32
+ // Open the instance's stylesheets directory (where custom themes from
+ // modlists live), or the user data dir as fallback.
QString ssPath;
if (auto ci = InstanceManager::singleton().currentInstance()) {
- ssPath = ci->directory() + "/" +
- QString::fromStdWString(AppConfig::stylesheetsPath());
+ ssPath =
+ ci->directory() + "/" + QString::fromStdWString(AppConfig::stylesheetsPath());
} else {
ssPath = fluorineDataDir() + "/stylesheets";
}
QDir().mkpath(ssPath);
-#else
- QString ssPath = QCoreApplication::applicationDirPath() + "/" +
- ToQString(AppConfig::stylesheetsPath());
-#endif
shell::Explore(ssPath);
}
diff --git a/src/src/settingsdialogworkarounds.cpp b/src/src/settingsdialogworkarounds.cpp index 658ae7d..2f8360c 100644 --- a/src/src/settingsdialogworkarounds.cpp +++ b/src/src/settingsdialogworkarounds.cpp @@ -213,15 +213,10 @@ void WorkaroundsSettingsTab::on_skipDirectoriesBtn_clicked() void WorkaroundsSettingsTab::on_bsaDateBtn_clicked()
{
const auto* game = qApp->property("managed_game").value<MOBase::IPluginGame*>();
- QDir dir = game->dataDirectory();
-
-#ifdef _WIN32
- helper::backdateBSAs(parentWidget(), qApp->applicationDirPath().toStdWString(),
- dir.absolutePath().toStdWString());
-#else
- // backdateBSAs is Windows-only (uses helper.exe for admin rights)
+ // BSA backdating used the Win32 helper.exe for admin rights — the equivalent
+ // on Linux would just touch the files but it's never been wired up.
+ QDir dir = game->dataDirectory();
Q_UNUSED(dir);
-#endif
}
void WorkaroundsSettingsTab::on_resetGeometryBtn_clicked()
diff --git a/src/src/settingsutilities.cpp b/src/src/settingsutilities.cpp index 5e7d3e2..1f7d0d8 100644 --- a/src/src/settingsutilities.cpp +++ b/src/src/settingsutilities.cpp @@ -1,11 +1,10 @@ #include "settingsutilities.h"
#include "expanderwidget.h"
+
#include <utility.h>
-#ifndef _WIN32
#include <QDir>
#include <QSettings>
-#endif
using namespace MOBase;
@@ -215,117 +214,9 @@ QString credentialName(const QString& key) return "ModOrganizer2_" + key;
}
-#ifdef _WIN32
-bool deleteWindowsCredential(const QString& key)
-{
- const auto credName = credentialName(key);
-
- if (!CredDeleteW(credName.toStdWString().c_str(), CRED_TYPE_GENERIC, 0)) {
- const auto e = GetLastError();
-
- // not an error if the key already doesn't exist, and don't log it because
- // it happens all the time when the settings dialog is closed since it
- // doesn't check first
- if (e == ERROR_NOT_FOUND) {
- return true;
- }
-
- log::error("failed to delete windows credential {}, {}", credName,
- formatSystemMessage(e));
- return false;
- }
-
- log::debug("deleted windows credential {}", credName);
-
- return true;
-}
-
-bool addWindowsCredential(const QString& key, const QString& data)
-{
- const auto credName = credentialName(key);
-
- const auto wname = credName.toStdWString();
- const auto wdata = data.toStdWString();
-
- const auto* blob = reinterpret_cast<const BYTE*>(wdata.data());
- const auto blobSize = wdata.size() * sizeof(decltype(wdata)::value_type);
-
- CREDENTIALW cred = {};
- cred.Flags = 0;
- cred.Type = CRED_TYPE_GENERIC;
- cred.TargetName = const_cast<wchar_t*>(wname.c_str());
- cred.CredentialBlob = const_cast<BYTE*>(blob);
- cred.CredentialBlobSize = static_cast<DWORD>(blobSize);
- cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
-
- if (!CredWriteW(&cred, 0)) {
- const auto e = GetLastError();
-
- log::error("failed to delete windows credential {}, {}", credName,
- formatSystemMessage(e));
-
- return false;
- }
-
- log::debug("set windows credential {}", credName);
-
- return true;
-}
-
-struct CredentialFreer
-{
- void operator()(CREDENTIALW* c)
- {
- if (c) {
- CredFree(c);
- }
- }
-};
-
-using CredentialPtr = std::unique_ptr<CREDENTIALW, CredentialFreer>;
-
-QString getWindowsCredential(const QString& key)
-{
- const QString credName = credentialName(key);
-
- CREDENTIALW* rawCreds = nullptr;
-
- const auto ret =
- CredReadW(credName.toStdWString().c_str(), CRED_TYPE_GENERIC, 0, &rawCreds);
-
- CredentialPtr creds(rawCreds);
-
- if (!ret) {
- const auto e = GetLastError();
-
- if (e != ERROR_NOT_FOUND) {
- log::error("failed to retrieve windows credential {}: {}", credName,
- formatSystemMessage(e));
- }
-
- return {};
- }
-
- QString value;
- if (creds->CredentialBlob) {
- value =
- QString::fromWCharArray(reinterpret_cast<const wchar_t*>(creds->CredentialBlob),
- creds->CredentialBlobSize / sizeof(wchar_t));
- }
-
- return value;
-}
-
-bool setWindowsCredential(const QString& key, const QString& data)
-{
- if (data.isEmpty()) {
- return deleteWindowsCredential(key);
- } else {
- return addWindowsCredential(key, data);
- }
-}
-#else
-// Linux stub: use QSettings for credential storage instead of Windows Credential Manager
+// Linux uses an unencrypted QSettings file for credentials. The Win32
+// Credential Manager equivalent (libsecret/kwallet) is not wired up; this
+// matches the upstream MO2 behaviour that already lived in the #else branch.
static QSettings& credentialSettings()
{
static QSettings s(QDir::homePath() + "/.config/ModOrganizer/credentials.ini",
@@ -347,4 +238,3 @@ bool setWindowsCredential(const QString& key, const QString& data) }
return true;
}
-#endif
diff --git a/src/src/shared/appconfig.cpp b/src/src/shared/appconfig.cpp index 579318f..8fe9b30 100644 --- a/src/src/shared/appconfig.cpp +++ b/src/src/shared/appconfig.cpp @@ -37,34 +37,28 @@ namespace AppConfig QString basePath()
{
-#ifndef _WIN32
const char* envBase = std::getenv("MO2_BASE_DIR");
if (envBase && envBase[0] != '\0') {
return QString::fromUtf8(envBase);
}
-#endif
return QCoreApplication::applicationDirPath();
}
QString pluginsPath()
{
-#ifndef _WIN32
const char* envDir = std::getenv("MO2_PLUGINS_DIR");
if (envDir && envDir[0] != '\0') {
return QString::fromUtf8(envDir);
}
-#endif
return basePath() + "/" + QString::fromStdWString(pluginPath());
}
QString dllsPath()
{
-#ifndef _WIN32
const char* envDir = std::getenv("MO2_DLLS_DIR");
if (envDir && envDir[0] != '\0') {
return QString::fromUtf8(envDir);
}
-#endif
return basePath() + "/dlls";
}
diff --git a/src/src/shared/directoryentry.cpp b/src/src/shared/directoryentry.cpp index 3f9eeef..43588a4 100644 --- a/src/src/shared/directoryentry.cpp +++ b/src/src/shared/directoryentry.cpp @@ -52,39 +52,15 @@ void elapsedImpl(std::chrono::nanoseconds& out, F&& f) #define elapsed(OUT, F) (F)();
// #define elapsed(OUT, F) elapsedImpl(OUT, F);
-#ifdef _WIN32
static bool SupportOptimizedFind()
{
- // large fetch and basic info for FindFirstFileEx is supported on win server 2008 r2,
- // win 7 and newer
-
- OSVERSIONINFOEX versionInfo;
- versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
- versionInfo.dwMajorVersion = 6;
- versionInfo.dwMinorVersion = 1;
-
- ULONGLONG mask = ::VerSetConditionMask(
- ::VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL), VER_MINORVERSION,
- VER_GREATER_EQUAL);
-
- return (::VerifyVersionInfo(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION,
- mask) == TRUE);
+ return true;
}
-#else
-static bool SupportOptimizedFind()
-{
- return true; // Always true on Linux
-}
-#endif
bool DirCompareByName::operator()(const DirectoryEntry* lhs,
const DirectoryEntry* rhs) const
{
-#ifdef _WIN32
- return _wcsicmp(lhs->getName().c_str(), rhs->getName().c_str()) < 0;
-#else
return wcscasecmp(lhs->getName().c_str(), rhs->getName().c_str()) < 0;
-#endif
}
DirectoryEntry::DirectoryEntry(std::wstring name, DirectoryEntry* parent, int originID)
@@ -642,14 +618,10 @@ void DirectoryEntry::addFiles(env::DirectoryWalker& walker, FilesOrigin& origin, Context cx = {origin, stats};
cx.current.push(this);
-#ifdef _WIN32
- const bool pathExists = std::filesystem::exists(path);
-#else
- // On Linux, convert wstring to narrow string for std::filesystem
- // to avoid locale-dependent wchar_t conversion issues
+ // Convert wstring to narrow string for std::filesystem to avoid
+ // locale-dependent wchar_t conversion issues on glibc.
const bool pathExists =
std::filesystem::exists(QString::fromStdWString(path).toStdString());
-#endif
if (pathExists) {
walker.forEachEntry(
@@ -925,21 +897,12 @@ struct DumpFailed : public std::runtime_error void DirectoryEntry::dump(const std::wstring& file) const
{
try {
-#ifdef _WIN32
- std::FILE* f = nullptr;
- auto e = _wfopen_s(&f, file.c_str(), L"wb");
-
- if (e != 0 || !f) {
- throw DumpFailed(std::format("failed to open, {} ({})", std::strerror(e), e));
- }
-#else
std::string narrowFile(file.begin(), file.end());
std::FILE* f = std::fopen(narrowFile.c_str(), "wb");
if (!f) {
auto e = errno;
throw DumpFailed(std::format("failed to open, {} ({})", std::strerror(e), e));
}
-#endif
Guard g([&] {
std::fclose(f);
diff --git a/src/src/shared/util.cpp b/src/src/shared/util.cpp index 71e735e..603cad2 100644 --- a/src/src/shared/util.cpp +++ b/src/src/shared/util.cpp @@ -1,502 +1,270 @@ -/*
-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 "util.h"
-#include "../env.h"
-#include "../mainwindow.h"
-#include "windows_error.h"
-#include <fluorine_build_info.h>
-#include <uibase/log.h>
-#ifndef _WIN32
-#include <pthread.h>
-#include <filesystem>
-#include <algorithm>
-#include <cwctype>
-#endif
-
-using namespace MOBase;
-
-namespace MOShared
-{
-
-#ifdef _WIN32
-bool FileExists(const std::string& filename)
-{
- DWORD dwAttrib = ::GetFileAttributesA(filename.c_str());
-
- return (dwAttrib != INVALID_FILE_ATTRIBUTES);
-}
-
-bool FileExists(const std::wstring& filename)
-{
- DWORD dwAttrib = ::GetFileAttributesW(filename.c_str());
-
- return (dwAttrib != INVALID_FILE_ATTRIBUTES);
-}
-#else
-bool FileExists(const std::string& filename)
-{
- return std::filesystem::exists(filename);
-}
-
-bool FileExists(const std::wstring& filename)
-{
- return std::filesystem::exists(std::filesystem::path(filename));
-}
-#endif
-
-bool FileExists(const std::wstring& searchPath, const std::wstring& filename)
-{
- std::wstringstream stream;
- stream << searchPath << "\\" << filename;
- return FileExists(stream.str());
-}
-
-#ifdef _WIN32
-std::string ToString(const std::wstring& source, bool utf8)
-{
- std::string result;
- if (source.length() > 0) {
- UINT codepage = CP_UTF8;
- if (!utf8) {
- codepage = AreFileApisANSI() ? GetACP() : GetOEMCP();
- }
- int sizeRequired = ::WideCharToMultiByte(codepage, 0, &source[0], -1, nullptr, 0,
- nullptr, nullptr);
- if (sizeRequired == 0) {
- throw windows_error("failed to convert string to multibyte");
- }
- // the size returned by WideCharToMultiByte contains zero termination IF -1 is
- // specified for the length. we don't want that \0 in the string because then the
- // length field would be wrong. Because madness
- result.resize(sizeRequired - 1, '\0');
- ::WideCharToMultiByte(codepage, 0, &source[0], (int)source.size(), &result[0],
- sizeRequired, nullptr, nullptr);
- }
-
- return result;
-}
-
-std::wstring ToWString(const std::string& source, bool utf8)
-{
- std::wstring result;
- if (source.length() > 0) {
- UINT codepage = CP_UTF8;
- if (!utf8) {
- codepage = AreFileApisANSI() ? GetACP() : GetOEMCP();
- }
- int sizeRequired = ::MultiByteToWideChar(
- codepage, 0, source.c_str(), static_cast<int>(source.length()), nullptr, 0);
- if (sizeRequired == 0) {
- throw windows_error("failed to convert string to wide character");
- }
- result.resize(sizeRequired, L'\0');
- ::MultiByteToWideChar(codepage, 0, source.c_str(),
- static_cast<int>(source.length()), &result[0], sizeRequired);
- }
-
- return result;
-}
-#else
-std::string ToString(const std::wstring& source, bool utf8)
-{
- Q_UNUSED(utf8);
- // On Linux, use Qt for wstring -> string conversion (always UTF-8)
- return QString::fromStdWString(source).toStdString();
-}
-
-std::wstring ToWString(const std::string& source, bool utf8)
-{
- Q_UNUSED(utf8);
- // On Linux, use Qt for string -> wstring conversion (always UTF-8)
- return QString::fromStdString(source).toStdWString();
-}
-#endif
-
-static std::locale makeUserLocale()
-{
- // std::locale("") reads LANG/LC_* env vars. If user's system lacks the
- // requested locale (e.g. en_US.UTF-8 not generated), constructor throws
- // runtime_error. Fall back to "C" so startup doesn't abort.
- try {
- return std::locale("");
- } catch (const std::runtime_error&) {
- return std::locale::classic();
- }
-}
-
-static std::locale loc = makeUserLocale();
-static auto locToLowerW = [](wchar_t in) -> wchar_t {
- return std::tolower(in, loc);
-};
-
-static auto locToLower = [](char in) -> char {
- return std::tolower(in, loc);
-};
-
-#ifdef _WIN32
-std::string& ToLowerInPlace(std::string& text)
-{
- CharLowerBuffA(const_cast<CHAR*>(text.c_str()), static_cast<DWORD>(text.size()));
- return text;
-}
-
-std::string ToLowerCopy(const std::string& text)
-{
- std::string result(text);
- CharLowerBuffA(const_cast<CHAR*>(result.c_str()), static_cast<DWORD>(result.size()));
- return result;
-}
-
-std::wstring& ToLowerInPlace(std::wstring& text)
-{
- CharLowerBuffW(const_cast<WCHAR*>(text.c_str()), static_cast<DWORD>(text.size()));
- return text;
-}
-
-std::wstring ToLowerCopy(const std::wstring& text)
-{
- std::wstring result(text);
- CharLowerBuffW(const_cast<WCHAR*>(result.c_str()), static_cast<DWORD>(result.size()));
- return result;
-}
-#else
-std::string& ToLowerInPlace(std::string& text)
-{
- std::transform(text.begin(), text.end(), text.begin(), [](char c) {
- return std::tolower(static_cast<unsigned char>(c));
- });
- return text;
-}
-
-std::string ToLowerCopy(const std::string& text)
-{
- std::string result(text);
- return ToLowerInPlace(result);
-}
-
-std::wstring& ToLowerInPlace(std::wstring& text)
-{
- std::transform(text.begin(), text.end(), text.begin(), [](wchar_t c) {
- return std::towlower(c);
- });
- return text;
-}
-
-std::wstring ToLowerCopy(const std::wstring& text)
-{
- std::wstring result(text);
- return ToLowerInPlace(result);
-}
-#endif
-
-std::wstring ToLowerCopy(std::wstring_view text)
-{
- std::wstring result(text.begin(), text.end());
- ToLowerInPlace(result);
- return result;
-}
-
-bool CaseInsenstiveComparePred(wchar_t lhs, wchar_t rhs)
-{
- return std::tolower(lhs, loc) == std::tolower(rhs, loc);
-}
-
-bool CaseInsensitiveEqual(const std::wstring& lhs, const std::wstring& rhs)
-{
- return (lhs.length() == rhs.length()) &&
- std::equal(lhs.begin(), lhs.end(), rhs.begin(),
- [](wchar_t lhs, wchar_t rhs) -> bool {
- return std::tolower(lhs, loc) == std::tolower(rhs, loc);
- });
-}
-
-#ifdef _WIN32
-VS_FIXEDFILEINFO GetFileVersion(const std::wstring& fileName)
-{
- DWORD handle = 0UL;
- DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle);
- if (size == 0) {
- throw windows_error("failed to determine file version info size");
- }
-
- boost::scoped_array<char> buffer(new char[size]);
- try {
- handle = 0UL;
- if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.get())) {
- throw windows_error("failed to determine file version info");
- }
-
- void* versionInfoPtr = nullptr;
- UINT versionInfoLength = 0;
- if (!::VerQueryValue(buffer.get(), L"\\", &versionInfoPtr, &versionInfoLength)) {
- throw windows_error("failed to determine file version");
- }
-
- VS_FIXEDFILEINFO result = *(VS_FIXEDFILEINFO*)versionInfoPtr;
- return result;
- } catch (...) {
- throw;
- }
-}
-
-std::wstring GetFileVersionString(const std::wstring& fileName)
-{
- DWORD handle = 0UL;
- DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle);
- if (size == 0) {
- throw windows_error("failed to determine file version info size");
- }
-
- boost::scoped_array<char> buffer(new char[size]);
- try {
- handle = 0UL;
- if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.get())) {
- throw windows_error("failed to determine file version info");
- }
-
- LPVOID strBuffer = nullptr;
- UINT strLength = 0;
- if (!::VerQueryValue(buffer.get(), L"\\StringFileInfo\\040904B0\\ProductVersion",
- &strBuffer, &strLength)) {
- throw windows_error("failed to determine file version");
- }
-
- return std::wstring((LPCTSTR)strBuffer);
- } catch (...) {
- throw;
- }
-}
-
-Version createVersionInfo()
-{
- VS_FIXEDFILEINFO version = GetFileVersion(env::thisProcessPath().native());
-
- std::optional<Version::ReleaseType> releaseType;
-
- if (version.dwFileFlags & VS_FF_PRERELEASE) {
- // Pre-release builds need annotating
- QString versionString =
- QString::fromStdWString(GetFileVersionString(env::thisProcessPath().native()));
-
- // The pre-release flag can be set without the string specifying what type of
- // pre-release
- bool noLetters = true;
- for (QChar character : versionString) {
- if (character.isLetter()) {
- noLetters = false;
- break;
- }
- }
-
- if (!noLetters) {
- // trust the string to make sense
- return Version::parse(versionString, Version::ParseMode::MO2);
- }
-
- if (noLetters) {
- // default to development when release type is unspecified
- releaseType = Version::Development;
- } else {
- }
- }
-
- const int major = version.dwFileVersionMS >> 16,
- minor = version.dwFileVersionMS & 0xFFFF,
- patch = version.dwFileVersionLS >> 16,
- subpatch = version.dwFileVersionLS & 0xFFFF;
-
- std::vector<std::variant<int, Version::ReleaseType>> prereleases;
- if (releaseType) {
- prereleases.push_back(*releaseType);
- }
-
- return Version(major, minor, patch, subpatch, std::move(prereleases));
-}
-#else
-Version createVersionInfo()
-{
- // Fluorine Manager version is the user-facing one. The numeric components
- // are injected by CMake from top-level FLUORINE_VERSION_* variables.
-#if FLUORINE_IS_BETA_BUILD
- // Beta builds tag themselves as Development pre-releases so the update
- // checker can distinguish them from stable tags when comparing versions.
- return Version(FLUORINE_VERSION_MAJOR, FLUORINE_VERSION_MINOR,
- FLUORINE_VERSION_PATCH, 0,
- {Version::Development});
-#else
- return Version(FLUORINE_VERSION_MAJOR, FLUORINE_VERSION_MINOR,
- FLUORINE_VERSION_PATCH, 0);
-#endif
-}
-#endif
-
-
-#ifdef _WIN32
-void SetThisThreadName(const QString& s)
-{
- using SetThreadDescriptionType = HRESULT(HANDLE hThread, PCWSTR lpThreadDescription);
-
- static SetThreadDescriptionType* SetThreadDescription = [] {
- SetThreadDescriptionType* p = nullptr;
-
- env::LibraryPtr kernel32(LoadLibraryW(L"kernel32.dll"));
- if (!kernel32) {
- return p;
- }
-
- p = reinterpret_cast<SetThreadDescriptionType*>(
- GetProcAddress(kernel32.get(), "SetThreadDescription"));
-
- return p;
- }();
-
- if (SetThreadDescription) {
- SetThreadDescription(GetCurrentThread(), s.toStdWString().c_str());
- }
-}
-#else
-void SetThisThreadName(const QString& s)
-{
- // On Linux, use pthread_setname_np (limited to 16 chars including null)
- std::string name = s.toStdString();
- if (name.size() > 15) {
- name.resize(15);
- }
- pthread_setname_np(pthread_self(), name.c_str());
-}
-#endif
-
-char shortcutChar(const QAction* a)
-{
- const auto text = a->text();
- char shortcut = 0;
-
- for (int i = 0; i < text.size(); ++i) {
- const auto c = text[i];
- if (c == '&') {
- if (i >= (text.size() - 1)) {
- log::error("ampersand at the end");
- return 0;
- }
-
- return text[i + 1].toLatin1();
- }
- }
-
- log::error("action {} has no shortcut", text);
- return 0;
-}
-
-void checkDuplicateShortcuts(const QMenu& m)
-{
- const auto actions = m.actions();
-
- for (int i = 0; i < actions.size(); ++i) {
- const auto* action1 = actions[i];
- if (action1->isSeparator()) {
- continue;
- }
-
- const char shortcut1 = shortcutChar(action1);
- if (shortcut1 == 0) {
- continue;
- }
-
- for (int j = i + 1; j < actions.size(); ++j) {
- const auto* action2 = actions[j];
- if (action2->isSeparator()) {
- continue;
- }
-
- const char shortcut2 = shortcutChar(action2);
-
- if (shortcut1 == shortcut2) {
- log::error("duplicate shortcut {} for {} and {}", shortcut1, action1->text(),
- action2->text());
-
- break;
- }
- }
- }
-}
-
-} // namespace MOShared
-
-static bool g_exiting = false;
-static bool g_canClose = false;
-
-MainWindow* findMainWindow()
-{
- for (auto* tl : qApp->topLevelWidgets()) {
- if (auto* mw = dynamic_cast<MainWindow*>(tl)) {
- return mw;
- }
- }
-
- return nullptr;
-}
-
-bool ExitModOrganizer(ExitFlags e)
-{
- if (g_exiting) {
- return true;
- }
-
- g_exiting = true;
- Guard g([&] {
- g_exiting = false;
- });
-
- if (!e.testFlag(Exit::Force)) {
- if (auto* mw = findMainWindow()) {
- if (!mw->canExit()) {
- return false;
- }
- }
- }
-
- g_canClose = true;
-
- const int code = (e.testFlag(Exit::Restart) ? RestartExitCode : 0);
- qApp->exit(code);
-
- return true;
-}
-
-bool ModOrganizerCanCloseNow()
-{
- return g_canClose;
-}
-
-bool ModOrganizerExiting()
-{
- return g_exiting;
-}
-
-void ResetExitFlag()
-{
- g_exiting = false;
-}
-
-bool isNxmLink(const QString& link)
-{
- return link.startsWith("nxm://", Qt::CaseInsensitive) ||
- link.startsWith("modl://", Qt::CaseInsensitive);
-}
+/* +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 "util.h" +#include "../env.h" +#include "../mainwindow.h" +#include "windows_error.h" + +#include <fluorine_build_info.h> +#include <uibase/log.h> + +#include <pthread.h> +#include <algorithm> +#include <cwctype> +#include <filesystem> + +using namespace MOBase; + +namespace MOShared +{ + +bool FileExists(const std::string& filename) +{ + return std::filesystem::exists(filename); +} + +bool FileExists(const std::wstring& filename) +{ + return std::filesystem::exists(std::filesystem::path(filename)); +} + +bool FileExists(const std::wstring& searchPath, const std::wstring& filename) +{ + std::wstringstream stream; + stream << searchPath << "\\" << filename; + return FileExists(stream.str()); +} + +std::string ToString(const std::wstring& source, bool utf8) +{ + Q_UNUSED(utf8); + return QString::fromStdWString(source).toStdString(); +} + +std::wstring ToWString(const std::string& source, bool utf8) +{ + Q_UNUSED(utf8); + return QString::fromStdString(source).toStdWString(); +} + +static std::locale makeUserLocale() +{ + // std::locale("") reads LANG/LC_* env vars. If the user's system lacks the + // requested locale (e.g. en_US.UTF-8 not generated), the constructor throws + // runtime_error. Fall back to "C" so startup doesn't abort. + try { + return std::locale(""); + } catch (const std::runtime_error&) { + return std::locale::classic(); + } +} + +static std::locale loc = makeUserLocale(); + +std::string& ToLowerInPlace(std::string& text) +{ + std::transform(text.begin(), text.end(), text.begin(), [](char c) { + return std::tolower(static_cast<unsigned char>(c)); + }); + return text; +} + +std::string ToLowerCopy(const std::string& text) +{ + std::string result(text); + return ToLowerInPlace(result); +} + +std::wstring& ToLowerInPlace(std::wstring& text) +{ + std::transform(text.begin(), text.end(), text.begin(), [](wchar_t c) { + return std::towlower(c); + }); + return text; +} + +std::wstring ToLowerCopy(const std::wstring& text) +{ + std::wstring result(text); + return ToLowerInPlace(result); +} + +std::wstring ToLowerCopy(std::wstring_view text) +{ + std::wstring result(text.begin(), text.end()); + ToLowerInPlace(result); + return result; +} + +bool CaseInsenstiveComparePred(wchar_t lhs, wchar_t rhs) +{ + return std::tolower(lhs, loc) == std::tolower(rhs, loc); +} + +bool CaseInsensitiveEqual(const std::wstring& lhs, const std::wstring& rhs) +{ + return (lhs.length() == rhs.length()) && + std::equal(lhs.begin(), lhs.end(), rhs.begin(), + [](wchar_t lhs, wchar_t rhs) -> bool { + return std::tolower(lhs, loc) == std::tolower(rhs, loc); + }); +} + +Version createVersionInfo() +{ + // Fluorine Manager version is the user-facing one. The numeric components + // are injected by CMake from top-level FLUORINE_VERSION_* variables. +#if FLUORINE_IS_BETA_BUILD + // Beta builds tag themselves as Development pre-releases so the update + // checker can distinguish them from stable tags when comparing versions. + return Version(FLUORINE_VERSION_MAJOR, FLUORINE_VERSION_MINOR, + FLUORINE_VERSION_PATCH, 0, {Version::Development}); +#else + return Version(FLUORINE_VERSION_MAJOR, FLUORINE_VERSION_MINOR, + FLUORINE_VERSION_PATCH, 0); +#endif +} + +void SetThisThreadName(const QString& s) +{ + // pthread_setname_np is limited to 16 chars including the null terminator. + std::string name = s.toStdString(); + if (name.size() > 15) { + name.resize(15); + } + pthread_setname_np(pthread_self(), name.c_str()); +} + +char shortcutChar(const QAction* a) +{ + const auto text = a->text(); + + for (int i = 0; i < text.size(); ++i) { + const auto c = text[i]; + if (c == '&') { + if (i >= (text.size() - 1)) { + log::error("ampersand at the end"); + return 0; + } + + return text[i + 1].toLatin1(); + } + } + + log::error("action {} has no shortcut", text); + return 0; +} + +void checkDuplicateShortcuts(const QMenu& m) +{ + const auto actions = m.actions(); + + for (int i = 0; i < actions.size(); ++i) { + const auto* action1 = actions[i]; + if (action1->isSeparator()) { + continue; + } + + const char shortcut1 = shortcutChar(action1); + if (shortcut1 == 0) { + continue; + } + + for (int j = i + 1; j < actions.size(); ++j) { + const auto* action2 = actions[j]; + if (action2->isSeparator()) { + continue; + } + + const char shortcut2 = shortcutChar(action2); + + if (shortcut1 == shortcut2) { + log::error("duplicate shortcut {} for {} and {}", shortcut1, action1->text(), + action2->text()); + + break; + } + } + } +} + +} // namespace MOShared + +static bool g_exiting = false; +static bool g_canClose = false; + +MainWindow* findMainWindow() +{ + for (auto* tl : qApp->topLevelWidgets()) { + if (auto* mw = dynamic_cast<MainWindow*>(tl)) { + return mw; + } + } + + return nullptr; +} + +bool ExitModOrganizer(ExitFlags e) +{ + if (g_exiting) { + return true; + } + + g_exiting = true; + Guard g([&] { + g_exiting = false; + }); + + if (!e.testFlag(Exit::Force)) { + if (auto* mw = findMainWindow()) { + if (!mw->canExit()) { + return false; + } + } + } + + g_canClose = true; + + const int code = (e.testFlag(Exit::Restart) ? RestartExitCode : 0); + qApp->exit(code); + + return true; +} + +bool ModOrganizerCanCloseNow() +{ + return g_canClose; +} + +bool ModOrganizerExiting() +{ + return g_exiting; +} + +void ResetExitFlag() +{ + g_exiting = false; +} + +bool isNxmLink(const QString& link) +{ + return link.startsWith("nxm://", Qt::CaseInsensitive) || + link.startsWith("modl://", Qt::CaseInsensitive); +} diff --git a/src/src/shared/util.h b/src/src/shared/util.h index fdb59d8..035827f 100644 --- a/src/src/shared/util.h +++ b/src/src/shared/util.h @@ -31,11 +31,7 @@ class Executable; namespace MOShared
{
-#ifdef _WIN32
-inline constexpr wchar_t NativeWPathSep = L'\\';
-#else
inline constexpr wchar_t NativeWPathSep = L'/';
-#endif
/// Test if a file (or directory) by the specified name exists
bool FileExists(const std::string& filename);
diff --git a/src/src/shared/windows_compat.h b/src/src/shared/windows_compat.h index 1a62823..2c71ac3 100644 --- a/src/src/shared/windows_compat.h +++ b/src/src/shared/windows_compat.h @@ -1,13 +1,10 @@ #ifndef WINDOWS_COMPAT_H
#define WINDOWS_COMPAT_H
-// Compatibility typedefs for Windows types used throughout the organizer codebase.
-// On Windows these come from <Windows.h>; on Linux we provide minimal stubs.
-//
-// This header is included by the organizer PCH and also by uibase/utility.h
-// to provide a single source of truth for Windows type compatibility on Linux.
-
-#ifndef _WIN32
+// Compatibility typedefs for Windows types used throughout the organizer
+// codebase. The original MO2 source uses Win32 types (DWORD, HANDLE, FILETIME,
+// etc.) extensively; rather than rewrite every reference, this header maps
+// them onto Linux equivalents so the upstream signatures keep compiling.
#include <cstdint>
#include <cerrno>
@@ -84,5 +81,4 @@ constexpr int SE_ERR_SHARE = 26; #define INVALID_FILE_ATTRIBUTES ((DWORD)-1)
#endif
-#endif // !_WIN32
#endif // WINDOWS_COMPAT_H
diff --git a/src/src/shared/windows_error.cpp b/src/src/shared/windows_error.cpp index 3e81e51..184fd00 100644 --- a/src/src/shared/windows_error.cpp +++ b/src/src/shared/windows_error.cpp @@ -18,6 +18,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */
#include "windows_error.h"
+
+#include <cerrno>
+#include <cstring>
#include <sstream>
namespace MOShared
@@ -28,34 +31,13 @@ std::string windows_error::constructMessage(const std::string& input, int inErro std::ostringstream finalMessage;
finalMessage << input;
-#ifdef _WIN32
- LPSTR buffer = nullptr;
-
- DWORD errorCode = inErrorCode != -1 ? inErrorCode : ::GetLastError();
-
- // TODO: the message is not english?
- if (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
- nullptr, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
- (LPSTR)&buffer, 0, nullptr) == 0) {
- finalMessage << " (errorcode " << errorCode << ")";
- } else {
- LPSTR lastChar = buffer + strlen(buffer) - 2;
- *lastChar = '\0';
- finalMessage << " (" << buffer << " [" << errorCode << "])";
- LocalFree(buffer); // allocated by FormatMessage
- }
-
- ::SetLastError(
- errorCode); // restore error code because FormatMessage might have modified it
-#else
- int errorCode = inErrorCode != -1 ? inErrorCode : errno;
+ int errorCode = inErrorCode != -1 ? inErrorCode : errno;
const char* errStr = std::strerror(errorCode);
if (errStr) {
finalMessage << " (" << errStr << " [" << errorCode << "])";
} else {
finalMessage << " (errorcode " << errorCode << ")";
}
-#endif
return finalMessage.str();
}
diff --git a/src/src/shared/windows_error.h b/src/src/shared/windows_error.h index 4d19c5e..c1eac82 100644 --- a/src/src/shared/windows_error.h +++ b/src/src/shared/windows_error.h @@ -20,15 +20,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #ifndef WINDOWS_ERROR_H
#define WINDOWS_ERROR_H
-#include <stdexcept>
-
-#ifdef _WIN32
-#define WIN32_LEAN_AND_MEAN
-#include <windows.h>
-#else
#include <cerrno>
#include <cstring>
-#endif
+#include <stdexcept>
namespace MOShared
{
@@ -36,11 +30,7 @@ namespace MOShared class windows_error : public std::runtime_error
{
public:
-#ifdef _WIN32
- windows_error(const std::string& message, int errorcode = ::GetLastError())
-#else
windows_error(const std::string& message, int errorcode = errno)
-#endif
: runtime_error(constructMessage(message, errorcode)), m_ErrorCode(errorcode)
{}
int getErrorCode() const { return m_ErrorCode; }
diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 0d41401..f51c043 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -1,1513 +1,765 @@ -/*
-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 "spawn.h"
-
-#include "env.h"
-#include "envmodule.h"
-#include "fluorineconfig.h"
-#include "protonlauncher.h"
-#include "settings.h"
-#include "settingsdialogworkarounds.h"
-#include "shared/appconfig.h"
-#include <QApplication>
-#include <QDir>
-#include <QFile>
-#include <QMessageBox>
-#include <QRegularExpression>
-#include <QSet>
-#include <QSettings>
-#include <QTextStream>
-#include <QtDebug>
-#include <uibase/errorcodes.h>
-#include <uibase/log.h>
-#include <uibase/report.h>
-#include <uibase/utility.h>
-
-#ifdef _WIN32
-#include "envsecurity.h"
-#include "envwindows.h"
-#include "shared/windows_error.h"
-#include <Shellapi.h>
-#else
-#include <QProcess>
-#include <QStandardPaths>
-#include <cerrno>
-#include <cstring>
-#include <signal.h>
-#include <sys/types.h>
-#endif
-
-using namespace MOBase;
-using namespace MOShared;
-
-namespace spawn::dialogs {
-
-#ifdef _WIN32
-std::wstring makeRightsDetails(const env::FileSecurity &fs) {
- if (fs.rights.normalRights) {
- return L"(normal rights)";
- }
-
- if (fs.rights.list.isEmpty()) {
- return L"(none)";
- }
-
- std::wstring s = fs.rights.list.join("|").toStdWString();
- if (!fs.rights.hasExecute) {
- s += L" (execute is missing)";
- }
-
- return s;
-}
-
-QString makeDetails(const SpawnParameters &sp, DWORD code,
- const QString &more = {}) {
- std::wstring owner, rights;
-
- if (sp.binary.isFile()) {
- const auto fs = env::getFileSecurity(sp.binary.absoluteFilePath());
-
- if (fs.error.isEmpty()) {
- owner = fs.owner.toStdWString();
- rights = makeRightsDetails(fs);
- } else {
- owner = fs.error.toStdWString();
- rights = fs.error.toStdWString();
- }
- } else {
- owner = L"(file not found)";
- rights = L"(file not found)";
- }
-
- const bool cwdExists =
- (sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists());
-
- const auto appDir = QCoreApplication::applicationDirPath();
- const auto sep = QDir::separator();
-
- const std::wstring usvfs_x86_dll =
- QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found";
-
- const std::wstring usvfs_x64_dll =
- QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found";
-
- const std::wstring usvfs_x86_proxy =
- QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok"
- : L"not found";
-
- const std::wstring usvfs_x64_proxy =
- QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok"
- : L"not found";
-
- std::wstring elevated;
- if (auto b = env::Environment().windowsInfo().isElevated()) {
- elevated = (*b ? L"yes" : L"no");
- } else {
- elevated = L"(not available)";
- }
-
- auto s = std::format(
- L"Error {} {}{}: {}\n"
- L" . binary: '{}'\n"
- L" . owner: {}\n"
- L" . rights: {}\n"
- L" . arguments: '{}'\n"
- L" . cwd: '{}'{}\n"
- L" . stdout: {}, stderr: {}, hooked: {}\n"
- L" . MO elevated: {}",
- code, errorCodeName(code), (more.isEmpty() ? more : ", " + more),
- formatSystemMessage(code),
- QDir::toNativeSeparators(sp.binary.absoluteFilePath()), owner, rights,
- sp.arguments,
- QDir::toNativeSeparators(sp.currentDirectory.absolutePath()),
- (cwdExists ? L"" : L" (not found)"),
- (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
- (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
- (sp.hooked ? L"yes" : L"no"), elevated);
-
- if (sp.hooked) {
- s += std::format(L"\n . usvfs x86:{} x64:{} proxy_x86:{} proxy_x64:{}",
- usvfs_x86_dll, usvfs_x64_dll, usvfs_x86_proxy,
- usvfs_x64_proxy);
- }
-
- return QString::fromStdWString(s);
-}
-#else
-QString makeDetails(const SpawnParameters &sp, int code,
- const QString &more = {}) {
- const bool cwdExists =
- (sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists());
-
- QString s = QString("Error %1%2: %3\n"
- " . binary: '%4'\n"
- " . arguments: '%5'\n"
- " . cwd: '%6'%7\n"
- " . stdout: %8, stderr: %9, hooked: %10")
- .arg(code)
- .arg(more.isEmpty() ? more : ", " + more)
- .arg(QString::fromUtf8(strerror(code)))
- .arg(sp.binary.absoluteFilePath())
- .arg(sp.arguments)
- .arg(sp.currentDirectory.absolutePath())
- .arg(cwdExists ? "" : " (not found)")
- .arg(sp.stdOut == -1 ? "no" : "yes")
- .arg(sp.stdErr == -1 ? "no" : "yes")
- .arg(sp.hooked ? "yes" : "no");
-
- return s;
-}
-#endif
-
-#ifdef _WIN32
-QString makeContent(const SpawnParameters &sp, DWORD code) {
- if (code == ERROR_INVALID_PARAMETER) {
- return QObject::tr(
- "This error typically happens because an antivirus has deleted "
- "critical "
- "files from Mod Organizer's installation folder or has made them "
- "generally inaccessible. Add an exclusion for Mod Organizer's "
- "installation folder in your antivirus, reinstall Mod Organizer and "
- "try "
- "again.");
- } else if (code == ERROR_ACCESS_DENIED) {
- return QObject::tr(
- "This error typically happens because an antivirus is preventing Mod "
- "Organizer from starting programs. Add an exclusion for Mod "
- "Organizer's "
- "installation folder in your antivirus and try again.");
- } else if (code == ERROR_FILE_NOT_FOUND) {
- return QObject::tr("The file '%1' does not exist.")
- .arg(QDir::toNativeSeparators(sp.binary.absoluteFilePath()));
- } else if (code == ERROR_DIRECTORY) {
- if (!sp.currentDirectory.exists()) {
- return QObject::tr("The working directory '%1' does not exist.")
- .arg(QDir::toNativeSeparators(sp.currentDirectory.absolutePath()));
- }
- }
-
- return QString::fromStdWString(formatSystemMessage(code));
-}
-#else
-QString makeContent(const SpawnParameters &sp, int code) {
- if (code == ENOENT) {
- return QObject::tr("The file '%1' does not exist.")
- .arg(sp.binary.absoluteFilePath());
- } else if (code == EACCES) {
- return QObject::tr("Permission denied when trying to start '%1'. "
- "Check that the file is executable.")
- .arg(sp.binary.absoluteFilePath());
- }
-
- if (!sp.currentDirectory.exists()) {
- return QObject::tr("The working directory '%1' does not exist.")
- .arg(sp.currentDirectory.absolutePath());
- }
-
- return QString::fromUtf8(strerror(code));
-}
-#endif
-
-#ifdef _WIN32
-QMessageBox::StandardButton badSteamReg(QWidget *parent, const QString &keyName,
- const QString &valueName) {
- const auto details =
- QString(
- "can't start steam, registry value at '%1' is empty or doesn't exist")
- .arg(keyName + "\\" + valueName);
-
- log::error("{}", details);
-
- return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
- .main(QObject::tr("Cannot start Steam"))
- .content(QObject::tr(
- "The path to the Steam executable cannot be found. You might try "
- "reinstalling Steam."))
- .details(details)
- .icon(QMessageBox::Critical)
- .button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."),
- QMessageBox::Yes})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .exec();
-}
-#endif
-
-#ifdef _WIN32
-QMessageBox::StandardButton
-startSteamFailed(QWidget *parent, const QString &keyName,
- const QString &valueName, const QString &exe,
- const SpawnParameters &sp, DWORD e) {
- auto details =
- QString("a steam install was found in the registry at '%1': '%2'\n\n")
- .arg(keyName + "\\" + valueName)
- .arg(exe);
-
- details += makeDetails(sp, e);
-
- log::error("{}", details);
-
- return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
- .main(QObject::tr("Cannot start Steam"))
- .content(makeContent(sp, e))
- .details(details)
- .icon(QMessageBox::Critical)
- .button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."),
- QMessageBox::Yes})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .exec();
-}
-#endif
-
-void spawnFailed(QWidget *parent, const SpawnParameters &sp,
-#ifdef _WIN32
- DWORD code
-#else
- int code
-#endif
-) {
- const auto details = makeDetails(sp, code);
- log::error("{}", details);
-
- const auto title = QObject::tr("Cannot launch program");
-
- const auto mainText =
- QObject::tr("Cannot start %1").arg(sp.binary.fileName());
-
- MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(makeContent(sp, code))
- .details(details)
- .icon(QMessageBox::Critical)
- .exec();
-}
-
-#ifdef _WIN32
-void helperFailed(QWidget *parent, DWORD code, const QString &why,
- const std::wstring &binary, const std::wstring &cwd,
- const std::wstring &args) {
- SpawnParameters sp;
- sp.binary = QFileInfo(QString::fromStdWString(binary));
- sp.currentDirectory.setPath(QString::fromStdWString(cwd));
- sp.arguments = QString::fromStdWString(args);
-
- const auto details = makeDetails(sp, code, "in " + why);
- log::error("{}", details);
-
- const auto title = QObject::tr("Cannot launch helper");
-
- const auto mainText =
- QObject::tr("Cannot start %1").arg(sp.binary.fileName());
-
- MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(makeContent(sp, code))
- .details(details)
- .icon(QMessageBox::Critical)
- .exec();
-}
-
-bool confirmRestartAsAdmin(QWidget *parent, const SpawnParameters &sp) {
- const auto details = makeDetails(sp, ERROR_ELEVATION_REQUIRED);
-
- log::error("{}", details);
-
- const auto title = QObject::tr("Elevation required");
-
- const auto mainText =
- QObject::tr("Cannot start %1").arg(sp.binary.fileName());
-
- const auto content = QObject::tr(
- "This program is requesting to run as administrator but Mod Organizer "
- "itself is not running as administrator. Running programs as "
- "administrator "
- "is typically unnecessary as long as the game and Mod Organizer have "
- "been "
- "installed outside \"Program Files\".\r\n\r\n"
- "You can restart Mod Organizer as administrator and try launching the "
- "program again.");
-
- log::debug("asking user to restart MO as administrator");
-
- const auto r =
- MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(details)
- .icon(QMessageBox::Question)
- .button({QObject::tr("Restart Mod Organizer as administrator"),
- QObject::tr("You must allow \"helper.exe\" to make changes "
- "to the system."),
- QMessageBox::Yes})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .exec();
-
- return (r == QMessageBox::Yes);
-}
-#endif // _WIN32
-
-QMessageBox::StandardButton confirmStartSteam(QWidget *parent,
- const SpawnParameters &sp,
- const QString &details) {
- const auto title = QObject::tr("Launch Steam");
- const auto mainText = QObject::tr("This program requires Steam");
- const auto content = QObject::tr("Mod Organizer has detected that this "
- "program likely requires Steam to be "
- "running to function properly.");
-
- return MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(details)
- .icon(QMessageBox::Question)
- .button({QObject::tr("Start Steam"), QMessageBox::Yes})
- .button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program might fail to run."), QMessageBox::No})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .remember("steamQuery", sp.binary.fileName())
- .exec();
-}
-
-#ifdef _WIN32
-QMessageBox::StandardButton
-confirmRestartAsAdminForSteam(QWidget *parent, const SpawnParameters &sp) {
- const auto title = QObject::tr("Elevation required");
- const auto mainText = QObject::tr("Steam is running as administrator");
- const auto content = QObject::tr(
- "Running Steam as administrator is typically unnecessary and can cause "
- "problems when Mod Organizer itself is not running as administrator."
- "\r\n\r\n"
- "You can restart Mod Organizer as administrator and try launching the "
- "program again.");
-
- return MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .icon(QMessageBox::Question)
- .button(
- {QObject::tr("Restart Mod Organizer as administrator"),
- QObject::tr(
- "You must allow \"helper.exe\" to make changes to the system."),
- QMessageBox::Yes})
- .button({QObject::tr("Continue"),
- QObject::tr("The program might fail to run."), QMessageBox::No})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .remember("steamAdminQuery", sp.binary.fileName())
- .exec();
-}
-
-bool eventLogNotRunning(QWidget *parent, const env::Service &s,
- const SpawnParameters &sp) {
- const auto title = QObject::tr("Event Log not running");
- const auto mainText = QObject::tr("The Event Log service is not running");
- const auto content = QObject::tr("The Windows Event Log service is not "
- "running. This can prevent USVFS from "
- "running properly and your mods may not be "
- "recognized by the program being "
- "launched.");
-
- const auto r =
- MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(s.toString())
- .icon(QMessageBox::Question)
- .remember("eventLogService", sp.binary.fileName())
- .button({QObject::tr("Continue"),
- QObject::tr("Your mods might not work."), QMessageBox::Yes})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .exec();
-
- return (r == QMessageBox::Yes);
-}
-#endif // _WIN32
-
-QMessageBox::StandardButton confirmBlacklisted(QWidget *parent,
- const SpawnParameters &sp,
- Settings &settings) {
- const auto title = QObject::tr("Blacklisted program");
- const auto mainText =
- QObject::tr("The program %1 is blacklisted").arg(sp.binary.fileName());
- const auto content = QObject::tr(
- "The program you are attempting to launch is blacklisted in the virtual "
- "filesystem. This will likely prevent it from seeing any mods, INI files "
- "or any other virtualized files.");
-
- const auto details = "Executable: " + sp.binary.fileName() +
- "\n"
- "Current blacklist: " +
- settings.executablesBlacklist();
-
- auto r =
- MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(details)
- .icon(QMessageBox::Question)
- .remember("blacklistedExecutable", sp.binary.fileName())
- .button({QObject::tr("Continue"),
- QObject::tr("Your mods might not work."), QMessageBox::Yes})
- .button({QObject::tr("Change the blacklist"), QMessageBox::Retry})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .exec();
-
- if (r == QMessageBox::Retry) {
- if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) {
- r = QMessageBox::Cancel;
- }
- }
-
- return r;
-}
-
-} // namespace spawn::dialogs
-
-namespace spawn {
-
-void logSpawning(const SpawnParameters &sp, const QString &realCmd) {
- log::debug("spawning binary:\n"
- " . exe: '{}'\n"
- " . args: '{}'\n"
- " . cwd: '{}'\n"
- " . steam id: '{}'\n"
- " . hooked: {}\n"
- " . stdout: {}\n"
- " . stderr: {}\n"
- " . real cmd: '{}'",
- sp.binary.absoluteFilePath(), sp.arguments,
- sp.currentDirectory.absolutePath(), sp.steamAppID, sp.hooked,
-#ifdef _WIN32
- (sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"),
- (sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"),
-#else
- (sp.stdOut == -1 ? "no" : "yes"), (sp.stdErr == -1 ? "no" : "yes"),
-#endif
- realCmd);
-}
-
-#ifndef _WIN32
-uint32_t parseSteamAppId(const QString &steamAppId) {
- bool ok = false;
- const auto n = steamAppId.toUInt(&ok);
- return (ok ? n : 0u);
-}
-
-QString firstExistingSetting(const QSettings &settings,
- const QStringList &keys) {
- for (const QString &key : keys) {
- const QString value = settings.value(key).toString().trimmed();
- if (!value.isEmpty()) {
- return value;
- }
- }
-
- return {};
-}
-
-// Strip "<letter>:"="..." entries (other than C:/Z:) from the
-// [Software\\Wine\\Drives] section of system.reg. Without this, Wine
-// recreates pruned dosdevices symlinks at the next prefix start from the
-// registry, so the more-specific drive (e.g. X:\Games) keeps winning over
-// Z:\home\user\Games during path canonicalisation.
-static QStringList pruneDriveRegistry(const QString &prefixPath) {
- const QString regPath = QDir(prefixPath).filePath("system.reg");
- QFile file(regPath);
- if (!file.exists()) {
- return {};
- }
- if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
- MOBase::log::warn("pruneDriveRegistry: cannot open '{}'",
- regPath.toStdString());
- return {};
- }
-
- static const QRegularExpression sectionRe(
- QStringLiteral(R"(^\[Software\\\\Wine\\\\Drives\])"),
- QRegularExpression::CaseInsensitiveOption);
- static const QRegularExpression driveRe(
- QStringLiteral(R"(^"([A-Za-z]):"\s*=)"));
-
- QStringList lines;
- QStringList removed;
- bool inDrives = false;
-
- QTextStream in(&file);
- while (!in.atEnd()) {
- const QString line = in.readLine();
- const QString trimmed = line.trimmed();
-
- if (trimmed.startsWith('[')) {
- inDrives = sectionRe.match(trimmed).hasMatch();
- lines.append(line);
- continue;
- }
-
- if (inDrives) {
- const auto m = driveRe.match(trimmed);
- if (m.hasMatch()) {
- const QChar letter = m.captured(1).at(0).toLower();
- if (letter != QLatin1Char('c') && letter != QLatin1Char('z')) {
- removed << QString(letter.toUpper()) + QLatin1Char(':');
- continue; // drop this line
- }
- }
- }
-
- lines.append(line);
- }
- file.close();
-
- if (removed.isEmpty()) {
- return {};
- }
-
- if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate |
- QIODevice::Text)) {
- MOBase::log::warn("pruneDriveRegistry: cannot rewrite '{}'",
- regPath.toStdString());
- return {};
- }
- QTextStream out(&file);
- for (const QString &l : lines) {
- out << l << "\n";
- }
- return removed;
-}
-
-// Remove dosdevices/<letter>: symlinks for any drive other than C: and Z:,
-// and strip the matching [Software\\Wine\\Drives] entries from system.reg
-// so Wine doesn't recreate them at the next prefix start. External tooling
-// (Faugus, manual edits, modlist installers) can re-add drives like X: that
-// map subtrees of the host filesystem; Wine then prefers the more specific
-// drive when canonicalising paths, which mangles binaries we passed in as
-// Z:\home\user\... into X:\.... Keeping the allowed list minimal means MO2
-// can rely on Z: being the only host-mapped drive.
-void pruneExtraDrives(const QString &prefixPath) {
- static const QSet<QString> kept = {QStringLiteral("c:"), QStringLiteral("z:")};
-
- const QString dosdevices = QDir(prefixPath).filePath("dosdevices");
- QDir dir(dosdevices);
- if (!dir.exists()) {
- return;
- }
-
- QStringList removed;
- for (const QString &entry :
- dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries)) {
- const QString lower = entry.toLower();
- if (lower.length() != 2 || !lower.endsWith(':') ||
- !lower.at(0).isLetter()) {
- continue;
- }
- if (kept.contains(lower)) {
- continue;
- }
- if (QFile::remove(dir.filePath(entry))) {
- removed << entry.toUpper();
- }
- }
-
- const QStringList regRemoved = pruneDriveRegistry(prefixPath);
-
- QSet<QString> all;
- for (const QString &d : removed) {
- all.insert(d);
- }
- for (const QString &d : regRemoved) {
- all.insert(d);
- }
- if (!all.isEmpty()) {
- QStringList sorted(all.begin(), all.end());
- sorted.sort();
- MOBase::log::info(
- "Pruned stale drive letters from prefix '{}': {} (symlinks: {}, "
- "registry: {})",
- prefixPath, sorted.join(QStringLiteral(", ")).toStdString(),
- removed.join(QStringLiteral(", ")).toStdString(),
- regRemoved.join(QStringLiteral(", ")).toStdString());
- }
-}
-
-QString resolvePrefixPath() {
- // The Fluorine config is authoritative: it's the prefix the user
- // explicitly created through Settings > Proton and that Fluorine itself
- // initialises with wineboot/DLL installs. Always prefer it.
- if (auto cfg = FluorineConfig::load();
- cfg.has_value() && cfg->prefixExists()) {
- MOBase::log::debug("resolvePrefixPath: using Fluorine config prefix '{}'",
- cfg->prefix_path);
- return cfg->prefix_path.trimmed();
- }
-
- const Settings *settings = Settings::maybeInstance();
- if (settings == nullptr) {
- return {};
- }
-
- // Fallbacks, in priority order. `fluorine/prefix_path` is set only by
- // explicit user action (CLI `--prefix` or the instance creation wizard),
- // so we trust it above the `Settings/*` keys that game-detection can
- // populate automatically with an external manager's prefix (Heroic,
- // Bottles, Lutris). Those external prefixes are fine as discovery hints
- // but must not silently override the user's chosen Fluorine prefix —
- // see issue #52.
- const QSettings instanceSettings(settings->filename(), QSettings::IniFormat);
- const QString explicitPath =
- instanceSettings.value("fluorine/prefix_path").toString().trimmed();
- if (!explicitPath.isEmpty()) {
- MOBase::log::debug(
- "resolvePrefixPath: using explicit fluorine/prefix_path '{}'",
- explicitPath);
- return explicitPath;
- }
-
- const QString fallback = firstExistingSetting(
- instanceSettings, {"Settings/proton_prefix_path", "Settings/prefix_path",
- "Proton/prefix_path"});
- if (!fallback.isEmpty()) {
- MOBase::log::warn(
- "resolvePrefixPath: falling back to auto-detected prefix '{}' — this "
- "may point at an external manager's prefix (Heroic/Bottles). Create a "
- "Fluorine prefix in Settings > Proton to override.",
- fallback);
- }
- return fallback;
-}
-
-QString resolveProtonPath() {
- if (auto cfg = FluorineConfig::load(); cfg.has_value()) {
- const QString protonPath = cfg->proton_path.trimmed();
- if (!protonPath.isEmpty()) {
- return protonPath;
- }
- }
-
- const Settings *settings = Settings::maybeInstance();
- if (settings == nullptr) {
- return {};
- }
-
- const QSettings instanceSettings(settings->filename(), QSettings::IniFormat);
- return firstExistingSetting(
- instanceSettings,
- {"Settings/proton_path", "Proton/path", "fluorine/proton_path"});
-}
-#endif
-
-#ifdef _WIN32
-DWORD spawn(const SpawnParameters &sp, HANDLE &processHandle) {
- BOOL inheritHandles = FALSE;
-
- STARTUPINFO si = {};
- si.cb = sizeof(si);
-
- // inherit handles if we plan to use stdout or stderr reroute
- if (sp.stdOut != INVALID_HANDLE_VALUE) {
- si.hStdOutput = sp.stdOut;
- inheritHandles = TRUE;
- si.dwFlags |= STARTF_USESTDHANDLES;
- }
-
- if (sp.stdErr != INVALID_HANDLE_VALUE) {
- si.hStdError = sp.stdErr;
- inheritHandles = TRUE;
- si.dwFlags |= STARTF_USESTDHANDLES;
- }
-
- const auto bin = QDir::toNativeSeparators(sp.binary.absoluteFilePath());
- const auto cwd = QDir::toNativeSeparators(sp.currentDirectory.absolutePath());
-
- QString commandLine = "\"" + bin + "\"";
- if (!sp.arguments.isEmpty()) {
- commandLine += " " + sp.arguments;
- }
-
- const QString moPath = QCoreApplication::applicationDirPath();
- const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath));
-
- PROCESS_INFORMATION pi = {};
- BOOL success = FALSE;
-
- logSpawning(sp, commandLine);
-
- const auto wcommandLine = commandLine.toStdWString();
- const auto wcwd = cwd.toStdWString();
-
- const DWORD flags = CREATE_BREAKAWAY_FROM_JOB;
-
- if (sp.hooked) {
- success = ::usvfsCreateProcessHooked(
- nullptr, const_cast<wchar_t *>(wcommandLine.c_str()), nullptr, nullptr,
- inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
- } else {
- success = ::CreateProcess(
- nullptr, const_cast<wchar_t *>(wcommandLine.c_str()), nullptr, nullptr,
- inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
- }
-
- const auto e = GetLastError();
- env::setPath(oldPath);
-
- if (!success) {
- return e;
- }
-
- processHandle = pi.hProcess;
- ::CloseHandle(pi.hThread);
-
- return ERROR_SUCCESS;
-}
-#else
-int spawn(const SpawnParameters &sp, pid_t &processId) {
- const QString bin =
- MOBase::normalizePathForHost(sp.binary.absoluteFilePath());
- QString cwd =
- MOBase::normalizePathForHost(sp.currentDirectory.absolutePath());
-
- QStringList argList;
- if (!sp.arguments.isEmpty()) {
- argList = QProcess::splitCommand(sp.arguments);
- }
-
- if (cwd.isEmpty()) {
- cwd = QFileInfo(bin).absolutePath();
- }
-
- logSpawning(sp, bin + " " + sp.arguments);
-
- ProtonLauncher launcher;
- launcher.setBinary(bin)
- .setArguments(argList)
- .setWorkingDir(cwd)
- .setSteamAppId(parseSteamAppId(sp.steamAppID));
-
- if (sp.useProton) {
- // Read per-instance settings from the instance INI (not the global
- // QSettings).
- const Settings *instanceForLaunch = Settings::maybeInstance();
- bool useSteamDrm = true; // default
- bool useSLR = true; // default on
- bool useSteamOverlay = false; // default off
- QString storeVariant;
- if (instanceForLaunch) {
- const QSettings instanceIni(instanceForLaunch->filename(),
- QSettings::IniFormat);
- useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool();
- useSLR = instanceIni.value("fluorine/use_slr", true).toBool();
- useSteamOverlay = instanceIni.value("fluorine/steam_overlay", false).toBool();
- storeVariant = instanceIni.value("game_edition").toString().trimmed();
- }
-
- launcher.setSteamDrm(useSteamDrm)
- .setSteamOverlay(useSteamOverlay)
- .setStoreVariant(storeVariant)
- .setUseSLR(useSLR);
-
- const QString prefixPath = resolvePrefixPath();
- if (prefixPath.isEmpty()) {
- MOBase::log::error(
- "No Wine prefix configured - cannot launch game. "
- "Configure a prefix in Settings > Proton.");
- return ENOENT;
- } else if (!QDir(QDir(prefixPath).filePath("drive_c")).exists()) {
- MOBase::log::error(
- "Wine prefix '{}' does not contain drive_c/ - prefix is invalid",
- prefixPath);
- return ENOENT;
- } else {
- MOBase::log::info("Using Wine prefix: {}", prefixPath);
- pruneExtraDrives(prefixPath);
- launcher.setPrefix(prefixPath);
- }
-
- const QString protonPath = resolveProtonPath();
- if (!protonPath.isEmpty()) {
- launcher.setProtonPath(protonPath);
- }
-
- const QString wrapper =
- QSettings().value("fluorine/launch_wrapper").toString().trimmed();
- if (!wrapper.isEmpty()) {
- launcher.setWrapper(wrapper);
- }
-
- if (!sp.saveBindMountSource.isEmpty() && !sp.saveBindMountTarget.isEmpty()) {
- launcher.setSavesBindMount(sp.saveBindMountSource, sp.saveBindMountTarget);
- }
- } else {
- MOBase::log::info("Proton disabled for this executable, launching directly");
- }
-
- launcher.setUseTerminal(sp.useTerminal);
-
- const auto [ok, pid] = launcher.launch();
- if (!ok) {
- return (errno != 0 ? errno : EIO);
- }
-
- processId = static_cast<pid_t>(pid);
- return 0;
-}
-#endif
-
-#ifdef _WIN32
-bool restartAsAdmin(QWidget *parent) {
- WCHAR cwd[MAX_PATH] = {};
- if (!GetCurrentDirectory(MAX_PATH, cwd)) {
- cwd[0] = L'\0';
- }
-
- if (!helper::adminLaunch(parent, qApp->applicationDirPath().toStdWString(),
- qApp->applicationFilePath().toStdWString(),
- std::wstring(cwd))) {
- log::error("admin launch failed");
- return false;
- }
-
- log::debug("exiting MO");
- ExitModOrganizer(Exit::Force);
-
- return true;
-}
-
-void startBinaryAdmin(QWidget *parent, const SpawnParameters &sp) {
- if (!dialogs::confirmRestartAsAdmin(parent, sp)) {
- log::debug("user declined");
- return;
- }
-
- log::info("restarting MO as administrator");
- restartAsAdmin(parent);
-}
-#endif // _WIN32
-
-struct SteamStatus {
- bool running = false;
- bool accessible = false;
-};
-
-SteamStatus getSteamStatus() {
- SteamStatus ss;
-
-#ifdef _WIN32
- const auto ps = env::Environment().runningProcesses();
-
- for (const auto &p : ps) {
- if ((p.name().compare("Steam.exe", Qt::CaseInsensitive) == 0) ||
- (p.name().compare("SteamService.exe", Qt::CaseInsensitive) == 0)) {
- ss.running = true;
- ss.accessible = p.canAccess();
-
- log::debug("'{}' is running, accessible={}", p.name(),
- (ss.accessible ? "yes" : "no"));
-
- break;
- }
- }
-#else
- // On Linux, check for steam process via /proc
- QProcess pgrep;
- pgrep.start("pgrep", QStringList() << "-x" << "steam");
- pgrep.waitForFinished(3000);
-
- if (pgrep.exitCode() == 0) {
- ss.running = true;
- ss.accessible = true;
- log::debug("steam is running");
- }
-#endif
-
- return ss;
-}
-
-QString makeSteamArguments(const QString &username, const QString &password) {
- QString args;
-
- if (username != "") {
- args += "-login " + username;
-
- if (password != "") {
- args += " " + password;
- }
- }
-
- return args;
-}
-
-bool startSteam(QWidget *parent) {
-#ifdef _WIN32
- const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam";
- const QString valueName = "SteamExe";
-
- const QSettings steamSettings(keyName, QSettings::NativeFormat);
- const QString exe = steamSettings.value(valueName, "").toString();
-
- if (exe.isEmpty()) {
- return (dialogs::badSteamReg(parent, keyName, valueName) ==
- QMessageBox::Yes);
- }
-
- SpawnParameters sp;
- sp.binary = QFileInfo(exe);
-
- // See if username and password supplied. If so, pass them into steam.
- QString username, password;
- if (Settings::instance().steam().login(username, password)) {
- if (username.length() > 0)
- MOBase::log::getDefault().addToBlacklist(username.toStdString(),
- "STEAM_USERNAME");
- if (password.length() > 0)
- MOBase::log::getDefault().addToBlacklist(password.toStdString(),
- "STEAM_PASSWORD");
- sp.arguments = makeSteamArguments(username, password);
- }
-
- log::debug("starting steam process:\n"
- " . program: '{}'\n"
- " . username={}, password={}",
- sp.binary.filePath().toStdString(),
- (username.isEmpty() ? "no" : "yes"),
- (password.isEmpty() ? "no" : "yes"));
-
- HANDLE ph = INVALID_HANDLE_VALUE;
- const auto e = spawn(sp, ph);
- ::CloseHandle(ph);
-
- if (e != ERROR_SUCCESS) {
- // make sure username and passwords are not shown
- sp.arguments = makeSteamArguments((username.isEmpty() ? "" : "USERNAME"),
- (password.isEmpty() ? "" : "PASSWORD"));
-
- const auto r =
- dialogs::startSteamFailed(parent, keyName, valueName, exe, sp, e);
-
- return (r == QMessageBox::Yes);
- }
-
- QMessageBox::information(
- parent, QObject::tr("Waiting"),
- QObject::tr("Please press OK once you're logged into steam."));
-
- return true;
-#else
- // On Linux, find steam via common locations or PATH
- QString steamPath;
-
- // Try ~/.steam/root/steam.sh first
- const QString homeDir = QDir::homePath();
- const QString steamSh = homeDir + "/.steam/root/steam.sh";
- if (QFileInfo::exists(steamSh)) {
- steamPath = steamSh;
- } else {
- // Try finding steam in PATH
- steamPath = QStandardPaths::findExecutable("steam");
- }
-
- if (steamPath.isEmpty()) {
- log::error("could not find steam installation");
-
- const auto title = QObject::tr("Cannot start Steam");
- MOBase::TaskDialog(parent, title)
- .main(title)
- .content(QObject::tr("The Steam executable could not be found. "
- "Make sure Steam is installed."))
- .icon(QMessageBox::Critical)
- .button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."),
- QMessageBox::Yes})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .exec();
-
- return false;
- }
-
- SpawnParameters sp;
- sp.binary = QFileInfo(steamPath);
-
- pid_t pid = -1;
- const auto e = spawn(sp, pid);
-
- if (e != 0) {
- log::error("failed to start steam");
- return false;
- }
-
- QMessageBox::information(
- parent, QObject::tr("Waiting"),
- QObject::tr("Please press OK once you're logged into steam."));
-
- return true;
-#endif
-}
-
-bool checkSteam(QWidget *parent, const SpawnParameters &sp,
- const QDir &gameDirectory, const QString &steamAppID,
- const Settings &settings) {
-#ifdef _WIN32
- static const std::vector<QString> steamFiles = {"steam_api.dll",
- "steam_api64.dll"};
-#else
- static const std::vector<QString> steamFiles = {"libsteam_api.so"};
-#endif
-
- log::debug("checking steam");
-
- if (!steamAppID.isEmpty()) {
- env::set("SteamAPPId", steamAppID);
- } else {
- env::set("SteamAPPId", settings.steam().appID());
- }
-
- bool steamRequired = false;
- QString details;
-
- for (const auto &file : steamFiles) {
- const QFileInfo fi(gameDirectory.absoluteFilePath(file));
- if (fi.exists()) {
- details = QString("managed game is located at '%1' and file '%2' exists")
- .arg(gameDirectory.absolutePath())
- .arg(fi.absoluteFilePath());
-
- log::debug("{}", details);
- steamRequired = true;
-
- break;
- }
- }
-
- if (!steamRequired) {
- log::debug("program doesn't seem to require steam");
- return true;
- }
-
- auto ss = getSteamStatus();
-
- if (!ss.running) {
- log::debug("steam isn't running, asking to start steam");
-
- const auto c = dialogs::confirmStartSteam(parent, sp, details);
-
- if (c == QMessageBox::Yes) {
- log::debug("user wants to start steam");
-
- if (!startSteam(parent)) {
- // cancel
- return false;
- }
-
- // double-check that Steam is started
- ss = getSteamStatus();
- if (!ss.running) {
- log::error("steam is still not running, hoping for the best");
- return true;
- }
- } else if (c == QMessageBox::No) {
- log::debug("user declined to start steam");
- return true;
- } else {
- log::debug("user cancelled");
- return false;
- }
- }
-
-#ifdef _WIN32
- if (ss.running && !ss.accessible) {
- log::debug("steam is running but is not accessible, asking to restart MO");
- const auto c = dialogs::confirmRestartAsAdminForSteam(parent, sp);
-
- if (c == QDialogButtonBox::Yes) {
- restartAsAdmin(parent);
- return false;
- } else if (c == QDialogButtonBox::No) {
- log::debug("user declined to restart MO, continuing");
- return true;
- } else {
- log::debug("user cancelled");
- return false;
- }
- }
-#endif
-
- return true;
-}
-
-bool checkBlacklist(QWidget *parent, const SpawnParameters &sp,
- Settings &settings) {
- for (;;) {
- if (!settings.isExecutableBlacklisted(sp.binary.fileName())) {
- return true;
- }
-
- const auto r = dialogs::confirmBlacklisted(parent, sp, settings);
-
- if (r != QMessageBox::Retry) {
- return (r == QMessageBox::Yes);
- }
- }
-}
-
-#ifdef _WIN32
-HANDLE startBinary(QWidget *parent, const SpawnParameters &sp) {
- HANDLE handle = INVALID_HANDLE_VALUE;
- const auto e = spawn::spawn(sp, handle);
-
- switch (e) {
- case ERROR_SUCCESS: {
- return handle;
- }
-
- case ERROR_ELEVATION_REQUIRED: {
- startBinaryAdmin(parent, sp);
- return INVALID_HANDLE_VALUE;
- }
-
- default: {
- dialogs::spawnFailed(parent, sp, e);
- return INVALID_HANDLE_VALUE;
- }
- }
-}
-#else
-pid_t startBinary(QWidget *parent, const SpawnParameters &sp) {
- pid_t pid = -1;
- const auto e = spawn::spawn(sp, pid);
-
- if (e != 0) {
- if (e == ENOENT && sp.useProton && !FluorineConfig::isSetup()) {
- QMessageBox::critical(
- parent, QObject::tr("No Wine Prefix"),
- QObject::tr("No Wine prefix has been configured for this instance.\n\n"
- "A Wine prefix is required to run Windows games through "
- "Proton.\n\n"
- "To create one, go to:\n"
- " Settings \u2192 Wine/Proton\n\n"
- "Set the prefix location, then click \"Create Prefix\" "
- "to generate a new prefix."));
- } else {
- dialogs::spawnFailed(parent, sp, e);
- }
- return -1;
- }
-
- return pid;
-}
-#endif
-
-#ifdef _WIN32
-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";
-}
-#endif // _WIN32
-
-QString findJavaInstallation(const QString &jarFile) {
-#ifdef _WIN32
- // 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;
- }
-#else
- Q_UNUSED(jarFile);
-
- // On Linux, find java via PATH
- const auto javaPath = QStandardPaths::findExecutable("java");
- if (!javaPath.isEmpty()) {
- return javaPath;
- }
-#endif
-
- // not found
- return {};
-}
-
-bool isBatchFile(const QFileInfo &target) {
-#ifdef _WIN32
- const auto batchExtensions = {"cmd", "bat"};
-#else
- const auto batchExtensions = {"sh"};
-#endif
-
- 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) {
-#ifdef _WIN32
- return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0);
-#else
- // On Linux, check if file is executable
- return target.isExecutable() && target.isFile();
-#endif
-}
-
-bool isJavaFile(const QFileInfo &target) {
- return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0);
-}
-
-QFileInfo getCmdPath() {
-#ifdef _WIN32
- const auto p = env::get("COMSPEC");
- if (!p.isEmpty()) {
- return QFileInfo(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 QFileInfo(systemDirectory + "cmd.exe");
-#else
- const auto p = env::get("SHELL");
- if (!p.isEmpty()) {
- return QFileInfo(p);
- }
-
- return QFileInfo("/bin/bash");
-#endif
-}
-
-FileExecutionTypes getFileExecutionType(const QFileInfo &target) {
- if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) {
- return FileExecutionTypes::Executable;
- }
-
- return FileExecutionTypes::Other;
-}
-
-FileExecutionContext getFileExecutionContext(QWidget *parent,
- const QFileInfo &target) {
- if (isExeFile(target)) {
- return {target, "", FileExecutionTypes::Executable};
- }
-
- if (isBatchFile(target)) {
-#ifdef _WIN32
- return {getCmdPath(),
- QString("/C \"%1\"")
- .arg(QDir::toNativeSeparators(target.absoluteFilePath())),
- FileExecutionTypes::Executable};
-#else
- return {getCmdPath(), QString("\"%1\"").arg(target.absoluteFilePath()),
- FileExecutionTypes::Executable};
-#endif
- }
-
- if (isJavaFile(target)) {
- auto java = findJavaInstallation(target.absoluteFilePath());
-
- if (java.isEmpty()) {
-#ifdef _WIN32
- java = QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"),
- QString(),
- QObject::tr("Binary") + " (*.exe)");
-#else
- java = QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"),
- QString(),
- QObject::tr("Binary") + " (*)");
-#endif
- }
-
- if (!java.isEmpty()) {
- return {QFileInfo(java),
- QString("-jar \"%1\"")
- .arg(QDir::toNativeSeparators(target.absoluteFilePath())),
- FileExecutionTypes::Executable};
- }
- }
-
- return {{}, {}, FileExecutionTypes::Other};
-}
-
-} // namespace spawn
-
-#ifdef _WIN32
-namespace helper {
-
-bool helperExec(QWidget *parent, const std::wstring &moDirectory,
- const std::wstring &commandLine, BOOL async) {
- const std::wstring fileName = moDirectory + L"\\helper.exe";
-
- env::HandlePtr process;
-
- {
- SHELLEXECUTEINFOW execInfo = {};
-
- ULONG flags = SEE_MASK_FLAG_NO_UI;
- if (!async)
- flags |= SEE_MASK_NOCLOSEPROCESS;
-
- execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
- execInfo.fMask = flags;
- execInfo.hwnd = 0;
- execInfo.lpVerb = L"runas";
- execInfo.lpFile = fileName.c_str();
- execInfo.lpParameters = commandLine.c_str();
- execInfo.lpDirectory = moDirectory.c_str();
- execInfo.nShow = SW_SHOW;
-
- if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) {
- const auto e = GetLastError();
-
- spawn::dialogs::helperFailed(parent, e, "ShellExecuteExW()", fileName,
- moDirectory, commandLine);
-
- return false;
- }
-
- if (async) {
- return true;
- }
-
- process.reset(execInfo.hProcess);
- }
-
- const auto r = ::WaitForSingleObject(process.get(), INFINITE);
-
- if (r != WAIT_OBJECT_0) {
- // for WAIT_ABANDONED, the documentation doesn't mention that GetLastError()
- // returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so
- // use that instead
- const auto code =
- (r == WAIT_ABANDONED ? ERROR_ABANDONED_WAIT_0 : GetLastError());
-
- spawn::dialogs::helperFailed(parent, code, "WaitForSingleObject()",
- fileName, moDirectory, commandLine);
-
- return false;
- }
-
- DWORD exitCode = 0;
- if (!GetExitCodeProcess(process.get(), &exitCode)) {
- const auto e = GetLastError();
-
- spawn::dialogs::helperFailed(parent, e, "GetExitCodeProcess()", fileName,
- moDirectory, commandLine);
-
- return false;
- }
-
- return (exitCode == 0);
-}
-
-bool backdateBSAs(QWidget *parent, const std::wstring &moPath,
- const std::wstring &dataPath) {
- const std::wstring commandLine = std::format(L"backdateBSA \"{}\"", dataPath);
-
- return helperExec(parent, moPath, commandLine, FALSE);
-}
-
-bool adminLaunch(QWidget *parent, const std::wstring &moPath,
- const std::wstring &moFile, const std::wstring &workingDir) {
- const std::wstring commandLine =
- std::format(L"adminLaunch {} \"{}\" \"{}\"", ::GetCurrentProcessId(),
- moFile, workingDir);
-
- return helperExec(parent, moPath, commandLine, true);
-}
-
-} // namespace helper
-#endif // _WIN32
+/* +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 "spawn.h" + +#include "env.h" +#include "envmodule.h" +#include "fluorineconfig.h" +#include "protonlauncher.h" +#include "settings.h" +#include "settingsdialogworkarounds.h" +#include "shared/appconfig.h" + +#include <QApplication> +#include <QDir> +#include <QFile> +#include <QFileDialog> +#include <QMessageBox> +#include <QProcess> +#include <QRegularExpression> +#include <QSet> +#include <QSettings> +#include <QStandardPaths> +#include <QTextStream> +#include <QtDebug> + +#include <uibase/errorcodes.h> +#include <uibase/log.h> +#include <uibase/report.h> +#include <uibase/utility.h> + +#include <cerrno> +#include <cstring> +#include <signal.h> +#include <sys/types.h> + +using namespace MOBase; +using namespace MOShared; + +namespace spawn::dialogs +{ + +QString makeDetails(const SpawnParameters& sp, int code, const QString& more = {}) +{ + const bool cwdExists = + (sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists()); + + return QString("Error %1%2: %3\n" + " . binary: '%4'\n" + " . arguments: '%5'\n" + " . cwd: '%6'%7\n" + " . stdout: %8, stderr: %9, hooked: %10") + .arg(code) + .arg(more.isEmpty() ? more : ", " + more) + .arg(QString::fromUtf8(strerror(code))) + .arg(sp.binary.absoluteFilePath()) + .arg(sp.arguments) + .arg(sp.currentDirectory.absolutePath()) + .arg(cwdExists ? "" : " (not found)") + .arg(sp.stdOut == -1 ? "no" : "yes") + .arg(sp.stdErr == -1 ? "no" : "yes") + .arg(sp.hooked ? "yes" : "no"); +} + +QString makeContent(const SpawnParameters& sp, int code) +{ + if (code == ENOENT) { + return QObject::tr("The file '%1' does not exist.") + .arg(sp.binary.absoluteFilePath()); + } else if (code == EACCES) { + return QObject::tr("Permission denied when trying to start '%1'. " + "Check that the file is executable.") + .arg(sp.binary.absoluteFilePath()); + } + + if (!sp.currentDirectory.exists()) { + return QObject::tr("The working directory '%1' does not exist.") + .arg(sp.currentDirectory.absolutePath()); + } + + return QString::fromUtf8(strerror(code)); +} + +void spawnFailed(QWidget* parent, const SpawnParameters& sp, int code) +{ + const auto details = makeDetails(sp, code); + log::error("{}", details); + + const auto title = QObject::tr("Cannot launch program"); + const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName()); + + MOBase::TaskDialog(parent, title) + .main(mainText) + .content(makeContent(sp, code)) + .details(details) + .icon(QMessageBox::Critical) + .exec(); +} + +QMessageBox::StandardButton confirmStartSteam(QWidget* parent, const SpawnParameters& sp, + const QString& details) +{ + const auto title = QObject::tr("Launch Steam"); + const auto mainText = QObject::tr("This program requires Steam"); + const auto content = + QObject::tr("Mod Organizer has detected that this " + "program likely requires Steam to be " + "running to function properly."); + + return MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .details(details) + .icon(QMessageBox::Question) + .button({QObject::tr("Start Steam"), QMessageBox::Yes}) + .button({QObject::tr("Continue without starting Steam"), + QObject::tr("The program might fail to run."), QMessageBox::No}) + .button({QObject::tr("Cancel"), QMessageBox::Cancel}) + .remember("steamQuery", sp.binary.fileName()) + .exec(); +} + +QMessageBox::StandardButton confirmBlacklisted(QWidget* parent, + const SpawnParameters& sp, + Settings& settings) +{ + const auto title = QObject::tr("Blacklisted program"); + const auto mainText = + QObject::tr("The program %1 is blacklisted").arg(sp.binary.fileName()); + const auto content = QObject::tr( + "The program you are attempting to launch is blacklisted in the virtual " + "filesystem. This will likely prevent it from seeing any mods, INI files " + "or any other virtualized files."); + + const auto details = "Executable: " + sp.binary.fileName() + + "\nCurrent blacklist: " + settings.executablesBlacklist(); + + auto r = MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .details(details) + .icon(QMessageBox::Question) + .remember("blacklistedExecutable", sp.binary.fileName()) + .button({QObject::tr("Continue"), + QObject::tr("Your mods might not work."), QMessageBox::Yes}) + .button({QObject::tr("Change the blacklist"), QMessageBox::Retry}) + .button({QObject::tr("Cancel"), QMessageBox::Cancel}) + .exec(); + + if (r == QMessageBox::Retry) { + if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) { + r = QMessageBox::Cancel; + } + } + + return r; +} + +} // namespace spawn::dialogs + +namespace spawn +{ + +void logSpawning(const SpawnParameters& sp, const QString& realCmd) +{ + log::debug("spawning binary:\n" + " . exe: '{}'\n" + " . args: '{}'\n" + " . cwd: '{}'\n" + " . steam id: '{}'\n" + " . hooked: {}\n" + " . stdout: {}\n" + " . stderr: {}\n" + " . real cmd: '{}'", + sp.binary.absoluteFilePath(), sp.arguments, + sp.currentDirectory.absolutePath(), sp.steamAppID, sp.hooked, + (sp.stdOut == -1 ? "no" : "yes"), (sp.stdErr == -1 ? "no" : "yes"), + realCmd); +} + +uint32_t parseSteamAppId(const QString& steamAppId) +{ + bool ok = false; + const auto n = steamAppId.toUInt(&ok); + return (ok ? n : 0u); +} + +QString firstExistingSetting(const QSettings& settings, const QStringList& keys) +{ + for (const QString& key : keys) { + const QString value = settings.value(key).toString().trimmed(); + if (!value.isEmpty()) { + return value; + } + } + + return {}; +} + +// Strip "<letter>:"="..." entries (other than C:/Z:) from the +// [Software\\Wine\\Drives] section of system.reg. Without this, Wine +// recreates pruned dosdevices symlinks at the next prefix start from the +// registry, so the more-specific drive (e.g. X:\Games) keeps winning over +// Z:\home\user\Games during path canonicalisation. +static QStringList pruneDriveRegistry(const QString& prefixPath) +{ + const QString regPath = QDir(prefixPath).filePath("system.reg"); + QFile file(regPath); + if (!file.exists()) { + return {}; + } + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + MOBase::log::warn("pruneDriveRegistry: cannot open '{}'", regPath.toStdString()); + return {}; + } + + static const QRegularExpression sectionRe( + QStringLiteral(R"(^\[Software\\\\Wine\\\\Drives\])"), + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression driveRe(QStringLiteral(R"(^"([A-Za-z]):"\s*=)")); + + QStringList lines; + QStringList removed; + bool inDrives = false; + + QTextStream in(&file); + while (!in.atEnd()) { + const QString line = in.readLine(); + const QString trimmed = line.trimmed(); + + if (trimmed.startsWith('[')) { + inDrives = sectionRe.match(trimmed).hasMatch(); + lines.append(line); + continue; + } + + if (inDrives) { + const auto m = driveRe.match(trimmed); + if (m.hasMatch()) { + const QChar letter = m.captured(1).at(0).toLower(); + if (letter != QLatin1Char('c') && letter != QLatin1Char('z')) { + removed << QString(letter.toUpper()) + QLatin1Char(':'); + continue; + } + } + } + + lines.append(line); + } + file.close(); + + if (removed.isEmpty()) { + return {}; + } + + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { + MOBase::log::warn("pruneDriveRegistry: cannot rewrite '{}'", regPath.toStdString()); + return {}; + } + QTextStream out(&file); + for (const QString& l : lines) { + out << l << "\n"; + } + return removed; +} + +// Remove dosdevices/<letter>: symlinks for any drive other than C: and Z:, +// and strip the matching [Software\\Wine\\Drives] entries from system.reg +// so Wine doesn't recreate them at the next prefix start. External tooling +// (Faugus, manual edits, modlist installers) can re-add drives like X: that +// map subtrees of the host filesystem; Wine then prefers the more specific +// drive when canonicalising paths, which mangles binaries we passed in as +// Z:\home\user\... into X:\.... Keeping the allowed list minimal means MO2 +// can rely on Z: being the only host-mapped drive. +void pruneExtraDrives(const QString& prefixPath) +{ + static const QSet<QString> kept = {QStringLiteral("c:"), QStringLiteral("z:")}; + + const QString dosdevices = QDir(prefixPath).filePath("dosdevices"); + QDir dir(dosdevices); + if (!dir.exists()) { + return; + } + + QStringList removed; + for (const QString& entry : + dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries)) { + const QString lower = entry.toLower(); + if (lower.length() != 2 || !lower.endsWith(':') || !lower.at(0).isLetter()) { + continue; + } + if (kept.contains(lower)) { + continue; + } + if (QFile::remove(dir.filePath(entry))) { + removed << entry.toUpper(); + } + } + + const QStringList regRemoved = pruneDriveRegistry(prefixPath); + + QSet<QString> all; + for (const QString& d : removed) { + all.insert(d); + } + for (const QString& d : regRemoved) { + all.insert(d); + } + if (!all.isEmpty()) { + QStringList sorted(all.begin(), all.end()); + sorted.sort(); + MOBase::log::info( + "Pruned stale drive letters from prefix '{}': {} (symlinks: {}, " + "registry: {})", + prefixPath, sorted.join(QStringLiteral(", ")).toStdString(), + removed.join(QStringLiteral(", ")).toStdString(), + regRemoved.join(QStringLiteral(", ")).toStdString()); + } +} + +QString resolvePrefixPath() +{ + // The Fluorine config is authoritative: it's the prefix the user + // explicitly created through Settings > Proton and that Fluorine itself + // initialises with wineboot/DLL installs. Always prefer it. + if (auto cfg = FluorineConfig::load(); cfg.has_value() && cfg->prefixExists()) { + MOBase::log::debug("resolvePrefixPath: using Fluorine config prefix '{}'", + cfg->prefix_path); + return cfg->prefix_path.trimmed(); + } + + const Settings* settings = Settings::maybeInstance(); + if (settings == nullptr) { + return {}; + } + + // Fallbacks, in priority order. `fluorine/prefix_path` is set only by + // explicit user action (CLI `--prefix` or the instance creation wizard), + // so we trust it above the `Settings/*` keys that game-detection can + // populate automatically with an external manager's prefix (Heroic, + // Bottles, Lutris). Those external prefixes are fine as discovery hints + // but must not silently override the user's chosen Fluorine prefix — + // see issue #52. + const QSettings instanceSettings(settings->filename(), QSettings::IniFormat); + const QString explicitPath = + instanceSettings.value("fluorine/prefix_path").toString().trimmed(); + if (!explicitPath.isEmpty()) { + MOBase::log::debug("resolvePrefixPath: using explicit fluorine/prefix_path '{}'", + explicitPath); + return explicitPath; + } + + const QString fallback = firstExistingSetting( + instanceSettings, {"Settings/proton_prefix_path", "Settings/prefix_path", + "Proton/prefix_path"}); + if (!fallback.isEmpty()) { + MOBase::log::warn( + "resolvePrefixPath: falling back to auto-detected prefix '{}' — this " + "may point at an external manager's prefix (Heroic/Bottles). Create a " + "Fluorine prefix in Settings > Proton to override.", + fallback); + } + return fallback; +} + +QString resolveProtonPath() +{ + if (auto cfg = FluorineConfig::load(); cfg.has_value()) { + const QString protonPath = cfg->proton_path.trimmed(); + if (!protonPath.isEmpty()) { + return protonPath; + } + } + + const Settings* settings = Settings::maybeInstance(); + if (settings == nullptr) { + return {}; + } + + const QSettings instanceSettings(settings->filename(), QSettings::IniFormat); + return firstExistingSetting( + instanceSettings, + {"Settings/proton_path", "Proton/path", "fluorine/proton_path"}); +} + +int spawn(const SpawnParameters& sp, pid_t& processId) +{ + const QString bin = MOBase::normalizePathForHost(sp.binary.absoluteFilePath()); + QString cwd = MOBase::normalizePathForHost(sp.currentDirectory.absolutePath()); + + QStringList argList; + if (!sp.arguments.isEmpty()) { + argList = QProcess::splitCommand(sp.arguments); + } + + if (cwd.isEmpty()) { + cwd = QFileInfo(bin).absolutePath(); + } + + logSpawning(sp, bin + " " + sp.arguments); + + ProtonLauncher launcher; + launcher.setBinary(bin) + .setArguments(argList) + .setWorkingDir(cwd) + .setSteamAppId(parseSteamAppId(sp.steamAppID)); + + if (sp.useProton) { + // Read per-instance settings from the instance INI (not the global QSettings). + const Settings* instanceForLaunch = Settings::maybeInstance(); + bool useSteamDrm = true; + bool useSLR = true; + bool useSteamOverlay = false; + QString storeVariant; + if (instanceForLaunch) { + const QSettings instanceIni(instanceForLaunch->filename(), QSettings::IniFormat); + useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool(); + useSLR = instanceIni.value("fluorine/use_slr", true).toBool(); + useSteamOverlay = instanceIni.value("fluorine/steam_overlay", false).toBool(); + storeVariant = instanceIni.value("game_edition").toString().trimmed(); + } + + launcher.setSteamDrm(useSteamDrm) + .setSteamOverlay(useSteamOverlay) + .setStoreVariant(storeVariant) + .setUseSLR(useSLR); + + const QString prefixPath = resolvePrefixPath(); + if (prefixPath.isEmpty()) { + MOBase::log::error("No Wine prefix configured - cannot launch game. " + "Configure a prefix in Settings > Proton."); + return ENOENT; + } else if (!QDir(QDir(prefixPath).filePath("drive_c")).exists()) { + MOBase::log::error("Wine prefix '{}' does not contain drive_c/ - prefix is invalid", + prefixPath); + return ENOENT; + } else { + MOBase::log::info("Using Wine prefix: {}", prefixPath); + pruneExtraDrives(prefixPath); + launcher.setPrefix(prefixPath); + } + + const QString protonPath = resolveProtonPath(); + if (!protonPath.isEmpty()) { + launcher.setProtonPath(protonPath); + } + + const QString wrapper = + QSettings().value("fluorine/launch_wrapper").toString().trimmed(); + if (!wrapper.isEmpty()) { + launcher.setWrapper(wrapper); + } + + if (!sp.saveBindMountSource.isEmpty() && !sp.saveBindMountTarget.isEmpty()) { + launcher.setSavesBindMount(sp.saveBindMountSource, sp.saveBindMountTarget); + } + } else { + MOBase::log::info("Proton disabled for this executable, launching directly"); + } + + launcher.setUseTerminal(sp.useTerminal); + + const auto [ok, pid] = launcher.launch(); + if (!ok) { + return (errno != 0 ? errno : EIO); + } + + processId = static_cast<pid_t>(pid); + return 0; +} + +struct SteamStatus +{ + bool running = false; + bool accessible = false; +}; + +SteamStatus getSteamStatus() +{ + SteamStatus ss; + + QProcess pgrep; + pgrep.start("pgrep", QStringList() << "-x" << "steam"); + pgrep.waitForFinished(3000); + + if (pgrep.exitCode() == 0) { + ss.running = true; + ss.accessible = true; + log::debug("steam is running"); + } + + return ss; +} + +QString makeSteamArguments(const QString& username, const QString& password) +{ + QString args; + + if (username != "") { + args += "-login " + username; + + if (password != "") { + args += " " + password; + } + } + + return args; +} + +bool startSteam(QWidget* parent) +{ + QString steamPath; + + // Prefer ~/.steam/root/steam.sh, fall back to PATH. + const QString homeDir = QDir::homePath(); + const QString steamSh = homeDir + "/.steam/root/steam.sh"; + if (QFileInfo::exists(steamSh)) { + steamPath = steamSh; + } else { + steamPath = QStandardPaths::findExecutable("steam"); + } + + if (steamPath.isEmpty()) { + log::error("could not find steam installation"); + + const auto title = QObject::tr("Cannot start Steam"); + MOBase::TaskDialog(parent, title) + .main(title) + .content(QObject::tr("The Steam executable could not be found. " + "Make sure Steam is installed.")) + .icon(QMessageBox::Critical) + .button({QObject::tr("Continue without starting Steam"), + QObject::tr("The program may fail to launch."), QMessageBox::Yes}) + .button({QObject::tr("Cancel"), QMessageBox::Cancel}) + .exec(); + + return false; + } + + SpawnParameters sp; + sp.binary = QFileInfo(steamPath); + + pid_t pid = -1; + const auto e = spawn(sp, pid); + + if (e != 0) { + log::error("failed to start steam"); + return false; + } + + QMessageBox::information( + parent, QObject::tr("Waiting"), + QObject::tr("Please press OK once you're logged into steam.")); + + return true; +} + +bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDirectory, + const QString& steamAppID, const Settings& settings) +{ + static const std::vector<QString> steamFiles = {"libsteam_api.so"}; + + log::debug("checking steam"); + + if (!steamAppID.isEmpty()) { + env::set("SteamAPPId", steamAppID); + } else { + env::set("SteamAPPId", settings.steam().appID()); + } + + bool steamRequired = false; + QString details; + + for (const auto& file : steamFiles) { + const QFileInfo fi(gameDirectory.absoluteFilePath(file)); + if (fi.exists()) { + details = QString("managed game is located at '%1' and file '%2' exists") + .arg(gameDirectory.absolutePath()) + .arg(fi.absoluteFilePath()); + + log::debug("{}", details); + steamRequired = true; + + break; + } + } + + if (!steamRequired) { + log::debug("program doesn't seem to require steam"); + return true; + } + + auto ss = getSteamStatus(); + + if (!ss.running) { + log::debug("steam isn't running, asking to start steam"); + + const auto c = dialogs::confirmStartSteam(parent, sp, details); + + if (c == QMessageBox::Yes) { + log::debug("user wants to start steam"); + + if (!startSteam(parent)) { + return false; + } + + ss = getSteamStatus(); + if (!ss.running) { + log::error("steam is still not running, hoping for the best"); + return true; + } + } else if (c == QMessageBox::No) { + log::debug("user declined to start steam"); + return true; + } else { + log::debug("user cancelled"); + return false; + } + } + + return true; +} + +bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, Settings& settings) +{ + for (;;) { + if (!settings.isExecutableBlacklisted(sp.binary.fileName())) { + return true; + } + + const auto r = dialogs::confirmBlacklisted(parent, sp, settings); + + if (r != QMessageBox::Retry) { + return (r == QMessageBox::Yes); + } + } +} + +pid_t startBinary(QWidget* parent, const SpawnParameters& sp) +{ + pid_t pid = -1; + const auto e = spawn::spawn(sp, pid); + + if (e != 0) { + if (e == ENOENT && sp.useProton && !FluorineConfig::isSetup()) { + QMessageBox::critical( + parent, QObject::tr("No Wine Prefix"), + QObject::tr("No Wine prefix has been configured for this instance.\n\n" + "A Wine prefix is required to run Windows games through " + "Proton.\n\n" + "To create one, go to:\n" + " Settings → Wine/Proton\n\n" + "Set the prefix location, then click \"Create Prefix\" " + "to generate a new prefix.")); + } else { + dialogs::spawnFailed(parent, sp, e); + } + return -1; + } + + return pid; +} + +QString findJavaInstallation(const QString& jarFile) +{ + Q_UNUSED(jarFile); + + const auto javaPath = QStandardPaths::findExecutable("java"); + if (!javaPath.isEmpty()) { + return javaPath; + } + + return {}; +} + +bool isBatchFile(const QFileInfo& target) +{ + const auto batchExtensions = {"sh"}; + + 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.isExecutable() && target.isFile(); +} + +bool isJavaFile(const QFileInfo& target) +{ + return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0); +} + +QFileInfo getCmdPath() +{ + const auto p = env::get("SHELL"); + if (!p.isEmpty()) { + return QFileInfo(p); + } + + return QFileInfo("/bin/bash"); +} + +FileExecutionTypes getFileExecutionType(const QFileInfo& target) +{ + if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) { + return FileExecutionTypes::Executable; + } + + return FileExecutionTypes::Other; +} + +FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& target) +{ + if (isExeFile(target)) { + return {target, "", FileExecutionTypes::Executable}; + } + + if (isBatchFile(target)) { + return {getCmdPath(), QString("\"%1\"").arg(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") + " (*)"); + } + + if (!java.isEmpty()) { + return {QFileInfo(java), + QString("-jar \"%1\"") + .arg(QDir::toNativeSeparators(target.absoluteFilePath())), + FileExecutionTypes::Executable}; + } + } + + return {{}, {}, FileExecutionTypes::Other}; +} + +} // namespace spawn diff --git a/src/src/spawn.h b/src/src/spawn.h index da124b3..40207c3 100644 --- a/src/src/spawn.h +++ b/src/src/spawn.h @@ -23,14 +23,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QDir>
#include <QFileInfo>
-#ifdef _WIN32
-#define WIN32_LEAN_AND_MEAN
-#include <tchar.h>
-#include <windows.h>
-#else
#include <sys/types.h>
#include <unistd.h>
-#endif
class QProcess;
class Settings;
@@ -59,12 +53,8 @@ struct SpawnParameters bool hooked = false;
bool useProton = true;
bool useTerminal = false;
-#ifdef _WIN32
- HANDLE stdOut = INVALID_HANDLE_VALUE;
- HANDLE stdErr = INVALID_HANDLE_VALUE;
-#else
- int stdOut = -1;
- int stdErr = -1;
+ int stdOut = -1;
+ int stdErr = -1;
// When both are set and unprivileged user namespaces are available,
// spawn() wraps the launch so `saveBindMountTarget` becomes a live view
// of `saveBindMountSource` for the duration of the game process tree.
@@ -72,7 +62,6 @@ struct SpawnParameters // without symlinks, which Wine can accidentally replace.
QString saveBindMountSource;
QString saveBindMountTarget;
-#endif
};
bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDirectory,
@@ -81,14 +70,9 @@ bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDire bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, Settings& settings);
/**
- * @brief spawn a binary with Mod Organizer injected
- * @return the process handle
+ * @brief spawn a binary, returning the new pid (or -1 on failure)
**/
-#ifdef _WIN32
-HANDLE startBinary(QWidget* parent, const SpawnParameters& sp);
-#else
pid_t startBinary(QWidget* parent, const SpawnParameters& sp);
-#endif
enum class FileExecutionTypes
{
@@ -111,34 +95,4 @@ FileExecutionTypes getFileExecutionType(const QFileInfo& target); } // namespace spawn
-#ifdef _WIN32
-// convenience functions to work with the external helper program, which is used
-// to make changes on the system that require administrative rights, so that
-// ModOrganizer itself can run without special privileges
-//
-namespace helper
-{
-
-/**
- * @brief sets the last modified time for all .bsa-files in the target directory well
- *into the past
- * @param moPath absolute path to the modOrganizer base directory
- * @param dataPath the path taht contains the .bsa-files, usually the data directory of
- *the game
- **/
-bool backdateBSAs(QWidget* parent, const std::wstring& moPath,
- const std::wstring& dataPath);
-
-/**
- * @brief waits for the current process to exit and restarts it as an administrator
- * @param moPath absolute path to the modOrganizer base directory
- * @param moFile file name of modOrganizer
- * @param workingDir current working directory
- **/
-bool adminLaunch(QWidget* parent, const std::wstring& moPath,
- const std::wstring& moFile, const std::wstring& workingDir);
-
-} // namespace helper
-#endif // _WIN32
-
#endif // SPAWN_H
diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp index 8ac9fd6..09ab825 100644 --- a/src/src/wineprefix.cpp +++ b/src/src/wineprefix.cpp @@ -10,10 +10,8 @@ #include <log.h> #include <uibase/filesystemutilities.h> -#ifndef _WIN32 #include <sys/stat.h> #include <utime.h> -#endif namespace { @@ -36,8 +34,7 @@ bool copyFileWithParents(const QString& source, const QString& destination) return false; } -#ifndef _WIN32 - // QFile::copy() does not preserve timestamps. Restore the source file's + // QFile::copy() does not preserve timestamps. Restore the source file's // mtime so that games display the correct save date. struct stat srcStat; if (stat(source.toUtf8().constData(), &srcStat) == 0) { @@ -46,7 +43,6 @@ bool copyFileWithParents(const QString& source, const QString& destination) times.modtime = srcStat.st_mtime; utime(destination.toUtf8().constData(), ×); } -#endif return true; } |
