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)