aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-28 21:22:45 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-28 21:22:45 -0500
commit5d42085dd36f14aa3385547947ccd6e6c5f03d50 (patch)
treead09bcb7acf58caba0cc53411d5bab4df0d17899 /src
parent2edfae46cc600079cb705e78f885df1fac269ce2 (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>
Diffstat (limited to 'src')
-rw-r--r--src/src/browserview.cpp3
-rw-r--r--src/src/commandline.cpp82
-rw-r--r--src/src/commandline.h7
-rw-r--r--src/src/datatab.cpp9
-rw-r--r--src/src/editexecutablesdialog.cpp3
-rw-r--r--src/src/env.cpp2030
-rw-r--r--src/src/env.h98
-rw-r--r--src/src/envdump.h7
-rw-r--r--src/src/envfs.cpp780
-rw-r--r--src/src/envmodule.cpp1515
-rw-r--r--src/src/envmodule.h366
-rw-r--r--src/src/envsecurity.cpp883
-rw-r--r--src/src/filetreeitem.cpp82
-rw-r--r--src/src/fluorinepaths.cpp4
-rw-r--r--src/src/iconfetcher.cpp4
-rw-r--r--src/src/installationmanager.cpp13
-rw-r--r--src/src/installationmanager.h5
-rw-r--r--src/src/instancemanager.cpp23
-rw-r--r--src/src/loglist.cpp16
-rw-r--r--src/src/main.cpp724
-rw-r--r--src/src/mainwindow.cpp61
-rw-r--r--src/src/mainwindow.h6
-rw-r--r--src/src/messagedialog.cpp3
-rw-r--r--src/src/moapplication.cpp44
-rw-r--r--src/src/modinfodialog.cpp4
-rw-r--r--src/src/modinfowithconflictinfo.cpp7
-rw-r--r--src/src/motddialog.cpp3
-rw-r--r--src/src/multiprocess.cpp97
-rw-r--r--src/src/organizercore.cpp74
-rw-r--r--src/src/organizercore.h2
-rw-r--r--src/src/organizerproxy.cpp18
-rw-r--r--src/src/overwriteinfodialog.cpp3
-rw-r--r--src/src/pch.h19
-rw-r--r--src/src/plugincontainer.cpp27
-rw-r--r--src/src/pluginlist.cpp81
-rw-r--r--src/src/problemsdialog.cpp3
-rw-r--r--src/src/processrunner.cpp3128
-rw-r--r--src/src/processrunner.h23
-rw-r--r--src/src/profile.cpp72
-rw-r--r--src/src/profilesdialog.cpp4
-rw-r--r--src/src/sanitychecks.cpp588
-rw-r--r--src/src/savestab.cpp17
-rw-r--r--src/src/selfupdater.cpp4
-rw-r--r--src/src/settings.cpp22
-rw-r--r--src/src/settingsdialogtheme.cpp21
-rw-r--r--src/src/settingsdialogworkarounds.cpp11
-rw-r--r--src/src/settingsutilities.cpp118
-rw-r--r--src/src/shared/appconfig.cpp6
-rw-r--r--src/src/shared/directoryentry.cpp43
-rw-r--r--src/src/shared/util.cpp772
-rw-r--r--src/src/shared/util.h4
-rw-r--r--src/src/shared/windows_compat.h12
-rw-r--r--src/src/shared/windows_error.cpp26
-rw-r--r--src/src/shared/windows_error.h12
-rw-r--r--src/src/spawn.cpp2278
-rw-r--r--src/src/spawn.h52
-rw-r--r--src/src/wineprefix.cpp6
57 files changed, 4200 insertions, 10125 deletions
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(), &times);
}
-#endif
return true;
}