intro.cpp raw

   1  // Copyright (c) 2011-present The Bitcoin Core developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <bitcoin-build-config.h> // IWYU pragma: keep
   6  
   7  #include <chainparams.h>
   8  #include <qt/intro.h>
   9  #include <qt/forms/ui_intro.h>
  10  #include <util/chaintype.h>
  11  #include <util/fs.h>
  12  
  13  #include <qt/freespacechecker.h>
  14  #include <qt/guiconstants.h>
  15  #include <qt/guiutil.h>
  16  #include <qt/optionsmodel.h>
  17  
  18  #include <common/args.h>
  19  #include <interfaces/node.h>
  20  #include <node/interface_ui.h>
  21  #include <util/fs_helpers.h>
  22  #include <util/translation.h>
  23  #include <validation.h>
  24  
  25  #include <QFileDialog>
  26  #include <QSettings>
  27  #include <QMessageBox>
  28  
  29  #include <cmath>
  30  #include <cstdlib>
  31  
  32  namespace {
  33  //! Return pruning size that will be used if automatic pruning is enabled.
  34  int GetPruneTargetGB()
  35  {
  36      int64_t prune_target_mib = gArgs.GetIntArg("-prune", 0);
  37      // >1 means automatic pruning is enabled by config, 1 means manual pruning, 0 means no pruning.
  38      return prune_target_mib > 1 ? PruneMiBtoGB(prune_target_mib) : DEFAULT_PRUNE_TARGET_GB;
  39  }
  40  } // namespace
  41  
  42  Intro::Intro(QWidget *parent, int64_t blockchain_size_gb, int64_t chain_state_size_gb) :
  43      QDialog(parent, GUIUtil::dialog_flags),
  44      ui(new Ui::Intro),
  45      m_blockchain_size_gb(blockchain_size_gb),
  46      m_chain_state_size_gb(chain_state_size_gb),
  47      m_prune_target_gb{GetPruneTargetGB()}
  48  {
  49      ui->setupUi(this);
  50      ui->welcomeLabel->setText(ui->welcomeLabel->text().arg(CLIENT_NAME));
  51      ui->storageLabel->setText(ui->storageLabel->text().arg(CLIENT_NAME));
  52  
  53      ui->lblExplanation1->setText(ui->lblExplanation1->text()
  54          .arg(CLIENT_NAME)
  55          .arg(m_blockchain_size_gb)
  56          .arg(2009)
  57          .arg(tr("Bitcoin"))
  58      );
  59      ui->lblExplanation2->setText(ui->lblExplanation2->text().arg(CLIENT_NAME));
  60  
  61      const int min_prune_target_GB = std::ceil(MIN_DISK_SPACE_FOR_BLOCK_FILES / 1e9);
  62      ui->pruneGB->setRange(min_prune_target_GB, std::numeric_limits<int>::max());
  63      if (const auto arg{gArgs.GetIntArg("-prune")}) {
  64          m_prune_checkbox_is_default = false;
  65          ui->prune->setChecked(*arg >= 1);
  66          ui->prune->setEnabled(false);
  67      }
  68      ui->pruneGB->setValue(m_prune_target_gb);
  69      ui->pruneGB->setToolTip(ui->prune->toolTip());
  70      ui->lblPruneSuffix->setToolTip(ui->prune->toolTip());
  71      UpdatePruneLabels(ui->prune->isChecked());
  72  
  73      connect(ui->prune, &QCheckBox::toggled, [this](bool prune_checked) {
  74          m_prune_checkbox_is_default = false;
  75          UpdatePruneLabels(prune_checked);
  76          UpdateFreeSpaceLabel();
  77      });
  78      connect(ui->pruneGB, qOverload<int>(&QSpinBox::valueChanged), [this](int prune_GB) {
  79          m_prune_target_gb = prune_GB;
  80          UpdatePruneLabels(ui->prune->isChecked());
  81          UpdateFreeSpaceLabel();
  82      });
  83  
  84      startThread();
  85  }
  86  
  87  Intro::~Intro()
  88  {
  89      delete ui;
  90      /* Ensure thread is finished before it is deleted */
  91      thread->quit();
  92      thread->wait();
  93  }
  94  
  95  QString Intro::getDataDirectory()
  96  {
  97      return ui->dataDirectory->text();
  98  }
  99  
 100  void Intro::setDataDirectory(const QString &dataDir)
 101  {
 102      ui->dataDirectory->setText(dataDir);
 103      if(dataDir == GUIUtil::getDefaultDataDirectory())
 104      {
 105          ui->dataDirDefault->setChecked(true);
 106          ui->dataDirectory->setEnabled(false);
 107          ui->ellipsisButton->setEnabled(false);
 108      } else {
 109          ui->dataDirCustom->setChecked(true);
 110          ui->dataDirectory->setEnabled(true);
 111          ui->ellipsisButton->setEnabled(true);
 112      }
 113  }
 114  
 115  int64_t Intro::getPruneMiB() const
 116  {
 117      switch (ui->prune->checkState()) {
 118      case Qt::Checked:
 119          return PruneGBtoMiB(m_prune_target_gb);
 120      case Qt::Unchecked: default:
 121          return 0;
 122      }
 123  }
 124  
 125  bool Intro::showIfNeeded(bool& did_show_intro, int64_t& prune_MiB)
 126  {
 127      did_show_intro = false;
 128  
 129      QSettings settings;
 130      /* If data directory provided on command line, no need to look at settings
 131         or show a picking dialog */
 132      if(!gArgs.GetArg("-datadir", "").empty())
 133          return true;
 134      /* 1) Default data directory for operating system */
 135      QString dataDir = GUIUtil::getDefaultDataDirectory();
 136      /* 2) Allow QSettings to override default dir */
 137      dataDir = settings.value("strDataDir", dataDir).toString();
 138  
 139      if(!fs::exists(GUIUtil::QStringToPath(dataDir)) || gArgs.GetBoolArg("-choosedatadir", DEFAULT_CHOOSE_DATADIR) || settings.value("fReset", false).toBool() || gArgs.GetBoolArg("-resetguisettings", false))
 140      {
 141          /* Use selectParams here to guarantee Params() can be used by node interface */
 142          try {
 143              SelectParams(gArgs.GetChainType());
 144          } catch (const std::exception& e) {
 145              InitError(Untranslated(e.what()));
 146              QMessageBox::critical(nullptr, CLIENT_NAME, QObject::tr("Error: %1").arg(QString(e.what())));
 147              std::exit(EXIT_FAILURE);
 148          }
 149  
 150          /* If current default data directory does not exist, let the user choose one */
 151          Intro intro(nullptr, Params().AssumedBlockchainSize(), Params().AssumedChainStateSize());
 152          intro.setDataDirectory(dataDir);
 153          intro.setWindowIcon(QIcon(":icons/bitcoin"));
 154          did_show_intro = true;
 155  
 156          while(true)
 157          {
 158              if(!intro.exec())
 159              {
 160                  /* Cancel clicked */
 161                  return false;
 162              }
 163              dataDir = intro.getDataDirectory();
 164              try {
 165                  if (TryCreateDirectories(GUIUtil::QStringToPath(dataDir))) {
 166                      // If a new data directory has been created, make wallets subdirectory too
 167                      TryCreateDirectories(GUIUtil::QStringToPath(dataDir) / "wallets");
 168                  }
 169                  break;
 170              } catch (const fs::filesystem_error&) {
 171                  QMessageBox::critical(nullptr, CLIENT_NAME,
 172                      tr("Error: Specified data directory \"%1\" cannot be created.").arg(dataDir));
 173                  /* fall through, back to choosing screen */
 174              }
 175          }
 176  
 177          // Additional preferences:
 178          prune_MiB = intro.getPruneMiB();
 179  
 180          settings.setValue("strDataDir", dataDir);
 181          settings.setValue("fReset", false);
 182      }
 183      /* Only override -datadir if different from the default, to make it possible to
 184       * override -datadir in the bitcoin.conf file in the default data directory
 185       * (to be consistent with bitcoind behavior)
 186       */
 187      if(dataDir != GUIUtil::getDefaultDataDirectory()) {
 188          gArgs.SoftSetArg("-datadir", fs::PathToString(GUIUtil::QStringToPath(dataDir))); // use OS locale for path setting
 189      }
 190      return true;
 191  }
 192  
 193  void Intro::setStatus(int status, const QString &message, quint64 bytesAvailable)
 194  {
 195      switch(status)
 196      {
 197      case FreespaceChecker::ST_OK:
 198          ui->errorMessage->setText(message);
 199          ui->errorMessage->setStyleSheet("");
 200          break;
 201      case FreespaceChecker::ST_ERROR:
 202          ui->errorMessage->setText(tr("Error") + ": " + message);
 203          ui->errorMessage->setStyleSheet("QLabel { color: #800000 }");
 204          break;
 205      }
 206      /* Indicate number of bytes available */
 207      if(status == FreespaceChecker::ST_ERROR)
 208      {
 209          ui->freeSpace->setText("");
 210      } else {
 211          m_bytes_available = bytesAvailable;
 212          if (ui->prune->isEnabled() && m_prune_checkbox_is_default) {
 213              ui->prune->setChecked(m_bytes_available < (m_blockchain_size_gb + m_chain_state_size_gb + 10) * GB_BYTES);
 214          }
 215          UpdateFreeSpaceLabel();
 216      }
 217      /* Don't allow confirm in ERROR state */
 218      ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status != FreespaceChecker::ST_ERROR);
 219  }
 220  
 221  void Intro::UpdateFreeSpaceLabel()
 222  {
 223      QString freeString = tr("%n GB of space available", "", m_bytes_available / GB_BYTES);
 224      if (m_bytes_available < m_required_space_gb * GB_BYTES) {
 225          freeString += " " + tr("(of %n GB needed)", "", m_required_space_gb);
 226          ui->freeSpace->setStyleSheet("QLabel { color: #800000 }");
 227      } else if (m_bytes_available / GB_BYTES - m_required_space_gb < 10) {
 228          freeString += " " + tr("(%n GB needed for full chain)", "", m_required_space_gb);
 229          ui->freeSpace->setStyleSheet("QLabel { color: #999900 }");
 230      } else {
 231          ui->freeSpace->setStyleSheet("");
 232      }
 233      ui->freeSpace->setText(freeString + ".");
 234  }
 235  
 236  void Intro::on_dataDirectory_textChanged(const QString &dataDirStr)
 237  {
 238      /* Disable OK button until check result comes in */
 239      ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
 240      checkPath(dataDirStr);
 241  }
 242  
 243  void Intro::on_ellipsisButton_clicked()
 244  {
 245      QString dir = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(nullptr, tr("Choose data directory"), ui->dataDirectory->text()));
 246      if(!dir.isEmpty())
 247          ui->dataDirectory->setText(dir);
 248  }
 249  
 250  void Intro::on_dataDirDefault_clicked()
 251  {
 252      setDataDirectory(GUIUtil::getDefaultDataDirectory());
 253  }
 254  
 255  void Intro::on_dataDirCustom_clicked()
 256  {
 257      ui->dataDirectory->setEnabled(true);
 258      ui->ellipsisButton->setEnabled(true);
 259  }
 260  
 261  void Intro::startThread()
 262  {
 263      thread = new QThread(this);
 264      FreespaceChecker *executor = new FreespaceChecker(this);
 265      executor->moveToThread(thread);
 266  
 267      connect(executor, &FreespaceChecker::reply, this, &Intro::setStatus);
 268      connect(this, &Intro::requestCheck, executor, &FreespaceChecker::check);
 269      /*  make sure executor object is deleted in its own thread */
 270      connect(thread, &QThread::finished, executor, &QObject::deleteLater);
 271  
 272      thread->start();
 273  }
 274  
 275  void Intro::checkPath(const QString &dataDir)
 276  {
 277      mutex.lock();
 278      pathToCheck = dataDir;
 279      if(!signalled)
 280      {
 281          signalled = true;
 282          Q_EMIT requestCheck();
 283      }
 284      mutex.unlock();
 285  }
 286  
 287  QString Intro::getPathToCheck()
 288  {
 289      QString retval;
 290      mutex.lock();
 291      retval = pathToCheck;
 292      signalled = false; /* new request can be queued now */
 293      mutex.unlock();
 294      return retval;
 295  }
 296  
 297  void Intro::UpdatePruneLabels(bool prune_checked)
 298  {
 299      m_required_space_gb = m_blockchain_size_gb + m_chain_state_size_gb;
 300      QString storageRequiresMsg = tr("At least %1 GB of data will be stored in this directory, and it will grow over time.");
 301      if (prune_checked && m_prune_target_gb <= m_blockchain_size_gb) {
 302          m_required_space_gb = m_prune_target_gb + m_chain_state_size_gb;
 303          storageRequiresMsg = tr("Approximately %1 GB of data will be stored in this directory.");
 304      }
 305      ui->lblExplanation3->setVisible(prune_checked);
 306      ui->pruneGB->setEnabled(prune_checked);
 307      static constexpr uint64_t nPowTargetSpacing = 10 * 60;  // from chainparams, which we don't have at this stage
 308      static constexpr uint32_t expected_block_data_size = 2250000;  // includes undo data
 309      const uint64_t expected_backup_days = m_prune_target_gb * 1e9 / (uint64_t(expected_block_data_size) * 86400 / nPowTargetSpacing);
 310      ui->lblPruneSuffix->setText(
 311          //: Explanatory text on the capability of the current prune target.
 312          tr("(sufficient to restore backups %n day(s) old)", "", expected_backup_days));
 313      ui->sizeWarningLabel->setText(
 314          tr("%1 will download and store a copy of the Bitcoin block chain.").arg(CLIENT_NAME) + " " +
 315          storageRequiresMsg.arg(m_required_space_gb) + " " +
 316          tr("The wallet will also be stored in this directory.")
 317      );
 318      this->adjustSize();
 319  }
 320