/ src / qt / intro.cpp
intro.cpp
  1  // Copyright (c) 2011-2022 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  #if defined(HAVE_CONFIG_H)
  6  #include <config/bitcoin-config.h>
  7  #endif
  8  
  9  #include <chainparams.h>
 10  #include <qt/intro.h>
 11  #include <qt/forms/ui_intro.h>
 12  #include <util/chaintype.h>
 13  #include <util/fs.h>
 14  
 15  #include <qt/guiconstants.h>
 16  #include <qt/guiutil.h>
 17  #include <qt/optionsmodel.h>
 18  
 19  #include <common/args.h>
 20  #include <interfaces/node.h>
 21  #include <util/fs_helpers.h>
 22  #include <validation.h>
 23  
 24  #include <QFileDialog>
 25  #include <QSettings>
 26  #include <QMessageBox>
 27  
 28  #include <cmath>
 29  
 30  /* Check free space asynchronously to prevent hanging the UI thread.
 31  
 32     Up to one request to check a path is in flight to this thread; when the check()
 33     function runs, the current path is requested from the associated Intro object.
 34     The reply is sent back through a signal.
 35  
 36     This ensures that no queue of checking requests is built up while the user is
 37     still entering the path, and that always the most recently entered path is checked as
 38     soon as the thread becomes available.
 39  */
 40  class FreespaceChecker : public QObject
 41  {
 42      Q_OBJECT
 43  
 44  public:
 45      explicit FreespaceChecker(Intro *intro);
 46  
 47      enum Status {
 48          ST_OK,
 49          ST_ERROR
 50      };
 51  
 52  public Q_SLOTS:
 53      void check();
 54  
 55  Q_SIGNALS:
 56      void reply(int status, const QString &message, quint64 available);
 57  
 58  private:
 59      Intro *intro;
 60  };
 61  
 62  #include <qt/intro.moc>
 63  
 64  FreespaceChecker::FreespaceChecker(Intro *_intro)
 65  {
 66      this->intro = _intro;
 67  }
 68  
 69  void FreespaceChecker::check()
 70  {
 71      QString dataDirStr = intro->getPathToCheck();
 72      fs::path dataDir = GUIUtil::QStringToPath(dataDirStr);
 73      uint64_t freeBytesAvailable = 0;
 74      int replyStatus = ST_OK;
 75      QString replyMessage = tr("A new data directory will be created.");
 76  
 77      /* Find first parent that exists, so that fs::space does not fail */
 78      fs::path parentDir = dataDir;
 79      fs::path parentDirOld = fs::path();
 80      while(parentDir.has_parent_path() && !fs::exists(parentDir))
 81      {
 82          parentDir = parentDir.parent_path();
 83  
 84          /* Check if we make any progress, break if not to prevent an infinite loop here */
 85          if (parentDirOld == parentDir)
 86              break;
 87  
 88          parentDirOld = parentDir;
 89      }
 90  
 91      try {
 92          freeBytesAvailable = fs::space(parentDir).available;
 93          if(fs::exists(dataDir))
 94          {
 95              if(fs::is_directory(dataDir))
 96              {
 97                  QString separator = "<code>" + QDir::toNativeSeparators("/") + tr("name") + "</code>";
 98                  replyStatus = ST_OK;
 99                  replyMessage = tr("Directory already exists. Add %1 if you intend to create a new directory here.").arg(separator);
100              } else {
101                  replyStatus = ST_ERROR;
102                  replyMessage = tr("Path already exists, and is not a directory.");
103              }
104          }
105      } catch (const fs::filesystem_error&)
106      {
107          /* Parent directory does not exist or is not accessible */
108          replyStatus = ST_ERROR;
109          replyMessage = tr("Cannot create data directory here.");
110      }
111      Q_EMIT reply(replyStatus, replyMessage, freeBytesAvailable);
112  }
113  
114  namespace {
115  //! Return pruning size that will be used if automatic pruning is enabled.
116  int GetPruneTargetGB()
117  {
118      int64_t prune_target_mib = gArgs.GetIntArg("-prune", 0);
119      // >1 means automatic pruning is enabled by config, 1 means manual pruning, 0 means no pruning.
120      return prune_target_mib > 1 ? PruneMiBtoGB(prune_target_mib) : DEFAULT_PRUNE_TARGET_GB;
121  }
122  } // namespace
123  
124  Intro::Intro(QWidget *parent, int64_t blockchain_size_gb, int64_t chain_state_size_gb) :
125      QDialog(parent, GUIUtil::dialog_flags),
126      ui(new Ui::Intro),
127      m_blockchain_size_gb(blockchain_size_gb),
128      m_chain_state_size_gb(chain_state_size_gb),
129      m_prune_target_gb{GetPruneTargetGB()}
130  {
131      ui->setupUi(this);
132      ui->welcomeLabel->setText(ui->welcomeLabel->text().arg(PACKAGE_NAME));
133      ui->storageLabel->setText(ui->storageLabel->text().arg(PACKAGE_NAME));
134  
135      ui->lblExplanation1->setText(ui->lblExplanation1->text()
136          .arg(PACKAGE_NAME)
137          .arg(m_blockchain_size_gb)
138          .arg(2009)
139          .arg(tr("Bitcoin"))
140      );
141      ui->lblExplanation2->setText(ui->lblExplanation2->text().arg(PACKAGE_NAME));
142  
143      const int min_prune_target_GB = std::ceil(MIN_DISK_SPACE_FOR_BLOCK_FILES / 1e9);
144      ui->pruneGB->setRange(min_prune_target_GB, std::numeric_limits<int>::max());
145      if (gArgs.IsArgSet("-prune")) {
146          m_prune_checkbox_is_default = false;
147          ui->prune->setChecked(gArgs.GetIntArg("-prune", 0) >= 1);
148          ui->prune->setEnabled(false);
149      }
150      ui->pruneGB->setValue(m_prune_target_gb);
151      ui->pruneGB->setToolTip(ui->prune->toolTip());
152      ui->lblPruneSuffix->setToolTip(ui->prune->toolTip());
153      UpdatePruneLabels(ui->prune->isChecked());
154  
155      connect(ui->prune, &QCheckBox::toggled, [this](bool prune_checked) {
156          m_prune_checkbox_is_default = false;
157          UpdatePruneLabels(prune_checked);
158          UpdateFreeSpaceLabel();
159      });
160      connect(ui->pruneGB, qOverload<int>(&QSpinBox::valueChanged), [this](int prune_GB) {
161          m_prune_target_gb = prune_GB;
162          UpdatePruneLabels(ui->prune->isChecked());
163          UpdateFreeSpaceLabel();
164      });
165  
166      startThread();
167  }
168  
169  Intro::~Intro()
170  {
171      delete ui;
172      /* Ensure thread is finished before it is deleted */
173      thread->quit();
174      thread->wait();
175  }
176  
177  QString Intro::getDataDirectory()
178  {
179      return ui->dataDirectory->text();
180  }
181  
182  void Intro::setDataDirectory(const QString &dataDir)
183  {
184      ui->dataDirectory->setText(dataDir);
185      if(dataDir == GUIUtil::getDefaultDataDirectory())
186      {
187          ui->dataDirDefault->setChecked(true);
188          ui->dataDirectory->setEnabled(false);
189          ui->ellipsisButton->setEnabled(false);
190      } else {
191          ui->dataDirCustom->setChecked(true);
192          ui->dataDirectory->setEnabled(true);
193          ui->ellipsisButton->setEnabled(true);
194      }
195  }
196  
197  int64_t Intro::getPruneMiB() const
198  {
199      switch (ui->prune->checkState()) {
200      case Qt::Checked:
201          return PruneGBtoMiB(m_prune_target_gb);
202      case Qt::Unchecked: default:
203          return 0;
204      }
205  }
206  
207  bool Intro::showIfNeeded(bool& did_show_intro, int64_t& prune_MiB)
208  {
209      did_show_intro = false;
210  
211      QSettings settings;
212      /* If data directory provided on command line, no need to look at settings
213         or show a picking dialog */
214      if(!gArgs.GetArg("-datadir", "").empty())
215          return true;
216      /* 1) Default data directory for operating system */
217      QString dataDir = GUIUtil::getDefaultDataDirectory();
218      /* 2) Allow QSettings to override default dir */
219      dataDir = settings.value("strDataDir", dataDir).toString();
220  
221      if(!fs::exists(GUIUtil::QStringToPath(dataDir)) || gArgs.GetBoolArg("-choosedatadir", DEFAULT_CHOOSE_DATADIR) || settings.value("fReset", false).toBool() || gArgs.GetBoolArg("-resetguisettings", false))
222      {
223          /* Use selectParams here to guarantee Params() can be used by node interface */
224          try {
225              SelectParams(gArgs.GetChainType());
226          } catch (const std::exception&) {
227              return false;
228          }
229  
230          /* If current default data directory does not exist, let the user choose one */
231          Intro intro(nullptr, Params().AssumedBlockchainSize(), Params().AssumedChainStateSize());
232          intro.setDataDirectory(dataDir);
233          intro.setWindowIcon(QIcon(":icons/bitcoin"));
234          did_show_intro = true;
235  
236          while(true)
237          {
238              if(!intro.exec())
239              {
240                  /* Cancel clicked */
241                  return false;
242              }
243              dataDir = intro.getDataDirectory();
244              try {
245                  if (TryCreateDirectories(GUIUtil::QStringToPath(dataDir))) {
246                      // If a new data directory has been created, make wallets subdirectory too
247                      TryCreateDirectories(GUIUtil::QStringToPath(dataDir) / "wallets");
248                  }
249                  break;
250              } catch (const fs::filesystem_error&) {
251                  QMessageBox::critical(nullptr, PACKAGE_NAME,
252                      tr("Error: Specified data directory \"%1\" cannot be created.").arg(dataDir));
253                  /* fall through, back to choosing screen */
254              }
255          }
256  
257          // Additional preferences:
258          prune_MiB = intro.getPruneMiB();
259  
260          settings.setValue("strDataDir", dataDir);
261          settings.setValue("fReset", false);
262      }
263      /* Only override -datadir if different from the default, to make it possible to
264       * override -datadir in the bitcoin.conf file in the default data directory
265       * (to be consistent with bitcoind behavior)
266       */
267      if(dataDir != GUIUtil::getDefaultDataDirectory()) {
268          gArgs.SoftSetArg("-datadir", fs::PathToString(GUIUtil::QStringToPath(dataDir))); // use OS locale for path setting
269      }
270      return true;
271  }
272  
273  void Intro::setStatus(int status, const QString &message, quint64 bytesAvailable)
274  {
275      switch(status)
276      {
277      case FreespaceChecker::ST_OK:
278          ui->errorMessage->setText(message);
279          ui->errorMessage->setStyleSheet("");
280          break;
281      case FreespaceChecker::ST_ERROR:
282          ui->errorMessage->setText(tr("Error") + ": " + message);
283          ui->errorMessage->setStyleSheet("QLabel { color: #800000 }");
284          break;
285      }
286      /* Indicate number of bytes available */
287      if(status == FreespaceChecker::ST_ERROR)
288      {
289          ui->freeSpace->setText("");
290      } else {
291          m_bytes_available = bytesAvailable;
292          if (ui->prune->isEnabled() && m_prune_checkbox_is_default) {
293              ui->prune->setChecked(m_bytes_available < (m_blockchain_size_gb + m_chain_state_size_gb + 10) * GB_BYTES);
294          }
295          UpdateFreeSpaceLabel();
296      }
297      /* Don't allow confirm in ERROR state */
298      ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status != FreespaceChecker::ST_ERROR);
299  }
300  
301  void Intro::UpdateFreeSpaceLabel()
302  {
303      QString freeString = tr("%n GB of space available", "", m_bytes_available / GB_BYTES);
304      if (m_bytes_available < m_required_space_gb * GB_BYTES) {
305          freeString += " " + tr("(of %n GB needed)", "", m_required_space_gb);
306          ui->freeSpace->setStyleSheet("QLabel { color: #800000 }");
307      } else if (m_bytes_available / GB_BYTES - m_required_space_gb < 10) {
308          freeString += " " + tr("(%n GB needed for full chain)", "", m_required_space_gb);
309          ui->freeSpace->setStyleSheet("QLabel { color: #999900 }");
310      } else {
311          ui->freeSpace->setStyleSheet("");
312      }
313      ui->freeSpace->setText(freeString + ".");
314  }
315  
316  void Intro::on_dataDirectory_textChanged(const QString &dataDirStr)
317  {
318      /* Disable OK button until check result comes in */
319      ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
320      checkPath(dataDirStr);
321  }
322  
323  void Intro::on_ellipsisButton_clicked()
324  {
325      QString dir = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(nullptr, tr("Choose data directory"), ui->dataDirectory->text()));
326      if(!dir.isEmpty())
327          ui->dataDirectory->setText(dir);
328  }
329  
330  void Intro::on_dataDirDefault_clicked()
331  {
332      setDataDirectory(GUIUtil::getDefaultDataDirectory());
333  }
334  
335  void Intro::on_dataDirCustom_clicked()
336  {
337      ui->dataDirectory->setEnabled(true);
338      ui->ellipsisButton->setEnabled(true);
339  }
340  
341  void Intro::startThread()
342  {
343      thread = new QThread(this);
344      FreespaceChecker *executor = new FreespaceChecker(this);
345      executor->moveToThread(thread);
346  
347      connect(executor, &FreespaceChecker::reply, this, &Intro::setStatus);
348      connect(this, &Intro::requestCheck, executor, &FreespaceChecker::check);
349      /*  make sure executor object is deleted in its own thread */
350      connect(thread, &QThread::finished, executor, &QObject::deleteLater);
351  
352      thread->start();
353  }
354  
355  void Intro::checkPath(const QString &dataDir)
356  {
357      mutex.lock();
358      pathToCheck = dataDir;
359      if(!signalled)
360      {
361          signalled = true;
362          Q_EMIT requestCheck();
363      }
364      mutex.unlock();
365  }
366  
367  QString Intro::getPathToCheck()
368  {
369      QString retval;
370      mutex.lock();
371      retval = pathToCheck;
372      signalled = false; /* new request can be queued now */
373      mutex.unlock();
374      return retval;
375  }
376  
377  void Intro::UpdatePruneLabels(bool prune_checked)
378  {
379      m_required_space_gb = m_blockchain_size_gb + m_chain_state_size_gb;
380      QString storageRequiresMsg = tr("At least %1 GB of data will be stored in this directory, and it will grow over time.");
381      if (prune_checked && m_prune_target_gb <= m_blockchain_size_gb) {
382          m_required_space_gb = m_prune_target_gb + m_chain_state_size_gb;
383          storageRequiresMsg = tr("Approximately %1 GB of data will be stored in this directory.");
384      }
385      ui->lblExplanation3->setVisible(prune_checked);
386      ui->pruneGB->setEnabled(prune_checked);
387      static constexpr uint64_t nPowTargetSpacing = 10 * 60;  // from chainparams, which we don't have at this stage
388      static constexpr uint32_t expected_block_data_size = 2250000;  // includes undo data
389      const uint64_t expected_backup_days = m_prune_target_gb * 1e9 / (uint64_t(expected_block_data_size) * 86400 / nPowTargetSpacing);
390      ui->lblPruneSuffix->setText(
391          //: Explanatory text on the capability of the current prune target.
392          tr("(sufficient to restore backups %n day(s) old)", "", expected_backup_days));
393      ui->sizeWarningLabel->setText(
394          tr("%1 will download and store a copy of the Bitcoin block chain.").arg(PACKAGE_NAME) + " " +
395          storageRequiresMsg.arg(m_required_space_gb) + " " +
396          tr("The wallet will also be stored in this directory.")
397      );
398      this->adjustSize();
399  }