intro.cpp raw

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