mirror of
https://github.com/EvernodeXRPL/hpcore.git
synced 2026-04-29 15:37:59 +00:00
Contract state monitoring and rollback infrastructure. (#61)
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
**/**
|
||||
!build/hpcore
|
||||
!build/hpcore
|
||||
!build/hpstatemon
|
||||
!libfuse3.so.3
|
||||
@@ -40,10 +40,19 @@ target_link_libraries(hpsupport
|
||||
${CMAKE_DL_LIBS}
|
||||
)
|
||||
|
||||
add_library(hpstatefs
|
||||
src/statefs/hasher.cpp
|
||||
src/statefs/state_common.cpp
|
||||
src/statefs/hashmap_builder.cpp
|
||||
src/statefs/hashtree_builder.cpp
|
||||
src/statefs/state_restore.cpp
|
||||
)
|
||||
target_link_libraries(hpstatefs hpsupport)
|
||||
|
||||
add_library(hpproc
|
||||
src/proc/proc.cpp
|
||||
)
|
||||
target_link_libraries(hpproc hpsupport)
|
||||
target_link_libraries(hpproc hpsupport hpstatefs)
|
||||
|
||||
add_library(hpbill
|
||||
src/bill/corebill.cpp
|
||||
@@ -82,7 +91,6 @@ target_link_libraries(hpusr hpsupport hpsock hpschema)
|
||||
add_library(hpcons
|
||||
src/cons/cons.cpp
|
||||
src/cons/ledger_handler.cpp
|
||||
src/cons/statemap_handler.cpp
|
||||
)
|
||||
target_link_libraries(hpcons hpsupport hpproc hpp2p hpusr)
|
||||
|
||||
@@ -100,6 +108,21 @@ target_link_libraries(hpcore
|
||||
hpcons
|
||||
)
|
||||
|
||||
add_executable(hpstatemon
|
||||
src/statefs/state_monitor/fusefs.cpp
|
||||
src/statefs/state_monitor/state_monitor.cpp
|
||||
src/statefs/hasher.cpp
|
||||
src/statefs/state_common.cpp
|
||||
)
|
||||
target_link_libraries(hpstatemon
|
||||
hpsupport
|
||||
libfuse3.so.3
|
||||
)
|
||||
|
||||
add_dependencies(hpcore
|
||||
hpstatemon
|
||||
)
|
||||
|
||||
target_precompile_headers(hpsupport PUBLIC src/pchheader.hpp)
|
||||
target_precompile_headers(hpcore REUSE_FROM hpsupport)
|
||||
target_precompile_headers(hpsock REUSE_FROM hpsupport)
|
||||
@@ -107,6 +130,7 @@ target_precompile_headers(hpschema REUSE_FROM hpsupport)
|
||||
target_precompile_headers(hpp2p REUSE_FROM hpsupport)
|
||||
target_precompile_headers(hpusr REUSE_FROM hpsupport)
|
||||
target_precompile_headers(hpcons REUSE_FROM hpsupport)
|
||||
target_precompile_headers(hpstatemon REUSE_FROM hpsupport)
|
||||
|
||||
# Create docker image from hpcore build output with 'make docker'
|
||||
# Requires docker to be runnable without 'sudo'
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
# Otherwise, hpcore itself can run on any docker image like ubuntu or debian without NodeJs.
|
||||
FROM node:10.17.0-buster-slim
|
||||
|
||||
# Copy fuse shared library and register it.
|
||||
COPY ./libfuse3.so.3 /usr/local/lib/
|
||||
RUN ldconfig
|
||||
# Install fuse.
|
||||
RUN apt-get update && apt-get install -y fuse && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# hpcore binary is copied to /hp directory withtin the docker image.
|
||||
WORKDIR /hp
|
||||
COPY ./build/hpcore .
|
||||
COPY ./build/hpstatemon .
|
||||
ENTRYPOINT ["/hp/hpcore"]
|
||||
12
README.md
12
README.md
@@ -13,6 +13,7 @@ A C++ version of hotpocket designed for production envrionments, original protot
|
||||
* RapidJSON - http://rapidjson.org
|
||||
* P2P Protocol - https://google.github.io/flatbuffers/
|
||||
* TLS - https://www.openssl.org/
|
||||
* Fuse filesystem - https://github.com/libfuse/libfuse
|
||||
|
||||
## Steps to setup Hot Pocket
|
||||
|
||||
@@ -68,6 +69,13 @@ Example: When you make a change to `p2pmsg_content_.fbc` defnition file, you nee
|
||||
3. Run `./config && make`
|
||||
4. Run `sudo make install`
|
||||
|
||||
#### Install libfuse
|
||||
1. `sudo apt-get install -y meson ninja-build pkg-config`
|
||||
2. Download [libfuse 3.8](https://github.com/libfuse/libfuse/releases/download/fuse-3.8.0/fuse-3.8.0.tar.xz) and extract.
|
||||
3. `mkdir build; cd build`
|
||||
4. `meson .. && ninja`
|
||||
6. `sudo ninja install`
|
||||
|
||||
#### Run ldconfig
|
||||
`sudo ldconfig`
|
||||
|
||||
@@ -98,4 +106,6 @@ Code is divided into subsystems via namespaces.
|
||||
|
||||
**sock::** Handles generic web sockets functionality. Mainly acts as a wrapper for boost/beast.
|
||||
|
||||
**util::** Contains shared data structures/helper functions used by multiple subsystems.
|
||||
**util::** Contains shared data structures/helper functions used by multiple subsystems.
|
||||
|
||||
**statefs::** Fuse-based state filesystem monitoring and contract state maintenence subsystem.
|
||||
@@ -50,8 +50,9 @@ do
|
||||
binargs: './bin/contract.js', \
|
||||
peerport: ${peerport}, \
|
||||
pubport: ${pubport}, \
|
||||
roundtime: 1000,
|
||||
loglevel: 'debug'
|
||||
roundtime: 1000, \
|
||||
loglevel: 'debug', \
|
||||
loggers:['console'] \
|
||||
}, null, 2)" > hp.cfg
|
||||
rm tmp.json
|
||||
|
||||
|
||||
@@ -23,5 +23,6 @@ let pubport=8080+$n
|
||||
# We specify --name for each node so it will be the virtual dns name for each node.
|
||||
docker run --rm -t -i --network=hpnet --name=node${n} \
|
||||
-p ${pubport}:${pubport} \
|
||||
--device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined \
|
||||
--mount type=bind,source=${clusterloc}/node${n},target=/contract \
|
||||
hpcore:latest run /contract
|
||||
@@ -6,11 +6,10 @@ const fs = require('fs')
|
||||
//console.log("===Sample contract started===");
|
||||
//console.log("Contract args received from hp: " + input);
|
||||
|
||||
let hpargsstr = fs.readFileSync(0, 'utf8');
|
||||
let hpargs = JSON.parse(hpargsstr);
|
||||
let hpargs = JSON.parse( fs.readFileSync(0, 'utf8'));
|
||||
|
||||
// We just save execution args as an example state file change.
|
||||
fs.appendFileSync("state/execargs.txt", hpargsstr + "\n");
|
||||
fs.appendFileSync("state/exects.txt", "ts:" + hpargs.ts + "\n");
|
||||
|
||||
Object.keys(hpargs.usrfd).forEach(function (key, index) {
|
||||
let userfds = hpargs.usrfd[key];
|
||||
|
||||
BIN
libfuse3.so.3
Executable file
BIN
libfuse3.so.3
Executable file
Binary file not shown.
31
src/conf.cpp
31
src/conf.cpp
@@ -17,15 +17,6 @@ contract_config cfg;
|
||||
const static char *MODE_PASSIVE = "passive";
|
||||
const static char *MODE_ACTIVE = "active";
|
||||
|
||||
// provide a safe std::string overload for realpath
|
||||
std::string realpath(std::string path)
|
||||
{
|
||||
std::array<char, PATH_MAX> buffer;
|
||||
::realpath(path.c_str(), buffer.data());
|
||||
buffer[PATH_MAX] = '\0';
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and initializes the contract config for execution. Must be called once during application startup.
|
||||
* @return 0 for success. -1 for failure.
|
||||
@@ -91,7 +82,7 @@ int create_contract()
|
||||
boost::filesystem::create_directories(ctx.configdir);
|
||||
boost::filesystem::create_directories(ctx.histdir);
|
||||
boost::filesystem::create_directories(ctx.statedir);
|
||||
boost::filesystem::create_directories(ctx.statemapdir);
|
||||
boost::filesystem::create_directories(ctx.statehistdir);
|
||||
|
||||
//Create config file with default settings.
|
||||
|
||||
@@ -128,9 +119,16 @@ int create_contract()
|
||||
* Updates the contract context with directory paths based on provided base directory.
|
||||
* This is called after parsing HP command line arg in order to populate the ctx.
|
||||
*/
|
||||
void set_contract_dir_paths(std::string basedir)
|
||||
void set_contract_dir_paths(std::string exepath, std::string basedir)
|
||||
{
|
||||
if (basedir == "")
|
||||
if (exepath.empty())
|
||||
{
|
||||
// this code branch will never execute the way main is currently coded, but it might change in future
|
||||
std::cerr << "Executable path must be specified\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (basedir.empty())
|
||||
{
|
||||
// this code branch will never execute the way main is currently coded, but it might change in future
|
||||
std::cerr << "a contract directory must be specified\n";
|
||||
@@ -138,7 +136,10 @@ void set_contract_dir_paths(std::string basedir)
|
||||
}
|
||||
|
||||
// resolving the path through realpath will remove any trailing slash if present
|
||||
basedir = realpath(basedir);
|
||||
basedir = util::realpath(basedir);
|
||||
|
||||
ctx.exedir = boost::filesystem::path(util::realpath(exepath)).parent_path().string();
|
||||
ctx.statemonexepath = ctx.exedir + "/" + "hpstatemon";
|
||||
|
||||
ctx.contractdir = basedir;
|
||||
ctx.configdir = basedir + "/cfg";
|
||||
@@ -147,7 +148,7 @@ void set_contract_dir_paths(std::string basedir)
|
||||
ctx.tlscertfile = ctx.configdir + "/tlscert.pem";
|
||||
ctx.histdir = basedir + "/hist";
|
||||
ctx.statedir = basedir + "/state";
|
||||
ctx.statemapdir = basedir + "/statemap";
|
||||
ctx.statehistdir = basedir + "/statehist";
|
||||
ctx.logdir = basedir + "/log";
|
||||
}
|
||||
|
||||
@@ -503,7 +504,7 @@ int validate_contract_dir_paths()
|
||||
ctx.configfile,
|
||||
ctx.histdir,
|
||||
ctx.statedir,
|
||||
ctx.statemapdir,
|
||||
ctx.statehistdir,
|
||||
ctx.tlskeyfile,
|
||||
ctx.tlscertfile};
|
||||
|
||||
|
||||
@@ -24,11 +24,13 @@ enum OPERATING_MODE
|
||||
struct contract_ctx
|
||||
{
|
||||
std::string command; // The CLI command issued to launch HotPocket
|
||||
std::string exedir; // Hot Pocket executable dir.
|
||||
std::string statemonexepath;// State monitor executable file path.
|
||||
|
||||
std::string contractdir; // Contract base directory full path
|
||||
std::string histdir; // Contract history dir full path
|
||||
std::string statedir; // Contract state dir full path
|
||||
std::string statemapdir; // Contract state map dir (.merkel files) full path
|
||||
std::string statedir; // Contract executing state dir full path (This is the fuse mount point)
|
||||
std::string statehistdir; // Contract state history dir full path
|
||||
std::string logdir; // Contract log dir full path
|
||||
std::string configdir; // Contract config dir full path
|
||||
std::string configfile; // Full path to the contract config file
|
||||
@@ -90,7 +92,7 @@ int rekey();
|
||||
|
||||
int create_contract();
|
||||
|
||||
void set_contract_dir_paths(std::string basedir);
|
||||
void set_contract_dir_paths(std::string exepath, std::string basedir);
|
||||
|
||||
//------Internal-use functions for this namespace.
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "../crypto.hpp"
|
||||
#include "../proc/proc.hpp"
|
||||
#include "ledger_handler.hpp"
|
||||
#include "statemap_handler.hpp"
|
||||
#include "cons.hpp"
|
||||
|
||||
namespace p2pmsg = fbschema::p2pmsg;
|
||||
@@ -590,7 +589,6 @@ void apply_ledger(const p2p::proposal &cons_prop)
|
||||
|
||||
extract_user_outputs_from_contract_bufmap(useriobufmap);
|
||||
broadcast_npl_output(nplbufpair.output);
|
||||
update_state_blockmap(updated_blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
#include "../pchheader.hpp"
|
||||
#include "../conf.hpp"
|
||||
#include "../hplog.hpp"
|
||||
#include "../proc/proc.hpp"
|
||||
|
||||
namespace cons
|
||||
{
|
||||
|
||||
constexpr size_t BLOCK_SIZE = 4 * 1024 * 1024; //this is the logical block size files are decomposed in, it's also the size of the merkle tree
|
||||
constexpr size_t HASH_SIZE = crypto_generichash_blake2b_BYTES;
|
||||
constexpr size_t MAX_HASHES = BLOCK_SIZE / HASH_SIZE;
|
||||
constexpr const char *MERKLE_EXTENSION = ".merkle";
|
||||
|
||||
struct B2H // blake2b hash is 32 bytes which we store as 4 quad words
|
||||
{
|
||||
uint64_t data[4];
|
||||
};
|
||||
|
||||
// provide some helper functions for working with 32 byte hash type
|
||||
bool operator==(B2H &lhs, B2H &rhs)
|
||||
{
|
||||
return lhs.data[0] == rhs.data[0] && lhs.data[1] == rhs.data[1] && lhs.data[2] == rhs.data[2] && lhs.data[3] == rhs.data[3];
|
||||
}
|
||||
|
||||
// the actual hash function, note that the B2H datatype is always passed by value being only 4 quadwords
|
||||
B2H hash(const void *ptr, size_t len)
|
||||
{
|
||||
B2H ret;
|
||||
crypto_generichash_blake2b_state state;
|
||||
crypto_generichash_blake2b_init(&state, NULL, 0, HASH_SIZE);
|
||||
crypto_generichash_blake2b_update(&state,
|
||||
reinterpret_cast<const unsigned char *>(ptr), len);
|
||||
crypto_generichash_blake2b_final(
|
||||
&state,
|
||||
reinterpret_cast<unsigned char *>(&ret),
|
||||
HASH_SIZE);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the .merkel block map for the given state file.
|
||||
* @param filepath Full path of the state file.
|
||||
* @param hinted_blocks Set of updated file block ids. If empty full merkel block map will be recomputed.
|
||||
*/
|
||||
int update_file_blockmap(const std::string &filepath, const std::set<uint32_t> &hinted_blocks)
|
||||
{
|
||||
// .merkel file path will be corresponding path in "statemap" directory.
|
||||
std::string merkle_fn;
|
||||
const size_t relative_path_len = filepath.length() - conf::ctx.statedir.length();
|
||||
merkle_fn.reserve(conf::ctx.statemapdir.length() + relative_path_len + 7);
|
||||
merkle_fn.append(conf::ctx.statemapdir);
|
||||
merkle_fn.append(filepath.substr(conf::ctx.statedir.length(), relative_path_len));
|
||||
merkle_fn.append(MERKLE_EXTENSION);
|
||||
|
||||
// To benefit from hint mode, the .merkle file must already exist. If not we simply disable hint mode
|
||||
// because we anyway have to rebuild entire merkle file from scratch.
|
||||
bool hint_mode = !hinted_blocks.empty();
|
||||
if (access(merkle_fn.c_str(), F_OK) == -1)
|
||||
hint_mode = false;
|
||||
|
||||
// open the target file for which we are building or updating a merkle tree
|
||||
FILE *f = fopen(filepath.c_str(), "rb");
|
||||
if (!f)
|
||||
{
|
||||
LOG_ERR << "Failed to open state file: " << filepath << " for reading.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// the merkle tree structure is only 4mb and could technically sit on stack in most cases but
|
||||
// TODO: ******Why can't we allocate this on the stack?
|
||||
auto merkle_tree = std::make_unique<B2H[]>(MAX_HASHES);
|
||||
// same with the read buffer
|
||||
auto read_buffer = std::make_unique<uint8_t[]>(BLOCK_SIZE);
|
||||
|
||||
// this iterator will be used if we are in hint mode
|
||||
auto hint = hinted_blocks.begin();
|
||||
|
||||
size_t block_counter = 0;
|
||||
size_t block_location = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// if hint blocks have been specified we'll seek to specific blocks in the file based on hint list.
|
||||
if (hint_mode)
|
||||
{
|
||||
// check if we've run out of elements
|
||||
if (hint == hinted_blocks.end())
|
||||
break;
|
||||
|
||||
// get the next block on their list
|
||||
block_location = *hint++;
|
||||
|
||||
// seek the file cursor to the block
|
||||
fseek(f, block_location * BLOCK_SIZE, SEEK_SET);
|
||||
}
|
||||
|
||||
// read the block
|
||||
int bytesread = fread(read_buffer.get(), 1, BLOCK_SIZE, f);
|
||||
if (bytesread <= 0)
|
||||
break;
|
||||
|
||||
// calculate the block hash
|
||||
merkle_tree[block_location++] = hash(read_buffer.get(), std::min(BLOCK_SIZE, (size_t)bytesread));
|
||||
block_counter++;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
// now that we've computed all the block hashes we are interested in we have to deal with the .merkle file
|
||||
|
||||
// open the .merkle file
|
||||
// if we are in hint_mode we will open it in rb+ which will preserve its contents
|
||||
// otherwise we will truncate it if it already exists, because we will have to overwrite everything anyway
|
||||
FILE *fm = fopen(merkle_fn.c_str(), (hint_mode ? "rb+" : "wb+"));
|
||||
if (!fm)
|
||||
{
|
||||
LOG_ERR << "Failed to open merkle file: " << filepath << " for writing.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// get the size of the .merkle file
|
||||
fseek(fm, 0L, SEEK_END);
|
||||
const size_t len = ftell(fm);
|
||||
rewind(fm);
|
||||
|
||||
// write the updated hashes
|
||||
if (hint_mode)
|
||||
{
|
||||
// write selectively the updated block hashes
|
||||
const int fd = fileno(fm);
|
||||
for (int block : hinted_blocks)
|
||||
pwrite(fd, &(merkle_tree[block]), HASH_SIZE, HASH_SIZE * block);
|
||||
}
|
||||
else
|
||||
{
|
||||
// write the whole tree to the file
|
||||
fwrite(reinterpret_cast<void *>(merkle_tree.get()), 1, std::min(BLOCK_SIZE, HASH_SIZE * block_counter), fm);
|
||||
}
|
||||
|
||||
// compute the root hash
|
||||
|
||||
B2H root_hash;
|
||||
|
||||
if (hint_mode)
|
||||
{
|
||||
// if we only updated selective hashes (hint mode) then now we need to compute a hash over the whole merkle file
|
||||
// so we first need to read it in
|
||||
rewind(f);
|
||||
int bytesread = fread(read_buffer.get(), 1, BLOCK_SIZE, f);
|
||||
if (bytesread <= 0)
|
||||
fprintf(stderr, "could not read merkle file after writing to it?!\n");
|
||||
|
||||
// now simply compute the hash of what we just read, that's our root hash
|
||||
root_hash = hash(read_buffer.get(), std::min(BLOCK_SIZE, (size_t)bytesread));
|
||||
}
|
||||
else
|
||||
{
|
||||
// if we've just written out the whole merkle tree we already know it
|
||||
root_hash = hash(merkle_tree.get(), std::min(BLOCK_SIZE, HASH_SIZE * block_counter));
|
||||
}
|
||||
|
||||
fclose(fm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state block map for the files updated as specified by the provided blockmap.
|
||||
* TODO: This doesn't currently support deleted file tracking.
|
||||
*/
|
||||
void update_state_blockmap(const proc::contract_fblockmap_t &updates)
|
||||
{
|
||||
for (const auto &[filepath, blocks] : updates)
|
||||
{
|
||||
update_file_blockmap(filepath, blocks);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace cons
|
||||
@@ -1,11 +0,0 @@
|
||||
#ifndef _HP_CONS_STATEMAP_HANDLER_
|
||||
#define _HP_CONS_STATEMAP_HANDLER_
|
||||
|
||||
#include "../proc/proc.hpp"
|
||||
|
||||
namespace cons
|
||||
{
|
||||
void update_state_blockmap(const proc::contract_fblockmap_t &updates);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "usr/usr.hpp"
|
||||
#include "p2p/p2p.hpp"
|
||||
#include "cons/cons.hpp"
|
||||
#include "statefs/state_common.hpp"
|
||||
|
||||
/**
|
||||
* Parses CLI args and extracts hot pocket command and parameters given.
|
||||
@@ -35,7 +36,7 @@ int parse_cmd(int argc, char **argv)
|
||||
{
|
||||
// We inform the conf subsystem to populate the contract directory context values
|
||||
// based on the directory argument from the command line.
|
||||
conf::set_contract_dir_paths(argv[2]);
|
||||
conf::set_contract_dir_paths(argv[0], argv[2]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -114,7 +115,7 @@ void std_terminate() noexcept
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
//seed rand
|
||||
srand(time(0));
|
||||
srand(time(0));
|
||||
|
||||
// Register exception handler for std exceptions.
|
||||
std::set_terminate(&std_terminate);
|
||||
@@ -172,6 +173,8 @@ int main(int argc, char **argv)
|
||||
if (p2p::init() != 0 || usr::init() != 0 || cons::init() != 0)
|
||||
return -1;
|
||||
|
||||
statefs::init(conf::ctx.statehistdir);
|
||||
|
||||
// After initializing primary subsystems, register the SIGINT handler.
|
||||
signal(SIGINT, signal_handler);
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include "../fbschema/common_helpers.hpp"
|
||||
#include "../fbschema/p2pmsg_container_generated.h"
|
||||
#include "../fbschema/p2pmsg_content_generated.h"
|
||||
#include "../statefs/hasher.hpp"
|
||||
#include "../statefs/state_common.hpp"
|
||||
#include "../statefs/hashtree_builder.hpp"
|
||||
#include "proc.hpp"
|
||||
|
||||
namespace proc
|
||||
@@ -34,6 +37,9 @@ std::vector<int> hpscfds;
|
||||
// Holds the contract process id (if currently executing).
|
||||
pid_t contract_pid;
|
||||
|
||||
// Holds the state monitor process id (if currently executing).
|
||||
pid_t statemon_pid;
|
||||
|
||||
/**
|
||||
* Executes the contract process and passes the specified arguments.
|
||||
* @return 0 on successful process creation. -1 on failure or contract process is already running.
|
||||
@@ -48,6 +54,10 @@ int exec_contract(const contract_exec_args &args)
|
||||
if (feed_inputs(args) != 0)
|
||||
return -1;
|
||||
|
||||
// Start the state monitor before starting the contract process.
|
||||
if (start_state_monitor() != 0)
|
||||
return -1;
|
||||
|
||||
const pid_t pid = fork();
|
||||
if (pid > 0)
|
||||
{
|
||||
@@ -58,7 +68,7 @@ int exec_contract(const contract_exec_args &args)
|
||||
close_unused_fds(true);
|
||||
|
||||
// Wait for child process (contract process) to complete execution.
|
||||
const int presult = await_contract_execution();
|
||||
const int presult = await_process_execution(contract_pid);
|
||||
LOG_INFO << "Contract process ended.";
|
||||
|
||||
contract_pid = 0;
|
||||
@@ -68,6 +78,9 @@ int exec_contract(const contract_exec_args &args)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (stop_state_monitor() != 0)
|
||||
return -1;
|
||||
|
||||
// After contract execution, collect contract outputs.
|
||||
if (fetch_outputs(args) != 0)
|
||||
return -1;
|
||||
@@ -92,11 +105,12 @@ int exec_contract(const contract_exec_args &args)
|
||||
execv_args[conf::cfg.runtime_binexec_args.size()] = NULL;
|
||||
|
||||
int ret = execv(execv_args[0], execv_args);
|
||||
LOG_ERR << "Execv failed: " << ret;
|
||||
LOG_ERR << "Contract process execv failed: " << ret;
|
||||
exit(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERR << "fork() failed.";
|
||||
LOG_ERR << "fork() failed when starting contract process.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -104,21 +118,84 @@ int exec_contract(const contract_exec_args &args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks the calling thread until the contract process compelted exeution (if running).
|
||||
* @return 0 if contract process exited normally, exit code of contract process if abnormally exited.
|
||||
* Blocks the calling thread until the specified process compelted exeution (if running).
|
||||
* @return 0 if process exited normally or exit code of process if abnormally exited.
|
||||
*/
|
||||
int await_contract_execution()
|
||||
int await_process_execution(pid_t pid)
|
||||
{
|
||||
if (contract_pid > 0)
|
||||
if (pid > 0)
|
||||
{
|
||||
int scstatus;
|
||||
waitpid(contract_pid, &scstatus, 0);
|
||||
waitpid(pid, &scstatus, 0);
|
||||
if (!WIFEXITED(scstatus))
|
||||
return WEXITSTATUS(scstatus);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounts the fuse file system at the contract state dir by starting the state monitor process.
|
||||
* State monitor will automatically create a state history checkpoint as well.
|
||||
*/
|
||||
int start_state_monitor()
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid > 0)
|
||||
{
|
||||
// HP process.
|
||||
statemon_pid = pid;
|
||||
return 0;
|
||||
}
|
||||
else if (pid == 0)
|
||||
{
|
||||
// State monitor process.
|
||||
LOG_DBG << "Starting state monitor...";
|
||||
|
||||
// Fill process args.
|
||||
char *execv_args[4];
|
||||
execv_args[0] = conf::ctx.statemonexepath.data();
|
||||
execv_args[1] = conf::ctx.statehistdir.data();
|
||||
execv_args[2] = conf::ctx.statedir.data();
|
||||
execv_args[3] = NULL;
|
||||
|
||||
int ret = execv(execv_args[0], execv_args);
|
||||
LOG_ERR << "State monitor execv failed: " << ret;
|
||||
exit(1);
|
||||
}
|
||||
else if (pid < 0)
|
||||
{
|
||||
LOG_ERR << "fork() failed when starting state monitor.";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the state monitor and update the latest state hash tree.
|
||||
*/
|
||||
int stop_state_monitor()
|
||||
{
|
||||
kill(statemon_pid, SIGINT);
|
||||
|
||||
// Wait for state monitor process to complete execution after the SIGINT.
|
||||
const int presult = await_process_execution(statemon_pid);
|
||||
LOG_DBG << "State monitor stopped.";
|
||||
|
||||
statemon_pid = 0;
|
||||
|
||||
if (presult != 0)
|
||||
LOG_ERR << "State monitor process exited with non-normal status code: " << presult;
|
||||
|
||||
// Update the hash tree.
|
||||
hasher::B2H statehash = {0, 0, 0, 0};
|
||||
statefs::hashtree_builder htreebuilder(statefs::get_statedir_context());
|
||||
if (htreebuilder.generate(statehash) != 0)
|
||||
return -1;
|
||||
|
||||
LOG_DBG << "State hash: " << std::hex << statehash << std::dec;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contract args (JSON) into the stdin of the contract process.
|
||||
* Args format:
|
||||
|
||||
@@ -80,7 +80,11 @@ int exec_contract(const contract_exec_args &args);
|
||||
|
||||
//------Internal-use functions for this namespace.
|
||||
|
||||
int await_contract_execution();
|
||||
int await_process_execution(pid_t pid);
|
||||
|
||||
int start_state_monitor();
|
||||
|
||||
int stop_state_monitor();
|
||||
|
||||
int write_contract_args(const contract_exec_args &args);
|
||||
|
||||
|
||||
55
src/statefs/hasher.cpp
Normal file
55
src/statefs/hasher.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
#include "hasher.hpp"
|
||||
|
||||
namespace hasher
|
||||
{
|
||||
|
||||
// provide some helper functions for working with 32 byte hash type
|
||||
bool operator==(const B2H &lhs, const B2H &rhs)
|
||||
{
|
||||
return lhs.data[0] == rhs.data[0] && lhs.data[1] == rhs.data[1] && lhs.data[2] == rhs.data[2] && lhs.data[3] == rhs.data[3];
|
||||
}
|
||||
|
||||
bool operator!=(const B2H &lhs, const B2H &rhs)
|
||||
{
|
||||
return lhs.data[0] != rhs.data[0] || lhs.data[1] != rhs.data[1] || lhs.data[2] != rhs.data[2] || lhs.data[3] != rhs.data[3];
|
||||
}
|
||||
|
||||
void operator^=(B2H &lhs, const B2H &rhs)
|
||||
{
|
||||
lhs.data[0] ^= rhs.data[0];
|
||||
lhs.data[1] ^= rhs.data[1];
|
||||
lhs.data[2] ^= rhs.data[2];
|
||||
lhs.data[3] ^= rhs.data[3];
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &output, const B2H &h)
|
||||
{
|
||||
output << h.data[0] << h.data[1] << h.data[2] << h.data[3];
|
||||
return output;
|
||||
}
|
||||
|
||||
std::stringstream &operator<<(std::stringstream &output, const B2H &h)
|
||||
{
|
||||
output << std::hex << h;
|
||||
return output;
|
||||
}
|
||||
|
||||
// the actual hash function, note that the B2H datatype is always passed by value being only 4 quadwords
|
||||
B2H hash(const void *buf1, size_t buf1len, const void *buf2, size_t buf2len)
|
||||
{
|
||||
crypto_generichash_blake2b_state state;
|
||||
crypto_generichash_blake2b_init(&state, NULL, 0, HASH_SIZE);
|
||||
|
||||
crypto_generichash_blake2b_update(&state,
|
||||
reinterpret_cast<const unsigned char *>(buf1), buf1len);
|
||||
crypto_generichash_blake2b_update(&state,
|
||||
reinterpret_cast<const unsigned char *>(buf2), buf2len);
|
||||
B2H ret;
|
||||
crypto_generichash_blake2b_final(
|
||||
&state,
|
||||
reinterpret_cast<unsigned char *>(&ret),
|
||||
HASH_SIZE);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace hasher
|
||||
27
src/statefs/hasher.hpp
Normal file
27
src/statefs/hasher.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef _HASHER_
|
||||
#define _HASHER_
|
||||
|
||||
#include "../pchheader.hpp"
|
||||
|
||||
namespace hasher
|
||||
{
|
||||
|
||||
constexpr size_t HASH_SIZE = crypto_generichash_blake2b_BYTES;
|
||||
|
||||
struct B2H // blake2b hash is 32 bytes which we store as 4 quad words
|
||||
{
|
||||
uint64_t data[4];
|
||||
};
|
||||
|
||||
// provide some helper functions for working with 32 byte hash type
|
||||
bool operator==(const B2H &lhs, const B2H &rhs);
|
||||
bool operator!=(const B2H &lhs, const B2H &rhs);
|
||||
void operator^=(B2H &lhs, const B2H &rhs);
|
||||
std::ostream &operator<<(std::ostream &output, const B2H &h);
|
||||
std::stringstream &operator<<(std::stringstream &output, const B2H &h);
|
||||
|
||||
B2H hash(const void *buf1, size_t buf1len, const void *buf2, size_t buf2len);
|
||||
|
||||
} // namespace hasher
|
||||
|
||||
#endif
|
||||
334
src/statefs/hashmap_builder.cpp
Normal file
334
src/statefs/hashmap_builder.cpp
Normal file
@@ -0,0 +1,334 @@
|
||||
#include "../pchheader.hpp"
|
||||
#include "../hplog.hpp"
|
||||
#include "state_common.hpp"
|
||||
#include "hashmap_builder.hpp"
|
||||
#include "hasher.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
hashmap_builder::hashmap_builder(const statedir_context &ctx) : ctx(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
int hashmap_builder::generate_hashmap_forfile(hasher::B2H &parentdirhash, const std::string &filepath)
|
||||
{
|
||||
// We attempt to avoid a full rebuild of the block hash map file when possible.
|
||||
// For this optimisation, both the block hash map (.bhmap) file and the
|
||||
// delta block index (.bindex) file must exist.
|
||||
|
||||
// If the block index exists, we generate/update the hashmap file with the aid of that.
|
||||
// Block index file contains the updated blockids. If not, we simply rehash all the blocks.
|
||||
|
||||
std::string relpath = get_relpath(filepath, ctx.datadir);
|
||||
|
||||
// Open the actual data file and calculate the block count.
|
||||
int orifd = open(filepath.data(), O_RDONLY);
|
||||
if (orifd == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Open failed " << filepath;
|
||||
return -1;
|
||||
}
|
||||
const off_t filelength = lseek(orifd, 0, SEEK_END);
|
||||
uint32_t blockcount = ceil((double)filelength / (double)BLOCK_SIZE);
|
||||
|
||||
// Attempt to read the existing block hash map file.
|
||||
std::string bhmapfile;
|
||||
std::vector<char> bhmapdata;
|
||||
if (read_blockhashmap(bhmapdata, bhmapfile, relpath) == -1)
|
||||
return -1;
|
||||
|
||||
hasher::B2H oldfilehash = {0, 0, 0, 0};
|
||||
if (!bhmapdata.empty())
|
||||
memcpy(&oldfilehash, bhmapdata.data(), hasher::HASH_SIZE);
|
||||
|
||||
// Attempt to read the delta block index file.
|
||||
std::map<uint32_t, hasher::B2H> bindex;
|
||||
uint32_t original_blockcount;
|
||||
if (get_blockindex(bindex, original_blockcount, relpath) == -1)
|
||||
return -1;
|
||||
|
||||
// Array to contain the updated block hashes. Slot 0 is for the root hash.
|
||||
// Allocating hash array on the heap to avoid filling limited stack space.
|
||||
std::unique_ptr<hasher::B2H[]> hashes = std::make_unique<hasher::B2H[]>(1 + blockcount);
|
||||
const size_t hashes_size = (1 + blockcount) * hasher::HASH_SIZE;
|
||||
|
||||
if (update_hashes(hashes.get(), hashes_size, relpath, orifd, blockcount, original_blockcount, bindex, bhmapdata) == -1)
|
||||
return -1;
|
||||
|
||||
if (write_blockhashmap(bhmapfile, hashes.get(), hashes_size) == -1)
|
||||
return -1;
|
||||
|
||||
if (update_hashtree_entry(parentdirhash, !bhmapdata.empty(), oldfilehash, hashes[0], bhmapfile, relpath) == -1)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hashmap_builder::read_blockhashmap(std::vector<char> &bhmapdata, std::string &bhmapfile, const std::string &relpath)
|
||||
{
|
||||
bhmapfile.reserve(ctx.blockhashmapdir.length() + relpath.length() + HASHMAP_EXT_LEN);
|
||||
bhmapfile.append(ctx.blockhashmapdir).append(relpath).append(HASHMAP_EXT);
|
||||
|
||||
if (boost::filesystem::exists(bhmapfile))
|
||||
{
|
||||
int hmapfd = open(bhmapfile.c_str(), O_RDONLY);
|
||||
if (hmapfd == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Open failed " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
off_t size = lseek(hmapfd, 0, SEEK_END);
|
||||
bhmapdata.resize(size);
|
||||
|
||||
if (pread(hmapfd, bhmapdata.data(), size, 0) == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Read failed " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create directory tree if not exist so we are able to create the hashmap files.
|
||||
boost::filesystem::path hmapsubdir = boost::filesystem::path(bhmapfile).parent_path();
|
||||
if (created_bhmapsubdirs.count(hmapsubdir.string()) == 0)
|
||||
{
|
||||
boost::filesystem::create_directories(hmapsubdir);
|
||||
created_bhmapsubdirs.emplace(hmapsubdir.string());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hashmap_builder::get_blockindex(std::map<uint32_t, hasher::B2H> &idxmap, uint32_t &totalblockcount, const std::string &filerelpath)
|
||||
{
|
||||
std::string bindexfile;
|
||||
bindexfile.reserve(ctx.deltadir.length() + filerelpath.length() + BLOCKINDEX_EXT_LEN);
|
||||
bindexfile.append(ctx.deltadir).append(filerelpath).append(BLOCKINDEX_EXT);
|
||||
|
||||
if (boost::filesystem::exists(bindexfile))
|
||||
{
|
||||
std::ifstream infile(bindexfile, std::ios::binary | std::ios::ate);
|
||||
std::streamsize idxsize = infile.tellg();
|
||||
infile.seekg(0, std::ios::beg);
|
||||
|
||||
// Read the block index file into a vector.
|
||||
std::vector<char> bindex(idxsize);
|
||||
if (infile.read(bindex.data(), idxsize))
|
||||
{
|
||||
// First 8 bytes contain the original file length.
|
||||
off_t orifilelen;
|
||||
memcpy(&orifilelen, bindex.data(), 8);
|
||||
totalblockcount = ceil((double)orifilelen / (double)BLOCK_SIZE);
|
||||
|
||||
// Skip the first 8 bytes and loop through index entries.
|
||||
for (uint32_t idxoffset = 8; idxoffset < bindex.size();)
|
||||
{
|
||||
// Read the block no. (4 bytes) of where this block is from in the original file.
|
||||
uint32_t blockno = 0;
|
||||
memcpy(&blockno, bindex.data() + idxoffset, 4);
|
||||
idxoffset += 12; // Skip the cached block offset (8 bytes)
|
||||
|
||||
// Read the block hash (32 bytes).
|
||||
hasher::B2H hash;
|
||||
memcpy(&hash, bindex.data() + idxoffset, 32);
|
||||
idxoffset += 32;
|
||||
|
||||
idxmap.try_emplace(blockno, hash);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERR << errno << ": Read failed " << bindexfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
infile.close();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hashmap_builder::update_hashes(
|
||||
hasher::B2H *hashes, const off_t hashes_size, const std::string &relpath, const int orifd,
|
||||
const uint32_t blockcount, const uint32_t original_blockcount, const std::map<uint32_t, hasher::B2H> &bindex, const std::vector<char> &bhmapdata)
|
||||
{
|
||||
uint32_t nohint_blockstart = 0;
|
||||
|
||||
// If both existing delta block index and block hash map is available, we can just overlay the
|
||||
// changed block hashes (mentioned in the delta block index) on top of the old block hashes.
|
||||
// This would prevent unncessarily hashing lot of blocks.
|
||||
if (!bhmapdata.empty() && !bindex.empty())
|
||||
{
|
||||
// Load old hashes.
|
||||
memcpy(hashes, bhmapdata.data(), hashes_size < bhmapdata.size() ? hashes_size : bhmapdata.size());
|
||||
|
||||
// Refer to the block index and rehash the changed blocks.
|
||||
for (const auto [blockid, oldhash] : bindex)
|
||||
{
|
||||
// If the blockid from the block index is no longer there, that means the current file is
|
||||
// shorter than the previous version. So we can stop hashing at this point.
|
||||
if (blockid >= blockcount)
|
||||
break;
|
||||
|
||||
if (compute_blockhash(hashes[blockid + 1], blockid, orifd, relpath) == -1)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If the current file has more blocks than the previous version, we need to hash those
|
||||
// additional blocks as well.
|
||||
if (blockcount > original_blockcount)
|
||||
nohint_blockstart = original_blockcount;
|
||||
else
|
||||
nohint_blockstart = blockcount; // No additional blocks remaining.
|
||||
}
|
||||
|
||||
//Hash any additional blocks that has to be hashed without the guidance of block index.
|
||||
for (uint32_t blockid = nohint_blockstart; blockid < blockcount; blockid++)
|
||||
{
|
||||
if (compute_blockhash(hashes[blockid + 1], blockid, orifd, relpath) == -1)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calculate the new file hash: filehash = HASH(filename + XOR(block hashes))
|
||||
hasher::B2H filehash{0, 0, 0, 0};
|
||||
for (int i = 1; i <= blockcount; i++)
|
||||
filehash ^= hashes[i];
|
||||
|
||||
// Rehash the file hash with filename included.
|
||||
const std::string filename = boost::filesystem::path(relpath.data()).filename().string();
|
||||
filehash = hasher::hash(filename.c_str(), filename.length(), &filehash, hasher::HASH_SIZE);
|
||||
|
||||
hashes[0] = filehash;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hashmap_builder::compute_blockhash(hasher::B2H &hash, uint32_t blockid, int filefd, const std::string &relpath)
|
||||
{
|
||||
// Allocating block buffer on the heap to avoid filling limited stack space.
|
||||
std::unique_ptr<char[]> blockbuf = std::make_unique<char[]>(BLOCK_SIZE);
|
||||
const off_t blockoffset = BLOCK_SIZE * blockid;
|
||||
size_t bytesread = pread(filefd, blockbuf.get(), BLOCK_SIZE, blockoffset);
|
||||
if (bytesread == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Read failed " << relpath;
|
||||
return -1;
|
||||
}
|
||||
|
||||
hash = hasher::hash(&blockoffset, 8, blockbuf.get(), bytesread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hashmap_builder::write_blockhashmap(const std::string &bhmapfile, const hasher::B2H *hashes, const off_t hashes_size)
|
||||
{
|
||||
int hmapfd = open(bhmapfile.c_str(), O_RDWR | O_TRUNC | O_CREAT, FILE_PERMS);
|
||||
if (hmapfd == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Open failed " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Write the updated hash list into the block hash map file.
|
||||
if (pwrite(hmapfd, hashes, hashes_size, 0) == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Write failed " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ftruncate(hmapfd, hashes_size) == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Truncate failed " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int hashmap_builder::update_hashtree_entry(hasher::B2H &parentdirhash, const bool oldbhmap_exists, const hasher::B2H oldfilehash, const hasher::B2H newfilehash, const std::string &bhmapfile, const std::string &relpath)
|
||||
{
|
||||
std::string hardlinkdir(ctx.hashtreedir);
|
||||
const std::string relpathdir = boost::filesystem::path(relpath).parent_path().string();
|
||||
|
||||
hardlinkdir.append(relpathdir);
|
||||
if (relpathdir != "/")
|
||||
hardlinkdir.append("/");
|
||||
|
||||
std::stringstream newhlpath;
|
||||
newhlpath << hardlinkdir << newfilehash << ".rh";
|
||||
|
||||
if (oldbhmap_exists)
|
||||
{
|
||||
// Rename the existing hard link if old block hash map existed.
|
||||
// We thereby assume the old hard link also existed.
|
||||
std::stringstream oldhlpath;
|
||||
oldhlpath << hardlinkdir << oldfilehash << ".rh";
|
||||
if (rename(oldhlpath.str().c_str(), newhlpath.str().c_str()) == -1)
|
||||
return -1;
|
||||
|
||||
// Subtract the old root hash and add the new root hash from the parent dir hash.
|
||||
parentdirhash ^= oldfilehash;
|
||||
parentdirhash ^= newfilehash;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a new hard link with new root hash as the name.
|
||||
if (link(bhmapfile.c_str(), newhlpath.str().c_str()) == -1)
|
||||
return -1;
|
||||
|
||||
// Add the new root hash to parent hash.
|
||||
parentdirhash ^= newfilehash;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hashmap_builder::remove_hashmapfile(hasher::B2H &parentdirhash, const std::string &bhmapfile)
|
||||
{
|
||||
if (boost::filesystem::exists(bhmapfile))
|
||||
{
|
||||
int hmapfd = open(bhmapfile.data(), O_RDONLY);
|
||||
if (hmapfd == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Open failed " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
hasher::B2H filehash;
|
||||
if (read(hmapfd, &filehash, hasher::HASH_SIZE) == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Read failed " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Delete the .bhmap file.
|
||||
if (remove(bhmapfile.c_str()) == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Delete failed " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Delete the hardlink of the .bhmap file.
|
||||
std::string hardlinkdir(ctx.hashtreedir);
|
||||
const std::string relpath = get_relpath(bhmapfile, ctx.blockhashmapdir);
|
||||
const std::string relpathdir = boost::filesystem::path(relpath).parent_path().string();
|
||||
|
||||
hardlinkdir.append(relpathdir);
|
||||
if (relpathdir != "/")
|
||||
hardlinkdir.append("/");
|
||||
|
||||
std::stringstream hlpath;
|
||||
hlpath << hardlinkdir << filehash << ".rh";
|
||||
if (remove(hlpath.str().c_str()) == -1)
|
||||
{
|
||||
LOG_ERR << errno << ": Delete failed for halrd link " << filehash << " of " << bhmapfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// XOR parent dir hash with file hash so the file hash gets removed from parent dir hash.
|
||||
parentdirhash ^= filehash;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace statefs
|
||||
35
src/statefs/hashmap_builder.hpp
Normal file
35
src/statefs/hashmap_builder.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef _STATEFS_HASHMAP_BUILDER_
|
||||
#define _STATEFS_HASHMAP_BUILDER_
|
||||
|
||||
#include "../pchheader.hpp"
|
||||
#include "hasher.hpp"
|
||||
#include "state_common.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
class hashmap_builder
|
||||
{
|
||||
private:
|
||||
const statedir_context ctx;
|
||||
// List of new block hash map sub directories created during the session.
|
||||
std::unordered_set<std::string> created_bhmapsubdirs;
|
||||
|
||||
int read_blockhashmap(std::vector<char> &bhmapdata, std::string &hmapfile, const std::string &relpath);
|
||||
int get_blockindex(std::map<uint32_t, hasher::B2H> &idxmap, uint32_t &totalblockcount, const std::string &filerelpath);
|
||||
int update_hashes(
|
||||
hasher::B2H *hashes, const off_t hashes_size, const std::string &relpath, const int orifd,
|
||||
const uint32_t blockcount, const uint32_t original_blockcount, const std::map<uint32_t, hasher::B2H> &bindex, const std::vector<char> &bhmapdata);
|
||||
int compute_blockhash(hasher::B2H &hash, uint32_t blockid, int filefd, const std::string &relpath);
|
||||
int write_blockhashmap(const std::string &bhmapfile, const hasher::B2H *hashes, const off_t hashes_size);
|
||||
int update_hashtree_entry(hasher::B2H &parentdirhash, const bool oldbhmap_exists, const hasher::B2H oldfilehash, const hasher::B2H newfilehash, const std::string &bhmapfile, const std::string &relpath);
|
||||
|
||||
public:
|
||||
hashmap_builder(const statedir_context &ctx);
|
||||
int generate_hashmap_forfile(hasher::B2H &parentdirhash, const std::string &filepath);
|
||||
int remove_hashmapfile(hasher::B2H &parentdirhash, const std::string &filepath);
|
||||
};
|
||||
|
||||
} // namespace statefs
|
||||
|
||||
#endif
|
||||
246
src/statefs/hashtree_builder.cpp
Normal file
246
src/statefs/hashtree_builder.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
#include "../pchheader.hpp"
|
||||
#include "hashtree_builder.hpp"
|
||||
#include "state_restore.hpp"
|
||||
#include "state_common.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
hashtree_builder::hashtree_builder(const statedir_context &ctx) : ctx(ctx), hmapbuilder(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
int hashtree_builder::generate(hasher::B2H &roothash)
|
||||
{
|
||||
// Load modified file path hints if available.
|
||||
populate_hintpaths(IDX_TOUCHEDFILES);
|
||||
populate_hintpaths(IDX_NEWFILES);
|
||||
hintmode = !hintpaths.empty();
|
||||
|
||||
traversel_rootdir = ctx.datadir;
|
||||
removal_mode = false;
|
||||
if (update_hashtree(roothash) != 0)
|
||||
return -1;
|
||||
|
||||
// If there are any remaining hint files directly under this directory, that means
|
||||
// those files are no longer there. So we need to delete the corresponding .bhmap and rh files
|
||||
// and adjust the directory hash accordingly.
|
||||
if (hintmode && !hintpaths.empty())
|
||||
{
|
||||
traversel_rootdir = ctx.blockhashmapdir;
|
||||
removal_mode = true;
|
||||
if (update_hashtree(roothash) != 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hashtree_builder::update_hashtree(hasher::B2H &roothash)
|
||||
{
|
||||
hintpath_map::iterator hintdir_itr = hintpaths.end();
|
||||
if (!should_process_dir(hintdir_itr, traversel_rootdir))
|
||||
return 0;
|
||||
|
||||
if (update_hashtree_fordir(roothash, traversel_rootdir, hintdir_itr, true) != 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hashtree_builder::update_hashtree_fordir(hasher::B2H &parentdirhash, const std::string &dirpath, const hintpath_map::iterator hintdir_itr, const bool isrootlevel)
|
||||
{
|
||||
const std::string htreedirpath = switch_basepath(dirpath, traversel_rootdir, ctx.hashtreedir);
|
||||
|
||||
// Load current dir hash if exist.
|
||||
const std::string dirhashfile = htreedirpath + "/" + DIRHASH_FNAME;
|
||||
hasher::B2H dirhash = get_existingdirhash(dirhashfile);
|
||||
|
||||
// Remember the dir hash before we mutate it.
|
||||
hasher::B2H original_dirhash = dirhash;
|
||||
|
||||
// Iterate files/subdirs inside this dir.
|
||||
const boost::filesystem::directory_iterator itrend;
|
||||
for (boost::filesystem::directory_iterator itr(dirpath); itr != itrend; itr++)
|
||||
{
|
||||
const bool isdir = boost::filesystem::is_directory(itr->path());
|
||||
const std::string pathstr = itr->path().string();
|
||||
|
||||
if (isdir)
|
||||
{
|
||||
hintpath_map::iterator hintsubdir_itr = hintpaths.end();
|
||||
if (!should_process_dir(hintsubdir_itr, pathstr))
|
||||
continue;
|
||||
|
||||
if (update_hashtree_fordir(dirhash, pathstr, hintsubdir_itr, false) != 0)
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!should_process_file(hintdir_itr, pathstr))
|
||||
continue;
|
||||
|
||||
if (process_file(dirhash, pathstr, htreedirpath) != 0)
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no more files in the hint dir, delete the hint dir entry as well.
|
||||
if (hintdir_itr != hintpaths.end() && hintdir_itr->second.empty())
|
||||
hintpaths.erase(hintdir_itr);
|
||||
|
||||
// In removalmode, we check whether the dir is empty. If so we remove the dir as well.
|
||||
if (removal_mode && boost::filesystem::is_empty(dirpath))
|
||||
{
|
||||
// We remove the dirs if we are below root level only.
|
||||
// Otherwise we only remove root dir.hash file.
|
||||
if (!isrootlevel)
|
||||
{
|
||||
boost::filesystem::remove_all(dirpath);
|
||||
boost::filesystem::remove_all(htreedirpath);
|
||||
}
|
||||
else
|
||||
{
|
||||
boost::filesystem::remove(dirhashfile);
|
||||
}
|
||||
|
||||
// Subtract the original dir hash from the parent dir hash.
|
||||
parentdirhash ^= original_dirhash;
|
||||
}
|
||||
else if (dirhash != original_dirhash)
|
||||
{
|
||||
// If dir hash has changed, write it back to dir hash file.
|
||||
if (save_dirhash(dirhashfile, dirhash) == -1)
|
||||
return -1;
|
||||
|
||||
// Also update the parent dir hash by subtracting the old hash and adding the new hash.
|
||||
parentdirhash ^= original_dirhash;
|
||||
parentdirhash ^= dirhash;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
hasher::B2H hashtree_builder::get_existingdirhash(const std::string &dirhashfile)
|
||||
{
|
||||
// Load current dir hash if exist.
|
||||
hasher::B2H dirhash{0, 0, 0, 0};
|
||||
int dirhashfd = open(dirhashfile.c_str(), O_RDONLY);
|
||||
if (dirhashfd > 0)
|
||||
{
|
||||
read(dirhashfd, &dirhash, hasher::HASH_SIZE);
|
||||
close(dirhashfd);
|
||||
}
|
||||
return dirhash;
|
||||
}
|
||||
|
||||
int hashtree_builder::save_dirhash(const std::string &dirhashfile, hasher::B2H dirhash)
|
||||
{
|
||||
int dirhashfd = open(dirhashfile.c_str(), O_RDWR | O_TRUNC | O_CREAT, FILE_PERMS);
|
||||
if (dirhashfd == -1)
|
||||
return -1;
|
||||
|
||||
if (write(dirhashfd, &dirhash, hasher::HASH_SIZE) == -1)
|
||||
{
|
||||
close(dirhashfd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(dirhashfd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline bool hashtree_builder::should_process_dir(hintpath_map::iterator &dir_itr, const std::string &dirpath)
|
||||
{
|
||||
return (hintmode ? get_hinteddir_match(dir_itr, dirpath) : true);
|
||||
}
|
||||
|
||||
bool hashtree_builder::should_process_file(const hintpath_map::iterator hintdir_itr, const std::string filepath)
|
||||
{
|
||||
if (hintmode)
|
||||
{
|
||||
if (hintdir_itr == hintpaths.end())
|
||||
return false;
|
||||
|
||||
std::string relpath = get_relpath(filepath, traversel_rootdir);
|
||||
|
||||
// If in removal mode, we are traversing .bhmap files. Hence we should truncate .bhmap extension
|
||||
// before we search for the path in file hints.
|
||||
if (removal_mode)
|
||||
relpath = relpath.substr(0, relpath.length() - HASHMAP_EXT_LEN);
|
||||
|
||||
std::unordered_set<std::string> &hintfiles = hintdir_itr->second;
|
||||
const auto hintfile_itr = hintfiles.find(relpath);
|
||||
if (hintfile_itr == hintfiles.end())
|
||||
return false;
|
||||
|
||||
// Erase the visiting filepath from hint files.
|
||||
hintfiles.erase(hintfile_itr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int hashtree_builder::process_file(hasher::B2H &parentdirhash, const std::string &filepath, const std::string &htreedirpath)
|
||||
{
|
||||
if (!removal_mode)
|
||||
{
|
||||
// Create directory tree if not exist so we are able to create the file root hash files (hard links).
|
||||
if (created_htreesubdirs.count(htreedirpath) == 0)
|
||||
{
|
||||
boost::filesystem::create_directories(htreedirpath);
|
||||
created_htreesubdirs.emplace(htreedirpath);
|
||||
}
|
||||
|
||||
if (hmapbuilder.generate_hashmap_forfile(parentdirhash, filepath) == -1)
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hmapbuilder.remove_hashmapfile(parentdirhash, filepath) == -1)
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void hashtree_builder::populate_hintpaths(const char *const idxfile)
|
||||
{
|
||||
std::ifstream infile(std::string(ctx.deltadir).append(idxfile));
|
||||
if (!infile.fail())
|
||||
{
|
||||
for (std::string relpath; std::getline(infile, relpath);)
|
||||
{
|
||||
std::string parentdir = boost::filesystem::path(relpath).parent_path().string();
|
||||
hintpaths[parentdir].emplace(relpath);
|
||||
}
|
||||
infile.close();
|
||||
}
|
||||
}
|
||||
|
||||
bool hashtree_builder::get_hinteddir_match(hintpath_map::iterator &matchitr, const std::string &dirpath)
|
||||
{
|
||||
// First check whether there's an exact match. If not check for a partial match.
|
||||
// Exact match will return the iterator. Partial match or not found will return end() iterator.
|
||||
const std::string relpath = get_relpath(dirpath, traversel_rootdir);
|
||||
const auto exactmatchitr = hintpaths.find(relpath);
|
||||
|
||||
if (exactmatchitr != hintpaths.end())
|
||||
{
|
||||
matchitr = exactmatchitr;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (auto itr = hintpaths.begin(); itr != hintpaths.end(); itr++)
|
||||
{
|
||||
if (strncmp(relpath.c_str(), itr->first.c_str(), relpath.length()) == 0)
|
||||
{
|
||||
// Partial match found.
|
||||
matchitr = hintpaths.end();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Not found at all.
|
||||
}
|
||||
|
||||
} // namespace statefs
|
||||
48
src/statefs/hashtree_builder.hpp
Normal file
48
src/statefs/hashtree_builder.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#ifndef _STATEFS_HASHTREE_BUILDER_
|
||||
#define _STATEFS_HASHTREE_BUILDER_
|
||||
|
||||
#include "../pchheader.hpp"
|
||||
#include "hasher.hpp"
|
||||
#include "hashmap_builder.hpp"
|
||||
#include "state_common.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
typedef std::unordered_map<std::string, std::unordered_set<std::string>> hintpath_map;
|
||||
|
||||
class hashtree_builder
|
||||
{
|
||||
private:
|
||||
const statedir_context ctx;
|
||||
hashmap_builder hmapbuilder;
|
||||
|
||||
// Hint path map with parent dir as key and list of file paths under each parent dir.
|
||||
hintpath_map hintpaths;
|
||||
bool hintmode;
|
||||
bool removal_mode;
|
||||
std::string traversel_rootdir;
|
||||
|
||||
// List of new root hash map sub directories created during the session.
|
||||
std::unordered_set<std::string> created_htreesubdirs;
|
||||
|
||||
int update_hashtree(hasher::B2H &roothash);
|
||||
int update_hashtree_fordir(hasher::B2H &parentdirhash, const std::string &relpath, const hintpath_map::iterator hintdir_itr, const bool isrootlevel);
|
||||
|
||||
hasher::B2H get_existingdirhash(const std::string &dirhashfile);
|
||||
int save_dirhash(const std::string &dirhashfile, hasher::B2H dirhash);
|
||||
bool should_process_dir(hintpath_map::iterator &hintsubdir_itr, const std::string &dirpath);
|
||||
bool should_process_file(const hintpath_map::iterator hintdir_itr, const std::string filepath);
|
||||
int process_file(hasher::B2H &parentdirhash, const std::string &filepath, const std::string &htreedirpath);
|
||||
int update_hashtree_entry(hasher::B2H &parentdirhash, const bool oldbhmap_exists, const hasher::B2H oldfilehash, const hasher::B2H newfilehash, const std::string &bhmapfile, const std::string &relpath);
|
||||
void populate_hintpaths(const char *const idxfile);
|
||||
bool get_hinteddir_match(hintpath_map::iterator &matchitr, const std::string &dirpath);
|
||||
|
||||
public:
|
||||
hashtree_builder(const statedir_context &ctx);
|
||||
int generate(hasher::B2H &roothash);
|
||||
};
|
||||
|
||||
} // namespace statefs
|
||||
|
||||
#endif
|
||||
60
src/statefs/state_common.cpp
Normal file
60
src/statefs/state_common.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include <string>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include "state_common.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
std::string statehistdir;
|
||||
|
||||
void init(const std::string &statehist_dir_root)
|
||||
{
|
||||
statehistdir = realpath(statehist_dir_root.c_str(), NULL);
|
||||
|
||||
// Initialize 0 state (current state) directory.
|
||||
get_statedir_context(0, true);
|
||||
}
|
||||
|
||||
std::string get_statedir_root(const int16_t checkpointid)
|
||||
{
|
||||
return statehistdir + "/" + std::to_string(checkpointid);
|
||||
}
|
||||
|
||||
statedir_context get_statedir_context(const int16_t checkpointid, const bool createdirs)
|
||||
{
|
||||
statedir_context ctx;
|
||||
ctx.rootdir = get_statedir_root(checkpointid);
|
||||
ctx.datadir = ctx.rootdir + DATA_DIR;
|
||||
ctx.blockhashmapdir = ctx.rootdir + BHMAP_DIR;
|
||||
ctx.hashtreedir = ctx.rootdir + HTREE_DIR;
|
||||
ctx.deltadir = ctx.rootdir + DELTA_DIR;
|
||||
|
||||
if (createdirs)
|
||||
{
|
||||
if (!boost::filesystem::exists(ctx.datadir))
|
||||
boost::filesystem::create_directories(ctx.datadir);
|
||||
if (!boost::filesystem::exists(ctx.blockhashmapdir))
|
||||
boost::filesystem::create_directories(ctx.blockhashmapdir);
|
||||
if (!boost::filesystem::exists(ctx.hashtreedir))
|
||||
boost::filesystem::create_directories(ctx.hashtreedir);
|
||||
if (!boost::filesystem::exists(ctx.deltadir))
|
||||
boost::filesystem::create_directories(ctx.deltadir);
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
std::string get_relpath(const std::string &fullpath, const std::string &base_path)
|
||||
{
|
||||
std::string relpath = fullpath.substr(base_path.length(), fullpath.length() - base_path.length());
|
||||
if (relpath.empty())
|
||||
relpath = "/";
|
||||
return relpath;
|
||||
}
|
||||
|
||||
std::string switch_basepath(const std::string &fullpath, const std::string &from_base_path, const std::string &to_base_path)
|
||||
{
|
||||
return to_base_path + get_relpath(fullpath, from_base_path);
|
||||
}
|
||||
|
||||
} // namespace statefs
|
||||
60
src/statefs/state_common.hpp
Normal file
60
src/statefs/state_common.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#ifndef _STATEFS_STATE_COMMON_
|
||||
#define _STATEFS_STATE_COMMON_
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <string>
|
||||
#include "hasher.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
constexpr int16_t MAX_CHECKPOINTS = 5;
|
||||
|
||||
// Cache block size.
|
||||
constexpr size_t BLOCK_SIZE = 4 * 1024 * 1024; // 4MB
|
||||
|
||||
// Cache block index entry bytes length.
|
||||
constexpr size_t BLOCKINDEX_ENTRY_SIZE = 44;
|
||||
constexpr size_t MAX_HASHES = BLOCK_SIZE / hasher::HASH_SIZE;
|
||||
|
||||
// Permissions used when creating block cache and index files.
|
||||
constexpr int FILE_PERMS = 0644;
|
||||
|
||||
const char *const HASHMAP_EXT = ".bhmap";
|
||||
constexpr size_t HASHMAP_EXT_LEN = 6;
|
||||
|
||||
const char *const BLOCKINDEX_EXT = ".bindex";
|
||||
constexpr size_t BLOCKINDEX_EXT_LEN = 7;
|
||||
|
||||
const char *const BLOCKCACHE_EXT = ".bcache";
|
||||
constexpr size_t BLOCKCACHE_EXT_LEN = 7;
|
||||
|
||||
const char *const IDX_NEWFILES = "/idxnew.idx";
|
||||
const char *const IDX_TOUCHEDFILES = "/idxtouched.idx";
|
||||
const char *const DIRHASH_FNAME = "dir.hash";
|
||||
|
||||
const char *const DATA_DIR = "/data";
|
||||
const char *const BHMAP_DIR = "/bhmap";
|
||||
const char *const HTREE_DIR = "/htree";
|
||||
const char *const DELTA_DIR = "/delta";
|
||||
|
||||
extern std::string statehistdir;
|
||||
|
||||
struct statedir_context
|
||||
{
|
||||
std::string rootdir;
|
||||
std::string datadir;
|
||||
std::string blockhashmapdir;
|
||||
std::string hashtreedir;
|
||||
std::string deltadir;
|
||||
};
|
||||
|
||||
void init(const std::string &statehist_dir_root);
|
||||
std::string get_statedir_root(const int16_t checkpointid);
|
||||
statedir_context get_statedir_context(int16_t checkpointid = 0, bool createdirs = false);
|
||||
std::string get_relpath(const std::string &fullpath, const std::string &base_path);
|
||||
std::string switch_basepath(const std::string &fullpath, const std::string &from_base_path, const std::string &to_base_path);
|
||||
|
||||
} // namespace statefs
|
||||
|
||||
#endif
|
||||
1342
src/statefs/state_monitor/fusefs.cpp
Normal file
1342
src/statefs/state_monitor/fusefs.cpp
Normal file
File diff suppressed because it is too large
Load Diff
9
src/statefs/state_monitor/fusefs.hpp
Normal file
9
src/statefs/state_monitor/fusefs.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef _FUSE_FS_
|
||||
#define _FUSE_FS_
|
||||
|
||||
namespace fusefs
|
||||
{
|
||||
int start(const char *arg0, const char *source, const char *mountpoint, const char *deltadir);
|
||||
}
|
||||
|
||||
#endif
|
||||
519
src/statefs/state_monitor/state_monitor.cpp
Normal file
519
src/statefs/state_monitor/state_monitor.cpp
Normal file
@@ -0,0 +1,519 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <unordered_map>
|
||||
#include <cmath>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <errno.h>
|
||||
#include "../hasher.hpp"
|
||||
#include "../state_common.hpp"
|
||||
#include "state_monitor.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
void state_monitor::create_checkpoint()
|
||||
{
|
||||
// Shift -1 and below checkpoints by 1 more.
|
||||
// If MAX oldest checkpoint is there, remove it and work our way upwards.
|
||||
int16_t oldest_chkpnt = (MAX_CHECKPOINTS + 1) * -1; // +1 because we maintain one extra checkpoint in case of rollbacks.
|
||||
for (int16_t chkpnt = oldest_chkpnt; chkpnt <= -1; chkpnt++)
|
||||
{
|
||||
std::string dir = get_statedir_root(chkpnt);
|
||||
|
||||
if (boost::filesystem::exists(dir))
|
||||
{
|
||||
if (chkpnt == oldest_chkpnt)
|
||||
{
|
||||
boost::filesystem::remove_all(dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string dirshift = get_statedir_root(chkpnt - 1);
|
||||
boost::filesystem::rename(dir, dirshift);
|
||||
}
|
||||
}
|
||||
|
||||
if (chkpnt == -1)
|
||||
{
|
||||
statedir_context ctx = get_statedir_context(0, true);
|
||||
|
||||
// Shift 0-state delta dir to -1.
|
||||
std::string delta_1 = dir + DELTA_DIR;
|
||||
boost::filesystem::create_directories(delta_1);
|
||||
|
||||
boost::filesystem::rename(ctx.deltadir, delta_1);
|
||||
boost::filesystem::create_directories(ctx.deltadir);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void state_monitor::oncreate(const int fd)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(monitor_mutex);
|
||||
|
||||
std::string filepath;
|
||||
if (extract_filepath(filepath, fd) == 0)
|
||||
oncreate_filepath(filepath);
|
||||
}
|
||||
|
||||
void state_monitor::onopen(const int inodefd, const int flags)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(monitor_mutex);
|
||||
|
||||
std::string filepath;
|
||||
if (extract_filepath(filepath, inodefd) == 0)
|
||||
{
|
||||
state_file_info *fi;
|
||||
if (get_tracked_fileinfo(&fi, filepath) == 0)
|
||||
{
|
||||
// Check whether fd is open in truncate mode. If so cache the entire file immediately.
|
||||
if (flags & O_TRUNC)
|
||||
cache_blocks(*fi, 0, fi->original_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void state_monitor::onwrite(const int fd, const off_t offset, const size_t length)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(monitor_mutex);
|
||||
|
||||
std::string filepath;
|
||||
if (get_fd_filepath(filepath, fd) == 0)
|
||||
{
|
||||
state_file_info *fi;
|
||||
if (get_tracked_fileinfo(&fi, filepath) == 0)
|
||||
cache_blocks(*fi, offset, length);
|
||||
}
|
||||
}
|
||||
|
||||
void state_monitor::onrename(const std::string &oldfilepath, const std::string &newfilepath)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(monitor_mutex);
|
||||
|
||||
ondelete_filepath(oldfilepath);
|
||||
oncreate_filepath(newfilepath);
|
||||
}
|
||||
|
||||
void state_monitor::ondelete(const std::string &filepath)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(monitor_mutex);
|
||||
ondelete_filepath(filepath);
|
||||
}
|
||||
|
||||
void state_monitor::ontruncate(const int fd, const off_t newsize)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(monitor_mutex);
|
||||
|
||||
std::string filepath;
|
||||
if (get_fd_filepath(filepath, fd) == 0)
|
||||
{
|
||||
// If truncated size is less than the original, cache the entire file.
|
||||
state_file_info *fi;
|
||||
if (get_tracked_fileinfo(&fi, filepath) == 0 && newsize < fi->original_length)
|
||||
cache_blocks(*fi, 0, fi->original_length);
|
||||
}
|
||||
}
|
||||
|
||||
void state_monitor::onclose(const int fd)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(monitor_mutex);
|
||||
|
||||
auto pitr = fdpathmap.find(fd);
|
||||
if (pitr != fdpathmap.end())
|
||||
{
|
||||
// Close any block cache/index fds we have opened for this file.
|
||||
auto fitr = fileinfomap.find(pitr->second); // pitr->second is the filepath string.
|
||||
if (fitr != fileinfomap.end())
|
||||
close_cachingfds(fitr->second); // fitr->second is the fileinfo struct.
|
||||
|
||||
fdpathmap.erase(pitr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the full physical file path for a given fd.
|
||||
* @param filepath String to assign the extracted file path.
|
||||
* @param fd The file descriptor to find the filepath.
|
||||
* @return 0 on successful file path extraction. -1 on failure.
|
||||
*/
|
||||
int state_monitor::extract_filepath(std::string &filepath, const int fd)
|
||||
{
|
||||
char proclnk[32];
|
||||
sprintf(proclnk, "/proc/self/fd/%d", fd);
|
||||
|
||||
filepath.resize(PATH_MAX);
|
||||
ssize_t len = readlink(proclnk, filepath.data(), PATH_MAX);
|
||||
if (len > 0)
|
||||
{
|
||||
filepath.resize(len);
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the full physical file path for a given fd using the fd map.
|
||||
* @param filepath String to assign the extracted file path.
|
||||
* @param fd The file descriptor to find the filepath.
|
||||
* @return 0 on successful file path extraction. -1 on failure.
|
||||
*/
|
||||
int state_monitor::get_fd_filepath(std::string &filepath, const int fd)
|
||||
{
|
||||
// Return path from the map if found.
|
||||
const auto itr = fdpathmap.find(fd);
|
||||
if (itr != fdpathmap.end())
|
||||
{
|
||||
filepath = itr->second;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Extract the file path and populate the fd-->filepath map.
|
||||
if (extract_filepath(filepath, fd) == 0)
|
||||
{
|
||||
fdpathmap[fd] = filepath;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void state_monitor::oncreate_filepath(const std::string &filepath)
|
||||
{
|
||||
// Check whether we are already tracking this file path.
|
||||
// Only way this could happen is deleting an existing file and creating a new file with same name.
|
||||
if (fileinfomap.count(filepath) == 0)
|
||||
{
|
||||
// Add an entry for the new file in the file info map. This information will be used to ignore
|
||||
// future operations (eg. write/delete) done to this file.
|
||||
state_file_info fi;
|
||||
fi.isnew = true;
|
||||
fi.filepath = filepath;
|
||||
fileinfomap[filepath] = std::move(fi);
|
||||
|
||||
// Add to the list of new files added during this session.
|
||||
write_newfileentry(filepath);
|
||||
}
|
||||
}
|
||||
|
||||
void state_monitor::ondelete_filepath(const std::string &filepath)
|
||||
{
|
||||
state_file_info *fi;
|
||||
if (get_tracked_fileinfo(&fi, filepath) == 0)
|
||||
{
|
||||
if (fi->isnew)
|
||||
{
|
||||
// If this is a new file, just remove from existing index entries.
|
||||
// No need to cache the file blocks.
|
||||
remove_newfileentry(fi->filepath);
|
||||
fileinfomap.erase(filepath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not a new file, cache the entire file.
|
||||
cache_blocks(*fi, 0, fi->original_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the tracked state file information for the given filepath.
|
||||
* @param fi Reference pointer to assign the state file info struct.
|
||||
* @param filepath Full physical path of the file.
|
||||
* @return 0 on successful find. -1 on failure.
|
||||
*/
|
||||
int state_monitor::get_tracked_fileinfo(state_file_info **fi, const std::string &filepath)
|
||||
{
|
||||
// Return from filepath-->fileinfo map if found.
|
||||
const auto itr = fileinfomap.find(filepath);
|
||||
if (itr != fileinfomap.end())
|
||||
{
|
||||
*fi = &itr->second;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Initialize a new state file info struct for the given filepath.
|
||||
state_file_info &fileinfo = fileinfomap[filepath];
|
||||
|
||||
// We use stat() to find out the length of the file.
|
||||
struct stat stat_buf;
|
||||
if (stat(filepath.c_str(), &stat_buf) != 0)
|
||||
{
|
||||
std::cerr << errno << ": Error occured in stat() of " << filepath << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
fileinfo.original_length = stat_buf.st_size;
|
||||
fileinfo.filepath = filepath;
|
||||
*fi = &fileinfo;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the specified bytes range of the given file.
|
||||
* @param fi The file info struct pointing to the file to be cached.
|
||||
* @param offset The start byte position for caching.
|
||||
* @param length How many bytes to cache.
|
||||
* @return 0 on successful execution. -1 on failure.
|
||||
*/
|
||||
int state_monitor::cache_blocks(state_file_info &fi, const off_t offset, const size_t length)
|
||||
{
|
||||
// No caching required if this is a new file created during this session.
|
||||
if (fi.isnew)
|
||||
return 0;
|
||||
|
||||
uint32_t original_blockcount = ceil((double)fi.original_length / (double)BLOCK_SIZE);
|
||||
|
||||
// Check whether we have already cached the entire file.
|
||||
if (original_blockcount == fi.cached_blockids.size())
|
||||
return 0;
|
||||
|
||||
// Initialize fds and indexes required for caching.
|
||||
if (prepare_caching(fi) != 0)
|
||||
return -1;
|
||||
|
||||
// Return if incoming write is outside any of the original blocks.
|
||||
if (offset > original_blockcount * BLOCK_SIZE)
|
||||
return 0;
|
||||
|
||||
uint32_t startblock = offset / BLOCK_SIZE;
|
||||
uint32_t endblock = (offset + length) / BLOCK_SIZE;
|
||||
|
||||
// std::cout << "Cache blocks: '" << fi.filepath << "' [" << offset << "," << length << "] " << startblock << "," << endblock << "\n";
|
||||
|
||||
// If this is the first time we are caching this file, write an entry to the touched file index.
|
||||
if (fi.cached_blockids.empty() && write_touchedfileentry(fi.filepath) != 0)
|
||||
return -1;
|
||||
|
||||
for (uint32_t i = startblock; i <= endblock; i++)
|
||||
{
|
||||
// Skip if we have already cached this block.
|
||||
if (fi.cached_blockids.count(i) > 0)
|
||||
continue;
|
||||
|
||||
// Read the block being replaced and send to cache file.
|
||||
// Allocating block buffer on the heap to avoid filling limited stack space.
|
||||
std::unique_ptr<char[]> blockbuf = std::make_unique<char[]>(BLOCK_SIZE);
|
||||
off_t blockoffset = BLOCK_SIZE * i;
|
||||
size_t bytesread = pread(fi.readfd, blockbuf.get(), BLOCK_SIZE, BLOCK_SIZE * i);
|
||||
if (bytesread < 0)
|
||||
{
|
||||
std::cerr << errno << ": Read failed " << fi.filepath << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// No more bytes to read in this file.
|
||||
if (bytesread == 0)
|
||||
return 0;
|
||||
|
||||
if (write(fi.cachefd, blockbuf.get(), bytesread) < 0)
|
||||
{
|
||||
std::cerr << errno << ": Write to block cache failed. " << fi.filepath << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Append an entry (44 bytes) into the block cache index. We maintain this index to
|
||||
// help random block access for external use cases. We currently do not sort this index here.
|
||||
// Whoever is using the index must sort it if required.
|
||||
// Entry format: [blocknum(4 bytes) | cacheoffset(8 bytes) | blockhash(32 bytes)]
|
||||
|
||||
char entrybuf[BLOCKINDEX_ENTRY_SIZE];
|
||||
hasher::B2H hash = hasher::hash(&blockoffset, 8, blockbuf.get(), bytesread);
|
||||
|
||||
// Original file block id.
|
||||
memcpy(entrybuf, &i, 4);
|
||||
// Position of the block within the cache file.
|
||||
off_t cacheoffset = fi.cached_blockids.size() * BLOCK_SIZE;
|
||||
memcpy(entrybuf + 4, &cacheoffset, 8);
|
||||
// The block hash.
|
||||
memcpy(entrybuf + 12, hash.data, 32);
|
||||
if (write(fi.indexfd, entrybuf, BLOCKINDEX_ENTRY_SIZE) < 0)
|
||||
{
|
||||
std::cerr << errno << ": Write to block index failed. " << fi.filepath << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Mark the block as cached.
|
||||
fi.cached_blockids.emplace(i);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes fds and indexes required for caching.
|
||||
* @param fi The state file info struct pointing to the file being cached.
|
||||
* @return 0 on succesful initialization. -1 on failure.
|
||||
*/
|
||||
int state_monitor::prepare_caching(state_file_info &fi)
|
||||
{
|
||||
// If readfd is greater than 0 then we take it as caching being already initialized.
|
||||
if (fi.readfd > 0)
|
||||
return 0;
|
||||
|
||||
// Open up the file using a read-only fd. This fd will be used to fetch blocks to be cached.
|
||||
fi.readfd = open(fi.filepath.c_str(), O_RDONLY);
|
||||
if (fi.readfd < 0)
|
||||
{
|
||||
std::cerr << errno << ": Open failed " << fi.filepath << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get the path of the file relative to the state dir. We maintain this same reative path for the
|
||||
// corresponding cache and index files in the cache dir.
|
||||
std::string relpath = get_relpath(fi.filepath, ctx.datadir);
|
||||
|
||||
std::string tmppath;
|
||||
tmppath.reserve(ctx.deltadir.length() + relpath.length() + BLOCKCACHE_EXT_LEN);
|
||||
tmppath.append(ctx.deltadir).append(relpath).append(BLOCKCACHE_EXT);
|
||||
|
||||
// Create directory tree if not exist so we are able to create the cache and index files.
|
||||
boost::filesystem::path cachesubdir = boost::filesystem::path(tmppath).parent_path();
|
||||
if (created_cachesubdirs.count(cachesubdir.string()) == 0)
|
||||
{
|
||||
boost::filesystem::create_directories(cachesubdir);
|
||||
created_cachesubdirs.emplace(cachesubdir.string());
|
||||
}
|
||||
|
||||
// Create and open the block cache file.
|
||||
fi.cachefd = open(tmppath.c_str(), O_WRONLY | O_APPEND | O_CREAT, FILE_PERMS);
|
||||
if (fi.cachefd <= 0)
|
||||
{
|
||||
std::cerr << errno << ": Open failed " << tmppath << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Create and open the block index file.
|
||||
tmppath.replace(tmppath.length() - BLOCKCACHE_EXT_LEN, BLOCKINDEX_EXT_LEN, BLOCKINDEX_EXT);
|
||||
fi.indexfd = open(tmppath.c_str(), O_WRONLY | O_APPEND | O_CREAT, FILE_PERMS);
|
||||
if (fi.indexfd <= 0)
|
||||
{
|
||||
std::cerr << errno << ": Open failed " << tmppath << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Write first entry (8 bytes) to the index file. First entry is the length of the original file.
|
||||
// This will be helpful when restoring/rolling back a file.
|
||||
if (write(fi.indexfd, &fi.original_length, 8) == -1)
|
||||
{
|
||||
std::cerr << errno << ": Error writing to index file " << tmppath << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes any open caching fds for a given file.
|
||||
*/
|
||||
void state_monitor::close_cachingfds(state_file_info &fi)
|
||||
{
|
||||
if (fi.readfd > 0)
|
||||
close(fi.readfd);
|
||||
|
||||
if (fi.cachefd > 0)
|
||||
close(fi.cachefd);
|
||||
|
||||
if (fi.indexfd > 0)
|
||||
close(fi.indexfd);
|
||||
|
||||
fi.readfd = 0;
|
||||
fi.cachefd = 0;
|
||||
fi.indexfd = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a file into the modified files list of this session.
|
||||
* This index is used to restore modified files during restore.
|
||||
*/
|
||||
int state_monitor::write_touchedfileentry(std::string_view filepath)
|
||||
{
|
||||
if (touchedfileindexfd <= 0)
|
||||
{
|
||||
std::string indexfile = ctx.deltadir + "/idxtouched.idx";
|
||||
touchedfileindexfd = open(indexfile.c_str(), O_WRONLY | O_APPEND | O_CREAT, FILE_PERMS);
|
||||
if (touchedfileindexfd <= 0)
|
||||
{
|
||||
std::cerr << errno << ": Open failed " << indexfile << "\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the relative file path line to the index.
|
||||
filepath = filepath.substr(ctx.datadir.length(), filepath.length() - ctx.datadir.length());
|
||||
write(touchedfileindexfd, filepath.data(), filepath.length());
|
||||
write(touchedfileindexfd, "\n", 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a file into the list of new files created during this session.
|
||||
* This index is used in deleting new files during restore.
|
||||
*/
|
||||
int state_monitor::write_newfileentry(std::string_view filepath)
|
||||
{
|
||||
std::string indexfile = ctx.deltadir + "/idxnew.idx";
|
||||
int fd = open(indexfile.c_str(), O_WRONLY | O_APPEND | O_CREAT, FILE_PERMS);
|
||||
if (fd <= 0)
|
||||
{
|
||||
std::cerr << errno << ": Open failed " << indexfile << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Write the relative file path line to the index.
|
||||
filepath = filepath.substr(ctx.datadir.length(), filepath.length() - ctx.datadir.length());
|
||||
write(fd, filepath.data(), filepath.length());
|
||||
write(fd, "\n", 1);
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans and removes the given filepath from the new files index.
|
||||
*/
|
||||
void state_monitor::remove_newfileentry(std::string_view filepath)
|
||||
{
|
||||
filepath = filepath.substr(ctx.datadir.length(), filepath.length() - ctx.datadir.length());
|
||||
|
||||
// We create a copy of the new file index and transfer lines from first file
|
||||
// to the second file except the line matching the given filepath.
|
||||
|
||||
std::string indexfile = ctx.deltadir + "/idxnew.idx";
|
||||
std::string indexfile_tmp = ctx.deltadir + "/idxnew.idx.tmp";
|
||||
|
||||
std::ifstream infile(indexfile);
|
||||
std::ofstream outfile(indexfile_tmp);
|
||||
|
||||
bool linestransferred = false;
|
||||
for (std::string line; std::getline(infile, line);)
|
||||
{
|
||||
if (line != filepath) // Skip the file being removed.
|
||||
{
|
||||
outfile << line << "\n";
|
||||
linestransferred = true;
|
||||
}
|
||||
}
|
||||
|
||||
infile.close();
|
||||
outfile.close();
|
||||
|
||||
// Remove the old index.
|
||||
std::remove(indexfile.c_str());
|
||||
|
||||
// If no lines transferred, delete the temp file as well.
|
||||
if (linestransferred)
|
||||
std::rename(indexfile_tmp.c_str(), indexfile.c_str());
|
||||
else
|
||||
std::remove(indexfile_tmp.c_str());
|
||||
}
|
||||
|
||||
} // namespace statefs
|
||||
77
src/statefs/state_monitor/state_monitor.hpp
Normal file
77
src/statefs/state_monitor/state_monitor.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#ifndef _STATEFS_STATE_MONITOR_
|
||||
#define _STATEFS_STATE_MONITOR_
|
||||
|
||||
#include <cstdint>
|
||||
#include <sys/types.h>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <mutex>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include "../state_common.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
// Holds information about an original file in state that we are tracking.
|
||||
struct state_file_info
|
||||
{
|
||||
bool isnew;
|
||||
off_t original_length;
|
||||
std::unordered_set<uint32_t> cached_blockids;
|
||||
std::string filepath;
|
||||
int readfd;
|
||||
int cachefd;
|
||||
int indexfd;
|
||||
};
|
||||
|
||||
// Invoked by fuse file system for relevent file system calls.
|
||||
class state_monitor
|
||||
{
|
||||
private:
|
||||
// Map of fd-->filepath
|
||||
std::unordered_map<int, std::string> fdpathmap;
|
||||
|
||||
// Map of filepath-->fileinfo
|
||||
std::unordered_map<std::string, state_file_info> fileinfomap;
|
||||
|
||||
// Complete list of modified files during the session.
|
||||
std::unordered_set<std::string> touchedfiles;
|
||||
|
||||
// List of new cache sub directories created during the session.
|
||||
std::unordered_set<std::string> created_cachesubdirs;
|
||||
|
||||
// Mutex to synchronize parallel file system calls into our custom state tracking logic.
|
||||
std::mutex monitor_mutex;
|
||||
|
||||
// Holds the fd used to write into modified files index. This will be kept open for the entire
|
||||
// life of the state monitor.
|
||||
int touchedfileindexfd = 0;
|
||||
|
||||
int extract_filepath(std::string &filepath, const int fd);
|
||||
int get_fd_filepath(std::string &filepath, const int fd);
|
||||
void oncreate_filepath(const std::string &filepath);
|
||||
void ondelete_filepath(const std::string &filepath);
|
||||
int get_tracked_fileinfo(state_file_info **fileinfo, const std::string &filepath);
|
||||
|
||||
int cache_blocks(state_file_info &fi, const off_t offset, const size_t length);
|
||||
int prepare_caching(state_file_info &fi);
|
||||
void close_cachingfds(state_file_info &fi);
|
||||
int write_touchedfileentry(std::string_view filepath);
|
||||
int write_newfileentry(std::string_view filepath);
|
||||
void remove_newfileentry(std::string_view filepath);
|
||||
|
||||
public:
|
||||
statedir_context ctx;
|
||||
void create_checkpoint();
|
||||
void oncreate(const int fd);
|
||||
void onopen(const int inodefd, const int flags);
|
||||
void onwrite(const int fd, const off_t offset, const size_t length);
|
||||
void onrename(const std::string &oldfilepath, const std::string &newfilepath);
|
||||
void ondelete(const std::string &filepath);
|
||||
void ontruncate(const int fd, const off_t newsize);
|
||||
void onclose(const int fd);
|
||||
};
|
||||
|
||||
} // namespace statefs
|
||||
|
||||
#endif
|
||||
200
src/statefs/state_restore.cpp
Normal file
200
src/statefs/state_restore.cpp
Normal file
@@ -0,0 +1,200 @@
|
||||
#include "../pchheader.hpp"
|
||||
#include "../hplog.hpp"
|
||||
#include "hasher.hpp"
|
||||
#include "state_restore.hpp"
|
||||
#include "hashtree_builder.hpp"
|
||||
#include "state_common.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
// Look at new files added and delete them if still exist.
|
||||
void state_restore::delete_newfiles()
|
||||
{
|
||||
std::string indexfile(ctx.deltadir);
|
||||
indexfile.append(IDX_NEWFILES);
|
||||
|
||||
std::ifstream infile(indexfile);
|
||||
for (std::string file; std::getline(infile, file);)
|
||||
{
|
||||
std::string filepath(ctx.datadir);
|
||||
filepath.append(file);
|
||||
|
||||
std::remove(filepath.c_str());
|
||||
}
|
||||
|
||||
infile.close();
|
||||
}
|
||||
|
||||
// Look at touched files and restore them.
|
||||
int state_restore::restore_touchedfiles()
|
||||
{
|
||||
std::unordered_set<std::string> processed;
|
||||
|
||||
std::string indexfile(ctx.deltadir);
|
||||
indexfile.append(IDX_TOUCHEDFILES);
|
||||
|
||||
std::ifstream infile(indexfile);
|
||||
for (std::string file; std::getline(infile, file);)
|
||||
{
|
||||
// Skip if already processed.
|
||||
if (processed.count(file) > 0)
|
||||
continue;
|
||||
|
||||
std::vector<char> bindex;
|
||||
if (read_blockindex(bindex, file) != 0)
|
||||
return -1;
|
||||
|
||||
if (restore_blocks(file, bindex) != 0)
|
||||
return -1;
|
||||
|
||||
// Add to processed file list.
|
||||
processed.emplace(file);
|
||||
}
|
||||
|
||||
infile.close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read the delta block index.
|
||||
int state_restore::read_blockindex(std::vector<char> &buffer, std::string_view file)
|
||||
{
|
||||
std::string bindexfile(ctx.deltadir);
|
||||
bindexfile.append(file).append(BLOCKINDEX_EXT);
|
||||
std::ifstream infile(bindexfile, std::ios::binary | std::ios::ate);
|
||||
std::streamsize idxsize = infile.tellg();
|
||||
infile.seekg(0, std::ios::beg);
|
||||
|
||||
buffer.resize(idxsize);
|
||||
if (!infile.read(buffer.data(), idxsize))
|
||||
{
|
||||
LOG_ERR << errno << ": Read failed " << bindexfile;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Restore blocks mentioned in the delta block index.
|
||||
int state_restore::restore_blocks(std::string_view file, const std::vector<char> &bindex)
|
||||
{
|
||||
int bcachefd = 0, orifilefd = 0;
|
||||
const char *idxptr = bindex.data();
|
||||
|
||||
// First 8 bytes of the index contains the supposed length of the original file.
|
||||
off_t originallen = 0;
|
||||
memcpy(&originallen, idxptr, 8);
|
||||
|
||||
// Open block cache file.
|
||||
{
|
||||
std::string bcachefile(ctx.deltadir);
|
||||
bcachefile.append(file).append(BLOCKCACHE_EXT);
|
||||
bcachefd = open(bcachefile.c_str(), O_RDONLY);
|
||||
if (bcachefd <= 0)
|
||||
{
|
||||
LOG_ERR << errno << ": Open failed " << bcachefile;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create or Open original file.
|
||||
{
|
||||
std::string originalfile(ctx.datadir);
|
||||
originalfile.append(file);
|
||||
|
||||
// Create directory tree if not exist so we are able to create the file.
|
||||
boost::filesystem::path filedir = boost::filesystem::path(originalfile).parent_path();
|
||||
if (created_dirs.count(filedir.string()) == 0)
|
||||
{
|
||||
boost::filesystem::create_directories(filedir);
|
||||
created_dirs.emplace(filedir.string());
|
||||
}
|
||||
|
||||
orifilefd = open(originalfile.c_str(), O_WRONLY | O_CREAT, FILE_PERMS);
|
||||
if (orifilefd <= 0)
|
||||
{
|
||||
LOG_ERR << errno << ": Open failed " << originalfile;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the blocks as specified in block index.
|
||||
for (uint32_t idxoffset = 8; idxoffset < bindex.size();)
|
||||
{
|
||||
// Find the block no. of where this block is from in the original file.
|
||||
uint32_t blockno = 0;
|
||||
memcpy(&blockno, idxptr + idxoffset, 4);
|
||||
idxoffset += 4;
|
||||
off_t orifileoffset = blockno * BLOCK_SIZE;
|
||||
|
||||
// Find the offset where the block is located in the block cache file.
|
||||
off_t bcacheoffset;
|
||||
memcpy(&bcacheoffset, idxptr + idxoffset, 8);
|
||||
idxoffset += 40; // Skip the hash(32)
|
||||
|
||||
// Transfer the cached block to the target file.
|
||||
copy_file_range(bcachefd, &bcacheoffset, orifilefd, &orifileoffset, BLOCK_SIZE, 0);
|
||||
}
|
||||
|
||||
// If the target file is bigger than the original size, truncate it to the original size.
|
||||
off_t currentlen = lseek(orifilefd, 0, SEEK_END);
|
||||
if (currentlen > originallen)
|
||||
ftruncate(orifilefd, originallen);
|
||||
|
||||
close(bcachefd);
|
||||
close(orifilefd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// This is called after a rollback so the all checkpoint dirs shift by 1.
|
||||
void state_restore::rewind_checkpoints()
|
||||
{
|
||||
// Assuming we have restored the current state with current delta,
|
||||
// we need to shift each history delta by 1 place.
|
||||
|
||||
// Delete the state 0 (current) delta.
|
||||
boost::filesystem::remove_all(ctx.deltadir);
|
||||
|
||||
int16_t oldest_chkpnt = (MAX_CHECKPOINTS + 1) * -1; // +1 because we maintain one extra checkpoint in case of rollbacks.
|
||||
for (int16_t chkpnt = -1; chkpnt >= oldest_chkpnt; chkpnt--)
|
||||
{
|
||||
std::string dir = get_statedir_root(chkpnt);
|
||||
|
||||
if (boost::filesystem::exists(dir))
|
||||
{
|
||||
if (chkpnt == -1)
|
||||
{
|
||||
// Shift -1 state delta dir to 0-state and delete -1 dir.
|
||||
std::string delta_1 = dir + DELTA_DIR;
|
||||
boost::filesystem::rename(delta_1, ctx.deltadir);
|
||||
boost::filesystem::remove_all(dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string dirshift = get_statedir_root(chkpnt + 1);
|
||||
boost::filesystem::rename(dir, dirshift);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rolls back current state to previous state.
|
||||
int state_restore::rollback(hasher::B2H &roothash)
|
||||
{
|
||||
ctx = get_statedir_context();
|
||||
|
||||
delete_newfiles();
|
||||
if (restore_touchedfiles() == -1)
|
||||
return -1;
|
||||
|
||||
// Update hash tree.
|
||||
hashtree_builder htreebuilder(ctx);
|
||||
htreebuilder.generate(roothash);
|
||||
|
||||
rewind_checkpoints();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace statefs
|
||||
28
src/statefs/state_restore.hpp
Normal file
28
src/statefs/state_restore.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef _STATEFS_STATE_RESTORE_
|
||||
#define _STATEFS_STATE_RESTORE_
|
||||
|
||||
#include "../pchheader.hpp"
|
||||
#include "hasher.hpp"
|
||||
#include "state_common.hpp"
|
||||
|
||||
namespace statefs
|
||||
{
|
||||
|
||||
class state_restore
|
||||
{
|
||||
private:
|
||||
statedir_context ctx;
|
||||
std::unordered_set<std::string> created_dirs;
|
||||
void delete_newfiles();
|
||||
int restore_touchedfiles();
|
||||
int read_blockindex(std::vector<char> &buffer, std::string_view file);
|
||||
int restore_blocks(std::string_view file, const std::vector<char> &bindex);
|
||||
void rewind_checkpoints();
|
||||
|
||||
public:
|
||||
int rollback(hasher::B2H &roothash);
|
||||
};
|
||||
|
||||
} // namespace statefs
|
||||
|
||||
#endif
|
||||
@@ -188,4 +188,13 @@ std::string_view getsv(const rapidjson::Value &v)
|
||||
return std::string_view(v.GetString(), v.GetStringLength());
|
||||
}
|
||||
|
||||
// provide a safe std::string overload for realpath
|
||||
std::string realpath(std::string path)
|
||||
{
|
||||
std::array<char, PATH_MAX> buffer;
|
||||
::realpath(path.c_str(), buffer.data());
|
||||
buffer[PATH_MAX] = '\0';
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
@@ -11,10 +11,10 @@ namespace util
|
||||
{
|
||||
|
||||
// Hot Pocket version. Displayed on 'hotpocket version' and written to new contract configs.
|
||||
constexpr const char* HP_VERSION = "0.1";
|
||||
constexpr const char *HP_VERSION = "0.1";
|
||||
|
||||
// Minimum compatible contract config version (this will be used to validate contract configs)
|
||||
constexpr const char* MIN_CONTRACT_VERSION = "0.1";
|
||||
constexpr const char *MIN_CONTRACT_VERSION = "0.1";
|
||||
|
||||
// Current version of the peer message protocol.
|
||||
constexpr uint8_t PEERMSG_VERSION = 1;
|
||||
@@ -74,6 +74,8 @@ int version_compare(const std::string &x, const std::string &y);
|
||||
|
||||
std::string_view getsv(const rapidjson::Value &v);
|
||||
|
||||
std::string realpath(std::string path);
|
||||
|
||||
} // namespace util
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user