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