From 5467e210668eb24de9a4429ef0ddcbc6442e8932 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Thu, 19 Feb 2026 17:25:48 -0600 Subject: Add Python init diagnostics, AppImage build system, misc Linux fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - python: switch Py_InitializeEx → Py_InitializeFromConfig for explicit error reporting; add dladdr + fprintf diagnostics to trace which DSO Python symbols resolve to and whether Py_IsInitialized succeeds - python: log RTLD_NOLOAD vs fresh-load outcome when promoting libpython to RTLD_GLOBAL - build: add Docker-based AppImage build script (build.sh), update Dockerfile and build-inner.sh for AppImage-only workflow - build: remove Flatpak support entirely; move AppImage metadata (desktop/metainfo/icon) from flatpak/ to data/ - Add Monster Hunter Wilds basic_games plugin - .gitignore: exclude squashfs-root, build-source, flatpak-repo, __pycache__, fomod-plus-settings.ini - Various Linux port fixes across wineprefix, processrunner, proton launcher, FUSE connector, instance manager, download manager, sanitychecks, and NaK Rust paths/runtime_wrap Co-Authored-By: Claude Sonnet 4.6 --- src/plugins/rootbuilder.py | 52 +---- src/src/CMakeLists.txt | 45 ----- src/src/commandline.cpp | 9 +- src/src/createinstancedialog.cpp | 5 +- src/src/createinstancedialogpages.cpp | 19 +- src/src/downloadlist.cpp | 12 ++ src/src/downloadlist.h | 2 + src/src/downloadlistview.cpp | 5 + src/src/downloadmanager.cpp | 150 +++++++------- src/src/filedialogmemory.cpp | 9 - src/src/fluorinepaths.cpp | 43 +++- src/src/fluorinepaths.h | 6 +- src/src/fuseconnector.cpp | 180 ----------------- src/src/fuseconnector.h | 11 -- src/src/instancemanager.cpp | 14 +- src/src/instancemanagerdialog.cpp | 33 ++-- src/src/instancemanagerdialog.ui | 10 + src/src/mainwindow.cpp | 110 ++++++++--- src/src/mainwindow.h | 7 +- src/src/moapplication.cpp | 85 +++++--- src/src/nxmhandler_linux.cpp | 68 +++---- src/src/organizercore.cpp | 120 ++++++------ src/src/plugincontainer.cpp | 214 +++++++++++++------- src/src/process_helper_main.cpp | 359 ---------------------------------- src/src/processrunner.cpp | 28 +-- src/src/processrunner.h | 3 - src/src/protonlauncher.cpp | 275 ++++++-------------------- src/src/protonlauncher.h | 9 +- src/src/sanitychecks.cpp | 5 +- src/src/settingsdialogpaths.cpp | 24 +-- src/src/settingsdialogproton.cpp | 58 ++---- src/src/spawn.cpp | 2 +- src/src/spawn.h | 1 - src/src/vfs/vfs_helper_main.cpp | 337 ------------------------------- src/src/wineprefix.cpp | 10 +- 35 files changed, 641 insertions(+), 1679 deletions(-) delete mode 100644 src/src/process_helper_main.cpp delete mode 100644 src/src/vfs/vfs_helper_main.cpp (limited to 'src') diff --git a/src/plugins/rootbuilder.py b/src/plugins/rootbuilder.py index dcc6706..342e1a0 100644 --- a/src/plugins/rootbuilder.py +++ b/src/plugins/rootbuilder.py @@ -57,18 +57,6 @@ def _walk_files(root_dir: str): yield os.path.join(dirpath, name) -def _host_cp(src: str, dst: str) -> bool: - """Copy a file via the host OS (bypasses Flatpak sandbox restrictions).""" - try: - result = subprocess.run( - ["flatpak-spawn", "--host", "cp", "-f", "--reflink=auto", "--", src, dst], - capture_output=True, timeout=30, - ) - return result.returncode == 0 - except (subprocess.SubprocessError, OSError): - return False - - def _ensure_readable(path: str): """Ensure a file has owner-read permission (mod archives sometimes strip it).""" try: @@ -99,8 +87,6 @@ def _reflink_copy(src: str, dst: str): return except OSError as e: last_err = f"{e.strerror} (errno {e.errno})" - if _IN_FLATPAK and _host_cp(src, dst): - return raise OSError(f"Root Builder: failed to copy {src} -> {dst}: {last_err}") @@ -113,33 +99,6 @@ def _ensure_writable(path: str): pass -_IN_FLATPAK = os.path.exists("/.flatpak-info") - - -def _host_rm(path: str) -> bool: - """Remove a file via the host OS (bypasses Flatpak sandbox restrictions).""" - try: - result = subprocess.run( - ["flatpak-spawn", "--host", "rm", "-f", "--", path], - capture_output=True, timeout=10, - ) - return result.returncode == 0 - except (subprocess.SubprocessError, OSError): - return False - - -def _host_mv(src: str, dst: str) -> bool: - """Move a file via the host OS (bypasses Flatpak sandbox restrictions).""" - try: - result = subprocess.run( - ["flatpak-spawn", "--host", "mv", "-f", "--", src, dst], - capture_output=True, timeout=10, - ) - return result.returncode == 0 - except (subprocess.SubprocessError, OSError): - return False - - def _force_remove(path: str) -> bool: """Remove a file, fixing permissions if needed. Returns True on success.""" try: @@ -154,8 +113,6 @@ def _force_remove(path: str) -> bool: return True except OSError: pass - if _IN_FLATPAK and _host_rm(path): - return True qWarning(f"Root Builder: could not remove {path}") return False @@ -556,11 +513,10 @@ class RootBuilder(mobase.IPluginTool): _force_remove(dst) shutil.move(bak, dst) except OSError: - if not (_IN_FLATPAK and _host_mv(bak, dst)): - qWarning( - f"Root Builder: could not restore backup " - f"{bak} -> {dst}" - ) + qWarning( + f"Root Builder: could not restore backup " + f"{bak} -> {dst}" + ) # Clean up backup dir backup_dir = os.path.join(storage, _BACKUP_SUBDIR) diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt index ab8b6c3..e85f730 100644 --- a/src/src/CMakeLists.txt +++ b/src/src/CMakeLists.txt @@ -48,14 +48,6 @@ list(REMOVE_ITEM ORGANIZER_SOURCES list(REMOVE_ITEM ORGANIZER_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/usvfsconnector.h) -# VFS helper has its own main() — exclude from organizer -list(REMOVE_ITEM ORGANIZER_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/vfs/vfs_helper_main.cpp) - -# Process helper has its own main() — exclude from organizer -list(REMOVE_ITEM ORGANIZER_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/process_helper_main.cpp) - # Remove WebEngine-dependent sources when not available if(NOT Qt6WebEngineWidgets_FOUND) list(REMOVE_ITEM ORGANIZER_SOURCES @@ -127,43 +119,6 @@ target_compile_definitions(organizer PRIVATE $<$:MO2_WEBENGINE>) if(NOT WIN32) - # ── Standalone VFS helper for Flatpak (runs on host via flatpak-spawn) ── - add_executable(mo2-vfs-helper - vfs/vfs_helper_main.cpp - vfs/vfstree.cpp - vfs/mo2filesystem.cpp - vfs/inodetable.cpp - vfs/overwritemanager.cpp) - # Prefer static libfuse3 so the helper is fully self-contained (Flatpak - # runs it on the host via flatpak-spawn where SDK .so files don't exist). - # Fall back to shared linking for source builds where libfuse3.a isn't - # available (the helper still works since libfuse3.so is on the host). - find_library(_fuse3_static libfuse3.a PATHS ${FUSE3_LIBRARY_DIRS} NO_DEFAULT_PATH) - if(NOT _fuse3_static) - find_library(_fuse3_static libfuse3.a) - endif() - target_link_directories(mo2-vfs-helper PRIVATE ${FUSE3_LIBRARY_DIRS}) - if(_fuse3_static) - target_link_libraries(mo2-vfs-helper PRIVATE - -Wl,-Bstatic -lfuse3 -Wl,-Bdynamic - Threads::Threads) - else() - message(STATUS "libfuse3.a not found, linking mo2-vfs-helper against shared libfuse3") - target_link_libraries(mo2-vfs-helper PRIVATE - PkgConfig::FUSE3 - Threads::Threads) - endif() - target_include_directories(mo2-vfs-helper PRIVATE ${FUSE3_INCLUDE_DIRS}) - target_compile_definitions(mo2-vfs-helper PRIVATE FUSE_USE_VERSION=35) - target_compile_features(mo2-vfs-helper PRIVATE cxx_std_23) - - # ── Standalone process helper for Flatpak game launching ── - # Keeps the flatpak-spawn proxy alive while monitoring the game process tree. - add_executable(mo2-process-helper - process_helper_main.cpp) - target_link_libraries(mo2-process-helper PRIVATE Threads::Threads) - target_compile_features(mo2-process-helper PRIVATE cxx_std_23) - option(MO2_BUNDLE_7Z_RUNTIME "Copy a Linux 7z module into organizer/dlls" ON) option(MO2_STAGE_PYTHON_PLUGIN_PAYLOAD "Stage shipped Python plugin payload into build plugins/ for Linux runs" ON) diff --git a/src/src/commandline.cpp b/src/src/commandline.cpp index 09aabb8..4613350 100644 --- a/src/src/commandline.cpp +++ b/src/src/commandline.cpp @@ -1025,7 +1025,6 @@ std::optional CreatePortableCommand::runEarly() // Generate ModOrganizer.sh { - const bool flatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info")); const QString launchPath = QDir(instanceDir).filePath("ModOrganizer.sh"); QFile launchFile(launchPath); if (launchFile.open(QIODevice::WriteOnly | QIODevice::Text)) { @@ -1033,12 +1032,8 @@ std::optional CreatePortableCommand::runEarly() out << "#!/bin/bash\n"; out << "# Auto-generated by fluorine-manager\n"; out << "INSTANCE_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n"; - if (flatpak) { - out << "exec flatpak run com.fluorine.manager --instance \"${INSTANCE_DIR}\" \"$@\"\n"; - } else { - out << "MO2_BIN=\"$(which ModOrganizer 2>/dev/null || echo ModOrganizer)\"\n"; - out << "exec \"${MO2_BIN}\" --instance \"${INSTANCE_DIR}\" \"$@\"\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 | diff --git a/src/src/createinstancedialog.cpp b/src/src/createinstancedialog.cpp index 89506e6..3bce099 100644 --- a/src/src/createinstancedialog.cpp +++ b/src/src/createinstancedialog.cpp @@ -67,8 +67,9 @@ private: return; } - // root directory - QDir d(cs[0]); + // root directory — on Unix, splitting "/home/user" produces ["", "home", "user"] + // so cs[0] is empty; use "/" as the root to keep paths absolute. + QDir d(cs[0].isEmpty() ? QStringLiteral("/") : cs[0]); // for each directory after the root for (int i = 1; i < cs.size(); ++i) { diff --git a/src/src/createinstancedialogpages.cpp b/src/src/createinstancedialogpages.cpp index 3138a17..5f134a0 100644 --- a/src/src/createinstancedialogpages.cpp +++ b/src/src/createinstancedialogpages.cpp @@ -14,19 +14,6 @@ namespace cid { -// On Flatpak the native file dialog returns XDG portal FUSE paths that may -// not properly expose directory contents. Returns options that force the -// Qt built-in dialog when running inside Flatpak. -static QFileDialog::Options flatpakSafeOptions() -{ - QFileDialog::Options opts; -#ifndef _WIN32 - if (qEnvironmentVariableIsSet("FLATPAK_ID")) - opts |= QFileDialog::DontUseNativeDialog; -#endif - return opts; -} - using namespace MOBase; using MOBase::TaskDialog; @@ -329,7 +316,7 @@ void GamePage::select(IPluginGame* game, const QString& dir) const auto path = QFileDialog::getExistingDirectory( &m_dlg, QObject::tr("Find game installation for %1").arg(game->displayGameName()), - {}, flatpakSafeOptions()); + {}, {}); if (path.isEmpty()) { // cancelled @@ -377,7 +364,7 @@ void GamePage::select(IPluginGame* game, const QString& dir) void GamePage::selectCustom() { const auto path = QFileDialog::getExistingDirectory( - &m_dlg, QObject::tr("Find game installation"), {}, flatpakSafeOptions()); + &m_dlg, QObject::tr("Find game installation"), {}, {}); if (path.isEmpty()) { // reselect the previous button @@ -1115,7 +1102,7 @@ void PathsPage::onChanged() void PathsPage::browse(QLineEdit* e) { const auto s = - QFileDialog::getExistingDirectory(&m_dlg, {}, e->text(), flatpakSafeOptions()); + QFileDialog::getExistingDirectory(&m_dlg, {}, e->text(), {}); if (s.isNull() || s.isEmpty()) { return; } diff --git a/src/src/downloadlist.cpp b/src/src/downloadlist.cpp index dd35ac2..457fab2 100644 --- a/src/src/downloadlist.cpp +++ b/src/src/downloadlist.cpp @@ -37,6 +37,7 @@ DownloadList::DownloadList(OrganizerCore& core, QObject* parent) { connect(&m_manager, SIGNAL(update(int)), this, SLOT(update(int))); connect(&m_manager, SIGNAL(aboutToUpdate()), this, SLOT(aboutToUpdate())); + connect(&m_manager, SIGNAL(stateChanged(int,int)), this, SLOT(rowChanged(int))); } int DownloadList::rowCount(const QModelIndex& parent) const @@ -107,6 +108,9 @@ QMimeData* DownloadList::mimeData(const QModelIndexList& indexes) const QVariant DownloadList::data(const QModelIndex& index, int role) const { + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) + return QVariant(); + bool pendingDownload = index.row() >= m_manager.numTotalDownloads(); if (role == Qt::DisplayRole) { if (pendingDownload) { @@ -256,6 +260,14 @@ void DownloadList::update(int row) log::error("invalid row {} in download list, update failed", row); } +void DownloadList::rowChanged(int row) +{ + if (row >= 0 && row < rowCount()) { + emit dataChanged(index(row, 0, QModelIndex()), + index(row, columnCount(QModelIndex()) - 1, QModelIndex())); + } +} + bool DownloadList::lessThanPredicate(const QModelIndex& left, const QModelIndex& right) { int leftIndex = left.row(); diff --git a/src/src/downloadlist.h b/src/src/downloadlist.h index 73e499d..1be2ea5 100644 --- a/src/src/downloadlist.h +++ b/src/src/downloadlist.h @@ -94,6 +94,8 @@ public slots: void aboutToUpdate(); + void rowChanged(int row); + private: DownloadManager& m_manager; Settings& m_settings; diff --git a/src/src/downloadlistview.cpp b/src/src/downloadlistview.cpp index 8928e6c..961af97 100644 --- a/src/src/downloadlistview.cpp +++ b/src/src/downloadlistview.cpp @@ -50,6 +50,11 @@ void DownloadProgressDelegate::paint(QPainter* painter, sourceIndex = index; } + if (sourceIndex.row() < 0 || sourceIndex.row() >= m_Manager->numTotalDownloads() + m_Manager->numPendingDownloads()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + bool pendingDownload = (sourceIndex.row() >= m_Manager->numTotalDownloads()); if (sourceIndex.column() == DownloadList::COL_STATUS && !pendingDownload && m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_DOWNLOADING) { diff --git a/src/src/downloadmanager.cpp b/src/src/downloadmanager.cpp index ae3b94e..457a58e 100644 --- a/src/src/downloadmanager.cpp +++ b/src/src/downloadmanager.cpp @@ -229,13 +229,13 @@ DownloadManager::DownloadManager(NexusInterface* nexusInterface, QObject* parent m_ParentWidget(nullptr) { m_OrganizerCore = dynamic_cast(parent); - connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, - SLOT(directoryChanged(QString))); - m_TimeoutTimer.setSingleShot(false); - connect(&m_TimeoutTimer, &QTimer::timeout, this, - &DownloadManager::checkDownloadTimeout); - m_TimeoutTimer.start(5 * 1000); -} + connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, + SLOT(directoryChanged(QString))); + m_TimeoutTimer.setSingleShot(false); + connect(&m_TimeoutTimer, &QTimer::timeout, this, + &DownloadManager::checkDownloadTimeout); + m_TimeoutTimer.start(5 * 1000); +} DownloadManager::~DownloadManager() { @@ -953,19 +953,19 @@ void DownloadManager::removeDownload(int index, bool deleteFile) refreshList(); } -void DownloadManager::cancelDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("cancel: invalid download index %1").arg(index)); - return; - } - - DownloadInfo* info = m_ActiveDownloads.at(index); - if (info->m_State == STATE_DOWNLOADING || info->m_State == STATE_STARTED || - info->m_State == STATE_PAUSING) { - setState(info, STATE_CANCELING); - } -} +void DownloadManager::cancelDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("cancel: invalid download index %1").arg(index)); + return; + } + + DownloadInfo* info = m_ActiveDownloads.at(index); + if (info->m_State == STATE_DOWNLOADING || info->m_State == STATE_STARTED || + info->m_State == STATE_PAUSING) { + setState(info, STATE_CANCELING); + } +} void DownloadManager::pauseDownload(int index) { @@ -1613,20 +1613,20 @@ void DownloadManager::setState(DownloadManager::DownloadInfo* info, row = i; break; } - } - info->m_State = state; - switch (state) { - case STATE_CANCELING: { - // Force termination so the download transitions through finished(). - if (info->m_Reply != nullptr && info->m_Reply->isRunning()) { - info->m_Reply->abort(); - } - } break; - case STATE_PAUSED: { - info->m_Reply->abort(); - info->m_Output.close(); - m_DownloadPaused(row); - } break; + } + info->m_State = state; + switch (state) { + case STATE_CANCELING: { + // Force termination so the download transitions through finished(). + if (info->m_Reply != nullptr && info->m_Reply->isRunning()) { + info->m_Reply->abort(); + } + } break; + case STATE_PAUSED: { + info->m_Reply->abort(); + info->m_Output.close(); + m_DownloadPaused(row); + } break; case STATE_ERROR: { info->m_Reply->abort(); info->m_Output.close(); @@ -2313,12 +2313,10 @@ void DownloadManager::downloadFinished(int index) "There may be an issue with the Nexus servers.")); emit update(-1); } else if (info->isPausedState() || info->m_State == STATE_PAUSING) { - emit aboutToUpdate(); info->m_Output.close(); createMetaFile(info); emit update(index); } else { - emit aboutToUpdate(); QString url = info->m_Urls[info->m_CurrentUrl]; if (info->m_FileInfo->userData.contains("downloadMap")) { foreach (const QVariant& server, @@ -2420,46 +2418,46 @@ void DownloadManager::managedGameChanged(MOBase::IPluginGame const* managedGame) m_ManagedGame = managedGame; } -void DownloadManager::checkDownloadTimeout() -{ - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - DownloadInfo* info = m_ActiveDownloads[i]; - if (info->m_State != STATE_DOWNLOADING || info->m_Reply == nullptr || - !info->m_Reply->isOpen()) { - continue; - } - - const bool stalled = - (info->m_StartTime.elapsed() - info->m_DownloadTimeLast) > (5 * 1000); - if (!stalled) { - continue; - } - - pauseDownload(i); - downloadFinished(i); - - // downloadFinished() can remove the download entry, so find it again. - const int index = indexByInfo(info); - if (index < 0) { - continue; - } - - if (info->m_Tries <= 0) { - emit showMessage(tr("Download stalled repeatedly and was paused. " - "Please retry manually.")); - continue; - } - - --info->m_Tries; - log::warn("download '{}' stalled, retrying ({} retries left)", info->m_FileName, - info->m_Tries); - resumeDownloadInt(index); - emit update(index); - } -} - -void DownloadManager::writeData(DownloadInfo* info) -{ +void DownloadManager::checkDownloadTimeout() +{ + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + DownloadInfo* info = m_ActiveDownloads[i]; + if (info->m_State != STATE_DOWNLOADING || info->m_Reply == nullptr || + !info->m_Reply->isOpen()) { + continue; + } + + const bool stalled = + (info->m_StartTime.elapsed() - info->m_DownloadTimeLast) > (5 * 1000); + if (!stalled) { + continue; + } + + pauseDownload(i); + downloadFinished(i); + + // downloadFinished() can remove the download entry, so find it again. + const int index = indexByInfo(info); + if (index < 0) { + continue; + } + + if (info->m_Tries <= 0) { + emit showMessage(tr("Download stalled repeatedly and was paused. " + "Please retry manually.")); + continue; + } + + --info->m_Tries; + log::warn("download '{}' stalled, retrying ({} retries left)", info->m_FileName, + info->m_Tries); + resumeDownloadInt(index); + emit update(index); + } +} + +void DownloadManager::writeData(DownloadInfo* info) +{ if (info != nullptr) { qint64 ret = info->m_Output.write(info->m_Reply->readAll()); if (ret < info->m_Reply->size()) { diff --git a/src/src/filedialogmemory.cpp b/src/src/filedialogmemory.cpp index 3c4ea11..390843f 100644 --- a/src/src/filedialogmemory.cpp +++ b/src/src/filedialogmemory.cpp @@ -72,15 +72,6 @@ QString FileDialogMemory::getExistingDirectory(const QString& dirID, QWidget* pa } } -#ifndef _WIN32 - // On Flatpak, the native file dialog goes through the XDG Desktop Portal, - // which returns FUSE paths (/run/user/.../doc/...) that may not properly - // expose directory contents. Use the Qt built-in dialog to get real paths. - if (qEnvironmentVariableIsSet("FLATPAK_ID")) { - options |= QFileDialog::DontUseNativeDialog; - } -#endif - QString result = QFileDialog::getExistingDirectory(parent, caption, currentDir, options); diff --git a/src/src/fluorinepaths.cpp b/src/src/fluorinepaths.cpp index 98cb7aa..b6e6633 100644 --- a/src/src/fluorinepaths.cpp +++ b/src/src/fluorinepaths.cpp @@ -3,16 +3,18 @@ #include #include +#include +#include #include #include -static const QString OldRoot = - QDir::homePath() + "/.local/share/fluorine"; +static const QString OldFlatpakRoot = + QDir::homePath() + "/.var/app/com.fluorine.manager"; QString fluorineDataDir() { - return QDir::homePath() + "/.var/app/com.fluorine.manager"; + return QDir::homePath() + "/.local/share/fluorine"; } void fluorineMigrateDataDir() @@ -21,9 +23,42 @@ void fluorineMigrateDataDir() return; #endif - const QString oldRoot = OldRoot; + const QString oldRoot = OldFlatpakRoot; const QString newRoot = fluorineDataDir(); + // Always check for config.json migration, even if data was already moved. + // The Flatpak stored config.json under its sandboxed XDG_CONFIG_HOME + // (~/.var/app/com.fluorine.manager/config/), but outside Flatpak + // QStandardPaths returns ~/.config/. + { + const QString oldConfigJson = oldRoot + "/config/fluorine/config.json"; + QString configRoot = QStandardPaths::writableLocation( + QStandardPaths::ConfigLocation); + if (configRoot.isEmpty()) { + configRoot = QDir::homePath() + "/.config"; + } + const QString newConfigJson = + QDir(configRoot).filePath("fluorine/config.json"); + if (QFile::exists(oldConfigJson) && !QFile::exists(newConfigJson)) { + QDir().mkpath(QFileInfo(newConfigJson).dir().absolutePath()); + if (QFile::copy(oldConfigJson, newConfigJson)) { + fprintf(stderr, "[fluorine] Migrated config.json to %s\n", + qUtf8Printable(newConfigJson)); + // Update prefix_path if it references the old Flatpak root + if (auto cfg = FluorineConfig::load()) { + if (cfg->prefix_path.startsWith(oldRoot)) { + cfg->prefix_path.replace(oldRoot, newRoot); + cfg->save(); + fprintf(stderr, "[fluorine] updated config prefix_path\n"); + } + } + } else { + fprintf(stderr, "[fluorine] FAILED to copy config.json to %s\n", + qUtf8Printable(newConfigJson)); + } + } + } + // Already migrated or old path never existed if (QFile::exists(oldRoot + "/MOVED.txt")) { return; diff --git a/src/src/fluorinepaths.h b/src/src/fluorinepaths.h index ab7f119..2bdde33 100644 --- a/src/src/fluorinepaths.h +++ b/src/src/fluorinepaths.h @@ -3,11 +3,11 @@ #include -/// Returns the Fluorine data directory: ~/.var/app/com.fluorine.manager +/// Returns the Fluorine data directory: ~/.local/share/fluorine QString fluorineDataDir(); -/// One-time migration from ~/.local/share/fluorine/ back to -/// ~/.var/app/com.fluorine.manager/. Call before initLogging(). +/// One-time migration from ~/.var/app/com.fluorine.manager/ to +/// ~/.local/share/fluorine/. Call before initLogging(). void fluorineMigrateDataDir(); #endif // FLUORINEPATHS_H diff --git a/src/src/fuseconnector.cpp b/src/src/fuseconnector.cpp index 2c5454f..3ba5c69 100644 --- a/src/src/fuseconnector.cpp +++ b/src/src/fuseconnector.cpp @@ -1,6 +1,5 @@ #include "fuseconnector.h" -#include "fluorinepaths.h" #include "settings.h" #include "vfs/vfstree.h" @@ -49,53 +48,6 @@ namespace { namespace fs = std::filesystem; -bool isFlatpak() -{ - static const bool result = QFile::exists(QStringLiteral("/.flatpak-info")); - return result; -} - -bool waitForHelperLine(QProcess* proc, const char* expected, int timeoutMs) -{ - const QByteArray target(expected); - const auto deadline = - std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); - - while (proc->state() == QProcess::Running) { - if (proc->canReadLine()) { - const QByteArray line = proc->readLine().trimmed(); - if (line == target) { - return true; - } - if (line.startsWith("error:")) { - log::error("VFS helper: {}", QString::fromUtf8(line)); - return false; - } - continue; - } - - const auto remaining = std::chrono::duration_cast( - deadline - std::chrono::steady_clock::now()) - .count(); - if (remaining <= 0) { - break; - } - proc->waitForReadyRead(static_cast(remaining)); - } - - return false; -} - -bool sendHelperCommand(QProcess* proc, const char* command, int timeoutMs) -{ - proc->write(command); - proc->write("\n"); - if (!proc->waitForBytesWritten(1000)) { - return false; - } - return waitForHelperLine(proc, "ok", timeoutMs); -} - std::string decodeProcMountField(const std::string& in) { std::string out; @@ -158,17 +110,6 @@ bool runUnmountCommand(const QString& program, const QStringList& args) return p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0; }; - // In Flatpak: try sandbox-local unmount first (mount was likely created - // inside the sandbox), then fall back to host-side unmount. - if (isFlatpak()) { - if (tryRun(program, args)) { - return true; - } - QStringList spawnArgs = {QStringLiteral("--host"), program}; - spawnArgs.append(args); - return tryRun(QStringLiteral("flatpak-spawn"), spawnArgs); - } - return tryRun(program, args); } @@ -268,10 +209,6 @@ bool FuseConnector::mount( tryCleanupStaleMount(QString::fromStdString(m_mountPoint)); - if (isFlatpak()) { - return mountViaHelper(overwrite_dir, game_dir, data_dir_name, mods); - } - const fs::path overwritePath(m_overwriteDir); m_stagingDir = (overwritePath.parent_path() / "VFS_staging").string(); @@ -370,23 +307,6 @@ void FuseConnector::unmount() return; } - if (m_helperProcess) { - sendHelperCommand(m_helperProcess, "quit", 10000); - m_helperProcess->waitForFinished(5000); - if (m_helperProcess->state() != QProcess::NotRunning) { - m_helperProcess->kill(); - m_helperProcess->waitForFinished(2000); - } - delete m_helperProcess; - m_helperProcess = nullptr; - m_mounted = false; - setFuseMountPointForCrashCleanup(nullptr); - cleanupExternalMappings(); - log::debug("VFS helper stopped, FUSE unmounted from {}", - QString::fromStdString(m_mountPoint)); - return; - } - if (m_session != nullptr) { fuse_session_exit(m_session); fuse_session_unmount(m_session); @@ -435,16 +355,6 @@ void FuseConnector::rebuild( m_dataDirName = data_dir_name.toStdString(); m_lastMods = mods; - if (m_helperProcess) { - const QString dataDir = fluorineDataDir(); - const QString configPath = QDir(dataDir).filePath("vfs.cfg"); - writeVfsConfig(configPath, QString::fromStdString(m_mountPoint), - overwrite_dir, QString::fromStdString(m_gameDir), - data_dir_name, mods); - sendHelperCommand(m_helperProcess, "rebuild", 10000); - return; - } - if (m_context == nullptr) { return; } @@ -700,11 +610,6 @@ void FuseConnector::flushStagingLive() return; } - if (m_helperProcess) { - sendHelperCommand(m_helperProcess, "flush", 30000); - return; - } - if (m_context == nullptr) { return; } @@ -784,88 +689,3 @@ void FuseConnector::tryCleanupStaleMount(const QString& path) doUnmount(path); } -bool FuseConnector::mountViaHelper( - const QString& overwrite_dir, const QString& game_dir, - const QString& data_dir_name, - const std::vector>& mods) -{ - const QString dataDir = fluorineDataDir(); - const QString configPath = QDir(dataDir).filePath("vfs.cfg"); - const QString helperBin = - QDir(dataDir).filePath("bin/mo2-vfs-helper"); - - if (!QFile::exists(helperBin)) { - throw FuseConnectorException( - QObject::tr("VFS helper not found: %1").arg(helperBin)); - } - - writeVfsConfig(configPath, QString::fromStdString(m_mountPoint), overwrite_dir, - game_dir, data_dir_name, mods); - - m_helperProcess = new QProcess(this); - m_helperProcess->setProcessChannelMode(QProcess::SeparateChannels); - m_helperProcess->start(QStringLiteral("flatpak-spawn"), - {QStringLiteral("--host"), helperBin, configPath}); - - if (!m_helperProcess->waitForStarted(5000)) { - const QString err = QString::fromUtf8(m_helperProcess->readAllStandardError()); - delete m_helperProcess; - m_helperProcess = nullptr; - throw FuseConnectorException( - QObject::tr("Failed to start VFS helper process. %1").arg(err)); - } - - if (!waitForHelperLine(m_helperProcess, "mounted", 10000)) { - const QString err = QString::fromUtf8(m_helperProcess->readAllStandardError()); - const QString out = QString::fromUtf8(m_helperProcess->readAllStandardOutput()); - log::error("VFS helper stderr: {}", err); - log::error("VFS helper stdout: {}", out); - m_helperProcess->kill(); - m_helperProcess->waitForFinished(2000); - delete m_helperProcess; - m_helperProcess = nullptr; - throw FuseConnectorException( - QObject::tr("VFS helper failed to mount FUSE. %1").arg(err)); - } - - m_mounted = true; - setFuseMountPointForCrashCleanup(m_mountPoint.c_str()); - log::debug("FUSE mounted via helper on {}", - QString::fromStdString(m_mountPoint)); - return true; -} - -void FuseConnector::writeVfsConfig( - const QString& configPath, const QString& mount_point, - const QString& overwrite_dir, const QString& game_dir, - const QString& data_dir_name, - const std::vector>& mods) -{ - QDir().mkpath(QFileInfo(configPath).absolutePath()); - - QFile file(configPath); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - throw FuseConnectorException( - QObject::tr("Failed to write VFS config: %1").arg(configPath)); - } - - QTextStream out(&file); - out << "mount_point=" << mount_point << "\n"; - out << "game_dir=" << game_dir << "\n"; - out << "data_dir_name=" << data_dir_name << "\n"; - out << "overwrite_dir=" << overwrite_dir << "\n"; - - if (!m_customOutputDir.empty()) { - out << "output_dir=" << QString::fromStdString(m_customOutputDir) << "\n"; - } - - for (const auto& [name, path] : mods) { - out << "mod=" << QString::fromStdString(name) << "|" - << QString::fromStdString(path) << "\n"; - } - - for (const auto& [relPath, realPath] : m_extraVfsFiles) { - out << "extra_file=" << QString::fromStdString(relPath) << "|" - << QString::fromStdString(realPath) << "\n"; - } -} diff --git a/src/src/fuseconnector.h b/src/src/fuseconnector.h index cf99d64..9a34e55 100644 --- a/src/src/fuseconnector.h +++ b/src/src/fuseconnector.h @@ -7,8 +7,6 @@ #include #include -class QProcess; - #include #include #include @@ -88,15 +86,6 @@ private: struct fuse_session* m_session = nullptr; std::thread m_fuseThread; bool m_mounted = false; - - QProcess* m_helperProcess = nullptr; - bool mountViaHelper(const QString& overwrite_dir, const QString& game_dir, - const QString& data_dir_name, - const std::vector>& mods); - void writeVfsConfig(const QString& configPath, const QString& mount_point, - const QString& overwrite_dir, const QString& game_dir, - const QString& data_dir_name, - const std::vector>& mods); }; #endif diff --git a/src/src/instancemanager.cpp b/src/src/instancemanager.cpp index 1269d26..7821761 100644 --- a/src/src/instancemanager.cpp +++ b/src/src/instancemanager.cpp @@ -55,21 +55,15 @@ Instance::Instance(QString dir, bool portable, QString profileName) if (m_portable && !m_dir.isEmpty()) { const QString launchPath = QDir(m_dir).filePath("ModOrganizer.sh"); if (!QFileInfo::exists(launchPath)) { - const bool flatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info")); - 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"; - if (flatpak) { - out << "exec flatpak run com.fluorine.manager --instance " - "\"${INSTANCE_DIR}\" \"$@\"\n"; - } else { - out << "MO2_BIN=\"$(which ModOrganizer 2>/dev/null || echo " - "ModOrganizer)\"\n"; - out << "exec \"${MO2_BIN}\" --instance \"${INSTANCE_DIR}\" \"$@\"\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, diff --git a/src/src/instancemanagerdialog.cpp b/src/src/instancemanagerdialog.cpp index cd4b865..5cdaecf 100644 --- a/src/src/instancemanagerdialog.cpp +++ b/src/src/instancemanagerdialog.cpp @@ -8,6 +8,7 @@ #include "shared/appconfig.h" #include "shared/util.h" #include "ui_instancemanagerdialog.h" +#include #include #include #include @@ -214,6 +215,15 @@ InstanceManagerDialog::InstanceManagerDialog(PluginContainer& pc, QWidget* paren deleteInstance(); }); + connect(ui->steamDrmCheckBox, &QCheckBox::toggled, [&](bool checked) { + const auto* inst = singleSelection(); + if (!inst) return; + const QString ini = inst->iniPath(); + if (ini.isEmpty()) return; + QSettings s(ini, QSettings::IniFormat); + s.setValue("fluorine/steam_drm", checked); + }); + connect(ui->switchToInstance, &QPushButton::clicked, [&] { openSelectedInstance(); }); @@ -772,16 +782,23 @@ void InstanceManagerDialog::fillData(const Instance& ii) ui->gameName->setText(ii.gameName()); ui->gameDir->setText(ii.gameDirectory()); - // read prefix info from the instance's INI + // read prefix info and fluorine settings from the instance's INI { const QString ini = ii.iniPath(); if (!ini.isEmpty() && QFile::exists(ini)) { QSettings s(ini, QSettings::IniFormat); ui->prefixPath->setText(s.value("Settings/proton_prefix_path").toString()); ui->protonVersion->setText(s.value("fluorine/proton_name").toString()); + + ui->steamDrmCheckBox->blockSignals(true); + ui->steamDrmCheckBox->setChecked(s.value("fluorine/steam_drm", true).toBool()); + ui->steamDrmCheckBox->blockSignals(false); } else { ui->prefixPath->clear(); ui->protonVersion->clear(); + ui->steamDrmCheckBox->blockSignals(true); + ui->steamDrmCheckBox->setChecked(false); + ui->steamDrmCheckBox->blockSignals(false); } } @@ -822,6 +839,9 @@ void InstanceManagerDialog::clearData() ui->gameDir->clear(); ui->prefixPath->clear(); ui->protonVersion->clear(); + ui->steamDrmCheckBox->blockSignals(true); + ui->steamDrmCheckBox->setChecked(false); + ui->steamDrmCheckBox->blockSignals(false); setButtonsEnabled(false); @@ -845,18 +865,9 @@ void InstanceManagerDialog::setButtonsEnabled(bool b) void InstanceManagerDialog::openExistingPortable() { // On Flatpak, the native file dialog goes through the XDG Desktop Portal, - // which returns FUSE paths (/run/user/.../doc/...) that may not properly - // expose directory contents. Use the Qt built-in dialog to get real paths. - QFileDialog::Options opts; -#ifndef _WIN32 - if (qEnvironmentVariableIsSet("FLATPAK_ID")) { - opts |= QFileDialog::DontUseNativeDialog; - } -#endif - const QString dir = QFileDialog::getExistingDirectory( this, tr("Select portable instance folder"), - QStandardPaths::writableLocation(QStandardPaths::HomeLocation), opts); + QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); if (dir.isEmpty()) { return; diff --git a/src/src/instancemanagerdialog.ui b/src/src/instancemanagerdialog.ui index 2b94a8d..2a1d136 100644 --- a/src/src/instancemanagerdialog.ui +++ b/src/src/instancemanagerdialog.ui @@ -314,6 +314,16 @@ + + + + Steam DRM integration + + + Enable Steam DRM bridge for this instance. Disable for GOG or DRM-free games to prevent crashes. + + + diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index 425d20c..7aa6964 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -544,11 +544,12 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, QApplication::instance()->installEventFilter(this); - scheduleCheckForProblems(); - refreshExecutablesList(); - updatePinnedExecutables(); - resetActionIcons(); - processUpdates(); + scheduleCheckForProblems(); + refreshExecutablesList(); + updatePinnedExecutables(); + resetActionIcons(); + resetButtonIcons(); + processUpdates(); ui->modList->updateModCount(); ui->espList->updatePluginCount(); @@ -572,8 +573,8 @@ void MainWindow::setupModList() }); } -void MainWindow::resetActionIcons() -{ +void MainWindow::resetActionIcons() +{ // this is a bit of a hack // // the .qss files have historically set qproperty-icon by id and these ids @@ -635,12 +636,38 @@ void MainWindow::resetActionIcons() } } - // update the button for the potentially new icon - updateProblemsButton(); -} - -MainWindow::~MainWindow() -{ + // update the button for the potentially new icon + updateProblemsButton(); +} + +void MainWindow::resetButtonIcons() +{ + // Some icon-only QPushButtons can lose their icons after stylesheet repolish on + // Linux/AppImage. Re-apply explicit resource icons to keep these controls visible. + ui->listOptionsBtn->setIcon(QIcon::fromTheme( + "preferences-system", QIcon(":/MO/gui/settings"))); + ui->openFolderMenu->setIcon( + QIcon::fromTheme("folder-open", QIcon(":/MO/gui/open_folder"))); + ui->restoreModsButton->setIcon( + QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore"))); + ui->saveModsButton->setIcon( + QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup"))); + ui->sortButton->setIcon(QIcon::fromTheme("view-sort-ascending", QIcon(":/MO/gui/sort"))); + ui->restoreButton->setIcon( + QIcon::fromTheme("edit-undo", QIcon(":/MO/gui/restore"))); + ui->saveButton->setIcon(QIcon::fromTheme("document-save", QIcon(":/MO/gui/backup"))); + + ui->listOptionsBtn->setIconSize(QSize(16, 16)); + ui->openFolderMenu->setIconSize(QSize(16, 16)); + ui->restoreModsButton->setIconSize(QSize(16, 16)); + ui->saveModsButton->setIconSize(QSize(16, 16)); + ui->sortButton->setIconSize(QSize(16, 16)); + ui->restoreButton->setIconSize(QSize(16, 16)); + ui->saveButton->setIconSize(QSize(16, 16)); +} + +MainWindow::~MainWindow() +{ try { cleanup(); @@ -712,10 +739,11 @@ void MainWindow::allowListResize() ui->espList->header()->setStretchLastSection(true); } -void MainWindow::updateStyle(const QString&) -{ - resetActionIcons(); -} +void MainWindow::updateStyle(const QString&) +{ + resetActionIcons(); + resetButtonIcons(); +} void MainWindow::resizeEvent(QResizeEvent* event) { @@ -1885,10 +1913,10 @@ bool MainWindow::refreshProfiles(bool selectProfile, QString newProfile) return profileBox->count() > 1; } -void MainWindow::refreshExecutablesList() -{ - QAbstractItemModel* model = ui->executablesListBox->model(); - +void MainWindow::refreshExecutablesList() +{ + QAbstractItemModel* model = ui->executablesListBox->model(); + auto add = [&](const QString& title, const QFileInfo& binary) { QIcon icon; if (!binary.fileName().isEmpty()) { @@ -1904,16 +1932,36 @@ void MainWindow::refreshExecutablesList() Qt::SizeHintRole); }; - ui->executablesListBox->setEnabled(false); - ui->executablesListBox->clear(); - - add(tr(""), {}); - - for (const auto& exe : *m_OrganizerCore.executablesList()) { - if (!exe.hide()) { - add(exe.title(), exe.binaryInfo()); - } - } + ui->executablesListBox->setEnabled(false); + ui->executablesListBox->clear(); + + add(tr(""), {}); + + auto shouldSkipInLaunchDropdown = [](const Executable& exe) { + const QString title = exe.title().trimmed(); + const QString lower = title.toLower(); + if (lower == QStringLiteral("proton") || lower == QStringLiteral("prefix") || + lower.startsWith(QStringLiteral("proton ")) || + lower.startsWith(QStringLiteral("prefix "))) { + return true; + } + + return false; + }; + + for (const auto& exe : *m_OrganizerCore.executablesList()) { + if (exe.hide()) { + continue; + } + + if (shouldSkipInLaunchDropdown(exe)) { + log::debug("Skipping internal executable entry '{}' from launch dropdown", + exe.title()); + continue; + } + + add(exe.title(), exe.binaryInfo()); + } if (ui->executablesListBox->count() == 1) { // all executables are hidden, add an empty one to at least be able to diff --git a/src/src/mainwindow.h b/src/src/mainwindow.h index b931c35..3e22fd1 100644 --- a/src/src/mainwindow.h +++ b/src/src/mainwindow.h @@ -440,9 +440,10 @@ private slots: void toolBar_customContextMenuRequested(const QPoint& point); void removeFromToolbar(QAction* action); - void about(); - - void resetActionIcons(); + void about(); + + void resetActionIcons(); + void resetButtonIcons(); private slots: // ui slots // actions diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp index 29e21c4..fd60745 100644 --- a/src/src/moapplication.cpp +++ b/src/src/moapplication.cpp @@ -41,9 +41,9 @@ along with Mod Organizer. If not, see . #include "shared/util.h" #include "thread_utils.h" #include "tutorialmanager.h" -#include -#include -#include +#include +#include +#include #include #include #include @@ -51,9 +51,10 @@ along with Mod Organizer. If not, see . #include #include #include -#include -#include -#include +#include +#include +#include +#include #ifdef _WIN32 // see addDllsToPath() below @@ -269,30 +270,43 @@ void MOApplication::firstTimeSetup(MOMultiProcess& multiProcess) Qt::QueuedConnection); } -int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) -{ - TimeThis tt("MOApplication setup()"); +int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) +{ + TimeThis tt("MOApplication setup()"); + std::fprintf(stderr, "[setup-diag] setup() entered\n"); + std::fflush(stderr); // makes plugin data path available to plugins, see // IOrganizer::getPluginDataPath() MOBase::details::setPluginDataPath(OrganizerCore::pluginDataPath()); // figuring out the current instance - m_instance = getCurrentInstance(forceSelect); - if (!m_instance) { - return 1; - } + m_instance = getCurrentInstance(forceSelect); + if (!m_instance) { + std::fprintf(stderr, "[setup-diag] getCurrentInstance() returned null\n"); + std::fflush(stderr); + return 1; + } + std::fprintf(stderr, "[setup-diag] instance dir='%s' ini='%s'\n", + qUtf8Printable(m_instance->directory()), + qUtf8Printable(m_instance->iniPath())); + std::fflush(stderr); // first time the data path is available, set the global property and log // directory, then log a bunch of debug stuff const QString dataPath = m_instance->directory(); setProperty("dataPath", dataPath); - if (!setLogDirectory(dataPath)) { - reportError(tr("Failed to create log folder.")); - InstanceManager::singleton().clearCurrentInstance(); - return 1; - } + if (!setLogDirectory(dataPath)) { + std::fprintf(stderr, "[setup-diag] setLogDirectory() failed for '%s'\n", + qUtf8Printable(dataPath)); + std::fflush(stderr); + reportError(tr("Failed to create log folder.")); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } + std::fprintf(stderr, "[setup-diag] setLogDirectory() ok\n"); + std::fflush(stderr); #ifdef _WIN32 log::debug("command line: '{}'", QString::fromWCharArray(GetCommandLineW())); @@ -322,8 +336,11 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) } // loading settings - m_settings.reset(new Settings(m_instance->iniPath(), true)); - log::getDefault().setLevel(m_settings->diagnostics().logLevel()); + m_settings.reset(new Settings(m_instance->iniPath(), true)); + std::fprintf(stderr, "[setup-diag] settings loaded from '%s'\n", + qUtf8Printable(m_instance->iniPath())); + std::fflush(stderr); + log::getDefault().setLevel(m_settings->diagnostics().logLevel()); log::debug("using ini at '{}'", m_settings->filename()); OrganizerCore::setGlobalCoreDumpType(m_settings->diagnostics().coreDumpType()); @@ -358,20 +375,30 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) tt.start("MOApplication::doOneRun() OrganizerCore"); log::debug("initializing core"); - m_core.reset(new OrganizerCore(*m_settings)); - if (!m_core->bootstrap()) { - reportError(tr("Failed to set up data paths.")); - InstanceManager::singleton().clearCurrentInstance(); - return 1; - } + m_core.reset(new OrganizerCore(*m_settings)); + std::fprintf(stderr, "[setup-diag] organizer core constructed\n"); + std::fflush(stderr); + if (!m_core->bootstrap()) { + std::fprintf(stderr, "[setup-diag] organizer core bootstrap failed\n"); + std::fflush(stderr); + reportError(tr("Failed to set up data paths.")); + InstanceManager::singleton().clearCurrentInstance(); + return 1; + } + std::fprintf(stderr, "[setup-diag] organizer core bootstrap ok\n"); + std::fflush(stderr); // plugins tt.start("MOApplication::doOneRun() plugins"); log::debug("initializing plugins"); - m_plugins = std::make_unique(m_core.get()); - m_plugins->loadPlugins(); - log::debug("all plugins loaded"); + m_plugins = std::make_unique(m_core.get()); + std::fprintf(stderr, "[setup-diag] plugin container constructed, loading plugins...\n"); + std::fflush(stderr); + m_plugins->loadPlugins(); + std::fprintf(stderr, "[setup-diag] plugin loading finished\n"); + std::fflush(stderr); + log::debug("all plugins loaded"); // instance log::debug("entering setupInstanceLoop..."); diff --git a/src/src/nxmhandler_linux.cpp b/src/src/nxmhandler_linux.cpp index a93826d..1203585 100644 --- a/src/src/nxmhandler_linux.cpp +++ b/src/src/nxmhandler_linux.cpp @@ -174,8 +174,6 @@ QString NxmHandlerLinux::socketPath() void NxmHandlerLinux::registerHandler() const { - const bool flatpak = QFile::exists(QStringLiteral("/.flatpak-info")); - const QString home = QDir::homePath(); if (home.isEmpty()) { log::error("cannot register nxm handler: home path is empty"); @@ -190,37 +188,30 @@ void NxmHandlerLinux::registerHandler() const return; } - QString execLine; - - if (flatpak) { - // In Flatpak the desktop file is invoked on the HOST, so use flatpak run - execLine = "flatpak run com.fluorine.manager nxm-handle %u"; - } else { - // Non-Flatpak: create a wrapper script and point the desktop file at it - const QString localBin = ensureDir(home + "/.local/bin"); - if (localBin.isEmpty()) { - log::error("cannot register nxm handler: failed to create ~/.local/bin"); - return; - } + // Create a wrapper script and point the desktop file at it + const QString localBin = ensureDir(home + "/.local/bin"); + if (localBin.isEmpty()) { + log::error("cannot register nxm handler: failed to create ~/.local/bin"); + return; + } - const QString wrapperPath = localBin + "/mo2-nxm-handler"; - const QString executable = QCoreApplication::applicationFilePath(); - const QString wrapper = - QString("#!/bin/sh\nexec \"%1\" nxm-handle \"$@\"\n").arg(executable); + const QString wrapperPath = localBin + "/mo2-nxm-handler"; + const QString executable = QCoreApplication::applicationFilePath(); + const QString wrapper = + QString("#!/bin/sh\nexec \"%1\" nxm-handle \"$@\"\n").arg(executable); - if (!writeTextFile(wrapperPath, wrapper)) { - log::error("failed to write nxm wrapper script '{}'", wrapperPath); - return; - } + if (!writeTextFile(wrapperPath, wrapper)) { + log::error("failed to write nxm wrapper script '{}'", wrapperPath); + return; + } - QFile::setPermissions(wrapperPath, - QFileDevice::ReadOwner | QFileDevice::WriteOwner | - QFileDevice::ExeOwner | QFileDevice::ReadGroup | - QFileDevice::ExeGroup | QFileDevice::ReadOther | - QFileDevice::ExeOther); + QFile::setPermissions(wrapperPath, + QFileDevice::ReadOwner | QFileDevice::WriteOwner | + QFileDevice::ExeOwner | QFileDevice::ReadGroup | + QFileDevice::ExeGroup | QFileDevice::ReadOther | + QFileDevice::ExeOther); - execLine = "mo2-nxm-handler nxm-handle %u"; - } + const QString execLine = "mo2-nxm-handler nxm-handle %u"; const QString desktopPath = appsDir + "/mo2-nxm-handler.desktop"; const QString desktop = QString("[Desktop Entry]\n" @@ -240,21 +231,10 @@ void NxmHandlerLinux::registerHandler() const updateMimeAppsList(appsDir + "/mimeapps.list", "x-scheme-handler/nxm", "mo2-nxm-handler.desktop"); - if (flatpak) { - // update-desktop-database doesn't exist inside the sandbox; run it on the host - QProcess p; - p.start(QStringLiteral("flatpak-spawn"), - {QStringLiteral("--host"), QStringLiteral("update-desktop-database"), - appsDir}); - if (!p.waitForFinished(5000) || p.exitCode() != 0) { - log::warn("update-desktop-database on host exited with code {}", p.exitCode()); - } - } else { - const auto result = - QProcess::execute("update-desktop-database", QStringList() << appsDir); - if (result != 0) { - log::warn("update-desktop-database exited with code {}", result); - } + const auto result = + QProcess::execute("update-desktop-database", QStringList() << appsDir); + if (result != 0) { + log::warn("update-desktop-database exited with code {}", result); } } diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index e1bdda1..630f23b 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -2193,22 +2193,23 @@ bool OrganizerCore::beforeRun( WinePrefix prefix(prefixPathStr); if (prefix.isValid()) { const QString dataDirName = resolveWineDataDirName(managedGame()); - const QString appDataLocal = prefix.appdataLocal(); - const QString pluginsTargetDir = - QDir(appDataLocal).filePath(dataDirName); - const QString documentsDir = - resolvePrefixGameDocumentsDir(prefix, dataDirName); + const QString appDataLocal = prefix.appdataLocal(); + const QString pluginsTargetDir = + QDir(appDataLocal).filePath(dataDirName); + const QString documentsDir = + resolvePrefixGameDocumentsDir(prefix, dataDirName); log::info("Wine prefix paths: AppData/Local plugins dir='{}', " "Documents/My Games INI dir='{}'", pluginsTargetDir, documentsDir); - const auto localSavesFeature = gameFeatures().gameFeature(); - const QString absoluteSaveDir = - resolveAbsoluteSaveDir(prefix, managedGame(), - localSavesFeature.get(), m_CurrentProfile); - - // Read plugin lines from profile's plugins.txt - QFile pluginsFile(m_CurrentProfile->getPluginsFileName()); - if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + const auto localSavesFeature = gameFeatures().gameFeature(); + const QString absoluteSaveDir = + resolveAbsoluteSaveDir(prefix, managedGame(), + localSavesFeature.get(), m_CurrentProfile); + log::info("Wine prefix save target: '{}'", absoluteSaveDir); + + // Read plugin lines from profile's plugins.txt + QFile pluginsFile(m_CurrentProfile->getPluginsFileName()); + if (pluginsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QStringList plugins; QTextStream stream(&pluginsFile); while (!stream.atEnd()) { @@ -2218,12 +2219,17 @@ bool OrganizerCore::beforeRun( } } pluginsFile.close(); - - if (!plugins.isEmpty()) { - if (prefix.deployPlugins(plugins, dataDirName)) { - log::debug("Deployed {} plugins to prefix '{}' (dataDirName='{}')", - plugins.size(), prefixPathStr, dataDirName); - } else { + + if (!plugins.isEmpty()) { + log::info( + "Deploying plugin list to '{}' and '{}' (load order to '{}')", + QDir(pluginsTargetDir).filePath("Plugins.txt"), + QDir(pluginsTargetDir).filePath("plugins.txt"), + QDir(pluginsTargetDir).filePath("loadorder.txt")); + if (prefix.deployPlugins(plugins, dataDirName)) { + log::debug("Deployed {} plugins to prefix '{}' (dataDirName='{}')", + plugins.size(), prefixPathStr, dataDirName); + } else { log::error("Failed to deploy {} plugins to prefix '{}' " "(dataDirName='{}')", plugins.size(), prefixPathStr, dataDirName); @@ -2238,18 +2244,18 @@ bool OrganizerCore::beforeRun( const QString targetIniBase = resolvePrefixGameDocumentsDir(prefix, dataDirName); int deployedIniCount = 0; - for (const QString& iniFile : managedGame()->iniFiles()) { - const QString sourceIni = - m_CurrentProfile->absoluteIniFilePath(iniFile); - const QString targetIni = - QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName()); - log::debug("INI deploy check: source='{}' exists={}, target='{}'", - sourceIni, QFileInfo::exists(sourceIni), targetIni); - if (QFileInfo::exists(sourceIni) && - prefix.deployProfileIni(sourceIni, targetIni)) { - ++deployedIniCount; - log::debug("Deployed profile INI '{}' -> '{}'", sourceIni, targetIni); - } + for (const QString& iniFile : managedGame()->iniFiles()) { + const QString sourceIni = + m_CurrentProfile->absoluteIniFilePath(iniFile); + const QString targetIni = + QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName()); + log::info("INI deploy target: '{}' -> '{}' (exists={})", sourceIni, + targetIni, QFileInfo::exists(sourceIni)); + if (QFileInfo::exists(sourceIni) && + prefix.deployProfileIni(sourceIni, targetIni)) { + ++deployedIniCount; + log::debug("Deployed profile INI '{}' -> '{}'", sourceIni, targetIni); + } } if (deployedIniCount > 0) { log::debug("Deployed {} profile INI files to prefix '{}'", deployedIniCount, @@ -2261,15 +2267,15 @@ bool OrganizerCore::beforeRun( managedGame()->documentsDirectory().absolutePath()); } - if (m_CurrentProfile->localSavesEnabled()) { - const QString profileSavesDir = - QDir(m_CurrentProfile->absolutePath()).filePath("saves"); - log::debug("Resolved local save mapping: profile='{}', target='{}'", - profileSavesDir, absoluteSaveDir); - if (!prefix.deployProfileSaves(profileSavesDir, absoluteSaveDir, true)) { - log::warn("Failed to deploy profile saves from '{}' to prefix '{}'", - profileSavesDir, prefixPathStr); - } else { + if (m_CurrentProfile->localSavesEnabled()) { + const QString profileSavesDir = + QDir(m_CurrentProfile->absolutePath()).filePath("saves"); + log::info("Save deploy target: '{}' -> '{}'", profileSavesDir, + absoluteSaveDir); + if (!prefix.deployProfileSaves(profileSavesDir, absoluteSaveDir, true)) { + log::warn("Failed to deploy profile saves from '{}' to prefix '{}'", + profileSavesDir, prefixPathStr); + } else { log::debug("Deployed profile saves '{}' -> '{}' in prefix '{}'", profileSavesDir, absoluteSaveDir, prefixPathStr); } @@ -2315,29 +2321,29 @@ void OrganizerCore::afterRun(const QFileInfo& binary, DWORD exitCode) resolveAbsoluteSaveDir(prefix, managedGame(), localSavesFeature.get(), m_CurrentProfile); - if (m_CurrentProfile->localSavesEnabled()) { - const QString profileSavesDir = - QDir(m_CurrentProfile->absolutePath()).filePath("saves"); - log::debug("Syncing local save mapping: profile='{}', target='{}'", - profileSavesDir, absoluteSaveDir); - if (!prefix.syncSavesBack(profileSavesDir, absoluteSaveDir)) { - log::warn("Failed to sync saves back from prefix '{}' to '{}'", - prefixPathStr, profileSavesDir); - } - } + if (m_CurrentProfile->localSavesEnabled()) { + const QString profileSavesDir = + QDir(m_CurrentProfile->absolutePath()).filePath("saves"); + log::info("Save sync target: '{}' <- '{}'", profileSavesDir, + absoluteSaveDir); + if (!prefix.syncSavesBack(profileSavesDir, absoluteSaveDir)) { + log::warn("Failed to sync saves back from prefix '{}' to '{}'", + prefixPathStr, profileSavesDir); + } + } if (m_CurrentProfile->localSettingsEnabled()) { const QString targetIniBase = resolvePrefixGameDocumentsDir(prefix, dataDirName); QList> iniMappings; for (const QString& iniFile : managedGame()->iniFiles()) { - const QString profileIni = - m_CurrentProfile->absoluteIniFilePath(iniFile); - const QString targetIni = - QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName()); - iniMappings.append({profileIni, targetIni}); - log::debug("Sync profile INI '{}' <- '{}'", profileIni, targetIni); - } + const QString profileIni = + m_CurrentProfile->absoluteIniFilePath(iniFile); + const QString targetIni = + QDir(targetIniBase).filePath(QFileInfo(iniFile).fileName()); + iniMappings.append({profileIni, targetIni}); + log::info("INI sync target: '{}' <- '{}'", profileIni, targetIni); + } if (!iniMappings.isEmpty() && !prefix.syncProfileInisBack(iniMappings)) { diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp index 98665fb..dad0117 100644 --- a/src/src/plugincontainer.cpp +++ b/src/src/plugincontainer.cpp @@ -4,11 +4,12 @@ #include "organizerproxy.h" #include "report.h" #include "shared/appconfig.h" -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -21,13 +22,13 @@ using namespace MOShared; namespace bf = boost::fusion; -#ifndef _WIN32 +#ifndef _WIN32 // Ensure the instance plugin directory contains symlinks to all bundled plugins. // Real user files (not matching any bundled plugin name) are left untouched. // Stale copies of bundled plugins are replaced with symlinks so that RPATH // resolution works correctly (critical for Flatpak where libs live in /app/). -static void ensureBundledPluginsLinked(const QString& bundledDir, - const QString& instanceDir) +static void ensureBundledPluginsLinked(const QString& bundledDir, + const QString& instanceDir) { QDir().mkpath(instanceDir); QDirIterator it(bundledDir, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); @@ -55,8 +56,14 @@ static void ensureBundledPluginsLinked(const QString& bundledDir, QFile::link(it.filePath(), target); } -} -#endif +} +#endif + +static void printPluginDiagToStderr(const QString& message) +{ + std::fprintf(stderr, "[plugin-diag] %s\n", qUtf8Printable(message)); + std::fflush(stderr); +} // Welcome to the wonderful world of MO2 plugin management! // @@ -636,22 +643,33 @@ IPlugin* PluginContainer::registerPlugin(QObject* plugin, const QString& filepat return preview; } } - { // proxy plugins - IPluginProxy* proxy = qobject_cast(plugin); - if (initPlugin(proxy, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(proxy); - emit pluginRegistered(proxy); - - QStringList filepaths = proxy->pluginList( - m_PluginPath.isEmpty() - ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath())) - : m_PluginPath); - for (const QString& filepath : filepaths) { - loadProxied(filepath, proxy); - } - return proxy; - } - } + { // proxy plugins + IPluginProxy* proxy = qobject_cast(plugin); + if (initPlugin(proxy, pluginProxy, skipInit)) { + bf::at_key(m_Plugins).push_back(proxy); + emit pluginRegistered(proxy); + + const QString pluginRoot = + m_PluginPath.isEmpty() + ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath())) + : m_PluginPath; + QStringList filepaths = proxy->pluginList(pluginRoot); + log::warn("proxy '{}' discovered {} proxied plugin candidate(s) in '{}'", + proxy->name(), filepaths.size(), + QDir::toNativeSeparators(pluginRoot)); + printPluginDiagToStderr( + QString("proxy '%1' discovered %2 proxied plugin candidate(s) in '%3'") + .arg(proxy->name()) + .arg(filepaths.size()) + .arg(QDir::toNativeSeparators(pluginRoot))); + for (const QString& filepath : filepaths) { + log::debug("proxy '{}' candidate: '{}'", proxy->name(), + QDir::toNativeSeparators(filepath)); + loadProxied(filepath, proxy); + } + return proxy; + } + } { // dummy plugins // only initialize these, no processing otherwise @@ -835,44 +853,70 @@ void PluginContainer::startPluginsImpl(const std::vector& plugins) con } } -std::vector PluginContainer::loadProxied(const QString& filepath, - IPluginProxy* proxy) -{ - std::vector proxiedPlugins; - - try { - // We get a list of matching plugins as proxies can return multiple plugins - // per file and do not have a good way of supporting multiple inheritance. - QList matchingPlugins = proxy->load(filepath); - - // We are going to group plugin by names and "fix" them later: - std::map> proxiedByNames; - - for (QObject* proxiedPlugin : matchingPlugins) { - if (proxiedPlugin != nullptr) { - - if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); - proxied) { - log::debug("loaded plugin '{}@{}' from '{}' - [{}]", proxied->name(), - proxied->version().canonicalString(), - QFileInfo(filepath).fileName(), - implementedInterfaces(proxied).join(", ")); - - // Store the plugin for later: - proxiedPlugins.push_back(proxiedPlugin); - proxiedByNames[proxied->name()].push_back(proxied); - } else { - log::warn("plugin \"{}\" failed to load. If this plugin is for an older " - "version of MO " - "you have to update it or delete it if no update exists.", - filepath); - } - } - } +std::vector PluginContainer::loadProxied(const QString& filepath, + IPluginProxy* proxy) +{ + std::vector proxiedPlugins; + + try { + log::warn("loading proxied plugin candidate '{}' via proxy '{}'", + QDir::toNativeSeparators(filepath), + (proxy ? proxy->name() : QStringLiteral(""))); + printPluginDiagToStderr( + QString("loading proxied plugin candidate '%1' via proxy '%2'") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral(""))); + + // We get a list of matching plugins as proxies can return multiple plugins + // per file and do not have a good way of supporting multiple inheritance. + QList matchingPlugins = proxy->load(filepath); + if (matchingPlugins.isEmpty()) { + log::warn("no plugins were returned for proxied candidate '{}' via proxy '{}'", + QDir::toNativeSeparators(filepath), proxy->name()); + printPluginDiagToStderr( + QString("no plugins were returned for proxied candidate '%1' via proxy '%2'") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy->name())); + } + + // We are going to group plugin by names and "fix" them later: + std::map> proxiedByNames; + + for (QObject* proxiedPlugin : matchingPlugins) { + if (proxiedPlugin == nullptr) { + log::warn("proxy '{}' returned a null QObject for '{}'", proxy->name(), + QDir::toNativeSeparators(filepath)); + printPluginDiagToStderr( + QString("proxy '%1' returned a null QObject for '%2'") + .arg(proxy->name()) + .arg(QDir::toNativeSeparators(filepath))); + continue; + } + + if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); proxied) { + log::debug("loaded plugin '{}@{}' from '{}' - [{}]", proxied->name(), + proxied->version().canonicalString(), + QFileInfo(filepath).fileName(), + implementedInterfaces(proxied).join(", ")); + + // Store the plugin for later: + proxiedPlugins.push_back(proxiedPlugin); + proxiedByNames[proxied->name()].push_back(proxied); + } else { + log::warn( + "proxied candidate '{}' from proxy '{}' failed to register as an MO2 plugin", + QDir::toNativeSeparators(filepath), proxy->name()); + printPluginDiagToStderr( + QString("proxied candidate '%1' from proxy '%2' failed to register as an " + "MO2 plugin") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy->name())); + } + } // Fake masters: - for (auto& [name, proxiedPlugins] : proxiedByNames) { - if (proxiedPlugins.size() > 1) { + for (auto& [name, proxiedPlugins] : proxiedByNames) { + if (proxiedPlugins.size() > 1) { auto it = std::min_element(std::begin(proxiedPlugins), std::end(proxiedPlugins), [&](auto const& lhs, auto const& rhs) { return isBetterInterface(as_qobject(lhs), @@ -884,15 +928,43 @@ std::vector PluginContainer::loadProxied(const QString& filepath, m_Requirements.at(proxiedPlugin).setMaster(*it); } } - } - } - } catch (const std::exception& e) { - reportError( - QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what())); - } - - return proxiedPlugins; -} + } + } + log::warn("finished proxied candidate '{}' via proxy '{}': {} plugin(s) loaded", + QDir::toNativeSeparators(filepath), proxy->name(), + proxiedPlugins.size()); + printPluginDiagToStderr( + QString("finished proxied candidate '%1' via proxy '%2': %3 plugin(s) loaded") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy->name()) + .arg(proxiedPlugins.size())); + } catch (const std::exception& e) { + log::error("failed to initialize proxied candidate '{}' via proxy '{}': {}", + QDir::toNativeSeparators(filepath), + (proxy ? proxy->name() : QStringLiteral("")), e.what()); + printPluginDiagToStderr( + QString("failed to initialize proxied candidate '%1' via proxy '%2': %3") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral("")) + .arg(e.what())); + reportError( + QObject::tr("failed to initialize plugin %1: %2").arg(filepath).arg(e.what())); + } catch (...) { + log::error("failed to initialize proxied candidate '{}' via proxy '{}': " + "unknown exception", + QDir::toNativeSeparators(filepath), + (proxy ? proxy->name() : QStringLiteral(""))); + printPluginDiagToStderr( + QString("failed to initialize proxied candidate '%1' via proxy '%2': unknown " + "exception") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral(""))); + reportError(QObject::tr("failed to initialize plugin %1: unknown exception") + .arg(filepath)); + } + + return proxiedPlugins; +} QObject* PluginContainer::loadQtPlugin(const QString& filepath) { diff --git a/src/src/process_helper_main.cpp b/src/src/process_helper_main.cpp deleted file mode 100644 index 185111c..0000000 --- a/src/src/process_helper_main.cpp +++ /dev/null @@ -1,359 +0,0 @@ -// Standalone process helper for Flatpak game launching. -// Runs on the host via flatpak-spawn --host, keeping the flatpak-spawn -// proxy alive for MO2's PID polling while the game process tree is running. -// -// Protocol (stdin/stdout, line-oriented): -// Config phase: MO2 writes key=value lines terminated by a blank line -// program=, arg= (repeatable), env=KEY=VALUE (repeatable), -// workdir= -// Helper responds: "started " or "error " -// Runtime commands (MO2→helper): "kill" (SIGTERM child tree), "quit" -// Helper reports: "exited " when game process tree exits - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -// ── Helpers ── - -static void writeResponse(const std::string& msg) -{ - std::string line = msg + "\n"; - ::write(STDOUT_FILENO, line.data(), line.size()); -} - -static bool readLine(std::string& out, int timeoutMs) -{ - out.clear(); - - struct pollfd pfd{}; - pfd.fd = STDIN_FILENO; - pfd.events = POLLIN; - - while (true) { - int ret = ::poll(&pfd, 1, timeoutMs); - if (ret < 0) { - if (errno == EINTR) - continue; - return false; - } - if (ret == 0) { - // timeout - return false; - } - - // Try reading if data is available, even if HUP is also set - // (pipe can have buffered data when the writer closes). - if (!(pfd.revents & POLLIN)) { - // No data available — must be HUP or ERR only - return false; - } - - char ch = 0; - ssize_t n = ::read(STDIN_FILENO, &ch, 1); - if (n <= 0) { - return false; - } - if (ch == '\n') { - return true; - } - out.push_back(ch); - } -} - -// Collect all descendant PIDs of a given root by scanning /proc. -static std::unordered_set collectDescendants(pid_t root) -{ - std::unordered_set descendants; - - // Build parent→children map - struct ProcEntry { - pid_t pid; - pid_t ppid; - }; - std::vector entries; - - DIR* proc = opendir("/proc"); - if (!proc) { - return descendants; - } - - struct dirent* entry = nullptr; - while ((entry = readdir(proc)) != nullptr) { - if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) { - continue; - } - - char* end = nullptr; - long pidLong = strtol(entry->d_name, &end, 10); - if (end == entry->d_name || *end != '\0' || pidLong <= 0) { - continue; - } - - pid_t pid = static_cast(pidLong); - - char statusPath[64]; - snprintf(statusPath, sizeof(statusPath), "/proc/%ld/status", pidLong); - - std::ifstream status(statusPath); - 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(strtol(line.c_str() + 5, nullptr, 10)); - break; - } - } - - if (ppid > 0) { - entries.push_back({pid, ppid}); - } - } - closedir(proc); - - // BFS from root - std::vector queue; - queue.push_back(root); - - while (!queue.empty()) { - pid_t cur = queue.back(); - queue.pop_back(); - - for (const auto& e : entries) { - if (e.ppid == cur && descendants.find(e.pid) == descendants.end()) { - descendants.insert(e.pid); - queue.push_back(e.pid); - } - } - } - - return descendants; -} - -// Check if any process in the given set is still alive. -static bool anyAlive(const std::unordered_set& pids) -{ - for (pid_t p : pids) { - if (::kill(p, 0) == 0 || errno == EPERM) { - return true; - } - } - return false; -} - -// ── Main ── - -int main() -{ - // Make stdout line-buffered for reliable protocol messages. - setvbuf(stdout, nullptr, _IOLBF, 0); - - // ── Config phase: read key=value lines until blank line ── - std::string program; - std::vector args; - std::vector envVars; // "KEY=VALUE" - std::string workdir; - - while (true) { - std::string line; - if (!readLine(line, 30000)) { - writeResponse("error stdin closed or timeout during config"); - return 1; - } - - if (line.empty()) { - break; // blank line = end of config - } - - auto eq = line.find('='); - if (eq == std::string::npos) { - continue; - } - - std::string key = line.substr(0, eq); - std::string val = line.substr(eq + 1); - - if (key == "program") { - program = val; - } else if (key == "arg") { - args.push_back(val); - } else if (key == "env") { - envVars.push_back(val); - } else if (key == "workdir") { - workdir = val; - } - } - - if (program.empty()) { - writeResponse("error no program specified"); - return 1; - } - - // ── Pipe for exec error reporting ── - // Child writes errno to this pipe if execvp fails; parent reads it. - int errPipe[2]; - if (::pipe2(errPipe, O_CLOEXEC) != 0) { - writeResponse("error pipe2 failed: " + std::string(strerror(errno))); - return 1; - } - - // ── Fork ── - pid_t child = ::fork(); - if (child < 0) { - writeResponse("error fork failed: " + std::string(strerror(errno))); - return 1; - } - - if (child == 0) { - // ── Child ── - ::close(errPipe[0]); // close read end - - // New session so we can kill the whole process group later. - ::setsid(); - - // Set environment variables. - for (const auto& ev : envVars) { - ::putenv(const_cast(ev.c_str())); - } - - // Change working directory. - if (!workdir.empty()) { - if (::chdir(workdir.c_str()) != 0) { - int err = errno; - (void)::write(errPipe[1], &err, sizeof(err)); - ::_exit(127); - } - } - - // Build argv for execvp. - std::vector argv; - argv.push_back(program.c_str()); - for (const auto& a : args) { - argv.push_back(a.c_str()); - } - argv.push_back(nullptr); - - ::execvp(program.c_str(), const_cast(argv.data())); - - // If we get here, exec failed. - int err = errno; - (void)::write(errPipe[1], &err, sizeof(err)); - ::_exit(127); - } - - // ── Parent ── - ::close(errPipe[1]); // close write end - - // Check if exec succeeded (pipe closes on successful exec due to O_CLOEXEC). - int execErr = 0; - ssize_t n = ::read(errPipe[0], &execErr, sizeof(execErr)); - ::close(errPipe[0]); - - if (n > 0) { - // exec failed in child - ::waitpid(child, nullptr, 0); - writeResponse("error exec failed: " + std::string(strerror(execErr))); - return 1; - } - - // Success - report PID. - writeResponse("started " + std::to_string(child)); - - // ── Monitor loop ── - // Wait for the direct child and then monitor descendants (handles Proton - // chain: proton → wine → game.exe). - bool childExited = false; - int childStatus = 0; - bool quit = false; - - while (!quit) { - // Check for commands on stdin. - std::string cmd; - // Non-blocking: if readLine returns false due to timeout, that's fine. - // If it returns false due to pipe close, MO2 crashed — kill child group. - struct pollfd pfd{}; - pfd.fd = STDIN_FILENO; - pfd.events = POLLIN; - - int pollRet = ::poll(&pfd, 1, 200); - - if (pollRet > 0) { - if (pfd.revents & (POLLHUP | POLLERR)) { - // MO2 crashed or closed pipe — kill child group and exit. - ::kill(-child, SIGTERM); - break; - } - - if (pfd.revents & POLLIN) { - if (readLine(cmd, 0)) { - if (cmd == "kill") { - ::kill(-child, SIGTERM); - } else if (cmd == "quit") { - quit = true; - break; - } - } else { - // read failed = pipe closed - ::kill(-child, SIGTERM); - break; - } - } - } - - // Check child status. - if (!childExited) { - int status = 0; - pid_t ret = ::waitpid(child, &status, WNOHANG); - if (ret == child) { - childExited = true; - childStatus = status; - } else if (ret < 0 && errno != EINTR) { - // Child somehow lost - childExited = true; - childStatus = 0; - } - } - - if (childExited) { - // Check for surviving descendants (e.g., game.exe still running - // after the proton wrapper exits). - auto desc = collectDescendants(child); - if (desc.empty() || !anyAlive(desc)) { - // All done. - int exitCode = 0; - if (WIFEXITED(childStatus)) { - exitCode = WEXITSTATUS(childStatus); - } else if (WIFSIGNALED(childStatus)) { - exitCode = 128 + WTERMSIG(childStatus); - } - writeResponse("exited " + std::to_string(exitCode)); - return 0; - } - // Descendants still alive, keep monitoring. - } - } - - // If we broke out of the loop, reap child if needed. - if (!childExited) { - ::waitpid(child, nullptr, 0); - } - - writeResponse("exited 0"); - return 0; -} diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index ac3c9eb..e7354f3 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -1175,7 +1175,6 @@ std::optional ProcessRunner::runBinary() #ifdef _WIN32 m_handle.reset(startBinary(parent, m_sp)); #else - m_sp.helperProcessOut = &m_processHelper; m_handle.reset(reinterpret_cast(static_cast(startBinary(parent, m_sp)))); #endif if (m_handle.get() == INVALID_HANDLE_VALUE) { @@ -1265,12 +1264,9 @@ ProcessRunner::Results ProcessRunner::postRun() const QFileInfo binary = m_sp.binary; QPointer core = &m_core; - QProcess* helper = m_processHelper; - m_processHelper = nullptr; - - std::thread([core, binary, pid, helper]() { - // For detached processes (including flatpak-spawn helper), - // waitpid will fail with ECHILD. Use kill(0) polling instead. + 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 { @@ -1294,16 +1290,6 @@ ProcessRunner::Results ProcessRunner::postRun() errno); } - // Clean up helper QProcess on the main thread. - if (helper) { - QMetaObject::invokeMethod( - QCoreApplication::instance(), [helper]() { - helper->waitForFinished(1000); - delete helper; - }, - Qt::QueuedConnection); - } - if (!core) { return; } @@ -1352,14 +1338,6 @@ ProcessRunner::Results ProcessRunner::postRun() }); } -#ifndef _WIN32 - // Clean up the process helper (keeps flatpak-spawn alive during game). - if (m_processHelper) { - m_processHelper->waitForFinished(1000); - delete m_processHelper; - m_processHelper = nullptr; - } -#endif if (shouldRefresh(r)) { QEventLoop loop; diff --git a/src/src/processrunner.h b/src/src/processrunner.h index e825e98..2736c86 100644 --- a/src/src/processrunner.h +++ b/src/src/processrunner.h @@ -185,9 +185,6 @@ private: QFileInfo m_shellOpen; env::HandlePtr m_handle; DWORD m_exitCode; -#ifndef _WIN32 - QProcess* m_processHelper = nullptr; -#endif bool shouldRunShell() const; bool shouldRefresh(Results r) const; diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index a2aa042..f2bd7a0 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -1,5 +1,4 @@ #include "protonlauncher.h" -#include "fluorinepaths.h" #include #include @@ -102,43 +101,6 @@ void maybeWrapWithSteamRun(bool useSteamRun, QString& program, QStringList& argu arguments = wrappedArgs; } -bool isFlatpak() -{ - return QFileInfo::exists(QStringLiteral("/.flatpak-info")); -} - -// In Flatpak, Wine/Proton binaries can't execute inside the sandbox (they need -// the Steam Runtime's linker and 32-bit libs). Wrap them with flatpak-spawn -// --host so they run on the host system instead. -// -// flatpak-spawn --host runs via the Flatpak portal D-Bus interface, which does -// NOT reliably forward the caller's process environment. We must pass any -// custom env vars explicitly with --env= flags. -void maybeWrapForFlatpak(QString& program, QStringList& arguments, - const QProcessEnvironment& env) -{ - if (!isFlatpak()) { - return; - } - - QStringList wrappedArgs; - wrappedArgs.append(QStringLiteral("--host")); - - // Pass every env var that differs from the inherited system environment. - const QProcessEnvironment sysEnv = QProcessEnvironment::systemEnvironment(); - for (const QString& key : env.keys()) { - const QString val = env.value(key); - if (val != sysEnv.value(key)) { - wrappedArgs.append(QStringLiteral("--env=%1=%2").arg(key, val)); - } - } - - wrappedArgs.append(program); - wrappedArgs.append(arguments); - program = QStringLiteral("flatpak-spawn"); - arguments = wrappedArgs; -} - bool isValidEnvKey(const QString& key) { if (key.isEmpty()) { @@ -179,7 +141,7 @@ bool parseEnvAssignment(const QString& token, QString& keyOut, QString& valueOut ProtonLauncher::ProtonLauncher() : m_steamAppId(0), m_useUmu(false), m_preferSystemUmu(false), - m_useSteamRun(false) + m_useSteamRun(false), m_useSteamDrm(true) {} ProtonLauncher& ProtonLauncher::setBinary(const QString& path) @@ -258,6 +220,12 @@ ProtonLauncher& ProtonLauncher::setUseSteamRun(bool useSteamRun) return *this; } +ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm) +{ + m_useSteamDrm = useSteamDrm; + return *this; +} + ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& value) { if (!key.isEmpty()) { @@ -267,12 +235,6 @@ ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& val return *this; } -ProtonLauncher& ProtonLauncher::setHelperProcessOut(QProcess** out) -{ - m_helperProcessOut = out; - return *this; -} - std::pair ProtonLauncher::launch() const { qint64 pid = -1; @@ -297,7 +259,9 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const return false; } - ensureSteamRunning(); + if (m_useSteamDrm) { + ensureSteamRunning(); + } QString protonScript = m_protonPath; if (QFileInfo(protonScript).isDir()) { @@ -311,7 +275,6 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const wrapProgram(m_wrapperCommands, protonScript, protonArgs, program, arguments); maybeWrapWithSteamRun(m_useSteamRun, program, arguments); - // Build environment BEFORE flatpak wrapping (flatpak-spawn needs --env= flags). QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); @@ -357,11 +320,6 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const MOBase::log::info("Proton launch: '{}' run '{}'", protonScript, m_binary); - if (isFlatpak()) { - return launchViaProcessHelper(program, arguments, env, m_workingDir, pid); - } - - maybeWrapForFlatpak(program, arguments, env); return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); } @@ -373,61 +331,51 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const // Steam must be running for games with Steamworks DRM (Application Load // Error 5:0000065434 occurs otherwise). - ensureSteamRunning(); + if (m_useSteamDrm) { + ensureSteamRunning(); + } // Resolve umu-run according to user preference (bundled vs system). - // In Flatpak, umu-run must run on the host (it needs Steam Runtime). - // Use the full path to our copied umu-run since the host PATH won't include it. + const QString appDir = QCoreApplication::applicationDirPath(); + const QString bundled = appDir + QStringLiteral("/umu-run"); + + // Search PATH for a system umu-run, excluding our own app directory + // (the launcher prepends it to PATH, so findExecutable would find the + // bundled copy otherwise). + QString system; + const QStringList pathDirs = + QString::fromLocal8Bit(qgetenv("PATH")).split(QLatin1Char(':'), Qt::SkipEmptyParts); + for (const QString& dir : pathDirs) { + if (QDir(dir) == QDir(appDir)) + continue; + const QString candidate = QStandardPaths::findExecutable( + QStringLiteral("umu-run"), {dir}); + if (!candidate.isEmpty()) { + system = candidate; + break; + } + } + QString umuRun; - if (isFlatpak()) { - const QString flatpakUmu = fluorineDataDir() + QStringLiteral("/umu-run"); - if (QFileInfo::exists(flatpakUmu)) { - umuRun = flatpakUmu; - } else { - // Fall back to bare name (requires host to have umu-run in PATH) - umuRun = QStringLiteral("umu-run"); + if (m_preferSystemUmu) { + if (!system.isEmpty()) { + umuRun = system; + } else if (QFileInfo::exists(bundled)) { + umuRun = bundled; + MOBase::log::warn( + "System umu-run preferred but not found in PATH, falling back to bundled"); } } else { - const QString appDir = QCoreApplication::applicationDirPath(); - const QString bundled = appDir + QStringLiteral("/umu-run"); - - // Search PATH for a system umu-run, excluding our own app directory - // (the launcher prepends it to PATH, so findExecutable would find the - // bundled copy otherwise). - QString system; - const QStringList pathDirs = - QString::fromLocal8Bit(qgetenv("PATH")).split(QLatin1Char(':'), Qt::SkipEmptyParts); - for (const QString& dir : pathDirs) { - if (QDir(dir) == QDir(appDir)) - continue; - const QString candidate = QStandardPaths::findExecutable( - QStringLiteral("umu-run"), {dir}); - if (!candidate.isEmpty()) { - system = candidate; - break; - } + if (QFileInfo::exists(bundled)) { + umuRun = bundled; + } else if (!system.isEmpty()) { + umuRun = system; } - - if (m_preferSystemUmu) { - if (!system.isEmpty()) { - umuRun = system; - } else if (QFileInfo::exists(bundled)) { - umuRun = bundled; - MOBase::log::warn( - "System umu-run preferred but not found in PATH, falling back to bundled"); - } - } else { - if (QFileInfo::exists(bundled)) { - umuRun = bundled; - } else if (!system.isEmpty()) { - umuRun = system; - } - } - - MOBase::log::info("umu-run: preferSystem={}, bundled='{}' (exists={}), system='{}', selected='{}'", - m_preferSystemUmu, bundled, QFileInfo::exists(bundled), system, umuRun); } + MOBase::log::info("umu-run: preferSystem={}, bundled='{}' (exists={}), system='{}', selected='{}'", + m_preferSystemUmu, bundled, QFileInfo::exists(bundled), system, umuRun); + if (umuRun.isEmpty()) { MOBase::log::warn("umu-run not found (bundled or in PATH)"); return false; @@ -440,7 +388,6 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const wrapProgram(m_wrapperCommands, umuRun, umuArgs, program, arguments); maybeWrapWithSteamRun(m_useSteamRun, program, arguments); - // Build environment BEFORE flatpak wrapping (flatpak-spawn needs --env= flags). QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); @@ -471,11 +418,15 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } } - if (effectiveSteamAppId != 0) { + if (m_useSteamDrm && effectiveSteamAppId != 0) { // umu-run expects GAMEID in "umu-" format to extract SteamAppId. env.insert("GAMEID", QStringLiteral("umu-") + QString::number(effectiveSteamAppId)); env.insert("SteamAppId", QString::number(effectiveSteamAppId)); env.insert("SteamGameId", QString::number(effectiveSteamAppId)); + env.insert("STORE", QStringLiteral("steam")); + } else { + // No Steam DRM — use a generic game ID so umu-run doesn't try Steam integration. + env.insert("GAMEID", QStringLiteral("umu-0")); } for (auto it = m_wrapperEnvVars.cbegin(); it != m_wrapperEnvVars.cend(); ++it) { @@ -503,11 +454,6 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const QString::number(effectiveSteamAppId)), steamPath); - if (isFlatpak()) { - return launchViaProcessHelper(program, arguments, env, m_workingDir, pid); - } - - maybeWrapForFlatpak(program, arguments, env); return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); } @@ -531,130 +477,21 @@ bool ProtonLauncher::launchDirect(qint64& pid) const env.insert(it.key(), it.value()); } - if (isFlatpak()) { - return launchViaProcessHelper(program, arguments, env, m_workingDir, pid); - } - - maybeWrapForFlatpak(program, arguments, env); return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); } -bool ProtonLauncher::launchViaProcessHelper( - const QString& program, const QStringList& arguments, - const QProcessEnvironment& env, const QString& workingDir, - qint64& pid) const -{ - const QString helperBin = fluorineDataDir() + QStringLiteral("/bin/mo2-process-helper"); - if (!QFileInfo::exists(helperBin)) { - MOBase::log::warn("mo2-process-helper not found at '{}', falling back to " - "flatpak-spawn direct launch", helperBin); - // Fall back to old direct flatpak-spawn path. - QString prog = program; - QStringList args = arguments; - QProcessEnvironment envCopy = env; - maybeWrapForFlatpak(prog, args, envCopy); - return startDetachedWithEnv(prog, args, workingDir, envCopy, pid); - } - - auto* proc = new QProcess(); - proc->setProcessChannelMode(QProcess::SeparateChannels); - proc->start(QStringLiteral("flatpak-spawn"), - {QStringLiteral("--host"), helperBin}); - - if (!proc->waitForStarted(5000)) { - MOBase::log::error("Failed to start flatpak-spawn for process helper"); - delete proc; - return false; - } - - // Write config block to helper's stdin. - auto writeLine = [&](const QString& line) { - proc->write(line.toUtf8()); - proc->write("\n"); - }; - - writeLine(QStringLiteral("program=") + program); - for (const QString& arg : arguments) { - writeLine(QStringLiteral("arg=") + arg); - } - - // Send env vars that differ from system environment. - const QProcessEnvironment sysEnv = QProcessEnvironment::systemEnvironment(); - for (const QString& key : env.keys()) { - const QString val = env.value(key); - if (val != sysEnv.value(key)) { - writeLine(QStringLiteral("env=") + key + QStringLiteral("=") + val); - } - } - - if (!workingDir.isEmpty()) { - writeLine(QStringLiteral("workdir=") + workingDir); - } - - // Blank line terminates config. - proc->write("\n"); - proc->waitForBytesWritten(2000); - - // Read response: "started " or "error " - if (!proc->waitForReadyRead(10000)) { - MOBase::log::error("Process helper did not respond in time"); - proc->kill(); - proc->waitForFinished(2000); - delete proc; - return false; - } - - const QString response = QString::fromUtf8(proc->readLine()).trimmed(); - if (response.startsWith(QStringLiteral("started "))) { - MOBase::log::info("Process helper: {}", response); - } else { - MOBase::log::error("Process helper error: {}", response); - proc->kill(); - proc->waitForFinished(2000); - delete proc; - return false; - } - - // Use the flatpak-spawn PID for kill(pid,0) polling. - pid = proc->processId(); - - // Store the QProcess so it stays alive (keeping flatpak-spawn alive). - if (m_helperProcessOut) { - *m_helperProcessOut = proc; - } else { - // No owner provided — leak intentionally to keep the pipe alive. - // The process will clean up when the game exits. - MOBase::log::debug("No helper process owner set, helper will self-manage"); - } - - return true; -} - bool ProtonLauncher::ensureSteamRunning() { QProcess pgrep; - if (isFlatpak()) { - // In Flatpak, check for Steam on the host. - pgrep.start("flatpak-spawn", {"--host", "pgrep", "-x", "steam"}); - } else { - pgrep.start("pgrep", {"-x", "steam"}); - } + pgrep.start("pgrep", {"-x", "steam"}); if (pgrep.waitForFinished(2000) && pgrep.exitCode() == 0) { return true; } qint64 pid = -1; - if (isFlatpak()) { - if (QProcess::startDetached("flatpak-spawn", - {"--host", "steam", "-silent"}, QString(), &pid)) { - MOBase::log::warn("Steam was not running, started it on host in silent mode"); - return true; - } - } else { - if (QProcess::startDetached("steam", {"-silent"}, QString(), &pid)) { - MOBase::log::warn("Steam was not running, started it in silent mode"); - return true; - } + if (QProcess::startDetached("steam", {"-silent"}, QString(), &pid)) { + MOBase::log::warn("Steam was not running, started it in silent mode"); + return true; } return false; diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index d0213f6..5480c69 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -8,8 +8,6 @@ #include #include -class QProcess; - class ProtonLauncher { public: @@ -25,8 +23,8 @@ public: ProtonLauncher& setUmu(bool useUmu); ProtonLauncher& setPreferSystemUmu(bool preferSystemUmu); ProtonLauncher& setUseSteamRun(bool useSteamRun); + ProtonLauncher& setSteamDrm(bool useSteamDrm); ProtonLauncher& addEnvVar(const QString& key, const QString& value); - ProtonLauncher& setHelperProcessOut(QProcess** out); // Launch dispatch: UMU -> Proton -> Direct std::pair launch() const; @@ -35,9 +33,6 @@ private: bool launchWithProton(qint64& pid) const; bool launchWithUmu(qint64& pid) const; bool launchDirect(qint64& pid) const; - bool launchViaProcessHelper(const QString& program, const QStringList& arguments, - const QProcessEnvironment& env, - const QString& workingDir, qint64& pid) const; static bool ensureSteamRunning(); QString m_binary; @@ -50,9 +45,9 @@ private: bool m_useUmu; bool m_preferSystemUmu; bool m_useSteamRun; + bool m_useSteamDrm; QMap m_envVars; QMap m_wrapperEnvVars; - QProcess** m_helperProcessOut = nullptr; }; #endif // PROTONLAUNCHER_H diff --git a/src/src/sanitychecks.cpp b/src/src/sanitychecks.cpp index d6b285e..df0d785 100644 --- a/src/src/sanitychecks.cpp +++ b/src/src/sanitychecks.cpp @@ -385,9 +385,8 @@ int checkMissingFiles() log::debug(" . checking Linux dependencies"); int n = 0; - // Check for FUSE (skip in Flatpak — the VFS helper runs on the host) - if (!QFileInfo::exists("/.flatpak-info") && - !QFileInfo::exists("/usr/lib/libfuse3.so") && + // Check for FUSE + if (!QFileInfo::exists("/usr/lib/libfuse3.so") && !QFileInfo::exists("/usr/lib64/libfuse3.so") && !QFileInfo::exists("/usr/lib/x86_64-linux-gnu/libfuse3.so")) { log::warn("libfuse3 not found - FUSE VFS will not work"); diff --git a/src/src/settingsdialogpaths.cpp b/src/src/settingsdialogpaths.cpp index dee5362..788ad60 100644 --- a/src/src/settingsdialogpaths.cpp +++ b/src/src/settingsdialogpaths.cpp @@ -4,18 +4,6 @@ #include #include -namespace { -QFileDialog::Options flatpakSafeOptions() -{ - QFileDialog::Options opts; -#ifndef _WIN32 - if (qEnvironmentVariableIsSet("FLATPAK_ID")) - opts |= QFileDialog::DontUseNativeDialog; -#endif - return opts; -} -} // namespace - PathsSettingsTab::PathsSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d), m_gameDir(settings().game().plugin()->gameDirectory()) { @@ -143,7 +131,7 @@ void PathsSettingsTab::on_browseBaseDirBtn_clicked() { QString temp = QFileDialog::getExistingDirectory( &dialog(), QObject::tr("Select base directory"), ui->baseDirEdit->text(), - flatpakSafeOptions()); + {}); if (!temp.isEmpty()) { ui->baseDirEdit->setText(temp); } @@ -156,7 +144,7 @@ void PathsSettingsTab::on_browseDownloadDirBtn_clicked() QString temp = QFileDialog::getExistingDirectory( &dialog(), QObject::tr("Select download directory"), searchPath, - flatpakSafeOptions()); + {}); if (!temp.isEmpty()) { ui->downloadDirEdit->setText(temp); } @@ -169,7 +157,7 @@ void PathsSettingsTab::on_browseModDirBtn_clicked() QString temp = QFileDialog::getExistingDirectory( &dialog(), QObject::tr("Select mod directory"), searchPath, - flatpakSafeOptions()); + {}); if (!temp.isEmpty()) { ui->modDirEdit->setText(temp); } @@ -182,7 +170,7 @@ void PathsSettingsTab::on_browseCacheDirBtn_clicked() QString temp = QFileDialog::getExistingDirectory( &dialog(), QObject::tr("Select cache directory"), searchPath, - flatpakSafeOptions()); + {}); if (!temp.isEmpty()) { ui->cacheDirEdit->setText(temp); } @@ -195,7 +183,7 @@ void PathsSettingsTab::on_browseProfilesDirBtn_clicked() QString temp = QFileDialog::getExistingDirectory( &dialog(), QObject::tr("Select profiles directory"), searchPath, - flatpakSafeOptions()); + {}); if (!temp.isEmpty()) { ui->profilesDirEdit->setText(temp); } @@ -208,7 +196,7 @@ void PathsSettingsTab::on_browseOverwriteDirBtn_clicked() QString temp = QFileDialog::getExistingDirectory( &dialog(), QObject::tr("Select overwrite directory"), searchPath, - flatpakSafeOptions()); + {}); if (!temp.isEmpty()) { ui->overwriteDirEdit->setText(temp); } diff --git a/src/src/settingsdialogproton.cpp b/src/src/settingsdialogproton.cpp index e9d1bec..f202f5f 100644 --- a/src/src/settingsdialogproton.cpp +++ b/src/src/settingsdialogproton.cpp @@ -313,14 +313,8 @@ void ProtonSettingsTab::onOpenPrefixFolder() void ProtonSettingsTab::onBrowsePrefixLocation() { - QFileDialog::Options opts; -#ifndef _WIN32 - if (qEnvironmentVariableIsSet("FLATPAK_ID")) - opts |= QFileDialog::DontUseNativeDialog; -#endif const QString dir = QFileDialog::getExistingDirectory( - parentWidget(), tr("Select Prefix Location"), ui->prefixLocationEdit->text(), - opts); + parentWidget(), tr("Select Prefix Location"), ui->prefixLocationEdit->text()); if (!dir.isEmpty()) { ui->prefixLocationEdit->setText(dir); } @@ -425,41 +419,22 @@ void ProtonSettingsTab::onWinetricks() } } - // In Flatpak, wine/winetricks must run on the host via flatpak-spawn --host - // because Proton binaries need the host's linker and Steam Runtime libs. - const bool flatpak = QFileInfo::exists(QStringLiteral("/.flatpak-info")); - - QString program; + QString program = winetricksPath; QStringList arguments; + arguments << QStringLiteral("--gui"); - if (flatpak) { - program = QStringLiteral("flatpak-spawn"); - arguments << QStringLiteral("--host"); - for (const QString& flag : envFlags) { - arguments << QStringLiteral("--env=%1").arg(flag); - } - arguments << winetricksPath << QStringLiteral("--gui"); - } else { - program = winetricksPath; - arguments << QStringLiteral("--gui"); - - // For non-Flatpak, set env vars directly on the process - QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - for (const QString& flag : envFlags) { - const int eq = flag.indexOf('='); - if (eq > 0) { - env.insert(flag.left(eq), flag.mid(eq + 1)); - } + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + for (const QString& flag : envFlags) { + const int eq = flag.indexOf('='); + if (eq > 0) { + env.insert(flag.left(eq), flag.mid(eq + 1)); } - QProcess proc; - proc.setProcessEnvironment(env); - proc.setProgram(program); - proc.setArguments(arguments); - proc.startDetached(); - return; } - - QProcess::startDetached(program, arguments); + QProcess proc; + proc.setProcessEnvironment(env); + proc.setProgram(program); + proc.setArguments(arguments); + proc.startDetached(); } void ProtonSettingsTab::onFixGameRegistries() @@ -516,14 +491,9 @@ void ProtonSettingsTab::showGameRegistryDialog() layout->addLayout(pathLayout); QObject::connect(browseBtn, &QPushButton::clicked, &dialog, [&dialog, pathEdit]() { - QFileDialog::Options opts; -#ifndef _WIN32 - if (qEnvironmentVariableIsSet("FLATPAK_ID")) - opts |= QFileDialog::DontUseNativeDialog; -#endif const QString dir = QFileDialog::getExistingDirectory( &dialog, QObject::tr("Select Game Installation Folder"), - pathEdit->text().isEmpty() ? QDir::homePath() : pathEdit->text(), opts); + pathEdit->text().isEmpty() ? QDir::homePath() : pathEdit->text()); if (!dir.isEmpty()) { pathEdit->setText(dir); } diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index c2f15eb..05e07bd 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -644,7 +644,7 @@ int spawn(const SpawnParameters& sp, pid_t& processId) QSettings().value("fluorine/prefer_system_umu", false).toBool()) .setUseSteamRun( QSettings().value("fluorine/use_steam_run", false).toBool()) - .setHelperProcessOut(sp.helperProcessOut); + .setSteamDrm(QSettings().value("fluorine/steam_drm", true).toBool()); const QString prefixPath = resolvePrefixPath(); if (prefixPath.isEmpty()) { diff --git a/src/src/spawn.h b/src/src/spawn.h index 72c81be..d922ddc 100644 --- a/src/src/spawn.h +++ b/src/src/spawn.h @@ -63,7 +63,6 @@ struct SpawnParameters #else int stdOut = -1; int stdErr = -1; - QProcess** helperProcessOut = nullptr; #endif }; diff --git a/src/src/vfs/vfs_helper_main.cpp b/src/src/vfs/vfs_helper_main.cpp deleted file mode 100644 index d983574..0000000 --- a/src/src/vfs/vfs_helper_main.cpp +++ /dev/null @@ -1,337 +0,0 @@ -// Standalone VFS helper for Flatpak FUSE support. -// Runs on the host via flatpak-spawn --host, where FUSE works normally. -// Communicates with MO2 GUI via stdin/stdout pipes. - -#include "inodetable.h" -#include "mo2filesystem.h" -#include "overwritemanager.h" -#include "vfstree.h" - -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -struct HelperConfig -{ - std::string mount_point; - std::string game_dir; - std::string data_dir_name; - std::string overwrite_dir; - std::string output_dir; - std::vector> mods; - std::vector> extra_files; -}; - -static HelperConfig readConfig(const std::string& path) -{ - HelperConfig cfg; - std::ifstream in(path); - std::string line; - - while (std::getline(in, line)) { - if (line.empty() || line[0] == '#') { - continue; - } - - const auto eq = line.find('='); - if (eq == std::string::npos) { - continue; - } - - const std::string key = line.substr(0, eq); - const std::string val = line.substr(eq + 1); - - if (key == "mount_point") { - cfg.mount_point = val; - } else if (key == "game_dir") { - cfg.game_dir = val; - } else if (key == "data_dir_name") { - cfg.data_dir_name = val; - } else if (key == "overwrite_dir") { - cfg.overwrite_dir = val; - } else if (key == "output_dir") { - cfg.output_dir = val; - } else if (key == "mod") { - const auto pipe = val.find('|'); - if (pipe != std::string::npos) { - cfg.mods.emplace_back(val.substr(0, pipe), val.substr(pipe + 1)); - } - } else if (key == "extra_file") { - const auto pipe = val.find('|'); - if (pipe != std::string::npos) { - cfg.extra_files.emplace_back(val.substr(0, pipe), val.substr(pipe + 1)); - } - } - } - - return cfg; -} - -static void tryUnmountStale(const std::string& path) -{ - pid_t pid = fork(); - if (pid == 0) { - int devnull = open("/dev/null", O_WRONLY); - if (devnull >= 0) { - dup2(devnull, STDERR_FILENO); - close(devnull); - } - execlp("fusermount3", "fusermount3", "-u", path.c_str(), nullptr); - _exit(1); - } - if (pid > 0) { - int status; - waitpid(pid, &status, 0); - } -} - -static void flushStaging(const std::string& stagingDir, - const std::string& overwriteDir, - const std::string& outputDir = {}) -{ - const fs::path staging(stagingDir); - const fs::path overwrite = outputDir.empty() - ? fs::path(overwriteDir) - : fs::path(outputDir); - if (!fs::exists(staging)) { - return; - } - - std::error_code ec; - for (auto it = fs::recursive_directory_iterator( - staging, fs::directory_options::skip_permission_denied); - it != fs::recursive_directory_iterator(); ++it) { - const auto& entry = *it; - const fs::path rel = fs::relative(entry.path(), staging, ec); - if (ec || rel.empty()) { - continue; - } - - const fs::path dest = overwrite / rel; - if (entry.is_directory(ec)) { - fs::create_directories(dest, ec); - continue; - } - - if (!entry.is_regular_file(ec)) { - continue; - } - - fs::create_directories(dest.parent_path(), ec); - fs::rename(entry.path(), dest, ec); - if (ec) { - ec.clear(); - fs::copy_file(entry.path(), dest, fs::copy_options::overwrite_existing, ec); - if (!ec) { - fs::remove(entry.path(), ec); - } - } - } - - fs::remove_all(staging, ec); -} - -static void setupFuseOps(struct fuse_lowlevel_ops* ops) -{ - std::memset(ops, 0, sizeof(struct fuse_lowlevel_ops)); - ops->lookup = mo2_lookup; - ops->getattr = mo2_getattr; - ops->readdir = mo2_readdir; - ops->open = mo2_open; - ops->read = mo2_read; - ops->write = mo2_write; - ops->create = mo2_create; - ops->rename = mo2_rename; - ops->setattr = mo2_setattr; - ops->unlink = mo2_unlink; - ops->mkdir = mo2_mkdir; - ops->release = mo2_release; -} - -static struct fuse_session* g_session = nullptr; - -static void signalHandler(int /*sig*/) -{ - if (g_session) { - fuse_session_exit(g_session); - } -} - -int main(int argc, char* argv[]) -{ - if (argc < 2) { - std::cerr << "Usage: mo2-vfs-helper \n"; - return 1; - } - - const std::string configPath = argv[1]; - auto config = readConfig(configPath); - - if (config.mount_point.empty()) { - std::cout << "error: mount_point not set in config" << std::endl; - return 1; - } - - const std::string dataDirPath = config.mount_point; - const std::string stagingDir = - (fs::path(config.overwrite_dir).parent_path() / "VFS_staging").string(); - - if (!fs::exists(dataDirPath)) { - std::cout << "error: data directory does not exist: " << dataDirPath - << std::endl; - return 1; - } - - std::error_code ec; - fs::create_directories(stagingDir, ec); - fs::create_directories(config.overwrite_dir, ec); - if (!config.output_dir.empty()) { - fs::create_directories(config.output_dir, ec); - } - - // Scan base game files BEFORE mounting (after mount they're hidden) - auto baseFileCache = scanDataDir(dataDirPath); - - // Open fd to data dir BEFORE mounting so we can access original files - int backingFd = open(dataDirPath.c_str(), O_RDONLY | O_DIRECTORY); - if (backingFd < 0) { - std::cout << "error: failed to open backing fd for " << dataDirPath - << std::endl; - return 1; - } - - // Clean up any stale FUSE mount - tryUnmountStale(dataDirPath); - - // Build VFS tree - auto tree = std::make_shared( - buildDataDirVfs(baseFileCache, dataDirPath, config.mods, - config.overwrite_dir)); - injectExtraFiles(*tree, config.extra_files); - - auto context = std::make_shared(); - context->tree = tree; - context->inodes = std::make_unique(); - context->overwrite = - std::make_unique(stagingDir, config.overwrite_dir); - context->backing_dir_fd = backingFd; - context->uid = ::getuid(); - context->gid = ::getgid(); - - // Setup FUSE - std::vector argvStorage = { - "mo2-vfs-helper", "-o", "fsname=mo2linux", "-o", "default_permissions", - "-o", "noatime"}; - - std::vector fuseArgv; - fuseArgv.reserve(argvStorage.size()); - for (auto& s : argvStorage) { - fuseArgv.push_back(s.data()); - } - - struct fuse_args args = - FUSE_ARGS_INIT(static_cast(fuseArgv.size()), fuseArgv.data()); - - struct fuse_lowlevel_ops ops; - setupFuseOps(&ops); - - struct fuse_session* session = - fuse_session_new(&args, &ops, sizeof(ops), context.get()); - if (session == nullptr) { - close(backingFd); - std::cout << "error: failed to create FUSE session" << std::endl; - return 1; - } - - if (fuse_session_mount(session, dataDirPath.c_str()) != 0) { - fuse_session_destroy(session); - close(backingFd); - std::cout << "error: failed to mount FUSE at " << dataDirPath << std::endl; - return 1; - } - - g_session = session; - - // Handle signals for clean shutdown - struct sigaction sa; - sa.sa_handler = signalHandler; - sigemptyset(&sa.sa_mask); - sa.sa_flags = 0; - sigaction(SIGINT, &sa, nullptr); - sigaction(SIGTERM, &sa, nullptr); - - // Start FUSE event loop in background thread - std::thread fuseThread([session]() { - fuse_session_loop_mt(session, nullptr); - }); - - std::cout << "mounted" << std::endl; - - // Command loop: read commands from stdin - std::string line; - while (std::getline(std::cin, line)) { - if (line == "rebuild") { - auto newConfig = readConfig(configPath); - auto newTree = std::make_shared(buildDataDirVfs( - baseFileCache, dataDirPath, newConfig.mods, newConfig.overwrite_dir)); - injectExtraFiles(*newTree, newConfig.extra_files); - - { - std::unique_lock lock(context->tree_mutex); - context->tree.swap(newTree); - } - - config = newConfig; - std::cout << "ok" << std::endl; - } else if (line == "flush") { - flushStaging(stagingDir, config.overwrite_dir, config.output_dir); - fs::create_directories(stagingDir, ec); - - auto newTree = std::make_shared(buildDataDirVfs( - baseFileCache, dataDirPath, config.mods, config.overwrite_dir)); - injectExtraFiles(*newTree, config.extra_files); - - { - std::unique_lock lock(context->tree_mutex); - context->tree.swap(newTree); - } - - context->overwrite = - std::make_unique(stagingDir, config.overwrite_dir); - std::cout << "ok" << std::endl; - } else if (line == "quit") { - break; - } - } - - // Clean shutdown - fuse_session_exit(session); - fuse_session_unmount(session); - - if (fuseThread.joinable()) { - fuseThread.join(); - } - - fuse_session_destroy(session); - g_session = nullptr; - - flushStaging(stagingDir, config.overwrite_dir, config.output_dir); - close(backingFd); - - std::cout << "ok" << std::endl; - return 0; -} diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp index 8ff20e6..bb4e166 100644 --- a/src/src/wineprefix.cpp +++ b/src/src/wineprefix.cpp @@ -145,8 +145,8 @@ bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDi } const QString pluginsDir = QDir(appdataLocal()).filePath(dataDir); - MOBase::log::debug("deployPlugins: target dir='{}', {} plugins to deploy", - pluginsDir, plugins.size()); + MOBase::log::info("deployPlugins: target dir='{}', count={}", pluginsDir, + plugins.size()); if (!QDir().mkpath(pluginsDir)) { MOBase::log::error("deployPlugins: failed to create directory '{}'", pluginsDir); @@ -179,8 +179,8 @@ bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDi pluginsStream << plugin << "\r\n"; } pluginsFile.close(); - MOBase::log::debug("deployPlugins: wrote {} plugins to '{}'", plugins.size(), - pluginsPath); + MOBase::log::info("deployPlugins: wrote {} plugins to '{}'", plugins.size(), + pluginsPath); // Also write lowercase "plugins.txt" for games that expect it (e.g. FalloutNV). const QString pluginsLower = QDir(pluginsDir).filePath("plugins.txt"); @@ -208,7 +208,7 @@ bool WinePrefix::deployPlugins(const QStringList& plugins, const QString& dataDi loadOrderStream << line << "\r\n"; } - MOBase::log::debug("deployPlugins: wrote loadorder.txt to '{}'", loadOrderPath); + MOBase::log::info("deployPlugins: wrote loadorder.txt to '{}'", loadOrderPath); return true; } -- cgit v1.3.1