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