diff options
Diffstat (limited to 'src')
35 files changed, 641 insertions, 1679 deletions
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 $<$<BOOL:${Qt6WebEngineWidgets_FOUND}>: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<int> 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<int> 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<OrganizerCore*>(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 <QDir> #include <QFile> +#include <QFileInfo> +#include <QStandardPaths> #include <QTextStream> #include <cstdio> -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 <QString> -/// 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<std::chrono::milliseconds>( - deadline - std::chrono::steady_clock::now()) - .count(); - if (remaining <= 0) { - break; - } - proc->waitForReadyRead(static_cast<int>(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<std::pair<std::string, std::string>>& 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<std::pair<std::string, std::string>>& 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 <QObject> #include <QString> -class QProcess; - #include <exception> #include <memory> #include <thread> @@ -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<std::pair<std::string, std::string>>& 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<std::pair<std::string, std::string>>& 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 <QCheckBox>
#include <QFile>
#include <QFileDialog>
#include <QSettings>
@@ -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 @@ </property> </widget> </item> + <item row="10" column="0" colspan="2"> + <widget class="QCheckBox" name="steamDrmCheckBox"> + <property name="text"> + <string>Steam DRM integration</string> + </property> + <property name="toolTip"> + <string>Enable Steam DRM bridge for this instance. Disable for GOG or DRM-free games to prevent crashes.</string> + </property> + </widget> + </item> </layout> </widget> </item> 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("<Edit...>"), {});
-
- for (const auto& exe : *m_OrganizerCore.executablesList()) {
- if (!exe.hide()) {
- add(exe.title(), exe.binaryInfo());
- }
- }
+ ui->executablesListBox->setEnabled(false); + ui->executablesListBox->clear(); + + add(tr("<Edit...>"), {}); + + 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 <http://www.gnu.org/licenses/>. #include "shared/util.h"
#include "thread_utils.h"
#include "tutorialmanager.h"
-#include <QDebug>
-#include <QFile>
-#include <QPainter>
+#include <QDebug> +#include <QFile> +#include <QPainter> #include <QProxyStyle>
#include <QSslSocket>
#include <QStringList>
@@ -51,9 +51,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QStyleOption>
#include <iplugingame.h>
#include <log.h>
-#include <report.h>
-#include <scopeguard.h>
-#include <utility.h>
+#include <report.h> +#include <scopeguard.h> +#include <utility.h> +#include <cstdio> #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<PluginContainer>(m_core.get());
- m_plugins->loadPlugins();
- log::debug("all plugins loaded");
+ m_plugins = std::make_unique<PluginContainer>(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<LocalSavegames>();
- 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<LocalSavegames>(); + 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<QPair<QString, QString>> 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 <QAction>
-#include <QCoreApplication>
-#include <QDirIterator>
-#include <QMessageBox>
-#include <QToolButton>
+#include <QAction> +#include <QCoreApplication> +#include <QDirIterator> +#include <QMessageBox> +#include <QToolButton> +#include <cstdio> #include <boost/fusion/algorithm/iteration/for_each.hpp>
#include <boost/fusion/include/at_key.hpp>
#include <boost/fusion/include/for_each.hpp>
@@ -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<IPluginProxy*>(plugin);
- if (initPlugin(proxy, pluginProxy, skipInit)) {
- bf::at_key<IPluginProxy>(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<IPluginProxy*>(plugin); + if (initPlugin(proxy, pluginProxy, skipInit)) { + bf::at_key<IPluginProxy>(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<QObject*>& plugins) con }
}
-std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath,
- IPluginProxy* proxy)
-{
- std::vector<QObject*> 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<QObject*> matchingPlugins = proxy->load(filepath);
-
- // We are going to group plugin by names and "fix" them later:
- std::map<QString, std::vector<IPlugin*>> 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<QObject*> PluginContainer::loadProxied(const QString& filepath, + IPluginProxy* proxy) +{ + std::vector<QObject*> proxiedPlugins; + + try { + log::warn("loading proxied plugin candidate '{}' via proxy '{}'", + QDir::toNativeSeparators(filepath), + (proxy ? proxy->name() : QStringLiteral("<null>"))); + printPluginDiagToStderr( + QString("loading proxied plugin candidate '%1' via proxy '%2'") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral("<null>"))); + + // 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<QObject*> 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<QString, std::vector<IPlugin*>> 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<QObject*> 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("<null>")), e.what()); + printPluginDiagToStderr( + QString("failed to initialize proxied candidate '%1' via proxy '%2': %3") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral("<null>")) + .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("<null>"))); + printPluginDiagToStderr( + QString("failed to initialize proxied candidate '%1' via proxy '%2': unknown " + "exception") + .arg(QDir::toNativeSeparators(filepath)) + .arg(proxy ? proxy->name() : QStringLiteral("<null>"))); + 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=<path>, arg=<value> (repeatable), env=KEY=VALUE (repeatable), -// workdir=<path> -// Helper responds: "started <pid>" or "error <message>" -// Runtime commands (MO2→helper): "kill" (SIGTERM child tree), "quit" -// Helper reports: "exited <code>" when game process tree exits - -#include <dirent.h> -#include <errno.h> -#include <fcntl.h> -#include <poll.h> -#include <signal.h> -#include <sys/wait.h> -#include <unistd.h> - -#include <cstdio> -#include <cstdlib> -#include <cstring> -#include <fstream> -#include <string> -#include <unordered_set> -#include <vector> - -// ── 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<pid_t> collectDescendants(pid_t root) -{ - std::unordered_set<pid_t> descendants; - - // Build parent→children map - struct ProcEntry { - pid_t pid; - pid_t ppid; - }; - std::vector<ProcEntry> 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<pid_t>(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<pid_t>(strtol(line.c_str() + 5, nullptr, 10)); - break; - } - } - - if (ppid > 0) { - entries.push_back({pid, ppid}); - } - } - closedir(proc); - - // BFS from root - std::vector<pid_t> 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<pid_t>& 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<std::string> args; - std::vector<std::string> 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<char*>(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<const char*> 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<char* const*>(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::Results> ProcessRunner::runBinary() #ifdef _WIN32
m_handle.reset(startBinary(parent, m_sp));
#else
- m_sp.helperProcessOut = &m_processHelper;
m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(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<OrganizerCore> 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 <nak_ffi.h> #include <QCoreApplication> @@ -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<bool, qint64> 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. - 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"); - } - } else { - const QString appDir = QCoreApplication::applicationDirPath(); - const QString bundled = appDir + QStringLiteral("/umu-run"); + 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; - } + // 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 (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; - } + QString umuRun; + 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-<AppID>" 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 <pid>" or "error <message>" - 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 <cstdint> #include <utility> -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<bool, qint64> 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<QString, QString> m_envVars; QMap<QString, QString> 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 <QFileDialog>
#include <iplugingame.h>
-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 <fuse3/fuse_lowlevel.h> - -#include <fcntl.h> -#include <signal.h> -#include <sys/wait.h> -#include <unistd.h> - -#include <cstring> -#include <filesystem> -#include <fstream> -#include <iostream> -#include <memory> -#include <string> -#include <thread> -#include <vector> - -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<std::pair<std::string, std::string>> mods; - std::vector<std::pair<std::string, std::string>> 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 <config-file>\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<VfsTree>( - buildDataDirVfs(baseFileCache, dataDirPath, config.mods, - config.overwrite_dir)); - injectExtraFiles(*tree, config.extra_files); - - auto context = std::make_shared<Mo2FsContext>(); - context->tree = tree; - context->inodes = std::make_unique<InodeTable>(); - context->overwrite = - std::make_unique<OverwriteManager>(stagingDir, config.overwrite_dir); - context->backing_dir_fd = backingFd; - context->uid = ::getuid(); - context->gid = ::getgid(); - - // Setup FUSE - std::vector<std::string> argvStorage = { - "mo2-vfs-helper", "-o", "fsname=mo2linux", "-o", "default_permissions", - "-o", "noatime"}; - - std::vector<char*> fuseArgv; - fuseArgv.reserve(argvStorage.size()); - for (auto& s : argvStorage) { - fuseArgv.push_back(s.data()); - } - - struct fuse_args args = - FUSE_ARGS_INIT(static_cast<int>(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<VfsTree>(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<VfsTree>(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<OverwriteManager>(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; } |
