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 }