intro.cpp raw
1 // Copyright (c) 2011-2022 The Limenka 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 #include <limenka-build-config.h> // IWYU pragma: keep
6
7 #include <chainparams.h>
8 #include <qt/intro.h>
9 #include <qt/forms/ui_intro.h>
10 #include <util/chaintype.h>
11 #include <util/fs.h>
12
13 #include <qt/guiconstants.h>
14 #include <qt/guiutil.h>
15 #include <qt/optionsmodel.h>
16
17 #include <common/args.h>
18 #include <interfaces/node.h>
19 #include <node/interface_ui.h>
20 #include <util/fs_helpers.h>
21 #include <util/translation.h>
22 #include <validation.h>
23
24 #include <QFileDialog>
25 #include <QSettings>
26 #include <QMessageBox>
27
28 #include <cmath>
29 #include <cstdlib>
30
31 /* Check free space asynchronously to prevent hanging the UI thread.
32
33 Up to one request to check a path is in flight to this thread; when the check()
34 function runs, the current path is requested from the associated Intro object.
35 The reply is sent back through a signal.
36
37 This ensures that no queue of checking requests is built up while the user is
38 still entering the path, and that always the most recently entered path is checked as
39 soon as the thread becomes available.
40 */
41 class FreespaceChecker : public QObject
42 {
43 Q_OBJECT
44
45 public:
46 explicit FreespaceChecker(Intro *intro);
47
48 enum Status {
49 ST_OK,
50 ST_ERROR
51 };
52
53 public Q_SLOTS:
54 void check();
55
56 Q_SIGNALS:
57 void reply(int status, const QString &message, quint64 available);
58
59 private:
60 Intro *intro;
61 };
62
63 #include <qt/intro.moc>
64
65 FreespaceChecker::FreespaceChecker(Intro *_intro)
66 {
67 this->intro = _intro;
68 }
69
70 void FreespaceChecker::check()
71 {
72 QString dataDirStr = intro->getPathToCheck();
73 fs::path dataDir = GUIUtil::QStringToPath(dataDirStr);
74 uint64_t freeBytesAvailable = 0;
75 int replyStatus = ST_OK;
76 QString replyMessage = tr("A new data directory will be created.");
77
78 /* Find first parent that exists, so that fs::space does not fail */
79 fs::path parentDir = dataDir;
80 fs::path parentDirOld = fs::path();
81 while(parentDir.has_parent_path() && !fs::exists(parentDir))
82 {
83 parentDir = parentDir.parent_path();
84
85 /* Check if we make any progress, break if not to prevent an infinite loop here */
86 if (parentDirOld == parentDir)
87 break;
88
89 parentDirOld = parentDir;
90 }
91
92 try {
93 freeBytesAvailable = fs::space(parentDir).available;
94 if(fs::exists(dataDir))
95 {
96 if(fs::is_directory(dataDir))
97 {
98 QString separator = "<code>" + QDir::toNativeSeparators("/") + tr("name") + "</code>";
99 replyStatus = ST_OK;
100 replyMessage = tr("Directory already exists. Add %1 if you intend to create a new directory here.").arg(separator);
101 } else {
102 replyStatus = ST_ERROR;
103 replyMessage = tr("Path already exists, and is not a directory.");
104 }
105 }
106 } catch (const fs::filesystem_error&)
107 {
108 /* Parent directory does not exist or is not accessible */
109 replyStatus = ST_ERROR;
110 replyMessage = tr("Cannot create data directory here.");
111 }
112 Q_EMIT reply(replyStatus, replyMessage, freeBytesAvailable);
113 }
114
115 namespace {
116 //! Return pruning size that will be used if automatic pruning is enabled.
117 int GetPruneTargetMiB()
118 {
119 int64_t prune_target_mib = gArgs.GetIntArg("-prune", 0);
120 // >1 means automatic pruning is enabled by config, 1 means manual pruning, 0 means no pruning.
121 return prune_target_mib > 1 ? prune_target_mib : DEFAULT_PRUNE_TARGET_MiB;
122 }
123 } // namespace
124
125 Intro::Intro(QWidget *parent, int64_t blockchain_size_gb, int64_t chain_state_size_gb) :
126 QDialog(parent, GUIUtil::dialog_flags),
127 ui(new Ui::Intro),
128 m_blockchain_size_gb(blockchain_size_gb),
129 m_chain_state_size_gb(chain_state_size_gb),
130 m_prune_target_mib{GetPruneTargetMiB()}
131 {
132 ui->setupUi(this);
133 ui->welcomeLabel->setText(ui->welcomeLabel->text().arg(CLIENT_NAME));
134 ui->storageLabel->setText(ui->storageLabel->text().arg(CLIENT_NAME));
135
136 ui->lblExplanation1->setText(ui->lblExplanation1->text()
137 .arg(CLIENT_NAME)
138 .arg(m_blockchain_size_gb)
139 .arg(2009)
140 .arg(tr("Limenka"))
141 );
142 ui->lblExplanation2->setText(ui->lblExplanation2->text().arg(CLIENT_NAME));
143
144 const int min_prune_target_MiB = (MIN_DISK_SPACE_FOR_BLOCK_FILES + MiB_BYTES - 1) / MiB_BYTES;
145 ui->pruneMiB->setRange(min_prune_target_MiB, std::numeric_limits<int>::max());
146 if (gArgs.IsArgSet("-prune")) {
147 m_prune_checkbox_is_default = false;
148 switch (gArgs.GetIntArg("-prune", 0)) {
149 case 0:
150 ui->prune->setChecked(false);
151 break;
152 case 1:
153 ui->prune->setTristate();
154 ui->prune->setCheckState(Qt::PartiallyChecked);
155 break;
156 default:
157 ui->prune->setChecked(true);
158 }
159 }
160 ui->pruneMiB->setValue(m_prune_target_mib);
161 ui->pruneMiB->setToolTip(ui->prune->toolTip());
162 ui->lblPruneSuffix->setToolTip(ui->prune->toolTip());
163 UpdatePruneLabels(ui->prune->checkState() == Qt::Checked);
164
165 #if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0))
166 connect(ui->prune, &QCheckBox::checkStateChanged, [this](const Qt::CheckState prune_state) {
167 #else
168 connect(ui->prune, &QCheckBox::stateChanged, [this](const int prune_state) {
169 #endif
170 m_prune_checkbox_is_default = false;
171 UpdatePruneLabels(prune_state == Qt::Checked);
172 UpdateFreeSpaceLabel();
173 });
174 connect(ui->pruneMiB, qOverload<int>(&QSpinBox::valueChanged), [this](int prune_MiB) {
175 m_prune_target_mib = prune_MiB;
176 UpdatePruneLabels(ui->prune->checkState() == Qt::Checked);
177 UpdateFreeSpaceLabel();
178 });
179
180 bool have_user_assumevalid = false;
181 if (gArgs.IsArgSet("-assumevalid")) {
182 const auto user_assumevalid = gArgs.GetArg("-assumevalid", /* ignored default; determines return type */ "");
183 const auto block_hash{uint256::FromUserHex(user_assumevalid)};
184 if (block_hash && !block_hash->IsNull()) {
185 // -assumevalid=blockhash: initialise with the user-specified value, enabled
186 ui->assumevalid->setChecked(true);
187 ui->assumevalidBlock->setText(QString::fromStdString(user_assumevalid));
188 have_user_assumevalid = true;
189 } else {
190 // -assumevalid=0: default checkbox to off, and initialise with chainparams later
191 ui->assumevalid->setChecked(false);
192 }
193 }
194 if (!have_user_assumevalid) {
195 const auto chainparams = CreateChainParams(gArgs, gArgs.GetChainType());
196 const uint256 default_assumevalid = chainparams ? chainparams->GetConsensus().defaultAssumeValid : uint256();
197 if (default_assumevalid.IsNull()) {
198 // no chainparams assumevalid (nor user-provided), so hide the options entirely
199 ui->groupAssumeValid->setVisible(false);
200 } else {
201 // assumevalid from chainparams only (normal case): disable editing of blockhash
202 ui->assumevalidBlock->setText(QString::fromStdString(default_assumevalid.GetHex()));
203 ui->assumevalidBlock->setReadOnly(true);
204 }
205 }
206 {
207 // TODO: Ideally, we would include actual margins here (instead of extra digits), but this seems non-trivial
208 const int text_width = ui->assumevalidBlock->fontMetrics().horizontalAdvance(QStringLiteral("4")) * (64 + 4);
209 ui->assumevalidBlock->setFixedWidth(text_width);
210 }
211
212 startThread();
213 }
214
215 Intro::~Intro()
216 {
217 delete ui;
218 /* Ensure thread is finished before it is deleted */
219 thread->quit();
220 thread->wait();
221 }
222
223 QString Intro::getDataDirectory()
224 {
225 return ui->dataDirectory->text();
226 }
227
228 void Intro::setDataDirectory(const QString &dataDir)
229 {
230 ui->dataDirectory->setText(dataDir);
231 if(dataDir == GUIUtil::getDefaultDataDirectory())
232 {
233 ui->dataDirDefault->setChecked(true);
234 ui->dataDirectory->setEnabled(false);
235 ui->ellipsisButton->setEnabled(false);
236 } else {
237 ui->dataDirCustom->setChecked(true);
238 ui->dataDirectory->setEnabled(true);
239 ui->ellipsisButton->setEnabled(true);
240 }
241 }
242
243 int64_t Intro::getPruneMiB() const
244 {
245 switch (ui->prune->checkState()) {
246 case Qt::Checked:
247 return m_prune_target_mib;
248 case Qt::PartiallyChecked:
249 return 1;
250 case Qt::Unchecked: default:
251 return 0;
252 }
253 }
254
255 QString Intro::getAssumeValid() const
256 {
257 if (!ui->assumevalid->isChecked()) {
258 return QStringLiteral("0");
259 }
260 return ui->assumevalidBlock->text();
261 }
262
263 bool Intro::showIfNeeded(std::unique_ptr<Intro>& intro)
264 {
265 intro.reset();
266
267 QSettings settings;
268 /* If data directory provided on command line, no need to look at settings
269 or show a picking dialog */
270 if(!gArgs.GetArg("-datadir", "").empty())
271 return true;
272 /* 1) Default data directory for operating system */
273 QString dataDir = GUIUtil::getDefaultDataDirectory();
274 /* 2) Allow QSettings to override default dir */
275 dataDir = settings.value("strDataDir", dataDir).toString();
276
277 if(!fs::exists(GUIUtil::QStringToPath(dataDir)) || gArgs.GetBoolArg("-choosedatadir", DEFAULT_CHOOSE_DATADIR) || settings.value("fReset", false).toBool() || gArgs.GetBoolArg("-resetguisettings", false))
278 {
279 /* Use selectParams here to guarantee Params() can be used by node interface */
280 try {
281 SelectParams(gArgs.GetChainType());
282 } catch (const std::exception& e) {
283 InitError(Untranslated(e.what()));
284 QMessageBox::critical(nullptr, CLIENT_NAME, QObject::tr("Error: %1").arg(QString(e.what())));
285 std::exit(EXIT_FAILURE);
286 }
287
288 /* If current default data directory does not exist, let the user choose one */
289 intro = std::make_unique<Intro>(nullptr, Params().AssumedBlockchainSize(), Params().AssumedChainStateSize());
290 intro->setDataDirectory(dataDir);
291 intro->setWindowIcon(QIcon(QStringLiteral(":icons/limenka")));
292
293 while(true)
294 {
295 if(!intro->exec())
296 {
297 /* Cancel clicked */
298 return false;
299 }
300 dataDir = intro->getDataDirectory();
301 try {
302 if (TryCreateDirectories(GUIUtil::QStringToPath(dataDir))) {
303 // If a new data directory has been created, make wallets subdirectory too
304 TryCreateDirectories(GUIUtil::QStringToPath(dataDir) / "wallets");
305 }
306 break;
307 } catch (const fs::filesystem_error&) {
308 QMessageBox::critical(nullptr, CLIENT_NAME,
309 tr("Error: Specified data directory \"%1\" cannot be created.").arg(dataDir));
310 /* fall through, back to choosing screen */
311 }
312 }
313
314 settings.setValue("strDataDir", dataDir);
315 settings.setValue("fReset", false);
316 }
317 /* Only override -datadir if different from the default, to make it possible to
318 * override -datadir in the limenka.conf file in the default data directory
319 * (to be consistent with limenkad behavior)
320 */
321 if(dataDir != GUIUtil::getDefaultDataDirectory()) {
322 gArgs.SoftSetArg("-datadir", fs::PathToString(GUIUtil::QStringToPath(dataDir))); // use OS locale for path setting
323 }
324 return true;
325 }
326
327 void Intro::setStatus(int status, const QString &message, quint64 bytesAvailable)
328 {
329 switch(status)
330 {
331 case FreespaceChecker::ST_OK:
332 ui->errorMessage->setText(message);
333 ui->errorMessage->setStyleSheet("");
334 break;
335 case FreespaceChecker::ST_ERROR:
336 ui->errorMessage->setText(tr("Error") + ": " + message);
337 ui->errorMessage->setStyleSheet("QLabel { color: #800000 }");
338 break;
339 }
340 /* Indicate number of bytes available */
341 if(status == FreespaceChecker::ST_ERROR)
342 {
343 ui->freeSpace->setText("");
344 } else {
345 m_bytes_available = bytesAvailable;
346 if (ui->prune->isEnabled() && m_prune_checkbox_is_default) {
347 ui->prune->setChecked(m_bytes_available < (m_blockchain_size_gb + m_chain_state_size_gb + 10) * GB_BYTES);
348 }
349 UpdateFreeSpaceLabel();
350 }
351 /* Don't allow confirm in ERROR state */
352 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status != FreespaceChecker::ST_ERROR);
353 }
354
355 void Intro::UpdateFreeSpaceLabel()
356 {
357 QString freeString = tr("%n GB of space available", "", m_bytes_available / GB_BYTES);
358 if (m_bytes_available < m_required_space_gb * GB_BYTES) {
359 freeString += " " + tr("(of %n GB needed)", "", m_required_space_gb);
360 ui->freeSpace->setStyleSheet("QLabel { color: #800000 }");
361 } else if (m_bytes_available / GB_BYTES - m_required_space_gb < 10) {
362 freeString += " " + tr("(%n GB needed)", "", m_required_space_gb);
363 ui->freeSpace->setStyleSheet("QLabel { color: #999900 }");
364 } else {
365 ui->freeSpace->setStyleSheet("");
366 }
367 ui->freeSpace->setText(freeString + ".");
368 }
369
370 void Intro::on_dataDirectory_textChanged(const QString &dataDirStr)
371 {
372 /* Disable OK button until check result comes in */
373 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
374 checkPath(dataDirStr);
375 }
376
377 void Intro::on_ellipsisButton_clicked()
378 {
379 QString dir = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(nullptr, tr("Choose data directory"), ui->dataDirectory->text()));
380 if(!dir.isEmpty())
381 ui->dataDirectory->setText(dir);
382 }
383
384 void Intro::on_dataDirDefault_clicked()
385 {
386 setDataDirectory(GUIUtil::getDefaultDataDirectory());
387 }
388
389 void Intro::on_dataDirCustom_clicked()
390 {
391 ui->dataDirectory->setEnabled(true);
392 ui->ellipsisButton->setEnabled(true);
393 }
394
395 void Intro::startThread()
396 {
397 thread = new QThread(this);
398 FreespaceChecker *executor = new FreespaceChecker(this);
399 executor->moveToThread(thread);
400
401 connect(executor, &FreespaceChecker::reply, this, &Intro::setStatus);
402 connect(this, &Intro::requestCheck, executor, &FreespaceChecker::check);
403 /* make sure executor object is deleted in its own thread */
404 connect(thread, &QThread::finished, executor, &QObject::deleteLater);
405
406 thread->start();
407 }
408
409 void Intro::checkPath(const QString &dataDir)
410 {
411 mutex.lock();
412 pathToCheck = dataDir;
413 if(!signalled)
414 {
415 signalled = true;
416 Q_EMIT requestCheck();
417 }
418 mutex.unlock();
419 }
420
421 QString Intro::getPathToCheck()
422 {
423 QString retval;
424 mutex.lock();
425 retval = pathToCheck;
426 signalled = false; /* new request can be queued now */
427 mutex.unlock();
428 return retval;
429 }
430
431 void Intro::UpdatePruneLabels(bool prune_checked)
432 {
433 m_required_space_gb = m_blockchain_size_gb + m_chain_state_size_gb;
434 QString storageRequiresMsg = tr("At least %1 GB of data will be stored in this directory, and it will grow over time.");
435 const int64_t prune_target_gb = (m_prune_target_mib * MiB_BYTES + GB_BYTES - 1) / GB_BYTES;
436 if (prune_checked && prune_target_gb <= m_blockchain_size_gb) {
437 m_required_space_gb = prune_target_gb + m_chain_state_size_gb;
438 storageRequiresMsg = tr("Approximately %1 GB of data will be stored in this directory.");
439 }
440 ui->pruneMiB->setEnabled(prune_checked);
441 static constexpr uint64_t nPowTargetSpacing = 10 * 60; // from chainparams, which we don't have at this stage
442 static constexpr uint32_t expected_block_data_size = 2250000; // includes undo data
443 const uint64_t expected_backup_days = m_prune_target_mib * MiB_BYTES / (uint64_t(expected_block_data_size) * 86400 / nPowTargetSpacing);
444 ui->lblPruneSuffix->setText(
445 //: Explanatory text on the capability of the current prune target.
446 tr("(sufficient to restore backups %n day(s) old)", "", expected_backup_days));
447 ui->sizeWarningLabel->setText(
448 tr("%1 will download and store a copy of the Limenka block chain.").arg(CLIENT_NAME) + " " +
449 storageRequiresMsg.arg(m_required_space_gb) + " " +
450 tr("The wallet will also be stored in this directory.")
451 );
452 this->adjustSize();
453 }
454