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