From 49c2f2537b4b1d4652a74328a5380e059d463cd6 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Tue, 28 Jul 2026 12:22:52 -0300 Subject: [PATCH] qml: stabilize pre-init datadir and settings resolution Keep the datadir selected in onboarding separate from the path Core resolves after config parsing. Run pre-init preview and apply through a scratch ArgsManager seeded from the original argv so live gArgs reaches InitConfig without preloaded config or settings state. Finalize onboarding settings after InitConfig and chain-specific QSettings setup. Apply reset across Core and GUI stores transactionally, preserve strDataDir, honor final source precedence, and recover unreadable settings with the Qt reset-or-abort behavior. Only open and synchronize settings stores that are used or modified, preserving rollback without allowing unrelated stores to block startup. Add unit and functional coverage for datadir precedence, reset behavior, recovery, persistence, and rollback. --- qml/bitcoin.cpp | 81 +- qml/datadir.cpp | 42 - qml/datadir.h | 1 - qml/legacy_settings_migration.cpp | 234 ++- qml/legacy_settings_migration.h | 13 +- qml/models/onboardingoptionsmodel.cpp | 27 +- qml/models/onboardingoptionsmodel.h | 6 +- qml/onboarding_settings.cpp | 754 +++++++-- qml/onboarding_settings.h | 45 +- test/functional/qml_test_resetguisettings.py | 375 ++++- test/test_options_model.cpp | 1466 +++++++++++++++--- 11 files changed, 2631 insertions(+), 413 deletions(-) diff --git a/qml/bitcoin.cpp b/qml/bitcoin.cpp index c02b985c4a..c94cd80824 100644 --- a/qml/bitcoin.cpp +++ b/qml/bitcoin.cpp @@ -69,6 +69,7 @@ #endif #include #include +#include #include #include #ifdef ENABLE_WALLET @@ -77,6 +78,7 @@ #include #include +#include #include #include @@ -89,6 +91,7 @@ #include #include #include +#include #include #include #include @@ -296,8 +299,35 @@ enum class PreInitOnboardingStatus { FAILED, }; +bool ErrorSettingsRead(const bilingual_str& error, const std::vector& details) +{ + if (gArgs.GetBoolArg("-resetguisettings", false)) { + return false; + } + + QMessageBox message_box{ + QMessageBox::Critical, + CLIENT_NAME, + QString::fromStdString(strprintf("%s.", error.translated)), + QMessageBox::Reset | QMessageBox::Abort, + }; + message_box.setInformativeText(QObject::tr("Do you want to reset settings to default values, or to abort without making changes?")); + message_box.setDetailedText(QString::fromStdString(util::MakeUnorderedList(details))); + message_box.setTextFormat(Qt::PlainText); + message_box.setDefaultButton(QMessageBox::Reset); + switch (message_box.exec()) { + case QMessageBox::Reset: + return false; + case QMessageBox::Abort: + return true; + default: + assert(false); + } +} + struct PreInitOnboardingContext { std::unique_ptr onboarding_options_model; + std::optional pending_apply; QScopedPointer network_style; std::unique_ptr engine; #ifdef ENABLE_TEST_AUTOMATION @@ -324,6 +354,9 @@ bool ShouldShowPreInitOnboarding(const std::vector& argv, bool can_ const QmlOnboardingSettings::OnboardingStartupStatus status{ QmlOnboardingSettings::ResolveOnboardingStartupStatus(argv, can_listen_ipc) }; + if (status.settings_file_unreadable) { + return false; + } return !status.ok || status.should_show_onboarding; } @@ -380,11 +413,13 @@ PreInitOnboardingStatus RunPreInitOnboarding(PreInitOnboardingContext& context, } QString error; - if (!context.onboarding_options_model->applyToArgs(gArgs, &error)) { + QmlOnboardingSettings::PendingApply pending_apply; + if (!context.onboarding_options_model->prepareApplyToArgs(gArgs, pending_apply, &error)) { InitError(Untranslated(error.toStdString())); context.close(); return PreInitOnboardingStatus::FAILED; } + context.pending_apply = std::move(pending_apply); return PreInitOnboardingStatus::COMPLETED; } } // namespace @@ -465,13 +500,9 @@ int QmlGuiMain(int argc, char* argv[]) app.setQuitOnLastWindowClosed(false); setupChainQSettings(&app, QString::fromStdString(gArgs.GetChainTypeString()).toUpper()); - if (gArgs.GetBoolArg("-resetguisettings", false)) { - QString reset_error; - if (!QmlDataDir::ResetGuiSettings(gArgs, &reset_error)) { - InitError(Untranslated(reset_error.toStdString())); - return EXIT_FAILURE; - } - } + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; LoadFontResource(":/fonts/bitcoincoresans/regular"); LoadFontResource(":/fonts/bitcoincoresans/semibold"); @@ -512,26 +543,21 @@ int QmlGuiMain(int argc, char* argv[]) if (auto error = common::InitConfig( gArgs, - [](const bilingual_str& msg, const std::vector& details) { - return InitError(msg, details); - })) { + ErrorSettingsRead)) { return EXIT_FAILURE; } - const QmlLegacySettings::MigrationResult legacy_migration{ - QmlLegacySettings::MigrateCoreSettings(gArgs, QmlLegacySettings::MigrationMode::Persist) - }; - if (!legacy_migration.error.isEmpty()) { - InitError(Untranslated(legacy_migration.error.toStdString())); + setupChainQSettings(&app, QString::fromStdString(gArgs.GetChainTypeString()).toUpper()); + QString finalize_settings_error; + if (!QmlOnboardingSettings::FinalizeStartupSettings( + gArgs, + bootstrap_gui_settings, + pre_init_onboarding_context.pending_apply ? &*pre_init_onboarding_context.pending_apply : nullptr, + /*result=*/nullptr, + &finalize_settings_error)) { + InitError(Untranslated(finalize_settings_error.toStdString())); return EXIT_FAILURE; } - if (legacy_migration.settings_changed) { - std::vector settings_errors; - if (!gArgs.WriteSettingsFile(&settings_errors)) { - InitError(_("Settings file could not be written"), settings_errors); - return EXIT_FAILURE; - } - } // legacy GUI: parameterSetup() // Default printtoconsole to false for the GUI. GUI programs should not @@ -600,15 +626,6 @@ int QmlGuiMain(int argc, char* argv[]) ChainModel chain_model{*chain}; chain_model.setCurrentNetworkName(QString::fromStdString(gArgs.GetChainTypeString())); - setupChainQSettings(&app, chain_model.currentNetworkName()); - // Settings reset must happen before model instantiation so the models - // read clean defaults from QSettings. - if (gArgs.IsArgSet("-resetguisettings")) { - QSettings settings; - settings.remove(QStringLiteral("fHideTrayIcon")); - settings.remove(QStringLiteral("fMinimizeToTray")); - settings.remove(QStringLiteral("fMinimizeOnClose")); - } QObject::connect(&node_model, &NodeModel::setTimeRatioList, &chain_model, &ChainModel::setTimeRatioList); QObject::connect(&node_model, &NodeModel::setTimeRatioListInitial, &chain_model, &ChainModel::setTimeRatioListInitial); diff --git a/qml/datadir.cpp b/qml/datadir.cpp index c47e72edeb..76b3fe73dd 100644 --- a/qml/datadir.cpp +++ b/qml/datadir.cpp @@ -225,48 +225,6 @@ void PersistDefaultDataDirSelection() QmlLegacySettings::ClearLegacyGuiSettings(QString::fromStdString(Params().GetChainTypeString())); } -bool ResetGuiSettings(ArgsManager& args, QString* error) -{ - if (error) error->clear(); - - QSettings settings; - settings.clear(); - settings.setValue(RESET_GUI_SETTINGS_KEY, false); - - try { - SelectParams(args.GetChainType()); - args.SelectConfigNetwork(args.GetChainTypeString()); - QmlLegacySettings::ClearLegacyGuiSettings(QString::fromStdString(args.GetChainTypeString())); - } catch (const std::exception& e) { - if (error) *error = QString::fromStdString(e.what()); - return false; - } - - fs::path settings_path; - if (!args.GetSettingsPath(&settings_path) || !fs::exists(settings_path)) { - return true; - } - - std::vector settings_errors; - if (!args.ReadSettingsFile(&settings_errors)) { - if (error) *error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file could not be read."} : settings_errors.front()); - return false; - } - if (!args.WriteSettingsFile(&settings_errors, /*backup=*/true)) { - if (error) *error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file backup could not be written."} : settings_errors.front()); - return false; - } - args.LockSettings([](common::Settings& settings) { - settings.rw_settings.clear(); - }); - settings_errors.clear(); - if (!args.WriteSettingsFile(&settings_errors)) { - if (error) *error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file could not be written."} : settings_errors.front()); - return false; - } - return true; -} - bool HasExplicitDataDirArg(const ArgsManager& args) { return args.IsArgSet("-datadir") && !args.GetPathArg("-datadir").empty(); diff --git a/qml/datadir.h b/qml/datadir.h index d46053952e..9626032f8c 100644 --- a/qml/datadir.h +++ b/qml/datadir.h @@ -49,7 +49,6 @@ StorageSpaceResult CheckStorageSpace(const QString& path); bool EnsureDataDir(const QString& path, QString* error = nullptr); bool PersistGuiDataDirSelection(const QString& path, QString* error = nullptr); void PersistDefaultDataDirSelection(); -bool ResetGuiSettings(ArgsManager& args, QString* error = nullptr); bool HasExplicitDataDirArg(const ArgsManager& args); bool ShouldShowDataDirChooser(const ArgsManager& args); diff --git a/qml/legacy_settings_migration.cpp b/qml/legacy_settings_migration.cpp index 1794c92824..42311bfc4e 100644 --- a/qml/legacy_settings_migration.cpp +++ b/qml/legacy_settings_migration.cpp @@ -28,10 +28,13 @@ #include #include #include +#include #include #include +#include #include +#include #include #include #include @@ -95,7 +98,9 @@ void RegisterLegacyBitcoinUnitMetaType() std::unique_ptr OpenSettings(const QString& org, const QString& app) { - return std::make_unique(QSettings::defaultFormat(), QSettings::UserScope, org, app); + auto settings = std::make_unique(QSettings::defaultFormat(), QSettings::UserScope, org, app); + settings->setFallbacksEnabled(false); + return settings; } std::unique_ptr OpenLegacyDataDirSettings() @@ -105,23 +110,163 @@ std::unique_ptr OpenLegacyDataDirSettings() struct SettingsStore { bool legacy_qt{false}; + bool dirty{false}; + QString organization; + QString application; std::unique_ptr settings; }; +SettingsStore MakeSettingsStore(bool legacy_qt, const QString& organization, const QString& application) +{ + return { + legacy_qt, + false, + organization, + application, + OpenSettings(organization, application), + }; +} + std::vector CoreSettingsStores(const QString& chain) { std::vector stores; - stores.push_back({ + stores.push_back(MakeSettingsStore( /*legacy_qt=*/false, - OpenSettings(QStringLiteral(QAPP_ORG_NAME), AppNameForChain(chain, /*legacy_qt=*/false)), - }); - stores.push_back({ + QStringLiteral(QAPP_ORG_NAME), + AppNameForChain(chain, /*legacy_qt=*/false))); + stores.push_back(MakeSettingsStore( /*legacy_qt=*/true, - OpenSettings(QString::fromUtf8(QT_ORG_NAME), AppNameForChain(chain, /*legacy_qt=*/true)), - }); + QString::fromUtf8(QT_ORG_NAME), + AppNameForChain(chain, /*legacy_qt=*/true))); return stores; } +SettingsStore& FindOrAddStore( + std::vector& stores, + bool legacy_qt, + const QString& organization, + const QString& application) +{ + for (SettingsStore& store : stores) { + if (store.organization == organization && store.application == application) { + return store; + } + } + stores.push_back(MakeSettingsStore(legacy_qt, organization, application)); + return stores.back(); +} + +SettingsStore& LegacyDataDirStore(std::vector& stores) +{ + return FindOrAddStore( + stores, + /*legacy_qt=*/true, + QString::fromUtf8(QT_ORG_NAME), + QString::fromUtf8(QT_APP_NAME_DEFAULT)); +} + +QVariantMap SnapshotSettings(const QSettings& settings) +{ + QVariantMap values; + for (const QString& key : settings.allKeys()) { + values.insert(key, settings.value(key)); + } + return values; +} + +std::vector SnapshotStores(const std::vector& stores) +{ + std::vector snapshots; + snapshots.reserve(stores.size()); + for (const SettingsStore& store : stores) { + snapshots.push_back(SnapshotSettings(*store.settings)); + } + return snapshots; +} + +void MarkChangedStores( + std::vector& stores, + const std::vector& snapshots) +{ + for (size_t i = 0; i < stores.size(); ++i) { + stores[i].dirty = SnapshotSettings(*stores[i].settings) != snapshots[i]; + } +} + +void ReplaceSettings(QSettings& settings, const QVariantMap& values) +{ + settings.clear(); + for (auto it = values.cbegin(); it != values.cend(); ++it) { + settings.setValue(it.key(), it.value()); + } +} + +bool RestoreStores( + std::vector& stores, + const std::vector& snapshots, + QString* error) +{ + for (SettingsStore& store : stores) { + if (store.dirty) store.settings.reset(); + } + + bool restored{true}; + for (size_t i = 0; i < stores.size(); ++i) { + SettingsStore& store{stores[i]}; + if (!store.dirty) continue; + store.settings = OpenSettings(store.organization, store.application); + ReplaceSettings(*store.settings, snapshots[i]); + store.settings->sync(); + if (store.settings->status() != QSettings::NoError) { + restored = false; + if (error) { + *error += QStringLiteral(" Legacy settings rollback failed for %1.") + .arg(store.settings->fileName()); + } + } + } + return restored; +} + +bool CommitStores( + std::vector& stores, + const std::vector& snapshots, + const QString& action, + QString* error) +{ + for (SettingsStore& store : stores) { + if (!store.dirty) continue; + store.settings->sync(); + if (store.settings->status() == QSettings::NoError) continue; + + if (error) { + *error = QStringLiteral("%1 failed for %2.") + .arg(action, store.settings->fileName()); + } + RestoreStores(stores, snapshots, error); + return false; + } + return true; +} + +std::map SnapshotRwSettings(ArgsManager& args) +{ + std::map values; + args.LockSettings([&](common::Settings& settings) { + values = settings.rw_settings; + }); + return values; +} + +void RestoreRwSettings( + ArgsManager& args, + const std::map& values) +{ + args.LockSettings([&](common::Settings& settings) { + settings.rw_settings = values; + }); +} + const QStringList& LegacyCoreKeys(bool include_language) { static const QStringList keys_with_language{ @@ -377,11 +522,18 @@ bool MigrateStore(ArgsManager& args, SettingsStore& store, QmlLegacySettings::Mi }); } - if (mode == QmlLegacySettings::MigrationMode::Persist) { - settings.sync(); - } return changed; } + +void ApplyGuiCleanup(QSettings& settings, QmlLegacySettings::GuiCleanup cleanup) +{ + if (cleanup == QmlLegacySettings::GuiCleanup::DataDirAndReset) { + settings.remove(SettingsKeys::DATA_DIR); + } + if (cleanup != QmlLegacySettings::GuiCleanup::None) { + settings.remove(QString::fromUtf8(RESET_GUI_SETTINGS_KEY)); + } +} } // namespace namespace QmlLegacySettings { @@ -425,35 +577,75 @@ QString ReadLegacyGuiLanguage(const QString& chain) return default_settings->value(SettingsKeys::LANGUAGE).toString(); } -void ClearLegacyGuiSettings(const QString& chain) +bool ClearLegacyGuiSettings(const QString& chain, QString* error) { - for (SettingsStore& store : CoreSettingsStores(chain)) { + std::vector stores{CoreSettingsStores(chain)}; + const size_t core_store_count{stores.size()}; + SettingsStore& data_dir_store{LegacyDataDirStore(stores)}; + const std::vector snapshots{SnapshotStores(stores)}; + + for (size_t i = 0; i < core_store_count; ++i) { + SettingsStore& store{stores[i]}; for (const QString& key : LegacyCoreKeys(store.legacy_qt)) { store.settings->remove(key); } store.settings->remove(SettingsKeys::DISPLAY_UNIT); - store.settings->sync(); } + ApplyGuiCleanup(*data_dir_store.settings, GuiCleanup::DataDirAndReset); + MarkChangedStores(stores, snapshots); - const std::unique_ptr legacy_default_settings = OpenLegacyDataDirSettings(); - legacy_default_settings->remove(SettingsKeys::DATA_DIR); - legacy_default_settings->remove(QString::fromUtf8(RESET_GUI_SETTINGS_KEY)); - legacy_default_settings->sync(); + return CommitStores(stores, snapshots, QStringLiteral("Legacy GUI settings cleanup"), error); } -MigrationResult MigrateCoreSettings(ArgsManager& args, MigrationMode mode) +MigrationResult MigrateCoreSettings(ArgsManager& args, MigrationMode mode, GuiCleanup cleanup) { MigrationResult result; - if (!args.GetSettingsPath()) { + const bool settings_enabled{args.GetSettingsPath()}; + if (!settings_enabled && (mode == MigrationMode::Preview || cleanup == GuiCleanup::None)) { return result; } + std::vector stores; + if (settings_enabled) { + stores = CoreSettingsStores(QString::fromStdString(args.GetChainTypeString())); + } + const size_t core_store_count{stores.size()}; + SettingsStore* data_dir_store{nullptr}; + if (mode == MigrationMode::Persist && cleanup != GuiCleanup::None) { + data_dir_store = &LegacyDataDirStore(stores); + } + + std::vector snapshots; + std::map original_rw_settings; + if (mode == MigrationMode::Persist) { + snapshots = SnapshotStores(stores); + original_rw_settings = SnapshotRwSettings(args); + } + try { - for (SettingsStore& store : CoreSettingsStores(QString::fromStdString(args.GetChainTypeString()))) { - result.settings_changed |= MigrateStore(args, store, mode); + for (size_t i = 0; i < core_store_count; ++i) { + result.settings_changed |= MigrateStore(args, stores[i], mode); + } + if (data_dir_store) { + ApplyGuiCleanup(*data_dir_store->settings, cleanup); + } + if (mode == MigrationMode::Persist) { + MarkChangedStores(stores, snapshots); + } + if (mode == MigrationMode::Persist && + !CommitStores( + stores, + snapshots, + QStringLiteral("Legacy GUI settings migration"), + &result.error)) { + RestoreRwSettings(args, original_rw_settings); } } catch (const std::exception& e) { result.error = QString::fromStdString(e.what()); + if (mode == MigrationMode::Persist) { + RestoreStores(stores, snapshots, &result.error); + RestoreRwSettings(args, original_rw_settings); + } } return result; } diff --git a/qml/legacy_settings_migration.h b/qml/legacy_settings_migration.h index 0578526791..f1f137b6f3 100644 --- a/qml/legacy_settings_migration.h +++ b/qml/legacy_settings_migration.h @@ -16,6 +16,12 @@ enum class MigrationMode { Persist, }; +enum class GuiCleanup { + None, + ResetOnly, + DataDirAndReset, +}; + struct MigrationResult { bool settings_changed{false}; QString error; @@ -25,8 +31,11 @@ QString ReadLegacyGuiDataDir(); bool ReadLegacyGuiReset(); int ReadLegacyGuiDisplayUnit(const QString& chain, int fallback); QString ReadLegacyGuiLanguage(const QString& chain); -void ClearLegacyGuiSettings(const QString& chain); -MigrationResult MigrateCoreSettings(ArgsManager& args, MigrationMode mode); +bool ClearLegacyGuiSettings(const QString& chain, QString* error = nullptr); +MigrationResult MigrateCoreSettings( + ArgsManager& args, + MigrationMode mode, + GuiCleanup cleanup = GuiCleanup::None); } // namespace QmlLegacySettings diff --git a/qml/models/onboardingoptionsmodel.cpp b/qml/models/onboardingoptionsmodel.cpp index e1ae57dfa2..e308540a6f 100644 --- a/qml/models/onboardingoptionsmodel.cpp +++ b/qml/models/onboardingoptionsmodel.cpp @@ -30,10 +30,12 @@ OnboardingOptionsModel::OnboardingOptionsModel(std::vector argv, bo , m_argv{std::move(argv)} , m_can_listen_ipc{can_listen_ipc} , m_data_dir{QmlDataDir::DefaultDataDirString()} + , m_resolved_data_dir{QmlDataDir::DefaultDataDirString()} { const QmlOnboardingSettings::OnboardingStartupStatus status{InitialStartupStatus(m_argv, m_can_listen_ipc)}; - m_data_dir = status.active_data_dir.isEmpty() ? QmlDataDir::ReadGuiDataDir() : status.active_data_dir; - m_data_dir_source = status.data_dir_source; + m_data_dir = status.selected_data_dir.isEmpty() ? QmlDataDir::ReadGuiDataDir() : status.selected_data_dir; + m_resolved_data_dir = status.resolved_data_dir.isEmpty() ? m_data_dir : status.resolved_data_dir; + m_data_dir_source = status.selected_data_dir_source; m_core_settings.setAfterChangeHandler([this](const QmlCoreSettings::Change& change, CoreSettingsModel::ChangeOrigin origin) { QmlCoreSettings::EmitCoreSettingSignals(*this, change); @@ -243,7 +245,7 @@ bool OnboardingOptionsModel::storageEnoughForFull() const void OnboardingOptionsModel::requestStorageCheck() { ++m_storage_request_id; - m_storage_check_path = m_data_dir; + m_storage_check_path = m_resolved_data_dir.isEmpty() ? m_data_dir : m_resolved_data_dir; m_storage_check_pending = true; emitStorageStatusChanged(); if (!m_storage_check_in_flight) { @@ -358,6 +360,10 @@ void OnboardingOptionsModel::refreshPreview() const int old_assumed_blockchain_size = m_assumed_blockchain_size; const int old_assumed_chainstate_size = m_assumed_chainstate_size; const bool old_existing_profile = m_profile.existing_profile; + m_resolved_data_dir = preview.resolved_data_dir.isEmpty() ? m_data_dir : preview.resolved_data_dir; + m_resolved_chain = preview.resolved_chain; + m_resolved_settings_path = preview.resolved_settings_path; + m_effective_reset = preview.effective_reset; m_assumed_blockchain_size = preview.assumed_blockchain_size; m_assumed_chainstate_size = preview.assumed_chainstate_size; m_profile = preview.profile; @@ -378,12 +384,21 @@ void OnboardingOptionsModel::refreshPreview() requestStorageCheck(); } -bool OnboardingOptionsModel::applyToArgs(ArgsManager& args, QString* error) const +bool OnboardingOptionsModel::prepareApplyToArgs(ArgsManager& args, QmlOnboardingSettings::PendingApply& pending, QString* error) const { - return QmlOnboardingSettings::ApplyToArgs( + const bool prepared{QmlOnboardingSettings::PrepareApplyToArgs( args, QmlOnboardingSettings::DataDirSelection{m_data_dir, m_data_dir_source}, + m_resolved_data_dir, m_core_settings.touchedSettings(), coreValues(), - error); + m_effective_reset, + pending, + error)}; + if (prepared) { + pending.resolved_chain = m_resolved_chain; + pending.resolved_settings_path = m_resolved_settings_path; + pending.target_complete = true; + } + return prepared; } diff --git a/qml/models/onboardingoptionsmodel.h b/qml/models/onboardingoptionsmodel.h index f6338f1c7b..0e6ad8e6bc 100644 --- a/qml/models/onboardingoptionsmodel.h +++ b/qml/models/onboardingoptionsmodel.h @@ -125,7 +125,7 @@ class OnboardingOptionsModel : public QObject Q_INVOKABLE bool commitTorLocation(const QString& location); Q_INVOKABLE QString defaultProxyAddress() const; - bool applyToArgs(ArgsManager& args, QString* error = nullptr) const; + bool prepareApplyToArgs(ArgsManager& args, QmlOnboardingSettings::PendingApply& pending, QString* error = nullptr) const; Q_SIGNALS: void customDataDirStringChanged(QString path); @@ -162,6 +162,10 @@ class OnboardingOptionsModel : public QObject std::vector m_argv; bool m_can_listen_ipc; QString m_data_dir; + QString m_resolved_data_dir; + QString m_resolved_chain; + QString m_resolved_settings_path; + bool m_effective_reset{false}; QmlOnboardingSettings::DataDirSource m_data_dir_source{QmlOnboardingSettings::DataDirSource::Default}; QString m_custom_datadir_string; CoreSettingsModel m_core_settings; diff --git a/qml/onboarding_settings.cpp b/qml/onboarding_settings.cpp index 34b3f54d85..6209b9cf27 100644 --- a/qml/onboarding_settings.cpp +++ b/qml/onboarding_settings.cpp @@ -15,15 +15,21 @@ #include #include #include +#include #include +#include #include #include #include #include +#include #include +#include +#include #include +#include #include #include #include @@ -63,6 +69,16 @@ QString NormalizedDataDirPath(const QmlOnboardingSettings::DataDirSelection& sel return normalized.isEmpty() ? QmlDataDir::DefaultDataDirString() : normalized; } +QString ComparableDataDirPath(const QString& path) +{ + return QDir::cleanPath(QmlDataDir::NormalizeLocalPath(path)); +} + +bool SameDataDirPath(const QString& a, const QString& b) +{ + return ComparableDataDirPath(a) == ComparableDataDirPath(b); +} + bool ShouldApplyDataDirBeforeConfig(QmlOnboardingSettings::DataDirSource source, bool explicit_datadir_arg, const QString& data_dir) { return !explicit_datadir_arg && @@ -87,6 +103,74 @@ QString ActiveDataDirString(const ArgsManager& args) return QmlDataDir::NormalizeLocalPath(QString::fromStdString(fs::PathToString(data_dir))); } +QmlOnboardingSettings::DataDirSource ResolvedDataDirSource( + QmlOnboardingSettings::DataDirSource selected_source, + const QString& selected_data_dir, + const QString& resolved_data_dir, + bool explicit_datadir_arg) +{ + if (explicit_datadir_arg) return QmlOnboardingSettings::DataDirSource::ExplicitArg; + if (!SameDataDirPath(selected_data_dir, resolved_data_dir)) return QmlOnboardingSettings::DataDirSource::Config; + return selected_source; +} + +bool ShouldDisplayResolvedConfigDataDir( + QmlOnboardingSettings::DataDirSource selected_source, + const QString& selected_data_dir, + const QString& resolved_data_dir, + bool explicit_datadir_arg) +{ + return !explicit_datadir_arg && + selected_source == QmlOnboardingSettings::DataDirSource::Default && + !QmlDataDir::IsDefaultDataDir(resolved_data_dir) && + !SameDataDirPath(selected_data_dir, resolved_data_dir); +} + +void SetStartupDataDirs( + QmlOnboardingSettings::OnboardingStartupStatus& status, + QString selected_data_dir, + QmlOnboardingSettings::DataDirSource selected_source, + QString resolved_data_dir, + bool explicit_datadir_arg) +{ + if (selected_data_dir.isEmpty()) selected_data_dir = QmlDataDir::DefaultDataDirString(); + if (resolved_data_dir.isEmpty()) resolved_data_dir = selected_data_dir; + + const bool display_resolved_config_data_dir{ + ShouldDisplayResolvedConfigDataDir(selected_source, selected_data_dir, resolved_data_dir, explicit_datadir_arg) + }; + status.selected_data_dir = display_resolved_config_data_dir ? resolved_data_dir : selected_data_dir; + status.selected_data_dir_source = display_resolved_config_data_dir ? QmlOnboardingSettings::DataDirSource::Config : selected_source; + status.resolved_data_dir = resolved_data_dir; + status.resolved_data_dir_source = ResolvedDataDirSource(selected_source, selected_data_dir, resolved_data_dir, explicit_datadir_arg); + status.config_redirected_data_dir = !SameDataDirPath(selected_data_dir, resolved_data_dir); + + // Compatibility fields keep existing callers working while new callers can + // distinguish the editable selection from the resolved Core datadir. + status.active_data_dir = status.resolved_data_dir; + status.data_dir_source = status.resolved_data_dir_source; +} + +void SetPreviewDataDirs( + QmlOnboardingSettings::PreviewResult& result, + QString selected_data_dir, + QmlOnboardingSettings::DataDirSource selected_source, + QString resolved_data_dir, + bool explicit_datadir_arg) +{ + if (selected_data_dir.isEmpty()) selected_data_dir = QmlDataDir::DefaultDataDirString(); + if (resolved_data_dir.isEmpty()) resolved_data_dir = selected_data_dir; + + const bool display_resolved_config_data_dir{ + ShouldDisplayResolvedConfigDataDir(selected_source, selected_data_dir, resolved_data_dir, explicit_datadir_arg) + }; + result.selected_data_dir = display_resolved_config_data_dir ? resolved_data_dir : selected_data_dir; + result.selected_data_dir_source = display_resolved_config_data_dir ? QmlOnboardingSettings::DataDirSource::Config : selected_source; + result.resolved_data_dir = resolved_data_dir; + result.resolved_data_dir_source = ResolvedDataDirSource(selected_source, selected_data_dir, resolved_data_dir, explicit_datadir_arg); + result.config_redirected_data_dir = !SameDataDirPath(selected_data_dir, resolved_data_dir); +} + bool ReadConfigAndSelectNetwork(ArgsManager& args, QString* error) { std::string config_error; @@ -105,16 +189,37 @@ bool ReadConfigAndSelectNetwork(ArgsManager& args, QString* error) return true; } -bool ReadSettingsFileIfPresent(ArgsManager& args, QString* error) +struct ReadOnlyProfileResult { + bool settings_enabled{true}; + bool config_file_path_available{false}; + bool settings_file_unreadable{false}; +}; + +bool ReadResolvedProfile(ArgsManager& args, ReadOnlyProfileResult& result, QString* error) { + result = {}; + if (!ReadConfigAndSelectNetwork(args, error)) return false; + result.config_file_path_available = true; + fs::path settings_path; - if (!args.GetSettingsPath(&settings_path) || !fs::exists(settings_path)) { + if (!args.GetSettingsPath(&settings_path)) { + result.settings_enabled = false; if (error) error->clear(); return true; } + const bool reset_without_settings{ + args.GetBoolArg("-resetguisettings", false) + }; std::vector settings_errors; if (!args.ReadSettingsFile(&settings_errors)) { + // Preserve settings.json precedence when it is readable, while still + // allowing an already-selected reset to recover an unreadable file. + if (reset_without_settings) { + if (error) error->clear(); + return true; + } + result.settings_file_unreadable = true; if (error) *error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file could not be read."} : settings_errors.front()); return false; } @@ -251,6 +356,305 @@ bool WriteSettingsFile(ArgsManager& args, QString* error) return true; } +bool BackupSettingsFile(ArgsManager& args, QString* error) +{ + fs::path settings_path; + if (!args.GetSettingsPath(&settings_path)) { + if (error) error->clear(); + return true; + } + + std::vector settings_errors; + if (!args.WriteSettingsFile(&settings_errors, /*backup=*/true)) { + if (error) *error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file backup could not be written."} : settings_errors.front()); + return false; + } + if (error) error->clear(); + return true; +} + +std::unique_ptr OpenGuiSettings(const QmlOnboardingSettings::GuiSettingsStore& store) +{ + std::unique_ptr settings; + if (!store.organization_name.isEmpty() && !store.application_name.isEmpty()) { + settings = std::make_unique( + store.format, + store.scope, + store.organization_name, + store.application_name); + } else { + settings = std::make_unique(store.file_name, store.format); + } + settings->setFallbacksEnabled(false); + return settings; +} + +QString GuiApplicationNameForChain(const QString& chain) +{ + const QString normalized{chain.toLower()}; + if (normalized == QStringLiteral("test")) return QStringLiteral(QAPP_APP_NAME_TESTNET); + if (normalized == QStringLiteral("testnet4")) return QStringLiteral(QAPP_APP_NAME_TESTNET4); + if (normalized == QStringLiteral("signet")) return QStringLiteral(QAPP_APP_NAME_SIGNET); + if (normalized == QStringLiteral("regtest")) return QStringLiteral(QAPP_APP_NAME_REGTEST); + return QStringLiteral(QAPP_APP_NAME_DEFAULT); +} + +bool ReadResolvedGuiReset(const ArgsManager& args) +{ + QmlOnboardingSettings::GuiSettingsStore store{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + store.application_name = GuiApplicationNameForChain( + QString::fromStdString(args.GetChainTypeString())); + const std::unique_ptr settings{OpenGuiSettings(store)}; + return settings->value(QStringLiteral("fReset"), false).toBool(); +} + +bool SameGuiSettingsStore( + const QmlOnboardingSettings::GuiSettingsStore& left, + const QmlOnboardingSettings::GuiSettingsStore& right) +{ + return left.file_name == right.file_name && left.format == right.format; +} + +QVariantMap SnapshotGuiSettings(const QSettings& settings) +{ + QVariantMap values; + for (const QString& key : settings.allKeys()) { + values.insert(key, settings.value(key)); + } + return values; +} + +bool SyncGuiSettings(QSettings& settings, const QString& action, QString* error) +{ + settings.sync(); + if (settings.status() == QSettings::NoError) return true; + if (error) *error = QStringLiteral("%1 failed for %2.").arg(action, settings.fileName()); + return false; +} + +bool WriteGuiSettings(QSettings& settings, const QVariantMap& values, const QString& action, QString* error) +{ + settings.clear(); + for (auto it = values.cbegin(); it != values.cend(); ++it) { + settings.setValue(it.key(), it.value()); + } + return SyncGuiSettings(settings, action, error); +} + +void RestoreGuiSettings( + const QmlOnboardingSettings::GuiSettingsStore& store, + const QVariantMap& values, + QString* error) +{ + std::unique_ptr settings{OpenGuiSettings(store)}; + QString rollback_error; + if (!WriteGuiSettings(*settings, values, QStringLiteral("GUI settings rollback"), &rollback_error) && error) { + *error += QStringLiteral(" %1").arg(rollback_error); + } +} + +bool BackupGuiSettings(QSettings& source, const fs::path& backup_path, QString* error) +{ + QSettings backup{ + QString::fromStdString(fs::PathToString(backup_path)), + QSettings::IniFormat, + }; + backup.setFallbacksEnabled(false); + return WriteGuiSettings( + backup, + SnapshotGuiSettings(source), + QStringLiteral("GUI settings backup"), + error); +} + +std::map SnapshotRwSettings(ArgsManager& args) +{ + std::map values; + args.LockSettings([&](common::Settings& settings) { + values = settings.rw_settings; + }); + return values; +} + +bool RwSettingsEqual( + const std::map& left, + const std::map& right) +{ + if (left.size() != right.size()) return false; + auto left_it = left.cbegin(); + auto right_it = right.cbegin(); + for (; left_it != left.cend(); ++left_it, ++right_it) { + if (left_it->first != right_it->first || + left_it->second.write() != right_it->second.write()) { + return false; + } + } + return true; +} + +void RestoreRwSettings(ArgsManager& args, const std::map& values) +{ + args.LockSettings([&](common::Settings& settings) { + settings.rw_settings = values; + }); +} + +bool RollBackSettingsFile(ArgsManager& args, const std::map& values, QString* error) +{ + RestoreRwSettings(args, values); + QString rollback_error; + if (WriteSettingsFile(args, &rollback_error)) return true; + if (error) { + *error += QStringLiteral(" Settings rollback failed: %1").arg(rollback_error); + } + return false; +} + +void RestoreForcedSettings( + ArgsManager& args, + const std::map& forced_settings) +{ + args.LockSettings([&](common::Settings& settings) { + settings.forced_settings = forced_settings; + }); +} + +bool ApplyPendingCoreSettings(ArgsManager& args, const QmlOnboardingSettings::PendingApply& pending, QString* error) +{ + std::map original_forced_settings; + args.LockSettings([&](common::Settings& settings) { + original_forced_settings = settings.forced_settings; + }); + + QmlCoreSettings::Session core_settings{ + pending.values, + QmlCoreSettings::BuildCoreSettingStatuses(args, QmlCoreSettings::OnboardingCoreSettingNames()), + }; + const auto write_setting = [&](const QString& name) { + return !pending.touched_settings.contains(name) || core_settings.writeToArgs(args, name); + }; + const auto recompute_interactions = [&] { + RestoreForcedSettings(args, original_forced_settings); + InitParameterInteraction(args); + }; + + bool settings_written{true}; + for (const QString& name : { + QStringLiteral("proxy"), + QStringLiteral("onion"), + QStringLiteral("server"), + QStringLiteral("prune"), + }) { + settings_written = write_setting(name) && settings_written; + } + recompute_interactions(); + settings_written = write_setting(QStringLiteral("listen")) && settings_written; + recompute_interactions(); + settings_written = write_setting(QStringLiteral("natpmp")) && settings_written; + RestoreForcedSettings(args, original_forced_settings); + + if (!settings_written) { + if (error) *error = QStringLiteral("One or more startup settings could not be written."); + return false; + } + + QmlCoreSettings::SetRwSetting(args, QString::fromLatin1(QML_ONBOARDED_KEY), common::SettingsValue{true}); + return true; +} + +bool SameOptionalPath(const QString& left, const QString& right) +{ + if (left.isEmpty() || right.isEmpty()) return left.isEmpty() && right.isEmpty(); + return SameDataDirPath(left, right); +} + +QString SettingsPathString(ArgsManager& args) +{ + fs::path settings_path; + if (!args.GetSettingsPath(&settings_path)) return {}; + return QmlDataDir::NormalizeLocalPath( + QString::fromStdString(fs::PathToString(settings_path))); +} + +QString SettingsPathForNewDataDir(ArgsManager& args, const QString& data_dir) +{ + const fs::path settings{args.GetPathArg("-settings", BITCOIN_SETTINGS_FILENAME)}; + if (settings.empty()) return {}; + + fs::path network_data_dir{QmlDataDir::QStringToPath(data_dir)}; + if (!BaseParams().DataDir().empty()) { + network_data_dir /= fs::PathFromString(BaseParams().DataDir()); + } + return QmlDataDir::NormalizeLocalPath( + QString::fromStdString( + fs::PathToString(fsbridge::AbsPathJoin(network_data_dir, settings)))); +} + +bool ValidatePendingApply( + ArgsManager& args, + const QmlOnboardingSettings::PendingApply& pending, + QString* error) +{ + if (pending.effective_reset != args.GetBoolArg("-resetguisettings", false)) { + if (error) { + *error = QStringLiteral( + "The selected Bitcoin profile changed while onboarding was open. " + "Restart and review the current data directory, network, and settings before applying."); + } + return false; + } + + if (!pending.target_complete) return true; + + const QString actual_data_dir{ActiveDataDirString(args)}; + const QString actual_chain{QString::fromStdString(args.GetChainTypeString())}; + const QString actual_settings_path{SettingsPathString(args)}; + if (SameDataDirPath(actual_data_dir, pending.resolved_data_dir) && + actual_chain == pending.resolved_chain && + SameOptionalPath(actual_settings_path, pending.resolved_settings_path)) { + return true; + } + + if (error) { + *error = QStringLiteral( + "The selected Bitcoin profile changed while onboarding was open. " + "Restart and review the current data directory, network, and settings before applying."); + } + return false; +} + +bool ShouldUpdateBootstrapDataDir(const QmlOnboardingSettings::PendingApply& pending) +{ + return ShouldPersistGuiDataDirSelection(pending.data_dir.source, pending.explicit_datadir_arg); +} + +void ApplyBootstrapGuiSettings(QVariantMap& settings, const QmlOnboardingSettings::PendingApply& pending) +{ + if (ShouldUpdateBootstrapDataDir(pending)) { + if (QmlDataDir::IsDefaultDataDir(pending.data_dir.path)) { + settings.remove(QString::fromUtf8(SettingsKeys::DATA_DIR)); + } else { + settings.insert(QString::fromUtf8(SettingsKeys::DATA_DIR), pending.data_dir.path); + } + } + settings.insert(QStringLiteral("fReset"), false); +} + +QVariantMap ResetGuiSettingsValues( + const QVariantMap& original, + bool onboarding_completed) +{ + QVariantMap values; + const QString data_dir_key{QString::fromUtf8(SettingsKeys::DATA_DIR)}; + if (original.contains(data_dir_key)) { + values.insert(data_dir_key, original.value(data_dir_key)); + } + values.insert(QStringLiteral("fReset"), !onboarding_completed); + return values; +} + std::optional CommandLineBoolArg(ArgsManager& args, const std::string& name) { std::optional value; @@ -277,6 +681,19 @@ bool PrepareArgs(ArgsManager& args, const std::vector& argv, bool c return args.ParseParameters(static_cast(raw_argv.size()), raw_argv.data(), error); } +GuiSettingsStore CurrentGuiSettingsStore() +{ + QSettings settings; + settings.setFallbacksEnabled(false); + return { + settings.organizationName(), + settings.applicationName(), + settings.fileName(), + settings.format(), + settings.scope(), + }; +} + OnboardingStartupStatus ResolveOnboardingStartupStatus(const std::vector& argv, bool can_listen_ipc) { OnboardingStartupStatus status; @@ -295,44 +712,45 @@ OnboardingStartupStatus ResolveOnboardingStartupStatus(const std::vector& argv, bool can_listen_ipc, PreviewResult result; const QString data_dir = NormalizedDataDirPath(data_dir_selection); + result.selected_data_dir = data_dir; + result.selected_data_dir_source = data_dir_selection.source; + result.resolved_data_dir = data_dir; + result.resolved_data_dir_source = data_dir_selection.source; const QString validation_error = QmlDataDir::ValidateCustomDataDir(data_dir); if (!validation_error.isEmpty()) { result.error = validation_error; @@ -400,6 +816,9 @@ PreviewResult Preview(const std::vector& argv, bool can_listen_ipc, } const bool explicit_datadir = HasExplicitDataDirArg(preview_args); + const QString selected_data_dir = explicit_datadir ? ExplicitDataDirString(preview_args) : data_dir; + const DataDirSource selected_data_dir_source = explicit_datadir ? DataDirSource::ExplicitArg : data_dir_selection.source; + SetPreviewDataDirs(result, selected_data_dir, selected_data_dir_source, selected_data_dir, explicit_datadir); const bool apply_datadir_before_config = ShouldApplyDataDirBeforeConfig(data_dir_selection.source, explicit_datadir, data_dir); const bool custom_datadir_exists = apply_datadir_before_config && QFileInfo::exists(data_dir); bool config_file_path_available{false}; @@ -408,26 +827,20 @@ PreviewResult Preview(const std::vector& argv, bool can_listen_ipc, } if (!apply_datadir_before_config || custom_datadir_exists) { - std::string config_error; - if (!preview_args.ReadConfigFiles(config_error, true)) { - result.error = QString::fromStdString(config_error); - return result; - } - config_file_path_available = true; - try { - SelectParams(preview_args.GetChainType()); - preview_args.SelectConfigNetwork(preview_args.GetChainTypeString()); - } catch (const std::exception& e) { - result.error = QString::fromStdString(e.what()); + ReadOnlyProfileResult profile_read; + QString read_error; + if (!ReadResolvedProfile(preview_args, profile_read, &read_error)) { + result.error = read_error; return result; } - const bool reset_gui_settings = preview_args.GetBoolArg("-resetguisettings", false); - if (!reset_gui_settings) { - std::vector settings_errors; - if (!preview_args.ReadSettingsFile(&settings_errors)) { - result.error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file could not be read."} : settings_errors.front()); - return result; - } + config_file_path_available = profile_read.config_file_path_available; + SetPreviewDataDirs(result, selected_data_dir, selected_data_dir_source, ActiveDataDirString(preview_args), explicit_datadir); + result.effective_reset = preview_args.GetBoolArg("-resetguisettings", false); + if (result.effective_reset) { + preview_args.LockSettings([](common::Settings& settings) { + settings.rw_settings.clear(); + }); + } else { const QmlLegacySettings::MigrationResult migration_result{ QmlLegacySettings::MigrateCoreSettings(preview_args, QmlLegacySettings::MigrationMode::Preview) }; @@ -438,7 +851,9 @@ PreviewResult Preview(const std::vector& argv, bool can_listen_ipc, } } else { preview_args.SelectConfigNetwork(preview_args.GetChainTypeString()); - if (!preview_args.GetBoolArg("-resetguisettings", false)) { + SetPreviewDataDirs(result, selected_data_dir, selected_data_dir_source, selected_data_dir, explicit_datadir); + result.effective_reset = preview_args.GetBoolArg("-resetguisettings", false); + if (!result.effective_reset) { const QmlLegacySettings::MigrationResult migration_result{ QmlLegacySettings::MigrateCoreSettings(preview_args, QmlLegacySettings::MigrationMode::Preview) }; @@ -460,6 +875,12 @@ PreviewResult Preview(const std::vector& argv, bool can_listen_ipc, result.profile = BuildProfileSummary(preview_args, config_file_path_available); result.core_setting_statuses = QmlCoreSettings::BuildCoreSettingStatuses(preview_args, QmlCoreSettings::OnboardingCoreSettingNames()); result.values = QmlCoreSettings::LoadEffectiveValues(preview_args); + result.resolved_chain = QString::fromStdString(preview_args.GetChainTypeString()); + if (apply_datadir_before_config && !custom_datadir_exists) { + result.resolved_settings_path = SettingsPathForNewDataDir(preview_args, result.resolved_data_dir); + } else { + result.resolved_settings_path = SettingsPathString(preview_args); + } result.ok = true; return result; } @@ -475,7 +896,7 @@ bool MarkQmlOnboarded(ArgsManager& args, QString* error) return WriteSettingsFile(args, error); } -bool ApplyToArgs(ArgsManager& args, const DataDirSelection& data_dir_selection, const QSet& touched_settings, const QmlCoreSettings::Values& values, QString* error) +bool PrepareApplyToArgs(ArgsManager& args, const DataDirSelection& data_dir_selection, const QString& resolved_data_dir, const QSet& touched_settings, const QmlCoreSettings::Values& values, bool effective_reset, PendingApply& pending, QString* error) { if (error) error->clear(); @@ -491,85 +912,184 @@ bool ApplyToArgs(ArgsManager& args, const DataDirSelection& data_dir_selection, QmlDataDir::ApplyDataDirArg(args, data_dir); } - std::string config_error; - if (!args.ReadConfigFiles(config_error, true)) { - if (error) *error = QString::fromStdString(config_error); + pending.data_dir = { + data_dir, + data_dir_selection.source, + }; + pending.resolved_data_dir = QmlDataDir::NormalizeLocalPath(resolved_data_dir); + pending.touched_settings = touched_settings; + pending.values = values; + pending.explicit_datadir_arg = explicit_datadir; + pending.effective_reset = effective_reset; + return true; +} + +bool FinalizeStartupSettings(ArgsManager& args, const GuiSettingsStore& bootstrap_gui_settings, const PendingApply* pending, FinalizeResult* result, QString* error) +{ + if (error) error->clear(); + if (result) *result = {}; + + if (pending && !ValidatePendingApply(args, *pending, error)) return false; + + const bool reset_gui_settings{args.GetBoolArg("-resetguisettings", false)}; + const std::map original_rw_settings{SnapshotRwSettings(args)}; + + const GuiSettingsStore active_gui_settings_store{CurrentGuiSettingsStore()}; + std::unique_ptr active_gui_settings{OpenGuiSettings(active_gui_settings_store)}; + const QVariantMap original_active_gui_settings{SnapshotGuiSettings(*active_gui_settings)}; + if (active_gui_settings->status() != QSettings::NoError) { + if (error) { + *error = QStringLiteral("GUI settings could not be read from %1.") + .arg(active_gui_settings->fileName()); + } return false; } - try { - SelectParams(args.GetChainType()); - args.SelectConfigNetwork(args.GetChainTypeString()); - } catch (const std::exception& e) { - if (error) *error = QString::fromStdString(e.what()); - return false; + const bool bootstrap_is_active{SameGuiSettingsStore(active_gui_settings_store, bootstrap_gui_settings)}; + const bool bootstrap_settings_needed{ + !bootstrap_is_active && (pending != nullptr || reset_gui_settings) + }; + std::unique_ptr bootstrap_gui_settings_owner; + QSettings* bootstrap_settings{bootstrap_is_active ? active_gui_settings.get() : nullptr}; + QVariantMap original_bootstrap_gui_settings; + if (bootstrap_settings_needed) { + bootstrap_gui_settings_owner = OpenGuiSettings(bootstrap_gui_settings); + bootstrap_settings = bootstrap_gui_settings_owner.get(); + original_bootstrap_gui_settings = SnapshotGuiSettings(*bootstrap_settings); + if (bootstrap_settings->status() != QSettings::NoError) { + if (error) { + *error = QStringLiteral("Bootstrap GUI settings could not be read from %1.") + .arg(bootstrap_settings->fileName()); + } + return false; + } } - std::vector settings_errors; - const bool reset_gui_settings = args.GetBoolArg("-resetguisettings", false); if (reset_gui_settings) { - fs::path settings_path; - if (args.GetSettingsPath(&settings_path) && fs::exists(settings_path)) { - if (!args.ReadSettingsFile(&settings_errors)) { - if (error) *error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file could not be read."} : settings_errors.front()); - return false; - } - if (!args.WriteSettingsFile(&settings_errors, /*backup=*/true)) { - if (error) *error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file backup could not be written."} : settings_errors.front()); - return false; - } - } + if (!BackupSettingsFile(args, error)) return false; + if (!BackupGuiSettings(*active_gui_settings, args.GetDataDirNet() / "guisettings.ini.bak", error)) return false; args.LockSettings([](common::Settings& settings) { settings.rw_settings.clear(); }); - QmlLegacySettings::ClearLegacyGuiSettings(QString::fromStdString(args.GetChainTypeString())); - } else { - if (!args.ReadSettingsFile(&settings_errors)) { - if (error) *error = QString::fromStdString(settings_errors.empty() ? std::string{"Settings file could not be read."} : settings_errors.front()); - return false; - } + } + + if (!reset_gui_settings) { const QmlLegacySettings::MigrationResult migration_result{ - QmlLegacySettings::MigrateCoreSettings(args, QmlLegacySettings::MigrationMode::Persist) + QmlLegacySettings::MigrateCoreSettings(args, QmlLegacySettings::MigrationMode::Preview) }; if (!migration_result.error.isEmpty()) { if (error) *error = migration_result.error; + RestoreRwSettings(args, original_rw_settings); return false; } } - std::map original_forced_settings; - args.LockSettings([&](common::Settings& settings) { - original_forced_settings = settings.forced_settings; - }); - InitParameterInteraction(args); + if (pending) { + if (!ApplyPendingCoreSettings(args, *pending, error)) { + RestoreRwSettings(args, original_rw_settings); + return false; + } + } - QmlCoreSettings::Session core_settings{values, QmlCoreSettings::BuildCoreSettingStatuses(args, QmlCoreSettings::OnboardingCoreSettingNames())}; - core_settings.setTouchedSettings(touched_settings); - const bool touched_settings_written = core_settings.writeTouchedToArgs(args); - args.LockSettings([&](common::Settings& settings) { - settings.forced_settings = std::move(original_forced_settings); - }); - if (!touched_settings_written) { - if (error) *error = QStringLiteral("One or more startup settings could not be written."); + const bool settings_changed{!RwSettingsEqual(SnapshotRwSettings(args), original_rw_settings)}; + if (settings_changed && !WriteSettingsFile(args, error)) { + RestoreRwSettings(args, original_rw_settings); return false; } - QmlCoreSettings::SetRwSetting(args, QString::fromLatin1(QML_ONBOARDED_KEY), common::SettingsValue{true}); - if (!WriteSettingsFile(args, error)) return false; + QVariantMap active_gui_settings_values{original_active_gui_settings}; + QVariantMap bootstrap_gui_settings_values{original_bootstrap_gui_settings}; + if (reset_gui_settings) { + active_gui_settings_values = ResetGuiSettingsValues( + original_active_gui_settings, + /*onboarding_completed=*/pending != nullptr); + } - if (ShouldPersistGuiDataDirSelection(data_dir_selection.source, explicit_datadir)) { - if (QmlDataDir::IsDefaultDataDir(data_dir)) { - QmlDataDir::PersistDefaultDataDirSelection(); - } else if (!QmlDataDir::PersistGuiDataDirSelection(data_dir, &data_dir_error)) { - if (error) *error = data_dir_error; - return false; + if (pending) { + active_gui_settings_values.insert(QStringLiteral("fReset"), false); + if (bootstrap_is_active) { + ApplyBootstrapGuiSettings(active_gui_settings_values, *pending); + } else { + ApplyBootstrapGuiSettings(bootstrap_gui_settings_values, *pending); } + } else if (reset_gui_settings && !bootstrap_is_active) { + bootstrap_gui_settings_values.insert(QStringLiteral("fReset"), true); } - return true; -} -bool ApplyToArgs(ArgsManager& args, const QString& data_dir, const QSet& touched_settings, const QmlCoreSettings::Values& values, QString* error) -{ - return ApplyToArgs(args, DataDirSelection{data_dir, DataDirSource::UserSelection}, touched_settings, values, error); + const bool active_gui_settings_changed{active_gui_settings_values != original_active_gui_settings}; + const bool bootstrap_gui_settings_changed{ + bootstrap_settings_needed && bootstrap_gui_settings_values != original_bootstrap_gui_settings + }; + if (active_gui_settings_changed && + !WriteGuiSettings( + *active_gui_settings, + active_gui_settings_values, + QStringLiteral("GUI settings update"), + error)) { + RestoreGuiSettings(active_gui_settings_store, original_active_gui_settings, error); + RollBackSettingsFile(args, original_rw_settings, error); + return false; + } + if (bootstrap_gui_settings_changed && + !WriteGuiSettings( + *bootstrap_settings, + bootstrap_gui_settings_values, + QStringLiteral("Bootstrap GUI settings update"), + error)) { + if (active_gui_settings_changed) { + RestoreGuiSettings(active_gui_settings_store, original_active_gui_settings, error); + } + RestoreGuiSettings(bootstrap_gui_settings, original_bootstrap_gui_settings, error); + RollBackSettingsFile(args, original_rw_settings, error); + return false; + } + + QString legacy_cleanup_error; + bool legacy_cleanup_ok{true}; + if (reset_gui_settings) { + legacy_cleanup_ok = QmlLegacySettings::ClearLegacyGuiSettings( + QString::fromStdString(args.GetChainTypeString()), + &legacy_cleanup_error); + } else { + QmlLegacySettings::GuiCleanup cleanup{QmlLegacySettings::GuiCleanup::None}; + if (pending && ShouldUpdateBootstrapDataDir(*pending)) { + cleanup = QmlLegacySettings::GuiCleanup::DataDirAndReset; + } else if (pending) { + cleanup = QmlLegacySettings::GuiCleanup::ResetOnly; + } + + const std::map committed_rw_settings{ + SnapshotRwSettings(args) + }; + const QmlLegacySettings::MigrationResult migration_result{ + QmlLegacySettings::MigrateCoreSettings( + args, + QmlLegacySettings::MigrationMode::Persist, + cleanup) + }; + // Persist removes the staged legacy keys, but the values already + // committed above remain authoritative for this process. + RestoreRwSettings(args, committed_rw_settings); + legacy_cleanup_ok = migration_result.error.isEmpty(); + legacy_cleanup_error = migration_result.error; + } + + if (!legacy_cleanup_ok) { + if (error) *error = legacy_cleanup_error; + if (bootstrap_gui_settings_changed) { + RestoreGuiSettings(bootstrap_gui_settings, original_bootstrap_gui_settings, error); + } + if (active_gui_settings_changed) { + RestoreGuiSettings(active_gui_settings_store, original_active_gui_settings, error); + } + RollBackSettingsFile(args, original_rw_settings, error); + return false; + } + + if (result) { + result->reset_applied = reset_gui_settings; + result->settings_changed = settings_changed; + } + return true; } } // namespace QmlOnboardingSettings diff --git a/qml/onboarding_settings.h b/qml/onboarding_settings.h index 0d5f11d5ea..6ceef6cc32 100644 --- a/qml/onboarding_settings.h +++ b/qml/onboarding_settings.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -32,6 +33,31 @@ struct DataDirSelection { DataDirSource source{DataDirSource::UserSelection}; }; +struct GuiSettingsStore { + QString organization_name; + QString application_name; + QString file_name; + QSettings::Format format{QSettings::NativeFormat}; + QSettings::Scope scope{QSettings::UserScope}; +}; + +struct PendingApply { + DataDirSelection data_dir; + QString resolved_data_dir; + QString resolved_chain; + QString resolved_settings_path; + QSet touched_settings; + QmlCoreSettings::Values values; + bool explicit_datadir_arg{false}; + bool effective_reset{false}; + bool target_complete{false}; +}; + +struct FinalizeResult { + bool reset_applied{false}; + bool settings_changed{false}; +}; + struct ProfileSummary { bool existing_profile{false}; bool has_settings_file{false}; @@ -43,6 +69,14 @@ struct ProfileSummary { struct PreviewResult { bool ok{false}; QString error; + QString selected_data_dir; + DataDirSource selected_data_dir_source{DataDirSource::Default}; + QString resolved_data_dir; + DataDirSource resolved_data_dir_source{DataDirSource::Default}; + bool config_redirected_data_dir{false}; + bool effective_reset{false}; + QString resolved_chain; + QString resolved_settings_path; QmlCoreSettings::Values values; QVariantMap core_setting_statuses; int assumed_blockchain_size{0}; @@ -53,6 +87,12 @@ struct PreviewResult { struct OnboardingStartupStatus { bool ok{false}; QString error; + bool settings_file_unreadable{false}; + QString selected_data_dir; + DataDirSource selected_data_dir_source{DataDirSource::Default}; + QString resolved_data_dir; + DataDirSource resolved_data_dir_source{DataDirSource::Default}; + bool config_redirected_data_dir{false}; QString active_data_dir; DataDirSource data_dir_source{DataDirSource::Default}; bool settings_enabled{true}; @@ -61,12 +101,13 @@ struct OnboardingStartupStatus { }; bool PrepareArgs(ArgsManager& args, const std::vector& argv, bool can_listen_ipc, std::string& error); +GuiSettingsStore CurrentGuiSettingsStore(); OnboardingStartupStatus ResolveOnboardingStartupStatus(const std::vector& argv, bool can_listen_ipc); PreviewResult Preview(const std::vector& argv, bool can_listen_ipc, const DataDirSelection& data_dir); PreviewResult Preview(const std::vector& argv, bool can_listen_ipc, const QString& data_dir); bool MarkQmlOnboarded(ArgsManager& args, QString* error = nullptr); -bool ApplyToArgs(ArgsManager& args, const DataDirSelection& data_dir, const QSet& touched_settings, const QmlCoreSettings::Values& values, QString* error = nullptr); -bool ApplyToArgs(ArgsManager& args, const QString& data_dir, const QSet& touched_settings, const QmlCoreSettings::Values& values, QString* error = nullptr); +bool PrepareApplyToArgs(ArgsManager& args, const DataDirSelection& data_dir, const QString& resolved_data_dir, const QSet& touched_settings, const QmlCoreSettings::Values& values, bool effective_reset, PendingApply& pending, QString* error = nullptr); +bool FinalizeStartupSettings(ArgsManager& args, const GuiSettingsStore& bootstrap_gui_settings, const PendingApply* pending, FinalizeResult* result = nullptr, QString* error = nullptr); } // namespace QmlOnboardingSettings diff --git a/test/functional/qml_test_resetguisettings.py b/test/functional/qml_test_resetguisettings.py index 8133765b5d..71a9f7092e 100755 --- a/test/functional/qml_test_resetguisettings.py +++ b/test/functional/qml_test_resetguisettings.py @@ -53,6 +53,14 @@ def click_to_connection(gui): gui.wait_for_page("onboardingConnection", timeout_ms=5000) +def complete_current_onboarding(harness): + gui = harness.driver + click_to_storage_location(gui) + click_to_connection(gui) + gui.click("onboardingConnectionButton") + harness.wait_for_main_window_reconnect() + + def open_connection_settings(gui): gui.click("connectionSettingsButton") gui.wait_for_page("gotoProxy", timeout_ms=5000) @@ -91,6 +99,357 @@ def load_settings(datadir): raise AssertionError(f"Timed out waiting for {settings_path}") +def qsettings_path(config_home, app_name="BitcoinCore-App-regtest"): + org_dir = "bitcoincore.org" if sys.platform == "darwin" else "BitcoinCore" + return os.path.join(config_home, org_dir, f"{app_name}.ini") + + +def read_qsettings(path): + with open(path, encoding="utf8") as settings_file: + return settings_file.read() + + +def qsettings_values(path): + values = {} + for line in read_qsettings(path).splitlines(): + if not line or line.startswith("["): + continue + key, value = line.split("=", 1) + values[key] = value + return values + + +def read_qsettings_for_datadir(config_home, datadir): + settings_path = qsettings_path(config_home) + settings_text = read_qsettings(settings_path) + assert f"strDataDir={datadir}" in settings_text, settings_text + return settings_text + + +def seed_qsettings( + config_home, + datadir, + *, + app_name="BitcoinCore-App-regtest", + reset=True, + sentinel=None, +): + settings_path = qsettings_path(config_home, app_name) + settings_dir = os.path.dirname(settings_path) + os.makedirs(settings_dir, exist_ok=True) + with open(settings_path, "w", encoding="utf8") as settings_file: + settings_file.write("[General]\n") + settings_file.write(f"strDataDir={datadir}\n") + settings_file.write("language=es\n") + settings_file.write(f"fReset={'true' if reset else 'false'}\n") + settings_file.write("dark=true\n") + settings_file.write("blockclocksize=0.4166666666666667\n") + if sentinel is not None: + settings_file.write(f"resetSentinel={sentinel}\n") + return settings_path + + +def navigate_to_display_settings(gui): + gui.wait_for_page("nodeSettingsButton", timeout_ms=30000) + gui.click("nodeSettingsButton") + gui.wait_for_property("settings_display", "visible", True, timeout_ms=5000) + gui.click("settings_display") + gui.wait_for_page("gotoLanguage", timeout_ms=5000) + + +def run_malformed_settings_reset_recovers(tmpdir): + case_tmpdir = os.path.join(tmpdir, "malformed-settings-reset") + os.makedirs(case_tmpdir, exist_ok=True) + harness = QmlTestHarness( + tmpdir=case_tmpdir, + reset_settings=True, + extra_args=["-regtest", "-disablewallet"], + ) + network_dir = os.path.join(harness.datadir, "regtest") + os.makedirs(network_dir, exist_ok=True) + settings_path = os.path.join(network_dir, "settings.json") + with open(settings_path, "w", encoding="utf8") as settings_file: + settings_file.write("{not valid json") + + gui = None + try: + harness.start() + gui = harness.driver + complete_current_onboarding(harness) + settings = load_settings(harness.datadir) + assert settings.get("qml_onboarded") is True, settings + except Exception: + if gui is not None: + dump_qml_tree(gui) + raise + finally: + harness.stop(cleanup=False) + + +def run_untouched_legacy_store_does_not_block(tmpdir): + case_tmpdir = os.path.join(tmpdir, "untouched-legacy-store") + os.makedirs(case_tmpdir, exist_ok=True) + harness = QmlTestHarness( + tmpdir=case_tmpdir, + extra_args=["-disablewallet"], + ) + legacy_path = os.path.join( + harness.config_home, + "Bitcoin", + "Bitcoin-Qt-regtest.ini", + ) + os.makedirs(legacy_path, exist_ok=True) + marker_path = os.path.join(legacy_path, "untouched") + with open(marker_path, "w", encoding="utf8") as marker_file: + marker_file.write("keep") + + gui = None + try: + harness.start() + gui = harness.driver + gui.wait_for_page("nodeSettingsButton", timeout_ms=30000) + assert read_qsettings(marker_path) == "keep" + except Exception: + if gui is not None: + dump_qml_tree(gui) + raise + finally: + harness.stop(cleanup=False) + + +def run_explicit_false_reset_preserves_qsettings(tmpdir): + name = "command-line-zero" + case_tmpdir = os.path.join(tmpdir, name) + os.makedirs(case_tmpdir, exist_ok=True) + harness = QmlTestHarness( + tmpdir=case_tmpdir, + extra_args=[ + "-regtest", + "-disablewallet", + "-qml_onboarded=1", + "-resetguisettings=0", + ], + ) + settings_path = seed_qsettings( + harness.config_home, + harness.datadir, + reset=False, + sentinel=name, + ) + gui = None + try: + harness.start() + gui = harness.driver + gui.wait_for_page("nodeSettingsButton", timeout_ms=30000) + harness.stop(cleanup=False) + settings_text = read_qsettings(settings_path) + assert f"strDataDir={harness.datadir}" in settings_text, settings_text + assert "language=es" in settings_text, settings_text + assert "fReset=false" in settings_text, settings_text + assert f"resetSentinel={name}" in settings_text, settings_text + except Exception: + if gui is not None: + dump_qml_tree(gui) + raise + finally: + harness.stop(cleanup=False) + + +def run_resolved_network_reset_flag_shows_onboarding(tmpdir): + case_tmpdir = os.path.join(tmpdir, "active-reset") + os.makedirs(case_tmpdir, exist_ok=True) + seed_harness = QmlTestHarness( + tmpdir=case_tmpdir, + extra_args=["-disablewallet"], + ) + active_qsettings_path = seed_qsettings( + seed_harness.config_home, + seed_harness.datadir, + reset=True, + sentinel="active-profile", + ) + bootstrap_qsettings_path = seed_qsettings( + seed_harness.config_home, + seed_harness.datadir, + app_name="BitcoinCore-App", + reset=False, + sentinel="bootstrap-profile", + ) + original_active_qsettings = qsettings_values(active_qsettings_path) + original_bootstrap_qsettings = qsettings_values(bootstrap_qsettings_path) + + harness = QmlTestHarness( + datadir=seed_harness.datadir, + use_datadir_arg=False, + extra_args=["-disablewallet"], + ) + gui = None + try: + harness.start() + gui = harness.driver + gui.wait_for_page("onboardingCover", timeout_ms=10000) + gui.close_window() + return_code = harness.process.wait(timeout=10) + assert return_code == 0, harness.process_output() + gui = None + + active_qsettings = qsettings_values(active_qsettings_path) + assert active_qsettings == original_active_qsettings, active_qsettings + bootstrap_qsettings = qsettings_values(bootstrap_qsettings_path) + assert bootstrap_qsettings == original_bootstrap_qsettings, bootstrap_qsettings + except Exception: + if gui is not None: + dump_qml_tree(gui) + raise + finally: + harness.stop(cleanup=False) + + +def run_config_only_reset_resets_final_profile(tmpdir): + case_tmpdir = os.path.join(tmpdir, "config-only-reset") + os.makedirs(case_tmpdir, exist_ok=True) + harness = QmlTestHarness( + tmpdir=case_tmpdir, + extra_args=["-disablewallet"], + ) + + config_path = os.path.join(harness.datadir, "bitcoin.conf") + with open(config_path, encoding="utf8") as config_file: + config = config_file.read() + with open(config_path, "w", encoding="utf8") as config_file: + config_file.write("resetguisettings=1\n") + config_file.write(config) + + network_dir = os.path.join(harness.datadir, "regtest") + os.makedirs(network_dir, exist_ok=True) + settings_path = os.path.join(network_dir, "settings.json") + original_settings = { + "qml_onboarded": True, + "server": True, + "proxy": "10.0.0.1:9050", + } + with open(settings_path, "w", encoding="utf8") as settings_file: + json.dump(original_settings, settings_file) + + active_qsettings_path = seed_qsettings( + harness.config_home, + harness.datadir, + reset=False, + sentinel="active-profile", + ) + bootstrap_qsettings_path = seed_qsettings( + harness.config_home, + harness.datadir, + app_name="BitcoinCore-App", + reset=False, + sentinel="bootstrap-profile", + ) + original_active_qsettings = qsettings_values(active_qsettings_path) + original_bootstrap_qsettings = qsettings_values(bootstrap_qsettings_path) + + gui = None + launched_harnesses = [harness] + try: + # Config-owned reset must be visible before InitConfig, and closing the + # onboarding window must not mutate either settings store. + harness.start() + gui = harness.driver + gui.wait_for_page("onboardingCover", timeout_ms=10000) + assert gui.get_text("onboardingCoverButton") == "Iniciar" + gui.close_window() + return_code = harness.process.wait(timeout=10) + assert return_code == 0, harness.process_output() + harness.stop(cleanup=False) + gui = None + + active_settings = qsettings_values(active_qsettings_path) + assert active_settings == original_active_qsettings, active_settings + + bootstrap_settings = qsettings_values(bootstrap_qsettings_path) + assert bootstrap_settings == original_bootstrap_qsettings, bootstrap_settings + + with open(settings_path, encoding="utf8") as settings_file: + assert json.load(settings_file) == original_settings + + settings_backup_path = settings_path + ".bak" + gui_backup_path = os.path.join(network_dir, "guisettings.ini.bak") + assert not os.path.exists(settings_backup_path) + assert not os.path.exists(gui_backup_path) + + # Completing onboarding applies the reset to the resolved regtest + # profile, preserves the bootstrap datadir, and clears both fReset + # buckets. + first_completion = QmlTestHarness( + datadir=harness.datadir, + use_datadir_arg=False, + extra_args=["-disablewallet"], + ) + launched_harnesses.append(first_completion) + first_completion.start() + gui = first_completion.driver + complete_current_onboarding(first_completion) + first_completion.stop(cleanup=False) + gui = None + + active_settings = read_qsettings(active_qsettings_path) + assert f"strDataDir={harness.datadir}" in active_settings, active_settings + assert "language=" not in active_settings, active_settings + assert "resetSentinel=" not in active_settings, active_settings + assert "fReset=false" in active_settings, active_settings + + bootstrap_settings = read_qsettings(bootstrap_qsettings_path) + assert f"strDataDir={harness.datadir}" in bootstrap_settings, bootstrap_settings + assert "language=es" in bootstrap_settings, bootstrap_settings + assert "resetSentinel=bootstrap-profile" in bootstrap_settings, bootstrap_settings + assert "fReset=false" in bootstrap_settings, bootstrap_settings + + with open(settings_path, encoding="utf8") as settings_file: + reset_settings = json.load(settings_file) + assert reset_settings.get("qml_onboarded") is True, reset_settings + assert "server" not in reset_settings, reset_settings + assert "proxy" not in reset_settings, reset_settings + + with open(settings_backup_path, encoding="utf8") as settings_backup_file: + settings_backup = json.load(settings_backup_file) + for key, value in original_settings.items(): + assert settings_backup.get(key) == value, settings_backup + + gui_backup = read_qsettings(gui_backup_path) + assert "language=es" in gui_backup, gui_backup + assert "resetSentinel=active-profile" in gui_backup, gui_backup + assert "fReset=false" in gui_backup, gui_backup + + # A persistent config value must show onboarding on every launch, not + # alternate with a silent reset. Cancelling that launch is still + # non-mutating. + settings_before_restart = load_settings(harness.datadir) + active_before_restart = qsettings_values(active_qsettings_path) + bootstrap_before_restart = qsettings_values(bootstrap_qsettings_path) + restart = QmlTestHarness( + datadir=harness.datadir, + use_datadir_arg=False, + extra_args=["-disablewallet"], + ) + launched_harnesses.append(restart) + restart.start() + gui = restart.driver + gui.wait_for_page("onboardingCover", timeout_ms=10000) + gui.close_window() + return_code = restart.process.wait(timeout=10) + assert return_code == 0, restart.process_output() + gui = None + + assert qsettings_values(active_qsettings_path) == active_before_restart + assert qsettings_values(bootstrap_qsettings_path) == bootstrap_before_restart + assert load_settings(harness.datadir) == settings_before_restart + except Exception: + if gui is not None: + dump_qml_tree(gui) + raise + finally: + for launched_harness in launched_harnesses: + launched_harness.stop(cleanup=False) + def run_first_reset_onboarding(tmpdir, custom_datadir): harness = QmlTestHarness( use_datadir_arg=False, @@ -123,9 +482,16 @@ def run_first_reset_onboarding(tmpdir, custom_datadir): gui.click("onboardingConnectionButton") harness.wait_for_main_window_reconnect() + navigate_to_display_settings(gui) + assert gui.get_property("gotoLanguage", "header") == "Language" + + settings_text = read_qsettings_for_datadir(harness.config_home, custom_datadir) + assert "language=es" not in settings_text, settings_text + assert "language=" not in settings_text, settings_text + assert "fReset=false" in settings_text, settings_text settings = load_settings(custom_datadir) - assert settings.get("listen") is False, settings + assert "listen" not in settings, settings assert "natpmp" not in settings, settings assert settings.get("server") is True, settings assert settings.get("proxy") == "10.0.0.1:9050", settings @@ -174,10 +540,15 @@ def run_tests(): if args.socket_path: raise RuntimeError("qml_test_resetguisettings.py must launch the app itself") - tmpdir = tempfile.mkdtemp(prefix="qml_resetguisettings_") + tmpdir = tempfile.mkdtemp(prefix="qml_") custom_datadir = os.path.join(tmpdir, "custom-data-dir") os.makedirs(custom_datadir, exist_ok=True) try: + run_malformed_settings_reset_recovers(tmpdir) + run_untouched_legacy_store_does_not_block(tmpdir) + run_explicit_false_reset_preserves_qsettings(tmpdir) + run_resolved_network_reset_flag_shows_onboarding(tmpdir) + run_config_only_reset_resets_final_profile(tmpdir) run_first_reset_onboarding(tmpdir, custom_datadir) run_second_reset_onboarding(tmpdir, custom_datadir) print("\n" + "=" * 50) diff --git a/test/test_options_model.cpp b/test/test_options_model.cpp index 765b7056a0..68a3cef5c7 100644 --- a/test/test_options_model.cpp +++ b/test/test_options_model.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -15,18 +16,23 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include +#include +#include + #ifndef BITCOINQML_NO_TEST_MAIN const TranslateFn G_TRANSLATION_FUN{nullptr}; #endif @@ -45,6 +51,8 @@ class OptionsModelTests : public GmockTestFixture Q_ENUM(LegacyDisplayUnit) private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); void proxyDisabledRemovesKey(); void torDisabledRemovesKey(); void proxyEnabledWritesAddress(); @@ -84,24 +92,29 @@ private Q_SLOTS: void legacyQtDataDirFallbackReadsOldQtSetting(); void guiDataDirChooserShowsForMissingConfiguredDir(); void guiDataDirChooserShowsForUnwritableConfiguredDir(); - void resetGuiSettingsClearsQSettings(); + void resetGuiSettingsClearsAndBacksUpQSettings(); void resetGuiSettingsClearsLegacyQtSettings(); - void resetGuiSettingsStartsOnboardingFromDefaultDataDir(); - void resetGuiSettingsClearsSettingsJson(); + void resetGuiSettingsClearsAndBacksUpSettingsJson(); + void resetGuiSettingsHonorsFinalSourcePrecedence(); + void resetGuiSettingsAllowsUnreadableSettingsProfile(); + void resetLegacyCleanupRollsBackOnWriteFailure(); void resetGuiSettingsPreviewIgnoresSelectedCustomDataDirSettingsJson(); void resetGuiSettingsApplyClearsSelectedCustomDataDirSettingsJson(); void resetGuiSettingsPreservesCommandLineOverrides(); void resetGuiSettingsPreservesBitcoinConfOverrides(); - void resetGuiSettingsExplicitDatadirClearsThatDatadirSettingsJson(); void qmlOnboardedProfileSkipsPreInitOnboarding(); void qmlOnboardedCommandLineOverrideShowsPreInitOnboarding(); void qmlOnboardedConfiguredDatadirProfileSkipsPreInitOnboarding(); void configuredDatadirPreviewKeepsConfigSource(); void configuredDatadirApplyDoesNotPersistGuiDataDir(); + void guiDatadirTakesPrecedenceOverConfigDatadir(); + void guiDatadirConfigUserSelectionPersistsNewPath(); void explicitDatadirApplyDoesNotPersistGuiDataDir(); + void resetGuiSettingsPreservesSavedDatadirOverConfigDatadir(); void qmlOnboardedResetGuiSettingsShowsPreInitOnboarding(); void qmlOnboardedChooseDataDirShowsPreInitOnboarding(); void qmlOnboardedCurrentResetFlagShowsPreInitOnboarding(); + void qmlOnboardedResolvedNetworkResetFlagShowsPreInitOnboarding(); void qmlOnboardedLegacyResetFlagShowsPreInitOnboarding(); void existingCoreProfileShowsFullOnboardingWithCurrentSettings(); void freshExplicitDatadirPreviewReportsFreshProfile(); @@ -113,10 +126,10 @@ private Q_SLOTS: void onboardingPreviewIgnoresUnrecognizedWalletsEntry(); void freshExplicitDatadirShowsFullOnboarding(); void onboardingApplyWithoutTouchedSettingsOnlyAddsQmlOnboardedMarker(); - void onboardingApplyCreatesWalletSubdirectoryForNewNetworkDataDir(); + void onboardingApplyCreatesNewCustomDataDir(); void onboardingApplyPreservesExistingNetworkWalletDiscovery(); - void fullOnboardingApplyWritesQmlOnboardedMarker(); void onboardingPreviewAppliesParameterInteractions(); + void onboardingStorageCheckUsesResolvedDataDir(); void storageSpaceCheckAcceptsExistingDirectory(); void storageSpaceCheckRejectsExistingFile(); void thirdPartyTransactionLinksParseValidUrls(); @@ -153,13 +166,56 @@ private Q_SLOTS: void legacyQtSettingsCommandLineOverrideStillMigratesPersistentValue(); void legacyQtSettingsBitcoinConfBlocksMigration(); void onboardingApplyMigratesLegacySettingsBeforeTouchedOverrides(); + void onboardingApplyRollsBackWhenLegacyCleanupFails(); + void onboardingApplyRollsBackWhenSettingsWriteFails(); + void onboardingApplyWithSettingsDisabledPreservesLegacyCoreValues(); void onboardingPreviewHelperReadsSelectedDatadirConfig(); void onboardingPreviewReadsSelectedDatadirConfig(); - void onboardingApplyDoesNotCopyUntouchedConfig(); void onboardingApplyWritesTouchedConfigOverride(); void onboardingApplyWritesTouchedParameterInteractionOverride(); + void onboardingApplyRetainsListenChoiceWhenDisablingProxy(); + void onboardingApplyRejectsProfileDrift(); + void onboardingFinalizeIgnoresUnusedBootstrapStore(); + void onboardingApplyClearsResetFlagInBootstrapAndActiveStores(); + +private: + std::unique_ptr m_qsettings_dir; + QSettings::Format m_previous_settings_format{QSettings::NativeFormat}; + QString m_previous_organization_name; + QString m_previous_organization_domain; + QString m_previous_application_name; }; +void OptionsModelTests::initTestCase() +{ + m_previous_settings_format = QSettings::defaultFormat(); + m_previous_organization_name = QCoreApplication::organizationName(); + m_previous_organization_domain = QCoreApplication::organizationDomain(); + m_previous_application_name = QCoreApplication::applicationName(); + + m_qsettings_dir = std::make_unique(); + QVERIFY(m_qsettings_dir->isValid()); + QSettings::setDefaultFormat(QSettings::IniFormat); + QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, m_qsettings_dir->path()); + QCoreApplication::setOrganizationName(QStringLiteral("BitcoinCoreAppTest")); + QCoreApplication::setOrganizationDomain({}); + QCoreApplication::setApplicationName(QStringLiteral("OptionsModelTests")); +} + +void OptionsModelTests::cleanupTestCase() +{ + QSettings settings; + settings.setFallbacksEnabled(false); + settings.clear(); + settings.sync(); + + QCoreApplication::setOrganizationName(m_previous_organization_name); + QCoreApplication::setOrganizationDomain(m_previous_organization_domain); + QCoreApplication::setApplicationName(m_previous_application_name); + QSettings::setDefaultFormat(m_previous_settings_format); + m_qsettings_dir.reset(); +} + // Convenience: set up a NiceMock whose getPersistentSetting returns null for // all keys by default, but returns a given address for the specified key. static common::SettingsValue MakeAddress(const std::string& addr) @@ -221,6 +277,7 @@ class SavedGuiDataDirSettings SavedGuiDataDirSettings() { QSettings settings; + settings.setFallbacksEnabled(false); for (const QString& key : settings.allKeys()) { m_values.insert(key, settings.value(key)); } @@ -229,10 +286,12 @@ class SavedGuiDataDirSettings ~SavedGuiDataDirSettings() { QSettings settings; + settings.setFallbacksEnabled(false); settings.clear(); for (auto it = m_values.cbegin(); it != m_values.cend(); ++it) { settings.setValue(it.key(), it.value()); } + settings.sync(); } private: @@ -366,6 +425,75 @@ static void PrepareArgsForDataDir(ArgsManager& args, const QString& data_dir) args.SelectConfigNetwork(args.GetChainTypeString()); } +static void ReadSettingsForDataDir(ArgsManager& args, const QString& data_dir) +{ + PrepareArgsForDataDir(args, data_dir); + std::vector settings_errors; + QVERIFY2(args.ReadSettingsFile(&settings_errors), settings_errors.empty() ? "" : settings_errors.front().c_str()); +} + +static void InitializeAndFinalizeSettings( + ArgsManager& args, + const QmlOnboardingSettings::GuiSettingsStore& bootstrap_gui_settings, + const QmlOnboardingSettings::PendingApply* pending = nullptr, + QmlOnboardingSettings::FinalizeResult* result = nullptr) +{ + const std::optional init_error{ + common::InitConfig(args, [](const bilingual_str&, const std::vector&) { + return true; + }) + }; + QVERIFY2(!init_error, init_error ? init_error->message.original.c_str() : ""); + args.SelectConfigNetwork(args.GetChainTypeString()); + + QString finalize_error; + QVERIFY2( + QmlOnboardingSettings::FinalizeStartupSettings( + args, + bootstrap_gui_settings, + pending, + result, + &finalize_error), + qPrintable(finalize_error)); +} + +static void PrepareAndFinalizeModelApply(OnboardingOptionsModel& model, ArgsManager& args) +{ + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + QmlOnboardingSettings::PendingApply pending; + QString apply_error; + QVERIFY2(model.prepareApplyToArgs(args, pending, &apply_error), qPrintable(apply_error)); + InitializeAndFinalizeSettings(args, bootstrap_gui_settings, &pending); +} + +static void PrepareAndFinalizeApply( + ArgsManager& args, + const QmlOnboardingSettings::DataDirSelection& data_dir, + const QString& resolved_data_dir, + const QSet& touched_settings, + const QmlCoreSettings::Values& values) +{ + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + QmlOnboardingSettings::PendingApply pending; + QString apply_error; + QVERIFY2( + QmlOnboardingSettings::PrepareApplyToArgs( + args, + data_dir, + resolved_data_dir, + touched_settings, + values, + /*effective_reset=*/false, + pending, + &apply_error), + qPrintable(apply_error)); + InitializeAndFinalizeSettings(args, bootstrap_gui_settings, &pending); +} + void OptionsModelTests::proxyDisabledRemovesKey() { using ::testing::_; @@ -1231,9 +1359,12 @@ void OptionsModelTests::guiDataDirChooserShowsForUnwritableConfiguredDir() QVERIFY(!explicit_datadir_should_show); } -void OptionsModelTests::resetGuiSettingsClearsQSettings() +void OptionsModelTests::resetGuiSettingsClearsAndBacksUpQSettings() { SavedGuiDataDirSettings saved_settings; + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + QSettings settings; settings.setValue(SettingsKeys::DATA_DIR, QStringLiteral("/tmp/old-bitcoin-data")); settings.setValue(SettingsKeys::LANGUAGE, QStringLiteral("de")); @@ -1241,22 +1372,37 @@ void OptionsModelTests::resetGuiSettingsClearsQSettings() settings.setValue(SettingsKeys::THIRD_PARTY_TRANSACTION_URLS, QStringLiteral("https://example.com/%s")); settings.setValue(SettingsKeys::MONEY_FONT_CHOICE, QStringLiteral("best_system")); settings.setValue("fReset", true); + settings.sync(); - std::vector argv = TestArgv(); - argv.emplace_back("-settings="); + std::vector argv = TestArgvWithDataDir(data_dir.path()); + argv.emplace_back("-resetguisettings"); + argv.emplace_back("-nosettings"); ArgsManager args; std::string parse_error; QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + QVERIFY(!args.GetSettingsPath()); - QString reset_error; - QVERIFY2(QmlDataDir::ResetGuiSettings(args, &reset_error), qPrintable(reset_error)); + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + QmlOnboardingSettings::FinalizeResult result; + InitializeAndFinalizeSettings(args, bootstrap_gui_settings, /*pending=*/nullptr, &result); - QVERIFY(!settings.contains(SettingsKeys::DATA_DIR)); + QCOMPARE(settings.value(SettingsKeys::DATA_DIR).toString(), QStringLiteral("/tmp/old-bitcoin-data")); QVERIFY(!settings.contains(SettingsKeys::LANGUAGE)); QVERIFY(!settings.contains(SettingsKeys::DISPLAY_UNIT)); QVERIFY(!settings.contains(SettingsKeys::THIRD_PARTY_TRANSACTION_URLS)); QVERIFY(!settings.contains(SettingsKeys::MONEY_FONT_CHOICE)); - QCOMPARE(settings.value("fReset").toBool(), false); + QCOMPARE(settings.value("fReset").toBool(), true); + QVERIFY(result.reset_applied); + + QSettings backup{ + QDir(data_dir.path()).filePath(QStringLiteral("regtest/guisettings.ini.bak")), + QSettings::IniFormat, + }; + QCOMPARE(backup.value(SettingsKeys::DATA_DIR).toString(), QStringLiteral("/tmp/old-bitcoin-data")); + QCOMPARE(backup.value(SettingsKeys::LANGUAGE).toString(), QStringLiteral("de")); + QCOMPARE(backup.value(SettingsKeys::DISPLAY_UNIT).toInt(), 3); } void OptionsModelTests::resetGuiSettingsClearsLegacyQtSettings() @@ -1270,14 +1416,19 @@ void OptionsModelTests::resetGuiSettingsClearsLegacyQtSettings() legacy_core_settings.settings().setValue(SettingsKeys::DISPLAY_UNIT, 3); legacy_default_settings.settings().setValue(SettingsKeys::DATA_DIR, QStringLiteral("/tmp/legacy-bitcoin-data")); - std::vector argv = TestArgv(); + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + std::vector argv = TestArgvWithDataDir(data_dir.path()); + argv.emplace_back("-resetguisettings"); argv.emplace_back("-settings="); ArgsManager args; std::string parse_error; QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString reset_error; - QVERIFY2(QmlDataDir::ResetGuiSettings(args, &reset_error), qPrintable(reset_error)); + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + InitializeAndFinalizeSettings(args, bootstrap_gui_settings); QVERIFY(!legacy_core_settings.settings().contains(QStringLiteral("fListen"))); QVERIFY(!legacy_core_settings.settings().contains(QStringLiteral("addrProxy"))); @@ -1285,29 +1436,7 @@ void OptionsModelTests::resetGuiSettingsClearsLegacyQtSettings() QVERIFY(!legacy_default_settings.settings().contains(SettingsKeys::DATA_DIR)); } -void OptionsModelTests::resetGuiSettingsStartsOnboardingFromDefaultDataDir() -{ - SavedGuiDataDirSettings saved_settings; - QTemporaryDir old_data_dir; - QVERIFY(old_data_dir.isValid()); - QSettings settings; - settings.setValue(SettingsKeys::DATA_DIR, old_data_dir.path()); - - std::vector argv = TestArgv(); - argv.emplace_back("-settings="); - ArgsManager args; - std::string parse_error; - QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - - QString reset_error; - QVERIFY2(QmlDataDir::ResetGuiSettings(args, &reset_error), qPrintable(reset_error)); - - OnboardingOptionsModel model(argv, /*can_listen_ipc=*/false); - QCOMPARE(model.dataDir(), QmlDataDir::DefaultDataDirString()); - QVERIFY(model.getCustomDataDirString().isEmpty()); -} - -void OptionsModelTests::resetGuiSettingsClearsSettingsJson() +void OptionsModelTests::resetGuiSettingsClearsAndBacksUpSettingsJson() { SavedGuiDataDirSettings saved_settings; QTemporaryDir data_dir; @@ -1318,6 +1447,7 @@ void OptionsModelTests::resetGuiSettingsClearsSettingsJson() std::string{"bitcoinqml"}, std::string{"-regtest"}, "-datadir=" + data_dir.path().toStdString(), + std::string{"-resetguisettings"}, }; ArgsManager args; @@ -1333,8 +1463,10 @@ void OptionsModelTests::resetGuiSettingsClearsSettingsJson() std::vector settings_errors; QVERIFY2(args.WriteSettingsFile(&settings_errors), settings_errors.empty() ? "" : settings_errors.front().c_str()); - QString reset_error; - QVERIFY2(QmlDataDir::ResetGuiSettings(args, &reset_error), qPrintable(reset_error)); + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + InitializeAndFinalizeSettings(args, bootstrap_gui_settings); fs::path backup_path; QVERIFY(args.GetSettingsPath(&backup_path, /*temp=*/false, /*backup=*/true)); @@ -1352,6 +1484,205 @@ void OptionsModelTests::resetGuiSettingsClearsSettingsJson() }); } +void OptionsModelTests::resetGuiSettingsHonorsFinalSourcePrecedence() +{ + struct ResetCase { + const char* name; + bool config_value; + std::optional settings_value; + std::optional command_line_value; + bool expected_reset; + }; + const std::array cases{ + ResetCase{"config-only true", true, std::nullopt, std::nullopt, true}, + ResetCase{"settings false overrides config true", true, false, std::nullopt, false}, + ResetCase{"settings true overrides config false", false, true, std::nullopt, true}, + ResetCase{"command line false overrides settings true", true, true, false, false}, + ResetCase{"command line true overrides settings false", false, false, true, true}, + }; + + SavedGuiDataDirSettings saved_settings; + for (const ResetCase& test_case : cases) { + QTemporaryDir data_dir; + QVERIFY2(data_dir.isValid(), test_case.name); + QVERIFY2(QDir(data_dir.path()).mkpath(QStringLiteral("regtest")), test_case.name); + + QFile conf{QDir(data_dir.path()).filePath(QStringLiteral("bitcoin.conf"))}; + QVERIFY2(conf.open(QIODevice::WriteOnly | QIODevice::Text), test_case.name); + const QByteArray config{ + QByteArrayLiteral("regtest=1\n[regtest]\nresetguisettings=") + + (test_case.config_value ? QByteArrayLiteral("1\n") : QByteArrayLiteral("0\n"))}; + QCOMPARE(conf.write(config), config.size()); + conf.close(); + + const std::vector seed_argv{TestArgvWithDataDir(data_dir.path())}; + ArgsManager seed_args; + std::string parse_error; + QVERIFY2(PrepareTestArgs(seed_args, seed_argv, parse_error), parse_error.c_str()); + SelectParams(seed_args.GetChainType()); + seed_args.SelectConfigNetwork(seed_args.GetChainTypeString()); + seed_args.LockSettings([&](common::Settings& settings) { + settings.rw_settings["qml_onboarded"] = common::SettingsValue{true}; + if (test_case.settings_value) { + settings.rw_settings["resetguisettings"] = common::SettingsValue{*test_case.settings_value}; + } + }); + std::vector settings_errors; + QVERIFY2( + seed_args.WriteSettingsFile(&settings_errors), + settings_errors.empty() ? test_case.name : settings_errors.front().c_str()); + + QSettings gui_settings; + gui_settings.setFallbacksEnabled(false); + gui_settings.clear(); + gui_settings.sync(); + + std::vector argv{seed_argv}; + if (test_case.command_line_value) { + argv.emplace_back(*test_case.command_line_value + ? "-resetguisettings=1" + : "-resetguisettings=0"); + } + const QmlOnboardingSettings::OnboardingStartupStatus startup_status{ + QmlOnboardingSettings::ResolveOnboardingStartupStatus( + argv, + /*can_listen_ipc=*/false) + }; + QVERIFY2(startup_status.ok, qPrintable(startup_status.error)); + QVERIFY2( + startup_status.should_show_onboarding == test_case.expected_reset, + test_case.name); + QVERIFY2( + startup_status.qml_onboarded != test_case.expected_reset, + test_case.name); + + ArgsManager args; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + const std::optional init_error{ + common::InitConfig(args, [](const bilingual_str&, const std::vector&) { + return true; + }) + }; + QVERIFY2( + !init_error, + init_error ? init_error->message.original.c_str() : ""); + args.SelectConfigNetwork(args.GetChainTypeString()); + QVERIFY2( + args.GetBoolArg("-resetguisettings", false) == + test_case.expected_reset, + test_case.name); + + QmlOnboardingSettings::FinalizeResult result; + QString finalize_error; + QVERIFY2( + QmlOnboardingSettings::FinalizeStartupSettings( + args, + bootstrap_gui_settings, + /*pending=*/nullptr, + &result, + &finalize_error), + qPrintable(finalize_error)); + QVERIFY2(result.reset_applied == test_case.expected_reset, test_case.name); + } +} + +void OptionsModelTests::resetGuiSettingsAllowsUnreadableSettingsProfile() +{ + SavedGuiDataDirSettings saved_settings; + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + QVERIFY(QDir(data_dir.path()).mkpath(QStringLiteral("regtest"))); + + QFile settings_file{ + QDir(data_dir.path()).filePath(QStringLiteral("regtest/settings.json")) + }; + QVERIFY(settings_file.open(QIODevice::WriteOnly | QIODevice::Text)); + QVERIFY(settings_file.write("{not valid json") > 0); + settings_file.close(); + + std::vector argv{TestArgvWithDataDir(data_dir.path())}; + const QmlOnboardingSettings::OnboardingStartupStatus unreadable_status{ + QmlOnboardingSettings::ResolveOnboardingStartupStatus( + argv, + /*can_listen_ipc=*/false) + }; + QVERIFY(!unreadable_status.ok); + QVERIFY(unreadable_status.settings_file_unreadable); + + argv.emplace_back("-resetguisettings"); + const QmlOnboardingSettings::OnboardingStartupStatus reset_status{ + QmlOnboardingSettings::ResolveOnboardingStartupStatus( + argv, + /*can_listen_ipc=*/false) + }; + QVERIFY2(reset_status.ok, qPrintable(reset_status.error)); + QVERIFY(!reset_status.settings_file_unreadable); + QVERIFY(reset_status.should_show_onboarding); + + QFile config_file{QDir(data_dir.path()).filePath(QStringLiteral("bitcoin.conf"))}; + QVERIFY(config_file.open(QIODevice::WriteOnly | QIODevice::Text)); + QVERIFY(config_file.write("regtest=1\nresetguisettings=1\n") > 0); + config_file.close(); + + const std::vector config_argv{ + std::string{"bitcoinqml"}, + "-datadir=" + data_dir.path().toStdString(), + }; + const QmlOnboardingSettings::OnboardingStartupStatus config_reset_status{ + QmlOnboardingSettings::ResolveOnboardingStartupStatus( + config_argv, + /*can_listen_ipc=*/false) + }; + QVERIFY2(config_reset_status.ok, qPrintable(config_reset_status.error)); + QVERIFY(config_reset_status.should_show_onboarding); +} + +void OptionsModelTests::resetLegacyCleanupRollsBackOnWriteFailure() +{ +#ifdef Q_OS_WIN + QSKIP("This test relies on POSIX directory permissions."); +#else + SavedNamedSettings qml_core_settings{QStringLiteral("BitcoinCore"), QStringLiteral("BitcoinCore-App-regtest")}; + SavedNamedSettings legacy_settings{QStringLiteral("Bitcoin"), QStringLiteral("Bitcoin-Qt-regtest")}; + qml_core_settings.settings().setValue(QStringLiteral("server"), true); + legacy_settings.settings().setValue(QStringLiteral("fListen"), false); + qml_core_settings.settings().sync(); + legacy_settings.settings().sync(); + QCOMPARE(qml_core_settings.settings().status(), QSettings::NoError); + QCOMPARE(legacy_settings.settings().status(), QSettings::NoError); + + const QString legacy_file{legacy_settings.settings().fileName()}; + const QString legacy_dir{QFileInfo(legacy_file).absolutePath()}; + const QFileDevice::Permissions file_permissions{QFile::permissions(legacy_file)}; + const QFileDevice::Permissions dir_permissions{QFile::permissions(legacy_dir)}; + bool cleared{true}; + QString clear_error; + { + [[maybe_unused]] const auto restore_permissions = qScopeGuard([&] { + QFile::setPermissions(legacy_dir, dir_permissions); + QFile::setPermissions(legacy_file, file_permissions); + }); + QVERIFY(QFile::setPermissions(legacy_file, QFileDevice::ReadOwner)); + QVERIFY(QFile::setPermissions( + legacy_dir, + QFileDevice::ReadOwner | QFileDevice::ExeOwner)); + cleared = QmlLegacySettings::ClearLegacyGuiSettings( + QStringLiteral("regtest"), + &clear_error); + } + + QVERIFY(!cleared); + QVERIFY(clear_error.contains(QStringLiteral("Legacy GUI settings cleanup"))); + qml_core_settings.settings().sync(); + legacy_settings.settings().sync(); + QCOMPARE(qml_core_settings.settings().value(QStringLiteral("server")).toBool(), true); + QCOMPARE(legacy_settings.settings().value(QStringLiteral("fListen")).toBool(), false); +#endif +} + void OptionsModelTests::resetGuiSettingsPreviewIgnoresSelectedCustomDataDirSettingsJson() { SavedGuiDataDirSettings saved_settings; @@ -1397,10 +1728,17 @@ void OptionsModelTests::resetGuiSettingsPreviewIgnoresSelectedCustomDataDirSetti void OptionsModelTests::resetGuiSettingsApplyClearsSelectedCustomDataDirSettingsJson() { SavedGuiDataDirSettings saved_settings; + QTemporaryDir old_data_dir; QTemporaryDir data_dir; + QVERIFY(old_data_dir.isValid()); QVERIFY(data_dir.isValid()); QVERIFY(QDir(data_dir.path()).mkpath(QStringLiteral("regtest"))); + QSettings gui_settings; + gui_settings.setValue(SettingsKeys::DATA_DIR, old_data_dir.path()); + gui_settings.setValue(SettingsKeys::LANGUAGE, QStringLiteral("de")); + gui_settings.setValue(QStringLiteral("fReset"), true); + const std::vector write_argv{ std::string{"bitcoinqml"}, std::string{"-regtest"}, @@ -1423,6 +1761,9 @@ void OptionsModelTests::resetGuiSettingsApplyClearsSelectedCustomDataDirSettings std::vector argv = TestArgv(); argv.emplace_back("-resetguisettings"); OnboardingOptionsModel model(argv, /*can_listen_ipc=*/false); + QCOMPARE(model.dataDir(), old_data_dir.path()); + QCOMPARE(gui_settings.value(SettingsKeys::DATA_DIR).toString(), old_data_dir.path()); + QCOMPARE(gui_settings.value(SettingsKeys::LANGUAGE).toString(), QStringLiteral("de")); QVERIFY(model.selectCustomDataDir(data_dir.path())); QCOMPARE(model.previewError(), QString{}); QVERIFY(model.listen()); @@ -1432,8 +1773,11 @@ void OptionsModelTests::resetGuiSettingsApplyClearsSelectedCustomDataDirSettings ArgsManager apply_args; QVERIFY2(PrepareTestArgs(apply_args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(model.applyToArgs(apply_args, &apply_error), qPrintable(apply_error)); + PrepareAndFinalizeModelApply(model, apply_args); + + QCOMPARE(gui_settings.value(SettingsKeys::DATA_DIR).toString(), data_dir.path()); + QVERIFY(!gui_settings.contains(SettingsKeys::LANGUAGE)); + QCOMPARE(gui_settings.value(QStringLiteral("fReset")).toBool(), false); fs::path backup_path; QVERIFY(apply_args.GetSettingsPath(&backup_path, /*temp=*/false, /*backup=*/true)); @@ -1465,6 +1809,7 @@ void OptionsModelTests::resetGuiSettingsPreservesCommandLineOverrides() "-datadir=" + data_dir.path().toStdString(), std::string{"-proxy=10.0.0.2:9050"}, std::string{"-prune=2048"}, + std::string{"-resetguisettings"}, }; ArgsManager args; @@ -1479,11 +1824,8 @@ void OptionsModelTests::resetGuiSettingsPreservesCommandLineOverrides() std::vector settings_errors; QVERIFY2(args.WriteSettingsFile(&settings_errors), settings_errors.empty() ? "" : settings_errors.front().c_str()); - QString reset_error; - QVERIFY2(QmlDataDir::ResetGuiSettings(args, &reset_error), qPrintable(reset_error)); - const QmlOnboardingSettings::PreviewResult preview{ - QmlOnboardingSettings::Preview(argv, /*can_listen_ipc=*/false, QmlDataDir::DefaultDataDirString()) + QmlOnboardingSettings::Preview(argv, /*can_listen_ipc=*/false, data_dir.path()) }; QVERIFY2(preview.ok, qPrintable(preview.error)); QVERIFY(preview.values.proxy_enabled); @@ -1538,46 +1880,6 @@ void OptionsModelTests::resetGuiSettingsPreservesBitcoinConfOverrides() QCOMPARE(preview.core_setting_statuses.value(QStringLiteral("proxy")).toMap().value(QStringLiteral("source")).toString(), QStringLiteral("bitcoin_conf")); } -void OptionsModelTests::resetGuiSettingsExplicitDatadirClearsThatDatadirSettingsJson() -{ - SavedGuiDataDirSettings saved_settings; - QTemporaryDir data_dir; - QVERIFY(data_dir.isValid()); - QVERIFY(QDir(data_dir.path()).mkpath(QStringLiteral("regtest"))); - - const std::vector argv{ - std::string{"bitcoinqml"}, - std::string{"-regtest"}, - "-datadir=" + data_dir.path().toStdString(), - std::string{"-resetguisettings"}, - }; - - ArgsManager args; - std::string parse_error; - QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - SelectParams(args.GetChainType()); - args.SelectConfigNetwork(args.GetChainTypeString()); - args.LockSettings([](common::Settings& settings) { - settings.rw_settings["server"] = common::SettingsValue{true}; - settings.rw_settings["proxy"] = common::SettingsValue{std::string{"10.0.0.1:9050"}}; - }); - std::vector settings_errors; - QVERIFY2(args.WriteSettingsFile(&settings_errors), settings_errors.empty() ? "" : settings_errors.front().c_str()); - - QString reset_error; - QVERIFY2(QmlDataDir::ResetGuiSettings(args, &reset_error), qPrintable(reset_error)); - - ArgsManager check_args; - QVERIFY2(PrepareTestArgs(check_args, argv, parse_error), parse_error.c_str()); - SelectParams(check_args.GetChainType()); - check_args.SelectConfigNetwork(check_args.GetChainTypeString()); - QVERIFY2(check_args.ReadSettingsFile(&settings_errors), settings_errors.empty() ? "" : settings_errors.front().c_str()); - check_args.LockSettings([](common::Settings& settings) { - QVERIFY(settings.rw_settings.count("server") == 0); - QVERIFY(settings.rw_settings.count("proxy") == 0); - }); -} - void OptionsModelTests::qmlOnboardedProfileSkipsPreInitOnboarding() { SavedGuiDataDirSettings saved_settings; @@ -1745,19 +2047,103 @@ void OptionsModelTests::configuredDatadirApplyDoesNotPersistGuiDataDir() ArgsManager args; std::string parse_error; QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(QmlOnboardingSettings::ApplyToArgs( + PrepareAndFinalizeApply( args, QmlOnboardingSettings::DataDirSelection{status.active_data_dir, status.data_dir_source}, + preview.resolved_data_dir, {}, - preview.values, - &apply_error), qPrintable(apply_error)); + preview.values); QVERIFY(!settings.contains(SettingsKeys::DATA_DIR)); - QCOMPARE(SettingToBool(args.GetPersistentSetting("qml_onboarded")), true); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, configured_data_dir.path()); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), true); QVERIFY(QFileInfo(QDir(configured_data_dir.path()).filePath(QStringLiteral("regtest/settings.json"))).isFile()); } +void OptionsModelTests::guiDatadirTakesPrecedenceOverConfigDatadir() +{ + SavedGuiDataDirSettings saved_settings; + QSettings settings; + QTemporaryDir selected_data_dir; + QTemporaryDir resolved_data_dir; + QVERIFY(selected_data_dir.isValid()); + QVERIFY(resolved_data_dir.isValid()); + settings.setValue(SettingsKeys::DATA_DIR, selected_data_dir.path()); + + QFile conf(QDir(selected_data_dir.path()).filePath(QStringLiteral("bitcoin.conf"))); + QVERIFY(conf.open(QIODevice::WriteOnly | QIODevice::Text)); + QVERIFY(conf.write(QStringLiteral("regtest=1\ndatadir=%1\n[regtest]\nserver=1\n").arg(resolved_data_dir.path()).toUtf8()) > 0); + conf.close(); + + const QmlOnboardingSettings::OnboardingStartupStatus status{ + QmlOnboardingSettings::ResolveOnboardingStartupStatus(TestArgv(), /*can_listen_ipc=*/false) + }; + QVERIFY2(status.ok, qPrintable(status.error)); + QCOMPARE(status.selected_data_dir, selected_data_dir.path()); + QVERIFY(status.selected_data_dir_source == QmlOnboardingSettings::DataDirSource::GuiSetting); + + const QmlOnboardingSettings::PreviewResult preview{ + QmlOnboardingSettings::Preview( + TestArgv(), + /*can_listen_ipc=*/false, + QmlOnboardingSettings::DataDirSelection{status.selected_data_dir, status.selected_data_dir_source}) + }; + QVERIFY2(preview.ok, qPrintable(preview.error)); + QCOMPARE(preview.resolved_data_dir, selected_data_dir.path()); + QVERIFY(preview.values.server); + QCOMPARE(preview.core_setting_statuses.value(QStringLiteral("server")).toMap().value(QStringLiteral("source")).toString(), QStringLiteral("bitcoin_conf")); + + OnboardingOptionsModel model(TestArgv(), /*can_listen_ipc=*/false); + QCOMPARE(model.dataDir(), selected_data_dir.path()); + + ArgsManager args; + std::string parse_error; + QVERIFY2(PrepareTestArgs(args, TestArgv(), parse_error), parse_error.c_str()); + PrepareAndFinalizeModelApply(model, args); + QCOMPARE(QmlDataDir::NormalizeLocalPath(QString::fromStdString(fs::PathToString(args.GetDataDirBase()))), selected_data_dir.path()); + QCOMPARE(settings.value(SettingsKeys::DATA_DIR).toString(), selected_data_dir.path()); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, selected_data_dir.path()); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), true); + QVERIFY(QFileInfo(QDir(selected_data_dir.path()).filePath(QStringLiteral("regtest/settings.json"))).isFile()); + QVERIFY(!QFileInfo(QDir(resolved_data_dir.path()).filePath(QStringLiteral("regtest/settings.json"))).exists()); +} + +void OptionsModelTests::guiDatadirConfigUserSelectionPersistsNewPath() +{ + SavedGuiDataDirSettings saved_settings; + QSettings settings; + QTemporaryDir selected_data_dir; + QTemporaryDir resolved_data_dir; + QTemporaryDir new_data_dir; + QVERIFY(selected_data_dir.isValid()); + QVERIFY(resolved_data_dir.isValid()); + QVERIFY(new_data_dir.isValid()); + settings.setValue(SettingsKeys::DATA_DIR, selected_data_dir.path()); + + QFile conf(QDir(selected_data_dir.path()).filePath(QStringLiteral("bitcoin.conf"))); + QVERIFY(conf.open(QIODevice::WriteOnly | QIODevice::Text)); + QVERIFY(conf.write(QStringLiteral("regtest=1\ndatadir=%1\n[regtest]\nserver=1\n").arg(resolved_data_dir.path()).toUtf8()) > 0); + conf.close(); + + OnboardingOptionsModel model(TestArgv(), /*can_listen_ipc=*/false); + QCOMPARE(model.dataDir(), selected_data_dir.path()); + QVERIFY(model.selectCustomDataDir(new_data_dir.path())); + + ArgsManager args; + std::string parse_error; + QVERIFY2(PrepareTestArgs(args, TestArgv(), parse_error), parse_error.c_str()); + PrepareAndFinalizeModelApply(model, args); + QCOMPARE(QmlDataDir::NormalizeLocalPath(QString::fromStdString(fs::PathToString(args.GetDataDirBase()))), new_data_dir.path()); + QCOMPARE(settings.value(SettingsKeys::DATA_DIR).toString(), new_data_dir.path()); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, new_data_dir.path()); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), true); + QVERIFY(QFileInfo(QDir(new_data_dir.path()).filePath(QStringLiteral("regtest/settings.json"))).isFile()); + QVERIFY(!QFileInfo(QDir(resolved_data_dir.path()).filePath(QStringLiteral("regtest/settings.json"))).exists()); +} + void OptionsModelTests::explicitDatadirApplyDoesNotPersistGuiDataDir() { SavedGuiDataDirSettings saved_settings; @@ -1774,11 +2160,58 @@ void OptionsModelTests::explicitDatadirApplyDoesNotPersistGuiDataDir() ArgsManager args; std::string parse_error; QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(model.applyToArgs(args, &apply_error), qPrintable(apply_error)); + PrepareAndFinalizeModelApply(model, args); QVERIFY(!settings.contains(SettingsKeys::DATA_DIR)); - QCOMPARE(SettingToBool(args.GetPersistentSetting("qml_onboarded")), true); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir.path()); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), true); +} + +void OptionsModelTests::resetGuiSettingsPreservesSavedDatadirOverConfigDatadir() +{ + SavedGuiDataDirSettings saved_settings; + QSettings settings; + QTemporaryDir saved_data_dir; + QTemporaryDir configured_data_dir; + QTemporaryDir config_dir; + QVERIFY(saved_data_dir.isValid()); + QVERIFY(configured_data_dir.isValid()); + QVERIFY(config_dir.isValid()); + settings.setValue(SettingsKeys::DATA_DIR, saved_data_dir.path()); + settings.setValue(QStringLiteral("fReset"), true); + + const QString conf_path = QDir(config_dir.path()).filePath(QStringLiteral("bitcoin.conf")); + QFile conf(conf_path); + QVERIFY(conf.open(QIODevice::WriteOnly | QIODevice::Text)); + QVERIFY(conf.write(QStringLiteral("regtest=1\ndatadir=%1\n[regtest]\nserver=1\n").arg(configured_data_dir.path()).toUtf8()) > 0); + conf.close(); + + std::vector argv{ + std::string{"bitcoinqml"}, + std::string{"-regtest"}, + "-conf=" + conf_path.toStdString(), + std::string{"-resetguisettings"}, + }; + + OnboardingOptionsModel model(argv, /*can_listen_ipc=*/false); + QCOMPARE(model.dataDir(), saved_data_dir.path()); + QCOMPARE(model.getCustomDataDirString(), saved_data_dir.path()); + + ArgsManager args; + std::string parse_error; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + PrepareAndFinalizeModelApply(model, args); + + QCOMPARE(QmlDataDir::NormalizeLocalPath(QString::fromStdString(fs::PathToString(args.GetDataDirBase()))), saved_data_dir.path()); + QCOMPARE(settings.value(SettingsKeys::DATA_DIR).toString(), saved_data_dir.path()); + QCOMPARE(settings.value(QStringLiteral("fReset")).toBool(), false); + QVERIFY(QFileInfo(QDir(saved_data_dir.path()).filePath(QStringLiteral("regtest/settings.json"))).isFile()); + QVERIFY(!QFileInfo(QDir(configured_data_dir.path()).filePath(QStringLiteral("regtest/settings.json"))).exists()); + + ArgsManager check_args; + ReadSettingsForDataDir(check_args, saved_data_dir.path()); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), true); } void OptionsModelTests::qmlOnboardedResetGuiSettingsShowsPreInitOnboarding() @@ -1850,34 +2283,78 @@ void OptionsModelTests::qmlOnboardedCurrentResetFlagShowsPreInitOnboarding() QCOMPARE(status.active_data_dir, data_dir.path()); } -void OptionsModelTests::qmlOnboardedLegacyResetFlagShowsPreInitOnboarding() +void OptionsModelTests::qmlOnboardedResolvedNetworkResetFlagShowsPreInitOnboarding() { SavedGuiDataDirSettings saved_settings; - SavedNamedSettings legacy_default_settings{QStringLiteral("Bitcoin"), QStringLiteral("Bitcoin-Qt")}; - QSettings settings; - settings.remove(SettingsKeys::DATA_DIR); - settings.remove(QStringLiteral("fReset")); - + SavedNamedSettings active_settings{ + QCoreApplication::organizationName(), + QStringLiteral(QAPP_APP_NAME_REGTEST), + }; + QSettings bootstrap_settings; QTemporaryDir data_dir; QVERIFY(data_dir.isValid()); - legacy_default_settings.settings().setValue(SettingsKeys::DATA_DIR, data_dir.path()); - legacy_default_settings.settings().setValue(QStringLiteral("fReset"), true); + bootstrap_settings.setValue(SettingsKeys::DATA_DIR, data_dir.path()); + bootstrap_settings.setValue(QStringLiteral("fReset"), false); + active_settings.settings().setValue(QStringLiteral("fReset"), true); + active_settings.settings().sync(); + + QFile conf{data_dir.filePath(QStringLiteral("bitcoin.conf"))}; + QVERIFY(conf.open(QIODevice::WriteOnly | QIODevice::Text)); + QVERIFY(conf.write("regtest=1\n") > 0); + conf.close(); ArgsManager write_args; PrepareArgsForDataDir(write_args, data_dir.path()); QString write_error; - QVERIFY2(QmlOnboardingSettings::MarkQmlOnboarded(write_args, &write_error), qPrintable(write_error)); + QVERIFY2( + QmlOnboardingSettings::MarkQmlOnboarded(write_args, &write_error), + qPrintable(write_error)); + const std::vector argv{std::string{"bitcoinqml"}}; const QmlOnboardingSettings::OnboardingStartupStatus status{ - QmlOnboardingSettings::ResolveOnboardingStartupStatus(TestArgv(), /*can_listen_ipc=*/false) + QmlOnboardingSettings::ResolveOnboardingStartupStatus( + argv, + /*can_listen_ipc=*/false) }; QVERIFY2(status.ok, qPrintable(status.error)); QVERIFY(!status.qml_onboarded); QVERIFY(status.should_show_onboarding); QCOMPARE(status.active_data_dir, data_dir.path()); - QmlDataDir::PersistDefaultDataDirSelection(); - QVERIFY(!legacy_default_settings.settings().contains(SettingsKeys::DATA_DIR)); + active_settings.settings().sync(); + QCOMPARE( + active_settings.settings().value(QStringLiteral("fReset")).toBool(), + true); +} + +void OptionsModelTests::qmlOnboardedLegacyResetFlagShowsPreInitOnboarding() +{ + SavedGuiDataDirSettings saved_settings; + SavedNamedSettings legacy_default_settings{QStringLiteral("Bitcoin"), QStringLiteral("Bitcoin-Qt")}; + QSettings settings; + settings.remove(SettingsKeys::DATA_DIR); + settings.remove(QStringLiteral("fReset")); + + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + legacy_default_settings.settings().setValue(SettingsKeys::DATA_DIR, data_dir.path()); + legacy_default_settings.settings().setValue(QStringLiteral("fReset"), true); + + ArgsManager write_args; + PrepareArgsForDataDir(write_args, data_dir.path()); + QString write_error; + QVERIFY2(QmlOnboardingSettings::MarkQmlOnboarded(write_args, &write_error), qPrintable(write_error)); + + const QmlOnboardingSettings::OnboardingStartupStatus status{ + QmlOnboardingSettings::ResolveOnboardingStartupStatus(TestArgv(), /*can_listen_ipc=*/false) + }; + QVERIFY2(status.ok, qPrintable(status.error)); + QVERIFY(!status.qml_onboarded); + QVERIFY(status.should_show_onboarding); + QCOMPARE(status.active_data_dir, data_dir.path()); + + QmlDataDir::PersistDefaultDataDirSelection(); + QVERIFY(!legacy_default_settings.settings().contains(SettingsKeys::DATA_DIR)); QVERIFY(!legacy_default_settings.settings().contains(QStringLiteral("fReset"))); } @@ -2091,15 +2568,25 @@ void OptionsModelTests::onboardingApplyWithoutTouchedSettingsOnlyAddsQmlOnboarde ArgsManager args; std::string parse_error; - QVERIFY2(PrepareTestArgs(args, TestArgvWithDataDir(data_dir.path()), parse_error), parse_error.c_str()); + const std::vector argv{TestArgvWithDataDir(data_dir.path())}; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(QmlOnboardingSettings::ApplyToArgs(args, data_dir.path(), {}, QmlCoreSettings::Values{}, &apply_error), qPrintable(apply_error)); + PrepareAndFinalizeApply( + args, + QmlOnboardingSettings::DataDirSelection{ + data_dir.path(), + QmlOnboardingSettings::DataDirSource::UserSelection, + }, + data_dir.path(), + {}, + QmlCoreSettings::Values{}); - QCOMPARE(SettingToBool(args.GetPersistentSetting("qml_onboarded")), true); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir.path()); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), true); bool has_listen_override{true}; bool has_prune_override{true}; - args.LockSettings([&](common::Settings& settings) { + check_args.LockSettings([&](common::Settings& settings) { has_listen_override = settings.rw_settings.count("listen") > 0; has_prune_override = settings.rw_settings.count("prune") > 0; }); @@ -2107,23 +2594,33 @@ void OptionsModelTests::onboardingApplyWithoutTouchedSettingsOnlyAddsQmlOnboarde QVERIFY(!has_prune_override); } -void OptionsModelTests::onboardingApplyCreatesWalletSubdirectoryForNewNetworkDataDir() +void OptionsModelTests::onboardingApplyCreatesNewCustomDataDir() { SavedGuiDataDirSettings saved_settings; - QTemporaryDir data_dir; - QVERIFY(data_dir.isValid()); - const QString network_wallets_dir = QDir(data_dir.path()).filePath(QStringLiteral("regtest/wallets")); - QVERIFY(!QFileInfo::exists(network_wallets_dir)); + QSettings gui_settings; + gui_settings.remove(SettingsKeys::DATA_DIR); + + QTemporaryDir parent_dir; + QVERIFY(parent_dir.isValid()); + const QString data_dir{parent_dir.filePath(QStringLiteral("new-data-dir"))}; + QVERIFY(!QFileInfo::exists(data_dir)); + + const std::vector argv{TestArgv()}; + OnboardingOptionsModel model{argv, /*can_listen_ipc=*/false}; + QVERIFY(model.selectCustomDataDir(data_dir)); + QCOMPARE(model.previewError(), QString{}); ArgsManager args; std::string parse_error; - QVERIFY2(PrepareTestArgs(args, TestArgvWithDataDir(data_dir.path()), parse_error), parse_error.c_str()); - - QString apply_error; - QVERIFY2(QmlOnboardingSettings::ApplyToArgs(args, data_dir.path(), {}, QmlCoreSettings::Values{}, &apply_error), qPrintable(apply_error)); + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + PrepareAndFinalizeModelApply(model, args); - QCOMPARE(SettingToBool(args.GetPersistentSetting("qml_onboarded")), true); - QVERIFY(QFileInfo(network_wallets_dir).isDir()); + QVERIFY(QFileInfo(data_dir).isDir()); + QVERIFY(QFileInfo(QDir(data_dir).filePath(QStringLiteral("regtest/wallets"))).isDir()); + QCOMPARE(gui_settings.value(SettingsKeys::DATA_DIR).toString(), data_dir); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), true); } void OptionsModelTests::onboardingApplyPreservesExistingNetworkWalletDiscovery() @@ -2138,37 +2635,26 @@ void OptionsModelTests::onboardingApplyPreservesExistingNetworkWalletDiscovery() ArgsManager args; std::string parse_error; - QVERIFY2(PrepareTestArgs(args, TestArgvWithDataDir(data_dir.path()), parse_error), parse_error.c_str()); + const std::vector argv{TestArgvWithDataDir(data_dir.path())}; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(QmlOnboardingSettings::ApplyToArgs(args, data_dir.path(), {}, QmlCoreSettings::Values{}, &apply_error), qPrintable(apply_error)); + PrepareAndFinalizeApply( + args, + QmlOnboardingSettings::DataDirSelection{ + data_dir.path(), + QmlOnboardingSettings::DataDirSource::UserSelection, + }, + data_dir.path(), + {}, + QmlCoreSettings::Values{}); - QCOMPARE(SettingToBool(args.GetPersistentSetting("qml_onboarded")), true); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir.path()); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), true); QVERIFY(QFileInfo(QDir(network_dir).filePath(QStringLiteral("settings.json"))).isFile()); QVERIFY(!QFileInfo::exists(network_wallets_dir)); } -void OptionsModelTests::fullOnboardingApplyWritesQmlOnboardedMarker() -{ - SavedGuiDataDirSettings saved_settings; - QTemporaryDir data_dir; - QVERIFY(data_dir.isValid()); - QSettings settings; - settings.remove(SettingsKeys::DATA_DIR); - - const std::vector argv = TestArgv(); - OnboardingOptionsModel model(argv, /*can_listen_ipc=*/false); - QVERIFY(model.selectCustomDataDir(data_dir.path())); - - ArgsManager args; - std::string parse_error; - QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(model.applyToArgs(args, &apply_error), qPrintable(apply_error)); - QCOMPARE(SettingToBool(args.GetPersistentSetting("qml_onboarded")), true); - QCOMPARE(settings.value(SettingsKeys::DATA_DIR).toString(), data_dir.path()); -} - void OptionsModelTests::onboardingPreviewAppliesParameterInteractions() { SavedGuiDataDirSettings saved_settings; @@ -2191,6 +2677,71 @@ void OptionsModelTests::onboardingPreviewAppliesParameterInteractions() QCOMPARE(preview.core_setting_statuses.value(QStringLiteral("proxy")).toMap().value(QStringLiteral("source")).toString(), QStringLiteral("bitcoin_conf")); } +void OptionsModelTests::onboardingStorageCheckUsesResolvedDataDir() +{ +#ifdef Q_OS_WIN + QSKIP("This test isolates the default datadir through HOME."); +#else + SavedGuiDataDirSettings saved_settings; + QTemporaryDir home_dir; + QTemporaryDir config_dir; + QTemporaryDir resolved_parent; + QVERIFY(home_dir.isValid()); + QVERIFY(config_dir.isValid()); + QVERIFY(resolved_parent.isValid()); + + const bool had_home{qEnvironmentVariableIsSet("HOME")}; + const QByteArray original_home{qgetenv("HOME")}; + [[maybe_unused]] const auto restore_home{qScopeGuard([&] { + if (had_home) { + qputenv("HOME", original_home); + } else { + qunsetenv("HOME"); + } + })}; + QVERIFY(qputenv("HOME", home_dir.path().toUtf8())); + + const QString selected_data_dir{QmlDataDir::DefaultDataDirString()}; + QVERIFY(!QFileInfo::exists(selected_data_dir)); + const QString resolved_data_dir{ + resolved_parent.filePath(QStringLiteral("resolved")) + }; + QVERIFY(QDir().mkpath(resolved_data_dir)); + + const QString config_path{ + config_dir.filePath(QStringLiteral("bitcoin.conf")) + }; + QFile config_file{config_path}; + QVERIFY(config_file.open(QIODevice::WriteOnly | QIODevice::Text)); + const QByteArray config{ + QStringLiteral("regtest=1\ndatadir=%1\n") + .arg(resolved_data_dir) + .toUtf8() + }; + QCOMPARE(config_file.write(config), config.size()); + config_file.close(); + + QSettings settings; + settings.remove(SettingsKeys::DATA_DIR); + + const std::vector argv{ + std::string{"bitcoinqml"}, + std::string{"-regtest"}, + "-conf=" + config_path.toStdString(), + }; + OnboardingOptionsModel model{argv, /*can_listen_ipc=*/false}; + model.useDefaultDataDir(); + QCOMPARE(model.dataDir(), selected_data_dir); + QCOMPARE(model.previewError(), QString{}); + QTRY_VERIFY_WITH_TIMEOUT(!model.storageCheckPending(), 5000); + QVERIFY2( + model.storagePathMessage().contains( + QStringLiteral("directory already exists"), + Qt::CaseInsensitive), + qPrintable(model.storagePathMessage())); +#endif +} + void OptionsModelTests::storageSpaceCheckAcceptsExistingDirectory() { QTemporaryDir temp_dir; @@ -3321,12 +3872,16 @@ void OptionsModelTests::onboardingApplyMigratesLegacySettingsBeforeTouchedOverri ArgsManager args; std::string parse_error; QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(model.applyToArgs(args, &apply_error), qPrintable(apply_error)); + PrepareAndFinalizeModelApply(model, args); + + QCOMPARE(args.GetBoolArg("-listen", true), true); + QCOMPARE(args.GetBoolArg("-server", false), true); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir.path()); bool has_listen_override{true}; common::SettingsValue server; - args.LockSettings([&](common::Settings& core_settings) { + check_args.LockSettings([&](common::Settings& core_settings) { has_listen_override = core_settings.rw_settings.count("listen") > 0; server = core_settings.rw_settings.at("server"); }); @@ -3336,6 +3891,252 @@ void OptionsModelTests::onboardingApplyMigratesLegacySettingsBeforeTouchedOverri QVERIFY(!legacy_settings.settings().contains(QStringLiteral("server"))); } +void OptionsModelTests::onboardingApplyRollsBackWhenLegacyCleanupFails() +{ +#ifdef Q_OS_WIN + QSKIP("This test relies on POSIX directory permissions."); +#else + SavedGuiDataDirSettings saved_settings; + SavedNamedSettings qml_core_settings{QStringLiteral("BitcoinCore"), QStringLiteral("BitcoinCore-App-regtest")}; + SavedNamedSettings legacy_settings{QStringLiteral("Bitcoin"), QStringLiteral("Bitcoin-Qt-regtest")}; + legacy_settings.settings().setValue(QStringLiteral("fListen"), false); + legacy_settings.settings().sync(); + QCOMPARE(legacy_settings.settings().status(), QSettings::NoError); + + QSettings gui_settings; + gui_settings.setFallbacksEnabled(false); + gui_settings.clear(); + gui_settings.setValue(QStringLiteral("rollbackSentinel"), QStringLiteral("keep")); + gui_settings.sync(); + + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + const std::vector argv{TestArgv()}; + OnboardingOptionsModel model{argv, /*can_listen_ipc=*/false}; + QVERIFY(model.selectCustomDataDir(data_dir.path())); + QVERIFY(!model.listen()); + model.setListen(true); + + ArgsManager args; + std::string parse_error; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + QmlOnboardingSettings::PendingApply pending; + QString prepare_error; + QVERIFY2(model.prepareApplyToArgs(args, pending, &prepare_error), qPrintable(prepare_error)); + + const std::optional init_error{ + common::InitConfig(args, [](const bilingual_str&, const std::vector&) { + return true; + }) + }; + QVERIFY2(!init_error, init_error ? init_error->message.original.c_str() : ""); + args.SelectConfigNetwork(args.GetChainTypeString()); + + const QString legacy_file{legacy_settings.settings().fileName()}; + const QString legacy_dir{QFileInfo(legacy_file).absolutePath()}; + const QFileDevice::Permissions file_permissions{QFile::permissions(legacy_file)}; + const QFileDevice::Permissions dir_permissions{QFile::permissions(legacy_dir)}; + bool finalized{true}; + QString finalize_error; + { + [[maybe_unused]] const auto restore_permissions = qScopeGuard([&] { + QFile::setPermissions(legacy_dir, dir_permissions); + QFile::setPermissions(legacy_file, file_permissions); + }); + QVERIFY(QFile::setPermissions(legacy_file, QFileDevice::ReadOwner)); + QVERIFY(QFile::setPermissions( + legacy_dir, + QFileDevice::ReadOwner | QFileDevice::ExeOwner)); + finalized = QmlOnboardingSettings::FinalizeStartupSettings( + args, + bootstrap_gui_settings, + &pending, + /*result=*/nullptr, + &finalize_error); + } + + QVERIFY(!finalized); + QVERIFY(finalize_error.contains(QStringLiteral("Legacy GUI settings migration"))); + QCOMPARE(args.GetBoolArg("-listen", true), true); + QCOMPARE(SettingToBool(args.GetPersistentSetting("qml_onboarded")), std::nullopt); + + gui_settings.sync(); + QCOMPARE(gui_settings.value(QStringLiteral("rollbackSentinel")).toString(), QStringLiteral("keep")); + QVERIFY(!gui_settings.contains(SettingsKeys::DATA_DIR)); + + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir.path()); + QCOMPARE(SettingToBool(check_args.GetPersistentSetting("qml_onboarded")), std::nullopt); + + legacy_settings.settings().sync(); + QVERIFY(legacy_settings.settings().contains(QStringLiteral("fListen"))); +#endif +} + +void OptionsModelTests::onboardingApplyRollsBackWhenSettingsWriteFails() +{ + SavedGuiDataDirSettings saved_settings; + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + QVERIFY(QDir(data_dir.path()).mkpath(QStringLiteral("regtest"))); + + ArgsManager seed_args; + PrepareArgsForDataDir(seed_args, data_dir.path()); + seed_args.LockSettings([](common::Settings& settings) { + settings.rw_settings["server"] = common::SettingsValue{true}; + }); + std::vector settings_errors; + QVERIFY2( + seed_args.WriteSettingsFile(&settings_errors), + settings_errors.empty() ? "" : settings_errors.front().c_str()); + + fs::path settings_path; + QVERIFY(seed_args.GetSettingsPath(&settings_path)); + QFile settings_file{ + QString::fromStdString(fs::PathToString(settings_path)) + }; + QVERIFY(settings_file.open(QIODevice::ReadOnly)); + const QByteArray original_settings{settings_file.readAll()}; + settings_file.close(); + + QSettings gui_settings; + gui_settings.setFallbacksEnabled(false); + gui_settings.setValue( + QStringLiteral("settingsWriteSentinel"), + QStringLiteral("keep")); + gui_settings.sync(); + + const std::vector argv{ + TestArgvWithDataDir(data_dir.path()) + }; + OnboardingOptionsModel model{argv, /*can_listen_ipc=*/false}; + ArgsManager args; + std::string parse_error; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + QmlOnboardingSettings::PendingApply pending; + QString prepare_error; + QVERIFY2( + model.prepareApplyToArgs(args, pending, &prepare_error), + qPrintable(prepare_error)); + + const std::optional init_error{ + common::InitConfig(args, [](const bilingual_str&, const std::vector&) { + return true; + }) + }; + QVERIFY2(!init_error, init_error ? init_error->message.original.c_str() : ""); + args.SelectConfigNetwork(args.GetChainTypeString()); + + fs::path temporary_settings_path; + QVERIFY(args.GetSettingsPath(&temporary_settings_path, /*temp=*/true)); + QVERIFY(QDir().mkpath( + QString::fromStdString(fs::PathToString(temporary_settings_path)))); + + QString finalize_error; + QVERIFY(!QmlOnboardingSettings::FinalizeStartupSettings( + args, + bootstrap_gui_settings, + &pending, + /*result=*/nullptr, + &finalize_error)); + QVERIFY(!finalize_error.isEmpty()); + + QCOMPARE(args.GetBoolArg("-server", false), true); + QCOMPARE( + SettingToBool(args.GetPersistentSetting("qml_onboarded")), + std::nullopt); + + QVERIFY(settings_file.open(QIODevice::ReadOnly)); + QCOMPARE(settings_file.readAll(), original_settings); + settings_file.close(); + + gui_settings.sync(); + QCOMPARE( + gui_settings.value(QStringLiteral("settingsWriteSentinel")).toString(), + QStringLiteral("keep")); + QVERIFY(!gui_settings.contains(SettingsKeys::DATA_DIR)); +} + +void OptionsModelTests::onboardingApplyWithSettingsDisabledPreservesLegacyCoreValues() +{ + SavedGuiDataDirSettings saved_settings; + SavedNamedSettings legacy_core_settings{QStringLiteral("Bitcoin"), QStringLiteral("Bitcoin-Qt-regtest")}; + SavedNamedSettings legacy_default_settings{QStringLiteral("Bitcoin"), QStringLiteral("Bitcoin-Qt")}; + legacy_core_settings.settings().setValue(QStringLiteral("fListen"), false); + legacy_core_settings.settings().setValue(QStringLiteral("addrProxy"), QStringLiteral("10.0.0.1:9050")); + legacy_default_settings.settings().setValue(SettingsKeys::DATA_DIR, QStringLiteral("/tmp/legacy-datadir")); + legacy_default_settings.settings().setValue(QStringLiteral("fReset"), true); + + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + + std::vector argv = TestArgv(); + argv.emplace_back("-nosettings"); + ArgsManager args; + std::string parse_error; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + QmlOnboardingSettings::PendingApply pending; + QString apply_error; + QVERIFY2( + QmlOnboardingSettings::PrepareApplyToArgs( + args, + {data_dir.path(), QmlOnboardingSettings::DataDirSource::UserSelection}, + data_dir.path(), + {}, + QmlCoreSettings::Values{}, + /*effective_reset=*/false, + pending, + &apply_error), + qPrintable(apply_error)); + QVERIFY(!args.GetSettingsPath()); + + const std::optional init_error{ + common::InitConfig(args, [](const bilingual_str&, const std::vector&) { + return true; + }) + }; + QVERIFY2(!init_error, init_error ? init_error->message.original.c_str() : ""); + args.SelectConfigNetwork(args.GetChainTypeString()); + QVERIFY(!args.GetSettingsPath()); + QVERIFY(!args.GetBoolArg("-resetguisettings", false)); + + QString finalize_error; + QVERIFY2( + QmlOnboardingSettings::FinalizeStartupSettings( + args, + bootstrap_gui_settings, + &pending, + nullptr, + &finalize_error), + qPrintable(finalize_error)); + + QCOMPARE( + legacy_core_settings.settings().value(QStringLiteral("fListen")).toBool(), + false); + QCOMPARE( + legacy_core_settings.settings() + .value(QStringLiteral("addrProxy")) + .toString(), + QStringLiteral("10.0.0.1:9050")); + QVERIFY(!legacy_default_settings.settings().contains(SettingsKeys::DATA_DIR)); + QVERIFY(!legacy_default_settings.settings().contains(QStringLiteral("fReset"))); + + QSettings settings; + settings.setFallbacksEnabled(false); + QCOMPARE(settings.value(SettingsKeys::DATA_DIR).toString(), data_dir.path()); +} + void OptionsModelTests::onboardingPreviewHelperReadsSelectedDatadirConfig() { QTemporaryDir data_dir; @@ -3372,35 +4173,6 @@ void OptionsModelTests::onboardingPreviewReadsSelectedDatadirConfig() QVERIFY(!model.listen()); } -void OptionsModelTests::onboardingApplyDoesNotCopyUntouchedConfig() -{ - SavedGuiDataDirSettings saved_settings; - QTemporaryDir data_dir; - QVERIFY(data_dir.isValid()); - - QFile conf(data_dir.filePath(QStringLiteral("bitcoin.conf"))); - QVERIFY(conf.open(QIODevice::WriteOnly | QIODevice::Text)); - QVERIFY(conf.write("listen=0\n") > 0); - conf.close(); - - const std::vector argv = TestArgv(); - OnboardingOptionsModel model(argv, /*can_listen_ipc=*/false); - QVERIFY(model.selectCustomDataDir(data_dir.path())); - QVERIFY(!model.listen()); - - ArgsManager args; - std::string parse_error; - QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(model.applyToArgs(args, &apply_error), qPrintable(apply_error)); - - bool has_listen_override{false}; - args.LockSettings([&](common::Settings& settings) { - has_listen_override = settings.rw_settings.count("listen") > 0; - }); - QVERIFY(!has_listen_override); -} - void OptionsModelTests::onboardingApplyWritesTouchedConfigOverride() { SavedGuiDataDirSettings saved_settings; @@ -3423,11 +4195,20 @@ void OptionsModelTests::onboardingApplyWritesTouchedConfigOverride() ArgsManager args; std::string parse_error; QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(QmlOnboardingSettings::ApplyToArgs(args, data_dir.path(), QSet{QStringLiteral("listen")}, preview.values, &apply_error), qPrintable(apply_error)); + PrepareAndFinalizeApply( + args, + QmlOnboardingSettings::DataDirSelection{ + data_dir.path(), + QmlOnboardingSettings::DataDirSource::UserSelection, + }, + preview.resolved_data_dir, + QSet{QStringLiteral("listen")}, + preview.values); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir.path()); common::SettingsValue listen_override; - args.LockSettings([&](common::Settings& settings) { + check_args.LockSettings([&](common::Settings& settings) { if (const auto* value = common::FindKey(settings.rw_settings, "listen")) { listen_override = *value; } @@ -3464,14 +4245,23 @@ void OptionsModelTests::onboardingApplyWritesTouchedParameterInteractionOverride ArgsManager args; std::string parse_error; QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); - QString apply_error; - QVERIFY2(QmlOnboardingSettings::ApplyToArgs(args, data_dir.path(), QSet{QStringLiteral("listen")}, preview.values, &apply_error), qPrintable(apply_error)); + PrepareAndFinalizeApply( + args, + QmlOnboardingSettings::DataDirSelection{ + data_dir.path(), + QmlOnboardingSettings::DataDirSource::UserSelection, + }, + preview.resolved_data_dir, + QSet{QStringLiteral("listen")}, + preview.values); + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir.path()); common::SettingsValue listen_override; bool has_forced_listen{true}; bool has_forced_natpmp{true}; bool has_forced_discover{true}; - args.LockSettings([&](common::Settings& settings) { + check_args.LockSettings([&](common::Settings& settings) { if (const auto* value = common::FindKey(settings.rw_settings, "listen")) { listen_override = *value; } @@ -3487,6 +4277,308 @@ void OptionsModelTests::onboardingApplyWritesTouchedParameterInteractionOverride QVERIFY(!has_forced_discover); } +void OptionsModelTests::onboardingApplyRetainsListenChoiceWhenDisablingProxy() +{ + SavedGuiDataDirSettings saved_settings; + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + QVERIFY(QDir(data_dir.path()).mkpath(QStringLiteral("regtest"))); + + ArgsManager seed_args; + std::string parse_error; + const std::vector seed_argv{TestArgvWithDataDir(data_dir.path())}; + QVERIFY2(PrepareTestArgs(seed_args, seed_argv, parse_error), parse_error.c_str()); + SelectParams(seed_args.GetChainType()); + seed_args.SelectConfigNetwork(seed_args.GetChainTypeString()); + seed_args.LockSettings([](common::Settings& settings) { + settings.rw_settings["proxy"] = common::SettingsValue{std::string{"10.0.0.3:9050"}}; + }); + std::vector settings_errors; + QVERIFY2( + seed_args.WriteSettingsFile(&settings_errors), + settings_errors.empty() ? "" : settings_errors.front().c_str()); + + const std::vector argv{TestArgv()}; + QmlOnboardingSettings::PreviewResult preview{ + QmlOnboardingSettings::Preview(argv, /*can_listen_ipc=*/false, data_dir.path()) + }; + QVERIFY2(preview.ok, qPrintable(preview.error)); + QVERIFY(preview.values.proxy_enabled); + QVERIFY(!preview.values.listen); + preview.values.proxy_enabled = false; + preview.values.listen = false; + + ArgsManager args; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + PrepareAndFinalizeApply( + args, + QmlOnboardingSettings::DataDirSelection{ + data_dir.path(), + QmlOnboardingSettings::DataDirSource::UserSelection, + }, + preview.resolved_data_dir, + QSet{ + QStringLiteral("proxy"), + QStringLiteral("listen"), + }, + preview.values); + + ArgsManager check_args; + ReadSettingsForDataDir(check_args, data_dir.path()); + common::SettingsValue listen_override; + bool has_proxy_override{true}; + check_args.LockSettings([&](common::Settings& settings) { + if (const auto* value = common::FindKey(settings.rw_settings, "listen")) { + listen_override = *value; + } + has_proxy_override = settings.rw_settings.count("proxy") > 0; + }); + QVERIFY(listen_override.isBool()); + QVERIFY(!listen_override.get_bool()); + QVERIFY(!has_proxy_override); +} + +void OptionsModelTests::onboardingApplyRejectsProfileDrift() +{ + SavedGuiDataDirSettings saved_settings; + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + QVERIFY(QDir(data_dir.path()).mkpath(QStringLiteral("regtest"))); + + QSettings gui_settings; + gui_settings.setFallbacksEnabled(false); + gui_settings.setValue( + QStringLiteral("profileDriftSentinel"), + QStringLiteral("keep")); + gui_settings.sync(); + + const std::vector argv{ + TestArgvWithDataDir(data_dir.path()) + }; + OnboardingOptionsModel model{argv, /*can_listen_ipc=*/false}; + QCOMPARE(model.previewError(), QString{}); + + ArgsManager args; + std::string parse_error; + QVERIFY2(PrepareTestArgs(args, argv, parse_error), parse_error.c_str()); + QmlOnboardingSettings::PendingApply pending; + QString prepare_error; + QVERIFY2( + model.prepareApplyToArgs(args, pending, &prepare_error), + qPrintable(prepare_error)); + QVERIFY(pending.target_complete); + + const std::optional init_error{ + common::InitConfig(args, [](const bilingual_str&, const std::vector&) { + return true; + }) + }; + QVERIFY2(!init_error, init_error ? init_error->message.original.c_str() : ""); + args.SelectConfigNetwork(args.GetChainTypeString()); + + fs::path settings_path; + QVERIFY(args.GetSettingsPath(&settings_path)); + QFile settings_file{ + QString::fromStdString(fs::PathToString(settings_path)) + }; + QVERIFY(settings_file.open(QIODevice::ReadOnly)); + const QByteArray original_settings{settings_file.readAll()}; + settings_file.close(); + + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + enum class Drift { + DataDir, + Chain, + SettingsPath, + Reset, + }; + const std::array drifts{ + Drift::DataDir, + Drift::Chain, + Drift::SettingsPath, + Drift::Reset, + }; + for (const Drift drift : drifts) { + QmlOnboardingSettings::PendingApply changed_pending{pending}; + switch (drift) { + case Drift::DataDir: + changed_pending.resolved_data_dir = + data_dir.filePath(QStringLiteral("other")); + break; + case Drift::Chain: + changed_pending.resolved_chain = QStringLiteral("main"); + break; + case Drift::SettingsPath: + changed_pending.resolved_settings_path += QStringLiteral(".other"); + break; + case Drift::Reset: + changed_pending.effective_reset = !pending.effective_reset; + break; + } + + QString finalize_error; + QVERIFY(!QmlOnboardingSettings::FinalizeStartupSettings( + args, + bootstrap_gui_settings, + &changed_pending, + /*result=*/nullptr, + &finalize_error)); + QVERIFY(finalize_error.contains(QStringLiteral("profile changed"))); + } + + gui_settings.sync(); + QCOMPARE( + gui_settings.value(QStringLiteral("profileDriftSentinel")).toString(), + QStringLiteral("keep")); + QVERIFY(settings_file.open(QIODevice::ReadOnly)); + QCOMPARE(settings_file.readAll(), original_settings); + settings_file.close(); + + fs::path settings_backup_path; + QVERIFY(args.GetSettingsPath( + &settings_backup_path, + /*temp=*/false, + /*backup=*/true)); + QVERIFY(!fs::exists(settings_backup_path)); + QVERIFY(!fs::exists(args.GetDataDirNet() / "guisettings.ini.bak")); +} + +void OptionsModelTests::onboardingFinalizeIgnoresUnusedBootstrapStore() +{ + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + QVERIFY(QDir(data_dir.path()).mkpath(QStringLiteral("regtest"))); + + QTemporaryDir unusable_store_path; + QVERIFY(unusable_store_path.isValid()); + QSettings unusable_store{unusable_store_path.path(), QSettings::IniFormat}; + unusable_store.setFallbacksEnabled(false); + unusable_store.sync(); + QVERIFY(unusable_store.status() != QSettings::NoError); + + ArgsManager args; + std::string parse_error; + QVERIFY2( + PrepareTestArgs(args, TestArgvWithDataDir(data_dir.path()), parse_error), + parse_error.c_str()); + + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + /*organization_name=*/{}, + /*application_name=*/{}, + /*file_name=*/unusable_store_path.path(), + QSettings::IniFormat, + QSettings::UserScope, + }; + InitializeAndFinalizeSettings( + args, + bootstrap_gui_settings, + /*pending=*/nullptr, + /*result=*/nullptr); +} + +void OptionsModelTests::onboardingApplyClearsResetFlagInBootstrapAndActiveStores() +{ + const QString original_app_name{QCoreApplication::applicationName()}; + [[maybe_unused]] const auto restore_app_name{ + qScopeGuard([&] { + QCoreApplication::setApplicationName(original_app_name); + }) + }; + const QString organization_name{QCoreApplication::organizationName()}; + const QString bootstrap_app_name{QStringLiteral("ResetBootstrap")}; + const QString active_app_name{QStringLiteral("ResetActive")}; + SavedNamedSettings bootstrap_settings{organization_name, bootstrap_app_name}; + SavedNamedSettings active_settings{organization_name, active_app_name}; + + QTemporaryDir data_dir; + QVERIFY(data_dir.isValid()); + QVERIFY(QDir(data_dir.path()).mkpath(QStringLiteral("regtest"))); + + QCoreApplication::setApplicationName(bootstrap_app_name); + bootstrap_settings.settings().setValue(SettingsKeys::DATA_DIR, data_dir.path()); + bootstrap_settings.settings().setValue(QStringLiteral("fReset"), true); + bootstrap_settings.settings().setValue( + QStringLiteral("bootstrapResetSentinel"), + QStringLiteral("keep")); + bootstrap_settings.settings().sync(); + const QmlOnboardingSettings::GuiSettingsStore bootstrap_gui_settings{ + QmlOnboardingSettings::CurrentGuiSettingsStore() + }; + + ArgsManager args; + std::string parse_error; + QVERIFY2( + PrepareTestArgs(args, TestArgvWithDataDir(data_dir.path()), parse_error), + parse_error.c_str()); + QmlOnboardingSettings::PendingApply pending; + QString prepare_error; + QVERIFY2( + QmlOnboardingSettings::PrepareApplyToArgs( + args, + { + data_dir.path(), + QmlOnboardingSettings::DataDirSource::ExplicitArg, + }, + data_dir.path(), + {}, + QmlCoreSettings::Values{}, + /*effective_reset=*/false, + pending, + &prepare_error), + qPrintable(prepare_error)); + + const std::optional init_error{ + common::InitConfig(args, [](const bilingual_str&, const std::vector&) { + return true; + }) + }; + QVERIFY2(!init_error, init_error ? init_error->message.original.c_str() : ""); + args.SelectConfigNetwork(args.GetChainTypeString()); + + QCoreApplication::setApplicationName(active_app_name); + active_settings.settings().setValue(SettingsKeys::DATA_DIR, data_dir.path()); + active_settings.settings().setValue(QStringLiteral("fReset"), true); + active_settings.settings().setValue( + QStringLiteral("activeResetSentinel"), + QStringLiteral("keep")); + active_settings.settings().sync(); + QVERIFY( + QmlOnboardingSettings::CurrentGuiSettingsStore().file_name != + bootstrap_gui_settings.file_name); + + QString finalize_error; + QVERIFY2( + QmlOnboardingSettings::FinalizeStartupSettings( + args, + bootstrap_gui_settings, + &pending, + /*result=*/nullptr, + &finalize_error), + qPrintable(finalize_error)); + + bootstrap_settings.settings().sync(); + QCOMPARE( + bootstrap_settings.settings().value(QStringLiteral("fReset")).toBool(), + false); + QCOMPARE( + bootstrap_settings.settings() + .value(QStringLiteral("bootstrapResetSentinel")) + .toString(), + QStringLiteral("keep")); + + active_settings.settings().sync(); + QCOMPARE( + active_settings.settings().value(QStringLiteral("fReset")).toBool(), + false); + QCOMPARE( + active_settings.settings() + .value(QStringLiteral("activeResetSentinel")) + .toString(), + QStringLiteral("keep")); +} + #ifdef BITCOINQML_NO_TEST_MAIN #include BITCOINQML_REGISTER_QT_TEST(OptionsModelTests)