Releasing the former Stadia file transfer tools

The tools allow efficient and fast synchronization of large directory
trees from a Windows workstation to a Linux target machine.

cdc_rsync* support efficient copy of files by using content-defined
chunking (CDC) to identify chunks within files that can be reused.

asset_stream_manager + cdc_fuse_fs support efficient streaming of a
local directory to a remote virtual file system based on FUSE. It also
employs CDC to identify and reuse unchanged data chunks.
This commit is contained in:
Christian Schneider
2022-10-07 10:47:04 +02:00
commit 4326e972ac
364 changed files with 49410 additions and 0 deletions

3
cdc_fuse_fs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
GGP/*
*.log
*.user

136
cdc_fuse_fs/BUILD Normal file
View File

@@ -0,0 +1,136 @@
package(default_visibility = ["//:__subpackages__"])
cc_binary(
name = "cdc_fuse_fs",
srcs = ["main.cc"],
deps = [
":cdc_fuse_fs_lib",
":constants",
"//absl_helper:jedec_size_flag",
"//common:gamelet_component",
"//common:log",
"//data_store:data_provider",
"//data_store:disk_data_store",
"//data_store:grpc_reader",
"@com_google_absl//absl/flags:parse",
],
)
# Dependencies for cdc_fuse_fs_lib, except for FUSE.
cdc_fuse_fs_lib_shared_deps = [
":asset",
":asset_stream_client",
":config_stream_client",
"//common:log",
"//common:path",
"//common:platform",
"//common:util",
"//common:threadpool",
"@com_github_jsoncpp//:jsoncpp",
]
cc_library(
name = "cdc_fuse_fs_lib",
srcs = ["cdc_fuse_fs.cc"],
hdrs = ["cdc_fuse_fs.h"],
target_compatible_with = ["@platforms//os:linux"],
deps = cdc_fuse_fs_lib_shared_deps + ["@com_github_fuse//:fuse_shared"],
)
cc_library(
name = "cdc_fuse_fs_lib_mocked",
srcs = ["cdc_fuse_fs.cc"],
hdrs = ["cdc_fuse_fs.h"],
copts = ["-DUSE_MOCK_LIBFUSE=1"],
deps = cdc_fuse_fs_lib_shared_deps + [":mock_libfuse"],
)
cc_test(
name = "cdc_fuse_fs_test",
srcs = ["cdc_fuse_fs_test.cc"],
deps = [
":cdc_fuse_fs_lib_mocked",
"//common:status_test_macros",
"//data_store",
"//data_store:mem_data_store",
"//manifest:fake_manifest_builder",
"//manifest:manifest_builder",
"@com_google_absl//absl/status",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_library(
name = "mock_libfuse",
srcs = ["mock_libfuse.cc"],
hdrs = ["mock_libfuse.h"],
deps = ["//common:platform"],
)
cc_library(
name = "constants",
hdrs = ["constants.h"],
)
cc_library(
name = "asset_stream_client",
srcs = ["asset_stream_client.cc"],
hdrs = ["asset_stream_client.h"],
deps = [
"//common:log",
"//common:status_macros",
"//common:stopwatch",
"//manifest:manifest_proto_defs",
"//proto:asset_stream_service_grpc_proto",
"@com_google_absl//absl/status:statusor",
],
)
cc_library(
name = "asset",
srcs = ["asset.cc"],
hdrs = ["asset.h"],
deps = [
"//common:buffer",
"//common:status",
"//data_store",
"//manifest:content_id",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/synchronization",
],
)
cc_test(
name = "asset_test",
srcs = ["asset_test.cc"],
deps = [
":asset",
"//common:path",
"//common:platform",
"//common:status_test_macros",
"//data_store:mem_data_store",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
],
)
cc_library(
name = "config_stream_client",
srcs = ["config_stream_client.cc"],
hdrs = ["config_stream_client.h"],
deps = [
"//common:grpc_status",
"//common:log",
"//manifest:content_id",
"//proto:asset_stream_service_grpc_proto",
"@com_google_absl//absl/status",
],
)
filegroup(
name = "all_test_sources",
srcs = glob(["*_test.cc"]),
)

520
cdc_fuse_fs/asset.cc Normal file
View File

@@ -0,0 +1,520 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cdc_fuse_fs/asset.h"
#include "common/buffer.h"
#include "common/status.h"
#include "data_store/data_store_reader.h"
namespace cdc_ft {
Asset::Asset() = default;
Asset::~Asset() = default;
void Asset::Initialize(ino_t parent_ino, DataStoreReader* data_store_reader,
const AssetProto* proto) {
parent_ino_ = parent_ino;
assert(!data_store_reader_ && data_store_reader);
data_store_reader_ = data_store_reader;
assert(!proto_ && proto);
proto_ = proto;
// Create a lookup for the direct assets, if any.
// Lock the mutex for convenience, it's not strictly necessary here as no
// other thread has access to this object.
absl::WriterMutexLock lock(&mutex_);
UpdateProtoLookup(proto_->dir_assets());
}
absl::StatusOr<std::vector<const AssetProto*>> Asset::GetAllChildProtos() {
mutex_.AssertNotHeld();
assert(proto_);
if (proto_->type() != AssetProto::DIRECTORY) {
return absl::InvalidArgumentError(
absl::StrFormat("Asset '%s' is not a directory asset", proto_->name()));
}
// Fetch all indirect dir asset lists.
for (;;) {
bool list_was_fetched;
ASSIGN_OR_RETURN(list_was_fetched, FetchNextDirAssetList(),
"Failed to fetch directory assets");
if (!list_was_fetched) break;
}
return GetLoadedChildProtos();
}
std::vector<const AssetProto*> Asset::GetLoadedChildProtos() const {
absl::ReaderMutexLock read_lock(&mutex_);
// Push all directory asset protos to a vector.
std::vector<const AssetProto*> protos;
protos.reserve(proto_lookup_.size());
for (const std::pair<const absl::string_view, const AssetProto*>& kv :
proto_lookup_) {
protos.push_back(kv.second);
}
return protos;
}
absl::StatusOr<const AssetProto*> Asset::Lookup(const char* name) {
mutex_.AssertNotHeld();
assert(proto_);
if (proto_->type() != AssetProto::DIRECTORY) {
return absl::InvalidArgumentError(
absl::StrFormat("Asset '%s' is not a directory asset", proto_->name()));
}
for (;;) {
{
absl::ReaderMutexLock read_lock(&mutex_);
// Check if we already have the asset.
std::unordered_map<absl::string_view, const AssetProto*>::iterator it =
proto_lookup_.find(name);
if (it != proto_lookup_.end()) {
return it->second;
}
}
// Fetch one more indirect asset list.
bool list_was_fetched;
ASSIGN_OR_RETURN(list_was_fetched, FetchNextDirAssetList(),
"Failed to fetch directory assets");
if (!list_was_fetched) {
// All lists were fetched, but asset still wasn't found.
return nullptr;
}
}
}
absl::StatusOr<uint64_t> Asset::Read(uint64_t offset, void* data,
uint64_t size) {
mutex_.AssertNotHeld();
assert(proto_);
if (proto_->type() != AssetProto::FILE)
return absl::InvalidArgumentError("Not a file asset");
if (size == 0) return 0;
// Find a chunk list such that list offset <= offset < next list offset.
int list_idx = FindChunkList(offset);
const RepeatedChunkRefProto* chunk_refs;
ASSIGN_OR_RETURN(chunk_refs, GetChunkRefList(list_idx),
"Failed to fetch indirect chunk list %i", list_idx);
uint64_t chunk_list_offset = ChunkListOffset(list_idx);
if (!chunk_refs) return 0; // Out of bounds.
// Find a chunk such that chunk offset <= offset < next chunk offset.
int chunk_idx = FindChunk(*chunk_refs, chunk_list_offset, offset);
if (chunk_idx < 0 || chunk_idx >= chunk_refs->size()) {
// Data is malformed, e.g. empty chunk list with non-zero file size.
return MakeStatus(
"Invalid chunk ref list %i. Found chunk index %i not in [0, %u).",
list_idx, chunk_idx, chunk_refs->size());
}
uint64_t data_bytes_left = size;
uint64_t prefetch_bytes_left = data_store_reader_->PrefetchSize(size);
// Collect the chunk IDs required to satisfy the read request.
ChunkTransferList chunks;
while (chunk_refs) {
const ChunkRefProto& chunk_ref = chunk_refs->at(chunk_idx);
// Figure out how much data we have to read from the current chunk.
uint64_t chunk_absolute_offset = chunk_list_offset + chunk_ref.offset();
uint64_t chunk_offset =
offset > chunk_absolute_offset ? offset - chunk_absolute_offset : 0;
uint64_t chunk_size = ChunkSize(list_idx, chunk_idx, chunk_refs);
assert(chunk_size >= chunk_offset);
uint64_t bytes_to_read =
std::min<uint64_t>(chunk_size - chunk_offset, data_bytes_left);
uint64_t bytes_to_prefetch =
std::min<uint64_t>(chunk_size - chunk_offset, prefetch_bytes_left);
// Enqueue a chunk transfer task.
chunks.emplace_back(chunk_ref.chunk_id(), chunk_offset,
bytes_to_read ? data : nullptr, bytes_to_read);
data = static_cast<char*>(data) + bytes_to_read;
data_bytes_left =
data_bytes_left > bytes_to_read ? data_bytes_left - bytes_to_read : 0;
prefetch_bytes_left -= bytes_to_prefetch;
offset += bytes_to_prefetch;
// If we request enough data, we are done.
if (!prefetch_bytes_left) break;
// Otherwise find next chunk.
++chunk_idx;
while (chunk_idx >= chunk_refs->size()) {
// Go to next list.
chunk_idx = 0;
++list_idx;
ASSIGN_OR_RETURN(chunk_refs, GetChunkRefList(list_idx),
"Failed to fetch indirect chunk list %i", list_idx);
chunk_list_offset = ChunkListOffset(list_idx);
if (!chunk_refs) {
// Out of bounds. If we're not at the file size now, it's an error.
if (offset != proto_->file_size()) {
return MakeStatus(
"Read error at position %u. Expected to be at file size %u.",
offset, proto_->file_size());
}
break;
}
}
if (chunk_refs) {
// We should be exactly at a chunk boundary now.
uint64_t chunk_rel_offset = chunk_refs->at(chunk_idx).offset();
if (offset != chunk_list_offset + chunk_rel_offset) {
return MakeStatus("Unexpected chunk offset %u, expected %u + %u = %u",
offset, chunk_list_offset, chunk_rel_offset,
chunk_list_offset + chunk_rel_offset);
}
}
}
// Read all data.
absl::Status status = data_store_reader_->Get(&chunks);
if (!status.ok() || !chunks.ReadDone()) {
std::string msg = absl::StrFormat(
"Failed to fetch chunk(s) [%s] for file '%s', offset %u, size %u",
chunks.ToHexString(
[](auto const& chunk) { return chunk.size && !chunk.done; }),
proto_->name(), offset, size);
return status.ok() ? absl::DataLossError(msg)
: WrapStatus(status, "%s", msg);
}
return size - data_bytes_left;
}
size_t Asset::GetNumFetchedFileChunkListsForTesting() {
mutex_.AssertNotHeld();
absl::ReaderMutexLock read_lock(&mutex_);
// In contrast to |dir_asset_lists_|, |file_chunk_lists_| might be fetched
// out-of-order, e.g. if someone tried to read the end of the file.
// Unfetched lists are nullptrs.
int num_fetched = 0;
for (const std::unique_ptr<ChunkListProto>& list : file_chunk_lists_) {
if (list) {
++num_fetched;
}
}
return num_fetched;
}
size_t Asset::GetNumFetchedDirAssetsListsForTesting() {
mutex_.AssertNotHeld();
absl::ReaderMutexLock read_lock(&mutex_);
return dir_asset_lists_.size();
}
void Asset::UpdateProto(const AssetProto* proto) {
absl::WriterMutexLock write_lock(&mutex_);
proto_lookup_.clear();
file_chunk_lists_.clear();
dir_asset_lists_.clear();
proto_ = proto;
if (proto_) {
UpdateProtoLookup(proto_->dir_assets());
}
}
bool Asset::IsConsistent(std::string* warning) const {
assert(proto_ && warning);
absl::ReaderMutexLock read_lock(&mutex_);
switch (proto_->type()) {
case AssetProto::FILE:
if (!proto_lookup_.empty() || !proto_->dir_assets().empty() ||
!proto_->dir_indirect_assets().empty()) {
*warning = "File asset contains sub-assets";
return false;
}
if (!proto_->symlink_target().empty()) {
*warning = "File asset contains a symlink";
return false;
}
break;
case AssetProto::DIRECTORY:
if (!proto_->file_chunks().empty() || !file_chunk_lists_.empty() ||
!proto_->file_indirect_chunks().empty()) {
*warning = "Directory asset contains file chunks";
return false;
}
if (!proto_->symlink_target().empty()) {
*warning = "Directory asset contains a symlink";
return false;
}
if (proto_->file_size() > 0) {
*warning = "File size is defined for a directory asset";
return false;
}
break;
case AssetProto::SYMLINK:
if (!proto_lookup_.empty() || !proto_->dir_assets().empty() ||
!proto_->dir_indirect_assets().empty()) {
*warning = "Symlink asset contains sub-assets";
return false;
}
if (!proto_->file_chunks().empty() || !file_chunk_lists_.empty() ||
!proto_->file_indirect_chunks().empty()) {
*warning = "Symlink asset contains file chunks";
return false;
}
if (proto_->file_size() > 0) {
*warning = "File size is defined for a symlink asset";
return false;
}
break;
default:
*warning = "Undefined asset type";
return false;
}
// Directory assets should not have any file chunks.
// Absolute file chunk offsets for all loaded direct and indirect chunks
// should be monotonically increasing.
if (proto_->type() == AssetProto::FILE) {
// Check direct chunks.
size_t total_offset = 0;
for (int idx = 0; idx < proto_->file_chunks_size(); ++idx) {
if (proto_->file_chunks(idx).offset() < total_offset) {
*warning = absl::StrFormat(
"Disordered direct chunks: idx=%u, total_offset=%u, "
"chunk_offset=%u",
idx, total_offset, proto_->file_chunks(idx).offset());
return false;
}
total_offset = proto_->file_chunks(idx).offset();
}
// Check indirect lists.
size_t prev_list_offset = total_offset;
for (int list_idx = 0; list_idx < proto_->file_indirect_chunks_size();
++list_idx) {
size_t list_offset = ChunkListOffset(list_idx);
if (list_idx == 0 && proto_->file_chunks_size() == 0 &&
list_offset != 0) {
*warning = absl::StrFormat(
"Disordered indirect chunk list: the list offset should be 0, as "
"there are no direct file chunks: "
"list_offset=%u, previous list_offset=%u",
list_offset, prev_list_offset);
return false;
} else if (list_idx > 0 && (prev_list_offset >= list_offset ||
total_offset >= list_offset)) {
*warning = absl::StrFormat(
"Disordered indirect chunk list: the list offset should increase: "
"list_offset=%u, previous list_offset=%u, total_offset=%u",
list_offset, prev_list_offset, total_offset);
return false;
}
if (file_chunk_lists_.size() <= list_idx ||
!file_chunk_lists_[list_idx]) {
total_offset = list_offset;
continue;
}
// If the list is fetched, check its chunks' order.
for (int chunk_idx = 0;
chunk_idx < file_chunk_lists_[list_idx]->chunks_size();
++chunk_idx) {
const ChunkRefProto& chunk =
file_chunk_lists_[list_idx]->chunks(chunk_idx);
if (chunk_idx == 0 && chunk.offset() != 0) {
*warning = absl::StrFormat(
"The offset of the first chunk in the list should be 0: "
"list_idx=%u, list_offset=%u, chunk_offset=%u",
list_idx, list_offset, chunk.offset());
return false;
}
if (chunk.offset() + list_offset < total_offset) {
*warning = absl::StrFormat(
"Disordered indirect chunk list: list_idx=%u, list_offset=%u, "
"offset=%u, chunk_offset=%u",
list_idx, list_offset, total_offset, chunk.offset());
return false;
}
total_offset = list_offset + chunk.offset();
}
}
if (total_offset == 0 && proto_->file_size() == 0) {
return true;
}
// The last absolute offset should be less than the file size.
if (total_offset >= proto_->file_size()) {
*warning = absl::StrFormat(
"The last absolute file offset exceeds the file size: %u >= %u",
total_offset, proto_->file_size());
return false;
}
}
return true;
}
absl::StatusOr<bool> Asset::FetchNextDirAssetList() {
mutex_.AssertNotHeld();
assert(proto_);
{
absl::ReaderMutexLock read_lock(&mutex_);
// Shortcut to prevent acquiring a write lock if everything has been loaded.
if (dir_asset_lists_.size() >=
static_cast<size_t>(proto_->dir_indirect_assets_size())) {
return false;
}
}
absl::WriterMutexLock write_lock(&mutex_);
// Check again in case some other thread has run this in the meantime.
if (dir_asset_lists_.size() >=
static_cast<size_t>(proto_->dir_indirect_assets_size())) {
return false;
}
// Read next indirect asset list.
const ContentIdProto& id =
proto_->dir_indirect_assets(static_cast<int>(dir_asset_lists_.size()));
auto list = std::make_unique<AssetListProto>();
RETURN_IF_ERROR(data_store_reader_->GetProto(id, list.get()),
"Failed to fetch AssetList proto with id %s",
ContentId::ToHexString(id));
dir_asset_lists_.push_back(std::move(list));
UpdateProtoLookup(dir_asset_lists_.back()->assets());
return true;
}
void Asset::UpdateProtoLookup(const RepeatedAssetProto& list) {
assert((mutex_.AssertHeld(), true));
for (const AssetProto& asset : list) {
proto_lookup_[asset.name().c_str()] = &asset;
}
}
int Asset::FindChunkList(uint64_t offset) {
assert(proto_);
const RepeatedIndirectChunkListProto& lists = proto_->file_indirect_chunks();
if (offset >= proto_->file_size()) {
// |offset| is not inside the file.
return proto_->file_indirect_chunks_size();
}
// TODO: Optimize search by using average chunk size.
auto it =
std::upper_bound(lists.begin(), lists.end(), offset,
[](uint64_t value, const IndirectChunkListProto& list) {
return value < list.offset();
});
return it - lists.begin() - 1;
}
int Asset::FindChunk(const RepeatedChunkRefProto& chunks,
uint64_t chunk_list_offset, uint64_t chunk_offset) {
assert(chunk_list_offset <= chunk_offset);
uint64_t rel_offset = chunk_offset - chunk_list_offset;
// TODO: Optimize search by using average chunk size.
auto it = std::upper_bound(chunks.begin(), chunks.end(), rel_offset,
[](uint64_t value, const ChunkRefProto& ch) {
return value < ch.offset();
});
return it - chunks.begin() - 1;
}
uint64_t Asset::ChunkListOffset(int list_idx) const {
assert(list_idx >= -1 && proto_ &&
list_idx <= proto_->file_indirect_chunks_size());
if (list_idx == -1) return 0;
if (list_idx < proto_->file_indirect_chunks_size())
return proto_->file_indirect_chunks(list_idx).offset();
return proto_->file_size();
}
uint64_t Asset::ChunkSize(int list_idx, int chunk_idx,
const RepeatedChunkRefProto* chunk_refs) {
assert(chunk_idx >= 0 && chunk_idx < chunk_refs->size());
assert(list_idx >= -1 && proto_ &&
list_idx <= proto_->file_indirect_chunks_size());
// If the next chunk is in the same chunk_refs list, just return offset diff.
if (chunk_idx + 1 < chunk_refs->size()) {
return chunk_refs->at(chunk_idx + 1).offset() -
chunk_refs->at(chunk_idx).offset();
}
// If the next chunk is on another list, use the next list's offset.
// Note that this also works for the last list, where
// GetChunkListOffset(list_idx + 1) returns the file size.
uint64_t chunk_absolute_offset =
chunk_refs->at(chunk_idx).offset() + ChunkListOffset(list_idx);
return ChunkListOffset(list_idx + 1) - chunk_absolute_offset;
}
absl::StatusOr<const RepeatedChunkRefProto*> Asset::GetChunkRefList(
int list_idx) {
mutex_.AssertNotHeld();
assert(list_idx >= -1 && proto_ &&
list_idx <= proto_->file_indirect_chunks_size());
if (list_idx == -1) {
// Direct chunk list.
return &proto_->file_chunks();
}
if (list_idx == proto_->file_indirect_chunks_size()) {
// Indicates EOF.
return nullptr;
}
{
absl::ReaderMutexLock read_lock(&mutex_);
// Do a quick check first if the list is already loaded.
// This only requires a read lock.
if (static_cast<size_t>(list_idx) < file_chunk_lists_.size() &&
file_chunk_lists_[list_idx]) {
return &file_chunk_lists_[list_idx]->chunks();
}
}
absl::WriterMutexLock write_lock(&mutex_);
// Indirect chunk list. Check if it has to be fetched.
if (file_chunk_lists_.size() < static_cast<size_t>(list_idx) + 1) {
file_chunk_lists_.resize(list_idx + 1);
}
if (!file_chunk_lists_[list_idx]) {
auto list = std::make_unique<ChunkListProto>();
const ContentIdProto& list_id =
proto_->file_indirect_chunks(list_idx).chunk_list_id();
RETURN_IF_ERROR(data_store_reader_->GetProto(list_id, list.get()),
"Failed to fetch ChunkListProto with id %s",
ContentId::ToHexString(list_id));
file_chunk_lists_[list_idx] = std::move(list);
}
return &file_chunk_lists_[list_idx]->chunks();
}
} // namespace cdc_ft

182
cdc_fuse_fs/asset.h Normal file
View File

@@ -0,0 +1,182 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CDC_FUSE_FS_ASSET_H_
#define CDC_FUSE_FS_ASSET_H_
#include <unordered_map>
#include "absl/base/thread_annotations.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/mutex.h"
#include "manifest/content_id.h"
namespace cdc_ft {
class Buffer;
class DataStoreReader;
// Wraps an asset proto for reading and adds additional functionality like name
// lookup maps and lazy loading of directory assets and file chunks.
// This class is accessed from multiple threads and has to be THREAD-SAFE.
class Asset {
public:
// Inode key type (cmp. fuse_ino_t).
using ino_t = uint64_t;
// Creates a new asset. Must call Initialize() before using it.
Asset();
~Asset();
// Make it non-copyable, non-assignable to prevent accidental misuse.
Asset(const Asset& other) = delete;
Asset& operator=(const Asset& other) = delete;
// Initialize the class. Must be called right after creation.
// NOT thread-safe! (OK as usually no other threads have access at this time.)
void Initialize(ino_t parent_ino, DataStoreReader* data_store_reader,
const AssetProto* proto);
// Returns the parent inode id passed to Initialize().
// Thread-safe.
ino_t parent_ino() const { return parent_ino_; }
// Returns the asset proto passed to Initialize().
// Thread-safe.
const AssetProto* proto() const { return proto_; }
// Returns all child asset protos. Loads them if necessary.
// Returns an error if loading an indirect asset list fails.
// Returns an InvalidArugmentError if *this is not a directory asset.
// |proto_| must be set.
// Thread-safe.
absl::StatusOr<std::vector<const AssetProto*>> GetAllChildProtos()
ABSL_LOCKS_EXCLUDED(mutex_);
// Returns loaded children's protos. Thread-safe.
std::vector<const AssetProto*> GetLoadedChildProtos() const;
// For directory assets, looks up a child asset by name. Loads indirect asset
// lists if needed. Returns an error if loading asset lists fails.
// Returns nullptr if the asset cannot be found.
// Returns an InvalidArugmentError if *this is not a directory asset.
// |proto_| must be set.
// Thread-safe.
absl::StatusOr<const AssetProto*> Lookup(const char* name)
ABSL_LOCKS_EXCLUDED(mutex_);
// For file assets, reads |size| bytes from the file, starting from |offset|,
// and puts the result into |data|. Returns the number of bytes read or 0 if
// |offset| >= file size. Loads indirect chunk lists if needed.
// Returns an error if loading chunk lists fails.
// Returns an InvalidArugmentError if *this is not a file asset.
// |proto_| must be set.
// Thread-safe.
absl::StatusOr<uint64_t> Read(uint64_t offset, void* data, uint64_t size);
size_t GetNumFetchedFileChunkListsForTesting() ABSL_LOCKS_EXCLUDED(mutex_);
size_t GetNumFetchedDirAssetsListsForTesting() ABSL_LOCKS_EXCLUDED(mutex_);
// Updates asset proto, all corresponding internal structures are cleaned up.
// This is an expensive operation as the previously created internal
// structures are removed. Thread-safe.
void UpdateProto(const AssetProto* proto) ABSL_LOCKS_EXCLUDED(mutex_);
// Checks consistency of the asset, for example: directory assets should not
// contain any file chunks. Any discovered inconsistencies are defined in
// |warning|.
bool IsConsistent(std::string* warning) const ABSL_LOCKS_EXCLUDED(mutex_);
private:
// Loads the next indirect directory asset list.
// Returns true if a list was fetched.
// Returns false if all lists have already been fetched.
// Returns an error if fetching an indirect asset list failed.
// |proto_| must be set.
absl::StatusOr<bool> FetchNextDirAssetList() ABSL_LOCKS_EXCLUDED(mutex_);
// Puts all assets from |list| into |proto_lookup_|.
void UpdateProtoLookup(const RepeatedAssetProto& list)
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
// Returns the index of the indirect chunk list that |offset| falls into or -1
// if |offset| is contained in the direct chunk list. Returns the number of
// indirect lists in |proto_| if |offset| is larger or equal to the file size.
// |proto_| must be set.
int FindChunkList(uint64_t offset);
// Returns the index of the chunk that |chunk_offset| falls into. The offsets
// in list |chunks| are interpreted relative to |chunk_list_offset|.
int FindChunk(const RepeatedChunkRefProto& chunks, uint64_t chunk_list_offset,
uint64_t chunk_offset);
// Gets the direct or an indirect chunk list. Fetches indirect chunk lists if
// necessary. |list_idx| must be in [-1, number of indirect chunk lists].
//
// Returns the direct chunk list if |list_idx| is -1. Returns nullptr if
// |list_idx| equals the number of indirect chunk lists. Returns the indirect
// chunk list at index |list_idx| otherwise. Returns an error if fetching an
// indirect chunk list fails.
// |proto_| must be set.
absl::StatusOr<const RepeatedChunkRefProto*> GetChunkRefList(int list_idx)
ABSL_LOCKS_EXCLUDED(mutex_);
// Returns the absolute offset of the chunk list with index |list_idx|.
// |list_idx| must be in [-1, number of indirect chunk lists]. -1 refers to
// the direct chunk list, in which case 0 is returned. If |list_idx| equals
// the number of indirect chunk lists, the file size is returned. Otherwise,
// the corresponding indirect chunk list's offset is returned.
// |proto_| must be set.
uint64_t ChunkListOffset(int list_idx) const;
// Returns the chunk size of the chunk with index |chunk_idx| on the chunk
// list with index |list_idx| and corresponding proto |chunk_refs|.
// |list_idx| must be in [-1, number of indirect chunk lists - 1].
// |chunk_idx| must be in [0, chunk_refs->size()].
// |proto_| must be set.
uint64_t ChunkSize(int list_idx, int chunk_idx,
const RepeatedChunkRefProto* chunk_refs);
// Parent inode, for ".." in dir listings.
ino_t parent_ino_ = 0;
// Interface for loading content (chunks, assets).
DataStoreReader* data_store_reader_ = nullptr;
// Corresponding asset proto.
const AssetProto* proto_ = nullptr;
// RW mutex for increased thread-safetiness.
mutable absl::Mutex mutex_;
// Maps asset proto names to asset protos for all protos loaded so far.
// The string views point directly into asset protos.
std::unordered_map<absl::string_view, const AssetProto*> proto_lookup_
ABSL_GUARDED_BY(mutex_);
// Fetched |file_indirect_chunks| chunk lists.
std::vector<std::unique_ptr<ChunkListProto>> file_chunk_lists_
ABSL_GUARDED_BY(mutex_);
// Fetched |dir_indirect_assets| fields so far.
std::vector<std::unique_ptr<AssetListProto>> dir_asset_lists_
ABSL_GUARDED_BY(mutex_);
};
} // namespace cdc_ft
#endif // CDC_FUSE_FS_ASSET_H_

View File

@@ -0,0 +1,112 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cdc_fuse_fs/asset_stream_client.h"
#include <thread>
#include "common/log.h"
#include "common/stopwatch.h"
namespace cdc_ft {
using GetContentRequest = proto::GetContentRequest;
using GetContentResponse = proto::GetContentResponse;
using SendCachedContentIdsRequest = proto::SendCachedContentIdsRequest;
using SendCachedContentIdsResponse = proto::SendCachedContentIdsResponse;
AssetStreamClient::AssetStreamClient(std::shared_ptr<grpc::Channel> channel,
bool enable_stats)
: enable_stats_(enable_stats) {
stub_ = AssetStreamService::NewStub(std::move(channel));
}
AssetStreamClient::~AssetStreamClient() = default;
size_t TotalDataSize(const RepeatedStringProto& data) {
size_t total_size = 0;
for (const std::string& s : data) {
total_size += s.size();
}
return total_size;
}
absl::StatusOr<std::string> AssetStreamClient::GetContent(
const ContentIdProto& id) {
GetContentRequest request;
*request.add_id() = id;
if (enable_stats_)
request.set_thread_id(thread_id_hash_(std::this_thread::get_id()));
grpc::ClientContext context;
GetContentResponse response;
Stopwatch sw;
grpc::Status status = stub_->GetContent(&context, request, &response);
LOG_DEBUG("GRPC TIME %0.3f sec for %u chunks with %u bytes",
sw.ElapsedSeconds(), response.data().size(),
TotalDataSize(response.data()));
if (!status.ok()) {
return absl::Status(static_cast<absl::StatusCode>(status.error_code()),
status.error_message());
}
assert(response.data_size() == 1);
return std::move(*response.mutable_data(0));
}
absl::StatusOr<RepeatedStringProto> AssetStreamClient::GetContent(
RepeatedContentIdProto chunk_ids) {
if (chunk_ids.empty()) return RepeatedStringProto();
GetContentRequest request;
*request.mutable_id() = std::move(chunk_ids);
if (enable_stats_)
request.set_thread_id(thread_id_hash_(std::this_thread::get_id()));
grpc::ClientContext context;
GetContentResponse response;
Stopwatch sw;
grpc::Status status = stub_->GetContent(&context, request, &response);
if (!status.ok()) {
return absl::Status(static_cast<absl::StatusCode>(status.error_code()),
status.error_message());
}
LOG_DEBUG("GRPC TIME %0.3f sec for %zu bytes", sw.ElapsedSeconds(),
TotalDataSize(response.data()));
return std::move(*response.mutable_data());
}
absl::Status AssetStreamClient::SendCachedContentIds(
std::vector<ContentIdProto> content_ids) {
SendCachedContentIdsRequest request;
for (ContentIdProto& id : content_ids) *request.add_id() = std::move(id);
grpc::ClientContext context;
SendCachedContentIdsResponse response;
grpc::Status status =
stub_->SendCachedContentIds(&context, request, &response);
if (!status.ok()) {
return absl::Status(static_cast<absl::StatusCode>(status.error_code()),
status.error_message());
}
return absl::OkStatus();
}
} // namespace cdc_ft

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CDC_FUSE_FS_ASSET_STREAM_CLIENT_H_
#define CDC_FUSE_FS_ASSET_STREAM_CLIENT_H_
#include <memory>
#include <string>
#include <thread>
#include "absl/status/statusor.h"
#include "grpcpp/channel.h"
#include "manifest/manifest_proto_defs.h"
#include "proto/asset_stream_service.grpc.pb.h"
namespace grpc_impl {
class Channel;
}
namespace cdc_ft {
// gRpc client for streaming assets to a gamelets. The client runs inside the
// CDC Fuse filesystem and requests chunks from the workstation.
class AssetStreamClient {
public:
// |channel| is a grpc channel to use.
// |enable_stats| determines whether additional statistics are sent.
AssetStreamClient(std::shared_ptr<grpc::Channel> channel, bool enable_stats);
~AssetStreamClient();
// Gets the content of the chunk with given |id|.
absl::StatusOr<std::string> GetContent(const ContentIdProto& id);
absl::StatusOr<RepeatedStringProto> GetContent(
RepeatedContentIdProto chunk_ids);
// Sends the IDs of all cached chunks to the workstation for statistical
// purposes.
absl::Status SendCachedContentIds(std::vector<ContentIdProto> content_ids);
private:
using AssetStreamService = proto::AssetStreamService;
std::unique_ptr<AssetStreamService::Stub> stub_;
bool enable_stats_;
std::hash<std::thread::id> thread_id_hash_;
};
} // namespace cdc_ft
#endif // CDC_FUSE_FS_ASSET_STREAM_CLIENT_H_

820
cdc_fuse_fs/asset_test.cc Normal file
View File

@@ -0,0 +1,820 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cdc_fuse_fs/asset.h"
#include "absl/strings/match.h"
#include "common/buffer.h"
#include "common/path.h"
#include "common/status_test_macros.h"
#include "data_store/mem_data_store.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace {
class AssetTest : public ::testing::Test {
public:
AssetTest()
: bad_id_(ContentId::FromDataString(std::string("does not exist"))) {
for (size_t n = 0; n < kNumChildProtos; ++n) {
child_protos_[n].set_name("file" + std::to_string(n));
}
}
protected:
static constexpr Asset::ino_t kParentIno = 1;
// Adds chunks with the data given by |data_vec| to the store, and
// adds references to the chunks to |list|. Updates |offset|.
void AddChunks(std::vector<std::vector<char>> data_vec, uint64_t* offset,
RepeatedChunkRefProto* list) {
for (auto& data : data_vec) {
ChunkRefProto* chunk_ref = list->Add();
chunk_ref->set_offset(*offset);
*offset += data.size();
*chunk_ref->mutable_chunk_id() = store_.AddData(std::move(data));
}
}
// Adds chunks with the data given by |data_vec| to the store,
// creates an indirect chunk list from those chunks and adds a reference to
// that list to |list|.
void AddIndirectChunks(std::vector<std::vector<char>> data_vec,
uint64_t* offset,
RepeatedIndirectChunkListProto* list) {
uint64_t indirect_list_offset = *offset;
*offset = 0;
ChunkListProto chunk_list;
AddChunks(data_vec, offset, chunk_list.mutable_chunks());
IndirectChunkListProto* indirect_list = list->Add();
indirect_list->set_offset(indirect_list_offset);
*indirect_list->mutable_chunk_list_id() = store_.AddProto(chunk_list);
*offset += indirect_list_offset;
}
// Checks if the given list |protos| contains an asset having |name|.
static bool ContainsAsset(const std::vector<const AssetProto*>& protos,
const std::string& name) {
return std::find_if(protos.begin(), protos.end(), [=](const AssetProto* p) {
return p->name() == name;
}) != protos.end();
}
MemDataStore store_;
AssetProto proto_;
Asset asset_;
static constexpr size_t kNumChildProtos = 4;
AssetProto child_protos_[kNumChildProtos];
const ContentIdProto bad_id_;
std::string asset_check_;
};
TEST_F(AssetTest, BasicGetters) {
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_EQ(asset_.parent_ino(), kParentIno);
EXPECT_EQ(asset_.proto(), &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ("Undefined asset type", asset_check_.c_str());
}
TEST_F(AssetTest, GetAllChildProtosDirectSucceeds) {
// Put all children into the direct asset list.
for (size_t n = 0; n < kNumChildProtos; ++n)
*proto_.add_dir_assets() = child_protos_[n];
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
absl::StatusOr<std::vector<const AssetProto*>> protos =
asset_.GetAllChildProtos();
ASSERT_OK(protos);
ASSERT_EQ(protos->size(), kNumChildProtos);
for (size_t n = 0; n < kNumChildProtos; ++n) {
EXPECT_TRUE(ContainsAsset(protos.value(), child_protos_[n].name()))
<< "Could not find asset " << child_protos_[n].name();
}
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, GetAllChildProtosIndirectSucceeds) {
// Put child0 into the direct asset list and children 1-N into indirect lists.
*proto_.add_dir_assets() = child_protos_[0];
for (size_t n = 1; n < kNumChildProtos; ++n) {
AssetListProto list;
*list.add_assets() = child_protos_[n];
*proto_.add_dir_indirect_assets() = store_.AddProto(list);
}
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
absl::StatusOr<std::vector<const AssetProto*>> protos =
asset_.GetAllChildProtos();
EXPECT_EQ(asset_.GetNumFetchedDirAssetsListsForTesting(),
kNumChildProtos - 1);
ASSERT_OK(protos);
ASSERT_EQ(protos->size(), kNumChildProtos);
for (size_t n = 0; n < kNumChildProtos; ++n) {
EXPECT_TRUE(ContainsAsset(protos.value(), child_protos_[n].name()))
<< "Could not find asset " << child_protos_[n].name();
}
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, GetAllChildProtosWithBadListIdFails) {
*proto_.add_dir_indirect_assets() = bad_id_;
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
absl::StatusOr<std::vector<const AssetProto*>> protos =
asset_.GetAllChildProtos();
ASSERT_NOT_OK(protos);
EXPECT_TRUE(absl::StrContains(protos.status().message(),
"Failed to fetch directory assets"));
}
TEST_F(AssetTest, GetAllChildProtosWithWrongTypeFails) {
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
absl::StatusOr<std::vector<const AssetProto*>> protos =
asset_.GetAllChildProtos();
ASSERT_NOT_OK(protos);
EXPECT_TRUE(absl::IsInvalidArgument(protos.status()));
}
TEST_F(AssetTest, GetLoadedChildProtosSucceedsForEmpty) {
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_TRUE(asset_.GetLoadedChildProtos().empty());
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ("Undefined asset type", asset_check_.c_str());
}
TEST_F(AssetTest, GetLoadedChildProtosSucceedsForNonEmpty) {
// Put child0 into the direct asset list and children 1-N into indirect lists.
*proto_.add_dir_assets() = child_protos_[0];
for (size_t n = 1; n < kNumChildProtos; ++n) {
AssetListProto list;
*list.add_assets() = child_protos_[n];
*proto_.add_dir_indirect_assets() = store_.AddProto(list);
}
proto_.set_type(AssetProto::DIRECTORY);
// The direct list is always loaded.
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<const AssetProto*> protos = asset_.GetLoadedChildProtos();
ASSERT_EQ(protos.size(), 1);
EXPECT_EQ(protos[0]->name(), child_protos_[0].name());
// A lookup for the first child triggers loading of the first indirect list.
EXPECT_OK(asset_.Lookup(child_protos_[1].name().c_str()));
protos = asset_.GetLoadedChildProtos();
ASSERT_EQ(protos.size(), 2);
EXPECT_TRUE(ContainsAsset(protos, child_protos_[0].name()));
EXPECT_TRUE(ContainsAsset(protos, child_protos_[1].name()));
// GetAllChildProtos() triggers loading of all indirect lists.
EXPECT_OK(asset_.GetAllChildProtos());
protos = asset_.GetLoadedChildProtos();
ASSERT_EQ(protos.size(), 4u);
for (size_t n = 0; n < protos.size(); ++n) {
EXPECT_TRUE(ContainsAsset(protos, child_protos_[n].name()))
<< "Could not find asset " << child_protos_[n].name();
}
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, LookupSucceeds) {
// Put child0 into the direct asset list and children 1-N into indirect lists.
*proto_.add_dir_assets() = child_protos_[0];
for (size_t n = 1; n < kNumChildProtos; ++n) {
AssetListProto list;
*list.add_assets() = child_protos_[n];
*proto_.add_dir_indirect_assets() = store_.AddProto(list);
}
proto_.set_type(AssetProto::DIRECTORY);
// Indirect asset lists should be fetched in a lazy fashion.
asset_.Initialize(kParentIno, &store_, &proto_);
absl::StatusOr<const AssetProto*> file0 = asset_.Lookup("file0");
EXPECT_EQ(asset_.GetNumFetchedDirAssetsListsForTesting(), 0);
absl::StatusOr<const AssetProto*> file1 = asset_.Lookup("file1");
EXPECT_EQ(asset_.GetNumFetchedDirAssetsListsForTesting(), 1);
absl::StatusOr<const AssetProto*> file3 = asset_.Lookup("file3");
EXPECT_EQ(asset_.GetNumFetchedDirAssetsListsForTesting(), 3);
ASSERT_OK(file0);
ASSERT_OK(file1);
ASSERT_OK(file3);
ASSERT_NE(*file0, nullptr);
ASSERT_NE(*file1, nullptr);
ASSERT_NE(*file3, nullptr);
EXPECT_EQ((*file0)->name(), child_protos_[0].name());
EXPECT_EQ((*file1)->name(), child_protos_[1].name());
EXPECT_EQ((*file3)->name(), child_protos_[3].name());
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, LookupNotFoundSucceeds) {
// Put child0 into the direct asset list and children 1-N into indirect lists.
*proto_.add_dir_assets() = child_protos_[0];
for (size_t n = 1; n < kNumChildProtos; ++n) {
AssetListProto list;
*list.add_assets() = child_protos_[n];
*proto_.add_dir_indirect_assets() = store_.AddProto(list);
}
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
absl::StatusOr<const AssetProto*> proto = asset_.Lookup("non_existing");
EXPECT_EQ(asset_.GetNumFetchedDirAssetsListsForTesting(),
kNumChildProtos - 1);
ASSERT_OK(proto);
ASSERT_EQ(*proto, nullptr);
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, LookupWithWrongTypeFails) {
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
absl::StatusOr<const AssetProto*> proto = asset_.Lookup("foo");
ASSERT_NOT_OK(proto);
EXPECT_TRUE(absl::IsInvalidArgument(proto.status()));
}
TEST_F(AssetTest, LookupWithBadListIdFails) {
*proto_.add_dir_assets() = child_protos_[0];
*proto_.add_dir_indirect_assets() = bad_id_;
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
// This should succeed since 'file0' on the direct assets list.
ASSERT_OK(asset_.Lookup("file0"));
// This should fail since it should trigger loading the bad id.
absl::StatusOr<const AssetProto*> proto = asset_.Lookup("file1");
ASSERT_NOT_OK(proto);
EXPECT_TRUE(absl::StrContains(proto.status().message(),
"Failed to fetch directory assets"));
}
TEST_F(AssetTest, ReadDirectSucceeds) {
uint64_t offset = 0;
AddChunks({{1, 2}, {3, 4}}, &offset, proto_.mutable_file_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(4);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_EQ(*bytes_read, 4);
EXPECT_EQ(data, std::vector<char>({1, 2, 3, 4}));
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, ReadIndirectSucceeds) {
uint64_t offset = 0;
AddChunks({{1, 2}}, &offset, proto_.mutable_file_chunks());
AddIndirectChunks({{3}, {4, 5, 6}}, &offset,
proto_.mutable_file_indirect_chunks());
AddIndirectChunks({{7, 8, 9}}, &offset,
proto_.mutable_file_indirect_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(9);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_EQ(*bytes_read, 9);
EXPECT_EQ(data, std::vector<char>({1, 2, 3, 4, 5, 6, 7, 8, 9}));
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, ReadIndirectOnlySucceeds) {
uint64_t offset = 0;
AddIndirectChunks({{1, 2}}, &offset, proto_.mutable_file_indirect_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(2);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_EQ(*bytes_read, 2);
EXPECT_EQ(data, std::vector<char>({1, 2}));
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ("", asset_check_.c_str());
}
TEST_F(AssetTest, ReadWithWrongType) {
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(1);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_NOT_OK(bytes_read);
EXPECT_TRUE(absl::IsInvalidArgument(bytes_read.status()));
}
TEST_F(AssetTest, ReadIndirectWithBadListIdFails) {
IndirectChunkListProto* indirect_list = proto_.add_file_indirect_chunks();
indirect_list->set_offset(0);
*indirect_list->mutable_chunk_list_id() = bad_id_;
proto_.set_file_size(1);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(1);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_NOT_OK(bytes_read);
EXPECT_TRUE(absl::StrContains(bytes_read.status().message(),
"Failed to fetch indirect chunk list 0"));
}
TEST_F(AssetTest, ReadFetchesIndirectListsLazily) {
uint64_t offset = 0;
AddChunks({{0, 1, 2}}, &offset, proto_.mutable_file_chunks());
AddIndirectChunks({{3}}, &offset, proto_.mutable_file_indirect_chunks());
AddIndirectChunks({{4, 5, 6}, {7}}, &offset,
proto_.mutable_file_indirect_chunks());
AddIndirectChunks({{8, 9}}, &offset, proto_.mutable_file_indirect_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_EQ(asset_.GetNumFetchedFileChunkListsForTesting(), 0);
// Read direct chunks. Should not trigger indirect reads.
std::vector<char> data(10);
absl::StatusOr<uint64_t> bytes_read = asset_.Read(0, data.data(), 3);
EXPECT_EQ(asset_.GetNumFetchedFileChunkListsForTesting(), 0);
// Read an indirect chunk near the end ({ {8, 9} }).
bytes_read = asset_.Read(8, data.data(), 1);
EXPECT_EQ(asset_.GetNumFetchedFileChunkListsForTesting(), 1);
// Read an indirect chunk in the beginning ({ {3} }).
bytes_read = asset_.Read(3, data.data(), 1);
EXPECT_EQ(asset_.GetNumFetchedFileChunkListsForTesting(), 2);
// Read an indirect chunk in the middle ({ {4, 5, 6}, {7} }).
bytes_read = asset_.Read(4, data.data(), 4);
EXPECT_EQ(asset_.GetNumFetchedFileChunkListsForTesting(), 3);
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, ReadEmptySucceeds) {
asset_.Initialize(kParentIno, &store_, &proto_);
proto_.set_type(AssetProto::FILE);
std::vector<char> data(4);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_EQ(*bytes_read, 0);
}
TEST_F(AssetTest, ReadEmptyDirectChunkSucceeds) {
uint64_t offset = 0;
AddChunks({{}, {1}}, &offset, proto_.mutable_file_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(4);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_EQ(*bytes_read, 1);
data.resize(1);
EXPECT_EQ(data, std::vector<char>({1}));
}
TEST_F(AssetTest, ReadEmptyIndirectChunkListFails) {
uint64_t offset = 0;
AddChunks({{1}}, &offset, proto_.mutable_file_chunks());
AddIndirectChunks({}, &offset, proto_.mutable_file_indirect_chunks());
AddIndirectChunks({{}}, &offset, proto_.mutable_file_indirect_chunks());
AddIndirectChunks({{2}}, &offset, proto_.mutable_file_indirect_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(4);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_EQ(*bytes_read, 2);
data.resize(2);
EXPECT_EQ(data, std::vector<char>({1, 2}));
}
TEST_F(AssetTest, ReadWithBadFileSizeFails) {
// Construct a case where the second chunk is empty, but file size indicates
// that it should be 1 byte long. Reading that byte should fail.
uint64_t offset = 0;
AddChunks({{1}, {}}, &offset, proto_.mutable_file_chunks());
proto_.set_file_size(offset + 1);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(1);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(1, data.data(), data.size());
ASSERT_NOT_OK(bytes_read);
EXPECT_TRUE(
absl::StrContains(bytes_read.status().message(),
"requested offset 0 is larger or equal than size 0"));
}
TEST_F(AssetTest, ReadWithBadChunkIdSizeFails) {
uint64_t offset = 0;
AddChunks({{1}}, &offset, proto_.mutable_file_chunks());
*proto_.mutable_file_chunks(0)->mutable_chunk_id() = bad_id_;
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(1);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_NOT_OK(bytes_read);
EXPECT_TRUE(absl::StrContains(bytes_read.status().message(),
"Failed to fetch chunk(s)"));
}
TEST_F(AssetTest, ReadWithBadOffsetFails) {
uint64_t offset = 0;
AddChunks({{1, 2, 3}, {4, 5, 6}}, &offset, proto_.mutable_file_chunks());
proto_.mutable_file_chunks(1)->set_offset(4); // Instead of 3.
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(6);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_NOT_OK(bytes_read);
EXPECT_TRUE(absl::StrContains(bytes_read.status().message(),
"requested size 4 at offset 0"));
}
TEST_F(AssetTest, ReadEmptyWithBadFileSize) {
uint64_t offset = 0;
AddChunks({}, &offset, proto_.mutable_file_chunks());
proto_.set_file_size(1);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(1);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_NOT_OK(bytes_read);
EXPECT_TRUE(absl::StrContains(bytes_read.status().message(),
"Invalid chunk ref list"));
}
TEST_F(AssetTest, ReadWithOffsetAndSizeSucceeds) {
uint64_t offset = 0;
AddChunks({{0, 1}, {}, {2}}, &offset, proto_.mutable_file_chunks());
AddIndirectChunks({{3}, {4, 5, 6}}, &offset,
proto_.mutable_file_indirect_chunks());
AddIndirectChunks({}, &offset, proto_.mutable_file_indirect_chunks());
AddIndirectChunks({{7, 8, 9}}, &offset,
proto_.mutable_file_indirect_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
// Test all kinds of different permutations of offsets and sizes.
std::vector<char> expected_data;
for (offset = 0; offset < 12; ++offset) {
for (uint64_t size = 0; size < 12; ++size) {
expected_data.clear();
for (uint64_t n = offset; n < std::min<uint64_t>(offset + size, 10);
++n) {
expected_data.push_back(static_cast<char>(n));
}
std::vector<char> data(size);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(offset, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_EQ(*bytes_read, expected_data.size());
data.resize(expected_data.size());
EXPECT_EQ(data, expected_data);
}
}
}
TEST_F(AssetTest, UpdateProtoWithEmptyAssetSucceeds) {
proto_.set_type(AssetProto::DIRECTORY);
// Put all children into the direct asset list.
for (size_t n = 0; n < kNumChildProtos; ++n)
*proto_.add_dir_assets() = child_protos_[n];
asset_.Initialize(kParentIno, &store_, &proto_);
absl::StatusOr<std::vector<const AssetProto*>> protos =
asset_.GetAllChildProtos();
ASSERT_OK(protos);
ASSERT_EQ(protos->size(), kNumChildProtos);
AssetProto proto_updated;
proto_updated.set_type(AssetProto::DIRECTORY);
asset_.UpdateProto(&proto_updated);
protos = asset_.GetAllChildProtos();
ASSERT_OK(protos);
ASSERT_TRUE(protos->empty());
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, UpdateProtoFromEmptyAssetSucceeds) {
AssetProto empty_proto;
empty_proto.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &empty_proto);
absl::StatusOr<std::vector<const AssetProto*>> protos =
asset_.GetAllChildProtos();
ASSERT_OK(protos);
ASSERT_TRUE(protos->empty());
proto_.set_type(AssetProto::DIRECTORY);
// Put all children into the direct asset list.
for (size_t n = 0; n < kNumChildProtos; ++n)
*proto_.add_dir_assets() = child_protos_[n];
asset_.UpdateProto(&proto_);
protos = asset_.GetAllChildProtos();
ASSERT_OK(protos);
ASSERT_EQ(protos->size(), kNumChildProtos);
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
}
TEST_F(AssetTest, AssetProtoComparison) {
AssetProto a;
AssetProto b;
EXPECT_EQ(a, b);
a.set_type(AssetProto::DIRECTORY);
b.set_type(AssetProto::FILE);
EXPECT_NE(a, b);
b.set_type(AssetProto::DIRECTORY);
EXPECT_EQ(a, b);
for (size_t n = 0; n < kNumChildProtos; ++n)
*a.add_dir_assets() = child_protos_[n];
EXPECT_NE(a, b);
for (size_t n = 0; n < kNumChildProtos; ++n)
*b.add_dir_assets() = child_protos_[n];
EXPECT_EQ(a, b);
}
TEST_F(AssetTest, IsConsistentFailsFileWithDirAssets) {
// Put all children into the direct asset list.
for (size_t n = 0; n < kNumChildProtos; ++n)
*proto_.add_dir_assets() = child_protos_[n];
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(), "File asset contains sub-assets");
}
TEST_F(AssetTest, IsConsistentFailsFileWithSymlink) {
proto_.set_symlink_target("symlink");
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(), "File asset contains a symlink");
}
TEST_F(AssetTest, IsConsistentFailsDirWithFileChunks) {
uint64_t offset = 0;
AddChunks({{1, 2}}, &offset, proto_.mutable_file_chunks());
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(), "Directory asset contains file chunks");
}
TEST_F(AssetTest, IsConsistentFailsDirWithIndirectFileChunks) {
uint64_t offset = 0;
AddIndirectChunks({{3}, {4, 5, 6}}, &offset,
proto_.mutable_file_indirect_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(), "Directory asset contains file chunks");
}
TEST_F(AssetTest, IsConsistentFailsDirWithSymlink) {
proto_.set_symlink_target("symlink");
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(), "Directory asset contains a symlink");
}
TEST_F(AssetTest, IsConsistentFailsDirWithFileSize) {
proto_.set_file_size(2);
proto_.set_type(AssetProto::DIRECTORY);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(),
"File size is defined for a directory asset");
}
TEST_F(AssetTest, IsConsistentFailsSymlinkWithDirAssets) {
// Put all children into the direct asset list.
for (size_t n = 0; n < kNumChildProtos; ++n)
*proto_.add_dir_assets() = child_protos_[n];
proto_.set_type(AssetProto::SYMLINK);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(), "Symlink asset contains sub-assets");
}
TEST_F(AssetTest, IsConsistentFailsSymlinkWithIndirectFileChunks) {
uint64_t offset = 0;
AddIndirectChunks({{3}, {4, 5, 6}}, &offset,
proto_.mutable_file_indirect_chunks());
proto_.set_file_size(offset);
proto_.set_type(AssetProto::SYMLINK);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(), "Symlink asset contains file chunks");
}
TEST_F(AssetTest, IsConsistentFailsSymlinkWithFileSize) {
proto_.set_file_size(2);
proto_.set_type(AssetProto::SYMLINK);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(),
"File size is defined for a symlink asset");
}
TEST_F(AssetTest, IsConsistentFailsUndefinedAssetType) {
proto_.set_type(AssetProto::UNKNOWN);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(), "Undefined asset type");
}
TEST_F(AssetTest, IsConsistentFailsFileChunkWrongOffsets) {
uint64_t offset = 10;
AddChunks({{1}}, &offset, proto_.mutable_file_chunks());
offset = 5;
AddChunks({{2}}, &offset, proto_.mutable_file_chunks());
proto_.set_file_size(2);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(
asset_check_.c_str(),
"Disordered direct chunks: idx=1, total_offset=10, chunk_offset=5");
}
TEST_F(AssetTest, IsConsistentFailsWrongFileSize) {
uint64_t offset = 0;
AddChunks({{1}, {2}}, &offset, proto_.mutable_file_chunks());
AddIndirectChunks({{3}, {4}, {5}, {6}}, &offset,
proto_.mutable_file_indirect_chunks());
AddIndirectChunks({{7}, {8}, {9}}, &offset,
proto_.mutable_file_indirect_chunks());
proto_.set_file_size(5);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
std::vector<char> data(9);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(asset_check_.c_str(),
"The last absolute file offset exceeds the file size: 8 >= 5");
}
TEST_F(AssetTest, IsConsistentFailsNonZeroFirstIndirectListOffset) {
uint64_t offset = 10;
AddIndirectChunks({{1}}, &offset, proto_.mutable_file_indirect_chunks());
proto_.set_file_size(1);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(
asset_check_.c_str(),
"Disordered indirect chunk list: the list offset should be 0, as there "
"are no direct file chunks: list_offset=10, previous list_offset=0");
}
TEST_F(AssetTest, IsConsistentFailsNonIncreasingIndirectListOffset) {
uint64_t offset = 0;
AddIndirectChunks({{1}, {2}, {3}}, &offset,
proto_.mutable_file_indirect_chunks());
offset = 1;
AddIndirectChunks({{3}}, &offset, proto_.mutable_file_indirect_chunks());
proto_.set_file_size(3);
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
// Read the first indirect list to fill the internal structure.
std::vector<char> data(3);
absl::StatusOr<uint64_t> bytes_read =
asset_.Read(0, data.data(), data.size());
ASSERT_OK(bytes_read);
EXPECT_EQ(*bytes_read, 3);
EXPECT_FALSE(asset_.IsConsistent(&asset_check_));
EXPECT_STREQ(
asset_check_.c_str(),
"Disordered indirect chunk list: the list offset should increase: "
"list_offset=1, previous list_offset=0, total_offset=2");
}
TEST_F(AssetTest, IsConsistentEmptyFileSucceeds) {
proto_.set_type(AssetProto::FILE);
asset_.Initialize(kParentIno, &store_, &proto_);
proto_.set_file_size(0);
EXPECT_TRUE(asset_.IsConsistent(&asset_check_));
EXPECT_TRUE(asset_check_.empty());
}
} // namespace
} // namespace cdc_ft

1553
cdc_fuse_fs/cdc_fuse_fs.cc Normal file

File diff suppressed because it is too large Load Diff

86
cdc_fuse_fs/cdc_fuse_fs.h Normal file
View File

@@ -0,0 +1,86 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CDC_FUSE_FS_CDC_FUSE_FS_H_
#define CDC_FUSE_FS_CDC_FUSE_FS_H_
#ifndef R_OK
#define R_OK 4
#endif
#ifndef W_OK
#define W_OK 2
#endif
#ifndef X_OK
#define X_OK 1
#endif
#include <memory>
#include "absl/status/status.h"
#include "cdc_fuse_fs/config_stream_client.h"
#include "grpcpp/channel.h"
#include "manifest/manifest_proto_defs.h"
namespace cdc_ft {
class DataStoreReader;
// CdcFuse filesystem constants, exposed for testing.
namespace internal {
// Number of hardlinks is not important since the fs is read-only (I think).
constexpr int kCdcFuseDefaultNLink = 1;
// Cloudcast user and group id.
constexpr int kCdcFuseCloudcastUid = 1000;
constexpr int kCdcFuseCloudcastGid = 1000;
// Root user and group id.
constexpr int kCdcFuseRootUid = 0;
constexpr int kCdcFuseRootGid = 0;
// Default timeout after which the kernel will assume inodes are stale.
constexpr double kCdcFuseInodeTimeoutSec = 1.0;
} // namespace internal
namespace cdc_fuse_fs {
// Initializes the CDC FUSE filesystem. Parses the command line, sets up a
// channel and a session, and optionally forks the process. For valid arguments
// see fuse_common.h.
absl::Status Initialize(int argc, char** argv);
// Starts a client to read configuration updates over gRPC |channel|.
// |instance| is the gamelet instance id.
absl::Status StartConfigClient(std::string instance,
std::shared_ptr<grpc::Channel> channel);
// Sets the |data_store_reader| to load data from, initializes FUSE with a
// manifest for an empty directory, and starts the filesystem. The call does
// not return until the filesystem finishes running.
// |consistency_check| defines whether FUSE consistency should be inspected
// after each manifest update.
absl::Status Run(DataStoreReader* data_store_reader, bool consistency_check);
// Releases resources. Should be called when the filesystem finished running.
void Shutdown();
// Sets |manifest_id| as a CDC FUSE root.
absl::Status SetManifest(const ContentIdProto& manifest_id);
} // namespace cdc_fuse_fs
} // namespace cdc_ft
#endif // CDC_FUSE_FS_CDC_FUSE_FS_H_

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|GGP">
<Configuration>Debug</Configuration>
<Platform>GGP</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|GGP">
<Configuration>Release</Configuration>
<Platform>GGP</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{a537310c-0571-43d5-b7fe-c867f702294f}</ProjectGuid>
<RootNamespace>cdc_fuse_fs</RootNamespace>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|GGP'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|GGP'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|GGP'">
<OutDir>$(SolutionDir)bazel-out\k8-dbg\bin\cdc_fuse_fs\</OutDir>
<NMakePreprocessorDefinitions>$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>
<AdditionalOptions>/std:c++17</AdditionalOptions>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|GGP'">
<OutDir>$(SolutionDir)bazel-out\k8-opt\bin\cdc_fuse_fs\</OutDir>
<NMakePreprocessorDefinitions>$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>
<AdditionalOptions>/std:c++17</AdditionalOptions>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
<Import Project="..\all_files.vcxitems" Label="Shared" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|GGP'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|GGP'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros">
</PropertyGroup>
<!-- Bazel setup -->
<PropertyGroup>
<BazelTargets>//cdc_fuse_fs</BazelTargets>
<BazelOutputFile>cdc_fuse_fs</BazelOutputFile>
<BazelIncludePaths>..\;..\third_party\absl;..\third_party\blake3\c;..\third_party\googletest\googletest\include;..\third_party\protobuf\src;..\third_party\grpc\include;$(AdditionalIncludeDirectories)</BazelIncludePaths>
<BazelSourcePathPrefix>..\/</BazelSourcePathPrefix>
</PropertyGroup>
<Import Project="..\NMakeBazelProject.targets" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cdc_fuse_fs/config_stream_client.h"
#include <thread>
#include "common/grpc_status.h"
#include "common/log.h"
#include "manifest/content_id.h"
namespace cdc_ft {
using GetManifestIdRequest = proto::GetManifestIdRequest;
using GetManifestIdResponse = proto::GetManifestIdResponse;
using AckManifestIdReceivedRequest = proto::AckManifestIdReceivedRequest;
using AckManifestIdReceivedResponse = proto::AckManifestIdReceivedResponse;
using ConfigStreamService = proto::ConfigStreamService;
// Asynchronous gRPC streaming client for streaming configuration changes to
// gamelets. The client runs inside the CDC FUSE and requests updated manifest
// from the workstation.
class ManifestIdReader {
public:
ManifestIdReader(ConfigStreamService::Stub* stub) : stub_(stub) {}
// Starts a GetManifestId() request and listens to the stream of manifest ids
// sent from the workstation. Calls |callback| on every manifest id received.
absl::Status StartListeningToManifestUpdates(
std::function<absl::Status(const ContentIdProto&)> callback) {
callback_ = callback;
GetManifestIdRequest request;
assert(!reader_);
reader_ = stub_->GetManifestId(&context_, request);
if (!reader_)
return absl::UnavailableError("Failed to create manifest id reader");
reader_thread_ =
std::make_unique<std::thread>([this]() { ReadThreadMain(); });
return absl::OkStatus();
}
// Thread that reads manifest ids from the GetManifestId() response stream.
void ReadThreadMain() {
GetManifestIdResponse response;
LOG_INFO("Started manifest id reader thread")
for (;;) {
LOG_INFO("Waiting for manifest id update")
if (!reader_->Read(&response)) break;
LOG_INFO("Received new manifest id '%s'",
ContentId::ToHexString(response.id()));
absl::Status status = callback_(response.id());
if (!status.ok()) {
LOG_ERROR("Failed to execute callback for manifest update '%s': '%s'",
ContentId::ToHexString(response.id()), status.message());
}
}
// This should happen if the server shuts down.
LOG_INFO("Stopped manifest id reader thread")
}
void Shutdown() {
if (!reader_thread_) return;
context_.TryCancel();
if (reader_thread_->joinable()) reader_thread_->join();
reader_thread_.reset();
}
private:
ConfigStreamService::Stub* stub_;
grpc::ClientContext context_;
std::unique_ptr<grpc::ClientReader<GetManifestIdResponse>> reader_;
std::function<absl::Status(const ContentIdProto&)> callback_;
std::unique_ptr<std::thread> reader_thread_;
};
ConfigStreamClient::ConfigStreamClient(std::string instance,
std::shared_ptr<grpc::Channel> channel)
: instance_(std::move(instance)),
stub_(ConfigStreamService::NewStub(std::move(channel))),
read_client_(std::make_unique<ManifestIdReader>(stub_.get())) {}
ConfigStreamClient::~ConfigStreamClient() = default;
absl::Status ConfigStreamClient::StartListeningToManifestUpdates(
std::function<absl::Status(const ContentIdProto&)> callback) {
LOG_INFO("Starting to listen to manifest updates");
return read_client_->StartListeningToManifestUpdates(callback);
}
absl::Status ConfigStreamClient::SendManifestAck(ContentIdProto manifest_id) {
AckManifestIdReceivedRequest request;
request.set_gamelet_id(instance_);
*request.mutable_manifest_id() = std::move(manifest_id);
grpc::ClientContext context_;
AckManifestIdReceivedResponse response;
RETURN_ABSL_IF_ERROR(
stub_->AckManifestIdReceived(&context_, request, &response));
return absl::OkStatus();
}
void ConfigStreamClient::Shutdown() {
LOG_INFO("Stopping to listen to manifest updates");
read_client_->Shutdown();
}
} // namespace cdc_ft

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CDC_FUSE_FS_CONFIG_STREAM_CLIENT_H_
#define CDC_FUSE_FS_CONFIG_STREAM_CLIENT_H_
#include <memory>
#include "absl/status/status.h"
#include "grpcpp/grpcpp.h"
#include "manifest/manifest_proto_defs.h"
#include "proto/asset_stream_service.grpc.pb.h"
namespace grpc_impl {
class Channel;
}
namespace cdc_ft {
class ManifestIdReader;
class ConfigStreamClient {
public:
// |instance| is the id of the gamelet.
// |channel| is a gRPC channel to use.
ConfigStreamClient(std::string instance,
std::shared_ptr<grpc::Channel> channel);
~ConfigStreamClient();
// Sends a request to get a stream of manifest id updates. |callback| is
// called from a background thread for every manifest id received.
// Returns immediately without waiting for the first manifest id.
absl::Status StartListeningToManifestUpdates(
std::function<absl::Status(const ContentIdProto&)> callback);
// Sends a message to indicate that the |manifest_id| was received and FUSE
// has been updated to use the new manifest.
absl::Status SendManifestAck(ContentIdProto manifest_id);
// Stops listening for manifest updates.
void Shutdown();
private:
using ConfigStreamService = proto::ConfigStreamService;
const std::string instance_;
const std::unique_ptr<ConfigStreamService::Stub> stub_;
std::unique_ptr<ManifestIdReader> read_client_;
};
} // namespace cdc_ft
#endif // CDC_FUSE_FS_CONFIG_STREAM_CLIENT_H_

33
cdc_fuse_fs/constants.h Normal file
View File

@@ -0,0 +1,33 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CDC_FUSE_FS_CONSTANTS_H_
#define CDC_FUSE_FS_CONSTANTS_H_
namespace cdc_ft {
// FUSE prints this to stdout when the binary timestamp and file size match the
// file on the workstation.
static constexpr char kFuseUpToDate[] = "cdc_fuse_fs is up-to-date";
// FUSE prints this to stdout when the binary timestamp or file size does not
// match the file on the workstation. It indicates that the binary has to be
// redeployed.
static constexpr char kFuseNotUpToDate[] = "cdc_fuse_fs is not up-to-date";
} // namespace cdc_ft
#endif // CDC_FUSE_FS_CONSTANTS_H_

202
cdc_fuse_fs/main.cc Normal file
View File

@@ -0,0 +1,202 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <string>
#include <vector>
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl_helper/jedec_size_flag.h"
#include "cdc_fuse_fs/cdc_fuse_fs.h"
#include "cdc_fuse_fs/constants.h"
#include "common/gamelet_component.h"
#include "common/log.h"
#include "common/path.h"
#include "data_store/data_provider.h"
#include "data_store/disk_data_store.h"
#include "data_store/grpc_reader.h"
#include "grpcpp/channel.h"
#include "grpcpp/create_channel.h"
#include "grpcpp/support/channel_arguments.h"
namespace cdc_ft {
namespace {
constexpr char kFuseFilename[] = "cdc_fuse_fs";
constexpr char kLibFuseFilename[] = "libfuse.so";
absl::StatusOr<bool> IsUpToDate(const std::string& components_arg) {
// Components are expected to reside in the same dir as the executable.
std::string component_dir;
RETURN_IF_ERROR(path::GetExeDir(&component_dir));
std::vector<GameletComponent> components =
GameletComponent::FromCommandLineArgs(components_arg);
std::vector<GameletComponent> our_components;
absl::Status status =
GameletComponent::Get({path::Join(component_dir, kFuseFilename),
path::Join(component_dir, kLibFuseFilename)},
&our_components);
if (!status.ok() || components != our_components) {
return false;
}
return true;
}
} // namespace
} // namespace cdc_ft
ABSL_FLAG(std::string, instance, "", "Gamelet instance id");
ABSL_FLAG(
std::string, components, "",
"Whitespace-separated triples filename, size and timestamp of the "
"workstation version of this binary and dependencies. Used for a fast "
"up-to-date check.");
ABSL_FLAG(uint16_t, port, 0, "Port to connect to on localhost");
ABSL_FLAG(cdc_ft::JedecSize, prefetch_size, cdc_ft::JedecSize(512 << 10),
"Additional data to request from the server when a FUSE read of "
"maximum size is detected. This amount is added to the original "
"request. Supports common unit suffixes K, M, G");
ABSL_FLAG(std::string, cache_dir, "/var/cache/asset_streaming",
"Cache directory to store data chunks.");
ABSL_FLAG(int, cache_dir_levels, 2,
"Fanout of sub-directories to create within the cache directory.");
ABSL_FLAG(int, verbosity, 0, "Log verbosity");
ABSL_FLAG(bool, stats, false, "Enable statistics");
ABSL_FLAG(bool, check, false, "Execute consistency check");
ABSL_FLAG(cdc_ft::JedecSize, cache_capacity,
cdc_ft::JedecSize(cdc_ft::DiskDataStore::kDefaultCapacity),
"Cache capacity. Supports common unit suffixes K, M, G.");
ABSL_FLAG(uint32_t, cleanup_timeout, cdc_ft::DataProvider::kCleanupTimeoutSec,
"Period in seconds at which instance cache cleanups are run");
ABSL_FLAG(uint32_t, access_idle_timeout, cdc_ft::DataProvider::kAccessIdleSec,
"Do not run instance cache cleanups for this many seconds after the "
"last file access");
static_assert(static_cast<int>(absl::StatusCode::kOk) == 0, "kOk != 0");
// Usage: cdc_fuse_fs <ABSL_FLAGs> -- mount_dir [-d|-s|..]
// Any args after -- are FUSE args, search third_party/fuse for FUSE_OPT_KEY or
// FUSE_LIB_OPT (there doesn't seem to be a place where they're all described).
int main(int argc, char* argv[]) {
// Parse absl flags.
std::vector<char*> mount_args = absl::ParseCommandLine(argc, argv);
std::string instance = absl::GetFlag(FLAGS_instance);
std::string components = absl::GetFlag(FLAGS_components);
uint16_t port = absl::GetFlag(FLAGS_port);
std::string cache_dir = absl::GetFlag(FLAGS_cache_dir);
int cache_dir_levels = absl::GetFlag(FLAGS_cache_dir_levels);
int verbosity = absl::GetFlag(FLAGS_verbosity);
bool stats = absl::GetFlag(FLAGS_stats);
bool consistency_check = absl::GetFlag(FLAGS_check);
uint64_t cache_capacity = absl::GetFlag(FLAGS_cache_capacity).Size();
unsigned int dp_cleanup_timeout = absl::GetFlag(FLAGS_cleanup_timeout);
unsigned int dp_access_idle_timeout =
absl::GetFlag(FLAGS_access_idle_timeout);
// Log to console. Logs are streamed back to the workstation through the SSH
// session.
cdc_ft::Log::Initialize(std::make_unique<cdc_ft::ConsoleLog>(
cdc_ft::Log::VerbosityToLogLevel(verbosity)));
// Perform up-to-date check.
absl::StatusOr<bool> is_up_to_date = cdc_ft::IsUpToDate(components);
if (!is_up_to_date.ok()) {
LOG_ERROR("Failed to check file system freshness: %s",
is_up_to_date.status().ToString());
return static_cast<int>(is_up_to_date.status().code());
}
if (!*is_up_to_date) {
printf("%s\n", cdc_ft::kFuseNotUpToDate);
return 0;
}
printf("%s\n", cdc_ft::kFuseUpToDate);
fflush(stdout);
// Create fs. The rest of the flags are mount flags, so pass them along.
absl::Status status = cdc_ft::cdc_fuse_fs::Initialize(
static_cast<int>(mount_args.size()), mount_args.data());
if (!status.ok()) {
LOG_ERROR("Failed to initialize file system: %s", status.ToString());
return static_cast<int>(status.code());
}
// Create disk data store.
absl::StatusOr<std::unique_ptr<cdc_ft::DiskDataStore>> store =
cdc_ft::DiskDataStore::Create(cache_dir_levels, cache_dir, false);
if (!store.ok()) {
LOG_ERROR("Failed to initialize the chunk cache in directory '%s': %s",
absl::GetFlag(FLAGS_cache_dir), store.status().ToString());
return 1;
}
LOG_INFO("Setting cache capacity to '%u'", cache_capacity);
store.value()->SetCapacity(cache_capacity);
LOG_INFO("Caching chunks in '%s'", store.value()->RootDir());
// Start a gRpc client.
std::string client_address = absl::StrFormat("localhost:%u", port);
grpc::ChannelArguments channel_args;
channel_args.SetMaxReceiveMessageSize(-1);
std::shared_ptr<grpc::Channel> grpc_channel = grpc::CreateCustomChannel(
client_address, grpc::InsecureChannelCredentials(), channel_args);
std::vector<std::unique_ptr<cdc_ft::DataStoreReader>> readers;
readers.emplace_back(
std::make_unique<cdc_ft::GrpcReader>(grpc_channel, stats));
cdc_ft::GrpcReader* grpc_reader =
static_cast<cdc_ft::GrpcReader*>(readers[0].get());
// Send all cached content ids to the client if statistics are enabled.
if (stats) {
LOG_INFO("Sending all cached content ids");
absl::StatusOr<std::vector<cdc_ft::ContentIdProto>> ids =
store.value()->List();
if (!ids.ok()) {
LOG_ERROR("Failed to get all cached content ids: %s",
ids.status().ToString());
return 1;
}
status = grpc_reader->SendCachedContentIds(*ids);
if (!status.ok()) {
LOG_ERROR("Failed to send all cached content ids: %s", status.ToString());
return 1;
}
}
// Create data provider.
size_t prefetch_size = absl::GetFlag(FLAGS_prefetch_size).Size();
cdc_ft::DataProvider data_provider(std::move(*store), std::move(readers),
prefetch_size, dp_cleanup_timeout,
dp_access_idle_timeout);
if (!cdc_ft::cdc_fuse_fs::StartConfigClient(instance, grpc_channel).ok()) {
LOG_ERROR("Could not start reading configuration updates'");
return 1;
}
// Run FUSE.
LOG_INFO("Running filesystem");
status = cdc_ft::cdc_fuse_fs::Run(&data_provider, consistency_check);
if (!status.ok()) {
LOG_ERROR("Filesystem stopped with error: %s", status.ToString());
}
LOG_INFO("Filesystem ran successfully and shuts down");
data_provider.Shutdown();
cdc_ft::cdc_fuse_fs::Shutdown();
cdc_ft::Log::Shutdown();
static_assert(static_cast<int>(absl::StatusCode::kOk) == 0, "kOk != 0");
return static_cast<int>(status.code());
}

111
cdc_fuse_fs/mock_libfuse.cc Normal file
View File

@@ -0,0 +1,111 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cdc_fuse_fs/mock_libfuse.h"
#include <cassert>
#include <cstring>
namespace cdc_ft {
namespace {
MockLibFuse* g_fuse;
}
MockLibFuse::MockLibFuse() {
assert(!g_fuse);
g_fuse = this;
}
MockLibFuse::~MockLibFuse() {
assert(g_fuse == this);
g_fuse = nullptr;
}
void MockLibFuse::SetUid(int uid) {
assert(g_fuse == this);
g_fuse->context.uid = uid;
}
void MockLibFuse::SetGid(int gid) {
assert(g_fuse == this);
g_fuse->context.gid = gid;
}
size_t fuse_add_direntry(fuse_req_t req, char* buf, size_t bufsize,
const char* name, const struct stat* stbuf,
off_t off) {
assert(g_fuse);
if (bufsize >= sizeof(MockLibFuse::DirEntry)) {
assert(stbuf);
auto* entry = reinterpret_cast<MockLibFuse::DirEntry*>(buf);
strncpy(entry->name, name, sizeof(entry->name));
entry->name[sizeof(entry->name) - 1] = 0;
entry->ino = stbuf->st_ino;
entry->mode = stbuf->st_mode;
entry->off = off;
}
return sizeof(MockLibFuse::DirEntry);
}
int fuse_reply_attr(fuse_req_t req, const struct stat* attr,
double attr_timeout) {
assert(g_fuse);
assert(attr);
g_fuse->attrs.emplace_back(*attr, attr_timeout);
return 0;
}
int fuse_reply_buf(fuse_req_t req, const char* buf, size_t size) {
assert(g_fuse);
std::vector<char> data;
if (buf && size > 0) {
data.insert(data.end(), buf, buf + size);
}
g_fuse->buffers.push_back(std::move(data));
return 0;
}
int fuse_reply_entry(fuse_req_t req, const struct fuse_entry_param* e) {
assert(g_fuse);
assert(e);
g_fuse->entries.push_back(*e);
return 0;
}
int fuse_reply_err(fuse_req_t req, int err) {
assert(g_fuse);
g_fuse->errors.push_back(err);
return 0;
}
int fuse_reply_open(fuse_req_t req, const struct fuse_file_info* fi) {
assert(g_fuse);
assert(fi);
g_fuse->open_files.push_back(*fi);
return 0;
}
void fuse_reply_none(fuse_req_t req) {
assert(g_fuse);
++g_fuse->none_counter;
}
int fuse_reply_statfs(fuse_req_t req, const struct statvfs* stbuf) { return 0; }
struct fuse_context* fuse_get_context() {
assert(g_fuse);
return &g_fuse->context;
}
} // namespace cdc_ft

123
cdc_fuse_fs/mock_libfuse.h Normal file
View File

@@ -0,0 +1,123 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef CDC_FUSE_FS_MOCK_LIBFUSE_H_
#define CDC_FUSE_FS_MOCK_LIBFUSE_H_
#include <fcntl.h>
#include <sys/stat.h>
#include <cstdint>
#include <vector>
namespace cdc_ft {
//
// The interface below mimics the part of the FUSE low level interface we need.
// See include/fuse_lowlevel.h for more information.
//
// Definitions.
using fuse_ino_t = uint64_t;
using fuse_req_t = void*;
using nlink_t = uint64_t;
constexpr fuse_ino_t FUSE_ROOT_ID = 1;
#ifndef O_DIRECT
constexpr uint32_t O_DIRECT = 040000;
#endif
struct fuse_entry_param {
fuse_ino_t ino;
struct stat attr;
double attr_timeout;
double entry_timeout;
};
struct fuse_file_info {
int flags = O_RDONLY;
unsigned int direct_io : 1;
unsigned int keep_cache : 1;
fuse_file_info() : direct_io(0), keep_cache(0) {}
explicit fuse_file_info(int flags)
: flags(flags), direct_io(0), keep_cache(0) {}
};
struct fuse_forget_data {
uint64_t ino;
uint64_t nlookup;
};
struct fuse_context {
int uid;
int gid;
};
struct statvfs {
uint32_t f_bsize;
uint32_t f_namemax;
};
// FUSE reply/action functions.
size_t fuse_add_direntry(fuse_req_t req, char* buf, size_t bufsize,
const char* name, const struct stat* stbuf, off_t off);
int fuse_reply_attr(fuse_req_t req, const struct stat* attr,
double attr_timeout);
int fuse_reply_buf(fuse_req_t req, const char* buf, size_t size);
int fuse_reply_entry(fuse_req_t req, const struct fuse_entry_param* e);
int fuse_reply_err(fuse_req_t req, int err);
int fuse_reply_open(fuse_req_t req, const struct fuse_file_info* fi);
void fuse_reply_none(fuse_req_t req);
int fuse_reply_statfs(fuse_req_t req, const struct statvfs* stbuf);
struct fuse_context* fuse_get_context();
// FUSE mocking class. Basically just a recorder for the fuse_* callbacks above.
struct MockLibFuse {
public:
MockLibFuse();
~MockLibFuse();
struct Attr {
struct stat value;
double timeout;
Attr(struct stat value, double timeout)
: value(std::move(value)), timeout(timeout) {}
};
void SetUid(int uid);
void SetGid(int gid);
// Struct stored in the buffer |buf| by fuse_add_direntry().
// Uses a maximum name size for simplicity.
struct DirEntry {
fuse_ino_t ino;
uint32_t mode;
char name[32];
off_t off;
};
std::vector<fuse_entry_param> entries;
std::vector<Attr> attrs;
std::vector<int> errors;
std::vector<fuse_file_info> open_files;
std::vector<std::vector<char>> buffers;
unsigned int none_counter = 0;
fuse_context context;
};
} // namespace cdc_ft
#endif // CDC_FUSE_FS_MOCK_LIBFUSE_H_