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 }