From 5d42085dd36f14aa3385547947ccd6e6c5f03d50 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Tue, 28 Apr 2026 21:22:45 -0500 Subject: 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) --- src/src/browserview.cpp | 3 - src/src/commandline.cpp | 82 +- src/src/commandline.h | 7 - src/src/datatab.cpp | 9 - src/src/editexecutablesdialog.cpp | 3 - src/src/env.cpp | 2030 +++++---------------- src/src/env.h | 98 +- src/src/envdump.h | 7 +- src/src/envfs.cpp | 780 +------- src/src/envmodule.cpp | 1515 +++++----------- src/src/envmodule.h | 366 ++-- src/src/envsecurity.cpp | 883 ++-------- src/src/filetreeitem.cpp | 82 +- src/src/fluorinepaths.cpp | 4 - src/src/iconfetcher.cpp | 4 - src/src/installationmanager.cpp | 13 +- src/src/installationmanager.h | 5 +- src/src/instancemanager.cpp | 23 +- src/src/loglist.cpp | 16 +- src/src/main.cpp | 724 ++++---- src/src/mainwindow.cpp | 61 +- src/src/mainwindow.h | 6 - src/src/messagedialog.cpp | 3 - src/src/moapplication.cpp | 44 +- src/src/modinfodialog.cpp | 4 - src/src/modinfowithconflictinfo.cpp | 7 +- src/src/motddialog.cpp | 3 - src/src/multiprocess.cpp | 97 +- src/src/organizercore.cpp | 74 +- src/src/organizercore.h | 2 - src/src/organizerproxy.cpp | 18 +- src/src/overwriteinfodialog.cpp | 3 - src/src/pch.h | 19 - src/src/plugincontainer.cpp | 27 +- src/src/pluginlist.cpp | 81 +- src/src/problemsdialog.cpp | 3 - src/src/processrunner.cpp | 3128 ++++++++++++++------------------- src/src/processrunner.h | 23 +- src/src/profile.cpp | 72 +- src/src/profilesdialog.cpp | 4 - src/src/sanitychecks.cpp | 588 ++----- src/src/savestab.cpp | 17 +- src/src/selfupdater.cpp | 4 - src/src/settings.cpp | 22 - src/src/settingsdialogtheme.cpp | 21 +- src/src/settingsdialogworkarounds.cpp | 11 +- src/src/settingsutilities.cpp | 118 +- src/src/shared/appconfig.cpp | 6 - src/src/shared/directoryentry.cpp | 43 +- src/src/shared/util.cpp | 772 +++----- src/src/shared/util.h | 4 - src/src/shared/windows_compat.h | 12 +- src/src/shared/windows_error.cpp | 26 +- src/src/shared/windows_error.h | 12 +- src/src/spawn.cpp | 2278 ++++++++---------------- src/src/spawn.h | 52 +- src/src/wineprefix.cpp | 6 +- 57 files changed, 4200 insertions(+), 10125 deletions(-) (limited to 'src') 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 . #include #include #include -#ifdef _WIN32 -#include -#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 CommandLine::process(const std::wstring& line) { m_originalLine = line; try { -#ifdef _WIN32 - auto args = po::split_winmain(line); -#else - // Convert wstring args to vector for compatibility with wcommand_line_parser + // Convert wstring args to vector for compatibility with + // wcommand_line_parser. auto narrow_args = po::split_unix(QString::fromStdWString(line).toStdString()); std::vector 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 LaunchCommand::runEarly() { -#ifdef _WIN32 - // needs at least the working directory and process name - if (untouched().size() < 2) { - return 1; - } - - std::vector 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(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& 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 runEarly() override; - -#ifdef _WIN32 - int SpawnWaitProcess(LPCWSTR workingDirectory, LPCWSTR commandLine); - - LPCWSTR UntouchedCommandLineArguments(int parseArgCount, - std::vector& 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 . #include "ui_editexecutablesdialog.h" #include -#ifdef _WIN32 -#include -#endif #include #include 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 -#include - -#ifndef _WIN32 -#include -#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 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( - 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& Environment::loadedModules() const -{ - if (m_modules.empty()) { - m_modules = getLoadedModules(); - } - - return m_modules; -} - -std::vector Environment::runningProcesses() const -{ - return getRunningProcesses(); -} - -const WindowsInfo& Environment::windowsInfo() const -{ - if (!m_windows) { - m_windows.reset(new WindowsInfo); - } - - return *m_windows; -} - -const std::vector& 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 -Environment::onModuleLoaded(QObject* o, std::function 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( - GetProcAddress(ntdll.get(), "LdrRegisterDllNotification")); - - if (!LdrRegisterDllNotification) { - log::error("LdrRegisterDllNotification not found in ntdll.dll"); - return {}; - } - - auto context = std::make_unique(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(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 -Environment::onModuleLoaded(QObject*, std::function) -{ - // 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 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 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(bufferSize); - - DWORD realSize = ::GetEnvironmentVariableW(name.toStdWString().c_str(), buffer.get(), - static_cast(bufferSize)); - - if (realSize > bufferSize) { - bufferSize = realSize; - buffer = std::make_unique(bufferSize); - - realSize = ::GetEnvironmentVariableW(name.toStdWString().c_str(), buffer.get(), - static_cast(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(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(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 config( - static_cast(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 status( - static_cast(std::malloc(size))); - - const auto r = QueryServiceStatusEx( - s, SC_STATUS_PROCESS_INFO, reinterpret_cast(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 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 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 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(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 args; - - // first one is the filename - const auto wpath = targetInfo.absoluteFilePath().toStdWString(); - args[0] = reinterpret_cast(wpath.c_str()); - - // remaining are "" - std::fill(args.begin() + 1, args.end(), reinterpret_cast(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(&buffer), 0, - reinterpret_cast(&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 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; - -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(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 f) -{ - auto name = std::make_unique(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(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(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(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 +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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& Environment::loadedModules() const +{ + if (m_modules.empty()) { + m_modules = getLoadedModules(); + } + + return m_modules; +} + +std::vector Environment::runningProcesses() const +{ + return getRunningProcesses(); +} + +const WindowsInfo& Environment::windowsInfo() const +{ + if (!m_windows) { + m_windows.reset(new WindowsInfo); + } + + return *m_windows; +} + +const std::vector& 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 +Environment::onModuleLoaded(QObject*, std::function) +{ + // 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 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 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(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(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(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; - -// 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; - -// 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; - -// used by COMPtr, calls Release() as the deleter -// -struct COMReleaser -{ - void operator()(IUnknown* p) - { - if (p) { - p->Release(); - } - } -}; - -template -using COMPtr = std::unique_ptr; - -// used by LocalPtr, calls LocalFree() as the deleter -// -template -struct LocalFreer -{ - using pointer = T; - - void operator()(T p) { ::LocalFree(p); } -}; - -template -using LocalPtr = std::unique_ptr>; - -// used by the CoTaskMemPtr, calls CoTaskMemFree() as the deleter -// -template -struct CoTaskMemFreer -{ - using pointer = T; - - void operator()(T p) { ::CoTaskMemFree(p); } -}; - -template -using CoTaskMemPtr = std::unique_ptr>; - -#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; -#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 -#include - -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(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 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 g_handleClosers; - -void setHandleCloserThreadCount(std::size_t n) -{ - g_handleClosers.setMax(n); -} - -void forEachEntryImpl(void* cx, HandleCloserThread& hc, - std::vector>& 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(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(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 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 -#include -#include -#include -#include -#include -#include +#include +#include + +#include #include -#include -#include - -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(ts.tv_sec) * 10000000ULL + - static_cast(ts.tv_nsec) / 100ULL + EPOCH_DIFF; - FILETIME ft; - ft.dwLowDateTime = static_cast(winTime & 0xFFFFFFFF); - ft.dwHighDateTime = static_cast(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(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() {} - return out; +Directory::Directory(std::wstring_view n) + : name(n.begin(), n.end()), lcname(MOShared::ToLowerCopy(name)) +{} + +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( + t - decltype(t)::clock::now() + system_clock::now()); + const auto epoch = sysTime.time_since_epoch(); + const auto ticks = duration_cast>>(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(winTicks & 0xFFFFFFFF); + ft.dwHighDateTime = static_cast((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(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 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(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 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 -#include - -#ifndef _WIN32 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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(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(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(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(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 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 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(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::children() -{ - return m_children; -} - -const std::vector& Process::children() const -{ - return m_children; -} - -// -- Free functions -- - -#ifdef _WIN32 - -std::vector 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 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 -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 getRunningProcesses() -{ - std::vector 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& 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 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 buffer(std::malloc(bufferSize)); - auto* ids = static_cast(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 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& 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 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 getLoadedModules() -{ - std::vector 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 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 getRunningProcesses() -{ - std::vector 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(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( - 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& 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(std::strtol(line.c_str() + 5, nullptr, 10)); - } - } - - return 0; -} - -#endif // _WIN32 - -} // namespace env +#include "envmodule.h" +#include "env.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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(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::children() +{ + return m_children; +} + +const std::vector& Process::children() const +{ + return m_children; +} + +// -- Free functions -- + +std::vector getLoadedModules() +{ + std::vector v; + + std::ifstream maps("/proc/self/maps"); + if (!maps.is_open()) { + log::error("failed to open /proc/self/maps"); + return {}; + } + + std::set 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 getRunningProcesses() +{ + std::vector 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(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( + 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& 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(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 -#include - -#ifdef _WIN32 -#include -#else -#include -#include -#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; -#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; -#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& children(); - const std::vector& children() const; - -private: -#ifdef _WIN32 - DWORD m_pid; - mutable std::optional m_ppid; -#else - pid_t m_pid; - mutable std::optional m_ppid; -#endif - mutable std::optional m_name; - std::vector m_children; -}; - -std::vector getRunningProcesses(); -std::vector 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 +#include + +#include +#include + +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; + +// 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& children(); + const std::vector& children() const; + +private: + pid_t m_pid; + mutable std::optional m_ppid; + mutable std::optional m_name; + std::vector m_children; +}; + +std::vector getRunningProcesses(); +std::vector 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 -#include - -#ifdef _WIN32 -#include -#include -#include -#include -#pragma comment(lib, "Wbemuuid.lib") - -#include -#include -#include -#pragma comment(lib, "advapi32.lib") -#else -#include -#include -#include -#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 - void query(const std::string& q, F&& f) - { - if (!m_locator || !m_service) { - return; - } - - auto enumerator = getEnumerator(q); - if (!enumerator) { - return; - } - - for (;;) { - COMPtr 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 m_locator; - COMPtr 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(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 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(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 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((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 getSecurityProductsFromWMI() -{ - std::map 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 v; - - for (auto&& p : map) { - v.push_back(p.second); - } - - return v; -} - -std::optional getWindowsFirewall() -{ - HRESULT hr = 0; - - COMPtr 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(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 getSecurityProducts() -{ - std::vector 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 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 sd( - static_cast(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 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 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 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(nameSize); - auto wsDomain = std::make_unique(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 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 +#include + +#include +#include +#include + +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 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 #include -#ifndef _WIN32 #include #include -#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> 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 . #include #include -#ifdef _WIN32 -#include -#endif - #include #include @@ -452,13 +448,10 @@ InstallationResult InstallationManager::testOverwrite(GuessedValue& 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 . #include #include -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#endif + #include #include 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 -#include -#include - -#ifdef _WIN32 -#include -#else -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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 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 +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 . #include #endif -#ifdef _WIN32 -#include -#endif - #include #include #include @@ -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(msecs) * 10000ULL) + 116444736000000000ULL; - ft.dwLowDateTime = static_cast(ticks & 0xFFFFFFFF); + // Win32 FILETIME = 100-ns intervals since 1601-01-01. + qint64 msecs = fi.birthTime().toMSecsSinceEpoch(); + uint64_t ticks = (static_cast(msecs) * 10000ULL) + + 116444736000000000ULL; + ft.dwLowDateTime = static_cast(ticks & 0xFFFFFFFF); ft.dwHighDateTime = static_cast(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 #endif -#ifdef _WIN32 -// Sigh - just for HANDLE -#define WIN32_LEAN_AND_MEAN -#include -#endif - #include #include #include 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 . #include "ui_messagedialog.h" #include #include -#ifdef _WIN32 -#include -#endif #include 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 . #include "organizercore.h" #include "sanitychecks.h" #include "settings.h" -#ifndef _WIN32 #include "fluorineconfig.h" #include "fluorinepaths.h" #include "fuseconnector.h" #include "wineprefix.h" + #include -#endif #include #include "shared/appconfig.h" #include "shared/util.h" @@ -56,15 +55,6 @@ along with Mod Organizer. If not, see . #include #include -#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& 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 . #include "organizercore.h" #include "ui_motddialog.h" #include "utility.h" -#ifdef _WIN32 -#include -#endif #include 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 -#ifndef _WIN32 #include -#endif #include #include @@ -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(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(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(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(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 #include // for qUtf8Printable, etc -#ifdef _WIN32 -#include -#include -#include // for _tcsicmp -#include -#endif - #include #include #include // 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 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 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(pid). + const pid_t pid = static_cast(reinterpret_cast(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(reinterpret_cast(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 . #include #include #include -#ifdef _WIN32 -#include -#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 #include -#ifdef _WIN32 -// keep this header separated to avoid ordering issue since this must be -// included before DbgHelp.h -#include - -// windows -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#else // linux #include #include @@ -59,7 +41,6 @@ #include #include #include "shared/windows_compat.h" -#endif // boost #include 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 . #include "shared/windows_error.h" #include "viewmarkingscrollbar.h" -#ifndef _WIN32 #include #include -#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(newWriteTime.dwHighDateTime) << 32) | - newWriteTime.dwLowDateTime; - // Convert from Windows epoch (1601) to Unix epoch (1970) - ticks -= 116444736000000000ULL; - time_t secs = static_cast(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(newWriteTime.dwHighDateTime) << 32) | + newWriteTime.dwLowDateTime; + // Convert from Windows epoch (1601) to Unix epoch (1970). + ticks -= 116444736000000000ULL; + time_t secs = static_cast(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 -#ifdef _WIN32 -#include -#endif #include #include #include 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 -#include -#include -#include -#ifndef _WIN32 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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 - // /Root/... so they point to /... 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 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 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 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 timedWait(HANDLE handle, DWORD pid, - UILocker::Session *ls, - std::chrono::milliseconds wait, - std::atomic &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(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(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 &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 &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 &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 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 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(reinterpret_cast(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//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//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(*name))) continue; - - const pid_t pid = static_cast(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> buildProcChildrenMap() { - std::unordered_map> 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(*name))) { - continue; - } - - const pid_t pid = static_cast(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(std::strtol(line.c_str() + 5, nullptr, 10)); - break; - } - } - - if (ppid > 0) { - children[ppid].push_back(pid); - } - } - - closedir(proc); - return children; -} - -std::unordered_set collectDescendants( - pid_t root, const std::unordered_map> &children) { - std::unordered_set out; - std::deque 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(*name))) - continue; - - const pid_t pid = static_cast(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(WEXITSTATUS(status)); - } - - if (WIFSIGNALED(status)) { - return static_cast(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(std::max(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 &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 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::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::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( - static_cast(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(reinterpret_cast(m_handle.get())); - const QFileInfo binary = m_sp.binary; - QPointer 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(WEXITSTATUS(status)); - } else if (WIFSIGNALED(status)) { - exitCode = static_cast(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(static_cast(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(reinterpret_cast(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 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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 + // /Root/... so they point to /... 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(reinterpret_cast(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//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//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(*name))) + continue; + + const pid_t pid = static_cast(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> buildProcChildrenMap() +{ + std::unordered_map> 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(*name))) { + continue; + } + + const pid_t pid = static_cast(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(std::strtol(line.c_str() + 5, nullptr, 10)); + break; + } + } + + if (ppid > 0) { + children[ppid].push_back(pid); + } + } + + closedir(proc); + return children; +} + +std::unordered_set collectDescendants( + pid_t root, const std::unordered_map>& children) +{ + std::unordered_set out; + std::deque 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(*name))) + continue; + + const pid_t pid = static_cast(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(WEXITSTATUS(status)); + } + + if (WIFSIGNALED(status)) { + return static_cast(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(std::max(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& 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 r; + + if (shouldRunShell()) { + r = runShell(); + } else { + r = runBinary(); + } + + if (r) { + return *r; + } + + return postRun(); +} + +std::optional 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::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( + static_cast(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(reinterpret_cast(m_handle.get())); + const QFileInfo binary = m_sp.binary; + QPointer 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(WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + exitCode = static_cast(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(static_cast(pid))); + return postRun(); +} + +DWORD ProcessRunner::exitCode() const +{ + return m_exitCode; +} + +pid_t ProcessRunner::getProcessHandle() const +{ + return static_cast(reinterpret_cast(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 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 -#ifndef _WIN32 #include #include -#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 . #include // for QStringList #include // for qUtf8Printable -#ifdef _WIN32 -#include -#endif - #include // for assert #include // for UINT_MAX, INT_MAX, etc #include // 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 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 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 sections = splitDZString(buffer.data()); - - // now iterate over all sections and retrieve a list of keys in each - for (std::vector::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 keys = splitDZString(buffer.data()); - - for (std::vector::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 . #include #include -#ifdef _WIN32 -#include -#endif - #include 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 -#include -#include - -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(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(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> 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 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> getSystemDirectories() -{ - // folder ids and display names for logging - const std::vector> 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> 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 +#include +#include + +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 . #include -#ifdef _WIN32 -#include //for VS_FIXEDFILEINFO, GetLastError -#endif - #include #include #include //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 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 #include -#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 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(); - 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 -#ifndef _WIN32 #include #include -#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(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(wname.c_str()); - cred.CredentialBlob = const_cast(blob); - cred.CredentialBlobSize = static_cast(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; - -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(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 . -*/ - -#include "util.h" -#include "../env.h" -#include "../mainwindow.h" -#include "windows_error.h" -#include -#include -#ifndef _WIN32 -#include -#include -#include -#include -#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(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(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(text.c_str()), static_cast(text.size())); - return text; -} - -std::string ToLowerCopy(const std::string& text) -{ - std::string result(text); - CharLowerBuffA(const_cast(result.c_str()), static_cast(result.size())); - return result; -} - -std::wstring& ToLowerInPlace(std::wstring& text) -{ - CharLowerBuffW(const_cast(text.c_str()), static_cast(text.size())); - return text; -} - -std::wstring ToLowerCopy(const std::wstring& text) -{ - std::wstring result(text); - CharLowerBuffW(const_cast(result.c_str()), static_cast(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(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 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 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 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> 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( - 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(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 . +*/ + +#include "util.h" +#include "../env.h" +#include "../mainwindow.h" +#include "windows_error.h" + +#include +#include + +#include +#include +#include +#include + +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(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(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 ; 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 #include @@ -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 . */ #include "windows_error.h" + +#include +#include #include 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 . #ifndef WINDOWS_ERROR_H #define WINDOWS_ERROR_H -#include - -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#else #include #include -#endif +#include 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 . -*/ - -#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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include "envsecurity.h" -#include "envwindows.h" -#include "shared/windows_error.h" -#include -#else -#include -#include -#include -#include -#include -#include -#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 ":"="..." 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/: 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 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 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(wcommandLine.c_str()), nullptr, nullptr, - inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi); - } else { - success = ::CreateProcess( - nullptr, const_cast(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); - 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 steamFiles = {"steam_api.dll", - "steam_api64.dll"}; -#else - static const std::vector 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(reinterpret_cast(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 . +*/ + +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +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 ":"="..." 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/: 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 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 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); + 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 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 . #include #include -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#else #include #include -#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 #include -#ifndef _WIN32 #include #include -#endif namespace { @@ -36,8 +34,7 @@ bool copyFileWithParents(const QString& source, const QString& destination) return false; } -#ifndef _WIN32 - // QFile::copy() does not preserve timestamps. Restore the source file's + // QFile::copy() does not preserve timestamps. Restore the source file's // mtime so that games display the correct save date. struct stat srcStat; if (stat(source.toUtf8().constData(), &srcStat) == 0) { @@ -46,7 +43,6 @@ bool copyFileWithParents(const QString& source, const QString& destination) times.modtime = srcStat.st_mtime; utime(destination.toUtf8().constData(), ×); } -#endif return true; } -- cgit v1.3.1