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