Skip to content

Commit 66e3af7

Browse files
committed
Merge #11904: Add a lock to the wallet directory
2f3bd47 Abstract directory locking into util.cpp (MeshCollider) 5260a4a Make .walletlock distinct from .lock (MeshCollider) 64226de Generalise walletdir lock error message for correctness (MeshCollider) c9ed4bd Add a test for wallet directory locking (MeshCollider) e60cb99 Add a lock to the wallet directory (MeshCollider) Pull request description: Fixes bitcoin/bitcoin#11888, needs a 0.16 milestone Also adds a test that the lock works. bitcoin/bitcoin#11687 will probably rework this to a per-wallet lock instead of just the walletdir, but this fixes the current issue Tree-SHA512: 98e52d67f820e3b8f919cf361ffbb7d928f1bd67603e0ed26c5076ea02d9b3a90c3535ddf7329f3b88171396fa28dd3c87adab3577a8a217bd1e4247bda99138
2 parents bbc91b7 + 2f3bd47 commit 66e3af7

File tree

6 files changed

+64
-44
lines changed

6 files changed

+64
-44
lines changed

src/init.cpp

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,23 +1143,10 @@ bool AppInitParameterInteraction()
11431143

11441144
static bool LockDataDirectory(bool probeOnly)
11451145
{
1146-
std::string strDataDir = GetDataDir().string();
1147-
11481146
// Make sure only a single Bitcoin process is using the data directory.
1149-
fs::path pathLockFile = GetDataDir() / ".lock";
1150-
FILE* file = fsbridge::fopen(pathLockFile, "a"); // empty lock file; created if it doesn't exist.
1151-
if (file) fclose(file);
1152-
1153-
try {
1154-
static boost::interprocess::file_lock lock(pathLockFile.string().c_str());
1155-
if (!lock.try_lock()) {
1156-
return InitError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running."), strDataDir, _(PACKAGE_NAME)));
1157-
}
1158-
if (probeOnly) {
1159-
lock.unlock();
1160-
}
1161-
} catch(const boost::interprocess::interprocess_exception& e) {
1162-
return InitError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running.") + " %s.", strDataDir, _(PACKAGE_NAME), e.what()));
1147+
fs::path datadir = GetDataDir();
1148+
if (!LockDirectory(datadir, ".lock", probeOnly)) {
1149+
return InitError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running."), datadir.string(), _(PACKAGE_NAME)));
11631150
}
11641151
return true;
11651152
}

src/util.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272

7373
#include <boost/algorithm/string/case_conv.hpp> // for to_lower()
7474
#include <boost/algorithm/string/predicate.hpp> // for startswith() and endswith()
75+
#include <boost/interprocess/sync/file_lock.hpp>
7576
#include <boost/program_options/detail/config_file.hpp>
7677
#include <boost/thread.hpp>
7778
#include <openssl/crypto.h>
@@ -375,6 +376,27 @@ int LogPrintStr(const std::string &str)
375376
return ret;
376377
}
377378

379+
bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only)
380+
{
381+
fs::path pathLockFile = directory / lockfile_name;
382+
FILE* file = fsbridge::fopen(pathLockFile, "a"); // empty lock file; created if it doesn't exist.
383+
if (file) fclose(file);
384+
385+
try {
386+
static std::map<std::string, boost::interprocess::file_lock> locks;
387+
boost::interprocess::file_lock& lock = locks.emplace(pathLockFile.string(), pathLockFile.string().c_str()).first->second;
388+
if (!lock.try_lock()) {
389+
return false;
390+
}
391+
if (probe_only) {
392+
lock.unlock();
393+
}
394+
} catch (const boost::interprocess::interprocess_exception& e) {
395+
return error("Error while attempting to lock directory %s: %s", directory.string(), e.what());
396+
}
397+
return true;
398+
}
399+
378400
/** Interpret string as boolean, for argument parsing */
379401
static bool InterpretBool(const std::string& strValue)
380402
{

src/util.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ bool TruncateFile(FILE *file, unsigned int length);
173173
int RaiseFileDescriptorLimit(int nMinFD);
174174
void AllocateFileRange(FILE *file, unsigned int offset, unsigned int length);
175175
bool RenameOver(fs::path src, fs::path dest);
176+
bool LockDirectory(const fs::path& directory, const std::string lockfile_name, bool probe_only=false);
176177
bool TryCreateDirectories(const fs::path& p);
177178
fs::path GetDefaultDataDir();
178179
const fs::path &GetDataDir(bool fNetSpecific = true);

src/wallet/db.cpp

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,19 @@ void CDBEnv::Close()
9595
EnvShutdown();
9696
}
9797

98-
bool CDBEnv::Open(const fs::path& pathIn)
98+
bool CDBEnv::Open(const fs::path& pathIn, bool retry)
9999
{
100100
if (fDbEnvInit)
101101
return true;
102102

103103
boost::this_thread::interruption_point();
104104

105105
strPath = pathIn.string();
106+
if (!LockDirectory(pathIn, ".walletlock")) {
107+
LogPrintf("Cannot obtain a lock on wallet directory %s. Another instance of bitcoin may be using it.\n", strPath);
108+
return false;
109+
}
110+
106111
fs::path pathLogDir = pathIn / "database";
107112
TryCreateDirectories(pathLogDir);
108113
fs::path pathErrorFile = pathIn / "db.log";
@@ -134,7 +139,24 @@ bool CDBEnv::Open(const fs::path& pathIn)
134139
S_IRUSR | S_IWUSR);
135140
if (ret != 0) {
136141
dbenv->close(0);
137-
return error("CDBEnv::Open: Error %d opening database environment: %s\n", ret, DbEnv::strerror(ret));
142+
LogPrintf("CDBEnv::Open: Error %d opening database environment: %s\n", ret, DbEnv::strerror(ret));
143+
if (retry) {
144+
// try moving the database env out of the way
145+
fs::path pathDatabaseBak = pathIn / strprintf("database.%d.bak", GetTime());
146+
try {
147+
fs::rename(pathLogDir, pathDatabaseBak);
148+
LogPrintf("Moved old %s to %s. Retrying.\n", pathLogDir.string(), pathDatabaseBak.string());
149+
} catch (const fs::filesystem_error&) {
150+
// failure is ok (well, not really, but it's not worse than what we started with)
151+
}
152+
// try opening it again one more time
153+
if (!Open(pathIn, false)) {
154+
// if it still fails, it probably means we can't even create the database env
155+
return false;
156+
}
157+
} else {
158+
return false;
159+
}
138160
}
139161

140162
fDbEnvInit = true;
@@ -269,25 +291,11 @@ bool CDB::VerifyEnvironment(const std::string& walletFile, const fs::path& walle
269291
return false;
270292
}
271293

272-
if (!bitdb.Open(walletDir))
273-
{
274-
// try moving the database env out of the way
275-
fs::path pathDatabase = walletDir / "database";
276-
fs::path pathDatabaseBak = walletDir / strprintf("database.%d.bak", GetTime());
277-
try {
278-
fs::rename(pathDatabase, pathDatabaseBak);
279-
LogPrintf("Moved old %s to %s. Retrying.\n", pathDatabase.string(), pathDatabaseBak.string());
280-
} catch (const fs::filesystem_error&) {
281-
// failure is ok (well, not really, but it's not worse than what we started with)
282-
}
283-
284-
// try again
285-
if (!bitdb.Open(walletDir)) {
286-
// if it still fails, it probably means we can't even create the database env
287-
errorStr = strprintf(_("Error initializing wallet database environment %s!"), walletDir);
288-
return false;
289-
}
294+
if (!bitdb.Open(walletDir, true)) {
295+
errorStr = strprintf(_("Error initializing wallet database environment %s!"), walletDir);
296+
return false;
290297
}
298+
291299
return true;
292300
}
293301

src/wallet/db.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class CDBEnv
6868
typedef std::pair<std::vector<unsigned char>, std::vector<unsigned char> > KeyValPair;
6969
bool Salvage(const std::string& strFile, bool fAggressive, std::vector<KeyValPair>& vResult);
7070

71-
bool Open(const fs::path& path);
71+
bool Open(const fs::path& path, bool retry = 0);
7272
void Close();
7373
void Flush(bool fShutdown);
7474
void CheckpointLSN(const std::string& strFile);

test/functional/multiwallet.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
class MultiWalletTest(BitcoinTestFramework):
1616
def set_test_params(self):
1717
self.setup_clean_chain = True
18-
self.num_nodes = 1
19-
self.extra_args = [['-wallet=w1', '-wallet=w2', '-wallet=w3', '-wallet=w']]
18+
self.num_nodes = 2
19+
self.extra_args = [['-wallet=w1', '-wallet=w2', '-wallet=w3', '-wallet=w'], []]
2020
self.supports_cli = True
2121

2222
def run_test(self):
@@ -28,7 +28,7 @@ def run_test(self):
2828

2929
assert_equal(set(node.listwallets()), {"w1", "w2", "w3", "w"})
3030

31-
self.stop_node(0)
31+
self.stop_nodes()
3232

3333
# should not initialize if there are duplicate wallets
3434
self.assert_start_raises_init_error(0, ['-wallet=w1', '-wallet=w1'], 'Error loading wallet w1. Duplicate -wallet filename specified.')
@@ -59,19 +59,21 @@ def run_test(self):
5959
assert_equal(set(node.listwallets()), {"w4", "w5"})
6060
w5 = wallet("w5")
6161
w5.generate(1)
62-
self.stop_node(0)
6362

6463
# now if wallets/ exists again, but the rootdir is specified as the walletdir, w4 and w5 should still be loaded
6564
os.rename(wallet_dir2, wallet_dir())
66-
self.start_node(0, ['-wallet=w4', '-wallet=w5', '-walletdir=' + data_dir()])
65+
self.restart_node(0, ['-wallet=w4', '-wallet=w5', '-walletdir=' + data_dir()])
6766
assert_equal(set(node.listwallets()), {"w4", "w5"})
6867
w5 = wallet("w5")
6968
w5_info = w5.getwalletinfo()
7069
assert_equal(w5_info['immature_balance'], 50)
7170

72-
self.stop_node(0)
71+
competing_wallet_dir = os.path.join(self.options.tmpdir, 'competing_walletdir')
72+
os.mkdir(competing_wallet_dir)
73+
self.restart_node(0, ['-walletdir='+competing_wallet_dir])
74+
self.assert_start_raises_init_error(1, ['-walletdir='+competing_wallet_dir], 'Error initializing wallet database environment')
7375

74-
self.start_node(0, self.extra_args[0])
76+
self.restart_node(0, self.extra_args[0])
7577

7678
w1 = wallet("w1")
7779
w2 = wallet("w2")

0 commit comments

Comments
 (0)