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