diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 54efcc8b9f16..d0cbab20b8b8 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -109,6 +109,7 @@ BITCOIN_TESTS =\ test/evo_deterministicmns_tests.cpp \ test/evo_specialtx_tests.cpp \ test/flatfile_tests.cpp \ + test/fs_tests.cpp \ test/getarg_tests.cpp \ test/hash_tests.cpp \ test/key_tests.cpp \ diff --git a/src/fs.cpp b/src/fs.cpp index cab0ec605221..6000b2a148e3 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -5,12 +5,221 @@ #include "fs.h" +#ifndef WIN32 +#include +#else +#define NOMINMAX +#include +#include +#endif + namespace fsbridge { FILE *fopen(const fs::path& p, const char *mode) { +#ifndef WIN32 return ::fopen(p.string().c_str(), mode); +#else + std::wstring_convert,wchar_t> utf8_cvt; + return ::_wfopen(p.wstring().c_str(), utf8_cvt.from_bytes(mode).c_str()); +#endif +} + + +#ifndef WIN32 + +static std::string GetErrorReason() { + return std::strerror(errno); +} + +FileLock::FileLock(const fs::path& file) +{ + fd = open(file.string().c_str(), O_RDWR); + if (fd == -1) { + reason = GetErrorReason(); + } +} + +FileLock::~FileLock() +{ + if (fd != -1) { + close(fd); + } +} + +bool FileLock::TryLock() +{ + if (fd == -1) { + return false; + } + struct flock lock; + lock.l_type = F_WRLCK; + lock.l_whence = SEEK_SET; + lock.l_start = 0; + lock.l_len = 0; + if (fcntl(fd, F_SETLK, &lock) == -1) { + reason = GetErrorReason(); + return false; + } + return true; +} +#else + +static std::string GetErrorReason() { + wchar_t* err; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast(&err), 0, nullptr); + std::wstring err_str(err); + LocalFree(err); + return std::wstring_convert>().to_bytes(err_str); +} + +FileLock::FileLock(const fs::path& file) +{ + hFile = CreateFileW(file.wstring().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile == INVALID_HANDLE_VALUE) { + reason = GetErrorReason(); + } +} + +FileLock::~FileLock() +{ + if (hFile != INVALID_HANDLE_VALUE) { + CloseHandle(hFile); + } } +bool FileLock::TryLock() +{ + if (hFile == INVALID_HANDLE_VALUE) { + return false; + } + _OVERLAPPED overlapped = {0}; + if (!LockFileEx(hFile, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, std::numeric_limits::max(), std::numeric_limits::max(), &overlapped)) { + reason = GetErrorReason(); + return false; + } + return true; +} +#endif + +std::string get_filesystem_error_message(const fs::filesystem_error& e) +{ +#ifndef WIN32 + return e.what(); +#else + // Convert from Multi Byte to utf-16 + std::string mb_string(e.what()); + int size = MultiByteToWideChar(CP_ACP, 0, mb_string.c_str(), mb_string.size(), nullptr, 0); + + std::wstring utf16_string(size, L'\0'); + MultiByteToWideChar(CP_ACP, 0, mb_string.c_str(), mb_string.size(), &*utf16_string.begin(), size); + // Convert from utf-16 to utf-8 + return std::wstring_convert, wchar_t>().to_bytes(utf16_string); +#endif +} + +#ifdef WIN32 +#ifdef __GLIBCXX__ + +// reference: https://github.com/gcc-mirror/gcc/blob/gcc-7_3_0-release/libstdc%2B%2B-v3/include/std/fstream#L270 + +static std::string openmodeToStr(std::ios_base::openmode mode) +{ + switch (mode & ~std::ios_base::ate) { + case std::ios_base::out: + case std::ios_base::out | std::ios_base::trunc: + return "w"; + case std::ios_base::out | std::ios_base::app: + case std::ios_base::app: + return "a"; + case std::ios_base::in: + return "r"; + case std::ios_base::in | std::ios_base::out: + return "r+"; + case std::ios_base::in | std::ios_base::out | std::ios_base::trunc: + return "w+"; + case std::ios_base::in | std::ios_base::out | std::ios_base::app: + case std::ios_base::in | std::ios_base::app: + return "a+"; + case std::ios_base::out | std::ios_base::binary: + case std::ios_base::out | std::ios_base::trunc | std::ios_base::binary: + return "wb"; + case std::ios_base::out | std::ios_base::app | std::ios_base::binary: + case std::ios_base::app | std::ios_base::binary: + return "ab"; + case std::ios_base::in | std::ios_base::binary: + return "rb"; + case std::ios_base::in | std::ios_base::out | std::ios_base::binary: + return "r+b"; + case std::ios_base::in | std::ios_base::out | std::ios_base::trunc | std::ios_base::binary: + return "w+b"; + case std::ios_base::in | std::ios_base::out | std::ios_base::app | std::ios_base::binary: + case std::ios_base::in | std::ios_base::app | std::ios_base::binary: + return "a+b"; + default: + return std::string(); + } +} + +void ifstream::open(const fs::path& p, std::ios_base::openmode mode) +{ + close(); + m_file = fsbridge::fopen(p, openmodeToStr(mode).c_str()); + if (m_file == nullptr) { + return; + } + m_filebuf = __gnu_cxx::stdio_filebuf(m_file, mode); + rdbuf(&m_filebuf); + if (mode & std::ios_base::ate) { + seekg(0, std::ios_base::end); + } +} + +void ifstream::close() +{ + if (m_file != nullptr) { + m_filebuf.close(); + fclose(m_file); + } + m_file = nullptr; +} + +void ofstream::open(const fs::path& p, std::ios_base::openmode mode) +{ + close(); + m_file = fsbridge::fopen(p, openmodeToStr(mode).c_str()); + if (m_file == nullptr) { + return; + } + m_filebuf = __gnu_cxx::stdio_filebuf(m_file, mode); + rdbuf(&m_filebuf); + if (mode & std::ios_base::ate) { + seekp(0, std::ios_base::end); + } +} + +void ofstream::close() +{ + if (m_file != nullptr) { + m_filebuf.close(); + fclose(m_file); + } + m_file = nullptr; +} +#else // __GLIBCXX__ + +static_assert(sizeof(*fs::path().BOOST_FILESYSTEM_C_STR) == sizeof(wchar_t), + "Warning: This build is using boost::filesystem ofstream and ifstream " + "implementations which will fail to open paths containing multibyte " + "characters. You should delete this static_assert to ignore this warning, " + "or switch to a different C++ standard library like the Microsoft C++ " + "Standard Library (where boost uses non-standard extensions to construct " + "stream objects with wide filenames), or the GNU libstdc++ library (where " + "a more complicated workaround has been implemented above)."); + +#endif // __GLIBCXX__ +#endif // WIN32 } // fsbridge diff --git a/src/fs.h b/src/fs.h index d1744eb6c9c0..5c8204d9f87b 100644 --- a/src/fs.h +++ b/src/fs.h @@ -8,6 +8,9 @@ #include #include +#if defined WIN32 && defined __GLIBCXX__ +#include +#endif #define BOOST_FILESYSTEM_NO_DEPRECATED #include @@ -19,6 +22,76 @@ namespace fs = boost::filesystem; /** Bridge operations to C stdio */ namespace fsbridge { FILE *fopen(const fs::path& p, const char *mode); -}; + class FileLock + { + public: + FileLock() = delete; + FileLock(const FileLock&) = delete; + FileLock(FileLock&&) = delete; + explicit FileLock(const fs::path& file); + ~FileLock(); + bool TryLock(); + std::string GetReason() { return reason; } + + private: + std::string reason; +#ifndef WIN32 + int fd = -1; +#else + void* hFile = (void*)-1; // INVALID_HANDLE_VALUE #endif + }; + + std::string get_filesystem_error_message(const fs::filesystem_error& e); + + // GNU libstdc++ specific workaround for opening UTF-8 paths on Windows. + // + // On Windows, it is only possible to reliably access multibyte file paths through + // `wchar_t` APIs, not `char` APIs. But because the C++ standard doesn't + // require ifstream/ofstream `wchar_t` constructors, and the GNU library doesn't + // provide them (in contrast to the Microsoft C++ library, see + // https://stackoverflow.com/questions/821873/how-to-open-an-stdfstream-ofstream-or-ifstream-with-a-unicode-filename/822032#822032), + // Boost is forced to fall back to `char` constructors which may not work properly. + // + // Work around this issue by creating stream objects with `_wfopen` in + // combination with `__gnu_cxx::stdio_filebuf`. This workaround can be removed + // with an upgrade to C++17, where streams can be constructed directly from + // `std::filesystem::path` objects. + +#if defined WIN32 && defined __GLIBCXX__ + class ifstream : public std::istream + { + public: + ifstream() = default; + explicit ifstream(const fs::path& p, std::ios_base::openmode mode = std::ios_base::in) { open(p, mode); } + ~ifstream() { close(); } + void open(const fs::path& p, std::ios_base::openmode mode = std::ios_base::in); + bool is_open() { return m_filebuf.is_open(); } + void close(); + + private: + __gnu_cxx::stdio_filebuf m_filebuf; + FILE* m_file = nullptr; + }; + class ofstream : public std::ostream + { + public: + ofstream() = default; + explicit ofstream(const fs::path& p, std::ios_base::openmode mode = std::ios_base::out) { open(p, mode); } + ~ofstream() { close(); } + void open(const fs::path& p, std::ios_base::openmode mode = std::ios_base::out); + bool is_open() { return m_filebuf.is_open(); } + void close(); + + private: + __gnu_cxx::stdio_filebuf m_filebuf; + FILE* m_file = nullptr; + }; +#else // !(WIN32 && __GLIBCXX__) + typedef fs::ifstream ifstream; + typedef fs::ofstream ofstream; +#endif // WIN32 && __GLIBCXX__ +}; + +#endif // BITCOIN_FS_H diff --git a/src/init.cpp b/src/init.cpp index 59b9b33cfcd5..ab52f44a272d 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -75,12 +75,12 @@ #ifndef WIN32 #include +#include #endif #include #include #include -#include #include #if ENABLE_ZMQ @@ -1190,6 +1190,9 @@ static bool LockDataDirectory(bool probeOnly) { // Make sure only a single PIVX process is using the data directory. fs::path datadir = GetDataDir(); + if (!DirIsWritable(datadir)) { + return UIError(strprintf(_("Cannot write to data directory '%s'; check permissions."), datadir.string())); + } if (!LockDirectory(datadir, ".lock", probeOnly)) { return UIError(strprintf(_("Cannot obtain a lock on data directory %s. %s is probably already running."), datadir.string(), _(PACKAGE_NAME))); } diff --git a/src/logging.h b/src/logging.h index a433acd327f3..f8268301445a 100644 --- a/src/logging.h +++ b/src/logging.h @@ -149,9 +149,9 @@ template std::string FormatStringFromLogArgs(const char *fmt, std::string _log_msg_; /* Unlikely name to avoid shadowing variables */ \ try { \ _log_msg_ = tfm::format(__VA_ARGS__); \ - } catch (tinyformat::format_error &e) { \ + } catch (tinyformat::format_error &fmterr) { \ /* Original format string will have newline so don't add one here */ \ - _log_msg_ = "Error \"" + std::string(e.what()) + \ + _log_msg_ = "Error \"" + std::string(fmterr.what()) + \ "\" while formatting log message: " + \ FormatStringFromLogArgs(__VA_ARGS__); \ } \ diff --git a/src/masternodeconfig.cpp b/src/masternodeconfig.cpp index 8e156b21a512..c5ce042fed85 100644 --- a/src/masternodeconfig.cpp +++ b/src/masternodeconfig.cpp @@ -37,7 +37,7 @@ bool CMasternodeConfig::read(std::string& strErr) LOCK(cs_entries); int linenumber = 1; fs::path pathMasternodeConfigFile = GetMasternodeConfigFile(); - fs::ifstream streamConfig(pathMasternodeConfigFile); + fsbridge::ifstream streamConfig(pathMasternodeConfigFile); if (!streamConfig.good()) { FILE* configFile = fsbridge::fopen(pathMasternodeConfigFile, "a"); diff --git a/src/netbase.cpp b/src/netbase.cpp index 73f7b9185200..0e2ec97cc5df 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -18,6 +18,8 @@ #ifndef WIN32 #include +#else +#include #endif #if !defined(HAVE_MSG_NOSIGNAL) && !defined(MSG_NOSIGNAL) @@ -703,12 +705,14 @@ bool LookupSubNet(const char* pszName, CSubNet& ret) #ifdef WIN32 std::string NetworkErrorString(int err) { - char buf[256]; + wchar_t buf[256]; buf[0] = 0; - if (FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK, - NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - buf, sizeof(buf), NULL)) { - return strprintf("%s (%d)", buf, err); + if(FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK, + nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + buf, ARRAYSIZE(buf), nullptr)) + { + const auto& bufConvert = std::wstring_convert,wchar_t>().to_bytes(buf); + return strprintf("%s (%d)", bufConvert, err); } else { return strprintf("Unknown error (%d)", err); } diff --git a/src/pivx-cli.cpp b/src/pivx-cli.cpp index 5f22ab957583..a7a51b3ea198 100644 --- a/src/pivx-cli.cpp +++ b/src/pivx-cli.cpp @@ -14,6 +14,7 @@ #include "utilstrencodings.h" #include +#include #include #include @@ -324,6 +325,10 @@ int CommandLineRPC(int argc, char* argv[]) int main(int argc, char* argv[]) { +#ifdef WIN32 + util::WinCmdLineArgs winArgs; + std::tie(argc, argv) = winArgs.get(); +#endif SetupEnvironment(); if (!SetupNetworking()) { fprintf(stderr, "Error: Initializing networking failed\n"); diff --git a/src/pivxd.cpp b/src/pivxd.cpp index dd9abfbcb74f..208f3b39066d 100644 --- a/src/pivxd.cpp +++ b/src/pivxd.cpp @@ -170,6 +170,10 @@ bool AppInit(int argc, char* argv[]) int main(int argc, char* argv[]) { +#ifdef WIN32 + util::WinCmdLineArgs winArgs; + std::tie(argc, argv) = winArgs.get(); +#endif SetupEnvironment(); // Connect pivxd signal handlers diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index e69ed0fc44af..baf74b5ffe03 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -535,7 +535,7 @@ fs::path static GetAutostartFilePath() bool GetStartOnSystemStartup() { - fs::ifstream optionFile(GetAutostartFilePath()); + fsbridge::ifstream optionFile(GetAutostartFilePath()); if (!optionFile.good()) return false; // Scan through file for "Hidden=true": @@ -564,7 +564,7 @@ bool SetStartOnSystemStartup(bool fAutoStart) fs::create_directories(GetAutostartDir()); - fs::ofstream optionFile(GetAutostartFilePath(), std::ios_base::out | std::ios_base::trunc); + fsbridge::ofstream optionFile(GetAutostartFilePath(), std::ios_base::out | std::ios_base::trunc); if (!optionFile.good()) return false; // Write a pivx.desktop file to the autostart directory: diff --git a/src/qt/pivx.cpp b/src/qt/pivx.cpp index eeaec7c187b5..5ab45ecb0af4 100644 --- a/src/qt/pivx.cpp +++ b/src/qt/pivx.cpp @@ -545,6 +545,10 @@ WId BitcoinApplication::getMainWinId() const #ifndef BITCOIN_QT_TEST int main(int argc, char* argv[]) { +#ifdef WIN32 + util::WinCmdLineArgs winArgs; + std::tie(argc, argv) = winArgs.get(); +#endif SetupEnvironment(); /// 1. Parse command-line options. These take precedence over anything else. diff --git a/src/qt/pivx/masternodeswidget.cpp b/src/qt/pivx/masternodeswidget.cpp index 709f7a67c9dd..53fa9ce1d8ba 100644 --- a/src/qt/pivx/masternodeswidget.cpp +++ b/src/qt/pivx/masternodeswidget.cpp @@ -383,7 +383,7 @@ void MasterNodesWidget::onDeleteMNClicked() fs::path pathBootstrap = GetDataDir() / strConfFile; if (fs::exists(pathBootstrap)) { fs::path pathMasternodeConfigFile = GetMasternodeConfigFile(); - fs::ifstream streamConfig(pathMasternodeConfigFile); + fsbridge::ifstream streamConfig(pathMasternodeConfigFile); if (!streamConfig.good()) { inform(tr("Invalid masternode.conf file")); diff --git a/src/qt/pivx/masternodewizarddialog.cpp b/src/qt/pivx/masternodewizarddialog.cpp index 8d5cb6297efd..03bc48841bfc 100644 --- a/src/qt/pivx/masternodewizarddialog.cpp +++ b/src/qt/pivx/masternodewizarddialog.cpp @@ -312,7 +312,7 @@ bool MasterNodeWizardDialog::createMN() } fs::path pathMasternodeConfigFile = GetMasternodeConfigFile(); - fs::ifstream streamConfig(pathMasternodeConfigFile); + fsbridge::ifstream streamConfig(pathMasternodeConfigFile); if (!streamConfig.good()) { returnStr = tr("Invalid masternode.conf file"); diff --git a/src/rpc/protocol.cpp b/src/rpc/protocol.cpp index 6a97883dceb2..b94af540c0f5 100644 --- a/src/rpc/protocol.cpp +++ b/src/rpc/protocol.cpp @@ -14,9 +14,6 @@ #include "utiltime.h" #include "version.h" -#include - - /** * JSON-RPC protocol. PIVX speaks version 1.0 for maximum compatibility, * but uses JSON-RPC 1.1/2.0 standards for parts of the 1.0 standard that were @@ -85,16 +82,16 @@ bool GenerateAuthCookie(std::string *cookie_out) /** the umask determines what permissions are used to create this file - * these are set to 077 in init.cpp unless overridden with -sysperms. */ - std::ofstream file; - fs::path filepath = GetAuthCookieFile(); - file.open(filepath.string().c_str()); + fsbridge::ofstream file; + fs::path filepath_tmp = GetAuthCookieFile(); + file.open(filepath_tmp); if (!file.is_open()) { - LogPrintf("Unable to open cookie authentication file %s for writing\n", filepath.string()); + LogPrintf("Unable to open cookie authentication file %s for writing\n", filepath_tmp.string()); return false; } file << cookie; file.close(); - LogPrintf("Generated RPC authentication cookie %s\n", filepath.string()); + LogPrintf("Generated RPC authentication cookie %s\n", filepath_tmp.string()); if (cookie_out) *cookie_out = cookie; @@ -103,10 +100,10 @@ bool GenerateAuthCookie(std::string *cookie_out) bool GetAuthCookie(std::string *cookie_out) { - std::ifstream file; + fsbridge::ifstream file; std::string cookie; fs::path filepath = GetAuthCookieFile(); - file.open(filepath.string().c_str()); + file.open(filepath); if (!file.is_open()) return false; std::getline(file, cookie); @@ -122,6 +119,6 @@ void DeleteAuthCookie() try { fs::remove(GetAuthCookieFile()); } catch (const fs::filesystem_error& e) { - LogPrintf("%s: Unable to remove random auth cookie file: %s\n", __func__, e.what()); + LogPrintf("%s: Unable to remove random auth cookie file: %s\n", __func__, fsbridge::get_filesystem_error_message(e)); } } diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index cd0e81583d83..62a3a3b2e404 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -127,6 +127,7 @@ set(BITCOIN_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/evo_deterministicmns_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/evo_specialtx_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/flatfile_tests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/fs_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/getarg_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/hash_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/key_tests.cpp diff --git a/src/test/fs_tests.cpp b/src/test/fs_tests.cpp new file mode 100644 index 000000000000..cb5efbc9392e --- /dev/null +++ b/src/test/fs_tests.cpp @@ -0,0 +1,56 @@ +// Copyright (c) 2011-2018 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// +#include "fs.h" +#include "test/test_pivx.h" + +#include + +BOOST_FIXTURE_TEST_SUITE(fs_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(fsbridge_fstream) +{ + fs::path tmpfolder = SetDataDir("fsbridge_fstream"); + // tmpfile1 should be the same as tmpfile2 + fs::path tmpfile1 = tmpfolder / "fs_tests_₿_🏃"; + fs::path tmpfile2 = tmpfolder / L"fs_tests_₿_🏃"; + { + fsbridge::ofstream file(tmpfile1); + file << "bitcoin"; + } + { + fsbridge::ifstream file(tmpfile2); + std::string input_buffer; + file >> input_buffer; + BOOST_CHECK_EQUAL(input_buffer, "bitcoin"); + } + { + fsbridge::ifstream file(tmpfile1, std::ios_base::in | std::ios_base::ate); + std::string input_buffer; + file >> input_buffer; + BOOST_CHECK_EQUAL(input_buffer, ""); + } + { + fsbridge::ofstream file(tmpfile2, std::ios_base::out | std::ios_base::app); + file << "tests"; + } + { + fsbridge::ifstream file(tmpfile1); + std::string input_buffer; + file >> input_buffer; + BOOST_CHECK_EQUAL(input_buffer, "bitcointests"); + } + { + fsbridge::ofstream file(tmpfile2, std::ios_base::out | std::ios_base::trunc); + file << "bitcoin"; + } + { + fsbridge::ifstream file(tmpfile1); + std::string input_buffer; + file >> input_buffer; + BOOST_CHECK_EQUAL(input_buffer, "bitcoin"); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 2b2f06ad87e3..34acaf333384 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -1093,6 +1093,22 @@ BOOST_AUTO_TEST_CASE(test_LockDirectory) fs::remove_all(dirname); } +BOOST_AUTO_TEST_CASE(test_DirIsWritable) +{ + // Should be able to write to the system tmp dir. + fs::path tmpdirname = fs::temp_directory_path(); + BOOST_CHECK_EQUAL(DirIsWritable(tmpdirname), true); + + // Should not be able to write to a non-existent dir. + tmpdirname = fs::temp_directory_path() / fs::unique_path(); + BOOST_CHECK_EQUAL(DirIsWritable(tmpdirname), false); + + fs::create_directory(tmpdirname); + // Should be able to write to it now. + BOOST_CHECK_EQUAL(DirIsWritable(tmpdirname), true); + fs::remove(tmpdirname); +} + namespace { struct Tracker diff --git a/src/util/system.cpp b/src/util/system.cpp index 39a762741745..93d44955c983 100644 --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -66,6 +66,7 @@ #include #include /* for _commit */ +#include #include #endif @@ -77,8 +78,6 @@ #include #endif -#include - const char * const PIVX_CONF_FILENAME = "pivx.conf"; const char * const PIVX_PID_FILENAME = "pivx.pid"; const char * const PIVX_MASTERNODE_CONF_FILENAME = "masternode.conf"; @@ -110,7 +109,7 @@ bool CheckDiskSpace(const fs::path& dir, uint64_t additional_bytes) * cleans them up and thus automatically unlocks them, or ReleaseDirectoryLocks * is called. */ -static std::map> dir_locks; +static std::map> dir_locks; /** Mutex to protect dir_locks. */ static std::mutex cs_dir_locks; @@ -127,18 +126,13 @@ bool LockDirectory(const fs::path& directory, const std::string& lockfile_name, // Create empty lock file if it doesn't exist. FILE* file = fsbridge::fopen(pathLockFile, "a"); if (file) fclose(file); - - try { - auto lock = std::make_unique(pathLockFile.string().c_str()); - if (!lock->try_lock()) { - return false; - } - if (!probe_only) { - // Lock successful and we're not just probing, put it into the map - dir_locks.emplace(pathLockFile.string(), std::move(lock)); - } - } catch (const boost::interprocess::interprocess_exception& e) { - return error("Error while attempting to lock directory %s: %s", directory.string(), e.what()); + auto lock = std::make_unique(pathLockFile); + if (!lock->TryLock()) { + return error("Error while attempting to lock directory %s: %s", directory.string(), lock->GetReason()); + } + if (!probe_only) { + // Lock successful and we're not just probing, put it into the map + dir_locks.emplace(pathLockFile.string(), std::move(lock)); } return true; } @@ -149,6 +143,19 @@ void ReleaseDirectoryLocks() dir_locks.clear(); } +bool DirIsWritable(const fs::path& directory) +{ + fs::path tmpFile = directory / fs::unique_path(); + + FILE* file = fsbridge::fopen(tmpFile, "a"); + if (!file) return false; + + fclose(file); + remove(tmpFile); + + return true; +} + /** * Interpret a string argument as a boolean. * @@ -829,7 +836,7 @@ void ArgsManager::ReadConfigFile(const std::string& confPath) m_config_args.clear(); } - fs::ifstream stream(GetConfigFile(confPath)); + fsbridge::ifstream stream(GetConfigFile(confPath)); // ok to not have a config file if (stream.good()) { @@ -885,8 +892,8 @@ void CreatePidFile(const fs::path& path, pid_t pid) bool RenameOver(fs::path src, fs::path dest) { #ifdef WIN32 - return MoveFileExA(src.string().c_str(), dest.string().c_str(), - MOVEFILE_REPLACE_EXISTING) != 0; + return MoveFileExW(src.wstring().c_str(), dest.wstring().c_str(), + MOVEFILE_REPLACE_EXISTING) != 0; #else int rc = std::rename(src.string().c_str(), dest.string().c_str()); return (rc == 0); @@ -1060,6 +1067,10 @@ void SetupEnvironment() } catch (const std::runtime_error&) { setenv("LC_ALL", "C", 1); } +#elif defined(WIN32) + // Set the default input/output charset is utf-8 + SetConsoleCP(CP_UTF8); + SetConsoleOutputCP(CP_UTF8); #endif // The path locale is lazy initialized and to avoid deinitialization errors // in multithreading environments, it is set explicitly by the main thread. @@ -1117,3 +1128,31 @@ int ScheduleBatchPriority(void) return 1; #endif } + +namespace util { +#ifdef WIN32 + WinCmdLineArgs::WinCmdLineArgs() +{ + wchar_t** wargv = CommandLineToArgvW(GetCommandLineW(), &argc); + std::wstring_convert, wchar_t> utf8_cvt; + argv = new char*[argc]; + args.resize(argc); + for (int i = 0; i < argc; i++) { + args[i] = utf8_cvt.to_bytes(wargv[i]); + argv[i] = &*args[i].begin(); + } + LocalFree(wargv); +} + +WinCmdLineArgs::~WinCmdLineArgs() +{ + delete[] argv; +} + +std::pair WinCmdLineArgs::get() +{ + return std::make_pair(argc, argv); +} +#endif +} // namespace util + diff --git a/src/util/system.h b/src/util/system.h index d3df7310272b..4f7d6ceb048d 100644 --- a/src/util/system.h +++ b/src/util/system.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -89,6 +90,7 @@ void AllocateFileRange(FILE* file, unsigned int offset, unsigned int length); bool CheckDiskSpace(const fs::path& dir, uint64_t additional_bytes = 0); bool RenameOver(fs::path src, fs::path dest); bool LockDirectory(const fs::path& directory, const std::string& lockfile_name, bool probe_only=false); +bool DirIsWritable(const fs::path& directory); /** Release all directory locks. This is used for unit testing only, at runtime * the global destructor will take care of the locks. @@ -300,4 +302,23 @@ fs::path AbsPathForConfigVal(const fs::path& path, bool net_specific = true); */ int ScheduleBatchPriority(void); +namespace util { + +#ifdef WIN32 +class WinCmdLineArgs +{ +public: + WinCmdLineArgs(); + ~WinCmdLineArgs(); + std::pair get(); + +private: + int argc; + char** argv; + std::vector args; +}; +#endif + +} // namespace util + #endif // BITCOIN_UTIL_SYSTEM_H diff --git a/src/wallet/db.cpp b/src/wallet/db.cpp index 9513abd2ea6f..d1bd780822b0 100644 --- a/src/wallet/db.cpp +++ b/src/wallet/db.cpp @@ -780,7 +780,7 @@ bool CWalletDBWrapper::Backup(const std::string& strDest) LogPrintf("copied %s to %s\n", strFile, pathDest.string()); return true; } catch (const fs::filesystem_error& e) { - LogPrintf("error copying %s to %s - %s\n", strFile, pathDest.string(), e.what()); + LogPrintf("error copying %s to %s - %s\n", strFile, pathDest.string(), fsbridge::get_filesystem_error_message(e)); return false; } } diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 7cded639bd48..54439ea6b91d 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -19,7 +19,6 @@ #include "wallet.h" #include "validation.h" -#include #include #include @@ -350,10 +349,11 @@ UniValue importwallet(const JSONRPCRequest& request) "\nImport using the json rpc call\n" + HelpExampleRpc("importwallet", "\"test\"")); - std::ifstream file; - file.open(request.params[0].get_str().c_str(), std::ios::in | std::ios::ate); - if (!file.is_open()) + fsbridge::ifstream file; + file.open(request.params[0].get_str(), std::ios::in | std::ios::ate); + if (!file.is_open()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file"); + } WalletRescanReserver reserver(pwallet); if (!reserver.reserve()) { @@ -547,8 +547,8 @@ UniValue dumpwallet(const JSONRPCRequest& request) throw JSONRPCError(RPC_INVALID_PARAMETER, filepath.string() + " already exists. If you are sure this is what you want, move it out of the way first"); } - std::ofstream file; - file.open(request.params[0].get_str().c_str()); + fsbridge::ofstream file; + file.open(filepath); if (!file.is_open()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file"); diff --git a/test/functional/feature_filelock.py b/test/functional/feature_filelock.py new file mode 100755 index 000000000000..910a2d645997 --- /dev/null +++ b/test/functional/feature_filelock.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Check that it's not possible to start a second pivxd instance using the same datadir or wallet.""" +import os + +from test_framework.test_framework import PivxTestFramework +from test_framework.test_node import ErrorMatch + +class FilelockTest(PivxTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + + def setup_network(self): + self.add_nodes(self.num_nodes, extra_args=None) + self.nodes[0].start([]) + self.nodes[0].wait_for_rpc_connection() + + def is_wallet_compiled(self): + return True + + def run_test(self): + datadir = os.path.join(self.nodes[0].datadir, 'regtest') + self.log.info("Using datadir {}".format(datadir)) + + self.log.info("Check that we can't start a second pivxd instance using the same datadir") + expected_msg = "Error: Cannot obtain a lock on data directory {}. PIVX Core is probably already running.".format(datadir) + self.nodes[1].assert_start_raises_init_error(extra_args=['-datadir={}'.format(self.nodes[0].datadir), '-noserver'], expected_msg=expected_msg) + + if self.is_wallet_compiled(): + wallet_dir = os.path.join(datadir, 'wallets') + self.log.info("Check that we can't start a second pivxd instance using the same wallet") + expected_msg = "Error: Error initializing wallet database environment" + self.nodes[1].assert_start_raises_init_error(extra_args=['-walletdir={}'.format(wallet_dir), '-noserver'], expected_msg=expected_msg, match=ErrorMatch.PARTIAL_REGEX) + +if __name__ == '__main__': + FilelockTest().main() diff --git a/test/functional/feature_uacomment.py b/test/functional/feature_uacomment.py index e825bf7dcc30..b91a90fb9b37 100755 --- a/test/functional/feature_uacomment.py +++ b/test/functional/feature_uacomment.py @@ -30,7 +30,7 @@ def run_test(self): self.nodes[0].assert_start_raises_init_error(["-uacomment=" + 'a' * 256], expected, match=ErrorMatch.FULL_REGEX) self.log.info("test -uacomment unsafe characters") - for unsafe_char in ['/', ':', '(', ')']: + for unsafe_char in ['/', ':', '(', ')', '₿', '🏃']: expected = r"Error: User Agent comment \(" + re.escape(unsafe_char) + r"\) contains unsafe characters." self.nodes[0].assert_start_raises_init_error(["-uacomment=" + unsafe_char], expected, match=ErrorMatch.FULL_REGEX) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 9dba4eff818b..cc49ce92771e 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -127,6 +127,7 @@ 'mining_v5_upgrade.py', # ~ 48 sec 'p2p_mempool.py', # ~ 46 sec 'rpc_named_arguments.py', # ~ 45 sec + 'feature_filelock.py', 'feature_help.py', # ~ 30 sec # Don't append tests at the end to avoid merge conflicts