Merge from release/0.10
This commit is contained in:
commit
58fd7c3956
@ -479,7 +479,7 @@ jobs:
|
||||
OMP_NUM_THREADS: "1"
|
||||
CXXFLAGS: "-O2 -fsanitize=thread -fno-omit-frame-pointer"
|
||||
BUILD_TYPE: "Debug"
|
||||
GTEST_ARGS: "--gtest_filter=-LoggingTest.LoggingAlsoWorksAfterFork:AssertTest_DebugBuild.*:CliTest_Setup.*:CliTest_IntegrityCheck.*:*/CliTest_WrongEnvironment.*:CliTest_Unmount.*"
|
||||
GTEST_ARGS: "--gtest_filter=-LoggingTest.LoggingAlsoWorksAfterFork:AssertTest_DebugBuild.*:SignalCatcherTest.*_thenDies:CliTest_Setup.*:CliTest_IntegrityCheck.*:*/CliTest_WrongEnvironment.*:CliTest_Unmount.*"
|
||||
CMAKE_FLAGS: ""
|
||||
RUN_TESTS: true
|
||||
clang_tidy:
|
||||
|
@ -52,6 +52,10 @@ void BlobOnBlocks::flush() {
|
||||
_datatree->flush();
|
||||
}
|
||||
|
||||
uint32_t BlobOnBlocks::numNodes() const {
|
||||
return _datatree->numNodes();
|
||||
}
|
||||
|
||||
const BlockId &BlobOnBlocks::blockId() const {
|
||||
return _datatree->blockId();
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ public:
|
||||
|
||||
void flush() override;
|
||||
|
||||
uint32_t numNodes() const override;
|
||||
|
||||
cpputils::unique_ref<parallelaccessdatatreestore::DataTreeRef> releaseTree();
|
||||
|
||||
private:
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include <cpp-utils/assert/assert.h>
|
||||
#include "impl/LeafTraverser.h"
|
||||
#include <boost/thread.hpp>
|
||||
#include <blobstore/implementations/onblocks/utils/Math.h>
|
||||
|
||||
using blockstore::BlockId;
|
||||
using blobstore::onblocks::datanodestore::DataNodeStore;
|
||||
@ -64,7 +65,16 @@ unique_ref<DataNode> DataTree::releaseRootNode() {
|
||||
return std::move(_rootNode);
|
||||
}
|
||||
|
||||
//TODO Test numLeaves(), for example also two configurations with same number of bytes but different number of leaves (last leaf has 0 bytes)
|
||||
uint32_t DataTree::numNodes() const {
|
||||
uint32_t numNodesCurrentLevel = numLeaves();
|
||||
uint32_t totalNumNodes = numNodesCurrentLevel;
|
||||
for(size_t level = 0; level < _rootNode->depth(); ++level) {
|
||||
numNodesCurrentLevel = blobstore::onblocks::utils::ceilDivision(numNodesCurrentLevel, static_cast<uint32_t>(_nodeStore->layout().maxChildrenPerInnerNode()));
|
||||
totalNumNodes += numNodesCurrentLevel;
|
||||
}
|
||||
return totalNumNodes;
|
||||
}
|
||||
|
||||
uint32_t DataTree::numLeaves() const {
|
||||
shared_lock<shared_mutex> lock(_treeStructureMutex);
|
||||
|
||||
|
@ -40,6 +40,7 @@ public:
|
||||
|
||||
void resizeNumBytes(uint64_t newNumBytes);
|
||||
|
||||
uint32_t numNodes() const;
|
||||
uint32_t numLeaves() const;
|
||||
uint64_t numBytes() const;
|
||||
|
||||
|
@ -54,6 +54,10 @@ public:
|
||||
return _baseTree->flush();
|
||||
}
|
||||
|
||||
uint32_t numNodes() const {
|
||||
return _baseTree->numNodes();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
datatreestore::DataTree *_baseTree;
|
||||
|
@ -26,6 +26,8 @@ public:
|
||||
|
||||
virtual void flush() = 0;
|
||||
|
||||
virtual uint32_t numNodes() const = 0;
|
||||
|
||||
//TODO Test tryRead
|
||||
};
|
||||
|
||||
|
@ -2,11 +2,14 @@
|
||||
#include "IntegrityBlockStore2.h"
|
||||
#include "KnownBlockVersions.h"
|
||||
#include <cpp-utils/data/SerializationHelper.h>
|
||||
#include <cpp-utils/process/SignalCatcher.h>
|
||||
#include <cpp-utils/io/ProgressBar.h>
|
||||
|
||||
using cpputils::Data;
|
||||
using cpputils::unique_ref;
|
||||
using cpputils::serialize;
|
||||
using cpputils::deserialize;
|
||||
using cpputils::SignalCatcher;
|
||||
using std::string;
|
||||
using boost::optional;
|
||||
using boost::none;
|
||||
@ -195,22 +198,32 @@ void IntegrityBlockStore2::forEachBlock(std::function<void (const BlockId &)> ca
|
||||
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
void IntegrityBlockStore2::migrateFromBlockstoreWithoutVersionNumbers(BlockStore2 *baseBlockStore, const boost::filesystem::path &integrityFilePath, uint32_t myClientId) {
|
||||
std::cout << "Migrating file system for integrity features. Please don't interrupt this process. This can take a while..." << std::flush;
|
||||
SignalCatcher signalCatcher;
|
||||
|
||||
KnownBlockVersions knownBlockVersions(integrityFilePath, myClientId);
|
||||
baseBlockStore->forEachBlock([&baseBlockStore, &knownBlockVersions] (const BlockId &blockId) {
|
||||
uint64_t numProcessedBlocks = 0;
|
||||
cpputils::ProgressBar progressbar("Migrating file system for integrity features. This can take a while...", baseBlockStore->numBlocks());
|
||||
baseBlockStore->forEachBlock([&] (const BlockId &blockId) {
|
||||
if (signalCatcher.signal_occurred()) {
|
||||
throw std::runtime_error("Caught signal");
|
||||
}
|
||||
migrateBlockFromBlockstoreWithoutVersionNumbers(baseBlockStore, blockId, &knownBlockVersions);
|
||||
progressbar.update(++numProcessedBlocks);
|
||||
});
|
||||
std::cout << "done" << std::endl;
|
||||
}
|
||||
|
||||
void IntegrityBlockStore2::migrateBlockFromBlockstoreWithoutVersionNumbers(blockstore::BlockStore2* baseBlockStore, const blockstore::BlockId& blockId, KnownBlockVersions *knownBlockVersions) {
|
||||
uint64_t version = knownBlockVersions->incrementVersion(blockId);
|
||||
|
||||
auto data_ = baseBlockStore->load(blockId);
|
||||
if (data_ == boost::none) {
|
||||
LOG(WARN, "Block not found, but was returned from forEachBlock before");
|
||||
return;
|
||||
}
|
||||
if (0 != _readFormatHeader(*data_)) {
|
||||
// already migrated
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t version = knownBlockVersions->incrementVersion(blockId);
|
||||
cpputils::Data data = std::move(*data_);
|
||||
cpputils::Data dataWithHeader = _prependHeaderToData(blockId, knownBlockVersions->myClientId(), version, data);
|
||||
baseBlockStore->store(blockId, dataWithHeader);
|
||||
|
@ -11,6 +11,7 @@ set(SOURCES
|
||||
crypto/hash/Hash.cpp
|
||||
process/daemonize.cpp
|
||||
process/subprocess.cpp
|
||||
process/SignalCatcher.cpp
|
||||
tempfile/TempFile.cpp
|
||||
tempfile/TempDir.cpp
|
||||
network/HttpClient.cpp
|
||||
@ -22,10 +23,12 @@ set(SOURCES
|
||||
io/IOStreamConsole.cpp
|
||||
io/NoninteractiveConsole.cpp
|
||||
io/pipestream.cpp
|
||||
io/ProgressBar.cpp
|
||||
thread/LoopThread.cpp
|
||||
thread/ThreadSystem.cpp
|
||||
thread/debugging_nonwindows.cpp
|
||||
thread/debugging_windows.cpp
|
||||
thread/LeftRight.cpp
|
||||
random/Random.cpp
|
||||
random/RandomGeneratorThread.cpp
|
||||
random/OSRandomGenerator.cpp
|
||||
|
35
src/cpp-utils/io/ProgressBar.cpp
Normal file
35
src/cpp-utils/io/ProgressBar.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
#include "ProgressBar.h"
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include "IOStreamConsole.h"
|
||||
|
||||
using std::string;
|
||||
|
||||
namespace cpputils {
|
||||
|
||||
ProgressBar::ProgressBar(const char* preamble, uint64_t max_value)
|
||||
: ProgressBar(std::make_shared<IOStreamConsole>(), preamble, max_value) {}
|
||||
|
||||
ProgressBar::ProgressBar(std::shared_ptr<Console> console, const char* preamble, uint64_t max_value)
|
||||
: _console(std::move(console))
|
||||
, _preamble(string("\r") + preamble + " ")
|
||||
, _max_value(max_value)
|
||||
, _lastPercentage(std::numeric_limits<decltype(_lastPercentage)>::max()) {
|
||||
ASSERT(_max_value > 0, "Progress bar can't handle max_value of 0");
|
||||
|
||||
_console->print("\n");
|
||||
|
||||
// show progress bar. _lastPercentage is different to zero, so it shows.
|
||||
update(0);
|
||||
}
|
||||
|
||||
void ProgressBar::update(uint64_t value) {
|
||||
const size_t percentage = (100 * value) / _max_value;
|
||||
if (percentage != _lastPercentage) {
|
||||
_console->print(_preamble + std::to_string(percentage) + "%");
|
||||
_lastPercentage = percentage;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
31
src/cpp-utils/io/ProgressBar.h
Normal file
31
src/cpp-utils/io/ProgressBar.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#ifndef MESSMER_CPPUTILS_IO_PROGRESSBAR_H
|
||||
#define MESSMER_CPPUTILS_IO_PROGRESSBAR_H
|
||||
|
||||
#include <cpp-utils/macros.h>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "Console.h"
|
||||
|
||||
namespace cpputils {
|
||||
|
||||
class ProgressBar final {
|
||||
public:
|
||||
explicit ProgressBar(std::shared_ptr<Console> console, const char* preamble, uint64_t max_value);
|
||||
explicit ProgressBar(const char* preamble, uint64_t max_value);
|
||||
|
||||
void update(uint64_t value);
|
||||
|
||||
private:
|
||||
|
||||
std::shared_ptr<Console> _console;
|
||||
std::string _preamble;
|
||||
uint64_t _max_value;
|
||||
size_t _lastPercentage;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(ProgressBar);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
239
src/cpp-utils/process/SignalCatcher.cpp
Normal file
239
src/cpp-utils/process/SignalCatcher.cpp
Normal file
@ -0,0 +1,239 @@
|
||||
#include "SignalCatcher.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <cpp-utils/assert/assert.h>
|
||||
#include <cpp-utils/thread/LeftRight.h>
|
||||
|
||||
using std::make_unique;
|
||||
using std::vector;
|
||||
using std::pair;
|
||||
|
||||
namespace cpputils {
|
||||
|
||||
namespace {
|
||||
|
||||
void got_signal(int signal);
|
||||
|
||||
using SignalHandlerFunction = void(int);
|
||||
|
||||
constexpr SignalHandlerFunction* signal_catcher_function = &got_signal;
|
||||
|
||||
#if !defined(_MSC_VER)
|
||||
|
||||
class SignalHandlerRAII final {
|
||||
public:
|
||||
SignalHandlerRAII(int signal)
|
||||
: _old_handler(), _signal(signal) {
|
||||
struct sigaction new_signal_handler{};
|
||||
std::memset(&new_signal_handler, 0, sizeof(new_signal_handler));
|
||||
new_signal_handler.sa_handler = signal_catcher_function; // NOLINT(cppcoreguidelines-pro-type-union-access)
|
||||
new_signal_handler.sa_flags = SA_RESTART;
|
||||
int error = sigfillset(&new_signal_handler.sa_mask); // block all signals while signal handler is running
|
||||
if (0 != error) {
|
||||
throw std::runtime_error("Error calling sigfillset. Errno: " + std::to_string(errno));
|
||||
}
|
||||
_sigaction(_signal, &new_signal_handler, &_old_handler);
|
||||
}
|
||||
|
||||
~SignalHandlerRAII() {
|
||||
// reset to old signal handler
|
||||
struct sigaction removed_handler{};
|
||||
_sigaction(_signal, &_old_handler, &removed_handler);
|
||||
if (signal_catcher_function != removed_handler.sa_handler) { // NOLINT(cppcoreguidelines-pro-type-union-access)
|
||||
ASSERT(false, "Signal handler screwup. We just replaced a signal handler that wasn't our own.");
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static void _sigaction(int signal, struct sigaction *new_handler, struct sigaction *old_handler) {
|
||||
int error = sigaction(signal, new_handler, old_handler);
|
||||
if (0 != error) {
|
||||
throw std::runtime_error("Error calling sigaction. Errno: " + std::to_string(errno));
|
||||
}
|
||||
}
|
||||
|
||||
struct sigaction _old_handler;
|
||||
int _signal;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(SignalHandlerRAII);
|
||||
};
|
||||
|
||||
#else
|
||||
|
||||
class SignalHandlerRAII final {
|
||||
public:
|
||||
SignalHandlerRAII(int signal)
|
||||
: _old_handler(nullptr), _signal(signal) {
|
||||
_old_handler = ::signal(_signal, signal_catcher_function);
|
||||
if (_old_handler == SIG_ERR) {
|
||||
throw std::logic_error("Error calling signal(). Errno: " + std::to_string(errno));
|
||||
}
|
||||
}
|
||||
|
||||
~SignalHandlerRAII() {
|
||||
// reset to old signal handler
|
||||
SignalHandlerFunction* error = ::signal(_signal, _old_handler);
|
||||
if (error == SIG_ERR) {
|
||||
throw std::logic_error("Error resetting signal(). Errno: " + std::to_string(errno));
|
||||
}
|
||||
if (error != signal_catcher_function) {
|
||||
throw std::logic_error("Signal handler screwup. We just replaced a signal handler that wasn't our own.");
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
SignalHandlerFunction* _old_handler;
|
||||
int _signal;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(SignalHandlerRAII);
|
||||
};
|
||||
|
||||
// The Linux default behavior (i.e. the way we set up sigaction above) is to disable signal processing while the signal
|
||||
// handler is running and to re-enable the custom handler once processing is finished. The Windows default behavior
|
||||
// is to reset the handler to the default handler directly before executing the handler, i.e. the handler will only
|
||||
// be called once. To fix this, we use this RAII class on Windows, of which an instance will live in the signal handler.
|
||||
// In its constructor, it disables signal handling, and in its destructor it re-sets the custom handler.
|
||||
// This is not perfect since there is a small time window between calling the signal handler and calling the constructor
|
||||
// of this class, but it's the best we can do.
|
||||
class SignalHandlerRunningRAII final {
|
||||
public:
|
||||
SignalHandlerRunningRAII(int signal) : _signal(signal) {
|
||||
SignalHandlerFunction* old_handler = ::signal(_signal, SIG_IGN);
|
||||
if (old_handler == SIG_ERR) {
|
||||
throw std::logic_error("Error disabling signal(). Errno: " + std::to_string(errno));
|
||||
}
|
||||
if (old_handler != SIG_DFL) {
|
||||
// see description above, we expected the signal handler to be reset.
|
||||
throw std::logic_error("We expected windows to reset the signal handler but it didn't. Did the Windows API change?");
|
||||
}
|
||||
}
|
||||
|
||||
~SignalHandlerRunningRAII() {
|
||||
SignalHandlerFunction* old_handler = ::signal(_signal, signal_catcher_function);
|
||||
if (old_handler == SIG_ERR) {
|
||||
throw std::logic_error("Error resetting signal() after calling handler. Errno: " + std::to_string(errno));
|
||||
}
|
||||
if (old_handler != SIG_IGN) {
|
||||
throw std::logic_error("Weird, we just did set the signal handler to ignore. Why isn't it still ignore?");
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int _signal;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
class SignalCatcherRegistry final {
|
||||
public:
|
||||
void add(int signal, std::atomic<bool>* signal_occurred_flag) {
|
||||
_catchers.write([&] (auto& catchers) {
|
||||
catchers.emplace_back(signal, signal_occurred_flag);
|
||||
});
|
||||
}
|
||||
|
||||
void remove(std::atomic<bool>* catcher) {
|
||||
_catchers.write([&] (auto& catchers) {
|
||||
auto found = std::find_if(catchers.rbegin(), catchers.rend(), [catcher] (const auto& entry) {return entry.second == catcher;});
|
||||
ASSERT(found != catchers.rend(), "Signal handler not found");
|
||||
catchers.erase(--found.base()); // decrement because it's a reverse iterator
|
||||
});
|
||||
}
|
||||
|
||||
~SignalCatcherRegistry() {
|
||||
ASSERT(0 == _catchers.read([] (auto& catchers) {return catchers.size();}), "Leftover signal catchers that weren't destroyed");
|
||||
}
|
||||
|
||||
std::atomic<bool>* find(int signal) {
|
||||
// this is called in a signal handler and must be mutex-free.
|
||||
return _catchers.read([&](auto& catchers) {
|
||||
auto found = std::find_if(catchers.rbegin(), catchers.rend(), [signal](const auto& entry) {return entry.first == signal; });
|
||||
ASSERT(found != catchers.rend(), "Signal handler not found");
|
||||
return found->second;
|
||||
});
|
||||
}
|
||||
|
||||
static SignalCatcherRegistry& singleton() {
|
||||
static SignalCatcherRegistry _singleton;
|
||||
return _singleton;
|
||||
}
|
||||
|
||||
private:
|
||||
SignalCatcherRegistry() = default;
|
||||
|
||||
// using LeftRight datastructure because we need mutex-free reads. Signal handlers can't use mutexes.
|
||||
LeftRight<vector<pair<int, std::atomic<bool>*>>> _catchers;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(SignalCatcherRegistry);
|
||||
};
|
||||
|
||||
void got_signal(int signal) {
|
||||
#if defined(_MSC_VER)
|
||||
// Only needed on Windows, Linux does this by default. See comment on SignalHandlerRunningRAII class.
|
||||
SignalHandlerRunningRAII disable_signal_processing_while_handler_running_and_reset_handler_afterwards(signal);
|
||||
#endif
|
||||
std::atomic<bool>* catcher = SignalCatcherRegistry::singleton().find(signal);
|
||||
*catcher = true;
|
||||
}
|
||||
|
||||
class SignalCatcherRegisterer final {
|
||||
public:
|
||||
SignalCatcherRegisterer(int signal, std::atomic<bool>* catcher)
|
||||
: _catcher(catcher) {
|
||||
SignalCatcherRegistry::singleton().add(signal, _catcher);
|
||||
}
|
||||
|
||||
~SignalCatcherRegisterer() {
|
||||
SignalCatcherRegistry::singleton().remove(_catcher);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<bool>* _catcher;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(SignalCatcherRegisterer);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace details {
|
||||
|
||||
class SignalCatcherImpl final {
|
||||
public:
|
||||
SignalCatcherImpl(int signal, std::atomic<bool>* signal_occurred_flag)
|
||||
: _registerer(signal, signal_occurred_flag)
|
||||
, _handler(signal) {
|
||||
// note: the order of the members ensures that:
|
||||
// - when registering the signal handler fails, the registerer will be destroyed, unregistering the signal_occurred_flag,
|
||||
// i.e. there is no leak.
|
||||
|
||||
// Allow only the set of signals that is supported on all platforms, see for Windows: https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal?view=vs-2017
|
||||
ASSERT(signal == SIGABRT || signal == SIGFPE || signal == SIGILL || signal == SIGINT || signal == SIGSEGV || signal == SIGTERM, "Unknown signal");
|
||||
}
|
||||
private:
|
||||
SignalCatcherRegisterer _registerer;
|
||||
SignalHandlerRAII _handler;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(SignalCatcherImpl);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
SignalCatcher::SignalCatcher(std::initializer_list<int> signals)
|
||||
: _signal_occurred(false)
|
||||
, _impls() {
|
||||
// note: the order of the members ensures that:
|
||||
// - when the signal handler is set, the _signal_occurred flag is already initialized.
|
||||
// - the _signal_occurred flag will not be destructed as long as the signal handler might be called (i.e. as long as _impls lives)
|
||||
|
||||
_impls.reserve(signals.size());
|
||||
for (int signal : signals) {
|
||||
_impls.emplace_back(make_unique<details::SignalCatcherImpl>(signal, &_signal_occurred));
|
||||
}
|
||||
}
|
||||
|
||||
SignalCatcher::~SignalCatcher() {}
|
||||
|
||||
}
|
42
src/cpp-utils/process/SignalCatcher.h
Normal file
42
src/cpp-utils/process/SignalCatcher.h
Normal file
@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#ifndef MESSMER_CPPUTILS_PROCESS_SIGNALCATCHER_H_
|
||||
#define MESSMER_CPPUTILS_PROCESS_SIGNALCATCHER_H_
|
||||
|
||||
#include <cpp-utils/macros.h>
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace cpputils {
|
||||
|
||||
namespace details {
|
||||
class SignalCatcherImpl;
|
||||
}
|
||||
|
||||
/*
|
||||
* While an instance of this class is in scope, the specified signal (e.g. SIGINT)
|
||||
* is caught and doesn't exit the application. You can poll if the signal occurred.
|
||||
*/
|
||||
class SignalCatcher final {
|
||||
public:
|
||||
SignalCatcher(): SignalCatcher({SIGINT, SIGTERM}) {}
|
||||
|
||||
SignalCatcher(std::initializer_list<int> signals);
|
||||
~SignalCatcher();
|
||||
|
||||
bool signal_occurred() const {
|
||||
return _signal_occurred;
|
||||
}
|
||||
|
||||
private:
|
||||
// note: _signal_occurred must be initialized before _impl because _impl might use it
|
||||
std::atomic<bool> _signal_occurred;
|
||||
std::vector<std::unique_ptr<details::SignalCatcherImpl>> _impls;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(SignalCatcher);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
1
src/cpp-utils/thread/LeftRight.cpp
Normal file
1
src/cpp-utils/thread/LeftRight.cpp
Normal file
@ -0,0 +1 @@
|
||||
#include "LeftRight.h"
|
165
src/cpp-utils/thread/LeftRight.h
Normal file
165
src/cpp-utils/thread/LeftRight.h
Normal file
@ -0,0 +1,165 @@
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <cpp-utils/macros.h>
|
||||
#include <array>
|
||||
|
||||
namespace cpputils {
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct IncrementRAII final {
|
||||
public:
|
||||
explicit IncrementRAII(std::atomic<int32_t> *counter): _counter(counter) {
|
||||
++(*_counter);
|
||||
}
|
||||
|
||||
~IncrementRAII() {
|
||||
--(*_counter);
|
||||
}
|
||||
private:
|
||||
std::atomic<int32_t> *_counter;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(IncrementRAII);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// LeftRight wait-free readers synchronization primitive
|
||||
// https://hal.archives-ouvertes.fr/hal-01207881/document
|
||||
template <class T>
|
||||
class LeftRight final {
|
||||
public:
|
||||
LeftRight()
|
||||
: _writeMutex()
|
||||
, _foregroundCounterIndex{0}
|
||||
, _foregroundDataIndex{0}
|
||||
, _counters{{{0}, {0}}}
|
||||
, _data{{{}, {}}}
|
||||
, _inDestruction(false) {}
|
||||
|
||||
~LeftRight() {
|
||||
// from now on, no new readers/writers will be accepted (see asserts in read()/write())
|
||||
_inDestruction = true;
|
||||
|
||||
// wait until any potentially running writers are finished
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_writeMutex);
|
||||
}
|
||||
|
||||
// wait until any potentially running readers are finished
|
||||
while (_counters[0].load() != 0 || _counters[1].load() != 0) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
auto read(F&& readFunc) const {
|
||||
if(_inDestruction.load()) {
|
||||
throw std::logic_error("Issued LeftRight::read() after the destructor started running");
|
||||
}
|
||||
|
||||
detail::IncrementRAII _increment_counter(&_counters[_foregroundCounterIndex.load()]); // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index)
|
||||
return readFunc(_data[_foregroundDataIndex.load()]); // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index)
|
||||
}
|
||||
|
||||
// Throwing from write would result in invalid state
|
||||
template <typename F>
|
||||
auto write(F&& writeFunc) {
|
||||
if(_inDestruction.load()) {
|
||||
throw std::logic_error("Issued LeftRight::read() after the destructor started running");
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(_writeMutex);
|
||||
return _write(writeFunc);
|
||||
}
|
||||
|
||||
private:
|
||||
template <class F>
|
||||
auto _write(const F& writeFunc) {
|
||||
/*
|
||||
* Assume, A is in background and B in foreground. In simplified terms, we want to do the following:
|
||||
* 1. Write to A (old background)
|
||||
* 2. Switch A/B
|
||||
* 3. Write to B (new background)
|
||||
*
|
||||
* More detailed algorithm (explanations on why this is important are below in code):
|
||||
* 1. Write to A
|
||||
* 2. Switch A/B data pointers
|
||||
* 3. Wait until A counter is zero
|
||||
* 4. Switch A/B counters
|
||||
* 5. Wait until B counter is zero
|
||||
* 6. Write to B
|
||||
*/
|
||||
|
||||
auto localDataIndex = _foregroundDataIndex.load();
|
||||
|
||||
// 1. Write to A
|
||||
_callWriteFuncOnBackgroundInstance(writeFunc, localDataIndex);
|
||||
|
||||
// 2. Switch A/B data pointers
|
||||
localDataIndex = localDataIndex ^ 1;
|
||||
_foregroundDataIndex = localDataIndex;
|
||||
|
||||
/*
|
||||
* 3. Wait until A counter is zero
|
||||
*
|
||||
* In the previous write run, A was foreground and B was background.
|
||||
* There was a time after switching _foregroundDataIndex (B to foreground) and before switching _foregroundCounterIndex,
|
||||
* in which new readers could have read B but incremented A's counter.
|
||||
*
|
||||
* In this current run, we just switched _foregroundDataIndex (A back to foreground), but before writing to
|
||||
* the new background B, we have to make sure A's counter was zero briefly, so all these old readers are gone.
|
||||
*/
|
||||
auto localCounterIndex = _foregroundCounterIndex.load();
|
||||
_waitForBackgroundCounterToBeZero(localCounterIndex);
|
||||
|
||||
/*
|
||||
*4. Switch A/B counters
|
||||
*
|
||||
* Now that we know all readers on B are really gone, we can switch the counters and have new readers
|
||||
* increment A's counter again, which is the correct counter since they're reading A.
|
||||
*/
|
||||
localCounterIndex = localCounterIndex ^ 1;
|
||||
_foregroundCounterIndex = localCounterIndex;
|
||||
|
||||
/*
|
||||
* 5. Wait until B counter is zero
|
||||
*
|
||||
* This waits for all the readers on B that came in while both data and counter for B was in foreground,
|
||||
* i.e. normal readers that happened outside of that brief gap between switching data and counter.
|
||||
*/
|
||||
_waitForBackgroundCounterToBeZero(localCounterIndex);
|
||||
|
||||
// 6. Write to B
|
||||
_callWriteFuncOnBackgroundInstance(writeFunc, localDataIndex);
|
||||
}
|
||||
|
||||
template<class F>
|
||||
auto _callWriteFuncOnBackgroundInstance(const F& writeFunc, uint8_t localDataIndex) {
|
||||
try {
|
||||
return writeFunc(_data[localDataIndex ^ 1]); // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index)
|
||||
} catch (...) {
|
||||
// recover invariant by copying from the foreground instance
|
||||
_data[localDataIndex ^ 1] = _data[localDataIndex]; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index)
|
||||
// rethrow
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void _waitForBackgroundCounterToBeZero(uint8_t counterIndex) {
|
||||
while (_counters[counterIndex ^ 1].load() != 0) { // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index)
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex _writeMutex;
|
||||
std::atomic<uint8_t> _foregroundCounterIndex;
|
||||
std::atomic<uint8_t> _foregroundDataIndex;
|
||||
mutable std::array<std::atomic<int32_t>, 2> _counters;
|
||||
std::array<T, 2> _data;
|
||||
std::atomic<bool> _inDestruction;
|
||||
};
|
||||
|
||||
}
|
@ -32,6 +32,7 @@ CryConfig::CryConfig()
|
||||
, _exclusiveClientId(none)
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
, _hasVersionNumbers(true)
|
||||
, _hasParentPointers(true)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
@ -53,6 +54,7 @@ CryConfig CryConfig::load(const Data &data) {
|
||||
cfg._exclusiveClientId = pt.get_optional<uint32_t>("cryfs.exclusiveClientId");
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
cfg._hasVersionNumbers = pt.get<bool>("cryfs.migrations.hasVersionNumbers", false);
|
||||
cfg._hasParentPointers = pt.get<bool>("cryfs.migrations.hasParentPointers", false);
|
||||
#endif
|
||||
|
||||
optional<string> filesystemIdOpt = pt.get_optional<string>("cryfs.filesystemId");
|
||||
@ -81,6 +83,7 @@ Data CryConfig::save() const {
|
||||
}
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
pt.put<bool>("cryfs.migrations.hasVersionNumbers", _hasVersionNumbers);
|
||||
pt.put<bool>("cryfs.migrations.hasParentPointers", _hasParentPointers);
|
||||
#endif
|
||||
|
||||
stringstream stream;
|
||||
@ -172,6 +175,14 @@ bool CryConfig::HasVersionNumbers() const {
|
||||
void CryConfig::SetHasVersionNumbers(bool value) {
|
||||
_hasVersionNumbers = value;
|
||||
}
|
||||
|
||||
bool CryConfig::HasParentPointers() const {
|
||||
return _hasParentPointers;
|
||||
}
|
||||
|
||||
void CryConfig::SetHasParentPointers(bool value) {
|
||||
_hasParentPointers = value;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
@ -56,6 +56,11 @@ public:
|
||||
// Version numbers cannot be disabled, but the file system will be migrated to version numbers automatically.
|
||||
bool HasVersionNumbers() const;
|
||||
void SetHasVersionNumbers(bool value);
|
||||
|
||||
// This is a trigger to recognize old file systems that didn't have version numbers.
|
||||
// Version numbers cannot be disabled, but the file system will be migrated to version numbers automatically.
|
||||
bool HasParentPointers() const;
|
||||
void SetHasParentPointers(bool value);
|
||||
#endif
|
||||
|
||||
static CryConfig load(const cpputils::Data &data);
|
||||
@ -73,6 +78,7 @@ private:
|
||||
boost::optional<uint32_t> _exclusiveClientId;
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
bool _hasVersionNumbers;
|
||||
bool _hasParentPointers;
|
||||
#endif
|
||||
|
||||
CryConfig &operator=(const CryConfig &rhs) = delete;
|
||||
|
@ -80,7 +80,14 @@ unique_ref<fsblobstore::FsBlobStore> CryDevice::MigrateOrCreateFsBlobStore(uniqu
|
||||
if ("" == rootBlobId) {
|
||||
return make_unique_ref<FsBlobStore>(std::move(blobStore));
|
||||
}
|
||||
return FsBlobStore::migrateIfNeeded(std::move(blobStore), BlockId::FromString(rootBlobId));
|
||||
if (!configFile->config()->HasParentPointers()) {
|
||||
auto result = FsBlobStore::migrate(std::move(blobStore), BlockId::FromString(rootBlobId));
|
||||
// Don't migrate again if it was successful
|
||||
configFile->config()->SetHasParentPointers(true);
|
||||
configFile->save();
|
||||
return result;
|
||||
}
|
||||
return make_unique_ref<FsBlobStore>(std::move(blobStore));
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -106,6 +113,7 @@ unique_ref<BlockStore2> CryDevice::CreateIntegrityEncryptedBlockStore(unique_ref
|
||||
if (!configFile->config()->HasVersionNumbers()) {
|
||||
IntegrityBlockStore2::migrateFromBlockstoreWithoutVersionNumbers(encryptedBlockStore.get(), integrityFilePath, myClientId);
|
||||
configFile->config()->SetBlocksizeBytes(configFile->config()->BlocksizeBytes() + IntegrityBlockStore2::HEADER_LENGTH - blockstore::BlockId::BINARY_LENGTH); // Minus BlockId size because EncryptedBlockStore doesn't store the BlockId anymore (that was moved to IntegrityBlockStore)
|
||||
// Don't migrate again if it was successful
|
||||
configFile->config()->SetHasVersionNumbers(true);
|
||||
configFile->save();
|
||||
}
|
||||
|
@ -2,9 +2,13 @@
|
||||
#include "FileBlob.h"
|
||||
#include "DirBlob.h"
|
||||
#include "SymlinkBlob.h"
|
||||
#include <cryfs/config/CryConfigFile.h>
|
||||
#include <cpp-utils/io/ProgressBar.h>
|
||||
#include <cpp-utils/process/SignalCatcher.h>
|
||||
|
||||
using cpputils::unique_ref;
|
||||
using cpputils::make_unique_ref;
|
||||
using cpputils::SignalCatcher;
|
||||
using blobstore::BlobStore;
|
||||
using blockstore::BlockId;
|
||||
using boost::none;
|
||||
@ -31,35 +35,43 @@ boost::optional<unique_ref<FsBlob>> FsBlobStore::load(const blockstore::BlockId
|
||||
}
|
||||
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
unique_ref<FsBlobStore> FsBlobStore::migrateIfNeeded(unique_ref<BlobStore> blobStore, const blockstore::BlockId &rootBlobId) {
|
||||
unique_ref<FsBlobStore> FsBlobStore::migrate(unique_ref<BlobStore> blobStore, const blockstore::BlockId &rootBlobId) {
|
||||
SignalCatcher signalCatcher;
|
||||
|
||||
auto rootBlob = blobStore->load(rootBlobId);
|
||||
if (rootBlob == none) {
|
||||
throw std::runtime_error("Could not load root blob");
|
||||
}
|
||||
uint16_t format = FsBlobView::getFormatVersionHeader(**rootBlob);
|
||||
|
||||
auto fsBlobStore = make_unique_ref<FsBlobStore>(std::move(blobStore));
|
||||
if (format == 0) {
|
||||
// migration needed
|
||||
std::cout << "Migrating file system for conflict resolution features. Please don't interrupt this process. This can take a while..." << std::flush;
|
||||
fsBlobStore->_migrate(std::move(*rootBlob), blockstore::BlockId::Null());
|
||||
std::cout << "done" << std::endl;
|
||||
}
|
||||
|
||||
uint64_t migratedBlocks = 0;
|
||||
cpputils::ProgressBar progressbar("Migrating file system for conflict resolution features. This can take a while...", fsBlobStore->numBlocks());
|
||||
fsBlobStore->_migrate(std::move(*rootBlob), blockstore::BlockId::Null(), &signalCatcher, [&] (uint32_t numNodes) {
|
||||
migratedBlocks += numNodes;
|
||||
progressbar.update(migratedBlocks);
|
||||
});
|
||||
|
||||
return fsBlobStore;
|
||||
}
|
||||
|
||||
void FsBlobStore::_migrate(unique_ref<blobstore::Blob> node, const blockstore::BlockId &parentId) {
|
||||
void FsBlobStore::_migrate(unique_ref<blobstore::Blob> node, const blockstore::BlockId &parentId, SignalCatcher* signalCatcher, std::function<void(uint32_t numNodes)> perBlobCallback) {
|
||||
FsBlobView::migrate(node.get(), parentId);
|
||||
perBlobCallback(node->numNodes());
|
||||
if (FsBlobView::blobType(*node) == FsBlobView::BlobType::DIR) {
|
||||
DirBlob dir(std::move(node), _getLstatSize());
|
||||
vector<fspp::Dir::Entry> children;
|
||||
dir.AppendChildrenTo(&children);
|
||||
for (const auto &child : children) {
|
||||
if (signalCatcher->signal_occurred()) {
|
||||
// on a SIGINT or SIGTERM, cancel migration but gracefully shutdown, i.e. call destructors.
|
||||
throw std::runtime_error("Caught signal");
|
||||
}
|
||||
auto childEntry = dir.GetChild(child.name);
|
||||
ASSERT(childEntry != none, "Couldn't load child, although it was returned as a child in the list.");
|
||||
auto childBlob = _baseBlobStore->load(childEntry->blockId());
|
||||
ASSERT(childBlob != none, "Couldn't load child blob");
|
||||
_migrate(std::move(*childBlob), dir.blockId());
|
||||
_migrate(std::move(*childBlob), dir.blockId(), signalCatcher, perBlobCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,9 @@
|
||||
#include "FileBlob.h"
|
||||
#include "DirBlob.h"
|
||||
#include "SymlinkBlob.h"
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
#include <cpp-utils/process/SignalCatcher.h>
|
||||
#endif
|
||||
|
||||
namespace cryfs {
|
||||
namespace fsblobstore {
|
||||
@ -29,13 +32,13 @@ namespace cryfs {
|
||||
uint64_t virtualBlocksizeBytes() const;
|
||||
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
static cpputils::unique_ref<FsBlobStore> migrateIfNeeded(cpputils::unique_ref<blobstore::BlobStore> blobStore, const blockstore::BlockId &blockId);
|
||||
static cpputils::unique_ref<FsBlobStore> migrate(cpputils::unique_ref<blobstore::BlobStore> blobStore, const blockstore::BlockId &blockId);
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
void _migrate(cpputils::unique_ref<blobstore::Blob> node, const blockstore::BlockId &parentId);
|
||||
void _migrate(cpputils::unique_ref<blobstore::Blob> node, const blockstore::BlockId &parentId, cpputils::SignalCatcher* signalCatcher, std::function<void(uint32_t numNodes)> perBlobCallback);
|
||||
#endif
|
||||
|
||||
std::function<fspp::num_bytes_t(const blockstore::BlockId &)> _getLstatSize();
|
||||
|
@ -10,7 +10,11 @@ namespace cryfs {
|
||||
void FsBlobView::migrate(blobstore::Blob *blob, const blockstore::BlockId &parentId) {
|
||||
constexpr unsigned int OLD_HEADER_SIZE = sizeof(FORMAT_VERSION_HEADER) + sizeof(uint8_t);
|
||||
|
||||
ASSERT(FsBlobView::getFormatVersionHeader(*blob) == 0, "Block already migrated");
|
||||
if(FsBlobView::getFormatVersionHeader(*blob) != 0) {
|
||||
// blob already migrated
|
||||
return;
|
||||
}
|
||||
|
||||
// Resize blob and move data back
|
||||
cpputils::Data data = blob->readAll();
|
||||
blob->resize(blob->size() + blockstore::BlockId::BINARY_LENGTH);
|
||||
|
@ -85,6 +85,10 @@ namespace cryfs {
|
||||
return _baseBlob->flush();
|
||||
}
|
||||
|
||||
uint32_t numNodes() const override {
|
||||
return _baseBlob->numNodes();
|
||||
}
|
||||
|
||||
cpputils::unique_ref<blobstore::Blob> releaseBaseBlob() {
|
||||
return std::move(_baseBlob);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include <cpp-utils/thread/debugging.h>
|
||||
#include <csignal>
|
||||
#include "InvalidFilesystem.h"
|
||||
#include <codecvt>
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#include <codecvt>
|
||||
@ -291,6 +292,9 @@ void Fuse::_removeAndWarnIfExists(vector<string> *fuseOptions, const std::string
|
||||
}
|
||||
|
||||
void Fuse::_run(const bf::path &mountdir, const vector<string> &fuseOptions) {
|
||||
// Avoid encoding errors for non-utf8 characters, see https://github.com/cryfs/cryfs/issues/247
|
||||
bf::path::imbue(std::locale(std::locale(), new std::codecvt_utf8_utf16<wchar_t>()));
|
||||
|
||||
_mountdir = mountdir;
|
||||
|
||||
ASSERT(_argv.size() == 0, "Filesystem already started");
|
||||
|
@ -164,6 +164,18 @@ int main(int argc, char* argv[]) {
|
||||
// TODO Show more info about error
|
||||
throw std::runtime_error("Error loading config file.");
|
||||
}
|
||||
const auto& config_ = config->configFile.config();
|
||||
std::cout << "Loading filesystem of version " << config_->Version() << std::endl;
|
||||
#ifndef CRYFS_NO_COMPATIBILITY
|
||||
const bool is_correct_format = config_->Version() == CryConfig::FilesystemFormatVersion && !config_->HasParentPointers() && !config_->HasVersionNumbers();
|
||||
#else
|
||||
const bool is_correct_format = config_->Version() == CryConfig::FilesystemFormatVersion;
|
||||
#endif
|
||||
if (!is_correct_format) {
|
||||
// TODO At this point, the cryfs.config file was already switched to 0.10 format. We should probably not do that.
|
||||
std::cerr << "The filesystem is not in the 0.10 format. It needs to be migrated. The cryfs-stats tool unfortunately can't handle this, please mount and unmount the filesystem once." << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
cout << "Listing all blocks..." << flush;
|
||||
set<BlockId> unaccountedBlocks = _getAllBlockIds(basedir, *config, localStateDir);
|
||||
|
@ -21,50 +21,68 @@ INSTANTIATE_TEST_CASE_P(EmptyLastLeaf, DataTreeTest_NumStoredBytes_P, Values(0u)
|
||||
INSTANTIATE_TEST_CASE_P(HalfFullLastLeaf, DataTreeTest_NumStoredBytes_P, Values(5u, 10u));
|
||||
INSTANTIATE_TEST_CASE_P(FullLastLeaf, DataTreeTest_NumStoredBytes_P, Values(static_cast<uint32_t>(DataNodeLayout(DataTreeTest_NumStoredBytes::BLOCKSIZE_BYTES).maxBytesPerLeaf())));
|
||||
|
||||
//TODO Test numLeaves() and numNodes() also two configurations with same number of bytes but different number of leaves (last leaf has 0 bytes)
|
||||
|
||||
TEST_P(DataTreeTest_NumStoredBytes_P, SingleLeaf) {
|
||||
BlockId blockId = CreateLeafWithSize(GetParam())->blockId();
|
||||
auto tree = treeStore.load(blockId).value();
|
||||
EXPECT_EQ(GetParam(), tree->numBytes());
|
||||
EXPECT_EQ(1, tree->numLeaves());
|
||||
EXPECT_EQ(1, tree->numNodes());
|
||||
}
|
||||
|
||||
TEST_P(DataTreeTest_NumStoredBytes_P, TwoLeafTree) {
|
||||
BlockId blockId = CreateTwoLeafWithSecondLeafSize(GetParam())->blockId();
|
||||
auto tree = treeStore.load(blockId).value();
|
||||
EXPECT_EQ(nodeStore->layout().maxBytesPerLeaf() + GetParam(), tree->numBytes());
|
||||
EXPECT_EQ(2, tree->numLeaves());
|
||||
EXPECT_EQ(3, tree->numNodes());
|
||||
}
|
||||
|
||||
TEST_P(DataTreeTest_NumStoredBytes_P, FullTwolevelTree) {
|
||||
BlockId blockId = CreateFullTwoLevelWithLastLeafSize(GetParam())->blockId();
|
||||
auto tree = treeStore.load(blockId).value();
|
||||
EXPECT_EQ(nodeStore->layout().maxBytesPerLeaf()*(nodeStore->layout().maxChildrenPerInnerNode()-1) + GetParam(), tree->numBytes());
|
||||
EXPECT_EQ(nodeStore->layout().maxChildrenPerInnerNode(), tree->numLeaves());
|
||||
EXPECT_EQ(1 + nodeStore->layout().maxChildrenPerInnerNode(), tree->numNodes());
|
||||
}
|
||||
|
||||
TEST_P(DataTreeTest_NumStoredBytes_P, ThreeLevelTreeWithOneChild) {
|
||||
BlockId blockId = CreateThreeLevelWithOneChildAndLastLeafSize(GetParam())->blockId();
|
||||
auto tree = treeStore.load(blockId).value();
|
||||
EXPECT_EQ(nodeStore->layout().maxBytesPerLeaf() + GetParam(), tree->numBytes());
|
||||
EXPECT_EQ(2, tree->numLeaves());
|
||||
EXPECT_EQ(4, tree->numNodes());
|
||||
}
|
||||
|
||||
TEST_P(DataTreeTest_NumStoredBytes_P, ThreeLevelTreeWithTwoChildren) {
|
||||
BlockId blockId = CreateThreeLevelWithTwoChildrenAndLastLeafSize(GetParam())->blockId();
|
||||
auto tree = treeStore.load(blockId).value();
|
||||
EXPECT_EQ(nodeStore->layout().maxBytesPerLeaf()*nodeStore->layout().maxChildrenPerInnerNode() + nodeStore->layout().maxBytesPerLeaf() + GetParam(), tree->numBytes());
|
||||
EXPECT_EQ(2 + nodeStore->layout().maxChildrenPerInnerNode(), tree->numLeaves());
|
||||
EXPECT_EQ(5 + nodeStore->layout().maxChildrenPerInnerNode(), tree->numNodes());
|
||||
}
|
||||
|
||||
TEST_P(DataTreeTest_NumStoredBytes_P, ThreeLevelTreeWithThreeChildren) {
|
||||
BlockId blockId = CreateThreeLevelWithThreeChildrenAndLastLeafSize(GetParam())->blockId();
|
||||
auto tree = treeStore.load(blockId).value();
|
||||
EXPECT_EQ(2*nodeStore->layout().maxBytesPerLeaf()*nodeStore->layout().maxChildrenPerInnerNode() + nodeStore->layout().maxBytesPerLeaf() + GetParam(), tree->numBytes());
|
||||
EXPECT_EQ(2 + 2*nodeStore->layout().maxChildrenPerInnerNode(), tree->numLeaves());
|
||||
EXPECT_EQ(6 + 2*nodeStore->layout().maxChildrenPerInnerNode(), tree->numNodes());
|
||||
}
|
||||
|
||||
TEST_P(DataTreeTest_NumStoredBytes_P, FullThreeLevelTree) {
|
||||
BlockId blockId = CreateFullThreeLevelWithLastLeafSize(GetParam())->blockId();
|
||||
auto tree = treeStore.load(blockId).value();
|
||||
EXPECT_EQ(nodeStore->layout().maxBytesPerLeaf()*nodeStore->layout().maxChildrenPerInnerNode()*(nodeStore->layout().maxChildrenPerInnerNode()-1) + nodeStore->layout().maxBytesPerLeaf()*(nodeStore->layout().maxChildrenPerInnerNode()-1) + GetParam(), tree->numBytes());
|
||||
EXPECT_EQ(nodeStore->layout().maxChildrenPerInnerNode()*nodeStore->layout().maxChildrenPerInnerNode(), tree->numLeaves());
|
||||
EXPECT_EQ(1 + nodeStore->layout().maxChildrenPerInnerNode() + nodeStore->layout().maxChildrenPerInnerNode()*nodeStore->layout().maxChildrenPerInnerNode(), tree->numNodes());
|
||||
}
|
||||
|
||||
TEST_P(DataTreeTest_NumStoredBytes_P, FourLevelMinDataTree) {
|
||||
BlockId blockId = CreateFourLevelMinDataWithLastLeafSize(GetParam())->blockId();
|
||||
auto tree = treeStore.load(blockId).value();
|
||||
EXPECT_EQ(nodeStore->layout().maxBytesPerLeaf()*nodeStore->layout().maxChildrenPerInnerNode()*nodeStore->layout().maxChildrenPerInnerNode() + GetParam(), tree->numBytes());
|
||||
EXPECT_EQ(1 + nodeStore->layout().maxChildrenPerInnerNode()*nodeStore->layout().maxChildrenPerInnerNode(), tree->numLeaves());
|
||||
EXPECT_EQ(5 + nodeStore->layout().maxChildrenPerInnerNode() + nodeStore->layout().maxChildrenPerInnerNode()*nodeStore->layout().maxChildrenPerInnerNode(), tree->numNodes());
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ set(SOURCES
|
||||
process/daemonize_include_test.cpp
|
||||
process/subprocess_include_test.cpp
|
||||
process/SubprocessTest.cpp
|
||||
process/SignalCatcherTest.cpp
|
||||
tempfile/TempFileTest.cpp
|
||||
tempfile/TempFileIncludeTest.cpp
|
||||
tempfile/TempDirIncludeTest.cpp
|
||||
@ -28,6 +29,7 @@ set(SOURCES
|
||||
io/ConsoleTest_Print.cpp
|
||||
io/ConsoleTest_Ask.cpp
|
||||
io/ConsoleTest_AskPassword.cpp
|
||||
io/ProgressBarTest.cpp
|
||||
random/RandomIncludeTest.cpp
|
||||
lock/LockPoolIncludeTest.cpp
|
||||
lock/ConditionBarrierIncludeTest.cpp
|
||||
@ -55,6 +57,7 @@ set(SOURCES
|
||||
system/HomedirTest.cpp
|
||||
system/EnvTest.cpp
|
||||
thread/debugging_test.cpp
|
||||
thread/LeftRightTest.cpp
|
||||
value_type/ValueTypeTest.cpp
|
||||
either_test.cpp
|
||||
)
|
||||
|
66
test/cpp-utils/io/ProgressBarTest.cpp
Normal file
66
test/cpp-utils/io/ProgressBarTest.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
#include <cpp-utils/io/ProgressBar.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
using cpputils::ProgressBar;
|
||||
using std::make_shared;
|
||||
|
||||
class MockConsole final: public cpputils::Console {
|
||||
public:
|
||||
void EXPECT_OUTPUT(const char* expected) {
|
||||
EXPECT_EQ(expected, _output);
|
||||
_output = "";
|
||||
}
|
||||
|
||||
void print(const std::string& text) override {
|
||||
_output += text;
|
||||
}
|
||||
|
||||
unsigned int ask(const std::string&, const std::vector<std::string>&) override {
|
||||
EXPECT_TRUE(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool askYesNo(const std::string&, bool) override {
|
||||
EXPECT_TRUE(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string askPassword(const std::string&) override {
|
||||
EXPECT_TRUE(false);
|
||||
return "";
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _output;
|
||||
};
|
||||
|
||||
TEST(ProgressBarTest, testProgressBar) {
|
||||
auto console = make_shared<MockConsole>();
|
||||
|
||||
ProgressBar bar(console, "Preamble", 2000);
|
||||
console->EXPECT_OUTPUT("\n\rPreamble 0%");
|
||||
|
||||
// when updating to 0, doesn't reprint
|
||||
bar.update(0);
|
||||
console->EXPECT_OUTPUT("");
|
||||
|
||||
// update to half
|
||||
bar.update(1000);
|
||||
console->EXPECT_OUTPUT("\rPreamble 50%");
|
||||
|
||||
// when updating to same value, doesn't reprint
|
||||
bar.update(1000);
|
||||
console->EXPECT_OUTPUT("");
|
||||
|
||||
// when updating to value with same percentage, doesn't reprint
|
||||
bar.update(1001);
|
||||
console->EXPECT_OUTPUT("");
|
||||
|
||||
// update to 0
|
||||
bar.update(0);
|
||||
console->EXPECT_OUTPUT("\rPreamble 0%");
|
||||
|
||||
// update to full
|
||||
bar.update(2000);
|
||||
console->EXPECT_OUTPUT("\rPreamble 100%");
|
||||
}
|
193
test/cpp-utils/process/SignalCatcherTest.cpp
Normal file
193
test/cpp-utils/process/SignalCatcherTest.cpp
Normal file
@ -0,0 +1,193 @@
|
||||
#include <cpp-utils/process/SignalCatcher.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <csignal>
|
||||
|
||||
using cpputils::SignalCatcher;
|
||||
|
||||
namespace {
|
||||
void raise_signal(int signal) {
|
||||
int error = ::raise(signal);
|
||||
if (error != 0) {
|
||||
throw std::runtime_error("Error raising signal");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenNoSignalCatcher_whenRaisingSigint_thenDies) {
|
||||
EXPECT_DEATH(
|
||||
raise_signal(SIGINT),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenNoSignalCatcher_whenRaisingSigterm_thenDies) {
|
||||
EXPECT_DEATH(
|
||||
raise_signal(SIGTERM),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigIntCatcher_whenRaisingSigInt_thenCatches) {
|
||||
SignalCatcher catcher({SIGINT});
|
||||
|
||||
EXPECT_FALSE(catcher.signal_occurred());
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
|
||||
// raise again
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigTermCatcher_whenRaisingSigTerm_thenCatches) {
|
||||
SignalCatcher catcher({SIGTERM});
|
||||
|
||||
EXPECT_FALSE(catcher.signal_occurred());
|
||||
raise_signal(SIGTERM);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
|
||||
// raise again
|
||||
raise_signal(SIGTERM);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigIntAndSigTermCatcher_whenRaisingSigInt_thenCatches) {
|
||||
SignalCatcher catcher({SIGINT, SIGTERM});
|
||||
|
||||
EXPECT_FALSE(catcher.signal_occurred());
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
|
||||
// raise again
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigIntAndSigTermCatcher_whenRaisingSigTerm_thenCatches) {
|
||||
SignalCatcher catcher({SIGINT, SIGTERM});
|
||||
|
||||
EXPECT_FALSE(catcher.signal_occurred());
|
||||
raise_signal(SIGTERM);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
|
||||
// raise again
|
||||
raise_signal(SIGTERM);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigIntAndSigTermCatcher_whenRaisingSigIntAndSigTerm_thenCatches) {
|
||||
SignalCatcher catcher({SIGINT, SIGTERM});
|
||||
|
||||
EXPECT_FALSE(catcher.signal_occurred());
|
||||
raise_signal(SIGTERM);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(catcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigIntCatcherAndSigTermCatcher_whenRaisingSignalsInOrder_thenCorrectCatcherCatches) {
|
||||
SignalCatcher sigintCatcher({SIGINT});
|
||||
SignalCatcher sigtermCatcher({SIGTERM});
|
||||
|
||||
EXPECT_FALSE(sigintCatcher.signal_occurred());
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(sigintCatcher.signal_occurred());
|
||||
|
||||
EXPECT_FALSE(sigtermCatcher.signal_occurred());
|
||||
raise_signal(SIGTERM);
|
||||
EXPECT_TRUE(sigtermCatcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigIntCatcherAndSigTermCatcher_whenRaisingSignalsInReverseOrder_thenCorrectCatcherCatches) {
|
||||
SignalCatcher sigintCatcher({SIGINT});
|
||||
SignalCatcher sigtermCatcher({SIGTERM});
|
||||
|
||||
EXPECT_FALSE(sigtermCatcher.signal_occurred());
|
||||
raise_signal(SIGTERM);
|
||||
EXPECT_TRUE(sigtermCatcher.signal_occurred());
|
||||
|
||||
EXPECT_FALSE(sigintCatcher.signal_occurred());
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(sigintCatcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenNestedSigIntCatchers_whenRaisingSignals_thenCorrectCatcherCatches) {
|
||||
SignalCatcher outerCatcher({SIGINT});
|
||||
{
|
||||
SignalCatcher middleCatcher({SIGINT});
|
||||
|
||||
EXPECT_FALSE(middleCatcher.signal_occurred());
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(middleCatcher.signal_occurred());
|
||||
|
||||
{
|
||||
SignalCatcher innerCatcher({SIGINT});
|
||||
|
||||
EXPECT_FALSE(innerCatcher.signal_occurred());
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(innerCatcher.signal_occurred());
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_FALSE(outerCatcher.signal_occurred());
|
||||
raise_signal(SIGINT);
|
||||
EXPECT_TRUE(outerCatcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenExpiredSigIntCatcher_whenRaisingSigInt_thenDies) {
|
||||
{
|
||||
SignalCatcher catcher({SIGINT});
|
||||
}
|
||||
|
||||
EXPECT_DEATH(
|
||||
raise_signal(SIGINT),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenExpiredSigTermCatcher_whenRaisingSigTerm_thenDies) {
|
||||
{
|
||||
SignalCatcher catcher({SIGTERM});
|
||||
}
|
||||
|
||||
EXPECT_DEATH(
|
||||
raise_signal(SIGTERM),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenExpiredSigIntCatcherAndSigTermCatcher_whenRaisingSigTerm_thenDies) {
|
||||
{
|
||||
SignalCatcher sigIntCatcher({SIGTERM});
|
||||
SignalCatcher sigTermCatcer({SIGTERM});
|
||||
}
|
||||
|
||||
EXPECT_DEATH(
|
||||
raise_signal(SIGTERM),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigTermCatcherAndExpiredSigIntCatcher_whenRaisingSigTerm_thenCatches) {
|
||||
SignalCatcher sigTermCatcher({SIGTERM});
|
||||
{
|
||||
SignalCatcher sigIntCatcher({SIGINT});
|
||||
}
|
||||
|
||||
EXPECT_FALSE(sigTermCatcher.signal_occurred());
|
||||
raise_signal(SIGTERM);
|
||||
EXPECT_TRUE(sigTermCatcher.signal_occurred());
|
||||
}
|
||||
|
||||
TEST(SignalCatcherTest, givenSigTermCatcherAndExpiredSigIntCatcher_whenRaisingSigInt_thenDies) {
|
||||
SignalCatcher sigTermCacher({SIGTERM});
|
||||
{
|
||||
SignalCatcher sigIntCatcher({SIGINT});
|
||||
}
|
||||
|
||||
EXPECT_DEATH(
|
||||
raise_signal(SIGINT),
|
||||
""
|
||||
);
|
||||
}
|
204
test/cpp-utils/thread/LeftRightTest.cpp
Normal file
204
test/cpp-utils/thread/LeftRightTest.cpp
Normal file
@ -0,0 +1,204 @@
|
||||
#include <cpp-utils/thread/LeftRight.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <vector>
|
||||
|
||||
using cpputils::LeftRight;
|
||||
using std::vector;
|
||||
|
||||
TEST(LeftRightTest, givenInt_whenWritingAndReading_thenChangesArePresent) {
|
||||
LeftRight<int> obj;
|
||||
|
||||
obj.write([] (auto& obj) {obj = 5;});
|
||||
int read = obj.read([] (auto& obj) {return obj;});
|
||||
EXPECT_EQ(5, read);
|
||||
|
||||
// check changes are also present in background copy
|
||||
obj.write([] (auto&) {}); // this switches to the background copy
|
||||
read = obj.read([] (auto& obj) {return obj;});
|
||||
EXPECT_EQ(5, read);
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, givenVector_whenWritingAndReading_thenChangesArePresent) {
|
||||
LeftRight<vector<int>> obj;
|
||||
|
||||
obj.write([] (auto& obj) {obj.push_back(5);});
|
||||
vector<int> read = obj.read([] (auto& obj) {return obj;});
|
||||
EXPECT_EQ((vector<int>{5}), read);
|
||||
|
||||
obj.write([] (auto& obj) {obj.push_back(6);});
|
||||
read = obj.read([] (auto& obj) {return obj;});
|
||||
EXPECT_EQ((vector<int>{5, 6}), read);
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, readsCanBeConcurrent) {
|
||||
LeftRight<int> obj;
|
||||
std::atomic<int> num_running_readers{0};
|
||||
|
||||
std::thread reader1([&] () {
|
||||
obj.read([&] (auto&) {
|
||||
++num_running_readers;
|
||||
while(num_running_readers.load() < 2) {}
|
||||
});
|
||||
});
|
||||
|
||||
std::thread reader2([&] () {
|
||||
obj.read([&] (auto&) {
|
||||
++num_running_readers;
|
||||
while(num_running_readers.load() < 2) {}
|
||||
});
|
||||
});
|
||||
|
||||
// the threads only finish after both entered the read function.
|
||||
// if LeftRight didn't allow concurrency, this would cause a deadlock.
|
||||
reader1.join();
|
||||
reader2.join();
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, writesCanBeConcurrentWithReads_readThenWrite) {
|
||||
LeftRight<int> obj;
|
||||
std::atomic<bool> reader_running{false};
|
||||
std::atomic<bool> writer_running{false};
|
||||
|
||||
std::thread reader([&] () {
|
||||
obj.read([&] (auto&) {
|
||||
reader_running = true;
|
||||
while(!writer_running.load()) {}
|
||||
});
|
||||
});
|
||||
|
||||
std::thread writer([&] () {
|
||||
// run read first, write second
|
||||
while (!reader_running.load()) {}
|
||||
|
||||
obj.write([&] (auto&) {
|
||||
writer_running = true;
|
||||
});
|
||||
});
|
||||
|
||||
// the threads only finish after both entered the read function.
|
||||
// if LeftRight didn't allow concurrency, this would cause a deadlock.
|
||||
reader.join();
|
||||
writer.join();
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, writesCanBeConcurrentWithReads_writeThenRead) {
|
||||
LeftRight<int> obj;
|
||||
std::atomic<bool> writer_running{false};
|
||||
std::atomic<bool> reader_running{false};
|
||||
|
||||
std::thread writer([&] () {
|
||||
obj.read([&] (auto&) {
|
||||
writer_running = true;
|
||||
while(!reader_running.load()) {}
|
||||
});
|
||||
});
|
||||
|
||||
std::thread reader([&] () {
|
||||
// run write first, read second
|
||||
while (!writer_running.load()) {}
|
||||
|
||||
obj.read([&] (auto&) {
|
||||
reader_running = true;
|
||||
});
|
||||
});
|
||||
|
||||
// the threads only finish after both entered the read function.
|
||||
// if LeftRight didn't allow concurrency, this would cause a deadlock.
|
||||
writer.join();
|
||||
reader.join();
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, writesCannotBeConcurrentWithWrites) {
|
||||
LeftRight<int> obj;
|
||||
std::atomic<bool> first_writer_started{false};
|
||||
std::atomic<bool> first_writer_finished{false};
|
||||
|
||||
std::thread writer1([&] () {
|
||||
obj.write([&] (auto&) {
|
||||
first_writer_started = true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
first_writer_finished = true;
|
||||
});
|
||||
});
|
||||
|
||||
std::thread writer2([&] () {
|
||||
// make sure the other writer runs first
|
||||
while (!first_writer_started.load()) {}
|
||||
|
||||
obj.write([&] (auto&) {
|
||||
// expect the other writer finished before this one starts
|
||||
EXPECT_TRUE(first_writer_finished.load());
|
||||
});
|
||||
});
|
||||
|
||||
writer1.join();
|
||||
writer2.join();
|
||||
}
|
||||
|
||||
namespace {
|
||||
class MyException : std::exception {};
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, whenReadThrowsException_thenThrowsThrough) {
|
||||
LeftRight<int> obj;
|
||||
|
||||
EXPECT_THROW(
|
||||
obj.read([](auto&) {throw MyException();}),
|
||||
MyException
|
||||
);
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, whenWriteThrowsException_thenThrowsThrough) {
|
||||
LeftRight<int> obj;
|
||||
|
||||
EXPECT_THROW(
|
||||
obj.write([](auto&) {throw MyException();}),
|
||||
MyException
|
||||
);
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, givenInt_whenWriteThrowsException_thenResetsToOldState) {
|
||||
LeftRight<int> obj;
|
||||
|
||||
obj.write([](auto& obj) {obj = 5;});
|
||||
|
||||
EXPECT_THROW(
|
||||
obj.write([](auto& obj) {
|
||||
obj = 6;
|
||||
throw MyException();
|
||||
}),
|
||||
MyException
|
||||
);
|
||||
|
||||
// check reading it returns old value
|
||||
int read = obj.read([] (auto& obj) {return obj;});
|
||||
EXPECT_EQ(5, read);
|
||||
|
||||
// check changes are also present in background copy
|
||||
obj.write([] (auto&) {}); // this switches to the background copy
|
||||
read = obj.read([] (auto& obj) {return obj;});
|
||||
EXPECT_EQ(5, read);
|
||||
}
|
||||
|
||||
TEST(LeftRightTest, givenVector_whenWriteThrowsException_thenResetsToOldState) {
|
||||
LeftRight<vector<int>> obj;
|
||||
|
||||
obj.write([](auto& obj) {obj.push_back(5);});
|
||||
|
||||
EXPECT_THROW(
|
||||
obj.write([](auto& obj) {
|
||||
obj.push_back(6);
|
||||
throw MyException();
|
||||
}),
|
||||
MyException
|
||||
);
|
||||
|
||||
// check reading it returns old value
|
||||
vector<int> read = obj.read([] (auto& obj) {return obj;});
|
||||
EXPECT_EQ((vector<int>{5}), read);
|
||||
|
||||
// check changes are also present in background copy
|
||||
obj.write([] (auto&) {}); // this switches to the background copy
|
||||
read = obj.read([] (auto& obj) {return obj;});
|
||||
EXPECT_EQ((vector<int>{5}), read);
|
||||
}
|
Loading…
Reference in New Issue
Block a user