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