diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-20 23:53:26 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-20 23:53:26 -0600 |
| commit | d22d8c267b012729b6b84142def2a30ca4691d7a (patch) | |
| tree | 4d04382b6c9a44fa72ade906e0bf51e8f97855a8 /src | |
| parent | 5467e210668eb24de9a4429ef0ddcbc6442e8932 (diff) | |
Fix AppImage plugin loading, save timestamps, and INI corruption
- Add AppConfig::pluginsPath()/dllsPath() that respect MO2_PLUGINS_DIR
and MO2_DLLS_DIR env vars, fixing plugin discovery when plugins live
inside read-only AppImage squashfs (#22, game plugin not found errors)
- Preserve file modification times in save deploy/sync so games display
correct save timestamps instead of all showing the same time (#17)
- Replace QSettings INI reader in saves tab with direct line-by-line
parser to avoid backslash-as-line-continuation corruption of
sLocalSavePath=__MO_Saves\ (#22, saves tab showing wrong directory)
- Add diagnostic logging to plugin symlink creation, save backup/restore
operations to help diagnose silent failures through prefix symlinks
- Fix Python SONAME mismatch in AppImage build (patch portable Python
SONAME to match librunner.so DT_NEEDED, preventing dual libpython load)
- Bundle PyQt6 Qt dependencies, improve RPATH for lib/ and plugins/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/src/commandline.cpp | 3 | ||||
| -rw-r--r-- | src/src/mainwindow.cpp | 162 | ||||
| -rw-r--r-- | src/src/moapplication.cpp | 129 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 129 | ||||
| -rw-r--r-- | src/src/plugincontainer.cpp | 326 | ||||
| -rw-r--r-- | src/src/processrunner.cpp | 412 | ||||
| -rw-r--r-- | src/src/protonlauncher.cpp | 201 | ||||
| -rw-r--r-- | src/src/protonlauncher.h | 2 | ||||
| -rw-r--r-- | src/src/savestab.cpp | 655 | ||||
| -rw-r--r-- | src/src/shared/appconfig.cpp | 22 | ||||
| -rw-r--r-- | src/src/shared/appconfig.h | 10 | ||||
| -rw-r--r-- | src/src/spawn.cpp | 550 | ||||
| -rw-r--r-- | src/src/wineprefix.cpp | 36 |
13 files changed, 1427 insertions, 1210 deletions
diff --git a/src/src/commandline.cpp b/src/src/commandline.cpp index 4613350..121fb58 100644 --- a/src/src/commandline.cpp +++ b/src/src/commandline.cpp @@ -853,8 +853,7 @@ std::optional<int> ReloadPluginCommand::runPostOrganizer(OrganizerCore& core) const QString name = QString::fromStdString(vm()["PLUGIN"].as<std::string>());
QString filepath =
- QDir(AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()))
- .absoluteFilePath(name);
+ QDir(AppConfig::pluginsPath()).absoluteFilePath(name);
log::debug("reloading plugin from {}", filepath);
core.pluginContainer().reloadPlugin(filepath);
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index 7aa6964..1eb5e36 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -544,12 +544,12 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, QApplication::instance()->installEventFilter(this);
- scheduleCheckForProblems(); - refreshExecutablesList(); - updatePinnedExecutables(); - resetActionIcons(); - resetButtonIcons(); - processUpdates(); + scheduleCheckForProblems();
+ refreshExecutablesList();
+ updatePinnedExecutables();
+ resetActionIcons();
+ resetButtonIcons();
+ processUpdates();
ui->modList->updateModCount();
ui->espList->updatePluginCount();
@@ -573,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
@@ -636,38 +636,38 @@ void MainWindow::resetActionIcons() }
}
- // 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() -{ + // 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();
@@ -739,11 +739,11 @@ void MainWindow::allowListResize() ui->espList->header()->setStretchLastSection(true);
}
-void MainWindow::updateStyle(const QString&) -{ - resetActionIcons(); - resetButtonIcons(); -} +void MainWindow::updateStyle(const QString&)
+{
+ resetActionIcons();
+ resetButtonIcons();
+}
void MainWindow::resizeEvent(QResizeEvent* event)
{
@@ -1913,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()) {
@@ -1932,36 +1932,36 @@ void MainWindow::refreshExecutablesList() Qt::SizeHintRole);
};
- 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()); - } + 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
@@ -2692,9 +2692,7 @@ void MainWindow::openInstallFolder() void MainWindow::openPluginsFolder()
{
- QString pluginsPath =
- AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath());
- shell::Explore(pluginsPath);
+ shell::Explore(AppConfig::pluginsPath());
}
void MainWindow::openStylesheetsFolder()
diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp index fd60745..3009a45 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,10 +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 <cstdio> +#include <report.h>
+#include <scopeguard.h>
+#include <utility.h>
+#include <cstdio>
#ifdef _WIN32
// see addDllsToPath() below
@@ -270,43 +270,43 @@ void MOApplication::firstTimeSetup(MOMultiProcess& multiProcess) Qt::QueuedConnection);
}
-int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) -{ - TimeThis tt("MOApplication setup()"); - std::fprintf(stderr, "[setup-diag] setup() entered\n"); - std::fflush(stderr); +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) { - 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); + 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)) { - 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); + 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()));
@@ -336,11 +336,11 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) }
// loading settings
- 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()); + 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());
@@ -375,30 +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)); - 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); + 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()); - 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"); + 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...");
@@ -456,14 +456,15 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) m_core->createDefaultProfile();
m_core->createOverwriteDirectories();
- log::info("using game plugin '{}' ('{}', variant {}, steam id '{}') at {}",
- m_instance->gamePlugin()->gameName(),
- m_instance->gamePlugin()->gameShortName(),
- (m_settings->game().edition().value_or("").isEmpty()
- ? "(none)"
- : *m_settings->game().edition()),
- m_instance->gamePlugin()->steamAPPId(),
- m_instance->gamePlugin()->gameDirectory().absolutePath());
+ {
+ const auto edition = m_settings->game().edition().value_or("");
+ const auto variant = edition.isEmpty() ? QString("Steam") : edition;
+ log::info("using game plugin '{}' ('{}', variant {}) at {}",
+ m_instance->gamePlugin()->gameName(),
+ m_instance->gamePlugin()->gameShortName(),
+ variant,
+ m_instance->gamePlugin()->gameDirectory().absolutePath());
+ }
CategoryFactory::instance().loadCategories();
m_core->updateExecutablesList();
diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 630f23b..79ea90b 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -904,8 +904,7 @@ QString OrganizerCore::pluginDataPath() // separate writable directory so mkdir() never hits a read-only FS.
return AppConfig::basePath() + "/plugin_data";
#else
- return AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()) +
- "/data";
+ return AppConfig::pluginsPath() + "/data";
#endif
}
@@ -2193,23 +2192,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); - 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)) { + 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()) {
@@ -2219,17 +2218,17 @@ bool OrganizerCore::beforeRun( }
}
pluginsFile.close();
- - 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 { +
+ 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);
@@ -2244,18 +2243,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::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); - } + 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,
@@ -2267,15 +2266,15 @@ bool OrganizerCore::beforeRun( managedGame()->documentsDirectory().absolutePath());
}
- 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 { + 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);
}
@@ -2321,29 +2320,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::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->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::info("INI sync target: '{}' <- '{}'", 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 dad0117..e215c0a 100644 --- a/src/src/plugincontainer.cpp +++ b/src/src/plugincontainer.cpp @@ -4,12 +4,12 @@ #include "organizerproxy.h"
#include "report.h"
#include "shared/appconfig.h"
-#include <QAction> -#include <QCoreApplication> -#include <QDirIterator> -#include <QMessageBox> -#include <QToolButton> -#include <cstdio> +#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>
@@ -22,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);
@@ -54,16 +54,19 @@ static void ensureBundledPluginsLinked(const QString& bundledDir, }
}
- QFile::link(it.filePath(), target);
+ if (!QFile::link(it.filePath(), target)) {
+ log::warn("ensureBundledPluginsLinked: failed to symlink '{}' -> '{}'",
+ it.filePath(), target);
+ }
}
-} -#endif - -static void printPluginDiagToStderr(const QString& message) -{ - std::fprintf(stderr, "[plugin-diag] %s\n", qUtf8Printable(message)); - std::fflush(stderr); -} +}
+#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!
//
@@ -446,9 +449,7 @@ QStringList PluginContainer::pluginFileNames() const std::vector<IPluginProxy*> proxyList = bf::at_key<IPluginProxy>(m_Plugins);
for (IPluginProxy* proxy : proxyList) {
QStringList proxiedPlugins = proxy->pluginList(
- m_PluginPath.isEmpty()
- ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()))
- : m_PluginPath);
+ m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath);
result.append(proxiedPlugins);
}
return result;
@@ -643,33 +644,31 @@ 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); - - 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; - } - } + { // 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::pluginsPath() : 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
@@ -853,70 +852,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 { - 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())); - } - } +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),
@@ -928,43 +927,43 @@ std::vector<QObject*> PluginContainer::loadProxied(const QString& filepath, m_Requirements.at(proxiedPlugin).setMaster(*it);
}
}
- } - } - 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; -} + }
+ }
+ 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)
{
@@ -1033,9 +1032,7 @@ void PluginContainer::loadPlugin(QString const& filepath) // We need to check if this can be handled by a proxy.
for (auto* proxy : this->plugins<IPluginProxy>()) {
auto filepaths = proxy->pluginList(
- m_PluginPath.isEmpty()
- ? (AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath()))
- : m_PluginPath);
+ m_PluginPath.isEmpty() ? AppConfig::pluginsPath() : m_PluginPath);
if (filepaths.contains(filepath)) {
plugins = loadProxied(filepath, proxy);
break;
@@ -1247,14 +1244,13 @@ void PluginContainer::loadPlugins() loadCheck.close();
}
- if (!loadCheck.open(QIODevice::WriteOnly)) { - log::warn("failed to open loadcheck file for writing '{}'", - QDir::toNativeSeparators(loadCheck.fileName())); - } + if (!loadCheck.open(QIODevice::WriteOnly)) {
+ log::warn("failed to open loadcheck file for writing '{}'",
+ QDir::toNativeSeparators(loadCheck.fileName()));
+ }
}
- QString pluginPath =
- AppConfig::basePath() + "/" + ToQString(AppConfig::pluginPath());
+ QString pluginPath = AppConfig::pluginsPath();
#ifndef _WIN32
// Per-instance plugin directory: symlink bundled plugins, then load from there.
@@ -1334,15 +1330,15 @@ void PluginContainer::loadPlugins() }
log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin);
- if (loadCheck.open(QIODevice::WriteOnly)) { - loadCheck.write(skipPlugin.toUtf8()); - loadCheck.write("\n"); - loadCheck.flush(); - } else { - log::warn("failed to persist skipped plugin to '{}'", - QDir::toNativeSeparators(loadCheck.fileName())); - } - } + if (loadCheck.open(QIODevice::WriteOnly)) {
+ loadCheck.write(skipPlugin.toUtf8());
+ loadCheck.write("\n");
+ loadCheck.flush();
+ } else {
+ log::warn("failed to persist skipped plugin to '{}'",
+ QDir::toNativeSeparators(loadCheck.fileName()));
+ }
+ }
bf::at_key<IPluginDiagnose>(m_Plugins).push_back(this);
diff --git a/src/src/processrunner.cpp b/src/src/processrunner.cpp index e7354f3..892fd79 100644 --- a/src/src/processrunner.cpp +++ b/src/src/processrunner.cpp @@ -22,43 +22,45 @@ #include <dirent.h>
#include <fstream>
#include <signal.h>
+#include <sys/wait.h>
#include <unordered_map>
#include <unordered_set>
-#include <sys/wait.h>
+
#endif
using namespace MOBase;
-void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp,
- const Settings& settings)
-{
+void adjustForVirtualized(const IPluginGame *game, spawn::SpawnParameters &sp,
+ const Settings &settings) {
const QString modsPath = settings.paths().mods();
// Check if this a request with either an executable or a working directory
// under our mods folder then will start the process in a virtualized
// "environment" with the appropriate paths fixed:
// (i.e. mods\FNIS\path\exe => game\data\path\exe)
- QString cwdPath = sp.currentDirectory.absolutePath();
+ QString cwdPath = sp.currentDirectory.absolutePath();
QString trailedModsPath = modsPath;
if (!trailedModsPath.endsWith('/')) {
trailedModsPath = trailedModsPath + '/';
}
- bool virtualizedCwd = cwdPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
- QString binPath = sp.binary.absoluteFilePath();
- bool virtualizedBin = binPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
+ bool virtualizedCwd =
+ cwdPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
+ QString binPath = sp.binary.absoluteFilePath();
+ bool virtualizedBin =
+ binPath.startsWith(trailedModsPath, Qt::CaseInsensitive);
if (virtualizedCwd || virtualizedBin) {
if (virtualizedCwd) {
- int cwdOffset = cwdPath.indexOf('/', trailedModsPath.length());
+ int cwdOffset = cwdPath.indexOf('/', trailedModsPath.length());
QString adjustedCwd = cwdPath.mid(cwdOffset, -1);
- cwdPath = game->dataDirectory().absolutePath();
+ cwdPath = game->dataDirectory().absolutePath();
if (cwdOffset >= 0)
cwdPath += adjustedCwd;
}
if (virtualizedBin) {
- int binOffset = binPath.indexOf('/', trailedModsPath.length());
+ int binOffset = binPath.indexOf('/', trailedModsPath.length());
QString adjustedBin = binPath.mid(binOffset, -1);
- binPath = game->dataDirectory().absolutePath();
+ binPath = game->dataDirectory().absolutePath();
if (binOffset >= 0)
binPath += adjustedBin;
}
@@ -69,7 +71,7 @@ void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp, .arg(QDir::toNativeSeparators(cwdPath),
QDir::toNativeSeparators(binPath), sp.arguments);
- sp.binary = QFileInfo(QCoreApplication::applicationFilePath());
+ sp.binary = QFileInfo(QCoreApplication::applicationFilePath());
sp.arguments = cmdline;
sp.currentDirectory.setPath(QCoreApplication::applicationDirPath());
#else
@@ -97,8 +99,7 @@ void adjustForVirtualized(const IPluginGame* game, spawn::SpawnParameters& sp, }
#ifdef _WIN32
-std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
-{
+std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) {
if (handle == INVALID_HANDLE_VALUE) {
return ProcessRunner::Error;
}
@@ -116,7 +117,7 @@ std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) return {};
}
- case WAIT_FAILED: // fall-through
+ case WAIT_FAILED: // fall-through
default: {
// error
const auto e = ::GetLastError();
@@ -126,23 +127,16 @@ std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) }
}
#else
-std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid)
-{
+std::optional<ProcessRunner::Results> singleWait(HANDLE handle, DWORD pid) {
Q_UNUSED(handle);
Q_UNUSED(pid);
return ProcessRunner::Completed;
}
#endif
-enum class Interest
-{
- None = 0,
- Weak,
- Strong
-};
+enum class Interest { None = 0, Weak, Strong };
-QString toString(Interest i)
-{
+QString toString(Interest i) {
switch (i) {
case Interest::Weak:
return "weak";
@@ -150,22 +144,20 @@ QString toString(Interest i) case Interest::Strong:
return "strong";
- case Interest::None: // fall-through
+ case Interest::None: // fall-through
default:
return "no";
}
}
-struct InterestingProcess
-{
+struct InterestingProcess {
env::Process p;
Interest interest = Interest::None;
env::HandlePtr handle;
};
-InterestingProcess findRandomProcess(const env::Process& root)
-{
- for (auto&& c : root.children()) {
+InterestingProcess findRandomProcess(const env::Process &root) {
+ for (auto &&c : root.children()) {
env::HandlePtr h = c.openHandleForWait();
if (h) {
return {c, Interest::Weak, std::move(h)};
@@ -183,18 +175,18 @@ InterestingProcess findRandomProcess(const env::Process& root) // returns a process that's in the hidden list, or the top-level process if
// they're all hidden; returns an invalid process if the list is empty
//
-InterestingProcess findInterestingProcessInTrees(const env::Process& root)
-{
+InterestingProcess findInterestingProcessInTrees(const env::Process &root) {
// Certain process names we wish to "hide" for aesthetic reason:
static const std::vector<QString> hiddenList = {
- QFileInfo(QCoreApplication::applicationFilePath()).fileName(), "conhost.exe"};
+ QFileInfo(QCoreApplication::applicationFilePath()).fileName(),
+ "conhost.exe"};
if (root.children().empty()) {
return {};
}
- auto isHidden = [&](auto&& p) {
- for (auto& h : hiddenList) {
+ auto isHidden = [&](auto &&p) {
+ for (auto &h : hiddenList) {
if (p.name().contains(h, Qt::CaseInsensitive)) {
return true;
}
@@ -203,7 +195,7 @@ InterestingProcess findInterestingProcessInTrees(const env::Process& root) return false;
};
- for (auto&& p : root.children()) {
+ for (auto &&p : root.children()) {
if (!isHidden(p)) {
env::HandlePtr h = p.openHandleForWait();
if (h) {
@@ -221,21 +213,19 @@ InterestingProcess findInterestingProcessInTrees(const env::Process& root) return findRandomProcess(root);
}
-void dump(const env::Process& p, int indent)
-{
- log::debug("{}{}, pid={}, ppid={}", std::string(indent * 4, ' '), p.name(), p.pid(),
- p.ppid());
+void dump(const env::Process &p, int indent) {
+ log::debug("{}{}, pid={}, ppid={}", std::string(indent * 4, ' '), p.name(),
+ p.pid(), p.ppid());
- for (auto&& c : p.children()) {
+ for (auto &&c : p.children()) {
dump(c, indent + 1);
}
}
-void dump(const env::Process& root)
-{
+void dump(const env::Process &root) {
log::debug("process tree:");
- for (auto&& p : root.children()) {
+ for (auto &&p : root.children()) {
dump(p, 1);
}
}
@@ -243,8 +233,7 @@ void dump(const env::Process& root) #ifdef _WIN32
// gets the most interesting process in the list
//
-InterestingProcess getInterestingProcess(HANDLE job)
-{
+InterestingProcess getInterestingProcess(HANDLE job) {
env::Process root = env::getProcessTree(job);
if (root.children().empty()) {
log::debug("nothing to wait for");
@@ -270,10 +259,9 @@ const std::chrono::milliseconds Infinite(-1); // waits for completion, times out after `wait` if not Infinite
//
std::optional<ProcessRunner::Results> timedWait(HANDLE handle, DWORD pid,
- UILocker::Session* ls,
+ UILocker::Session *ls,
std::chrono::milliseconds wait,
- std::atomic<bool>& interrupt)
-{
+ std::atomic<bool> &interrupt) {
using namespace std::chrono;
high_resolution_clock::time_point start;
@@ -310,7 +298,7 @@ std::optional<ProcessRunner::Results> timedWait(HANDLE handle, DWORD pid, return ProcessRunner::Cancelled;
}
- case UILocker::NoResult: // fall-through
+ case UILocker::NoResult: // fall-through
default: {
// shouldn't happen
log::debug("unexpected result {} while waiting for {}",
@@ -335,9 +323,9 @@ std::optional<ProcessRunner::Results> timedWait(HANDLE handle, DWORD pid, return ProcessRunner::ForceUnlocked;
}
-ProcessRunner::Results waitForProcessesThreadImpl(HANDLE job, UILocker::Session* ls,
- std::atomic<bool>& interrupt)
-{
+ProcessRunner::Results
+waitForProcessesThreadImpl(HANDLE job, UILocker::Session *ls,
+ std::atomic<bool> &interrupt) {
using namespace std::chrono;
DWORD currentPID = 0;
@@ -395,9 +383,9 @@ ProcessRunner::Results waitForProcessesThreadImpl(HANDLE job, UILocker::Session* return ProcessRunner::ForceUnlocked;
}
-void waitForProcessesThread(ProcessRunner::Results& result, HANDLE job,
- UILocker::Session* ls, std::atomic<bool>& interrupt)
-{
+void waitForProcessesThread(ProcessRunner::Results &result, HANDLE job,
+ UILocker::Session *ls,
+ std::atomic<bool> &interrupt) {
result = waitForProcessesThreadImpl(job, ls, interrupt);
// the session can be null when running shortcuts with locking disabled
@@ -406,9 +394,9 @@ void waitForProcessesThread(ProcessRunner::Results& result, HANDLE job, }
}
-ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProcesses,
- UILocker::Session* ls)
-{
+ProcessRunner::Results
+waitForProcesses(const std::vector<HANDLE> &initialProcesses,
+ UILocker::Session *ls) {
if (initialProcesses.empty()) {
// nothing to wait for
return ProcessRunner::Completed;
@@ -428,7 +416,7 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces bool oneWorked = false;
- for (auto&& h : initialProcesses) {
+ for (auto &&h : initialProcesses) {
if (::AssignProcessToJobObject(job.get(), h)) {
oneWorked = true;
} else {
@@ -457,13 +445,11 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces auto results = ProcessRunner::Running;
std::atomic<bool> interrupt(false);
- auto* t = QThread::create(waitForProcessesThread, std::ref(results), monitor, ls,
- std::ref(interrupt));
+ auto *t = QThread::create(waitForProcessesThread, std::ref(results), monitor,
+ ls, std::ref(interrupt));
QEventLoop events;
- QObject::connect(t, &QThread::finished, [&] {
- events.quit();
- });
+ QObject::connect(t, &QThread::finished, [&] { events.quit(); });
t->start();
events.exec();
@@ -479,8 +465,7 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces }
ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
- UILocker::Session* ls)
-{
+ UILocker::Session *ls) {
std::vector<HANDLE> processes = {initialProcess};
const auto r = waitForProcesses(processes, ls);
@@ -489,7 +474,8 @@ ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode, if (exitCode && r != ProcessRunner::Running) {
if (!::GetExitCodeProcess(initialProcess, exitCode)) {
const auto e = ::GetLastError();
- log::warn("failed to get exit code of process, {}", formatSystemMessage(e));
+ log::warn("failed to get exit code of process, {}",
+ formatSystemMessage(e));
}
}
@@ -498,13 +484,11 @@ ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode, #else // !_WIN32
-pid_t handleToPid(HANDLE h)
-{
+pid_t handleToPid(HANDLE h) {
return static_cast<pid_t>(reinterpret_cast<intptr_t>(h));
}
-QString readProcComm(pid_t pid)
-{
+QString readProcComm(pid_t pid) {
QFile f(QString("/proc/%1/comm").arg(pid));
if (!f.open(QIODevice::ReadOnly)) {
return {};
@@ -513,21 +497,20 @@ QString readProcComm(pid_t pid) return QString::fromUtf8(f.readAll()).trimmed();
}
-std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap()
-{
+std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap() {
std::unordered_map<pid_t, std::vector<pid_t>> children;
- DIR* proc = opendir("/proc");
+ DIR *proc = opendir("/proc");
if (!proc) {
return children;
}
- struct dirent* entry = nullptr;
+ struct dirent *entry = nullptr;
while ((entry = readdir(proc)) != nullptr) {
if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) {
continue;
}
- const char* name = entry->d_name;
+ const char *name = entry->d_name;
if (*name == '\0' || !std::isdigit(static_cast<unsigned char>(*name))) {
continue;
}
@@ -556,9 +539,8 @@ std::unordered_map<pid_t, std::vector<pid_t>> buildProcChildrenMap() return children;
}
-std::unordered_set<pid_t>
-collectDescendants(pid_t root, const std::unordered_map<pid_t, std::vector<pid_t>>& children)
-{
+std::unordered_set<pid_t> collectDescendants(
+ pid_t root, const std::unordered_map<pid_t, std::vector<pid_t>> &children) {
std::unordered_set<pid_t> out;
std::deque<pid_t> q;
q.push_back(root);
@@ -582,8 +564,8 @@ collectDescendants(pid_t root, const std::unordered_map<pid_t, std::vector<pid_t return out;
}
-QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arguments)
-{
+QStringList buildExpectedExecutables(const QFileInfo &binary,
+ const QString &arguments) {
QStringList expected;
auto addName = [&](QString name) {
name = name.trimmed().toLower();
@@ -595,7 +577,7 @@ QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arg addName(binary.fileName());
const auto args = QProcess::splitCommand(arguments);
- for (const QString& arg : args) {
+ for (const QString &arg : args) {
const QFileInfo fi(arg);
const QString base = fi.fileName();
if (base.endsWith(".exe", Qt::CaseInsensitive)) {
@@ -603,17 +585,18 @@ QStringList buildExpectedExecutables(const QFileInfo& binary, const QString& arg }
}
+ log::debug("buildExpectedExecutables: returning [{}]",
+ expected.join(", ").toStdString());
return expected;
}
-pid_t findTrackedProcess(pid_t rootPid, const QStringList& expected,
- QString* trackedNameOut)
-{
+pid_t findTrackedProcess(pid_t rootPid, const QStringList &expected,
+ QString *trackedNameOut) {
if (expected.isEmpty()) {
return 0;
}
- const auto children = buildProcChildrenMap();
+ const auto children = buildProcChildrenMap();
const auto descendants = collectDescendants(rootPid, children);
if (descendants.empty()) {
return 0;
@@ -636,12 +619,16 @@ pid_t findTrackedProcess(pid_t rootPid, const QStringList& expected, if (best > 0 && trackedNameOut) {
*trackedNameOut = bestName;
+ log::debug("findTrackedProcess: found best={}, name={}", best,
+ bestName.toStdString());
+ } else {
+ log::debug("findTrackedProcess: no tracked process found for rootPid={}",
+ rootPid);
}
return best;
}
-DWORD exitCodeFromWaitStatus(int status)
-{
+DWORD exitCodeFromWaitStatus(int status) {
if (WIFEXITED(status)) {
return static_cast<DWORD>(WEXITSTATUS(status));
}
@@ -653,9 +640,9 @@ DWORD exitCodeFromWaitStatus(int status) return 0;
}
-ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session* ls,
- const QStringList& expected)
-{
+ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode,
+ UILocker::Session *ls,
+ const QStringList &expected) {
if (pid <= 0) {
return ProcessRunner::Error;
}
@@ -665,10 +652,9 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session // which works for any process owned by the same user.
bool useKillPoll = false;
{
- int status = 0;
+ int status = 0;
const pid_t probe = ::waitpid(pid, &status, WNOHANG);
if (probe == pid) {
- // Already exited before we even started waiting
if (exitCode != nullptr) {
*exitCode = exitCodeFromWaitStatus(status);
}
@@ -686,13 +672,13 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session while (true) {
QString trackedName;
- pid_t displayPid = pid;
- QString displayName = readProcComm(pid);
- const pid_t tracked = findTrackedProcess(pid, expected, &trackedName);
+ pid_t displayPid = pid;
+ QString displayName = readProcComm(pid);
+ const pid_t tracked = findTrackedProcess(pid, expected, &trackedName);
if (tracked > 0) {
seenTrackedProcess = true;
- displayPid = tracked;
- displayName = trackedName;
+ displayPid = tracked;
+ displayName = trackedName;
} else if (seenTrackedProcess) {
if (exitCode != nullptr) {
*exitCode = 0;
@@ -702,7 +688,8 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session }
if (ls != nullptr) {
- ls->setInfo(static_cast<DWORD>(std::max<pid_t>(0, displayPid)), displayName);
+ ls->setInfo(static_cast<DWORD>(std::max<pid_t>(0, displayPid)),
+ displayName);
}
if (useKillPoll) {
@@ -716,13 +703,13 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session return ProcessRunner::Completed;
}
// EPERM means the process exists but we can't signal it; keep waiting
- if (errno != EPERM) {
+ else if (errno != EPERM) {
log::error("failed checking process {}, errno={}", pid, errno);
return ProcessRunner::Error;
}
}
} else {
- int status = 0;
+ int status = 0;
const pid_t waitResult = ::waitpid(pid, &status, WNOHANG);
if (waitResult == pid) {
@@ -776,23 +763,21 @@ ProcessRunner::Results waitForPid(pid_t pid, LPDWORD exitCode, UILocker::Session }
ProcessRunner::Results waitForProcess(HANDLE initialProcess, LPDWORD exitCode,
- UILocker::Session* ls,
- const QStringList& expected)
-{
+ UILocker::Session *ls,
+ const QStringList &expected) {
return waitForPid(handleToPid(initialProcess), exitCode, ls, expected);
}
-ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProcesses,
- UILocker::Session* ls,
- const QStringList& expected)
-{
+ProcessRunner::Results
+waitForProcesses(const std::vector<HANDLE> &initialProcesses,
+ UILocker::Session *ls, const QStringList &expected) {
if (initialProcesses.empty()) {
return ProcessRunner::Completed;
}
for (HANDLE h : initialProcesses) {
DWORD ignored = 0;
- const auto r = waitForPid(handleToPid(h), &ignored, ls, expected);
+ const auto r = waitForPid(handleToPid(h), &ignored, ls, expected);
if (r != ProcessRunner::Completed) {
return r;
}
@@ -803,16 +788,14 @@ ProcessRunner::Results waitForProcesses(const std::vector<HANDLE>& initialProces #endif // _WIN32
-ProcessRunner::ProcessRunner(OrganizerCore& core, IUserInterface* ui)
- : m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason), m_waitFlags(NoFlags),
- m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1)
-{
+ProcessRunner::ProcessRunner(OrganizerCore &core, IUserInterface *ui)
+ : m_core(core), m_ui(ui), m_lockReason(UILocker::NoReason),
+ m_waitFlags(NoFlags), m_handle(INVALID_HANDLE_VALUE), m_exitCode(-1) {
// all processes started in ProcessRunner are hooked by default
setHooked(true);
}
-ProcessRunner& ProcessRunner::setBinary(const QFileInfo& binary)
-{
+ProcessRunner &ProcessRunner::setBinary(const QFileInfo &binary) {
#ifndef _WIN32
m_sp.binary = QFileInfo(MOBase::normalizePathForHost(binary.filePath()));
#else
@@ -821,14 +804,12 @@ ProcessRunner& ProcessRunner::setBinary(const QFileInfo& binary) return *this;
}
-ProcessRunner& ProcessRunner::setArguments(const QString& arguments)
-{
+ProcessRunner &ProcessRunner::setArguments(const QString &arguments) {
m_sp.arguments = arguments;
return *this;
}
-ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory)
-{
+ProcessRunner &ProcessRunner::setCurrentDirectory(const QDir &directory) {
#ifndef _WIN32
m_sp.currentDirectory.setPath(MOBase::normalizePathForHost(directory.path()));
#else
@@ -837,37 +818,35 @@ ProcessRunner& ProcessRunner::setCurrentDirectory(const QDir& directory) return *this;
}
-ProcessRunner& ProcessRunner::setSteamID(const QString& steamID)
-{
+ProcessRunner &ProcessRunner::setSteamID(const QString &steamID) {
m_sp.steamAppID = steamID;
return *this;
}
-ProcessRunner& ProcessRunner::setCustomOverwrite(const QString& customOverwrite)
-{
+ProcessRunner &
+ProcessRunner::setCustomOverwrite(const QString &customOverwrite) {
m_customOverwrite = customOverwrite;
return *this;
}
-ProcessRunner& ProcessRunner::setForcedLibraries(const ForcedLibraries& forcedLibraries)
-{
+ProcessRunner &
+ProcessRunner::setForcedLibraries(const ForcedLibraries &forcedLibraries) {
m_forcedLibraries = forcedLibraries;
return *this;
}
-ProcessRunner& ProcessRunner::setProfileName(const QString& profileName)
-{
+ProcessRunner &ProcessRunner::setProfileName(const QString &profileName) {
m_profileName = profileName;
return *this;
}
-ProcessRunner& ProcessRunner::setWaitForCompletion(WaitFlags flags,
- UILocker::Reasons reason)
-{
- m_waitFlags = flags;
+ProcessRunner &ProcessRunner::setWaitForCompletion(WaitFlags flags,
+ UILocker::Reasons reason) {
+ m_waitFlags = flags;
m_lockReason = reason;
- if (m_waitFlags.testFlag(WaitForRefresh) && !m_waitFlags.testFlag(TriggerRefresh)) {
+ if (m_waitFlags.testFlag(WaitForRefresh) &&
+ !m_waitFlags.testFlag(TriggerRefresh)) {
log::warn("process runner: WaitForRefresh without TriggerRefresh "
"makes no sense, will be ignored");
}
@@ -875,14 +854,13 @@ ProcessRunner& ProcessRunner::setWaitForCompletion(WaitFlags flags, return *this;
}
-ProcessRunner& ProcessRunner::setHooked(bool b)
-{
+ProcessRunner &ProcessRunner::setHooked(bool b) {
m_sp.hooked = b;
return *this;
}
-ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targetInfo)
-{
+ProcessRunner &ProcessRunner::setFromFile(QWidget *parent,
+ const QFileInfo &targetInfo) {
if (!parent && m_ui) {
parent = m_ui->mainWindow();
}
@@ -900,7 +878,7 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ break;
}
- case spawn::FileExecutionTypes::Other: // fall-through
+ case spawn::FileExecutionTypes::Other: // fall-through
default: {
m_shellOpen = targetInfo;
setHooked(false);
@@ -911,8 +889,7 @@ ProcessRunner& ProcessRunner::setFromFile(QWidget* parent, const QFileInfo& targ return *this;
}
-ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe)
-{
+ProcessRunner &ProcessRunner::setFromExecutable(const Executable &exe) {
const auto profile = m_core.currentProfile();
if (!profile) {
throw MyException(QObject::tr("No profile set"));
@@ -941,33 +918,33 @@ ProcessRunner& ProcessRunner::setFromExecutable(const Executable& exe) return *this;
}
-ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut)
-{
+ProcessRunner &ProcessRunner::setFromShortcut(const MOShortcut &shortcut) {
const auto currentInstance = InstanceManager::singleton().currentInstance();
if (currentInstance) {
if (shortcut.hasInstance() && !shortcut.isForInstance(*currentInstance)) {
- MOBase::reportError(
- QObject::tr(
- "This shortcut is for instance '%1' but Mod Organizer is currently "
- "running for '%2'. Exit Mod Organizer before running the shortcut or "
- "change the active instance.")
- .arg(shortcut.instanceDisplayName())
- .arg(currentInstance->displayName()));
+ MOBase::reportError(QObject::tr("This shortcut is for instance '%1' but "
+ "Mod Organizer is currently "
+ "running for '%2'. Exit Mod Organizer "
+ "before running the shortcut or "
+ "change the active instance.")
+ .arg(shortcut.instanceDisplayName())
+ .arg(currentInstance->displayName()));
throw std::exception();
}
}
- const auto* exes = m_core.executablesList();
- const auto exe = exes->find(shortcut.executableName());
+ const auto *exes = m_core.executablesList();
+ const auto exe = exes->find(shortcut.executableName());
if (exe != exes->end()) {
setFromExecutable(*exe);
} else {
- MOBase::reportError(QObject::tr("Executable '%1' does not exist in instance '%2'.")
- .arg(shortcut.executableName())
- .arg(currentInstance->displayName()));
+ MOBase::reportError(
+ QObject::tr("Executable '%1' does not exist in instance '%2'.")
+ .arg(shortcut.executableName())
+ .arg(currentInstance->displayName()));
throw std::exception();
}
@@ -975,11 +952,10 @@ ProcessRunner& ProcessRunner::setFromShortcut(const MOShortcut& shortcut) return *this;
}
-ProcessRunner& ProcessRunner::setFromFileOrExecutable(
- const QString& executable, const QStringList& args, const QString& cwd,
- const QString& profileOverride, const QString& forcedCustomOverwrite,
- bool ignoreCustomOverwrite)
-{
+ProcessRunner &ProcessRunner::setFromFileOrExecutable(
+ const QString &executable, const QStringList &args, const QString &cwd,
+ const QString &profileOverride, const QString &forcedCustomOverwrite,
+ bool ignoreCustomOverwrite) {
const auto profile = m_core.currentProfile();
if (!profile) {
throw MyException(QObject::tr("No profile set"));
@@ -1004,24 +980,27 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( }
try {
- const Executable& exe = m_core.executablesList()->getByBinary(m_sp.binary);
+ const Executable &exe =
+ m_core.executablesList()->getByBinary(m_sp.binary);
setSteamID(exe.steamAppID());
- setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString());
+ setCustomOverwrite(
+ profile->setting("custom_overwrites", exe.title()).toString());
if (profile->forcedLibrariesEnabled(exe.title())) {
setForcedLibraries(profile->determineForcedLibraries(exe.title()));
}
- } catch (const std::runtime_error&) {
+ } catch (const std::runtime_error &) {
// nop
}
} else {
// only a file name, search executables list
try {
- const Executable& exe = m_core.executablesList()->get(executable);
+ const Executable &exe = m_core.executablesList()->get(executable);
setSteamID(exe.steamAppID());
- setCustomOverwrite(profile->setting("custom_overwrites", exe.title()).toString());
+ setCustomOverwrite(
+ profile->setting("custom_overwrites", exe.title()).toString());
if (profile->forcedLibrariesEnabled(exe.title())) {
setForcedLibraries(profile->determineForcedLibraries(exe.title()));
@@ -1036,7 +1015,7 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( if (cwd == "") {
setCurrentDirectory(exe.workingDirectory());
}
- } catch (const std::runtime_error&) {
+ } catch (const std::runtime_error &) {
log::warn("\"{}\" not set up as executable", executable);
}
}
@@ -1050,13 +1029,11 @@ ProcessRunner& ProcessRunner::setFromFileOrExecutable( return *this;
}
-bool ProcessRunner::shouldRunShell() const
-{
+bool ProcessRunner::shouldRunShell() const {
return !m_shellOpen.filePath().isEmpty();
}
-ProcessRunner::Results ProcessRunner::run()
-{
+ProcessRunner::Results ProcessRunner::run() {
// check if setHooked() was called after setFromFile(); this needs to
// modify the settings to run the associated executable instead of using
// shell::Open()
@@ -1097,9 +1074,9 @@ ProcessRunner::Results ProcessRunner::run() return postRun();
}
-std::optional<ProcessRunner::Results> ProcessRunner::runShell()
-{
- const auto file = MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath());
+std::optional<ProcessRunner::Results> ProcessRunner::runShell() {
+ const auto file =
+ MOBase::normalizePathForHost(m_shellOpen.absoluteFilePath());
log::debug("executing from shell: '{}'", file);
@@ -1122,8 +1099,7 @@ std::optional<ProcessRunner::Results> ProcessRunner::runShell() return {};
}
-std::optional<ProcessRunner::Results> ProcessRunner::runBinary()
-{
+std::optional<ProcessRunner::Results> ProcessRunner::runBinary() {
if (m_profileName.isEmpty()) {
// get the current profile name if it wasn't overridden
const auto profile = m_core.currentProfile();
@@ -1143,10 +1119,10 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary() }
// parent widget used for any dialog popped up while checking for things
- QWidget* parent = (m_ui ? m_ui->mainWindow() : nullptr);
+ QWidget *parent = (m_ui ? m_ui->mainWindow() : nullptr);
- const auto* game = m_core.managedGame();
- auto& settings = m_core.settings();
+ const auto *game = m_core.managedGame();
+ auto &settings = m_core.settings();
if (m_sp.steamAppID.trimmed().isEmpty()) {
const QString gameSteamId = game->steamAPPId().trimmed();
@@ -1158,7 +1134,8 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary() }
// start steam if needed
- if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID, settings)) {
+ if (!checkSteam(parent, m_sp, game->gameDirectory(), m_sp.steamAppID,
+ settings)) {
return Error;
}
@@ -1175,7 +1152,8 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary() #ifdef _WIN32
m_handle.reset(startBinary(parent, m_sp));
#else
- m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(startBinary(parent, m_sp))));
+ m_handle.reset(reinterpret_cast<HANDLE>(
+ static_cast<intptr_t>(startBinary(parent, m_sp))));
#endif
if (m_handle.get() == INVALID_HANDLE_VALUE) {
return Error;
@@ -1184,8 +1162,7 @@ std::optional<ProcessRunner::Results> ProcessRunner::runBinary() return {};
}
-bool ProcessRunner::shouldRefresh(Results r) const
-{
+bool ProcessRunner::shouldRefresh(Results r) const {
// afterRun() is only called with the Refresh flag; it refreshes the
// directory structure and notifies plugins
//
@@ -1211,11 +1188,12 @@ bool ProcessRunner::shouldRefresh(Results r) const case ForceUnlocked: {
// The process may still be running when the user force-unlocks.
// Refreshing in that state can race with file updates.
- log::debug("process runner: not refreshing because the ui was force unlocked");
+ log::debug(
+ "process runner: not refreshing because the ui was force unlocked");
return false;
}
- case Error: // fall-through
+ case Error: // fall-through
case Cancelled:
case Running:
default: {
@@ -1224,8 +1202,7 @@ bool ProcessRunner::shouldRefresh(Results r) const }
}
-ProcessRunner::Results ProcessRunner::postRun()
-{
+ProcessRunner::Results ProcessRunner::postRun() {
const bool mustWait = (m_waitFlags & ForceWait);
if (!m_sp.hooked && !mustWait) {
@@ -1248,8 +1225,9 @@ ProcessRunner::Results ProcessRunner::postRun() if (mustWait) {
if (!lockEnabled) {
// at least tell the user what's going on
- log::debug("locking is disabled, but the output of the application is required; "
- "overriding this setting and locking the ui");
+ log::debug(
+ "locking is disabled, but the output of the application is required; "
+ "overriding this setting and locking the ui");
}
} else {
// no force wait
@@ -1257,10 +1235,12 @@ ProcessRunner::Results ProcessRunner::postRun() if (m_lockReason == UILocker::NoReason) {
// no locking requested
#ifndef _WIN32
- // Main window launches typically use TriggerRefresh without waiting/locking.
- // In that mode we still need post-run refresh/sync once the process exits.
+ // Main window launches typically use TriggerRefresh without
+ // waiting/locking. In that mode we still need post-run refresh/sync once
+ // the process exits.
if (m_waitFlags.testFlag(TriggerRefresh)) {
- const pid_t pid = static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
+ const pid_t pid =
+ static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
const QFileInfo binary = m_sp.binary;
QPointer<OrganizerCore> core = &m_core;
@@ -1283,11 +1263,11 @@ ProcessRunner::Results ProcessRunner::postRun() } else if (errno == ECHILD) {
// Detached process — poll with kill(0) until gone.
while (::kill(pid, 0) == 0 || errno == EPERM) {
- usleep(200000); // 200ms
+ usleep(200000); // 200ms
}
} else {
- MOBase::log::warn("process runner: waitpid failed for pid {}: {}", pid,
- errno);
+ MOBase::log::warn("process runner: waitpid failed for pid {}: {}",
+ pid, errno);
}
if (!core) {
@@ -1295,7 +1275,8 @@ ProcessRunner::Results ProcessRunner::postRun() }
QMetaObject::invokeMethod(
- core, [core, binary, exitCode]() {
+ core,
+ [core, binary, exitCode]() {
if (core) {
core->afterRun(binary, exitCode);
}
@@ -1303,7 +1284,8 @@ ProcessRunner::Results ProcessRunner::postRun() Qt::QueuedConnection);
}).detach();
- log::debug("process runner: scheduled async post-run refresh for pid {}", pid);
+ log::debug(
+ "process runner: scheduled async post-run refresh for pid {}", pid);
}
#endif
return Running;
@@ -1331,14 +1313,14 @@ ProcessRunner::Results ProcessRunner::postRun() //
// MO will be running in the background with no visual feedback, but that's
// how it is
- r = waitForProcess(m_handle.get(), &m_exitCode, nullptr, expectedExecutables);
+ r = waitForProcess(m_handle.get(), &m_exitCode, nullptr,
+ expectedExecutables);
} else {
- withLock([&](auto& ls) {
+ withLock([&](auto &ls) {
r = waitForProcess(m_handle.get(), &m_exitCode, &ls, expectedExecutables);
});
}
-
if (shouldRefresh(r)) {
QEventLoop loop;
const bool wait = m_waitFlags.testFlag(WaitForRefresh);
@@ -1361,38 +1343,28 @@ ProcessRunner::Results ProcessRunner::postRun() }
#ifdef _WIN32
-ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h)
-{
+ProcessRunner::Results ProcessRunner::attachToProcess(HANDLE h) {
m_handle.reset(h);
return postRun();
}
#else
-ProcessRunner::Results ProcessRunner::attachToProcess(pid_t pid)
-{
+ProcessRunner::Results ProcessRunner::attachToProcess(pid_t pid) {
m_handle.reset(reinterpret_cast<HANDLE>(static_cast<intptr_t>(pid)));
return postRun();
}
#endif
-DWORD ProcessRunner::exitCode() const
-{
- return m_exitCode;
-}
+DWORD ProcessRunner::exitCode() const { return m_exitCode; }
#ifdef _WIN32
-HANDLE ProcessRunner::getProcessHandle() const
-{
- return m_handle.get();
-}
+HANDLE ProcessRunner::getProcessHandle() const { return m_handle.get(); }
#else
-pid_t ProcessRunner::getProcessHandle() const
-{
+pid_t ProcessRunner::getProcessHandle() const {
return static_cast<pid_t>(reinterpret_cast<intptr_t>(m_handle.get()));
}
#endif
-env::HandlePtr ProcessRunner::stealProcessHandle()
-{
+env::HandlePtr ProcessRunner::stealProcessHandle() {
auto h = m_handle.release();
m_handle.reset(INVALID_HANDLE_VALUE);
return env::HandlePtr(h);
@@ -1400,8 +1372,7 @@ env::HandlePtr ProcessRunner::stealProcessHandle() #ifdef _WIN32
ProcessRunner::Results
-ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason)
-{
+ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason) {
m_lockReason = reason;
if (!m_core.settings().interface().lockGUI()) {
@@ -1412,7 +1383,7 @@ ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason) auto r = Error;
for (;;) {
- withLock([&](auto& ls) {
+ withLock([&](auto &ls) {
const auto processes = getRunningUSVFSProcesses();
if (processes.empty()) {
r = Completed;
@@ -1439,8 +1410,7 @@ ProcessRunner::waitForAllUSVFSProcessesWithLock(UILocker::Reasons reason) }
#endif
-void ProcessRunner::withLock(std::function<void(UILocker::Session&)> f)
-{
+void ProcessRunner::withLock(std::function<void(UILocker::Session &)> f) {
auto ls = UILocker::instance().lock(m_lockReason);
f(*ls);
}
diff --git a/src/src/protonlauncher.cpp b/src/src/protonlauncher.cpp index f2bd7a0..599ef62 100644 --- a/src/src/protonlauncher.cpp +++ b/src/src/protonlauncher.cpp @@ -3,14 +3,98 @@ #include <nak_ffi.h> #include <QCoreApplication> #include <QDir> +#include <QFile> #include <QFileInfo> #include <QProcess> #include <QProcessEnvironment> #include <QStandardPaths> #include <log.h> + namespace { +// Restore the pre-AppImage environment for child processes (umu-run, Proton, +// pressure-vessel). The AppRun script saves FLUORINE_ORIG_* vars before +// modifying PATH, LD_LIBRARY_PATH, etc. We restore from those saved values +// so game processes get a clean host environment without AppImage library paths. +void cleanAppImageEnv(QProcessEnvironment& env) +{ + // Remove Fluorine/AppImage-specific vars that should never leak to game processes. + env.remove("QT_QPA_PLATFORM_PLUGIN_PATH"); + env.remove("MO2_PLUGINS_DIR"); + env.remove("MO2_DLLS_DIR"); + env.remove("MO2_PYTHON_DIR"); + env.remove("MO2_BASE_DIR"); + + // AppImage runtime injects these — they can confuse pressure-vessel/umu-run. + env.remove("APPIMAGE"); + env.remove("APPDIR"); + env.remove("OWD"); + env.remove("ARGV0"); + env.remove("APPIMAGE_ORIGINAL_EXEC"); + + env.remove("DESKTOPINTEGRATION"); + + // Restore saved pre-AppImage values. AppRun sets FLUORINE_ORIG_* before + // modifying PATH, LD_LIBRARY_PATH, etc. If those vars exist, use them to + // restore the original host environment. If not (standalone/non-AppImage), + // fall back to stripping known AppImage patterns. + auto restoreOrStrip = [](const QString& var, const QString& origVar, + QProcessEnvironment& e) { + if (e.contains(origVar)) { + const QString orig = e.value(origVar); + if (orig.isEmpty()) { + e.remove(var); + } else { + e.insert(var, orig); + } + e.remove(origVar); + } else { + // Fallback: strip AppImage mount paths by pattern. + const QString value = e.value(var); + if (value.isEmpty()) return; + QStringList kept; + for (const QString& p : value.split(':')) { + if (p.contains(".mount_Fluori") || p.contains("/fluorine/python")) { + continue; + } + kept.append(p); + } + if (kept.isEmpty()) { + e.remove(var); + } else { + e.insert(var, kept.join(':')); + } + } + }; + + const bool hasOrigVars = env.contains("FLUORINE_ORIG_PATH"); + restoreOrStrip("LD_LIBRARY_PATH", "FLUORINE_ORIG_LD_LIBRARY_PATH", env); + restoreOrStrip("PATH", "FLUORINE_ORIG_PATH", env); + restoreOrStrip("XDG_DATA_DIRS", "FLUORINE_ORIG_XDG_DATA_DIRS", env); + restoreOrStrip("QT_PLUGIN_PATH", "FLUORINE_ORIG_QT_PLUGIN_PATH", env); + + MOBase::log::debug("cleanAppImageEnv: {} (LD_LIBRARY_PATH='{}')", + hasOrigVars ? "restored from FLUORINE_ORIG_*" : "pattern-strip fallback", + env.value("LD_LIBRARY_PATH", "<unset>")); +} + +// GE-Proton crashes in update_builtin_libs() if tracked_files doesn't exist +// (e.g. prefix was created by plain Wine, not Proton). Create an empty one. +void ensureTrackedFilesExist(const QString& compatDataPath) +{ + if (compatDataPath.isEmpty()) return; + const QString tracked = QDir(compatDataPath).filePath("tracked_files"); + if (!QFileInfo::exists(tracked)) { + QDir().mkpath(compatDataPath); + QFile f(tracked); + if (f.open(QIODevice::WriteOnly)) { + f.close(); + MOBase::log::debug("created empty tracked_files at '{}'", tracked); + } + } +} + QString compatDataPathFromPrefix(const QString& prefixPath) { if (prefixPath.isEmpty()) { @@ -226,6 +310,12 @@ ProtonLauncher& ProtonLauncher::setSteamDrm(bool useSteamDrm) return *this; } +ProtonLauncher& ProtonLauncher::setStoreVariant(const QString& variant) +{ + m_storeVariant = variant.trimmed(); + return *this; +} + ProtonLauncher& ProtonLauncher::addEnvVar(const QString& key, const QString& value) { if (!key.isEmpty()) { @@ -277,6 +367,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); + cleanAppImageEnv(env); if (!m_prefixPath.isEmpty()) { env.insert("WINEPREFIX", m_prefixPath); @@ -284,6 +375,7 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const const QString compatDataPath = compatDataPathFromPrefix(m_prefixPath); if (!compatDataPath.isEmpty()) { + ensureTrackedFilesExist(compatDataPath); env.insert("STEAM_COMPAT_DATA_PATH", compatDataPath); } @@ -320,6 +412,10 @@ bool ProtonLauncher::launchWithProton(qint64& pid) const MOBase::log::info("Proton launch: '{}' run '{}'", protonScript, m_binary); + if (!m_workingDir.isEmpty()) { + env.insert("PWD", m_workingDir); + } + return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); } @@ -335,9 +431,11 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const ensureSteamRunning(); } - // Resolve umu-run according to user preference (bundled vs system). + // Resolve umu-run: prefer the copy deployed to the Fluorine data dir + // (~/.local/share/fluorine/umu-run), then system PATH, then AppImage bundled. const QString appDir = QCoreApplication::applicationDirPath(); const QString bundled = appDir + QStringLiteral("/umu-run"); + const QString dataDir = QDir::home().filePath(".local/share/fluorine/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 @@ -356,25 +454,20 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } } + // Priority: data dir > system (if preferred) > bundled > system (fallback) 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; - } + if (QFileInfo::exists(dataDir)) { + umuRun = dataDir; + } else if (m_preferSystemUmu && !system.isEmpty()) { + umuRun = system; + } 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: dataDir='{}' (exists={}), bundled='{}' (exists={}), system='{}', selected='{}'", + dataDir, QFileInfo::exists(dataDir), bundled, QFileInfo::exists(bundled), system, umuRun); if (umuRun.isEmpty()) { MOBase::log::warn("umu-run not found (bundled or in PATH)"); @@ -390,6 +483,7 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); + cleanAppImageEnv(env); if (!m_prefixPath.isEmpty()) { env.insert("WINEPREFIX", m_prefixPath); @@ -400,12 +494,8 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } // umu-run sets STEAM_COMPAT_DATA_PATH internally from WINEPREFIX, so we - // do NOT set it here. However, the game's Steamworks DRM still needs - // STEAM_COMPAT_CLIENT_INSTALL_PATH to locate the Steam client libraries. - const QString steamPath = detectSteamPath(); - if (!steamPath.isEmpty()) { - env.insert("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamPath); - } + // do NOT set it here. However, ensure tracked_files exists for Proton. + ensureTrackedFilesExist(compatDataPathFromPrefix(m_prefixPath)); uint32_t effectiveSteamAppId = m_steamAppId; if (effectiveSteamAppId == 0) { @@ -418,17 +508,57 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } } - if (m_useSteamDrm && effectiveSteamAppId != 0) { - // umu-run expects GAMEID in "umu-<AppID>" format to extract SteamAppId. + // Always pass the game's Steam App ID as GAMEID for protonfixes lookup. + // GAMEID identifies the game (for compatibility fixes), separate from DRM. + if (effectiveSteamAppId != 0) { 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")); } + if (m_useSteamDrm) { + // Steam DRM games need the Steam client path and Steam identity env vars. + env.insert("STORE", QStringLiteral("steam")); + const QString steamPath = detectSteamPath(); + if (!steamPath.isEmpty()) { + env.insert("STEAM_COMPAT_CLIENT_INSTALL_PATH", steamPath); + } + if (effectiveSteamAppId != 0) { + env.insert("SteamAppId", QString::number(effectiveSteamAppId)); + env.insert("SteamGameId", QString::number(effectiveSteamAppId)); + } + } else { + // Non-Steam games: do NOT set SteamAppId/SteamGameId — those can trigger + // Steamworks initialization in Wine which crashes GOG/Epic games. + env.remove("SteamAPPId"); + env.remove("SteamAppId"); + env.remove("SteamGameId"); + + // Set STORE so umu-run knows this is a non-Steam game. Without STORE, + // umu-run may attempt Steam-specific behavior that breaks GOG/Epic titles. + // umu-run expects lowercase values (gog, egs, etc.). + if (!m_storeVariant.isEmpty()) { + env.insert("STORE", m_storeVariant.toLower()); + } else { + env.insert("STORE", QStringLiteral("gog")); + } + } + + // Remove FUSE VFS file descriptors — these are Fluorine-internal and must + // not leak into game processes (especially pressure-vessel containers). + env.remove("_FUSE_COMMFD"); + env.remove("_FUSE_COMMFD2"); + + // Remove Qt/KDE theme vars that can confuse pressure-vessel/Wine. + env.remove("QML_DISABLE_DISK_CACHE"); + env.remove("QT_ICON_THEME_NAME"); + env.remove("QT_STYLE_OVERRIDE"); + env.remove("OLDPWD"); + + if (!m_workingDir.isEmpty()) { + env.insert("PWD", m_workingDir); + } + for (auto it = m_wrapperEnvVars.cbegin(); it != m_wrapperEnvVars.cend(); ++it) { env.insert(it.key(), it.value()); } @@ -446,13 +576,11 @@ bool ProtonLauncher::launchWithUmu(qint64& pid) const } } - MOBase::log::info("UMU launch: '{}' '{}' (game id: {}, steam: '{}')", umuRun, - m_binary, - (effectiveSteamAppId == 0 - ? QStringLiteral("<unset>") - : QStringLiteral("umu-") + - QString::number(effectiveSteamAppId)), - steamPath); + MOBase::log::info("UMU launch: '{}' '{}' (GAMEID={}, STORE={}, steam_drm={})", + umuRun, m_binary, + env.value("GAMEID"), + env.value("STORE", "<unset>"), + m_useSteamDrm); return startDetachedWithEnv(program, arguments, m_workingDir, env, pid); } @@ -470,6 +598,7 @@ bool ProtonLauncher::launchDirect(qint64& pid) const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("PYTHONHOME"); + cleanAppImageEnv(env); for (auto it = m_wrapperEnvVars.cbegin(); it != m_wrapperEnvVars.cend(); ++it) { env.insert(it.key(), it.value()); } diff --git a/src/src/protonlauncher.h b/src/src/protonlauncher.h index 5480c69..7ef6e76 100644 --- a/src/src/protonlauncher.h +++ b/src/src/protonlauncher.h @@ -24,6 +24,7 @@ public: ProtonLauncher& setPreferSystemUmu(bool preferSystemUmu); ProtonLauncher& setUseSteamRun(bool useSteamRun); ProtonLauncher& setSteamDrm(bool useSteamDrm); + ProtonLauncher& setStoreVariant(const QString& variant); ProtonLauncher& addEnvVar(const QString& key, const QString& value); // Launch dispatch: UMU -> Proton -> Direct @@ -46,6 +47,7 @@ private: bool m_preferSystemUmu; bool m_useSteamRun; bool m_useSteamDrm; + QString m_storeVariant; // "GOG", "Epic", or empty for Steam QMap<QString, QString> m_envVars; QMap<QString, QString> m_wrapperEnvVars; }; diff --git a/src/src/savestab.cpp b/src/src/savestab.cpp index 2f921e5..29411fa 100644 --- a/src/src/savestab.cpp +++ b/src/src/savestab.cpp @@ -1,6 +1,6 @@ -#include "savestab.h"
-#include "activatemodsdialog.h"
-#include "organizercore.h"
+#include "savestab.h" +#include "activatemodsdialog.h" +#include "organizercore.h" #include "ui_mainwindow.h" #include <iplugingame.h> #include <isavegameinfowidget.h> @@ -8,12 +8,55 @@ #include <new> #include <report.h> +#include <QFile> +#include <QTextStream> + using namespace MOBase; namespace { bool g_disableSaveTooltipsAfterOOM = false; +// Read a value from a Bethesda-style INI file without QSettings. +// QSettings::IniFormat interprets backslashes as line continuations, +// which corrupts values like "sLocalSavePath=__MO_Saves\". +QString readIniValueDirect(const QString& iniFile, const QString& section, + const QString& key) +{ + QFile file(iniFile); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + + const QString sectionHeader = "[" + section + "]"; + QTextStream in(&file); + bool inSection = false; + + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + if (line.startsWith('[') && line.endsWith(']')) { + if (inSection) + break; + if (line.compare(sectionHeader, Qt::CaseInsensitive) == 0) + inSection = true; + continue; + } + + if (inSection && !line.isEmpty() && !line.startsWith(';') && + !line.startsWith('#')) { + int eqPos = line.indexOf('='); + if (eqPos > 0) { + QString existingKey = line.left(eqPos).trimmed(); + if (existingKey.compare(key, Qt::CaseInsensitive) == 0) { + return line.mid(eqPos + 1).trimmed(); + } + } + } + } + + return {}; +} + QString sanitizeText(QString in, int maxLen = 200) { for (int i = 0; i < in.size(); ++i) { @@ -51,48 +94,48 @@ bool isLikelyCorruptSaveText(QString const& in) } // namespace SavesTab::SavesTab(QWidget* window, OrganizerCore& core, Ui::MainWindow* mwui) - : m_window(window), m_core(core), m_CurrentSaveView(nullptr),
- ui{mwui->tabWidget, mwui->savesTab, mwui->savegameList}
-{
- m_SavesWatcherTimer.setSingleShot(true);
- m_SavesWatcherTimer.setInterval(500);
-
- ui.list->installEventFilter(this);
- ui.list->setMouseTracking(true);
-
- connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [&] {
- m_SavesWatcherTimer.start();
- });
-
- connect(&m_SavesWatcherTimer, &QTimer::timeout, [&] {
- refreshSavesIfOpen();
- });
-
- connect(ui.list, &QWidget::customContextMenuRequested, [&](auto pos) {
- onContextMenu(pos);
- });
-
- connect(ui.list, &QTreeWidget::itemEntered, [&](auto* item) {
- saveSelectionChanged(item);
- });
-}
-
-bool SavesTab::eventFilter(QObject* object, QEvent* e)
-{
- if (object == ui.list) {
- if (e->type() == QEvent::Leave || e->type() == QEvent::WindowDeactivate) {
- hideSaveGameInfo();
- } else if (e->type() == QEvent::KeyPress) {
- QKeyEvent* keyEvent = static_cast<QKeyEvent*>(e);
- if (keyEvent->key() == Qt::Key_Delete) {
- deleteSavegame();
- }
- }
- }
-
- return false;
-}
-
+ : m_window(window), m_core(core), m_CurrentSaveView(nullptr), + ui{mwui->tabWidget, mwui->savesTab, mwui->savegameList} +{ + m_SavesWatcherTimer.setSingleShot(true); + m_SavesWatcherTimer.setInterval(500); + + ui.list->installEventFilter(this); + ui.list->setMouseTracking(true); + + connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [&] { + m_SavesWatcherTimer.start(); + }); + + connect(&m_SavesWatcherTimer, &QTimer::timeout, [&] { + refreshSavesIfOpen(); + }); + + connect(ui.list, &QWidget::customContextMenuRequested, [&](auto pos) { + onContextMenu(pos); + }); + + connect(ui.list, &QTreeWidget::itemEntered, [&](auto* item) { + saveSelectionChanged(item); + }); +} + +bool SavesTab::eventFilter(QObject* object, QEvent* e) +{ + if (object == ui.list) { + if (e->type() == QEvent::Leave || e->type() == QEvent::WindowDeactivate) { + hideSaveGameInfo(); + } else if (e->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(e); + if (keyEvent->key() == Qt::Key_Delete) { + deleteSavegame(); + } + } + } + + return false; +} + void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem) { if (g_disableSaveTooltipsAfterOOM) { @@ -100,21 +143,21 @@ void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem) } // don't display the widget if the main window doesn't have focus - //
- // this goes against the standard behaviour for tooltips, which are displayed
- // on hover regardless of focus, but this widget is so large and busy that
- // it's probably better this way
- if (!m_window->isActiveWindow()) {
- return;
- }
-
+ // + // this goes against the standard behaviour for tooltips, which are displayed + // on hover regardless of focus, but this widget is so large and busy that + // it's probably better this way + if (!m_window->isActiveWindow()) { + return; + } + if (m_CurrentSaveView == nullptr) { auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>(); -
- if (info != nullptr) {
- m_CurrentSaveView = info->getSaveGameWidget(m_window);
- }
-
+ + if (info != nullptr) { + m_CurrentSaveView = info->getSaveGameWidget(m_window); + } + if (m_CurrentSaveView == nullptr) { return; } @@ -135,129 +178,135 @@ void SavesTab::displaySaveGameInfo(QTreeWidgetItem* newItem) hideSaveGameInfo(); return; } -
- QWindow* window = m_CurrentSaveView->window()->windowHandle();
- QRect screenRect;
- if (window == nullptr)
- screenRect = QGuiApplication::primaryScreen()->geometry();
- else
- screenRect = window->screen()->geometry();
-
- QPoint pos = QCursor::pos();
- if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) {
- pos.rx() -= (m_CurrentSaveView->width() + 2);
- } else {
- pos.rx() += 5;
- }
-
- if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) {
- pos.ry() -= (m_CurrentSaveView->height() + 10);
- } else {
- pos.ry() += 20;
- }
- m_CurrentSaveView->move(pos);
-
- m_CurrentSaveView->show();
- m_CurrentSaveView->setProperty("displayItem",
- QVariant::fromValue(static_cast<void*>(newItem)));
-}
-
-void SavesTab::saveSelectionChanged(QTreeWidgetItem* newItem)
-{
- if (newItem == nullptr) {
- hideSaveGameInfo();
- } else if (m_CurrentSaveView == nullptr ||
- newItem != m_CurrentSaveView->property("displayItem").value<void*>()) {
- displaySaveGameInfo(newItem);
- }
-}
-
-void SavesTab::hideSaveGameInfo()
-{
- if (m_CurrentSaveView != nullptr) {
- m_CurrentSaveView->deleteLater();
- m_CurrentSaveView = nullptr;
- }
-}
-
-void SavesTab::refreshSavesIfOpen()
-{
- if (ui.mainTabs->currentWidget() == ui.tab) {
- refreshSaveList();
- }
-}
-
-QDir SavesTab::currentSavesDir() const
-{
- // TODO: This code should probably be handled by the game plugins
- QDir savesDir;
- if (m_core.currentProfile()->localSavesEnabled()) {
- savesDir.setPath(m_core.currentProfile()->savePath());
- } else {
- auto iniFiles = m_core.managedGame()->iniFiles();
-
- if (iniFiles.isEmpty() ||
- m_core.gameFeatures().gameFeature<LocalSavegames>() == nullptr) {
- return m_core.managedGame()->savesDirectory();
- }
-
- QString iniPath = m_core.currentProfile()->absoluteIniFilePath(iniFiles[0]);
-
-#ifdef _WIN32
- wchar_t path[MAX_PATH];
- if (::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"", path, MAX_PATH,
- iniPath.toStdWString().c_str())) {
- savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(
- QString::fromWCharArray(path)));
- } else {
- savesDir = m_core.managedGame()->savesDirectory();
- }
-#else
- // On Linux, use QSettings to read the INI file
- QSettings ini(iniPath, QSettings::IniFormat);
- QString savePath = ini.value("General/SLocalSavePath").toString();
- if (!savePath.isEmpty()) {
- savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(savePath));
- } else {
- savesDir = m_core.managedGame()->savesDirectory();
- }
-#endif
- }
-
- return savesDir;
-}
-
-void SavesTab::startMonitorSaves()
-{
- stopMonitorSaves();
-
- QDir savesDir = currentSavesDir();
-
- m_SavesWatcher.addPath(savesDir.absolutePath());
-}
-
-void SavesTab::stopMonitorSaves()
-{
- if (m_SavesWatcher.directories().length() > 0) {
- m_SavesWatcher.removePaths(m_SavesWatcher.directories());
- }
-}
-
-void SavesTab::refreshSaveList()
-{
- TimeThis tt("MainWindow::refreshSaveList()");
-
- startMonitorSaves(); // re-starts monitoring
-
- try {
- QDir savesDir = currentSavesDir();
- MOBase::log::debug("reading save games from {}", savesDir.absolutePath());
- m_SaveGames = m_core.managedGame()->listSaves(savesDir);
- std::sort(m_SaveGames.begin(), m_SaveGames.end(),
- [](auto const& lhs, auto const& rhs) {
- return lhs->getCreationTime() > rhs->getCreationTime();
- });
-
+ + QWindow* window = m_CurrentSaveView->window()->windowHandle(); + QRect screenRect; + if (window == nullptr) + screenRect = QGuiApplication::primaryScreen()->geometry(); + else + screenRect = window->screen()->geometry(); + + QPoint pos = QCursor::pos(); + if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) { + pos.rx() -= (m_CurrentSaveView->width() + 2); + } else { + pos.rx() += 5; + } + + if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) { + pos.ry() -= (m_CurrentSaveView->height() + 10); + } else { + pos.ry() += 20; + } + m_CurrentSaveView->move(pos); + + m_CurrentSaveView->show(); + m_CurrentSaveView->setProperty("displayItem", + QVariant::fromValue(static_cast<void*>(newItem))); +} + +void SavesTab::saveSelectionChanged(QTreeWidgetItem* newItem) +{ + if (newItem == nullptr) { + hideSaveGameInfo(); + } else if (m_CurrentSaveView == nullptr || + newItem != m_CurrentSaveView->property("displayItem").value<void*>()) { + displaySaveGameInfo(newItem); + } +} + +void SavesTab::hideSaveGameInfo() +{ + if (m_CurrentSaveView != nullptr) { + m_CurrentSaveView->deleteLater(); + m_CurrentSaveView = nullptr; + } +} + +void SavesTab::refreshSavesIfOpen() +{ + if (ui.mainTabs->currentWidget() == ui.tab) { + refreshSaveList(); + } +} + +QDir SavesTab::currentSavesDir() const +{ + // TODO: This code should probably be handled by the game plugins + QDir savesDir; + if (m_core.currentProfile()->localSavesEnabled()) { + savesDir.setPath(m_core.currentProfile()->savePath()); + } else { + auto iniFiles = m_core.managedGame()->iniFiles(); + + if (iniFiles.isEmpty() || + m_core.gameFeatures().gameFeature<LocalSavegames>() == nullptr) { + return m_core.managedGame()->savesDirectory(); + } + + QString iniPath = m_core.currentProfile()->absoluteIniFilePath(iniFiles[0]); + +#ifdef _WIN32 + wchar_t path[MAX_PATH]; + if (::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"", path, MAX_PATH, + iniPath.toStdWString().c_str())) { + savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath( + QString::fromWCharArray(path))); + } else { + savesDir = m_core.managedGame()->savesDirectory(); + } +#else + // Read directly without QSettings — QSettings::IniFormat interprets + // trailing backslashes as line continuations, corrupting values like + // "sLocalSavePath=__MO_Saves\". + QString savePath = readIniValueDirect(iniPath, "General", "sLocalSavePath"); + // Strip trailing path separators (Bethesda INIs use "Saves\" with a + // trailing backslash that is part of the value, not a continuation). + while (savePath.endsWith('\\') || savePath.endsWith('/')) { + savePath.chop(1); + } + if (!savePath.isEmpty()) { + savesDir.setPath(m_core.managedGame()->documentsDirectory().absoluteFilePath(savePath)); + } else { + savesDir = m_core.managedGame()->savesDirectory(); + } +#endif + } + + return savesDir; +} + +void SavesTab::startMonitorSaves() +{ + stopMonitorSaves(); + + QDir savesDir = currentSavesDir(); + + m_SavesWatcher.addPath(savesDir.absolutePath()); +} + +void SavesTab::stopMonitorSaves() +{ + if (m_SavesWatcher.directories().length() > 0) { + m_SavesWatcher.removePaths(m_SavesWatcher.directories()); + } +} + +void SavesTab::refreshSaveList() +{ + TimeThis tt("MainWindow::refreshSaveList()"); + + startMonitorSaves(); // re-starts monitoring + + try { + QDir savesDir = currentSavesDir(); + MOBase::log::debug("reading save games from {}", savesDir.absolutePath()); + m_SaveGames = m_core.managedGame()->listSaves(savesDir); + std::sort(m_SaveGames.begin(), m_SaveGames.end(), + [](auto const& lhs, auto const& rhs) { + return lhs->getCreationTime() > rhs->getCreationTime(); + }); + ui.list->clear(); for (auto& save : m_SaveGames) { auto relpath = savesDir.relativeFilePath(save->getFilepath()); @@ -269,125 +318,125 @@ void SavesTab::refreshSaveList() ui.list->addTopLevelItem(new QTreeWidgetItem(ui.list, {display, relpath})); } } catch (std::exception& e) { - // listSaves() can throw
- log::error("{}", e.what());
- }
-}
-
-void SavesTab::deleteSavegame()
-{
- auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
-
- QString savesMsgLabel;
- QStringList deleteFiles;
-
- int count = 0;
-
- for (const QModelIndex& idx : ui.list->selectionModel()->selectedRows()) {
-
- auto& saveGame = m_SaveGames[idx.row()];
-
- if (count < 10) {
- savesMsgLabel +=
- "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>";
- }
- ++count;
-
- deleteFiles += saveGame->allFiles();
- }
-
- if (count > 10) {
- savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>";
- }
-
- if (QMessageBox::question(
- m_window, tr("Confirm"),
- tr("Are you sure you want to remove the following %n save(s)?<br>"
- "<ul>%1</ul><br>"
- "Removed saves will be sent to the Recycle Bin.",
- "", count)
- .arg(savesMsgLabel),
- QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
- shellDelete(deleteFiles, true); // recycle bin delete.
- refreshSaveList();
- }
-}
-
-void SavesTab::onContextMenu(const QPoint& pos)
-{
- QItemSelectionModel* selection = ui.list->selectionModel();
-
- if (!selection->hasSelection()) {
- return;
- }
-
- QMenu menu;
-
- auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
- if (info != nullptr) {
- QAction* action = menu.addAction(tr("Fix enabled mods..."));
- action->setEnabled(false);
- if (selection->selectedRows().count() == 1) {
- auto& save = m_SaveGames[selection->selectedRows()[0].row()];
- SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save);
- if (missing.size() != 0) {
- connect(action, &QAction::triggered, this, [this, missing] {
- fixMods(missing);
- });
- action->setEnabled(true);
- }
- }
- }
-
- QString deleteMenuLabel =
- tr("Delete %n save(s)", "", selection->selectedRows().count());
- menu.addAction(deleteMenuLabel, [&] {
- deleteSavegame();
- });
-
- menu.addAction(tr("Open in Explorer..."), [&] {
- openInExplorer();
- });
-
- menu.exec(ui.list->viewport()->mapToGlobal(pos));
-}
-
-void SavesTab::fixMods(SaveGameInfo::MissingAssets const& missingAssets)
-{
- ActivateModsDialog dialog(missingAssets, m_window);
- if (dialog.exec() == QDialog::Accepted) {
- // activate the required mods, then enable all esps
- std::set<QString> modsToActivate = dialog.getModsToActivate();
- for (std::set<QString>::iterator iter = modsToActivate.begin();
- iter != modsToActivate.end(); ++iter) {
- if ((*iter != "<data>") && (*iter != "<overwrite>")) {
- unsigned int modIndex = ModInfo::getIndex(*iter);
- m_core.currentProfile()->setModEnabled(modIndex, true);
- }
- }
-
- m_core.currentProfile()->writeModlist();
- m_core.refreshLists();
-
- std::set<QString> espsToActivate = dialog.getESPsToActivate();
- for (std::set<QString>::iterator iter = espsToActivate.begin();
- iter != espsToActivate.end(); ++iter) {
- m_core.pluginList()->enableESP(*iter);
- }
-
- m_core.saveCurrentLists();
- }
-}
-
-void SavesTab::openInExplorer()
-{
- auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>();
-
- const auto sel = ui.list->selectionModel()->selectedRows();
- if (sel.empty()) {
- return;
- }
-
- auto& saveGame = m_SaveGames[sel[0].row()];
- shell::Explore(saveGame->getFilepath());
-}
+ // listSaves() can throw + log::error("{}", e.what()); + } +} + +void SavesTab::deleteSavegame() +{ + auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>(); + + QString savesMsgLabel; + QStringList deleteFiles; + + int count = 0; + + for (const QModelIndex& idx : ui.list->selectionModel()->selectedRows()) { + + auto& saveGame = m_SaveGames[idx.row()]; + + if (count < 10) { + savesMsgLabel += + "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>"; + } + ++count; + + deleteFiles += saveGame->allFiles(); + } + + if (count > 10) { + savesMsgLabel += "<li><i>... " + tr("%1 more").arg(count - 10) + "</i></li>"; + } + + if (QMessageBox::question( + m_window, tr("Confirm"), + tr("Are you sure you want to remove the following %n save(s)?<br>" + "<ul>%1</ul><br>" + "Removed saves will be sent to the Recycle Bin.", + "", count) + .arg(savesMsgLabel), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + shellDelete(deleteFiles, true); // recycle bin delete. + refreshSaveList(); + } +} + +void SavesTab::onContextMenu(const QPoint& pos) +{ + QItemSelectionModel* selection = ui.list->selectionModel(); + + if (!selection->hasSelection()) { + return; + } + + QMenu menu; + + auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>(); + if (info != nullptr) { + QAction* action = menu.addAction(tr("Fix enabled mods...")); + action->setEnabled(false); + if (selection->selectedRows().count() == 1) { + auto& save = m_SaveGames[selection->selectedRows()[0].row()]; + SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save); + if (missing.size() != 0) { + connect(action, &QAction::triggered, this, [this, missing] { + fixMods(missing); + }); + action->setEnabled(true); + } + } + } + + QString deleteMenuLabel = + tr("Delete %n save(s)", "", selection->selectedRows().count()); + menu.addAction(deleteMenuLabel, [&] { + deleteSavegame(); + }); + + menu.addAction(tr("Open in Explorer..."), [&] { + openInExplorer(); + }); + + menu.exec(ui.list->viewport()->mapToGlobal(pos)); +} + +void SavesTab::fixMods(SaveGameInfo::MissingAssets const& missingAssets) +{ + ActivateModsDialog dialog(missingAssets, m_window); + if (dialog.exec() == QDialog::Accepted) { + // activate the required mods, then enable all esps + std::set<QString> modsToActivate = dialog.getModsToActivate(); + for (std::set<QString>::iterator iter = modsToActivate.begin(); + iter != modsToActivate.end(); ++iter) { + if ((*iter != "<data>") && (*iter != "<overwrite>")) { + unsigned int modIndex = ModInfo::getIndex(*iter); + m_core.currentProfile()->setModEnabled(modIndex, true); + } + } + + m_core.currentProfile()->writeModlist(); + m_core.refreshLists(); + + std::set<QString> espsToActivate = dialog.getESPsToActivate(); + for (std::set<QString>::iterator iter = espsToActivate.begin(); + iter != espsToActivate.end(); ++iter) { + m_core.pluginList()->enableESP(*iter); + } + + m_core.saveCurrentLists(); + } +} + +void SavesTab::openInExplorer() +{ + auto info = m_core.gameFeatures().gameFeature<SaveGameInfo>(); + + const auto sel = ui.list->selectionModel()->selectedRows(); + if (sel.empty()) { + return; + } + + auto& saveGame = m_SaveGames[sel[0].row()]; + shell::Explore(saveGame->getFilepath()); +} diff --git a/src/src/shared/appconfig.cpp b/src/src/shared/appconfig.cpp index 90bbffb..579318f 100644 --- a/src/src/shared/appconfig.cpp +++ b/src/src/shared/appconfig.cpp @@ -46,6 +46,28 @@ QString basePath() return QCoreApplication::applicationDirPath();
}
+QString pluginsPath()
+{
+#ifndef _WIN32
+ const char* envDir = std::getenv("MO2_PLUGINS_DIR");
+ if (envDir && envDir[0] != '\0') {
+ return QString::fromUtf8(envDir);
+ }
+#endif
+ return basePath() + "/" + QString::fromStdWString(pluginPath());
+}
+
+QString dllsPath()
+{
+#ifndef _WIN32
+ const char* envDir = std::getenv("MO2_DLLS_DIR");
+ if (envDir && envDir[0] != '\0') {
+ return QString::fromUtf8(envDir);
+ }
+#endif
+ return basePath() + "/dlls";
+}
+
namespace MOShared
{
#undef PARWSTRING
diff --git a/src/src/shared/appconfig.h b/src/src/shared/appconfig.h index 03f3300..0c0f7b4 100644 --- a/src/src/shared/appconfig.h +++ b/src/src/shared/appconfig.h @@ -36,6 +36,16 @@ namespace AppConfig // returned; otherwise falls back to QCoreApplication::applicationDirPath().
QString basePath();
+// Returns the directory containing MO2 plugins. On Linux, if the
+// MO2_PLUGINS_DIR environment variable is set (e.g. when plugins live
+// inside a read-only AppImage squashfs) that value is used; otherwise
+// falls back to basePath() + "/plugins".
+QString pluginsPath();
+
+// Returns the directory containing bundled DLLs (7z.so, etc.).
+// Respects MO2_DLLS_DIR on Linux, otherwise basePath() + "/dlls".
+QString dllsPath();
+
namespace MOShared
{
#undef PARWSTRING
diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 05e07bd..850d5a2 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -53,12 +53,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase;
using namespace MOShared;
-namespace spawn::dialogs
-{
+namespace spawn::dialogs {
#ifdef _WIN32
-std::wstring makeRightsDetails(const env::FileSecurity& fs)
-{
+std::wstring makeRightsDetails(const env::FileSecurity &fs) {
if (fs.rights.normalRights) {
return L"(normal rights)";
}
@@ -75,22 +73,22 @@ std::wstring makeRightsDetails(const env::FileSecurity& fs) return s;
}
-QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = {})
-{
+QString makeDetails(const SpawnParameters &sp, DWORD code,
+ const QString &more = {}) {
std::wstring owner, rights;
if (sp.binary.isFile()) {
const auto fs = env::getFileSecurity(sp.binary.absoluteFilePath());
if (fs.error.isEmpty()) {
- owner = fs.owner.toStdWString();
+ owner = fs.owner.toStdWString();
rights = makeRightsDetails(fs);
} else {
- owner = fs.error.toStdWString();
+ owner = fs.error.toStdWString();
rights = fs.error.toStdWString();
}
} else {
- owner = L"(file not found)";
+ owner = L"(file not found)";
rights = L"(file not found)";
}
@@ -98,7 +96,7 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = (sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists());
const auto appDir = QCoreApplication::applicationDirPath();
- const auto sep = QDir::separator();
+ const auto sep = QDir::separator();
const std::wstring usvfs_x86_dll =
QFileInfo(appDir + sep + "usvfs_x86.dll").isFile() ? L"ok" : L"not found";
@@ -107,10 +105,12 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = QFileInfo(appDir + sep + "usvfs_x64.dll").isFile() ? L"ok" : L"not found";
const std::wstring usvfs_x86_proxy =
- QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok" : L"not found";
+ QFileInfo(appDir + sep + "usvfs_proxy_x86.exe").isFile() ? L"ok"
+ : L"not found";
const std::wstring usvfs_x64_proxy =
- QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok" : L"not found";
+ QFileInfo(appDir + sep + "usvfs_proxy_x64.exe").isFile() ? L"ok"
+ : L"not found";
std::wstring elevated;
if (auto b = env::Environment().windowsInfo().isElevated()) {
@@ -119,42 +119,44 @@ QString makeDetails(const SpawnParameters& sp, DWORD code, const QString& more = elevated = L"(not available)";
}
- auto s = std::format(L"Error {} {}{}: {}\n"
- L" . binary: '{}'\n"
- L" . owner: {}\n"
- L" . rights: {}\n"
- L" . arguments: '{}'\n"
- L" . cwd: '{}'{}\n"
- L" . stdout: {}, stderr: {}, hooked: {}\n"
- L" . MO elevated: {}",
- code, errorCodeName(code), (more.isEmpty() ? more : ", " + more),
- formatSystemMessage(code),
- QDir::toNativeSeparators(sp.binary.absoluteFilePath()), owner,
- rights, sp.arguments,
- QDir::toNativeSeparators(sp.currentDirectory.absolutePath()),
- (cwdExists ? L"" : L" (not found)"),
- (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
- (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
- (sp.hooked ? L"yes" : L"no"), elevated);
+ auto s = std::format(
+ L"Error {} {}{}: {}\n"
+ L" . binary: '{}'\n"
+ L" . owner: {}\n"
+ L" . rights: {}\n"
+ L" . arguments: '{}'\n"
+ L" . cwd: '{}'{}\n"
+ L" . stdout: {}, stderr: {}, hooked: {}\n"
+ L" . MO elevated: {}",
+ code, errorCodeName(code), (more.isEmpty() ? more : ", " + more),
+ formatSystemMessage(code),
+ QDir::toNativeSeparators(sp.binary.absoluteFilePath()), owner, rights,
+ sp.arguments,
+ QDir::toNativeSeparators(sp.currentDirectory.absolutePath()),
+ (cwdExists ? L"" : L" (not found)"),
+ (sp.stdOut == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
+ (sp.stdErr == INVALID_HANDLE_VALUE ? L"no" : L"yes"),
+ (sp.hooked ? L"yes" : L"no"), elevated);
if (sp.hooked) {
s += std::format(L"\n . usvfs x86:{} x64:{} proxy_x86:{} proxy_x64:{}",
- usvfs_x86_dll, usvfs_x64_dll, usvfs_x86_proxy, usvfs_x64_proxy);
+ usvfs_x86_dll, usvfs_x64_dll, usvfs_x86_proxy,
+ usvfs_x64_proxy);
}
return QString::fromStdWString(s);
}
#else
-QString makeDetails(const SpawnParameters& sp, int code, const QString& more = {})
-{
+QString makeDetails(const SpawnParameters &sp, int code,
+ const QString &more = {}) {
const bool cwdExists =
(sp.currentDirectory.isEmpty() ? true : sp.currentDirectory.exists());
QString s = QString("Error %1%2: %3\n"
- " . binary: '%4'\n"
- " . arguments: '%5'\n"
- " . cwd: '%6'%7\n"
- " . stdout: %8, stderr: %9, hooked: %10")
+ " . binary: '%4'\n"
+ " . arguments: '%5'\n"
+ " . cwd: '%6'%7\n"
+ " . stdout: %8, stderr: %9, hooked: %10")
.arg(code)
.arg(more.isEmpty() ? more : ", " + more)
.arg(QString::fromUtf8(strerror(code)))
@@ -171,19 +173,21 @@ QString makeDetails(const SpawnParameters& sp, int code, const QString& more = { #endif
#ifdef _WIN32
-QString makeContent(const SpawnParameters& sp, DWORD code)
-{
+QString makeContent(const SpawnParameters &sp, DWORD code) {
if (code == ERROR_INVALID_PARAMETER) {
return QObject::tr(
- "This error typically happens because an antivirus has deleted critical "
+ "This error typically happens because an antivirus has deleted "
+ "critical "
"files from Mod Organizer's installation folder or has made them "
"generally inaccessible. Add an exclusion for Mod Organizer's "
- "installation folder in your antivirus, reinstall Mod Organizer and try "
+ "installation folder in your antivirus, reinstall Mod Organizer and "
+ "try "
"again.");
} else if (code == ERROR_ACCESS_DENIED) {
return QObject::tr(
"This error typically happens because an antivirus is preventing Mod "
- "Organizer from starting programs. Add an exclusion for Mod Organizer's "
+ "Organizer from starting programs. Add an exclusion for Mod "
+ "Organizer's "
"installation folder in your antivirus and try again.");
} else if (code == ERROR_FILE_NOT_FOUND) {
return QObject::tr("The file '%1' does not exist.")
@@ -198,15 +202,13 @@ QString makeContent(const SpawnParameters& sp, DWORD code) return QString::fromStdWString(formatSystemMessage(code));
}
#else
-QString makeContent(const SpawnParameters& sp, int code)
-{
+QString makeContent(const SpawnParameters &sp, int code) {
if (code == ENOENT) {
return QObject::tr("The file '%1' does not exist.")
.arg(sp.binary.absoluteFilePath());
} else if (code == EACCES) {
- return QObject::tr(
- "Permission denied when trying to start '%1'. "
- "Check that the file is executable.")
+ return QObject::tr("Permission denied when trying to start '%1'. "
+ "Check that the file is executable.")
.arg(sp.binary.absoluteFilePath());
}
@@ -220,38 +222,39 @@ QString makeContent(const SpawnParameters& sp, int code) #endif
#ifdef _WIN32
-QMessageBox::StandardButton badSteamReg(QWidget* parent, const QString& keyName,
- const QString& valueName)
-{
+QMessageBox::StandardButton badSteamReg(QWidget *parent, const QString &keyName,
+ const QString &valueName) {
const auto details =
- QString("can't start steam, registry value at '%1' is empty or doesn't exist")
+ QString(
+ "can't start steam, registry value at '%1' is empty or doesn't exist")
.arg(keyName + "\\" + valueName);
log::error("{}", details);
return MOBase::TaskDialog(parent, QObject::tr("Cannot start Steam"))
.main(QObject::tr("Cannot start Steam"))
- .content(
- QObject::tr("The path to the Steam executable cannot be found. You might try "
- "reinstalling Steam."))
+ .content(QObject::tr(
+ "The path to the Steam executable cannot be found. You might try "
+ "reinstalling Steam."))
.details(details)
.icon(QMessageBox::Critical)
.button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."), QMessageBox::Yes})
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
}
#endif
#ifdef _WIN32
-QMessageBox::StandardButton startSteamFailed(QWidget* parent, const QString& keyName,
- const QString& valueName,
- const QString& exe,
- const SpawnParameters& sp, DWORD e)
-{
- auto details = QString("a steam install was found in the registry at '%1': '%2'\n\n")
- .arg(keyName + "\\" + valueName)
- .arg(exe);
+QMessageBox::StandardButton
+startSteamFailed(QWidget *parent, const QString &keyName,
+ const QString &valueName, const QString &exe,
+ const SpawnParameters &sp, DWORD e) {
+ auto details =
+ QString("a steam install was found in the registry at '%1': '%2'\n\n")
+ .arg(keyName + "\\" + valueName)
+ .arg(exe);
details += makeDetails(sp, e);
@@ -263,26 +266,27 @@ QMessageBox::StandardButton startSteamFailed(QWidget* parent, const QString& key .details(details)
.icon(QMessageBox::Critical)
.button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."), QMessageBox::Yes})
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
}
#endif
-void spawnFailed(QWidget* parent, const SpawnParameters& sp,
+void spawnFailed(QWidget *parent, const SpawnParameters &sp,
#ifdef _WIN32
DWORD code
#else
int code
#endif
-)
-{
+) {
const auto details = makeDetails(sp, code);
log::error("{}", details);
const auto title = QObject::tr("Cannot launch program");
- const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName());
+ const auto mainText =
+ QObject::tr("Cannot start %1").arg(sp.binary.fileName());
MOBase::TaskDialog(parent, title)
.main(mainText)
@@ -293,10 +297,9 @@ void spawnFailed(QWidget* parent, const SpawnParameters& sp, }
#ifdef _WIN32
-void helperFailed(QWidget* parent, DWORD code, const QString& why,
- const std::wstring& binary, const std::wstring& cwd,
- const std::wstring& args)
-{
+void helperFailed(QWidget *parent, DWORD code, const QString &why,
+ const std::wstring &binary, const std::wstring &cwd,
+ const std::wstring &args) {
SpawnParameters sp;
sp.binary = QFileInfo(QString::fromStdWString(binary));
sp.currentDirectory.setPath(QString::fromStdWString(cwd));
@@ -307,7 +310,8 @@ void helperFailed(QWidget* parent, DWORD code, const QString& why, const auto title = QObject::tr("Cannot launch helper");
- const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName());
+ const auto mainText =
+ QObject::tr("Cannot start %1").arg(sp.binary.fileName());
MOBase::TaskDialog(parent, title)
.main(mainText)
@@ -317,20 +321,22 @@ void helperFailed(QWidget* parent, DWORD code, const QString& why, .exec();
}
-bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp)
-{
+bool confirmRestartAsAdmin(QWidget *parent, const SpawnParameters &sp) {
const auto details = makeDetails(sp, ERROR_ELEVATION_REQUIRED);
log::error("{}", details);
const auto title = QObject::tr("Elevation required");
- const auto mainText = QObject::tr("Cannot start %1").arg(sp.binary.fileName());
+ const auto mainText =
+ QObject::tr("Cannot start %1").arg(sp.binary.fileName());
const auto content = QObject::tr(
"This program is requesting to run as administrator but Mod Organizer "
- "itself is not running as administrator. Running programs as administrator "
- "is typically unnecessary as long as the game and Mod Organizer have been "
+ "itself is not running as administrator. Running programs as "
+ "administrator "
+ "is typically unnecessary as long as the game and Mod Organizer have "
+ "been "
"installed outside \"Program Files\".\r\n\r\n"
"You can restart Mod Organizer as administrator and try launching the "
"program again.");
@@ -344,24 +350,24 @@ bool confirmRestartAsAdmin(QWidget* parent, const SpawnParameters& sp) .details(details)
.icon(QMessageBox::Question)
.button({QObject::tr("Restart Mod Organizer as administrator"),
- QObject::tr(
- "You must allow \"helper.exe\" to make changes to the system."),
+ QObject::tr("You must allow \"helper.exe\" to make changes "
+ "to the system."),
QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
return (r == QMessageBox::Yes);
}
-#endif // _WIN32
+#endif // _WIN32
-QMessageBox::StandardButton
-confirmStartSteam(QWidget* parent, const SpawnParameters& sp, const QString& details)
-{
- const auto title = QObject::tr("Launch Steam");
+QMessageBox::StandardButton confirmStartSteam(QWidget *parent,
+ const SpawnParameters &sp,
+ const QString &details) {
+ const auto title = QObject::tr("Launch Steam");
const auto mainText = QObject::tr("This program requires Steam");
- const auto content = QObject::tr(
- "Mod Organizer has detected that this program likely requires Steam to be "
- "running to function properly.");
+ const auto content = QObject::tr("Mod Organizer has detected that this "
+ "program likely requires Steam to be "
+ "running to function properly.");
return MOBase::TaskDialog(parent, title)
.main(mainText)
@@ -377,17 +383,16 @@ confirmStartSteam(QWidget* parent, const SpawnParameters& sp, const QString& det }
#ifdef _WIN32
-QMessageBox::StandardButton confirmRestartAsAdminForSteam(QWidget* parent,
- const SpawnParameters& sp)
-{
- const auto title = QObject::tr("Elevation required");
+QMessageBox::StandardButton
+confirmRestartAsAdminForSteam(QWidget *parent, const SpawnParameters &sp) {
+ const auto title = QObject::tr("Elevation required");
const auto mainText = QObject::tr("Steam is running as administrator");
- const auto content = QObject::tr(
+ const auto content = QObject::tr(
"Running Steam as administrator is typically unnecessary and can cause "
- "problems when Mod Organizer itself is not running as administrator."
- "\r\n\r\n"
- "You can restart Mod Organizer as administrator and try launching the "
- "program again.");
+ "problems when Mod Organizer itself is not running as administrator."
+ "\r\n\r\n"
+ "You can restart Mod Organizer as administrator and try launching the "
+ "program again.");
return MOBase::TaskDialog(parent, title)
.main(mainText)
@@ -395,24 +400,25 @@ QMessageBox::StandardButton confirmRestartAsAdminForSteam(QWidget* parent, .icon(QMessageBox::Question)
.button(
{QObject::tr("Restart Mod Organizer as administrator"),
- QObject::tr("You must allow \"helper.exe\" to make changes to the system."),
+ QObject::tr(
+ "You must allow \"helper.exe\" to make changes to the system."),
QMessageBox::Yes})
- .button({QObject::tr("Continue"), QObject::tr("The program might fail to run."),
- QMessageBox::No})
+ .button({QObject::tr("Continue"),
+ QObject::tr("The program might fail to run."), QMessageBox::No})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.remember("steamAdminQuery", sp.binary.fileName())
.exec();
}
-bool eventLogNotRunning(QWidget* parent, const env::Service& s,
- const SpawnParameters& sp)
-{
- const auto title = QObject::tr("Event Log not running");
+bool eventLogNotRunning(QWidget *parent, const env::Service &s,
+ const SpawnParameters &sp) {
+ const auto title = QObject::tr("Event Log not running");
const auto mainText = QObject::tr("The Event Log service is not running");
- const auto content = QObject::tr(
- "The Windows Event Log service is not running. This can prevent USVFS from "
- "running properly and your mods may not be recognized by the program being "
- "launched.");
+ const auto content = QObject::tr("The Windows Event Log service is not "
+ "running. This can prevent USVFS from "
+ "running properly and your mods may not be "
+ "recognized by the program being "
+ "launched.");
const auto r =
MOBase::TaskDialog(parent, title)
@@ -421,18 +427,18 @@ bool eventLogNotRunning(QWidget* parent, const env::Service& s, .details(s.toString())
.icon(QMessageBox::Question)
.remember("eventLogService", sp.binary.fileName())
- .button({QObject::tr("Continue"), QObject::tr("Your mods might not work."),
- QMessageBox::Yes})
+ .button({QObject::tr("Continue"),
+ QObject::tr("Your mods might not work."), QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
return (r == QMessageBox::Yes);
}
-#endif // _WIN32
+#endif // _WIN32
-QMessageBox::StandardButton
-confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& settings)
-{
+QMessageBox::StandardButton confirmBlacklisted(QWidget *parent,
+ const SpawnParameters &sp,
+ Settings &settings) {
const auto title = QObject::tr("Blacklisted program");
const auto mainText =
QObject::tr("The program %1 is blacklisted").arg(sp.binary.fileName());
@@ -446,17 +452,18 @@ confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& setting "Current blacklist: " +
settings.executablesBlacklist();
- auto r = MOBase::TaskDialog(parent, title)
- .main(mainText)
- .content(content)
- .details(details)
- .icon(QMessageBox::Question)
- .remember("blacklistedExecutable", sp.binary.fileName())
- .button({QObject::tr("Continue"),
- QObject::tr("Your mods might not work."), QMessageBox::Yes})
- .button({QObject::tr("Change the blacklist"), QMessageBox::Retry})
- .button({QObject::tr("Cancel"), QMessageBox::Cancel})
- .exec();
+ auto r =
+ MOBase::TaskDialog(parent, title)
+ .main(mainText)
+ .content(content)
+ .details(details)
+ .icon(QMessageBox::Question)
+ .remember("blacklistedExecutable", sp.binary.fileName())
+ .button({QObject::tr("Continue"),
+ QObject::tr("Your mods might not work."), QMessageBox::Yes})
+ .button({QObject::tr("Change the blacklist"), QMessageBox::Retry})
+ .button({QObject::tr("Cancel"), QMessageBox::Cancel})
+ .exec();
if (r == QMessageBox::Retry) {
if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) {
@@ -467,13 +474,11 @@ confirmBlacklisted(QWidget* parent, const SpawnParameters& sp, Settings& setting return r;
}
-} // namespace spawn::dialogs
+} // namespace spawn::dialogs
-namespace spawn
-{
+namespace spawn {
-void logSpawning(const SpawnParameters& sp, const QString& realCmd)
-{
+void logSpawning(const SpawnParameters &sp, const QString &realCmd) {
log::debug("spawning binary:\n"
" . exe: '{}'\n"
" . args: '{}'\n"
@@ -489,23 +494,21 @@ void logSpawning(const SpawnParameters& sp, const QString& realCmd) (sp.stdOut == INVALID_HANDLE_VALUE ? "no" : "yes"),
(sp.stdErr == INVALID_HANDLE_VALUE ? "no" : "yes"),
#else
- (sp.stdOut == -1 ? "no" : "yes"),
- (sp.stdErr == -1 ? "no" : "yes"),
+ (sp.stdOut == -1 ? "no" : "yes"), (sp.stdErr == -1 ? "no" : "yes"),
#endif
realCmd);
}
#ifndef _WIN32
-uint32_t parseSteamAppId(const QString& steamAppId)
-{
- bool ok = false;
+uint32_t parseSteamAppId(const QString &steamAppId) {
+ bool ok = false;
const auto n = steamAppId.toUInt(&ok);
return (ok ? n : 0u);
}
-QString firstExistingSetting(const QSettings& settings, const QStringList& keys)
-{
- for (const QString& key : keys) {
+QString firstExistingSetting(const QSettings &settings,
+ const QStringList &keys) {
+ for (const QString &key : keys) {
const QString value = settings.value(key).toString().trimmed();
if (!value.isEmpty()) {
return value;
@@ -515,13 +518,13 @@ QString firstExistingSetting(const QSettings& settings, const QStringList& keys) return {};
}
-QString resolvePrefixPath()
-{
- if (auto cfg = FluorineConfig::load(); cfg.has_value() && cfg->prefixExists()) {
+QString resolvePrefixPath() {
+ if (auto cfg = FluorineConfig::load();
+ cfg.has_value() && cfg->prefixExists()) {
return cfg->prefix_path.trimmed();
}
- const Settings* settings = Settings::maybeInstance();
+ const Settings *settings = Settings::maybeInstance();
if (settings == nullptr) {
return {};
}
@@ -532,8 +535,7 @@ QString resolvePrefixPath() "Proton/prefix_path", "fluorine/prefix_path"});
}
-QString resolveProtonPath()
-{
+QString resolveProtonPath() {
if (auto cfg = FluorineConfig::load(); cfg.has_value()) {
const QString protonPath = cfg->proton_path.trimmed();
if (!protonPath.isEmpty()) {
@@ -541,35 +543,34 @@ QString resolveProtonPath() }
}
- const Settings* settings = Settings::maybeInstance();
+ const Settings *settings = Settings::maybeInstance();
if (settings == nullptr) {
return {};
}
const QSettings instanceSettings(settings->filename(), QSettings::IniFormat);
- return firstExistingSetting(instanceSettings,
- {"Settings/proton_path", "Proton/path",
- "fluorine/proton_path"});
+ return firstExistingSetting(
+ instanceSettings,
+ {"Settings/proton_path", "Proton/path", "fluorine/proton_path"});
}
#endif
#ifdef _WIN32
-DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle)
-{
+DWORD spawn(const SpawnParameters &sp, HANDLE &processHandle) {
BOOL inheritHandles = FALSE;
STARTUPINFO si = {};
- si.cb = sizeof(si);
+ si.cb = sizeof(si);
// inherit handles if we plan to use stdout or stderr reroute
if (sp.stdOut != INVALID_HANDLE_VALUE) {
- si.hStdOutput = sp.stdOut;
+ si.hStdOutput = sp.stdOut;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
if (sp.stdErr != INVALID_HANDLE_VALUE) {
- si.hStdError = sp.stdErr;
+ si.hStdError = sp.stdErr;
inheritHandles = TRUE;
si.dwFlags |= STARTF_USESTDHANDLES;
}
@@ -583,26 +584,26 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) }
const QString moPath = QCoreApplication::applicationDirPath();
- const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath));
+ const auto oldPath = env::appendToPath(QDir::toNativeSeparators(moPath));
PROCESS_INFORMATION pi = {};
- BOOL success = FALSE;
+ BOOL success = FALSE;
logSpawning(sp, commandLine);
const auto wcommandLine = commandLine.toStdWString();
- const auto wcwd = cwd.toStdWString();
+ const auto wcwd = cwd.toStdWString();
const DWORD flags = CREATE_BREAKAWAY_FROM_JOB;
if (sp.hooked) {
success = ::usvfsCreateProcessHooked(
- nullptr, const_cast<wchar_t*>(wcommandLine.c_str()), nullptr, nullptr,
+ nullptr, const_cast<wchar_t *>(wcommandLine.c_str()), nullptr, nullptr,
inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
} else {
- success = ::CreateProcess(nullptr, const_cast<wchar_t*>(wcommandLine.c_str()),
- nullptr, nullptr, inheritHandles, flags, nullptr,
- wcwd.c_str(), &si, &pi);
+ success = ::CreateProcess(
+ nullptr, const_cast<wchar_t *>(wcommandLine.c_str()), nullptr, nullptr,
+ inheritHandles, flags, nullptr, wcwd.c_str(), &si, &pi);
}
const auto e = GetLastError();
@@ -618,10 +619,11 @@ DWORD spawn(const SpawnParameters& sp, HANDLE& processHandle) return ERROR_SUCCESS;
}
#else
-int spawn(const SpawnParameters& sp, pid_t& processId)
-{
- const QString bin = MOBase::normalizePathForHost(sp.binary.absoluteFilePath());
- QString cwd = MOBase::normalizePathForHost(sp.currentDirectory.absolutePath());
+int spawn(const SpawnParameters &sp, pid_t &processId) {
+ const QString bin =
+ MOBase::normalizePathForHost(sp.binary.absoluteFilePath());
+ QString cwd =
+ MOBase::normalizePathForHost(sp.currentDirectory.absolutePath());
QStringList argList;
if (!sp.arguments.isEmpty()) {
@@ -634,6 +636,21 @@ int spawn(const SpawnParameters& sp, pid_t& processId) logSpawning(sp, bin + " " + sp.arguments);
+ // Read per-instance settings from the instance INI (not the global
+ // QSettings).
+ const Settings *instanceForLaunch = Settings::maybeInstance();
+ bool useSteamDrm = true; // default
+ QString storeVariant;
+ if (instanceForLaunch) {
+ const QSettings instanceIni(instanceForLaunch->filename(),
+ QSettings::IniFormat);
+ useSteamDrm = instanceIni.value("fluorine/steam_drm", true).toBool();
+ // settingName() maps "General" section keys to top-level (no section
+ // prefix), so game_edition is stored as a plain key under [General] in the
+ // INI.
+ storeVariant = instanceIni.value("game_edition").toString().trimmed();
+ }
+
ProtonLauncher launcher;
launcher.setBinary(bin)
.setArguments(argList)
@@ -644,14 +661,18 @@ 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())
- .setSteamDrm(QSettings().value("fluorine/steam_drm", true).toBool());
+ .setSteamDrm(useSteamDrm)
+ .setStoreVariant(storeVariant);
const QString prefixPath = resolvePrefixPath();
if (prefixPath.isEmpty()) {
- MOBase::log::warn("No Wine prefix configured - games may not launch correctly. "
- "Configure a prefix in Settings > Proton or via fluorine-manager.");
+ MOBase::log::warn(
+ "No Wine prefix configured - games may not launch correctly. "
+ "Configure a prefix in Settings > Proton or via fluorine-manager.");
} else if (!QDir(QDir(prefixPath).filePath("drive_c")).exists()) {
- MOBase::log::warn("Wine prefix '{}' does not contain drive_c/ - prefix may be invalid", prefixPath);
+ MOBase::log::warn(
+ "Wine prefix '{}' does not contain drive_c/ - prefix may be invalid",
+ prefixPath);
} else {
MOBase::log::info("Using Wine prefix: {}", prefixPath);
launcher.setPrefix(prefixPath);
@@ -662,7 +683,8 @@ int spawn(const SpawnParameters& sp, pid_t& processId) launcher.setProtonPath(protonPath);
}
- const QString wrapper = QSettings().value("fluorine/launch_wrapper").toString().trimmed();
+ const QString wrapper =
+ QSettings().value("fluorine/launch_wrapper").toString().trimmed();
if (!wrapper.isEmpty()) {
launcher.setWrapper(wrapper);
}
@@ -678,8 +700,7 @@ int spawn(const SpawnParameters& sp, pid_t& processId) #endif
#ifdef _WIN32
-bool restartAsAdmin(QWidget* parent)
-{
+bool restartAsAdmin(QWidget *parent) {
WCHAR cwd[MAX_PATH] = {};
if (!GetCurrentDirectory(MAX_PATH, cwd)) {
cwd[0] = L'\0';
@@ -698,8 +719,7 @@ bool restartAsAdmin(QWidget* parent) return true;
}
-void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp)
-{
+void startBinaryAdmin(QWidget *parent, const SpawnParameters &sp) {
if (!dialogs::confirmRestartAsAdmin(parent, sp)) {
log::debug("user declined");
return;
@@ -708,25 +728,23 @@ void startBinaryAdmin(QWidget* parent, const SpawnParameters& sp) log::info("restarting MO as administrator");
restartAsAdmin(parent);
}
-#endif // _WIN32
+#endif // _WIN32
-struct SteamStatus
-{
- bool running = false;
+struct SteamStatus {
+ bool running = false;
bool accessible = false;
};
-SteamStatus getSteamStatus()
-{
+SteamStatus getSteamStatus() {
SteamStatus ss;
#ifdef _WIN32
const auto ps = env::Environment().runningProcesses();
- for (const auto& p : ps) {
+ for (const auto &p : ps) {
if ((p.name().compare("Steam.exe", Qt::CaseInsensitive) == 0) ||
(p.name().compare("SteamService.exe", Qt::CaseInsensitive) == 0)) {
- ss.running = true;
+ ss.running = true;
ss.accessible = p.canAccess();
log::debug("'{}' is running, accessible={}", p.name(),
@@ -742,7 +760,7 @@ SteamStatus getSteamStatus() pgrep.waitForFinished(3000);
if (pgrep.exitCode() == 0) {
- ss.running = true;
+ ss.running = true;
ss.accessible = true;
log::debug("steam is running");
}
@@ -751,8 +769,7 @@ SteamStatus getSteamStatus() return ss;
}
-QString makeSteamArguments(const QString& username, const QString& password)
-{
+QString makeSteamArguments(const QString &username, const QString &password) {
QString args;
if (username != "") {
@@ -766,17 +783,17 @@ QString makeSteamArguments(const QString& username, const QString& password) return args;
}
-bool startSteam(QWidget* parent)
-{
+bool startSteam(QWidget *parent) {
#ifdef _WIN32
- const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam";
+ const QString keyName = "HKEY_CURRENT_USER\\Software\\Valve\\Steam";
const QString valueName = "SteamExe";
const QSettings steamSettings(keyName, QSettings::NativeFormat);
const QString exe = steamSettings.value(valueName, "").toString();
if (exe.isEmpty()) {
- return (dialogs::badSteamReg(parent, keyName, valueName) == QMessageBox::Yes);
+ return (dialogs::badSteamReg(parent, keyName, valueName) ==
+ QMessageBox::Yes);
}
SpawnParameters sp;
@@ -797,10 +814,11 @@ bool startSteam(QWidget* parent) log::debug("starting steam process:\n"
" . program: '{}'\n"
" . username={}, password={}",
- sp.binary.filePath().toStdString(), (username.isEmpty() ? "no" : "yes"),
+ sp.binary.filePath().toStdString(),
+ (username.isEmpty() ? "no" : "yes"),
(password.isEmpty() ? "no" : "yes"));
- HANDLE ph = INVALID_HANDLE_VALUE;
+ HANDLE ph = INVALID_HANDLE_VALUE;
const auto e = spawn(sp, ph);
::CloseHandle(ph);
@@ -809,7 +827,8 @@ bool startSteam(QWidget* parent) sp.arguments = makeSteamArguments((username.isEmpty() ? "" : "USERNAME"),
(password.isEmpty() ? "" : "PASSWORD"));
- const auto r = dialogs::startSteamFailed(parent, keyName, valueName, exe, sp, e);
+ const auto r =
+ dialogs::startSteamFailed(parent, keyName, valueName, exe, sp, e);
return (r == QMessageBox::Yes);
}
@@ -840,10 +859,11 @@ bool startSteam(QWidget* parent) MOBase::TaskDialog(parent, title)
.main(title)
.content(QObject::tr("The Steam executable could not be found. "
- "Make sure Steam is installed."))
+ "Make sure Steam is installed."))
.icon(QMessageBox::Critical)
.button({QObject::tr("Continue without starting Steam"),
- QObject::tr("The program may fail to launch."), QMessageBox::Yes})
+ QObject::tr("The program may fail to launch."),
+ QMessageBox::Yes})
.button({QObject::tr("Cancel"), QMessageBox::Cancel})
.exec();
@@ -869,11 +889,12 @@ bool startSteam(QWidget* parent) #endif
}
-bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDirectory,
- const QString& steamAppID, const Settings& settings)
-{
+bool checkSteam(QWidget *parent, const SpawnParameters &sp,
+ const QDir &gameDirectory, const QString &steamAppID,
+ const Settings &settings) {
#ifdef _WIN32
- static const std::vector<QString> steamFiles = {"steam_api.dll", "steam_api64.dll"};
+ static const std::vector<QString> steamFiles = {"steam_api.dll",
+ "steam_api64.dll"};
#else
static const std::vector<QString> steamFiles = {"libsteam_api.so"};
#endif
@@ -889,7 +910,7 @@ bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDire bool steamRequired = false;
QString details;
- for (const auto& file : steamFiles) {
+ for (const auto &file : steamFiles) {
const QFileInfo fi(gameDirectory.absoluteFilePath(file));
if (fi.exists()) {
details = QString("managed game is located at '%1' and file '%2' exists")
@@ -959,8 +980,8 @@ bool checkSteam(QWidget* parent, const SpawnParameters& sp, const QDir& gameDire return true;
}
-bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, Settings& settings)
-{
+bool checkBlacklist(QWidget *parent, const SpawnParameters &sp,
+ Settings &settings) {
for (;;) {
if (!settings.isExecutableBlacklisted(sp.binary.fileName())) {
return true;
@@ -975,10 +996,9 @@ bool checkBlacklist(QWidget* parent, const SpawnParameters& sp, Settings& settin }
#ifdef _WIN32
-HANDLE startBinary(QWidget* parent, const SpawnParameters& sp)
-{
+HANDLE startBinary(QWidget *parent, const SpawnParameters &sp) {
HANDLE handle = INVALID_HANDLE_VALUE;
- const auto e = spawn::spawn(sp, handle);
+ const auto e = spawn::spawn(sp, handle);
switch (e) {
case ERROR_SUCCESS: {
@@ -997,8 +1017,7 @@ HANDLE startBinary(QWidget* parent, const SpawnParameters& sp) }
}
#else
-pid_t startBinary(QWidget* parent, const SpawnParameters& sp)
-{
+pid_t startBinary(QWidget *parent, const SpawnParameters &sp) {
pid_t pid = -1;
const auto e = spawn::spawn(sp, pid);
@@ -1012,19 +1031,18 @@ pid_t startBinary(QWidget* parent, const SpawnParameters& sp) #endif
#ifdef _WIN32
-QString getExecutableForJarFile(const QString& jarFile)
-{
+QString getExecutableForJarFile(const QString &jarFile) {
const std::wstring jarFileW = jarFile.toStdWString();
WCHAR buffer[MAX_PATH];
const auto hinst = ::FindExecutableW(jarFileW.c_str(), nullptr, buffer);
- const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(hinst));
+ const auto r = static_cast<int>(reinterpret_cast<std::uintptr_t>(hinst));
// anything <= 32 signals failure
if (r <= 32) {
- log::warn("failed to find executable associated with file '{}', {}", jarFile,
- shell::formatError(r));
+ log::warn("failed to find executable associated with file '{}', {}",
+ jarFile, shell::formatError(r));
return {};
}
@@ -1050,8 +1068,7 @@ QString getExecutableForJarFile(const QString& jarFile) return QString::fromWCharArray(buffer);
}
-QString getJavaHome()
-{
+QString getJavaHome() {
const QString key =
"HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment";
const QString value = "CurrentVersion";
@@ -1064,11 +1081,12 @@ QString getJavaHome() }
const QString currentVersion = reg.value("CurrentVersion").toString();
- const QString javaHome = QString("%1/JavaHome").arg(currentVersion);
+ const QString javaHome = QString("%1/JavaHome").arg(currentVersion);
if (!reg.contains(javaHome)) {
- log::warn("java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist",
- currentVersion, key, value, key, javaHome);
+ log::warn(
+ "java version '{}' was found at '{}\\{}', but '{}\\{}' doesn't exist",
+ currentVersion, key, value, key, javaHome);
return {};
}
@@ -1076,10 +1094,9 @@ QString getJavaHome() const auto path = reg.value(javaHome).toString();
return path + "\\bin\\javaw.exe";
}
-#endif // _WIN32
+#endif // _WIN32
-QString findJavaInstallation(const QString& jarFile)
-{
+QString findJavaInstallation(const QString &jarFile) {
#ifdef _WIN32
// try to find java automatically based on the given jar file
if (!jarFile.isEmpty()) {
@@ -1108,8 +1125,7 @@ QString findJavaInstallation(const QString& jarFile) return {};
}
-bool isBatchFile(const QFileInfo& target)
-{
+bool isBatchFile(const QFileInfo &target) {
#ifdef _WIN32
const auto batchExtensions = {"cmd", "bat"};
#else
@@ -1117,7 +1133,7 @@ bool isBatchFile(const QFileInfo& target) #endif
const QString extension = target.suffix();
- for (auto&& e : batchExtensions) {
+ for (auto &&e : batchExtensions) {
if (extension.compare(e, Qt::CaseInsensitive) == 0) {
return true;
}
@@ -1126,8 +1142,7 @@ bool isBatchFile(const QFileInfo& target) return false;
}
-bool isExeFile(const QFileInfo& target)
-{
+bool isExeFile(const QFileInfo &target) {
#ifdef _WIN32
return (target.suffix().compare("exe", Qt::CaseInsensitive) == 0);
#else
@@ -1136,13 +1151,11 @@ bool isExeFile(const QFileInfo& target) #endif
}
-bool isJavaFile(const QFileInfo& target)
-{
+bool isJavaFile(const QFileInfo &target) {
return (target.suffix().compare("jar", Qt::CaseInsensitive) == 0);
}
-QFileInfo getCmdPath()
-{
+QFileInfo getCmdPath() {
#ifdef _WIN32
const auto p = env::get("COMSPEC");
if (!p.isEmpty()) {
@@ -1151,7 +1164,7 @@ QFileInfo getCmdPath() QString systemDirectory;
- const std::size_t buffer_size = 1000;
+ const std::size_t buffer_size = 1000;
wchar_t buffer[buffer_size + 1] = {};
const auto length = ::GetSystemDirectoryW(buffer, buffer_size);
@@ -1176,8 +1189,7 @@ QFileInfo getCmdPath() #endif
}
-FileExecutionTypes getFileExecutionType(const QFileInfo& target)
-{
+FileExecutionTypes getFileExecutionType(const QFileInfo &target) {
if (isExeFile(target) || isBatchFile(target) || isJavaFile(target)) {
return FileExecutionTypes::Executable;
}
@@ -1185,23 +1197,21 @@ FileExecutionTypes getFileExecutionType(const QFileInfo& target) return FileExecutionTypes::Other;
}
-FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& target)
-{
+FileExecutionContext getFileExecutionContext(QWidget *parent,
+ const QFileInfo &target) {
if (isExeFile(target)) {
return {target, "", FileExecutionTypes::Executable};
}
if (isBatchFile(target)) {
#ifdef _WIN32
- return {
- getCmdPath(),
- QString("/C \"%1\"").arg(QDir::toNativeSeparators(target.absoluteFilePath())),
- FileExecutionTypes::Executable};
+ return {getCmdPath(),
+ QString("/C \"%1\"")
+ .arg(QDir::toNativeSeparators(target.absoluteFilePath())),
+ FileExecutionTypes::Executable};
#else
- return {
- getCmdPath(),
- QString("\"%1\"").arg(target.absoluteFilePath()),
- FileExecutionTypes::Executable};
+ return {getCmdPath(), QString("\"%1\"").arg(target.absoluteFilePath()),
+ FileExecutionTypes::Executable};
#endif
}
@@ -1210,13 +1220,13 @@ FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& t if (java.isEmpty()) {
#ifdef _WIN32
- java =
- QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"), QString(),
- QObject::tr("Binary") + " (*.exe)");
+ java = QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"),
+ QString(),
+ QObject::tr("Binary") + " (*.exe)");
#else
- java =
- QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"), QString(),
- QObject::tr("Binary") + " (*)");
+ java = QFileDialog::getOpenFileName(parent, QObject::tr("Select binary"),
+ QString(),
+ QObject::tr("Binary") + " (*)");
#endif
}
@@ -1231,15 +1241,13 @@ FileExecutionContext getFileExecutionContext(QWidget* parent, const QFileInfo& t return {{}, {}, FileExecutionTypes::Other};
}
-} // namespace spawn
+} // namespace spawn
#ifdef _WIN32
-namespace helper
-{
+namespace helper {
-bool helperExec(QWidget* parent, const std::wstring& moDirectory,
- const std::wstring& commandLine, BOOL async)
-{
+bool helperExec(QWidget *parent, const std::wstring &moDirectory,
+ const std::wstring &commandLine, BOOL async) {
const std::wstring fileName = moDirectory + L"\\helper.exe";
env::HandlePtr process;
@@ -1251,14 +1259,14 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory, if (!async)
flags |= SEE_MASK_NOCLOSEPROCESS;
- execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
- execInfo.fMask = flags;
- execInfo.hwnd = 0;
- execInfo.lpVerb = L"runas";
- execInfo.lpFile = fileName.c_str();
+ execInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
+ execInfo.fMask = flags;
+ execInfo.hwnd = 0;
+ execInfo.lpVerb = L"runas";
+ execInfo.lpFile = fileName.c_str();
execInfo.lpParameters = commandLine.c_str();
- execInfo.lpDirectory = moDirectory.c_str();
- execInfo.nShow = SW_SHOW;
+ execInfo.lpDirectory = moDirectory.c_str();
+ execInfo.nShow = SW_SHOW;
if (!::ShellExecuteExW(&execInfo) && execInfo.hProcess == 0) {
const auto e = GetLastError();
@@ -1282,10 +1290,11 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory, // for WAIT_ABANDONED, the documentation doesn't mention that GetLastError()
// returns something meaningful, but code ERROR_ABANDONED_WAIT_0 exists, so
// use that instead
- const auto code = (r == WAIT_ABANDONED ? ERROR_ABANDONED_WAIT_0 : GetLastError());
+ const auto code =
+ (r == WAIT_ABANDONED ? ERROR_ABANDONED_WAIT_0 : GetLastError());
- spawn::dialogs::helperFailed(parent, code, "WaitForSingleObject()", fileName,
- moDirectory, commandLine);
+ spawn::dialogs::helperFailed(parent, code, "WaitForSingleObject()",
+ fileName, moDirectory, commandLine);
return false;
}
@@ -1303,22 +1312,21 @@ bool helperExec(QWidget* parent, const std::wstring& moDirectory, return (exitCode == 0);
}
-bool backdateBSAs(QWidget* parent, const std::wstring& moPath,
- const std::wstring& dataPath)
-{
+bool backdateBSAs(QWidget *parent, const std::wstring &moPath,
+ const std::wstring &dataPath) {
const std::wstring commandLine = std::format(L"backdateBSA \"{}\"", dataPath);
return helperExec(parent, moPath, commandLine, FALSE);
}
-bool adminLaunch(QWidget* parent, const std::wstring& moPath,
- const std::wstring& moFile, const std::wstring& workingDir)
-{
- const std::wstring commandLine = std::format(
- L"adminLaunch {} \"{}\" \"{}\"", ::GetCurrentProcessId(), moFile, workingDir);
+bool adminLaunch(QWidget *parent, const std::wstring &moPath,
+ const std::wstring &moFile, const std::wstring &workingDir) {
+ const std::wstring commandLine =
+ std::format(L"adminLaunch {} \"{}\" \"{}\"", ::GetCurrentProcessId(),
+ moFile, workingDir);
return helperExec(parent, moPath, commandLine, true);
}
-} // namespace helper
-#endif // _WIN32
+} // namespace helper
+#endif // _WIN32
diff --git a/src/src/wineprefix.cpp b/src/src/wineprefix.cpp index bb4e166..e144460 100644 --- a/src/src/wineprefix.cpp +++ b/src/src/wineprefix.cpp @@ -10,10 +10,17 @@ #include <log.h> #include <uibase/filesystemutilities.h> +#ifndef _WIN32 +#include <sys/stat.h> +#include <utime.h> +#endif + namespace { constexpr const char* BackupIniSuffix = ".mo2linux_backup"; +// Copy a file, preserving its modification time so that games see the +// original save timestamps rather than "now". bool copyFileWithParents(const QString& source, const QString& destination) { const QFileInfo destinationInfo(destination); @@ -25,7 +32,23 @@ bool copyFileWithParents(const QString& source, const QString& destination) return false; } - return QFile::copy(source, destination); + if (!QFile::copy(source, destination)) { + return false; + } + +#ifndef _WIN32 + // QFile::copy() does not preserve timestamps. Restore the source file's + // mtime so that games display the correct save date. + struct stat srcStat; + if (stat(source.toUtf8().constData(), &srcStat) == 0) { + struct utimbuf times; + times.actime = srcStat.st_atime; + times.modtime = srcStat.st_mtime; + utime(destination.toUtf8().constData(), ×); + } +#endif + + return true; } bool copyTreeContents(const QString& sourceRoot, const QString& destinationRoot) @@ -49,16 +72,22 @@ bool restoreBackedUpSaves(const QString& liveUpper, const QString& liveLower, const QString& backupUpper, const QString& backupLower) { if (QDir(liveUpper).exists() && !QDir(liveUpper).removeRecursively()) { + MOBase::log::warn("restoreBackedUpSaves: failed to remove '{}'", liveUpper); return false; } if (QDir(liveLower).exists() && !QDir(liveLower).removeRecursively()) { + MOBase::log::warn("restoreBackedUpSaves: failed to remove '{}'", liveLower); return false; } if (QDir(backupUpper).exists() && !QDir().rename(backupUpper, liveUpper)) { + MOBase::log::warn("restoreBackedUpSaves: failed to rename '{}' -> '{}'", + backupUpper, liveUpper); return false; } if (QDir(backupLower).exists() && !QDir().rename(backupLower, liveLower)) { + MOBase::log::warn("restoreBackedUpSaves: failed to rename '{}' -> '{}'", + backupLower, liveLower); return false; } @@ -296,16 +325,21 @@ bool WinePrefix::deployProfileSaves(const QString& profileSaveDir, if ((QDir(backupUpper).exists() || QDir(backupLower).exists()) && !restoreBackedUpSaves(absoluteSaveDir, lowerSaveDir, backupUpper, backupLower)) { + MOBase::log::warn("deployProfileSaves: failed to restore stale backup"); return false; } if (QDir(absoluteSaveDir).exists() && !QDir().rename(absoluteSaveDir, backupUpper)) { + MOBase::log::warn("deployProfileSaves: failed to backup '{}' -> '{}'", + absoluteSaveDir, backupUpper); return false; } if (absoluteSaveDir != lowerSaveDir && QDir(lowerSaveDir).exists() && !QDir().rename(lowerSaveDir, backupLower)) { + MOBase::log::warn("deployProfileSaves: failed to backup '{}' -> '{}'", + lowerSaveDir, backupLower); return false; } } |
