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
__anonbe6b4cac0202(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