Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1b137a7bb | |||
| 939606a5f7 | |||
| 73cecd90d1 | |||
| c74c92c542 | |||
| 97ae675f0a | |||
| b0b591364f | |||
| bf257858a4 | |||
| 16611efe72 | |||
| 1f3deacb7a | |||
| 8847571193 | |||
| a58eb3e9ef | |||
| dfb4b221dd |
Regular → Executable
+7
-2
@@ -15,8 +15,13 @@ if [ -e "$(which git)" ]; then
|
||||
# clean 'dirty' status of touched files that haven't been modified
|
||||
git diff >/dev/null 2>/dev/null
|
||||
|
||||
# get a string like "v0.6.0-66-g59887e8-dirty"
|
||||
DESC="$(git describe --dirty 2>/dev/null)"
|
||||
# Try exact tag match first (when building from a release tag)
|
||||
DESC="$(git describe --tags --exact-match 2>/dev/null)"
|
||||
|
||||
# If no exact match, fall back to git describe with commit distance
|
||||
if [ -z "$DESC" ]; then
|
||||
DESC="$(git describe --tags --dirty 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# get a string like "2012-04-10 16:27:19 +0200"
|
||||
TIME="$(git log -n 1 --format="%ci")"
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
|
||||
#define CLIENT_VERSION_MAJOR 5
|
||||
#define CLIENT_VERSION_MINOR 4
|
||||
#define CLIENT_VERSION_REVISION 0
|
||||
#define CLIENT_VERSION_REVISION 4
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
|
||||
+6
-2
@@ -176,6 +176,12 @@ void Shutdown(void* parg)
|
||||
}
|
||||
|
||||
SecureMsgShutdown();
|
||||
|
||||
// Stop network threads FIRST so nothing references Tor objects
|
||||
nTransactionsUpdated++;
|
||||
StopNode();
|
||||
|
||||
// NOW safe to destroy Tor state - all threads have stopped
|
||||
ShutdownTorV3();
|
||||
StopEmbeddedTor();
|
||||
|
||||
@@ -194,10 +200,8 @@ void Shutdown(void* parg)
|
||||
pNotificationQueue = NULL;
|
||||
}
|
||||
|
||||
nTransactionsUpdated++;
|
||||
// CTxDB().Close();
|
||||
bitdb.Flush(false);
|
||||
StopNode();
|
||||
bitdb.Flush(true);
|
||||
fs::remove(GetPidFile());
|
||||
UnregisterWallet(pwalletMain);
|
||||
|
||||
+13
-3
@@ -2568,7 +2568,17 @@ bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos, const u
|
||||
|
||||
// New best — keep the batch open so SetBestChain can add ConnectBlock
|
||||
// writes to the same transaction, cutting the per-block commit count in half.
|
||||
// v5.4: deterministic tiebreaker — when two chains have equal trust,
|
||||
// all nodes agree on the one whose tip has the lower block hash.
|
||||
// This prevents permanent forks from PoS blocks with identical difficulty.
|
||||
bool fNewBest = false;
|
||||
if (pindexNew->nChainTrust > nBestChainTrust)
|
||||
fNewBest = true;
|
||||
else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest &&
|
||||
pindexNew->GetBlockHash() < pindexBest->GetBlockHash())
|
||||
fNewBest = true;
|
||||
|
||||
if (fNewBest)
|
||||
{
|
||||
if (!SetBestChain(txdb, pindexNew))
|
||||
return false;
|
||||
@@ -2719,7 +2729,7 @@ bool CBlock::AcceptBlock()
|
||||
if (nHeight % 10000 == 0 || nHeight > 2186900)
|
||||
printf("ProcessBlock(): Check proof-of-stake/work OK for block %d\n", nHeight);
|
||||
// Check timestamp against prev
|
||||
if (GetBlockTime() <= pindexPrev->GetPastTimeLimit() || FutureDrift(GetBlockTime()) < pindexPrev->GetBlockTime())
|
||||
if (GetBlockTime() <= pindexPrev->GetPastTimeLimit() || FutureDrift(GetBlockTime(), nHeight) < pindexPrev->GetBlockTime())
|
||||
return error("AcceptBlock() : block's timestamp is too early");
|
||||
|
||||
// Check that all transactions are finalized
|
||||
@@ -2999,13 +3009,13 @@ bool CBlock::SignBlock(CWallet& wallet, int64_t nFees)
|
||||
{
|
||||
if (wallet.CreateCoinStake(wallet, nBits, nSearchTime-nLastCoinStakeSearchTime, nFees, txCoinStake, key))
|
||||
{
|
||||
if (txCoinStake.nTime >= max(pindexBest->GetPastTimeLimit()+1, PastDrift(pindexBest->GetBlockTime())))
|
||||
if (txCoinStake.nTime >= max(pindexBest->GetPastTimeLimit()+1, PastDrift(pindexBest->GetBlockTime(), pindexBest->nHeight + 1)))
|
||||
{
|
||||
// make sure coinstake would meet timestamp protocol
|
||||
// as it would be the same as the block timestamp
|
||||
vtx[0].nTime = nTime = txCoinStake.nTime;
|
||||
nTime = max(pindexBest->GetPastTimeLimit()+1, GetMaxTransactionTime());
|
||||
nTime = max(GetBlockTime(), PastDrift(pindexBest->GetBlockTime()));
|
||||
nTime = max(GetBlockTime(), PastDrift(pindexBest->GetBlockTime(), pindexBest->nHeight + 1));
|
||||
|
||||
// we have to make sure that we have no future timestamps in
|
||||
// our transactions set
|
||||
|
||||
+7
-3
@@ -29,6 +29,7 @@ class CNode;
|
||||
static const int CUTOFF_POW_BLOCK = 9000;
|
||||
static const int CRAPCHAIN_CUTOFF_BLOCK = 17691; // pre-Pharao (version 4) blockchain until block 17691
|
||||
static const int FORK_HEIGHT_V5 = 17651; // v5 hard fork: decentralization + Tor v3 (next block after last checkpoint)
|
||||
static const int FORK_HEIGHT_V5_4 = 2186941; // v5.4: tighter timestamps, deterministic fork resolution
|
||||
|
||||
static const unsigned int MAX_BLOCK_SIZE = 1000000;
|
||||
static const unsigned int MAX_BLOCK_SIZE_GEN = MAX_BLOCK_SIZE/2;
|
||||
@@ -39,7 +40,7 @@ static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
|
||||
static const unsigned int MAX_INV_SZ = 50000;
|
||||
static const int64_t MIN_TX_FEE = (1 * CENT) / 100;
|
||||
static const int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100;
|
||||
static const int64_t MAX_MONEY = 222222 * COIN;
|
||||
static const int64_t MAX_MONEY = 2222222 * COIN;
|
||||
static const int64_t COIN_YEAR_REWARD = 33 * CENT; // 33% per year
|
||||
static const int64_t MAX_TRI_PROOF_OF_STAKE = 0.33 * COIN;
|
||||
static const int MODIFIER_INTERVAL_SWITCH = 1;
|
||||
@@ -56,8 +57,11 @@ static const int fHaveUPnP = false;
|
||||
|
||||
static const uint256 hashGenesisBlockOfficial("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
static const uint256 hashGenesisBlockTestNet ("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
inline int64_t PastDrift(int64_t nTime) { return nTime - 10 * 60; } // up to 10 minutes from the past
|
||||
inline int64_t FutureDrift(int64_t nTime) { return nTime + 10 * 60; } // up to 10 minutes from the future
|
||||
inline int64_t GetMaxTimeDrift(int nHeight) { return (nHeight >= FORK_HEIGHT_V5_4) ? 3 * 60 : 10 * 60; }
|
||||
inline int64_t PastDrift(int64_t nTime, int nHeight) { return nTime - GetMaxTimeDrift(nHeight); }
|
||||
inline int64_t FutureDrift(int64_t nTime, int nHeight) { return nTime + GetMaxTimeDrift(nHeight); }
|
||||
inline int64_t PastDrift(int64_t nTime) { return PastDrift(nTime, nBestHeight); }
|
||||
inline int64_t FutureDrift(int64_t nTime) { return FutureDrift(nTime, nBestHeight); }
|
||||
|
||||
|
||||
extern CScript COINBASE_FLAGS;
|
||||
|
||||
+1
-1
@@ -365,7 +365,7 @@ CBlock* CreateNewBlock(CWallet* pwallet, bool fProofOfStake, int64_t* pFees)
|
||||
// Fill in header
|
||||
pblock->hashPrevBlock = pindexPrev->GetBlockHash();
|
||||
pblock->nTime = max(pindexPrev->GetPastTimeLimit()+1, pblock->GetMaxTransactionTime());
|
||||
pblock->nTime = max(pblock->GetBlockTime(), PastDrift(pindexPrev->GetBlockTime()));
|
||||
pblock->nTime = max(pblock->GetBlockTime(), PastDrift(pindexPrev->GetBlockTime(), pindexPrev->nHeight + 1));
|
||||
if (!fProofOfStake)
|
||||
pblock->UpdateTime(pindexPrev);
|
||||
pblock->nNonce = 0;
|
||||
|
||||
+33
-10
@@ -878,10 +878,19 @@ void ThreadSocketHandler2(void* parg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vNodes.size() != nPrevNodeCount)
|
||||
{
|
||||
nPrevNodeCount = vNodes.size();
|
||||
uiInterface.NotifyNumConnectionsChanged(vNodes.size());
|
||||
// Read vNodes.size() under the lock to avoid data race
|
||||
unsigned int nNodeCount;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
nNodeCount = vNodes.size();
|
||||
}
|
||||
if (nNodeCount != nPrevNodeCount)
|
||||
{
|
||||
nPrevNodeCount = nNodeCount;
|
||||
if (!fShutdown)
|
||||
uiInterface.NotifyNumConnectionsChanged(nNodeCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1013,7 +1022,7 @@ void ThreadSocketHandler2(void* parg)
|
||||
for (CNode* pnode : vNodesCopy)
|
||||
{
|
||||
if (fShutdown)
|
||||
return;
|
||||
break;
|
||||
|
||||
//
|
||||
// Receive
|
||||
@@ -1039,7 +1048,7 @@ void ThreadSocketHandler2(void* parg)
|
||||
if (!pnode->ReceiveMsgBytes(pchBuf, nBytes))
|
||||
pnode->CloseSocketDisconnect();
|
||||
pnode->nLastRecv = GetTime();
|
||||
pnode->nRecvBytes += nBytes;
|
||||
pnode->nRecvBytes += nBytes;
|
||||
}
|
||||
else if (nBytes == 0)
|
||||
{
|
||||
@@ -1104,6 +1113,8 @@ void ThreadSocketHandler2(void* parg)
|
||||
pnode->Release();
|
||||
}
|
||||
|
||||
if (fShutdown)
|
||||
return;
|
||||
MilliSleep(10);
|
||||
}
|
||||
}
|
||||
@@ -1832,6 +1843,9 @@ void ThreadMessageHandler2(void* parg)
|
||||
pnodeTrickle = vNodesCopy[GetRand(vNodesCopy.size())];
|
||||
for (CNode* pnode : vNodesCopy)
|
||||
{
|
||||
if (fShutdown)
|
||||
break;
|
||||
|
||||
// Receive messages
|
||||
{
|
||||
TRY_LOCK(pnode->cs_vRecvMsg, lockRecv);
|
||||
@@ -1839,8 +1853,6 @@ void ThreadMessageHandler2(void* parg)
|
||||
if (!ProcessMessages(pnode))
|
||||
pnode->CloseSocketDisconnect();
|
||||
}
|
||||
if (fShutdown)
|
||||
return;
|
||||
|
||||
// Send messages
|
||||
{
|
||||
@@ -1848,8 +1860,6 @@ void ThreadMessageHandler2(void* parg)
|
||||
if (lockSend)
|
||||
SendMessages(pnode, pnode == pnodeTrickle);
|
||||
}
|
||||
if (fShutdown)
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
@@ -2180,6 +2190,18 @@ bool StopNode()
|
||||
}
|
||||
MilliSleep(50);
|
||||
DumpAddresses();
|
||||
|
||||
// Force-disconnect and clean up all remaining nodes now that threads have stopped.
|
||||
// Close sockets first so any lingering I/O fails immediately.
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
{
|
||||
pnode->CloseSocketDisconnect();
|
||||
pnode->Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2191,7 +2213,8 @@ public:
|
||||
}
|
||||
~CNetCleanup()
|
||||
{
|
||||
// Close sockets
|
||||
// Close sockets - acquire lock in case other threads are still winding down
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
if (pnode->hSocket != INVALID_SOCKET)
|
||||
closesocket(pnode->hSocket);
|
||||
|
||||
+10
-4
@@ -1,13 +1,19 @@
|
||||
|
||||
#ifndef TRIANGLES_ONIONSEED_H
|
||||
#define TRIANGLES_ONIONSEED_H
|
||||
|
||||
// hidden service seeds
|
||||
// v5 hard fork: v2 onion seeds removed (Tor v2 deprecated Oct 2021)
|
||||
// v3 onion seeds will be added when bootstrap nodes are deployed
|
||||
// v3 onion seeds - bootstrap nodes deployed 2026-03-30
|
||||
static const char *strMainNetOnionSeed[][1] = {
|
||||
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"},
|
||||
{"futmtrvh6j34t7s6yjdxfia6iwuyfzwh4k5eqfof5kfhoqk3xmi3qoqd.onion"},
|
||||
// Main nodes
|
||||
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"}, // DNS2
|
||||
{"futmtrvh6j34t7s6yjdxfia6iwuyfzwh4k5eqfof5kfhoqk3xmi3qoqd.onion"}, // Original seed
|
||||
{"i6tk7soznftvoibtskwlezviskiererhjndpsmrff4kaxw7jnd5izfqd.onion"}, // DNS3
|
||||
// Docker seed nodes (contabo-de)
|
||||
{"uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion"}, // seed-1
|
||||
{"el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion"}, // seed-2
|
||||
{"sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion"}, // seed-3
|
||||
{"i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion"}, // seed-4
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
||||
@@ -193,13 +193,16 @@ static void NotifyBlocksChanged(ClientModel *clientmodel)
|
||||
|
||||
static void NotifyNumConnectionsChanged(ClientModel *clientmodel, int newNumConnections)
|
||||
{
|
||||
// Too noisy: OutputDebugStringF("NotifyNumConnectionsChanged %i\n", newNumConnections);
|
||||
// Don't queue UI updates during shutdown - ClientModel may be destroyed
|
||||
// before Qt processes the queued invocation, causing use-after-free.
|
||||
if (fShutdown) return;
|
||||
QMetaObject::invokeMethod(clientmodel, "updateNumConnections", Qt::QueuedConnection,
|
||||
Q_ARG(int, newNumConnections));
|
||||
}
|
||||
|
||||
static void NotifyAlertChanged(ClientModel *clientmodel, const uint256 &hash, ChangeType status)
|
||||
{
|
||||
if (fShutdown) return;
|
||||
OutputDebugStringF("NotifyAlertChanged %s status=%i\n", hash.GetHex().c_str(), status);
|
||||
QMetaObject::invokeMethod(clientmodel, "updateAlert", Qt::QueuedConnection,
|
||||
Q_ARG(QString, QString::fromStdString(hash.GetHex())),
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <set>
|
||||
|
||||
IntroDialog::IntroDialog(QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
@@ -190,6 +192,24 @@ bool IntroDialog::pickDataDirectory()
|
||||
settings.setValue("strDataDir", dataDir);
|
||||
}
|
||||
|
||||
// Check for pending data directory migration
|
||||
if (settings.value("fPendingDataDirMigration", false).toBool()) {
|
||||
QString oldDir = settings.value("strDataDirPrevious", "").toString();
|
||||
if (!oldDir.isEmpty() && oldDir != dataDir) {
|
||||
if (!migrateDataDirectory(oldDir, dataDir)) {
|
||||
// Migration failed - revert to old directory
|
||||
QMessageBox::warning(0, "Triangles",
|
||||
QString("Data directory migration failed.\nContinuing with the previous directory:\n%1")
|
||||
.arg(oldDir));
|
||||
dataDir = oldDir;
|
||||
settings.setValue("strDataDir", oldDir);
|
||||
}
|
||||
}
|
||||
// Clear migration state regardless
|
||||
settings.remove("strDataDirPrevious");
|
||||
settings.setValue("fPendingDataDirMigration", false);
|
||||
}
|
||||
|
||||
// If the saved path is the default, don't set -datadir (let normal defaults work)
|
||||
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
|
||||
if (dataDir != defaultDir) {
|
||||
@@ -275,3 +295,145 @@ bool IntroDialog::pickDataDirectory()
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void copyDirectoryRecursive(const boost::filesystem::path& src,
|
||||
const boost::filesystem::path& dst)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
fs::create_directories(dst);
|
||||
for (fs::directory_iterator it(src), end; it != end; ++it) {
|
||||
fs::path dstChild = dst / it->path().filename();
|
||||
if (fs::is_directory(it->path())) {
|
||||
copyDirectoryRecursive(it->path(), dstChild);
|
||||
} else {
|
||||
fs::copy_file(it->path(), dstChild, fs::copy_options::overwrite_existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IntroDialog::migrateDataDirectory(const QString& oldPath, const QString& newPath)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
fs::path srcDir(oldPath.toStdString());
|
||||
fs::path dstDir(newPath.toStdString());
|
||||
|
||||
if (!fs::exists(srcDir) || !fs::is_directory(srcDir))
|
||||
return false;
|
||||
|
||||
// Create destination directory
|
||||
try {
|
||||
fs::create_directories(dstDir);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
printf("Migration: Cannot create destination directory: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check free space
|
||||
try {
|
||||
quint64 srcSize = 0;
|
||||
for (fs::recursive_directory_iterator it(srcDir), end; it != end; ++it) {
|
||||
if (fs::is_regular_file(*it))
|
||||
srcSize += fs::file_size(*it);
|
||||
}
|
||||
fs::space_info si = fs::space(dstDir);
|
||||
if (si.available < srcSize + (50 * 1024 * 1024)) { // 50MB headroom
|
||||
printf("Migration: Insufficient disk space. Need %llu, have %llu\n",
|
||||
(unsigned long long)srcSize, (unsigned long long)si.available);
|
||||
return false;
|
||||
}
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
printf("Migration: Cannot check disk space: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Files/directories to skip during copy
|
||||
static const std::set<std::string> skipFiles = {
|
||||
".lock",
|
||||
"debug.log",
|
||||
"db.log",
|
||||
};
|
||||
|
||||
// Show progress dialog
|
||||
QProgressDialog progress("Moving data directory...", QString(), 0, 0, 0);
|
||||
progress.setWindowTitle("Triangles - Data Migration");
|
||||
progress.setWindowModality(Qt::ApplicationModal);
|
||||
progress.setMinimumDuration(0);
|
||||
progress.setCancelButton(0);
|
||||
progress.show();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Phase 1: Copy wallet.dat FIRST (most critical file)
|
||||
fs::path walletSrc = srcDir / "wallet.dat";
|
||||
fs::path walletDst = dstDir / "wallet.dat";
|
||||
if (fs::exists(walletSrc)) {
|
||||
progress.setLabelText("Copying wallet.dat...");
|
||||
QApplication::processEvents();
|
||||
try {
|
||||
// Copy to temp name first, then rename for atomicity
|
||||
fs::path walletTmp = dstDir / "wallet.dat.migrating";
|
||||
fs::copy_file(walletSrc, walletTmp, fs::copy_options::overwrite_existing);
|
||||
|
||||
// Verify copy by checking file size
|
||||
if (fs::file_size(walletTmp) != fs::file_size(walletSrc)) {
|
||||
fs::remove(walletTmp);
|
||||
printf("Migration: wallet.dat copy size mismatch!\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rename into place
|
||||
if (fs::exists(walletDst))
|
||||
fs::remove(walletDst);
|
||||
fs::rename(walletTmp, walletDst);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
printf("Migration: Failed to copy wallet.dat: %s\n", e.what());
|
||||
return false; // Abort - wallet is critical
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Copy everything else
|
||||
int filesCopied = 0;
|
||||
try {
|
||||
for (fs::directory_iterator it(srcDir), end; it != end; ++it) {
|
||||
std::string filename = it->path().filename().string();
|
||||
|
||||
// Skip special files
|
||||
if (skipFiles.count(filename))
|
||||
continue;
|
||||
|
||||
// Skip wallet.dat (already copied)
|
||||
if (filename == "wallet.dat")
|
||||
continue;
|
||||
|
||||
fs::path dst = dstDir / filename;
|
||||
|
||||
progress.setLabelText(QString("Copying %1...").arg(QString::fromStdString(filename)));
|
||||
QApplication::processEvents();
|
||||
|
||||
if (fs::is_directory(it->path())) {
|
||||
copyDirectoryRecursive(it->path(), dst);
|
||||
} else {
|
||||
fs::copy_file(it->path(), dst, fs::copy_options::overwrite_existing);
|
||||
}
|
||||
filesCopied++;
|
||||
}
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
// Non-wallet copy failure: log but don't abort
|
||||
// Chain data can be re-synced; wallet was already safely copied
|
||||
printf("Migration: Warning: failed to copy some files: %s\n", e.what());
|
||||
}
|
||||
|
||||
// Phase 3: Rename old wallet.dat as safety backup (don't delete old dir)
|
||||
try {
|
||||
if (fs::exists(walletSrc)) {
|
||||
fs::rename(walletSrc, srcDir / "wallet.dat.bak-migrated");
|
||||
}
|
||||
} catch (...) {
|
||||
// Not critical
|
||||
}
|
||||
|
||||
progress.close();
|
||||
printf("Migration: Successfully copied %d items from %s to %s\n",
|
||||
filesCopied, srcDir.string().c_str(), dstDir.string().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,13 @@ public:
|
||||
*/
|
||||
static bool pickDataDirectory();
|
||||
|
||||
/**
|
||||
* Migrate data directory contents from oldPath to newPath.
|
||||
* Returns true on success, false on failure.
|
||||
* Shows a progress dialog during the copy.
|
||||
*/
|
||||
static bool migrateDataDirectory(const QString& oldPath, const QString& newPath);
|
||||
|
||||
private slots:
|
||||
void on_browseButton_clicked();
|
||||
void on_defaultRadio_toggled(bool checked);
|
||||
|
||||
+202
-1
@@ -7,12 +7,21 @@
|
||||
#include "optionsmodel.h"
|
||||
#include "dialog_move_handler.h"
|
||||
|
||||
#include "init.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QIntValidator>
|
||||
#include <QLocale>
|
||||
#include <QMessageBox>
|
||||
#include <QProcess>
|
||||
#include <QRegExp>
|
||||
#include <QRegExpValidator>
|
||||
#include <QSettings>
|
||||
|
||||
OptionsDialog::OptionsDialog(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
@@ -21,12 +30,54 @@ OptionsDialog::OptionsDialog(QWidget *parent) :
|
||||
mapper(0),
|
||||
fRestartWarningDisplayed_Proxy(false),
|
||||
fRestartWarningDisplayed_Lang(false),
|
||||
fProxyIpValid(true)
|
||||
fProxyIpValid(true),
|
||||
dataDirPath(0),
|
||||
dataDirFreeSpaceLabel(0)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Window);
|
||||
ui->wCaption->installEventFilter(new DialogMoveHandler(this));
|
||||
|
||||
/* Data Directory section in Main tab */
|
||||
m_currentDataDir = QString::fromStdString(GetDataDir(false).string());
|
||||
m_pendingDataDir.clear();
|
||||
|
||||
QGroupBox *groupDataDir = new QGroupBox(tr("Data Directory"), this);
|
||||
groupDataDir->setStyleSheet(
|
||||
"QGroupBox { border: 1px solid #61280E; margin-top: 8px; padding-top: 16px; color: #f26522; }"
|
||||
"QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; }");
|
||||
|
||||
QVBoxLayout *dataDirLayout = new QVBoxLayout(groupDataDir);
|
||||
|
||||
QHBoxLayout *dataDirPathLayout = new QHBoxLayout();
|
||||
dataDirPath = new QLineEdit(m_currentDataDir, groupDataDir);
|
||||
dataDirPath->setReadOnly(true);
|
||||
dataDirPath->setStyleSheet("QLineEdit { background-color: #1c1c1c; border: 1px solid #f26522; color: #f26522; padding: 2px; }");
|
||||
|
||||
QPushButton *dataDirBrowseButton = new QPushButton(tr("Browse..."), groupDataDir);
|
||||
dataDirBrowseButton->setStyleSheet(
|
||||
"QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; padding: 2px 12px; min-height: 20px; }"
|
||||
"QPushButton:hover { background-color: #61280E; }"
|
||||
"QPushButton:pressed:flat { color: #000; background-color: #f26522; }");
|
||||
|
||||
dataDirPathLayout->addWidget(dataDirPath);
|
||||
dataDirPathLayout->addWidget(dataDirBrowseButton);
|
||||
dataDirLayout->addLayout(dataDirPathLayout);
|
||||
|
||||
dataDirFreeSpaceLabel = new QLabel(groupDataDir);
|
||||
dataDirFreeSpaceLabel->setStyleSheet("color: #999; font-size: 11px;");
|
||||
dataDirLayout->addWidget(dataDirFreeSpaceLabel);
|
||||
|
||||
// Insert into Main tab layout, before the vertical spacer (last item)
|
||||
QVBoxLayout *mainTabLayout = qobject_cast<QVBoxLayout*>(ui->tabWidget->widget(0)->layout());
|
||||
if (mainTabLayout) {
|
||||
int spacerIndex = mainTabLayout->count() - 1; // vertical spacer is last
|
||||
mainTabLayout->insertWidget(spacerIndex, groupDataDir);
|
||||
}
|
||||
|
||||
connect(dataDirBrowseButton, SIGNAL(clicked()), this, SLOT(on_dataDirBrowseButton_clicked()));
|
||||
updateDataDirFreeSpace();
|
||||
|
||||
/* Network elements init */
|
||||
#ifndef USE_UPNP
|
||||
ui->mapPortUpnp->setEnabled(false);
|
||||
@@ -188,6 +239,8 @@ void OptionsDialog::setSaveButtonState(bool fState)
|
||||
void OptionsDialog::on_okButton_clicked()
|
||||
{
|
||||
mapper->submit();
|
||||
if (handleDataDirChange())
|
||||
return; // restart flow handles closing
|
||||
accept();
|
||||
}
|
||||
|
||||
@@ -199,6 +252,7 @@ void OptionsDialog::on_cancelButton_clicked()
|
||||
void OptionsDialog::on_applyButton_clicked()
|
||||
{
|
||||
mapper->submit();
|
||||
handleDataDirChange();
|
||||
disableApplyButton();
|
||||
}
|
||||
|
||||
@@ -303,3 +357,150 @@ bool OptionsDialog::eventFilter(QObject *object, QEvent *event)
|
||||
}
|
||||
return QDialog::eventFilter(object, event);
|
||||
}
|
||||
|
||||
void OptionsDialog::on_dataDirBrowseButton_clicked()
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(
|
||||
this, tr("Choose data directory"), m_currentDataDir);
|
||||
if (!dir.isEmpty() && dir != m_currentDataDir)
|
||||
{
|
||||
m_pendingDataDir = dir;
|
||||
dataDirPath->setText(dir);
|
||||
updateDataDirFreeSpace();
|
||||
enableApplyButton();
|
||||
}
|
||||
}
|
||||
|
||||
void OptionsDialog::updateDataDirFreeSpace()
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
QString path = dataDirPath->text();
|
||||
fs::path fsPath(path.toStdString());
|
||||
try {
|
||||
while (!fsPath.empty() && !fs::exists(fsPath))
|
||||
fsPath = fsPath.parent_path();
|
||||
if (!fsPath.empty()) {
|
||||
fs::space_info si = fs::space(fsPath);
|
||||
double freeGB = (double)si.available / (1024.0 * 1024.0 * 1024.0);
|
||||
dataDirFreeSpaceLabel->setText(
|
||||
tr("Free space: %1 GB").arg(QString::number(freeGB, 'f', 2)));
|
||||
} else {
|
||||
dataDirFreeSpaceLabel->setText(tr("Cannot determine free space"));
|
||||
}
|
||||
} catch (const fs::filesystem_error &) {
|
||||
dataDirFreeSpaceLabel->setText(tr("Cannot determine free space"));
|
||||
}
|
||||
}
|
||||
|
||||
quint64 OptionsDialog::calculateDirSize(const QString& path)
|
||||
{
|
||||
namespace fs = boost::filesystem;
|
||||
quint64 totalSize = 0;
|
||||
try {
|
||||
for (fs::recursive_directory_iterator it(path.toStdString()), end; it != end; ++it) {
|
||||
if (fs::is_regular_file(*it))
|
||||
totalSize += fs::file_size(*it);
|
||||
}
|
||||
} catch (...) {}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
bool OptionsDialog::handleDataDirChange()
|
||||
{
|
||||
if (m_pendingDataDir.isEmpty() || m_pendingDataDir == m_currentDataDir)
|
||||
return false;
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
fs::path destPath(m_pendingDataDir.toStdString());
|
||||
|
||||
// Check destination is writable
|
||||
try {
|
||||
fs::create_directories(destPath);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Cannot create directory: %1").arg(QString::fromStdString(e.what())));
|
||||
m_pendingDataDir.clear();
|
||||
dataDirPath->setText(m_currentDataDir);
|
||||
updateDataDirFreeSpace();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check free space vs current data dir size
|
||||
quint64 dataDirSize = calculateDirSize(m_currentDataDir);
|
||||
try {
|
||||
fs::space_info si = fs::space(destPath);
|
||||
quint64 required = dataDirSize + (dataDirSize / 10); // 10% headroom
|
||||
if (si.available < required) {
|
||||
QMessageBox::critical(this, tr("Insufficient Space"),
|
||||
tr("The destination has %1 MB free but the data directory requires approximately %2 MB.")
|
||||
.arg(si.available / (1024*1024))
|
||||
.arg(required / (1024*1024)));
|
||||
m_pendingDataDir.clear();
|
||||
dataDirPath->setText(m_currentDataDir);
|
||||
updateDataDirFreeSpace();
|
||||
return false;
|
||||
}
|
||||
} catch (const fs::filesystem_error&) {
|
||||
// If we can't check space, proceed anyway
|
||||
}
|
||||
|
||||
// Save migration state to QSettings
|
||||
QSettings settings;
|
||||
settings.setValue("strDataDirPrevious", m_currentDataDir);
|
||||
settings.setValue("strDataDir", m_pendingDataDir);
|
||||
settings.setValue("fPendingDataDirMigration", true);
|
||||
|
||||
// Ask about restart
|
||||
QMessageBox msgBox(this);
|
||||
msgBox.setWindowFlags(Qt::FramelessWindowHint);
|
||||
msgBox.setWindowTitle(tr("Data Directory Changed"));
|
||||
msgBox.setText(tr("The data directory will be moved from:\n%1\n\nTo:\n%2\n\n"
|
||||
"This will happen when the wallet restarts.")
|
||||
.arg(m_currentDataDir).arg(m_pendingDataDir));
|
||||
msgBox.setIcon(QMessageBox::Information);
|
||||
msgBox.setIconPixmap(QPixmap(":/msgbox/information"));
|
||||
msgBox.setStyleSheet("QMessageBox { border: 2px solid #f26522; background-color: #000; color: #f26522; }");
|
||||
|
||||
QPushButton *restartBtn = msgBox.addButton(tr("Restart Now"), QMessageBox::AcceptRole);
|
||||
QPushButton *laterBtn = msgBox.addButton(tr("Later"), QMessageBox::RejectRole);
|
||||
|
||||
QString btnStyle =
|
||||
"QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; "
|
||||
"min-width: 120px; max-width: 120px; max-height: 20px; min-height: 20px; }"
|
||||
"QPushButton:hover { background-color: #61280E; }"
|
||||
"QPushButton:pressed:flat { color: #000; background-color: #f26522; }";
|
||||
restartBtn->setStyleSheet(btnStyle);
|
||||
laterBtn->setStyleSheet(btnStyle);
|
||||
|
||||
msgBox.exec();
|
||||
|
||||
if (msgBox.clickedButton() == restartBtn) {
|
||||
performRestart();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void OptionsDialog::performRestart()
|
||||
{
|
||||
// Launch a new instance of ourselves
|
||||
QString exePath = QApplication::applicationFilePath();
|
||||
QStringList args = QApplication::arguments();
|
||||
args.removeFirst(); // remove argv[0]
|
||||
|
||||
// Remove any existing -datadir argument so the new instance
|
||||
// reads strDataDir from QSettings and performs migration
|
||||
QMutableStringListIterator it(args);
|
||||
while (it.hasNext()) {
|
||||
QString arg = it.next();
|
||||
if (arg.startsWith("-datadir") || arg.startsWith("/datadir"))
|
||||
it.remove();
|
||||
}
|
||||
|
||||
// Start new process detached so it survives our shutdown
|
||||
QProcess::startDetached(exePath, args);
|
||||
|
||||
// Close dialog and trigger wallet shutdown
|
||||
accept();
|
||||
StartShutdown();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QLineEdit;
|
||||
class QLabel;
|
||||
|
||||
namespace Ui {
|
||||
class OptionsDialog;
|
||||
}
|
||||
@@ -45,17 +48,29 @@ private slots:
|
||||
void updateDisplayUnit();
|
||||
void handleProxyIpValid(QValidatedLineEdit *object, bool fState);
|
||||
void applyTorDefaults(bool enabled);
|
||||
void on_dataDirBrowseButton_clicked();
|
||||
void updateDataDirFreeSpace();
|
||||
|
||||
signals:
|
||||
void proxyIpValid(QValidatedLineEdit *object, bool fValid);
|
||||
|
||||
private:
|
||||
bool handleDataDirChange();
|
||||
void performRestart();
|
||||
quint64 calculateDirSize(const QString& path);
|
||||
|
||||
Ui::OptionsDialog *ui;
|
||||
OptionsModel *model;
|
||||
MonitoredDataMapper *mapper;
|
||||
bool fRestartWarningDisplayed_Proxy;
|
||||
bool fRestartWarningDisplayed_Lang;
|
||||
bool fProxyIpValid;
|
||||
|
||||
// Data directory widgets (built programmatically)
|
||||
QLineEdit *dataDirPath;
|
||||
QLabel *dataDirFreeSpaceLabel;
|
||||
QString m_currentDataDir;
|
||||
QString m_pendingDataDir;
|
||||
};
|
||||
|
||||
#endif // OPTIONSDIALOG_H
|
||||
|
||||
@@ -2206,7 +2206,9 @@ void ThreadTorMaintenance(void* parg)
|
||||
|
||||
while (!fShutdown)
|
||||
{
|
||||
MilliSleep(30000); // check every 30 seconds
|
||||
// Sleep in short intervals so the thread exits promptly on shutdown
|
||||
for (int i = 0; i < 60 && !fShutdown; i++)
|
||||
MilliSleep(500);
|
||||
if (fShutdown) break;
|
||||
|
||||
// --- Tor health check & auto-restart ---
|
||||
@@ -2243,7 +2245,8 @@ void ThreadTorMaintenance(void* parg)
|
||||
else
|
||||
{
|
||||
printf("WARNING: Tor restart failed, retrying in %d seconds\n", restartBackoffSec);
|
||||
MilliSleep(restartBackoffSec * 1000);
|
||||
for (int i = 0; i < restartBackoffSec * 2 && !fShutdown; i++)
|
||||
MilliSleep(500);
|
||||
if (restartBackoffSec < 300)
|
||||
restartBackoffSec *= 2;
|
||||
}
|
||||
|
||||
+17
-2
@@ -946,9 +946,24 @@ void ThreadRPCServer2(void* parg)
|
||||
|
||||
vnThreadsRunning[THREAD_RPCLISTENER]--;
|
||||
while (!fShutdown)
|
||||
io_service.run_one();
|
||||
{
|
||||
// Use poll_one + sleep instead of blocking run_one so the thread
|
||||
// remains responsive to fShutdown and can exit promptly.
|
||||
if (!io_service.poll_one())
|
||||
{
|
||||
io_service.restart();
|
||||
MilliSleep(50);
|
||||
}
|
||||
}
|
||||
vnThreadsRunning[THREAD_RPCLISTENER]++;
|
||||
StopRequests();
|
||||
|
||||
// Safely shut down: close acceptors, then drain any remaining handlers
|
||||
try {
|
||||
StopRequests();
|
||||
} catch (...) {
|
||||
// Absorb bad_weak_ptr or other exceptions from stale tracked slots
|
||||
}
|
||||
io_service.poll(); // process cancellation callbacks so shared_ptrs are released
|
||||
}
|
||||
|
||||
class JSONRequest
|
||||
|
||||
Reference in New Issue
Block a user